Files
SnowLuma/.pnpm-store/v11/files/04/c6b44dffa32b6e6430d35a1708635146aedad3007195a66301b8097c713dfc305dc96ad87c9e16af67967efac6bd14859b4cad559b417a3eaa06c589f16684
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) Skipped
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

247 lines
9.4 KiB
Plaintext

#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 <string>
#include <queue>
#include <vector>
#include <sqlite3.h>
#include <napi.h>
#include <uv.h>
#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<Session> {
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 <class T> 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<Call*> 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<ChangesetIter> {
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