Files
SnowLuma/.pnpm-store/v11/files/1b/49de810789151364338f6ef1c5ce98a6e698575d345c9f2cb7e122316f67dd6c21befa5c72e28b7218f41862af173f6db1bba7175200fec63f4d54b34bd605
T
TanTan 2a69b23b47
Dev Build / cleanup (push) Canceled after 0s
Dev Build / typecheck (push) Canceled after 0s
Dev Build / build (linux-arm64, ubuntu-24.04-arm, linux-arm64) (push) Canceled after 0s
Dev Build / build (linux-x64, ubuntu-latest, linux-x64) (push) Canceled after 0s
Dev Build / build (win-x64, windows-latest, win32-x64) (push) Canceled after 0s
Dev Build / dispatch-dev-docker (push) Canceled after 0s
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

794 lines
26 KiB
Plaintext

// Incremental BLOB I/O (Deliverable 08). See src/blob.h for the
// threading model and lifetime answers.
#include <cstring>
#include <string>
#include <sqlite3.h>
#include <napi.h>
#include "macros.h"
#include "database.h"
#include "blob.h"
using namespace node_sqlite3;
namespace {
#define BLOB_BEGIN(type) \
assert(baton); \
assert(baton->blob); \
assert(!baton->blob->locked); \
assert(!baton->blob->closed); \
assert(baton->blob->inited); \
assert(baton->blob->_handle != NULL); \
baton->blob->locked = true; \
baton->blob->db->pending++; \
auto env = baton->blob->Env(); \
CREATE_WORK("sqlite3.Blob." #type, Work_##type, Work_After##type);
// A view of a JS binary value with its base pointer and available byte
// count, honouring byteOffset.
struct TargetView {
void* data = NULL;
size_t length = 0;
};
bool GetMutableTarget(napi_env env, const Napi::Value& value,
const std::string& what, TargetView* out) {
if (value.IsDataView()) {
size_t bytes = 0;
void* data = NULL;
napi_get_dataview_info(env, value, &bytes, &data, NULL, NULL);
out->data = data;
out->length = bytes;
return true;
}
if (value.IsBuffer()) {
Napi::Buffer<char> buf = value.As<Napi::Buffer<char>>();
out->data = buf.Data();
out->length = buf.Length();
return true;
}
if (value.IsTypedArray()) {
napi_typedarray_type type;
size_t elements = 0;
void* data = NULL;
napi_get_typedarray_info(env, value, &type, &elements, &data,
NULL, NULL);
switch (type) {
case napi_int8_array:
case napi_uint8_array:
case napi_uint8_clamped_array:
out->data = data;
out->length = elements;
return true;
default:
Napi::TypeError::New(env, "Cannot " + what +
": only byte-sized typed arrays are accepted"
).ThrowAsJavaScriptException();
return false;
}
}
Napi::TypeError::New(env, "Cannot " + what +
": a Buffer or Uint8Array is required"
).ThrowAsJavaScriptException();
return false;
}
// The message accompanying SQLITE_ABORT from blob read/write: the row
// was written after the handle was opened, invalidating it.
std::string BlobAbortMessage() {
return "the row this blob handle was opened on has been modified, "
"so the handle is no longer valid; reopen it (openBlob/reopen) "
"to continue";
}
// Carries a raw sqlite3_blob* whose wrapper is gone (~Blob).
struct BlobHandleBaton : Database::Baton {
sqlite3_blob* handle;
BlobHandleBaton(Database* db_, sqlite3_blob* handle_) :
Baton(db_, Napi::Function()), handle(handle_) {}
virtual ~BlobHandleBaton() override = default;
};
} // namespace
Napi::Object Blob::Init(Napi::Env env, Napi::Object exports) {
Napi::HandleScope scope(env);
auto napi_default_method = static_cast<napi_property_attributes>(
napi_writable | napi_configurable);
auto t = DefineClass(env, "Blob", {
InstanceMethod("read", &Blob::Read, napi_default_method),
InstanceMethod("write", &Blob::Write, napi_default_method),
InstanceMethod("reopen", &Blob::Reopen, napi_default_method),
InstanceMethod("close", &Blob::Close, napi_default_method),
InstanceAccessor("size", &Blob::SizeGetter, nullptr),
InstanceAccessor("closed", &Blob::ClosedGetter, nullptr),
});
// Per-env (see Database::AddonData).
env.GetInstanceData<Database::AddonData>()->blob_ctor =
Napi::Persistent(t);
exports.Set("Blob", t);
return exports;
}
void Blob::Process() {
if ((closed || !inited) && !queue.empty()) {
return CleanQueue();
}
while (inited && !locked && !closed && !queue.empty()) {
auto call = std::unique_ptr<Call>(queue.front());
queue.pop();
call->callback(call->baton);
}
}
void Blob::Schedule(Work_Callback callback, Baton* baton) {
if (closed) {
queue.emplace(new Call(callback, baton));
CleanQueue();
}
else if (!inited || locked || !queue.empty()) {
queue.emplace(new Call(callback, baton));
}
else {
callback(baton);
}
}
template <class T> void Blob::Error(T* baton) {
auto env = baton->blob->Env();
Napi::HandleScope scope(env);
Blob* blob = baton->blob;
assert(blob->status != 0);
EXCEPTION(blob->message, blob->status, exception);
Napi::Function cb = baton->callback.Value();
if (IS_FUNCTION(cb)) {
Napi::Value argv[] = { exception };
TRY_CATCH_CALL(blob->Value(), cb, 1, argv);
}
else {
Napi::Value info[] = { Napi::String::New(env, "error"), exception };
EMIT_EVENT(blob->Value(), 2, info);
}
}
void Blob::CleanQueue() {
auto env = this->Env();
Napi::HandleScope scope(env);
if (queue.empty()) return;
// Environment teardown (worker termination): failing the queued
// calls constructs JS on a dying environment, which is fatal. Drop
// them instead — reference cleanup still works there, so the
// batons are destroyed normally.
if (Database::EnvCannotRunJs(env)) {
while (!queue.empty()) {
auto call = std::unique_ptr<Call>(queue.front());
queue.pop();
delete call->baton;
}
return;
}
EXCEPTION("Blob is already closed", SQLITE_MISUSE, exception);
Napi::Value argv[] = { exception };
bool called = false;
while (!queue.empty()) {
auto call = std::unique_ptr<Call>(queue.front());
queue.pop();
std::unique_ptr<Baton> baton(call->baton);
Napi::Function cb = baton->callback.Value();
if (IS_FUNCTION(cb)) {
TRY_CATCH_CALL(Value(), cb, 1, argv);
called = true;
}
}
if (!called) {
Napi::Value info[] = { Napi::String::New(env, "error"), exception };
EMIT_EVENT(Value(), 2, info);
}
}
// { Database db, String dbName, String table, String column,
// Number rowid, Boolean readOnly, [Function callback] }
Blob::Blob(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Blob>(info) {
auto env = info.Env();
if (!info.IsConstructCall()) {
Napi::TypeError::New(env, "Use the new operator to create new Blob objects").ThrowAsJavaScriptException();
return;
}
if (info.Length() <= 0 || !Database::HasInstance(info[0])) {
Napi::TypeError::New(env, "Database object expected").ThrowAsJavaScriptException();
return;
}
else if (info.Length() <= 1 || !info[1].IsString()
|| info.Length() <= 2 || !info[2].IsString()
|| info.Length() <= 3 || !info[3].IsString()) {
Napi::TypeError::New(env, "Database, table and column names expected").ThrowAsJavaScriptException();
return;
}
else if (info.Length() <= 4 || !info[4].IsNumber()) {
Napi::TypeError::New(env, "Rowid expected").ThrowAsJavaScriptException();
return;
}
else if (info.Length() <= 5 || !info[5].IsBoolean()) {
Napi::TypeError::New(env, "Read-only flag expected").ThrowAsJavaScriptException();
return;
}
else if (info.Length() > 6 && !info[6].IsUndefined() && !info[6].IsFunction()) {
Napi::TypeError::New(env, "Callback expected").ThrowAsJavaScriptException();
return;
}
this->db = Napi::ObjectWrap<Database>::Unwrap(info[0].As<Napi::Object>());
this->db->Ref();
auto dbName = info[1].As<Napi::String>().Utf8Value();
auto table = info[2].As<Napi::String>().Utf8Value();
auto column = info[3].As<Napi::String>().Utf8Value();
sqlite3_int64 rowid = info[4].As<Napi::Number>().Int64Value();
bool readOnly = info[5].As<Napi::Boolean>().Value();
info.This().As<Napi::Object>().DefineProperty(
Napi::PropertyDescriptor::Value("db", info[1]));
info.This().As<Napi::Object>().DefineProperty(
Napi::PropertyDescriptor::Value("table", info[2]));
info.This().As<Napi::Object>().DefineProperty(
Napi::PropertyDescriptor::Value("column", info[3]));
info.This().As<Napi::Object>().DefineProperty(
Napi::PropertyDescriptor::Value("rowid", info[4]));
info.This().As<Napi::Object>().DefineProperty(
Napi::PropertyDescriptor::Value("readOnly", info[5]));
// Tracked from construction so CloseLiveBlobs cannot miss a blob
// whose open is still queued or in flight.
this->db->live_blobs.push_back(this);
auto* baton = new OpenBaton(this->db, info[6].As<Napi::Function>(), this);
baton->dbName = dbName;
baton->table = table;
baton->column = column;
baton->rowid = rowid;
baton->readOnly = readOnly;
this->db->Schedule(Work_BeginOpen, baton);
}
void Blob::Work_BeginOpen(Database::Baton* baton) {
assert(baton->db->IsOpen());
assert(baton->db->_handle);
baton->db->pending++;
auto env = baton->db->Env();
CREATE_WORK("sqlite3.Blob.Open", Work_Open, Work_AfterOpen);
}
void Blob::Work_Open(napi_env e, void* data) {
auto* baton = static_cast<OpenBaton*>(data);
Blob* blob = baton->blob;
// sqlite3_blob_open takes the connection mutex internally (and a
// read/write lock on the row); running here on the worker pool is
// what serializes it against other work.
blob->_handle = NULL;
int rc = sqlite3_blob_open(baton->db->_handle,
baton->dbName.c_str(), baton->table.c_str(),
baton->column.c_str(), baton->rowid,
baton->readOnly ? 0 : 1, &blob->_handle);
blob->status = rc;
if (rc != SQLITE_OK) {
blob->message = std::string(sqlite3_errmsg(baton->db->_handle));
}
}
void Blob::Work_AfterOpen(napi_env e, napi_status status, void* data) {
std::unique_ptr<OpenBaton> baton(static_cast<OpenBaton*>(data));
AFTER_WORK_TEARDOWN_GUARD(baton);
auto* blob = baton->blob;
auto* db = baton->db;
auto env = blob->Env();
Napi::HandleScope scope(env);
db->pending--;
db->Process();
if (blob->status != SQLITE_OK) {
blob->DetachFromDatabase();
Error(baton.get());
}
else {
blob->inited = true;
Napi::Function cb = baton->callback.Value();
if (IS_FUNCTION(cb)) {
Napi::Value argv[] = { env.Null() };
TRY_CATCH_CALL(blob->Value(), cb, 1, argv);
}
}
blob->Process();
}
void Blob::EndCall() {
assert(locked);
assert(db->pending);
locked = false;
db->pending--;
Process();
db->Process();
}
Blob::~Blob() {
// GC safety net, mirroring ~Statement: every queued Baton holds a Ref
// on the blob, so the queue is provably empty and CleanQueue cannot
// fire JS from this finalizer. sqlite3_blob_close takes the
// connection mutex internally; defer through the exclusive queue when
// a worker could be mid-round-trip holding that mutex while waiting
// for this very thread.
CleanQueue();
if (_handle != NULL) {
if (db != NULL && db->MayBlockOnWorkerRoundTrip()) {
db->Schedule(Work_DeferredBlobClose,
new BlobHandleBaton(db, _handle), true);
_handle = NULL;
}
else {
sqlite3_blob_close(_handle);
_handle = NULL;
}
}
// Detached blobs have already released the database (and nulled the
// pointer); this covers the never-detached case only.
if (db != NULL) {
UntrackFromDatabase();
db->Unref();
}
}
void Blob::Work_DeferredBlobClose(Database::Baton* b) {
auto baton = std::unique_ptr<BlobHandleBaton>(
static_cast<BlobHandleBaton*>(b));
auto* db = baton->db;
sqlite3_blob_close(baton->handle);
db->exclusiveHeld = false;
db->Process();
}
void Blob::UntrackFromDatabase() {
if (db == NULL) return;
auto& v = db->live_blobs;
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it == this) {
v.erase(it);
return;
}
}
}
// JS thread, nothing in flight (Work_BeginClose / ~Database) or at a
// completion point where the handle is already gone. Idempotent.
// Severs the link to the Database, and is the single place the blob's
// reference on it is released. Nulling `db` is the point: at environment
// teardown napi finalizes wrappers in an unspecified order, so ~Blob can
// run after ~Database, and a back-pointer left dangling there means
// UntrackFromDatabase walks a destroyed std::vector. (That is a
// segfault, not a stale read — it was reproducible on musl.)
//
// owner_dying is set when ~Database is the caller: the Database is
// already being destroyed, so its refcount is moot and calling Unref on
// a half-destroyed wrapper is exactly the dereference being avoided.
void Blob::DetachFromDatabase(bool owner_dying) {
if (db == NULL) return;
if (_handle != NULL) {
sqlite3_blob_close(_handle);
_handle = NULL;
}
UntrackFromDatabase();
closed = true;
if (owner_dying) {
// ~Database is running. napi finalizes wrappers in an
// unspecified order at environment teardown, so ~Blob can run
// afterwards — and UntrackFromDatabase would then walk a
// destroyed std::vector, which faults rather than reading stale
// data (reproducible on musl). Drop the back-pointer so the
// destructor skips it. The reference is deliberately not
// released: the Database is already being destroyed, and
// Unref'ing a half-destroyed wrapper is the same dereference.
//
// Only on this path: the ordinary close still needs `db` for
// the in-flight bookkeeping that runs after this returns
// (Work_AfterClose's CallGuard decrements db->pending).
db = NULL;
}
}
void Database::CloseLiveBlobs(bool owner_dying) {
// Main thread, nothing in flight (Work_BeginClose / ~Database).
std::vector<Blob*> blobs = live_blobs;
live_blobs.clear();
for (Blob* blob : blobs) {
blob->DetachFromDatabase(owner_dying);
}
}
// --- read ---------------------------------------------------------------------
// read(target[, offset[, callback]]): copies target.length bytes of the
// blob, starting at `offset` within the blob, into the target. The
// callback may sit in the offset's slot (the promise layer appends it
// there when no offset was given), so each slot accepts either its own
// type or the trailing function.
Napi::Value Blob::Read(const Napi::CallbackInfo& info) {
auto env = info.Env();
Blob* blob = this;
if (info.Length() < 1) {
Napi::TypeError::New(env, "Expected at least 1 argument").ThrowAsJavaScriptException();
return env.Null();
}
TargetView target;
if (!GetMutableTarget(env, info[0], "read into a blob target", &target)) {
return env.Null();
}
sqlite3_int64 offset = 0;
Napi::Function callback;
if (info.Length() > 1 && info[1].IsNumber()) {
offset = info[1].As<Napi::Number>().Int64Value();
if (offset < 0) {
Napi::RangeError::New(env, "blob offset must be a non-negative integer").ThrowAsJavaScriptException();
return env.Null();
}
if (info.Length() > 2 && !info[2].IsUndefined()) {
if (!info[2].IsFunction()) {
Napi::TypeError::New(env, "Argument 2 must be a function").ThrowAsJavaScriptException();
return env.Null();
}
callback = info[2].As<Napi::Function>();
}
}
else if (info.Length() > 1 && info[1].IsFunction()) {
callback = info[1].As<Napi::Function>();
}
else if (info.Length() > 1 && !info[1].IsUndefined()) {
Napi::TypeError::New(env, "blob offset must be a non-negative integer").ThrowAsJavaScriptException();
return env.Null();
}
auto* baton = new IoBaton(blob, callback);
baton->target = Napi::Persistent(info[0]);
baton->data = target.data;
baton->length = target.length;
baton->offset = offset;
blob->Schedule(Work_BeginRead, baton);
return info.This();
}
void Blob::Work_BeginRead(Baton* baton) {
BLOB_BEGIN(Read);
}
void Blob::Work_Read(napi_env e, void* data) {
auto* baton = static_cast<IoBaton*>(data);
Blob* blob = baton->blob;
int rc = sqlite3_blob_read(blob->_handle, baton->data,
static_cast<int>(baton->length), static_cast<int>(baton->offset));
blob->status = rc;
if (rc == SQLITE_ABORT) {
blob->message = BlobAbortMessage();
}
else if (rc != SQLITE_OK) {
blob->message = std::string(sqlite3_errmsg(blob->db->_handle));
}
}
void Blob::Work_AfterRead(napi_env e, napi_status status, void* data) {
std::unique_ptr<IoBaton> baton(static_cast<IoBaton*>(data));
AFTER_WORK_TEARDOWN_GUARD(baton);
auto* blob = baton->blob;
auto env = blob->Env();
Napi::HandleScope scope(env);
// Runs the end-of-call bookkeeping on every exit path, including
// TRY_CATCH_CALL's early return when a JS callback throws.
Blob::CallGuard blob_call_guard__(blob);
if (blob->status != SQLITE_OK) {
Error(baton.get());
return;
}
Napi::Function cb = baton->callback.Value();
if (IS_FUNCTION(cb)) {
// The number of bytes actually copied is the full target length:
// sqlite3_blob_read fails (rather than short-reading) when
// offset+length exceeds the blob size.
Napi::Value argv[] = { env.Null(),
Napi::Number::New(env, baton->length) };
TRY_CATCH_CALL(blob->Value(), cb, 2, argv);
}
}
// --- write --------------------------------------------------------------------
// write(source[, offset[, callback]]): copies source.length bytes into
// the blob starting at `offset` within the blob. The callback may sit in
// the offset's slot; see Read.
Napi::Value Blob::Write(const Napi::CallbackInfo& info) {
auto env = info.Env();
Blob* blob = this;
if (info.Length() < 1) {
Napi::TypeError::New(env, "Expected at least 1 argument").ThrowAsJavaScriptException();
return env.Null();
}
TargetView target;
if (!GetMutableTarget(env, info[0], "write from a blob source", &target)) {
return env.Null();
}
sqlite3_int64 offset = 0;
Napi::Function callback;
if (info.Length() > 1 && info[1].IsNumber()) {
offset = info[1].As<Napi::Number>().Int64Value();
if (offset < 0) {
Napi::RangeError::New(env, "blob offset must be a non-negative integer").ThrowAsJavaScriptException();
return env.Null();
}
if (info.Length() > 2 && !info[2].IsUndefined()) {
if (!info[2].IsFunction()) {
Napi::TypeError::New(env, "Argument 2 must be a function").ThrowAsJavaScriptException();
return env.Null();
}
callback = info[2].As<Napi::Function>();
}
}
else if (info.Length() > 1 && info[1].IsFunction()) {
callback = info[1].As<Napi::Function>();
}
else if (info.Length() > 1 && !info[1].IsUndefined()) {
Napi::TypeError::New(env, "blob offset must be a non-negative integer").ThrowAsJavaScriptException();
return env.Null();
}
auto* baton = new IoBaton(blob, callback);
baton->target = Napi::Persistent(info[0]);
baton->data = target.data;
baton->length = target.length;
baton->offset = offset;
blob->Schedule(Work_BeginWrite, baton);
return info.This();
}
void Blob::Work_BeginWrite(Baton* baton) {
BLOB_BEGIN(Write);
}
void Blob::Work_Write(napi_env e, void* data) {
auto* baton = static_cast<IoBaton*>(data);
Blob* blob = baton->blob;
int rc = sqlite3_blob_write(blob->_handle, baton->data,
static_cast<int>(baton->length), static_cast<int>(baton->offset));
blob->status = rc;
if (rc == SQLITE_ABORT) {
blob->message = BlobAbortMessage();
}
else if (rc != SQLITE_OK) {
blob->message = std::string(sqlite3_errmsg(blob->db->_handle));
}
}
void Blob::Work_AfterWrite(napi_env e, napi_status status, void* data) {
std::unique_ptr<IoBaton> baton(static_cast<IoBaton*>(data));
AFTER_WORK_TEARDOWN_GUARD(baton);
auto* blob = baton->blob;
auto env = blob->Env();
Napi::HandleScope scope(env);
Blob::CallGuard blob_call_guard__(blob);
if (blob->status != SQLITE_OK) {
Error(baton.get());
return;
}
Napi::Function cb = baton->callback.Value();
if (IS_FUNCTION(cb)) {
Napi::Value argv[] = { env.Null(),
Napi::Number::New(env, baton->length) };
TRY_CATCH_CALL(blob->Value(), cb, 2, argv);
}
}
// --- reopen --------------------------------------------------------------------
Napi::Value Blob::Reopen(const Napi::CallbackInfo& info) {
auto env = info.Env();
Blob* blob = this;
REQUIRE_ARGUMENT_INTEGER(0, rowid);
OPTIONAL_ARGUMENT_FUNCTION(1, callback);
auto* baton = new Baton(blob, callback);
baton->rowid = rowid;
blob->Schedule(Work_BeginReopen, baton);
return info.This();
}
void Blob::Work_BeginReopen(Baton* baton) {
BLOB_BEGIN(Reopen);
}
void Blob::Work_Reopen(napi_env e, void* data) {
auto* baton = static_cast<Baton*>(data);
Blob* blob = baton->blob;
int rc = sqlite3_blob_reopen(blob->_handle, baton->rowid);
blob->status = rc;
if (rc != SQLITE_OK) {
blob->message = std::string(sqlite3_errmsg(blob->db->_handle));
}
}
void Blob::Work_AfterReopen(napi_env e, napi_status status, void* data) {
std::unique_ptr<Baton> baton(static_cast<Baton*>(data));
AFTER_WORK_TEARDOWN_GUARD(baton);
auto* blob = baton->blob;
auto env = blob->Env();
Napi::HandleScope scope(env);
Blob::CallGuard blob_call_guard__(blob);
if (blob->status != SQLITE_OK) {
Error(baton.get());
return;
}
Napi::Function cb = baton->callback.Value();
if (IS_FUNCTION(cb)) {
Napi::Value argv[] = { env.Null() };
TRY_CATCH_CALL(blob->Value(), cb, 1, argv);
}
}
// --- close ----------------------------------------------------------------------
Napi::Value Blob::Close(const Napi::CallbackInfo& info) {
auto env = info.Env();
Blob* blob = this;
OPTIONAL_ARGUMENT_FUNCTION(0, callback);
if (blob->closed) {
// Dispose contract: a second close is a benign no-op, and one
// after the database closed finds the handle already gone.
if (IS_FUNCTION(callback)) {
Napi::Value argv[] = { env.Null() };
TRY_CATCH_CALL(blob->Value(), callback, 1, argv, info.This());
}
return info.This();
}
auto* baton = new Baton(blob, callback);
blob->Schedule(Work_BeginClose, baton);
return info.This();
}
void Blob::Work_BeginClose(Baton* baton) {
BLOB_BEGIN(Close);
}
void Blob::Work_Close(napi_env e, void* data) {
auto* baton = static_cast<Baton*>(data);
Blob* blob = baton->blob;
sqlite3_blob_close(blob->_handle);
blob->_handle = NULL;
}
void Blob::Work_AfterClose(napi_env e, napi_status status, void* data) {
std::unique_ptr<Baton> baton(static_cast<Baton*>(data));
AFTER_WORK_TEARDOWN_GUARD(baton);
auto* blob = baton->blob;
auto env = blob->Env();
Napi::HandleScope scope(env);
Blob::CallGuard blob_call_guard__(blob);
blob->DetachFromDatabase();
Napi::Function cb = baton->callback.Value();
if (IS_FUNCTION(cb)) {
Napi::Value argv[] = { env.Null() };
TRY_CATCH_CALL(blob->Value(), cb, 1, argv);
}
}
// --- accessors -------------------------------------------------------------------
Napi::Value Blob::SizeGetter(const Napi::CallbackInfo& info) {
auto env = info.Env();
Blob* blob = this;
// read()/write() queue behind the open, but a synchronous getter
// cannot; say which of the two states the caller is actually in,
// because "not open" reads as "closed" when it usually means "not
// open yet".
//
// Decision (D08 finding 5, settled in D09): keep throwing rather than
// join Statement's read-`undefined`-until-ready convention. The
// statement accessors expose derived metadata snapshots; `size` is a
// dimension of a live native handle, and `undefined` would flow into
// arithmetic (`size + offset` → NaN) and buffer sizing silently — the
// silent-coercion bug class this package exists to close. The error
// messages above are state-specific, so the loud path is precise.
// Order matters: _handle is NULL before the open completes as well as
// after a close, so the not-yet-open case has to be tested first.
if (!blob->inited && !blob->closed) {
Napi::Error::New(env,
"blob.size is not available until the handle has finished "
"opening; read it from the openBlob() callback, or after an "
"awaited read/write (those queue behind the open)"
).ThrowAsJavaScriptException();
return env.Null();
}
if (blob->closed || blob->_handle == NULL) {
Napi::Error::New(env, "Blob is already closed")
.ThrowAsJavaScriptException();
return env.Null();
}
if (blob->locked) {
// An op is in flight on this handle; reading the size now could
// observe a torn state.
Napi::Error::New(env,
"cannot read blob.size while an operation on this blob is "
"in flight; read it from a callback or after the operation"
).ThrowAsJavaScriptException();
return env.Null();
}
// The handle's own queue is idle (locked == false). Same gate as the
// other main-thread sqlite reads: refuse while a worker could be
// blocked mid-round-trip on this thread holding the connection mutex.
if (blob->db->MayBlockOnWorkerRoundTrip()) {
Napi::Error::New(env,
"cannot read blob.size while a JavaScript function, "
"collation or progress callback is mid-call on this "
"connection; read it from a callback or after the query"
).ThrowAsJavaScriptException();
return env.Null();
}
return Napi::Number::New(env,
static_cast<double>(sqlite3_blob_bytes(blob->_handle)));
}
Napi::Value Blob::ClosedGetter(const Napi::CallbackInfo& info) {
return Napi::Boolean::New(info.Env(), closed);
}