#ifndef NODE_SQLITE3_SRC_SESSION_H #define NODE_SQLITE3_SRC_SESSION_H // Sessions, changesets and serialize/deserialize (Deliverable 08). // // Threading model: // // - Session creation is scheduled exclusively and dispatched on the JS // thread at pending == 0 (the preupdate-slot ownership check below is // ordered against 'preupdate' listener registration that way); the // actual sqlite3session_create/attach pair runs on a libuv worker // under the connection mutex — never on the JS thread, so no // MayBlockOnWorkerRoundTrip deferral is needed for it. // - changeset()/patchset() run on a worker holding the connection mutex // (the session's hash tables are mutated by the preupdate hook inside // concurrent writes, which hold the same mutex). // - close() runs sqlite3session_delete on a worker; it takes the // connection mutex internally to unlink from the hook's session list. // // The preupdate-hook slot (one per connection) is shared between the // session module and the 'preupdate' event: sqlite3session_create // installs the session extension's own hook, displacing whatever was // there. Registering both would silently stop whichever lost the slot — // the D07 cancellation-token wedge shape — so ownership is exclusive and // enforced loudly in both directions, each on the JS thread at // pending == 0: creating a session while a 'preupdate' listener is // installed throws, and registering the listener while a session is // tracked fails the configure() with an 'error' event. Sessions are // tracked on the Database from construction (before the handle exists) // so the check cannot miss one still being created. #include #include #include #include #include #include #include "convert.h" #include "database.h" namespace node_sqlite3 { // One queued 'preupdate' event (Deliverable 08). The row values are // materialised eagerly inside the sqlite hook on the writing thread — // sqlite3_preupdate_old/new are only valid there — and converted to JS // on the loop thread from these Cells. struct PreupdateInfo { int op = 0; // SQLITE_INSERT / UPDATE / DELETE std::string database; std::string table; sqlite3_int64 key1 = 0; // rowid being inserted/deleted/updated sqlite3_int64 key2 = 0; // new rowid for a rowid-changing UPDATE Row old_row; // op != INSERT Row new_row; // op != DELETE }; class Session : public Napi::ObjectWrap { public: static Napi::Object Init(Napi::Env env, Napi::Object exports); // Cross-class argument validation, against this env's constructor — // see Database::HasInstanceIn. static inline bool HasInstance(Napi::Value val) { return Database::HasInstanceIn(val, &Database::AddonData::session_ctor); } struct Baton { napi_async_work request = NULL; Session* session; Napi::FunctionReference callback; Baton(Session* session_, Napi::Function cb_) : session(session_) { session->Ref(); callback.Reset(cb_, 1); } virtual ~Baton() { if (request) napi_delete_async_work(session->Env(), request); session->Unref(); callback.Reset(); } }; struct CreateBaton : Database::Baton { Session* session; std::string dbName; std::string table; // "" attaches every table bool indirect = false; CreateBaton(Database* db_, Napi::Function cb_, Session* session_) : Baton(db_, cb_), session(session_) { session->Ref(); } virtual ~CreateBaton() override { session->Unref(); if (db->IsClosed()) { // The database handle was closed before the session could // be created. session->DetachFromDatabase(); } } }; // changeset()/patchset(): the worker fills data (sqlite3_malloc'd); // Work_After* moves it into the Uint8Array handed to JS. struct BufferBaton : Baton { bool patch = false; int n = 0; void* data = NULL; BufferBaton(Session* session_, Napi::Function cb_) : Baton(session_, cb_) {} virtual ~BufferBaton() override { if (data != NULL) sqlite3_free(data); } }; typedef void (*Work_Callback)(Baton* baton); struct Call { Call(Work_Callback cb_, Baton* baton_) : callback(cb_), baton(baton_) {} Work_Callback callback; Baton* baton; }; Session(const Napi::CallbackInfo& info); // GC safety net for a collected session that was never closed; runs // in the ObjectWrap finalizer where no JS may be fired (the queue is // provably empty: every queued Baton holds a Ref). ~Session(); // End-of-call bookkeeping, run on every exit path via CallGuard. void EndCall(); struct CallGuard { Session* session; explicit CallGuard(Session* s) : session(s) {} ~CallGuard() { session->EndCall(); } CallGuard(const CallGuard&) = delete; CallGuard& operator=(const CallGuard&) = delete; }; // JS thread, nothing in flight (Work_BeginClose / ~Database) or at a // completion point where the handle is already gone: deletes the // native handle, untracks from the Database and marks the wrapper // inert. Idempotent. void DetachFromDatabase(bool owner_dying = false); protected: void Schedule(Work_Callback callback, Baton* baton); void Process(); void CleanQueue(); void UntrackFromDatabase(); template static void Error(T* baton); static void Work_BeginCreate(Database::Baton* baton); static void Work_Create(napi_env env, void* data); static void Work_AfterCreate(napi_env env, napi_status status, void* data); Napi::Value Changeset(const Napi::CallbackInfo& info); Napi::Value Patchset(const Napi::CallbackInfo& info); static void Work_BeginChangeset(Baton* baton); static void Work_Changeset(napi_env env, void* data); static void Work_AfterChangeset(napi_env env, napi_status status, void* data); // diff(table, fromDb): sqlite3session_diff — records the differences // between the session's table and the same table in another *attached* // database into the session, without either database being written. // The changeset is harvested with changeset() afterwards, as usual. struct DiffBaton : Baton { std::string table; std::string fromDb; DiffBaton(Session* session_, Napi::Function cb_) : Baton(session_, cb_) {} virtual ~DiffBaton() override = default; }; Napi::Value Diff(const Napi::CallbackInfo& info); static void Work_BeginDiff(Baton* baton); static void Work_Diff(napi_env env, void* data); static void Work_AfterDiff(napi_env env, napi_status status, void* data); Napi::Value Close(const Napi::CallbackInfo& info); static void Work_BeginClose(Baton* baton); static void Work_Close(napi_env env, void* data); static void Work_AfterClose(napi_env env, napi_status status, void* data); Napi::Value ClosedGetter(const Napi::CallbackInfo& info); Database* db = NULL; sqlite3_session* _handle = NULL; // The attached schema this session records ('main' unless created // with { db: ... }). Kept for diagnostics and for the diff() contract // (the session's schema is diff's "to" side; the argument names the // "from" side, and the two must differ). std::string db_name = "main"; bool inited = false; bool locked = false; bool closed = false; int status = SQLITE_OK; std::string message; std::queue queue; }; // Carries a raw sqlite3_session* whose wrapper is gone: the GC safety // net hands the delete to the exclusive queue when a worker could be // mid-round-trip holding the connection mutex (see ~Session). struct SessionHandleBaton : Database::Baton { sqlite3_session* handle; SessionHandleBaton(Database* db_, sqlite3_session* handle_) : Baton(db_, Napi::Function()), handle(handle_) {} virtual ~SessionHandleBaton() override = default; }; // Synchronous changeset iterator over a private copy of the bytes. // sqlite3changeset_* here involve no database connection, so this is // pure CPU on the JS thread — safe to drive with for..of. class ChangesetIter : public Napi::ObjectWrap { public: // Builds and caches the class constructor. The class itself is not // part of the public surface; iterateChangeset() hands out // instances, typed in lib/native.d.ts as an iterable. static Napi::Object Init(Napi::Env env, Napi::Object exports); ChangesetIter(const Napi::CallbackInfo& info); ~ChangesetIter() { if (_iter != NULL) sqlite3changeset_finalize(_iter); } protected: Napi::Value Next(const Napi::CallbackInfo& info); sqlite3_changeset_iter* _iter = NULL; std::string bytes; // owns the copy the iterator walks }; // Module-level changeset helpers, registered in src/node_sqlite3.cc. // Pure functions over memory — no connection, no mutex, no worker. Napi::Value InvertChangeset(const Napi::CallbackInfo& info); Napi::Value ConcatChangeset(const Napi::CallbackInfo& info); Napi::Value IterateChangeset(const Napi::CallbackInfo& info); Napi::Value RebaseChangeset(const Napi::CallbackInfo& info); } // namespace node_sqlite3 #endif