@helipod/docstore-sqlite 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +159 -0
- package/dist/index.js +589 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { CommitGuardUnit, ShardId, DocStore, SchemaSetupOptions, DocumentLogEntry, IndexWrite, ConflictStrategy, CommitUnit, InternalDocumentId, LatestDocument, Interval, Order, TimestampRange, PrevRevQuery, ClientVerdictRecord, ClientVerdictWrite } from '@helipod/docstore';
|
|
2
|
+
import { JSONValue } from '@helipod/values';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The narrow SQL seam the DocStore sits on. A `DatabaseAdapter` is the *only* thing that
|
|
6
|
+
* knows about a concrete SQLite driver — swap it (node:sqlite, bun:sqlite, better-sqlite3,
|
|
7
|
+
* D1) without touching `SqliteDocStore`. Integer columns are read as `bigint` so 64-bit
|
|
8
|
+
* timestamps survive.
|
|
9
|
+
*/
|
|
10
|
+
type SqlValue = null | number | bigint | string | Uint8Array;
|
|
11
|
+
type SqlRow = Record<string, SqlValue>;
|
|
12
|
+
interface RunResult {
|
|
13
|
+
changes: number;
|
|
14
|
+
lastInsertRowid: number | bigint;
|
|
15
|
+
}
|
|
16
|
+
interface PreparedStatement {
|
|
17
|
+
run(...params: SqlValue[]): RunResult;
|
|
18
|
+
get(...params: SqlValue[]): SqlRow | undefined;
|
|
19
|
+
all(...params: SqlValue[]): SqlRow[];
|
|
20
|
+
}
|
|
21
|
+
interface DatabaseAdapter {
|
|
22
|
+
exec(sql: string): void;
|
|
23
|
+
prepare(sql: string): PreparedStatement;
|
|
24
|
+
/** Run `fn` inside a single SQL transaction (BEGIN/COMMIT, ROLLBACK on throw). */
|
|
25
|
+
transaction<T>(fn: () => T): T;
|
|
26
|
+
close(): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface NodeSqliteOptions {
|
|
30
|
+
/** File path, or ":memory:" (default). */
|
|
31
|
+
path?: string;
|
|
32
|
+
}
|
|
33
|
+
declare class NodeSqliteAdapter implements DatabaseAdapter {
|
|
34
|
+
private readonly db;
|
|
35
|
+
constructor(options?: NodeSqliteOptions);
|
|
36
|
+
exec(sql: string): void;
|
|
37
|
+
prepare(sql: string): PreparedStatement;
|
|
38
|
+
transaction<T>(fn: () => T): T;
|
|
39
|
+
close(): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface BunSqliteOptions {
|
|
43
|
+
/** File path, or ":memory:" (default). */
|
|
44
|
+
path?: string;
|
|
45
|
+
}
|
|
46
|
+
declare class BunSqliteAdapter implements DatabaseAdapter {
|
|
47
|
+
private readonly db;
|
|
48
|
+
constructor(options?: BunSqliteOptions);
|
|
49
|
+
exec(sql: string): void;
|
|
50
|
+
prepare(sql: string): PreparedStatement;
|
|
51
|
+
transaction<T>(fn: () => T): T;
|
|
52
|
+
close(): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* `SqliteDocStore` — the MVCC document log over three physical tables:
|
|
57
|
+
*
|
|
58
|
+
* documents(table_id, internal_id, ts, prev_ts, value) -- one row per revision; value NULL = tombstone
|
|
59
|
+
* indexes (index_id, key, ts, table_id, internal_id, deleted) -- MVCC index entries
|
|
60
|
+
* persistence_globals(key, value) -- engine metadata KV
|
|
61
|
+
*
|
|
62
|
+
* Snapshot reads pick the newest revision with `ts <= readTimestamp`. Index scans pick,
|
|
63
|
+
* per key in the byte interval, the newest index entry `<= readTimestamp`, skip deletions,
|
|
64
|
+
* and resolve the pointed document at the same timestamp.
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/** The narrow SYNCHRONOUS querier a SQLite commit guard writes receipts through — the sync mirror
|
|
68
|
+
* of `docstore-postgres`'s async `PgQuerier`. SQLite's commit runs inside one synchronous
|
|
69
|
+
* `db.transaction(() => {...})`, so a guard can only ever be handed synchronous primitives. */
|
|
70
|
+
interface SqliteGuardQuerier {
|
|
71
|
+
run(sql: string, ...params: unknown[]): void;
|
|
72
|
+
get(sql: string, ...params: unknown[]): Record<string, unknown> | undefined;
|
|
73
|
+
}
|
|
74
|
+
/** A SQLite commit guard — see `SqliteDocStore.addCommitGuard`'s doc comment for the full
|
|
75
|
+
* contract. Unlike `PgCommitGuard`, this MUST be synchronous: it runs inside the one-transaction
|
|
76
|
+
* synchronous commit, which cannot await anything. Returning a thenable is a documented dev-time
|
|
77
|
+
* error — see `commitWriteBatch`'s thenable check. */
|
|
78
|
+
type SqliteCommitGuard = (q: SqliteGuardQuerier, units: readonly CommitGuardUnit[], shardId: ShardId) => void;
|
|
79
|
+
declare class SqliteDocStore implements DocStore {
|
|
80
|
+
private readonly db;
|
|
81
|
+
private readonly stmtCache;
|
|
82
|
+
/** The commit-guard CHAIN (Receipted Outbox decision 2), the SQLite counterpart of
|
|
83
|
+
* `PostgresDocStore.guards` — see `addCommitGuard`'s doc comment for the full contract. Empty
|
|
84
|
+
* at Tier 0 and in every non-fleet/non-receipts deployment (no guard ever runs — SQLite pays
|
|
85
|
+
* nothing for a feature it doesn't use). */
|
|
86
|
+
private guards;
|
|
87
|
+
constructor(db: DatabaseAdapter);
|
|
88
|
+
/** Append `guard` to the commit-guard chain — see `guards`'s doc comment. Guards run in
|
|
89
|
+
* REGISTRATION ORDER, SYNCHRONOUSLY, inside `commitWriteBatch`'s one `db.transaction(() => …)`,
|
|
90
|
+
* once per commit over the WHOLE unit array (never once per unit); ANY guard throwing aborts the
|
|
91
|
+
* whole synchronous transaction (no unit lands) — SQLite's transaction wrapper already rolls
|
|
92
|
+
* back on any thrown error, so this needs no special-casing here. A guard that returns a
|
|
93
|
+
* thenable (i.e. is `async`) is a dev-time bug — see the check in `commitWriteBatch`. Returns an
|
|
94
|
+
* unregister function that removes exactly this guard (a no-op if called again). */
|
|
95
|
+
addCommitGuard(guard: SqliteCommitGuard): () => void;
|
|
96
|
+
/** The synchronous querier handed to every SQLite commit guard — routes through the same
|
|
97
|
+
* prepared-statement cache (`this.prep`) every other method uses, so a guard's writes share
|
|
98
|
+
* SQLite's statement caching for free. */
|
|
99
|
+
private guardQuerier;
|
|
100
|
+
private prep;
|
|
101
|
+
private serializeValue;
|
|
102
|
+
private parseValue;
|
|
103
|
+
setupSchema(_options?: SchemaSetupOptions): Promise<void>;
|
|
104
|
+
/** Insert already-stamped document + index rows in the current transaction. Shared by `write()`
|
|
105
|
+
* (caller-supplied timestamps) and `commitWrite()` (store-allocated), so the row-building /
|
|
106
|
+
* column list lives in exactly one place. Must be called inside `this.db.transaction`. */
|
|
107
|
+
private insertRows;
|
|
108
|
+
write(documents: readonly DocumentLogEntry[], indexUpdates: readonly IndexWrite[], conflictStrategy: ConflictStrategy, shardId?: ShardId): Promise<void>;
|
|
109
|
+
commitWrite(documents: readonly DocumentLogEntry[], indexUpdates: readonly IndexWrite[], shardId?: ShardId, opts?: {
|
|
110
|
+
meta?: Record<string, string>;
|
|
111
|
+
}): Promise<bigint>;
|
|
112
|
+
commitWriteBatch(units: readonly CommitUnit[], shardId?: ShardId): Promise<bigint[]>;
|
|
113
|
+
get(id: InternalDocumentId, readTimestamp?: bigint): Promise<LatestDocument | null>;
|
|
114
|
+
index_scan(indexId: string, _tableId: string, readTimestamp: bigint, interval: Interval, order: Order, limit?: number): AsyncGenerator<readonly [Uint8Array, LatestDocument]>;
|
|
115
|
+
load_documents(range: TimestampRange, order: Order, limit?: number): AsyncGenerator<DocumentLogEntry>;
|
|
116
|
+
previous_revisions(queries: readonly PrevRevQuery[]): Promise<Map<string, DocumentLogEntry>>;
|
|
117
|
+
scan(tableId: string, readTimestamp?: bigint): Promise<LatestDocument[]>;
|
|
118
|
+
count(tableId: string): Promise<number>;
|
|
119
|
+
maxTimestamp(): Promise<bigint>;
|
|
120
|
+
getGlobal(key: string): Promise<JSONValue | null>;
|
|
121
|
+
writeGlobal(key: string, value: JSONValue): Promise<void>;
|
|
122
|
+
writeGlobalIfAbsent(key: string, value: JSONValue): Promise<boolean>;
|
|
123
|
+
getClientVerdict(identity: string, clientId: string, seq: number): Promise<ClientVerdictRecord | null>;
|
|
124
|
+
getClientFloor(identity: string, clientId: string): Promise<number | null>;
|
|
125
|
+
recordClientVerdict(identity: string, clientId: string, seq: number, record: ClientVerdictWrite): Promise<void>;
|
|
126
|
+
updateClientVerdictValue(identity: string, clientId: string, seq: number, value: JSONValue): Promise<void>;
|
|
127
|
+
pruneClientMutations(identity: string, clientId: string, opts: {
|
|
128
|
+
ackedThrough?: number;
|
|
129
|
+
ttlBeforeMs?: number;
|
|
130
|
+
}): Promise<{
|
|
131
|
+
prunedThroughSeq: number;
|
|
132
|
+
}>;
|
|
133
|
+
sweepExpiredClientMutations(beforeMs: number): Promise<{
|
|
134
|
+
deletedCount: number;
|
|
135
|
+
}>;
|
|
136
|
+
/**
|
|
137
|
+
* The store's CURRENT state (Tier 3 Slice 3, Task 3.1 — the snapshot source): for every document
|
|
138
|
+
* id across every table, its LATEST revision, EXCLUDING ids whose latest revision is a tombstone
|
|
139
|
+
* (`value === null`) — mirrors `scan()`'s per-table "newest revision per id" query, just without
|
|
140
|
+
* the `table_id` filter, so it spans the whole store in one pass. Plus every CURRENT row of the
|
|
141
|
+
* `indexes` table (the newest revision per `(index_id, key)`, live pointer OR deletion marker
|
|
142
|
+
* alike — mirrors `index_scan()`'s own `MAX(ts)`-per-key subquery, but unlike `index_scan` this
|
|
143
|
+
* does NOT skip deleted markers: the snapshot must reproduce the index table's own current rows
|
|
144
|
+
* exactly, not the documents they resolve to).
|
|
145
|
+
*
|
|
146
|
+
* Each returned `DocumentLogEntry`/`IndexWrite` carries its REAL `ts`/`prev_ts` (not renumbered) —
|
|
147
|
+
* `ObjectStoreDocStore.snapshot()` stamps the payload's own `frontierTs`/`segBase` around this, and
|
|
148
|
+
* restoring via `write(dump.documents, dump.indexUpdates, "Overwrite")` on a fresh store reproduces
|
|
149
|
+
* this exact state, with `prev_ts` chains intact so a tail segment's `prev_ts` still resolves.
|
|
150
|
+
*/
|
|
151
|
+
dumpCurrentState(): Promise<{
|
|
152
|
+
documents: DocumentLogEntry[];
|
|
153
|
+
indexUpdates: IndexWrite[];
|
|
154
|
+
}>;
|
|
155
|
+
/** Close the underlying database adapter (checkpoint + release the file). Used by graceful shutdown. */
|
|
156
|
+
close(): void;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export { BunSqliteAdapter, type BunSqliteOptions, type DatabaseAdapter, NodeSqliteAdapter, type NodeSqliteOptions, type PreparedStatement, type RunResult, type SqlRow, type SqlValue, type SqliteCommitGuard, SqliteDocStore, type SqliteGuardQuerier };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
// src/node-adapter.ts
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
var NodeStatement = class {
|
|
4
|
+
constructor(stmt) {
|
|
5
|
+
this.stmt = stmt;
|
|
6
|
+
stmt.setReadBigInts(true);
|
|
7
|
+
}
|
|
8
|
+
stmt;
|
|
9
|
+
run(...params) {
|
|
10
|
+
const r = this.stmt.run(...params);
|
|
11
|
+
return { changes: Number(r.changes), lastInsertRowid: r.lastInsertRowid };
|
|
12
|
+
}
|
|
13
|
+
get(...params) {
|
|
14
|
+
return this.stmt.get(...params);
|
|
15
|
+
}
|
|
16
|
+
all(...params) {
|
|
17
|
+
return this.stmt.all(...params);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var NodeSqliteAdapter = class {
|
|
21
|
+
db;
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
24
|
+
const { DatabaseSync } = nodeRequire("node:sqlite");
|
|
25
|
+
this.db = new DatabaseSync(options.path ?? ":memory:");
|
|
26
|
+
this.db.exec("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON;");
|
|
27
|
+
}
|
|
28
|
+
exec(sql) {
|
|
29
|
+
this.db.exec(sql);
|
|
30
|
+
}
|
|
31
|
+
prepare(sql) {
|
|
32
|
+
return new NodeStatement(this.db.prepare(sql));
|
|
33
|
+
}
|
|
34
|
+
transaction(fn) {
|
|
35
|
+
this.db.exec("BEGIN");
|
|
36
|
+
try {
|
|
37
|
+
const result = fn();
|
|
38
|
+
this.db.exec("COMMIT");
|
|
39
|
+
return result;
|
|
40
|
+
} catch (e) {
|
|
41
|
+
this.db.exec("ROLLBACK");
|
|
42
|
+
throw e;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
close() {
|
|
46
|
+
this.db.close();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/bun-adapter.ts
|
|
51
|
+
import { createRequire as createRequire2 } from "module";
|
|
52
|
+
var BunStatementWrapper = class {
|
|
53
|
+
constructor(stmt) {
|
|
54
|
+
this.stmt = stmt;
|
|
55
|
+
}
|
|
56
|
+
stmt;
|
|
57
|
+
run(...params) {
|
|
58
|
+
const r = this.stmt.run(...params);
|
|
59
|
+
return { changes: Number(r.changes), lastInsertRowid: r.lastInsertRowid };
|
|
60
|
+
}
|
|
61
|
+
get(...params) {
|
|
62
|
+
return this.stmt.get(...params);
|
|
63
|
+
}
|
|
64
|
+
all(...params) {
|
|
65
|
+
return this.stmt.all(...params);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var BunSqliteAdapter = class {
|
|
69
|
+
db;
|
|
70
|
+
constructor(options = {}) {
|
|
71
|
+
const bunRequire = createRequire2(import.meta.url);
|
|
72
|
+
const { Database } = bunRequire("bun:sqlite");
|
|
73
|
+
this.db = new Database(options.path ?? ":memory:", { safeIntegers: true });
|
|
74
|
+
this.db.exec("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON;");
|
|
75
|
+
}
|
|
76
|
+
exec(sql) {
|
|
77
|
+
this.db.exec(sql);
|
|
78
|
+
}
|
|
79
|
+
prepare(sql) {
|
|
80
|
+
return new BunStatementWrapper(this.db.prepare(sql));
|
|
81
|
+
}
|
|
82
|
+
transaction(fn) {
|
|
83
|
+
this.db.exec("BEGIN");
|
|
84
|
+
try {
|
|
85
|
+
const result = fn();
|
|
86
|
+
this.db.exec("COMMIT");
|
|
87
|
+
return result;
|
|
88
|
+
} catch (e) {
|
|
89
|
+
this.db.exec("ROLLBACK");
|
|
90
|
+
throw e;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
close() {
|
|
94
|
+
this.db.close();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// src/sqlite-docstore.ts
|
|
99
|
+
import { getPrevRevQueryKey, CLIENT_VERDICT_VALUE_CAP_BYTES } from "@helipod/docstore";
|
|
100
|
+
import { encodeStorageTableId, decodeStorageTableId, DEFAULT_SHARD } from "@helipod/id-codec";
|
|
101
|
+
import { convexToJson, jsonToConvex } from "@helipod/values";
|
|
102
|
+
var SCHEMA_SQL = `
|
|
103
|
+
CREATE TABLE IF NOT EXISTS documents (
|
|
104
|
+
table_id TEXT NOT NULL,
|
|
105
|
+
internal_id BLOB NOT NULL,
|
|
106
|
+
ts INTEGER NOT NULL,
|
|
107
|
+
prev_ts INTEGER,
|
|
108
|
+
value TEXT,
|
|
109
|
+
PRIMARY KEY (table_id, internal_id, ts)
|
|
110
|
+
) WITHOUT ROWID;
|
|
111
|
+
CREATE INDEX IF NOT EXISTS documents_by_ts ON documents (ts);
|
|
112
|
+
|
|
113
|
+
CREATE TABLE IF NOT EXISTS indexes (
|
|
114
|
+
index_id TEXT NOT NULL,
|
|
115
|
+
key BLOB NOT NULL,
|
|
116
|
+
ts INTEGER NOT NULL,
|
|
117
|
+
table_id TEXT,
|
|
118
|
+
internal_id BLOB,
|
|
119
|
+
deleted INTEGER NOT NULL DEFAULT 0,
|
|
120
|
+
PRIMARY KEY (index_id, key, ts)
|
|
121
|
+
) WITHOUT ROWID;
|
|
122
|
+
|
|
123
|
+
CREATE TABLE IF NOT EXISTS persistence_globals (
|
|
124
|
+
key TEXT PRIMARY KEY,
|
|
125
|
+
value TEXT NOT NULL
|
|
126
|
+
) WITHOUT ROWID;
|
|
127
|
+
|
|
128
|
+
CREATE TABLE IF NOT EXISTS client_mutations (
|
|
129
|
+
identity TEXT NOT NULL,
|
|
130
|
+
client_id TEXT NOT NULL,
|
|
131
|
+
seq INTEGER NOT NULL,
|
|
132
|
+
verdict TEXT NOT NULL,
|
|
133
|
+
commit_ts INTEGER NOT NULL,
|
|
134
|
+
value_json TEXT,
|
|
135
|
+
error_code TEXT,
|
|
136
|
+
created_at INTEGER NOT NULL,
|
|
137
|
+
PRIMARY KEY (identity, client_id, seq)
|
|
138
|
+
) WITHOUT ROWID;
|
|
139
|
+
CREATE INDEX IF NOT EXISTS client_mutations_by_created_at ON client_mutations (created_at);
|
|
140
|
+
|
|
141
|
+
CREATE TABLE IF NOT EXISTS client_floors (
|
|
142
|
+
identity TEXT NOT NULL,
|
|
143
|
+
client_id TEXT NOT NULL,
|
|
144
|
+
pruned_through_seq INTEGER NOT NULL,
|
|
145
|
+
updated_at INTEGER NOT NULL,
|
|
146
|
+
PRIMARY KEY (identity, client_id)
|
|
147
|
+
) WITHOUT ROWID;
|
|
148
|
+
`;
|
|
149
|
+
function asBigInt(v) {
|
|
150
|
+
return typeof v === "bigint" ? v : BigInt(v);
|
|
151
|
+
}
|
|
152
|
+
function asBigIntOrNull(v) {
|
|
153
|
+
return v === null || v === void 0 ? null : asBigInt(v);
|
|
154
|
+
}
|
|
155
|
+
function clientMutationsDeleteClause(opts) {
|
|
156
|
+
const parts = [];
|
|
157
|
+
const params = [];
|
|
158
|
+
if (opts.ackedThrough !== void 0) {
|
|
159
|
+
parts.push(`seq <= ?`);
|
|
160
|
+
params.push(opts.ackedThrough);
|
|
161
|
+
}
|
|
162
|
+
if (opts.ttlBeforeMs !== void 0) {
|
|
163
|
+
parts.push(`created_at < ?`);
|
|
164
|
+
params.push(opts.ttlBeforeMs);
|
|
165
|
+
}
|
|
166
|
+
return parts.length === 0 ? { clause: null, params: [] } : { clause: parts.join(" OR "), params };
|
|
167
|
+
}
|
|
168
|
+
function maxCandidate(ackedThrough, deletedMaxSeq) {
|
|
169
|
+
if (ackedThrough === null) return deletedMaxSeq;
|
|
170
|
+
if (deletedMaxSeq === null) return ackedThrough;
|
|
171
|
+
return Math.max(ackedThrough, deletedMaxSeq);
|
|
172
|
+
}
|
|
173
|
+
function cappedValueJson(value) {
|
|
174
|
+
if (value === void 0) return null;
|
|
175
|
+
const json = JSON.stringify(value);
|
|
176
|
+
return Buffer.byteLength(json, "utf8") > CLIENT_VERDICT_VALUE_CAP_BYTES ? null : json;
|
|
177
|
+
}
|
|
178
|
+
function clientVerdictRecordFromRow(row) {
|
|
179
|
+
return {
|
|
180
|
+
verdict: row.verdict,
|
|
181
|
+
commitTs: asBigInt(row.commit_ts),
|
|
182
|
+
hasValue: row.value_json !== null,
|
|
183
|
+
value: row.value_json === null ? null : JSON.parse(row.value_json),
|
|
184
|
+
errorCode: row.error_code ?? null,
|
|
185
|
+
createdAt: Number(row.created_at)
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
var SqliteDocStore = class {
|
|
189
|
+
constructor(db) {
|
|
190
|
+
this.db = db;
|
|
191
|
+
}
|
|
192
|
+
db;
|
|
193
|
+
stmtCache = /* @__PURE__ */ new Map();
|
|
194
|
+
/** The commit-guard CHAIN (Receipted Outbox decision 2), the SQLite counterpart of
|
|
195
|
+
* `PostgresDocStore.guards` — see `addCommitGuard`'s doc comment for the full contract. Empty
|
|
196
|
+
* at Tier 0 and in every non-fleet/non-receipts deployment (no guard ever runs — SQLite pays
|
|
197
|
+
* nothing for a feature it doesn't use). */
|
|
198
|
+
guards = [];
|
|
199
|
+
/** Append `guard` to the commit-guard chain — see `guards`'s doc comment. Guards run in
|
|
200
|
+
* REGISTRATION ORDER, SYNCHRONOUSLY, inside `commitWriteBatch`'s one `db.transaction(() => …)`,
|
|
201
|
+
* once per commit over the WHOLE unit array (never once per unit); ANY guard throwing aborts the
|
|
202
|
+
* whole synchronous transaction (no unit lands) — SQLite's transaction wrapper already rolls
|
|
203
|
+
* back on any thrown error, so this needs no special-casing here. A guard that returns a
|
|
204
|
+
* thenable (i.e. is `async`) is a dev-time bug — see the check in `commitWriteBatch`. Returns an
|
|
205
|
+
* unregister function that removes exactly this guard (a no-op if called again). */
|
|
206
|
+
addCommitGuard(guard) {
|
|
207
|
+
this.guards.push(guard);
|
|
208
|
+
return () => {
|
|
209
|
+
const i = this.guards.indexOf(guard);
|
|
210
|
+
if (i >= 0) this.guards.splice(i, 1);
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/** The synchronous querier handed to every SQLite commit guard — routes through the same
|
|
214
|
+
* prepared-statement cache (`this.prep`) every other method uses, so a guard's writes share
|
|
215
|
+
* SQLite's statement caching for free. */
|
|
216
|
+
guardQuerier() {
|
|
217
|
+
return {
|
|
218
|
+
run: (sql, ...params) => {
|
|
219
|
+
this.prep(sql).run(...params);
|
|
220
|
+
},
|
|
221
|
+
get: (sql, ...params) => this.prep(sql).get(...params)
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
prep(sql) {
|
|
225
|
+
let stmt = this.stmtCache.get(sql);
|
|
226
|
+
if (!stmt) {
|
|
227
|
+
stmt = this.db.prepare(sql);
|
|
228
|
+
this.stmtCache.set(sql, stmt);
|
|
229
|
+
}
|
|
230
|
+
return stmt;
|
|
231
|
+
}
|
|
232
|
+
serializeValue(value) {
|
|
233
|
+
return JSON.stringify(convexToJson(value));
|
|
234
|
+
}
|
|
235
|
+
parseValue(text) {
|
|
236
|
+
return jsonToConvex(JSON.parse(text));
|
|
237
|
+
}
|
|
238
|
+
async setupSchema(_options) {
|
|
239
|
+
this.db.exec(SCHEMA_SQL);
|
|
240
|
+
for (const table of ["documents", "indexes"]) {
|
|
241
|
+
const cols = this.prep(`PRAGMA table_info(${table})`).all();
|
|
242
|
+
if (!cols.some((c) => c.name === "shard_id")) {
|
|
243
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN shard_id TEXT NOT NULL DEFAULT 'default'`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** Insert already-stamped document + index rows in the current transaction. Shared by `write()`
|
|
248
|
+
* (caller-supplied timestamps) and `commitWrite()` (store-allocated), so the row-building /
|
|
249
|
+
* column list lives in exactly one place. Must be called inside `this.db.transaction`. */
|
|
250
|
+
insertRows(documents, indexUpdates, conflictStrategy, shardId) {
|
|
251
|
+
const docVerb = conflictStrategy === "Overwrite" ? "INSERT OR REPLACE" : "INSERT";
|
|
252
|
+
const docStmt = this.prep(
|
|
253
|
+
`${docVerb} INTO documents (table_id, internal_id, ts, prev_ts, value, shard_id) VALUES (?, ?, ?, ?, ?, ?)`
|
|
254
|
+
);
|
|
255
|
+
const idxStmt = this.prep(
|
|
256
|
+
`INSERT OR REPLACE INTO indexes (index_id, key, ts, table_id, internal_id, deleted, shard_id) VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
257
|
+
);
|
|
258
|
+
for (const entry of documents) {
|
|
259
|
+
docStmt.run(
|
|
260
|
+
encodeStorageTableId(entry.id.tableNumber),
|
|
261
|
+
entry.id.internalId,
|
|
262
|
+
entry.ts,
|
|
263
|
+
entry.prev_ts,
|
|
264
|
+
entry.value === null ? null : this.serializeValue(entry.value.value),
|
|
265
|
+
shardId
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
for (const { ts, update } of indexUpdates) {
|
|
269
|
+
const v = update.value;
|
|
270
|
+
idxStmt.run(
|
|
271
|
+
update.indexId,
|
|
272
|
+
update.key,
|
|
273
|
+
ts,
|
|
274
|
+
v.type === "NonClustered" ? encodeStorageTableId(v.docId.tableNumber) : null,
|
|
275
|
+
v.type === "NonClustered" ? v.docId.internalId : null,
|
|
276
|
+
v.type === "NonClustered" ? 0 : 1,
|
|
277
|
+
shardId
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async write(documents, indexUpdates, conflictStrategy, shardId) {
|
|
282
|
+
this.db.transaction(() => {
|
|
283
|
+
this.insertRows(documents, indexUpdates, conflictStrategy, shardId ?? DEFAULT_SHARD);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
async commitWrite(documents, indexUpdates, shardId, opts) {
|
|
287
|
+
const [ts] = await this.commitWriteBatch([{ documents, indexUpdates, meta: opts?.meta }], shardId);
|
|
288
|
+
return ts;
|
|
289
|
+
}
|
|
290
|
+
async commitWriteBatch(units, shardId) {
|
|
291
|
+
return this.db.transaction(() => {
|
|
292
|
+
const out = [];
|
|
293
|
+
const guardUnits = [];
|
|
294
|
+
const shard = shardId ?? DEFAULT_SHARD;
|
|
295
|
+
for (const unit of units) {
|
|
296
|
+
const row = this.prep(`SELECT MAX(ts) AS m FROM documents`).get();
|
|
297
|
+
const m = row?.m;
|
|
298
|
+
const commitTs = (m === null || m === void 0 ? 0n : asBigInt(m)) + 1n;
|
|
299
|
+
const stampedDocs = unit.documents.map((e) => ({ ...e, ts: commitTs }));
|
|
300
|
+
const stampedIdx = unit.indexUpdates.map((w) => ({ ...w, ts: commitTs }));
|
|
301
|
+
this.insertRows(stampedDocs, stampedIdx, "Error", shard);
|
|
302
|
+
out.push(commitTs);
|
|
303
|
+
guardUnits.push({ ts: commitTs, meta: unit.meta });
|
|
304
|
+
}
|
|
305
|
+
if (guardUnits.length > 0) {
|
|
306
|
+
const q = this.guardQuerier();
|
|
307
|
+
for (const g of this.guards) {
|
|
308
|
+
const ret = g(q, guardUnits, shard);
|
|
309
|
+
if (ret && typeof ret.then === "function") {
|
|
310
|
+
throw new Error(
|
|
311
|
+
"[docstore-sqlite] a commit guard returned a Promise; SQLite guards must be synchronous \u2014 its writes cannot be awaited inside the single-transaction commit"
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
async get(id, readTimestamp) {
|
|
320
|
+
const tableId = encodeStorageTableId(id.tableNumber);
|
|
321
|
+
const row = readTimestamp === void 0 ? this.prep(
|
|
322
|
+
`SELECT ts, prev_ts, value FROM documents WHERE table_id = ? AND internal_id = ? ORDER BY ts DESC LIMIT 1`
|
|
323
|
+
).get(tableId, id.internalId) : this.prep(
|
|
324
|
+
`SELECT ts, prev_ts, value FROM documents WHERE table_id = ? AND internal_id = ? AND ts <= ? ORDER BY ts DESC LIMIT 1`
|
|
325
|
+
).get(tableId, id.internalId, readTimestamp);
|
|
326
|
+
if (!row || row.value === null) return null;
|
|
327
|
+
return {
|
|
328
|
+
ts: asBigInt(row.ts),
|
|
329
|
+
prev_ts: asBigIntOrNull(row.prev_ts),
|
|
330
|
+
value: { id, value: this.parseValue(row.value) }
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
async *index_scan(indexId, _tableId, readTimestamp, interval, order, limit) {
|
|
334
|
+
const dir = order === "desc" ? "DESC" : "ASC";
|
|
335
|
+
const params = [indexId, interval.start];
|
|
336
|
+
let sql = `SELECT i.key AS key, i.table_id AS table_id, i.internal_id AS internal_id, i.deleted AS deleted FROM indexes i WHERE i.index_id = ? AND i.key >= ?`;
|
|
337
|
+
if (interval.end !== null) {
|
|
338
|
+
sql += ` AND i.key < ?`;
|
|
339
|
+
params.push(interval.end);
|
|
340
|
+
}
|
|
341
|
+
sql += ` AND i.ts <= ? AND i.ts = (SELECT MAX(i2.ts) FROM indexes i2 WHERE i2.index_id = i.index_id AND i2.key = i.key AND i2.ts <= ?)`;
|
|
342
|
+
params.push(readTimestamp, readTimestamp);
|
|
343
|
+
sql += ` ORDER BY i.key ${dir}`;
|
|
344
|
+
const rows = this.prep(sql).all(...params);
|
|
345
|
+
let yielded = 0;
|
|
346
|
+
for (const row of rows) {
|
|
347
|
+
if (Number(row.deleted) === 1 || row.internal_id === null || row.table_id === null) continue;
|
|
348
|
+
const docId = {
|
|
349
|
+
tableNumber: decodeStorageTableId(row.table_id),
|
|
350
|
+
internalId: row.internal_id
|
|
351
|
+
};
|
|
352
|
+
const doc = await this.get(docId, readTimestamp);
|
|
353
|
+
if (doc === null) continue;
|
|
354
|
+
yield [row.key, doc];
|
|
355
|
+
if (limit !== void 0 && ++yielded >= limit) return;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async *load_documents(range, order, limit) {
|
|
359
|
+
const dir = order === "desc" ? "DESC" : "ASC";
|
|
360
|
+
const limitSql = limit !== void 0 ? ` LIMIT ${Math.max(0, Math.floor(limit))}` : "";
|
|
361
|
+
const rows = this.prep(
|
|
362
|
+
`SELECT table_id, internal_id, ts, prev_ts, value FROM documents WHERE ts >= ? AND ts < ? ORDER BY ts ${dir}, table_id ${dir}, internal_id ${dir}${limitSql}`
|
|
363
|
+
).all(range.minInclusive, range.maxExclusive);
|
|
364
|
+
for (const row of rows) {
|
|
365
|
+
const id = {
|
|
366
|
+
tableNumber: decodeStorageTableId(row.table_id),
|
|
367
|
+
internalId: row.internal_id
|
|
368
|
+
};
|
|
369
|
+
const value = row.value === null ? null : { id, value: this.parseValue(row.value) };
|
|
370
|
+
yield { ts: asBigInt(row.ts), id, value, prev_ts: asBigIntOrNull(row.prev_ts) };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async previous_revisions(queries) {
|
|
374
|
+
const out = /* @__PURE__ */ new Map();
|
|
375
|
+
const stmt = this.prep(
|
|
376
|
+
`SELECT ts, prev_ts, value FROM documents WHERE table_id = ? AND internal_id = ? AND ts <= ? ORDER BY ts DESC LIMIT 1`
|
|
377
|
+
);
|
|
378
|
+
for (const q of queries) {
|
|
379
|
+
const row = stmt.get(encodeStorageTableId(q.id.tableNumber), q.id.internalId, q.ts);
|
|
380
|
+
if (!row) continue;
|
|
381
|
+
const value = row.value === null ? null : { id: q.id, value: this.parseValue(row.value) };
|
|
382
|
+
out.set(getPrevRevQueryKey(q.id, q.ts), {
|
|
383
|
+
ts: asBigInt(row.ts),
|
|
384
|
+
id: q.id,
|
|
385
|
+
value,
|
|
386
|
+
prev_ts: asBigIntOrNull(row.prev_ts)
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
391
|
+
async scan(tableId, readTimestamp) {
|
|
392
|
+
const tableNumber = decodeStorageTableId(tableId);
|
|
393
|
+
const rows = readTimestamp === void 0 ? this.prep(
|
|
394
|
+
`SELECT d.internal_id AS internal_id, d.ts AS ts, d.prev_ts AS prev_ts, d.value AS value FROM documents d WHERE d.table_id = ? AND d.ts = (SELECT MAX(d2.ts) FROM documents d2 WHERE d2.table_id = d.table_id AND d2.internal_id = d.internal_id) ORDER BY d.internal_id ASC`
|
|
395
|
+
).all(tableId) : this.prep(
|
|
396
|
+
`SELECT d.internal_id AS internal_id, d.ts AS ts, d.prev_ts AS prev_ts, d.value AS value FROM documents d WHERE d.table_id = ? AND d.ts <= ? AND d.ts = (SELECT MAX(d2.ts) FROM documents d2 WHERE d2.table_id = d.table_id AND d2.internal_id = d.internal_id AND d2.ts <= ?) ORDER BY d.internal_id ASC`
|
|
397
|
+
).all(tableId, readTimestamp, readTimestamp);
|
|
398
|
+
const out = [];
|
|
399
|
+
for (const row of rows) {
|
|
400
|
+
if (row.value === null) continue;
|
|
401
|
+
const id = { tableNumber, internalId: row.internal_id };
|
|
402
|
+
out.push({
|
|
403
|
+
ts: asBigInt(row.ts),
|
|
404
|
+
prev_ts: asBigIntOrNull(row.prev_ts),
|
|
405
|
+
value: { id, value: this.parseValue(row.value) }
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
async count(tableId) {
|
|
411
|
+
const row = this.prep(
|
|
412
|
+
`SELECT COUNT(*) AS n FROM ( SELECT d.value AS value FROM documents d WHERE d.table_id = ? AND d.ts = (SELECT MAX(d2.ts) FROM documents d2 WHERE d2.table_id = d.table_id AND d2.internal_id = d.internal_id) ) WHERE value IS NOT NULL`
|
|
413
|
+
).get(tableId);
|
|
414
|
+
return Number(row?.n ?? 0);
|
|
415
|
+
}
|
|
416
|
+
async maxTimestamp() {
|
|
417
|
+
const row = this.prep(`SELECT MAX(ts) AS m FROM documents`).get();
|
|
418
|
+
const m = row?.m;
|
|
419
|
+
return m === null || m === void 0 ? 0n : asBigInt(m);
|
|
420
|
+
}
|
|
421
|
+
async getGlobal(key) {
|
|
422
|
+
const row = this.prep(`SELECT value FROM persistence_globals WHERE key = ?`).get(key);
|
|
423
|
+
return row ? JSON.parse(row.value) : null;
|
|
424
|
+
}
|
|
425
|
+
async writeGlobal(key, value) {
|
|
426
|
+
this.prep(`INSERT OR REPLACE INTO persistence_globals (key, value) VALUES (?, ?)`).run(
|
|
427
|
+
key,
|
|
428
|
+
JSON.stringify(value)
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
async writeGlobalIfAbsent(key, value) {
|
|
432
|
+
const r = this.prep(`INSERT OR IGNORE INTO persistence_globals (key, value) VALUES (?, ?)`).run(
|
|
433
|
+
key,
|
|
434
|
+
JSON.stringify(value)
|
|
435
|
+
);
|
|
436
|
+
return r.changes > 0;
|
|
437
|
+
}
|
|
438
|
+
// ── Client mutation receipts (the Receipted Outbox, verdict §(c)) ─────────────────────────────
|
|
439
|
+
async getClientVerdict(identity, clientId, seq) {
|
|
440
|
+
const row = this.prep(
|
|
441
|
+
`SELECT verdict, commit_ts, value_json, error_code, created_at FROM client_mutations
|
|
442
|
+
WHERE identity = ? AND client_id = ? AND seq = ?`
|
|
443
|
+
).get(identity, clientId, seq);
|
|
444
|
+
if (!row) return null;
|
|
445
|
+
return clientVerdictRecordFromRow(row);
|
|
446
|
+
}
|
|
447
|
+
async getClientFloor(identity, clientId) {
|
|
448
|
+
const row = this.prep(
|
|
449
|
+
`SELECT pruned_through_seq FROM client_floors WHERE identity = ? AND client_id = ?`
|
|
450
|
+
).get(identity, clientId);
|
|
451
|
+
return row ? Number(row.pruned_through_seq) : null;
|
|
452
|
+
}
|
|
453
|
+
async recordClientVerdict(identity, clientId, seq, record) {
|
|
454
|
+
const valueJson = cappedValueJson(record.value);
|
|
455
|
+
this.prep(
|
|
456
|
+
`INSERT OR IGNORE INTO client_mutations
|
|
457
|
+
(identity, client_id, seq, verdict, commit_ts, value_json, error_code, created_at)
|
|
458
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
459
|
+
).run(
|
|
460
|
+
identity,
|
|
461
|
+
clientId,
|
|
462
|
+
seq,
|
|
463
|
+
record.verdict,
|
|
464
|
+
record.commitTs,
|
|
465
|
+
valueJson,
|
|
466
|
+
record.verdict === "failed" ? record.errorCode : null,
|
|
467
|
+
Date.now()
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
async updateClientVerdictValue(identity, clientId, seq, value) {
|
|
471
|
+
const valueJson = cappedValueJson(value);
|
|
472
|
+
this.prep(
|
|
473
|
+
`UPDATE client_mutations SET value_json = ? WHERE identity = ? AND client_id = ? AND seq = ?`
|
|
474
|
+
).run(valueJson, identity, clientId, seq);
|
|
475
|
+
}
|
|
476
|
+
async pruneClientMutations(identity, clientId, opts) {
|
|
477
|
+
return this.db.transaction(() => {
|
|
478
|
+
const currentFloorRow = this.prep(
|
|
479
|
+
`SELECT pruned_through_seq FROM client_floors WHERE identity = ? AND client_id = ?`
|
|
480
|
+
).get(identity, clientId);
|
|
481
|
+
const currentFloor = currentFloorRow ? Number(currentFloorRow.pruned_through_seq) : null;
|
|
482
|
+
let deletedMaxSeq = null;
|
|
483
|
+
const { clause, params } = clientMutationsDeleteClause(opts);
|
|
484
|
+
if (clause !== null) {
|
|
485
|
+
const rows = this.prep(
|
|
486
|
+
`DELETE FROM client_mutations WHERE identity = ? AND client_id = ? AND (${clause}) RETURNING seq`
|
|
487
|
+
).all(identity, clientId, ...params);
|
|
488
|
+
for (const row of rows) {
|
|
489
|
+
const s = Number(row.seq);
|
|
490
|
+
if (deletedMaxSeq === null || s > deletedMaxSeq) deletedMaxSeq = s;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const candidate = maxCandidate(opts.ackedThrough ?? null, deletedMaxSeq);
|
|
494
|
+
const base = currentFloor ?? -1;
|
|
495
|
+
if (candidate === null || candidate <= base) {
|
|
496
|
+
return { prunedThroughSeq: currentFloor ?? 0 };
|
|
497
|
+
}
|
|
498
|
+
this.prep(
|
|
499
|
+
`INSERT INTO client_floors (identity, client_id, pruned_through_seq, updated_at) VALUES (?, ?, ?, ?)
|
|
500
|
+
ON CONFLICT (identity, client_id) DO UPDATE SET
|
|
501
|
+
pruned_through_seq = MAX(client_floors.pruned_through_seq, excluded.pruned_through_seq),
|
|
502
|
+
updated_at = excluded.updated_at`
|
|
503
|
+
).run(identity, clientId, candidate, Date.now());
|
|
504
|
+
return { prunedThroughSeq: candidate };
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
async sweepExpiredClientMutations(beforeMs) {
|
|
508
|
+
return this.db.transaction(() => {
|
|
509
|
+
const rows = this.prep(
|
|
510
|
+
`DELETE FROM client_mutations WHERE created_at < ? RETURNING identity, client_id, seq`
|
|
511
|
+
).all(beforeMs);
|
|
512
|
+
if (rows.length === 0) return { deletedCount: 0 };
|
|
513
|
+
const maxByClient = /* @__PURE__ */ new Map();
|
|
514
|
+
for (const row of rows) {
|
|
515
|
+
const identity = row.identity;
|
|
516
|
+
const clientId = row.client_id;
|
|
517
|
+
const seq = Number(row.seq);
|
|
518
|
+
const key = `${identity}\0${clientId}`;
|
|
519
|
+
const cur = maxByClient.get(key);
|
|
520
|
+
if (!cur || seq > cur.maxSeq) maxByClient.set(key, { identity, clientId, maxSeq: seq });
|
|
521
|
+
}
|
|
522
|
+
const now = Date.now();
|
|
523
|
+
const upsert = this.prep(
|
|
524
|
+
`INSERT INTO client_floors (identity, client_id, pruned_through_seq, updated_at) VALUES (?, ?, ?, ?)
|
|
525
|
+
ON CONFLICT (identity, client_id) DO UPDATE SET
|
|
526
|
+
pruned_through_seq = MAX(client_floors.pruned_through_seq, excluded.pruned_through_seq),
|
|
527
|
+
updated_at = excluded.updated_at`
|
|
528
|
+
);
|
|
529
|
+
for (const { identity, clientId, maxSeq } of maxByClient.values()) upsert.run(identity, clientId, maxSeq, now);
|
|
530
|
+
return { deletedCount: rows.length };
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* The store's CURRENT state (Tier 3 Slice 3, Task 3.1 — the snapshot source): for every document
|
|
535
|
+
* id across every table, its LATEST revision, EXCLUDING ids whose latest revision is a tombstone
|
|
536
|
+
* (`value === null`) — mirrors `scan()`'s per-table "newest revision per id" query, just without
|
|
537
|
+
* the `table_id` filter, so it spans the whole store in one pass. Plus every CURRENT row of the
|
|
538
|
+
* `indexes` table (the newest revision per `(index_id, key)`, live pointer OR deletion marker
|
|
539
|
+
* alike — mirrors `index_scan()`'s own `MAX(ts)`-per-key subquery, but unlike `index_scan` this
|
|
540
|
+
* does NOT skip deleted markers: the snapshot must reproduce the index table's own current rows
|
|
541
|
+
* exactly, not the documents they resolve to).
|
|
542
|
+
*
|
|
543
|
+
* Each returned `DocumentLogEntry`/`IndexWrite` carries its REAL `ts`/`prev_ts` (not renumbered) —
|
|
544
|
+
* `ObjectStoreDocStore.snapshot()` stamps the payload's own `frontierTs`/`segBase` around this, and
|
|
545
|
+
* restoring via `write(dump.documents, dump.indexUpdates, "Overwrite")` on a fresh store reproduces
|
|
546
|
+
* this exact state, with `prev_ts` chains intact so a tail segment's `prev_ts` still resolves.
|
|
547
|
+
*/
|
|
548
|
+
async dumpCurrentState() {
|
|
549
|
+
const docRows = this.prep(
|
|
550
|
+
`SELECT d.table_id AS table_id, d.internal_id AS internal_id, d.ts AS ts, d.prev_ts AS prev_ts, d.value AS value FROM documents d WHERE d.ts = (SELECT MAX(d2.ts) FROM documents d2 WHERE d2.table_id = d.table_id AND d2.internal_id = d.internal_id) AND d.value IS NOT NULL ORDER BY d.table_id ASC, d.internal_id ASC`
|
|
551
|
+
).all();
|
|
552
|
+
const documents = docRows.map((row) => {
|
|
553
|
+
const id = {
|
|
554
|
+
tableNumber: decodeStorageTableId(row.table_id),
|
|
555
|
+
internalId: row.internal_id
|
|
556
|
+
};
|
|
557
|
+
const value = { id, value: this.parseValue(row.value) };
|
|
558
|
+
return { ts: asBigInt(row.ts), id, value, prev_ts: asBigIntOrNull(row.prev_ts) };
|
|
559
|
+
});
|
|
560
|
+
const idxRows = this.prep(
|
|
561
|
+
`SELECT i.index_id AS index_id, i.key AS key, i.ts AS ts, i.table_id AS table_id, i.internal_id AS internal_id, i.deleted AS deleted FROM indexes i WHERE i.ts = (SELECT MAX(i2.ts) FROM indexes i2 WHERE i2.index_id = i.index_id AND i2.key = i.key) ORDER BY i.index_id ASC, i.key ASC`
|
|
562
|
+
).all();
|
|
563
|
+
const indexUpdates = idxRows.map((row) => {
|
|
564
|
+
const deleted = Number(row.deleted) === 1;
|
|
565
|
+
const value = deleted ? { type: "Deleted" } : {
|
|
566
|
+
type: "NonClustered",
|
|
567
|
+
docId: {
|
|
568
|
+
tableNumber: decodeStorageTableId(row.table_id),
|
|
569
|
+
internalId: row.internal_id
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
return {
|
|
573
|
+
ts: asBigInt(row.ts),
|
|
574
|
+
update: { indexId: row.index_id, key: row.key, value }
|
|
575
|
+
};
|
|
576
|
+
});
|
|
577
|
+
return { documents, indexUpdates };
|
|
578
|
+
}
|
|
579
|
+
/** Close the underlying database adapter (checkpoint + release the file). Used by graceful shutdown. */
|
|
580
|
+
close() {
|
|
581
|
+
this.db.close();
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
export {
|
|
585
|
+
BunSqliteAdapter,
|
|
586
|
+
NodeSqliteAdapter,
|
|
587
|
+
SqliteDocStore
|
|
588
|
+
};
|
|
589
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/node-adapter.ts","../src/bun-adapter.ts","../src/sqlite-docstore.ts"],"sourcesContent":["/**\n * `DatabaseAdapter` backed by Node's built-in `node:sqlite` (no native dependency — keeps\n * the Tier 0 binary self-contained). Integer columns are read as `bigint` via\n * `setReadBigInts(true)` so logical timestamps beyond 2^53 are exact.\n *\n * `node:sqlite` is newer than most bundlers' builtin lists, so we load it through\n * `createRequire` at runtime (avoiding static ESM resolution) and keep types via a\n * type-only import.\n */\nimport { createRequire } from \"node:module\";\nimport type * as NodeSqlite from \"node:sqlite\";\nimport type { DatabaseAdapter, PreparedStatement, RunResult, SqlRow, SqlValue } from \"./adapter\";\n\nclass NodeStatement implements PreparedStatement {\n constructor(private readonly stmt: NodeSqlite.StatementSync) {\n stmt.setReadBigInts(true);\n }\n\n run(...params: SqlValue[]): RunResult {\n const r = this.stmt.run(...params);\n return { changes: Number(r.changes), lastInsertRowid: r.lastInsertRowid };\n }\n\n get(...params: SqlValue[]): SqlRow | undefined {\n return this.stmt.get(...params) as SqlRow | undefined;\n }\n\n all(...params: SqlValue[]): SqlRow[] {\n return this.stmt.all(...params) as SqlRow[];\n }\n}\n\nexport interface NodeSqliteOptions {\n /** File path, or \":memory:\" (default). */\n path?: string;\n}\n\nexport class NodeSqliteAdapter implements DatabaseAdapter {\n private readonly db: NodeSqlite.DatabaseSync;\n\n constructor(options: NodeSqliteOptions = {}) {\n // Lazy require (named `nodeRequire`, not `require`) so neither the bundler nor a\n // non-Node runtime resolves `node:sqlite` unless this adapter is actually instantiated.\n const nodeRequire = createRequire(import.meta.url);\n const { DatabaseSync } = nodeRequire(\"node:sqlite\") as typeof NodeSqlite;\n this.db = new DatabaseSync(options.path ?? \":memory:\");\n this.db.exec(\"PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON;\");\n }\n\n exec(sql: string): void {\n this.db.exec(sql);\n }\n\n prepare(sql: string): PreparedStatement {\n return new NodeStatement(this.db.prepare(sql));\n }\n\n transaction<T>(fn: () => T): T {\n this.db.exec(\"BEGIN\");\n try {\n const result = fn();\n this.db.exec(\"COMMIT\");\n return result;\n } catch (e) {\n this.db.exec(\"ROLLBACK\");\n throw e;\n }\n }\n\n close(): void {\n this.db.close();\n }\n}\n","/**\n * `DatabaseAdapter` backed by Bun's built-in `bun:sqlite` — the **primary** runtime adapter\n * (fast, stable, and what the single-binary `bun build --compile` target uses). Loaded\n * lazily via `createRequire` so the package stays importable under Node too; instantiate\n * `NodeSqliteAdapter` there instead.\n *\n * Structural types are declared inline to avoid a `bun-types` build dependency; the shape\n * is validated at runtime under Bun (see test/bun-smoke.ts).\n */\nimport { createRequire } from \"node:module\";\nimport type { DatabaseAdapter, PreparedStatement, RunResult, SqlRow, SqlValue } from \"./adapter\";\n\ninterface BunStatement {\n all(...params: SqlValue[]): SqlRow[];\n get(...params: SqlValue[]): SqlRow | undefined;\n run(...params: SqlValue[]): { changes: number | bigint; lastInsertRowid: number | bigint };\n}\ninterface BunDatabase {\n exec(sql: string): void;\n prepare(sql: string): BunStatement;\n close(): void;\n}\ntype BunDatabaseCtor = new (path: string, options?: { safeIntegers?: boolean }) => BunDatabase;\n\nclass BunStatementWrapper implements PreparedStatement {\n constructor(private readonly stmt: BunStatement) {}\n\n run(...params: SqlValue[]): RunResult {\n const r = this.stmt.run(...params);\n return { changes: Number(r.changes), lastInsertRowid: r.lastInsertRowid };\n }\n\n get(...params: SqlValue[]): SqlRow | undefined {\n return this.stmt.get(...params);\n }\n\n all(...params: SqlValue[]): SqlRow[] {\n return this.stmt.all(...params);\n }\n}\n\nexport interface BunSqliteOptions {\n /** File path, or \":memory:\" (default). */\n path?: string;\n}\n\nexport class BunSqliteAdapter implements DatabaseAdapter {\n private readonly db: BunDatabase;\n\n constructor(options: BunSqliteOptions = {}) {\n const bunRequire = createRequire(import.meta.url);\n const { Database } = bunRequire(\"bun:sqlite\") as { Database: BunDatabaseCtor };\n // safeIntegers → INTEGER columns read as bigint (64-bit timestamps stay exact).\n this.db = new Database(options.path ?? \":memory:\", { safeIntegers: true });\n this.db.exec(\"PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON;\");\n }\n\n exec(sql: string): void {\n this.db.exec(sql);\n }\n\n prepare(sql: string): PreparedStatement {\n return new BunStatementWrapper(this.db.prepare(sql));\n }\n\n transaction<T>(fn: () => T): T {\n this.db.exec(\"BEGIN\");\n try {\n const result = fn();\n this.db.exec(\"COMMIT\");\n return result;\n } catch (e) {\n this.db.exec(\"ROLLBACK\");\n throw e;\n }\n }\n\n close(): void {\n this.db.close();\n }\n}\n","/**\n * `SqliteDocStore` — the MVCC document log over three physical tables:\n *\n * documents(table_id, internal_id, ts, prev_ts, value) -- one row per revision; value NULL = tombstone\n * indexes (index_id, key, ts, table_id, internal_id, deleted) -- MVCC index entries\n * persistence_globals(key, value) -- engine metadata KV\n *\n * Snapshot reads pick the newest revision with `ts <= readTimestamp`. Index scans pick,\n * per key in the byte interval, the newest index entry `<= readTimestamp`, skip deletions,\n * and resolve the pointed document at the same timestamp.\n */\nimport type {\n CommitGuardUnit,\n ClientVerdictRecord,\n ClientVerdictWrite,\n CommitUnit,\n ConflictStrategy,\n DatabaseIndexUpdate,\n DocStore,\n DocumentLogEntry,\n DocumentValue,\n IndexWrite,\n Interval,\n LatestDocument,\n Order,\n PrevRevQuery,\n ResolvedDocument,\n SchemaSetupOptions,\n ShardId,\n TimestampRange,\n InternalDocumentId,\n} from \"@helipod/docstore\";\nimport { getPrevRevQueryKey, CLIENT_VERDICT_VALUE_CAP_BYTES } from \"@helipod/docstore\";\nimport { encodeStorageTableId, decodeStorageTableId, DEFAULT_SHARD } from \"@helipod/id-codec\";\nimport { convexToJson, jsonToConvex, type JSONValue, type Value } from \"@helipod/values\";\nimport type { DatabaseAdapter, PreparedStatement, SqlRow, SqlValue } from \"./adapter\";\n\nconst SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS documents (\n table_id TEXT NOT NULL,\n internal_id BLOB NOT NULL,\n ts INTEGER NOT NULL,\n prev_ts INTEGER,\n value TEXT,\n PRIMARY KEY (table_id, internal_id, ts)\n) WITHOUT ROWID;\nCREATE INDEX IF NOT EXISTS documents_by_ts ON documents (ts);\n\nCREATE TABLE IF NOT EXISTS indexes (\n index_id TEXT NOT NULL,\n key BLOB NOT NULL,\n ts INTEGER NOT NULL,\n table_id TEXT,\n internal_id BLOB,\n deleted INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (index_id, key, ts)\n) WITHOUT ROWID;\n\nCREATE TABLE IF NOT EXISTS persistence_globals (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n) WITHOUT ROWID;\n\nCREATE TABLE IF NOT EXISTS client_mutations (\n identity TEXT NOT NULL,\n client_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n verdict TEXT NOT NULL,\n commit_ts INTEGER NOT NULL,\n value_json TEXT,\n error_code TEXT,\n created_at INTEGER NOT NULL,\n PRIMARY KEY (identity, client_id, seq)\n) WITHOUT ROWID;\nCREATE INDEX IF NOT EXISTS client_mutations_by_created_at ON client_mutations (created_at);\n\nCREATE TABLE IF NOT EXISTS client_floors (\n identity TEXT NOT NULL,\n client_id TEXT NOT NULL,\n pruned_through_seq INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n PRIMARY KEY (identity, client_id)\n) WITHOUT ROWID;\n`;\n\nfunction asBigInt(v: SqlValue | undefined): bigint {\n return typeof v === \"bigint\" ? v : BigInt(v as number);\n}\nfunction asBigIntOrNull(v: SqlValue | undefined): bigint | null {\n return v === null || v === undefined ? null : asBigInt(v);\n}\n\n/** The narrow SYNCHRONOUS querier a SQLite commit guard writes receipts through — the sync mirror\n * of `docstore-postgres`'s async `PgQuerier`. SQLite's commit runs inside one synchronous\n * `db.transaction(() => {...})`, so a guard can only ever be handed synchronous primitives. */\nexport interface SqliteGuardQuerier {\n run(sql: string, ...params: unknown[]): void;\n get(sql: string, ...params: unknown[]): Record<string, unknown> | undefined;\n}\n\n/** A SQLite commit guard — see `SqliteDocStore.addCommitGuard`'s doc comment for the full\n * contract. Unlike `PgCommitGuard`, this MUST be synchronous: it runs inside the one-transaction\n * synchronous commit, which cannot await anything. Returning a thenable is a documented dev-time\n * error — see `commitWriteBatch`'s thenable check. */\nexport type SqliteCommitGuard = (\n q: SqliteGuardQuerier,\n units: readonly CommitGuardUnit[],\n shardId: ShardId,\n) => void;\n// ── Client mutation receipts (the Receipted Outbox, verdict §(c)) — pure helpers ──────────────────\n\n/** Build the WHERE fragment (and its bound params, in order) for `pruneClientMutations`'s DELETE —\n * the `seq <= ackedThrough OR createdAt < ttlBeforeMs` union (verdict §(c)). `{clause: null}` when\n * neither bound is set (nothing to delete this call — a legal no-op). */\nfunction clientMutationsDeleteClause(opts: {\n ackedThrough?: number;\n ttlBeforeMs?: number;\n}): { clause: string | null; params: number[] } {\n const parts: string[] = [];\n const params: number[] = [];\n if (opts.ackedThrough !== undefined) {\n parts.push(`seq <= ?`);\n params.push(opts.ackedThrough);\n }\n if (opts.ttlBeforeMs !== undefined) {\n parts.push(`created_at < ?`);\n params.push(opts.ttlBeforeMs);\n }\n return parts.length === 0 ? { clause: null, params: [] } : { clause: parts.join(\" OR \"), params };\n}\n\n/** The floor candidate a prune call covers: the client's own `ackedThrough` claim (which covers any\n * never-recorded holes below it — floor-covers-holes, verdict decision 3) and/or the highest seq\n * actually deleted this pass — whichever is higher. `null` when neither applies. */\nfunction maxCandidate(ackedThrough: number | null, deletedMaxSeq: number | null): number | null {\n if (ackedThrough === null) return deletedMaxSeq;\n if (deletedMaxSeq === null) return ackedThrough;\n return Math.max(ackedThrough, deletedMaxSeq);\n}\n\n/** Serialize + cap-check a receipt's optional value. Over-cap values are silently DROPPED (never\n * truncated, never rejected) — the receipt must still land (verdict §(c)); a dropped value reads\n * back as `hasValue: false`, mapping to the wire's `valueMissing`. */\nfunction cappedValueJson(value: JSONValue | undefined): string | null {\n if (value === undefined) return null;\n const json = JSON.stringify(value);\n return Buffer.byteLength(json, \"utf8\") > CLIENT_VERDICT_VALUE_CAP_BYTES ? null : json;\n}\n\nfunction clientVerdictRecordFromRow(row: SqlRow): ClientVerdictRecord {\n return {\n verdict: row.verdict as \"applied\" | \"failed\",\n commitTs: asBigInt(row.commit_ts),\n hasValue: row.value_json !== null,\n value: row.value_json === null ? null : (JSON.parse(row.value_json as string) as JSONValue),\n errorCode: (row.error_code as string | null | undefined) ?? null,\n createdAt: Number(row.created_at),\n };\n}\n\nexport class SqliteDocStore implements DocStore {\n private readonly stmtCache = new Map<string, PreparedStatement>();\n /** The commit-guard CHAIN (Receipted Outbox decision 2), the SQLite counterpart of\n * `PostgresDocStore.guards` — see `addCommitGuard`'s doc comment for the full contract. Empty\n * at Tier 0 and in every non-fleet/non-receipts deployment (no guard ever runs — SQLite pays\n * nothing for a feature it doesn't use). */\n private guards: SqliteCommitGuard[] = [];\n\n constructor(private readonly db: DatabaseAdapter) {}\n\n /** Append `guard` to the commit-guard chain — see `guards`'s doc comment. Guards run in\n * REGISTRATION ORDER, SYNCHRONOUSLY, inside `commitWriteBatch`'s one `db.transaction(() => …)`,\n * once per commit over the WHOLE unit array (never once per unit); ANY guard throwing aborts the\n * whole synchronous transaction (no unit lands) — SQLite's transaction wrapper already rolls\n * back on any thrown error, so this needs no special-casing here. A guard that returns a\n * thenable (i.e. is `async`) is a dev-time bug — see the check in `commitWriteBatch`. Returns an\n * unregister function that removes exactly this guard (a no-op if called again). */\n addCommitGuard(guard: SqliteCommitGuard): () => void {\n this.guards.push(guard);\n return () => {\n const i = this.guards.indexOf(guard);\n if (i >= 0) this.guards.splice(i, 1);\n };\n }\n\n /** The synchronous querier handed to every SQLite commit guard — routes through the same\n * prepared-statement cache (`this.prep`) every other method uses, so a guard's writes share\n * SQLite's statement caching for free. */\n private guardQuerier(): SqliteGuardQuerier {\n return {\n run: (sql, ...params) => {\n this.prep(sql).run(...(params as SqlValue[]));\n },\n get: (sql, ...params) => this.prep(sql).get(...(params as SqlValue[])),\n };\n }\n\n private prep(sql: string): PreparedStatement {\n let stmt = this.stmtCache.get(sql);\n if (!stmt) {\n stmt = this.db.prepare(sql);\n this.stmtCache.set(sql, stmt);\n }\n return stmt;\n }\n\n private serializeValue(value: DocumentValue): string {\n return JSON.stringify(convexToJson(value as Value));\n }\n private parseValue(text: string): DocumentValue {\n return jsonToConvex(JSON.parse(text) as JSONValue) as DocumentValue;\n }\n\n async setupSchema(_options?: SchemaSetupOptions): Promise<void> {\n this.db.exec(SCHEMA_SQL);\n // Additive `shard_id` column (Fenced Frontier B1, D6). `node:sqlite` has no\n // `ADD COLUMN IF NOT EXISTS`, so guard with a `pragma table_info` existence check — a\n // pre-B1 database upgrades in place and its old rows read as 'default' via the DEFAULT.\n for (const table of [\"documents\", \"indexes\"] as const) {\n // `table` is a fixed literal from this list — never user input, so interpolation is safe\n // (PRAGMA does not accept bound parameters for its argument).\n const cols = this.prep(`PRAGMA table_info(${table})`).all();\n if (!cols.some((c) => c.name === \"shard_id\")) {\n this.db.exec(`ALTER TABLE ${table} ADD COLUMN shard_id TEXT NOT NULL DEFAULT 'default'`);\n }\n }\n }\n\n /** Insert already-stamped document + index rows in the current transaction. Shared by `write()`\n * (caller-supplied timestamps) and `commitWrite()` (store-allocated), so the row-building /\n * column list lives in exactly one place. Must be called inside `this.db.transaction`. */\n private insertRows(\n documents: readonly DocumentLogEntry[],\n indexUpdates: readonly IndexWrite[],\n conflictStrategy: ConflictStrategy,\n shardId: ShardId,\n ): void {\n const docVerb = conflictStrategy === \"Overwrite\" ? \"INSERT OR REPLACE\" : \"INSERT\";\n const docStmt = this.prep(\n `${docVerb} INTO documents (table_id, internal_id, ts, prev_ts, value, shard_id) VALUES (?, ?, ?, ?, ?, ?)`,\n );\n const idxStmt = this.prep(\n `INSERT OR REPLACE INTO indexes (index_id, key, ts, table_id, internal_id, deleted, shard_id) VALUES (?, ?, ?, ?, ?, ?, ?)`,\n );\n for (const entry of documents) {\n docStmt.run(\n encodeStorageTableId(entry.id.tableNumber),\n entry.id.internalId,\n entry.ts,\n entry.prev_ts,\n entry.value === null ? null : this.serializeValue(entry.value.value),\n shardId,\n );\n }\n for (const { ts, update } of indexUpdates) {\n const v = update.value;\n idxStmt.run(\n update.indexId,\n update.key,\n ts,\n v.type === \"NonClustered\" ? encodeStorageTableId(v.docId.tableNumber) : null,\n v.type === \"NonClustered\" ? v.docId.internalId : null,\n v.type === \"NonClustered\" ? 0 : 1,\n shardId,\n );\n }\n }\n\n async write(\n documents: readonly DocumentLogEntry[],\n indexUpdates: readonly IndexWrite[],\n conflictStrategy: ConflictStrategy,\n shardId?: ShardId,\n ): Promise<void> {\n this.db.transaction(() => {\n this.insertRows(documents, indexUpdates, conflictStrategy, shardId ?? DEFAULT_SHARD);\n });\n }\n\n async commitWrite(\n documents: readonly DocumentLogEntry[],\n indexUpdates: readonly IndexWrite[],\n shardId?: ShardId,\n // Opaque commit metadata (Fleet B3, D3): threaded through to `commitWriteBatch`'s per-unit\n // `meta`, same as Postgres — SQLite now has a commit-guard chain too (Receipted Outbox\n // decision 2), so this is no longer inert. When no guard is registered (Tier 0, most\n // deployments) it costs nothing: the chain is empty and never runs.\n opts?: { meta?: Record<string, string> },\n ): Promise<bigint> {\n // Single commit = a one-unit batch (Fleet B4, D1) — one implementation, so `meta` reaches the\n // guard chain identically whether one or many units commit.\n const [ts] = await this.commitWriteBatch([{ documents, indexUpdates, meta: opts?.meta }], shardId);\n return ts!;\n }\n\n async commitWriteBatch(units: readonly CommitUnit[], shardId?: ShardId): Promise<bigint[]> {\n // Allocate + stamp + write the WHOLE batch in ONE synchronous transaction (Fleet B4, D1). Under\n // the single-writer invariant, `MAX(ts) + 1` computed inside the transaction is race-free: no other\n // writer can interleave a higher ts. Each unit re-reads MAX(ts), which now includes the prior\n // units' just-inserted rows, so the batch stamps CONSECUTIVE, strictly-increasing ts's in unit\n // order. Note: SQLite's flush is synchronous — nothing accumulates during it — so real batching\n // is opportunistic (typically batch-of-1) on Tier 0; the shared path is correct-but-inert here.\n return this.db.transaction(() => {\n const out: bigint[] = [];\n const guardUnits: CommitGuardUnit[] = [];\n const shard = shardId ?? DEFAULT_SHARD;\n for (const unit of units) {\n const row = this.prep(`SELECT MAX(ts) AS m FROM documents`).get();\n const m = row?.m;\n const commitTs = (m === null || m === undefined ? 0n : asBigInt(m)) + 1n;\n const stampedDocs = unit.documents.map((e) => ({ ...e, ts: commitTs }));\n const stampedIdx = unit.indexUpdates.map((w) => ({ ...w, ts: commitTs }));\n this.insertRows(stampedDocs, stampedIdx, \"Error\", shard);\n out.push(commitTs);\n guardUnits.push({ ts: commitTs, meta: unit.meta });\n }\n // The WHOLE chain, in registration order, ONE SYNCHRONOUS invocation each over the whole\n // batch — the sync mirror of Postgres's chain loop. Skipped for an empty batch. ANY guard\n // throwing propagates straight out of this `db.transaction(() => …)` callback, which rolls\n // the whole synchronous transaction back — no unit lands, exactly like an insert failing.\n if (guardUnits.length > 0) {\n const q = this.guardQuerier();\n for (const g of this.guards) {\n const ret = g(q, guardUnits, shard) as unknown;\n if (ret && typeof (ret as { then?: unknown }).then === \"function\") {\n throw new Error(\n \"[docstore-sqlite] a commit guard returned a Promise; SQLite guards must be \" +\n \"synchronous — its writes cannot be awaited inside the single-transaction commit\",\n );\n }\n }\n }\n return out;\n });\n }\n\n async get(id: InternalDocumentId, readTimestamp?: bigint): Promise<LatestDocument | null> {\n const tableId = encodeStorageTableId(id.tableNumber);\n const row =\n readTimestamp === undefined\n ? this.prep(\n `SELECT ts, prev_ts, value FROM documents WHERE table_id = ? AND internal_id = ? ORDER BY ts DESC LIMIT 1`,\n ).get(tableId, id.internalId)\n : this.prep(\n `SELECT ts, prev_ts, value FROM documents WHERE table_id = ? AND internal_id = ? AND ts <= ? ORDER BY ts DESC LIMIT 1`,\n ).get(tableId, id.internalId, readTimestamp);\n\n if (!row || row.value === null) return null; // missing or tombstone\n return {\n ts: asBigInt(row.ts),\n prev_ts: asBigIntOrNull(row.prev_ts),\n value: { id, value: this.parseValue(row.value as string) },\n };\n }\n\n async *index_scan(\n indexId: string,\n _tableId: string,\n readTimestamp: bigint,\n interval: Interval,\n order: Order,\n limit?: number,\n ): AsyncGenerator<readonly [Uint8Array, LatestDocument]> {\n const dir = order === \"desc\" ? \"DESC\" : \"ASC\";\n const params: SqlValue[] = [indexId, interval.start];\n let sql =\n `SELECT i.key AS key, i.table_id AS table_id, i.internal_id AS internal_id, i.deleted AS deleted ` +\n `FROM indexes i WHERE i.index_id = ? AND i.key >= ?`;\n if (interval.end !== null) {\n sql += ` AND i.key < ?`;\n params.push(interval.end);\n }\n sql += ` AND i.ts <= ? AND i.ts = (SELECT MAX(i2.ts) FROM indexes i2 WHERE i2.index_id = i.index_id AND i2.key = i.key AND i2.ts <= ?)`;\n params.push(readTimestamp, readTimestamp);\n sql += ` ORDER BY i.key ${dir}`;\n // NOTE: `limit` is applied AFTER skipping deletions/tombstones below — a SQL LIMIT would\n // count deleted index entries and return short pages.\n\n const rows = this.prep(sql).all(...params);\n let yielded = 0;\n for (const row of rows) {\n if (Number(row.deleted) === 1 || row.internal_id === null || row.table_id === null) continue;\n const docId: InternalDocumentId = {\n tableNumber: decodeStorageTableId(row.table_id as string),\n internalId: row.internal_id as Uint8Array,\n };\n const doc = await this.get(docId, readTimestamp);\n if (doc === null) continue; // resolved to a tombstone at this snapshot\n yield [row.key as Uint8Array, doc] as const;\n if (limit !== undefined && ++yielded >= limit) return;\n }\n }\n\n async *load_documents(\n range: TimestampRange,\n order: Order,\n limit?: number,\n ): AsyncGenerator<DocumentLogEntry> {\n const dir = order === \"desc\" ? \"DESC\" : \"ASC\";\n // A raw SQL LIMIT is correct here (unlike `index_scan`, which post-filters tombstones): the log\n // tail returns EVERY revision including tombstones, so no row is dropped after the LIMIT counts it.\n const limitSql = limit !== undefined ? ` LIMIT ${Math.max(0, Math.floor(limit))}` : \"\";\n const rows = this.prep(\n `SELECT table_id, internal_id, ts, prev_ts, value FROM documents WHERE ts >= ? AND ts < ? ` +\n `ORDER BY ts ${dir}, table_id ${dir}, internal_id ${dir}${limitSql}`,\n ).all(range.minInclusive, range.maxExclusive);\n\n for (const row of rows) {\n const id: InternalDocumentId = {\n tableNumber: decodeStorageTableId(row.table_id as string),\n internalId: row.internal_id as Uint8Array,\n };\n const value: ResolvedDocument | null =\n row.value === null ? null : { id, value: this.parseValue(row.value as string) };\n yield { ts: asBigInt(row.ts), id, value, prev_ts: asBigIntOrNull(row.prev_ts) };\n }\n }\n\n async previous_revisions(queries: readonly PrevRevQuery[]): Promise<Map<string, DocumentLogEntry>> {\n const out = new Map<string, DocumentLogEntry>();\n const stmt = this.prep(\n `SELECT ts, prev_ts, value FROM documents WHERE table_id = ? AND internal_id = ? AND ts <= ? ORDER BY ts DESC LIMIT 1`,\n );\n for (const q of queries) {\n const row = stmt.get(encodeStorageTableId(q.id.tableNumber), q.id.internalId, q.ts);\n if (!row) continue;\n const value: ResolvedDocument | null =\n row.value === null ? null : { id: q.id, value: this.parseValue(row.value as string) };\n out.set(getPrevRevQueryKey(q.id, q.ts), {\n ts: asBigInt(row.ts),\n id: q.id,\n value,\n prev_ts: asBigIntOrNull(row.prev_ts),\n });\n }\n return out;\n }\n\n async scan(tableId: string, readTimestamp?: bigint): Promise<LatestDocument[]> {\n const tableNumber = decodeStorageTableId(tableId);\n const rows: SqlRow[] =\n readTimestamp === undefined\n ? this.prep(\n `SELECT d.internal_id AS internal_id, d.ts AS ts, d.prev_ts AS prev_ts, d.value AS value FROM documents d ` +\n `WHERE d.table_id = ? AND d.ts = (SELECT MAX(d2.ts) FROM documents d2 WHERE d2.table_id = d.table_id AND d2.internal_id = d.internal_id) ` +\n `ORDER BY d.internal_id ASC`,\n ).all(tableId)\n : this.prep(\n `SELECT d.internal_id AS internal_id, d.ts AS ts, d.prev_ts AS prev_ts, d.value AS value FROM documents d ` +\n `WHERE d.table_id = ? AND d.ts <= ? AND d.ts = (SELECT MAX(d2.ts) FROM documents d2 WHERE d2.table_id = d.table_id AND d2.internal_id = d.internal_id AND d2.ts <= ?) ` +\n `ORDER BY d.internal_id ASC`,\n ).all(tableId, readTimestamp, readTimestamp);\n\n const out: LatestDocument[] = [];\n for (const row of rows) {\n if (row.value === null) continue; // tombstone\n const id: InternalDocumentId = { tableNumber, internalId: row.internal_id as Uint8Array };\n out.push({\n ts: asBigInt(row.ts),\n prev_ts: asBigIntOrNull(row.prev_ts),\n value: { id, value: this.parseValue(row.value as string) },\n });\n }\n return out;\n }\n\n async count(tableId: string): Promise<number> {\n const row = this.prep(\n `SELECT COUNT(*) AS n FROM ( ` +\n `SELECT d.value AS value FROM documents d WHERE d.table_id = ? AND d.ts = ` +\n `(SELECT MAX(d2.ts) FROM documents d2 WHERE d2.table_id = d.table_id AND d2.internal_id = d.internal_id) ` +\n `) WHERE value IS NOT NULL`,\n ).get(tableId);\n return Number(row?.n ?? 0);\n }\n\n async maxTimestamp(): Promise<bigint> {\n const row = this.prep(`SELECT MAX(ts) AS m FROM documents`).get();\n const m = row?.m;\n return m === null || m === undefined ? 0n : asBigInt(m);\n }\n\n async getGlobal(key: string): Promise<JSONValue | null> {\n const row = this.prep(`SELECT value FROM persistence_globals WHERE key = ?`).get(key);\n return row ? (JSON.parse(row.value as string) as JSONValue) : null;\n }\n\n async writeGlobal(key: string, value: JSONValue): Promise<void> {\n this.prep(`INSERT OR REPLACE INTO persistence_globals (key, value) VALUES (?, ?)`).run(\n key,\n JSON.stringify(value),\n );\n }\n\n async writeGlobalIfAbsent(key: string, value: JSONValue): Promise<boolean> {\n const r = this.prep(`INSERT OR IGNORE INTO persistence_globals (key, value) VALUES (?, ?)`).run(\n key,\n JSON.stringify(value),\n );\n return r.changes > 0;\n }\n\n // ── Client mutation receipts (the Receipted Outbox, verdict §(c)) ─────────────────────────────\n\n async getClientVerdict(identity: string, clientId: string, seq: number): Promise<ClientVerdictRecord | null> {\n const row = this.prep(\n `SELECT verdict, commit_ts, value_json, error_code, created_at FROM client_mutations\n WHERE identity = ? AND client_id = ? AND seq = ?`,\n ).get(identity, clientId, seq);\n if (!row) return null;\n return clientVerdictRecordFromRow(row);\n }\n\n async getClientFloor(identity: string, clientId: string): Promise<number | null> {\n const row = this.prep(\n `SELECT pruned_through_seq FROM client_floors WHERE identity = ? AND client_id = ?`,\n ).get(identity, clientId);\n return row ? Number(row.pruned_through_seq) : null;\n }\n\n async recordClientVerdict(identity: string, clientId: string, seq: number, record: ClientVerdictWrite): Promise<void> {\n const valueJson = cappedValueJson(record.value);\n this.prep(\n `INSERT OR IGNORE INTO client_mutations\n (identity, client_id, seq, verdict, commit_ts, value_json, error_code, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n identity,\n clientId,\n seq,\n record.verdict,\n record.commitTs,\n valueJson,\n record.verdict === \"failed\" ? record.errorCode : null,\n Date.now(),\n );\n }\n\n async updateClientVerdictValue(identity: string, clientId: string, seq: number, value: JSONValue): Promise<void> {\n const valueJson = cappedValueJson(value);\n this.prep(\n `UPDATE client_mutations SET value_json = ? WHERE identity = ? AND client_id = ? AND seq = ?`,\n ).run(valueJson, identity, clientId, seq);\n }\n\n async pruneClientMutations(\n identity: string,\n clientId: string,\n opts: { ackedThrough?: number; ttlBeforeMs?: number },\n ): Promise<{ prunedThroughSeq: number }> {\n return this.db.transaction(() => {\n const currentFloorRow = this.prep(\n `SELECT pruned_through_seq FROM client_floors WHERE identity = ? AND client_id = ?`,\n ).get(identity, clientId);\n const currentFloor = currentFloorRow ? Number(currentFloorRow.pruned_through_seq) : null;\n\n let deletedMaxSeq: number | null = null;\n const { clause, params } = clientMutationsDeleteClause(opts);\n if (clause !== null) {\n const rows = this.prep(\n `DELETE FROM client_mutations WHERE identity = ? AND client_id = ? AND (${clause}) RETURNING seq`,\n ).all(identity, clientId, ...params);\n for (const row of rows) {\n const s = Number(row.seq);\n if (deletedMaxSeq === null || s > deletedMaxSeq) deletedMaxSeq = s;\n }\n }\n\n const candidate = maxCandidate(opts.ackedThrough ?? null, deletedMaxSeq);\n const base = currentFloor ?? -1;\n if (candidate === null || candidate <= base) {\n return { prunedThroughSeq: currentFloor ?? 0 }; // no-op: nothing to advance to\n }\n this.prep(\n `INSERT INTO client_floors (identity, client_id, pruned_through_seq, updated_at) VALUES (?, ?, ?, ?)\n ON CONFLICT (identity, client_id) DO UPDATE SET\n pruned_through_seq = MAX(client_floors.pruned_through_seq, excluded.pruned_through_seq),\n updated_at = excluded.updated_at`,\n ).run(identity, clientId, candidate, Date.now());\n return { prunedThroughSeq: candidate };\n });\n }\n\n async sweepExpiredClientMutations(beforeMs: number): Promise<{ deletedCount: number }> {\n return this.db.transaction(() => {\n const rows = this.prep(\n `DELETE FROM client_mutations WHERE created_at < ? RETURNING identity, client_id, seq`,\n ).all(beforeMs);\n if (rows.length === 0) return { deletedCount: 0 };\n\n const maxByClient = new Map<string, { identity: string; clientId: string; maxSeq: number }>();\n for (const row of rows) {\n const identity = row.identity as string;\n const clientId = row.client_id as string;\n const seq = Number(row.seq);\n // NUL-delimited: identity/clientId are client-supplied strings, so an unescaped join\n // (e.g. a plain space) lets (\"a\",\"b c\") and (\"a b\",\"c\") collide onto the same batch key.\n const key = `${identity}\u0000${clientId}`;\n const cur = maxByClient.get(key);\n if (!cur || seq > cur.maxSeq) maxByClient.set(key, { identity, clientId, maxSeq: seq });\n }\n const now = Date.now();\n const upsert = this.prep(\n `INSERT INTO client_floors (identity, client_id, pruned_through_seq, updated_at) VALUES (?, ?, ?, ?)\n ON CONFLICT (identity, client_id) DO UPDATE SET\n pruned_through_seq = MAX(client_floors.pruned_through_seq, excluded.pruned_through_seq),\n updated_at = excluded.updated_at`,\n );\n for (const { identity, clientId, maxSeq } of maxByClient.values()) upsert.run(identity, clientId, maxSeq, now);\n return { deletedCount: rows.length };\n });\n }\n\n /**\n * The store's CURRENT state (Tier 3 Slice 3, Task 3.1 — the snapshot source): for every document\n * id across every table, its LATEST revision, EXCLUDING ids whose latest revision is a tombstone\n * (`value === null`) — mirrors `scan()`'s per-table \"newest revision per id\" query, just without\n * the `table_id` filter, so it spans the whole store in one pass. Plus every CURRENT row of the\n * `indexes` table (the newest revision per `(index_id, key)`, live pointer OR deletion marker\n * alike — mirrors `index_scan()`'s own `MAX(ts)`-per-key subquery, but unlike `index_scan` this\n * does NOT skip deleted markers: the snapshot must reproduce the index table's own current rows\n * exactly, not the documents they resolve to).\n *\n * Each returned `DocumentLogEntry`/`IndexWrite` carries its REAL `ts`/`prev_ts` (not renumbered) —\n * `ObjectStoreDocStore.snapshot()` stamps the payload's own `frontierTs`/`segBase` around this, and\n * restoring via `write(dump.documents, dump.indexUpdates, \"Overwrite\")` on a fresh store reproduces\n * this exact state, with `prev_ts` chains intact so a tail segment's `prev_ts` still resolves.\n */\n async dumpCurrentState(): Promise<{ documents: DocumentLogEntry[]; indexUpdates: IndexWrite[] }> {\n const docRows = this.prep(\n `SELECT d.table_id AS table_id, d.internal_id AS internal_id, d.ts AS ts, d.prev_ts AS prev_ts, d.value AS value FROM documents d ` +\n `WHERE d.ts = (SELECT MAX(d2.ts) FROM documents d2 WHERE d2.table_id = d.table_id AND d2.internal_id = d.internal_id) ` +\n `AND d.value IS NOT NULL ORDER BY d.table_id ASC, d.internal_id ASC`,\n ).all();\n\n const documents: DocumentLogEntry[] = docRows.map((row) => {\n const id: InternalDocumentId = {\n tableNumber: decodeStorageTableId(row.table_id as string),\n internalId: row.internal_id as Uint8Array,\n };\n const value: ResolvedDocument = { id, value: this.parseValue(row.value as string) };\n return { ts: asBigInt(row.ts), id, value, prev_ts: asBigIntOrNull(row.prev_ts) };\n });\n\n const idxRows = this.prep(\n `SELECT i.index_id AS index_id, i.key AS key, i.ts AS ts, i.table_id AS table_id, i.internal_id AS internal_id, i.deleted AS deleted FROM indexes i ` +\n `WHERE i.ts = (SELECT MAX(i2.ts) FROM indexes i2 WHERE i2.index_id = i.index_id AND i2.key = i.key) ` +\n `ORDER BY i.index_id ASC, i.key ASC`,\n ).all();\n\n const indexUpdates: IndexWrite[] = idxRows.map((row) => {\n const deleted = Number(row.deleted) === 1;\n const value: DatabaseIndexUpdate[\"value\"] = deleted\n ? { type: \"Deleted\" }\n : {\n type: \"NonClustered\",\n docId: {\n tableNumber: decodeStorageTableId(row.table_id as string),\n internalId: row.internal_id as Uint8Array,\n },\n };\n return {\n ts: asBigInt(row.ts),\n update: { indexId: row.index_id as string, key: row.key as Uint8Array, value },\n };\n });\n\n return { documents, indexUpdates };\n }\n\n /** Close the underlying database adapter (checkpoint + release the file). Used by graceful shutdown. */\n close(): void {\n this.db.close();\n }\n}\n"],"mappings":"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","names":["createRequire"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@helipod/docstore-sqlite",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:bun": "bun run test/bun-smoke.ts",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"clean": "rm -rf dist .turbo"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@helipod/docstore": "0.1.0",
|
|
28
|
+
"@helipod/id-codec": "0.1.0",
|
|
29
|
+
"@helipod/values": "0.1.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@helipod/index-key-codec": "0.1.0",
|
|
33
|
+
"@types/node": "^22.10.5",
|
|
34
|
+
"tsup": "^8.3.5",
|
|
35
|
+
"typescript": "^5.7.2",
|
|
36
|
+
"vitest": "^2.1.8"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/helipod-sh/helipod.git",
|
|
44
|
+
"directory": "packages/docstore-sqlite"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/helipod-sh/helipod"
|
|
47
|
+
}
|