@bitcobblers/wod-wiki-storage 0.35.2116 → 0.35.2119
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/{chunk-DAGT3P7M.mjs → chunk-7ZCBM5NS.mjs} +118 -36
- package/dist/chunk-7ZCBM5NS.mjs.map +1 -0
- package/dist/index.cjs +182 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -5
- package/dist/index.d.ts +12 -5
- package/dist/index.mjs +62 -16
- package/dist/index.mjs.map +1 -1
- package/dist/sql/index.cjs +821 -289
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +172 -62
- package/dist/sql/index.d.ts +172 -62
- package/dist/sql/index.mjs +726 -273
- package/dist/sql/index.mjs.map +1 -1
- package/dist/wire-CiFcjo6_.d.cts +207 -0
- package/dist/wire-CiFcjo6_.d.ts +207 -0
- package/package.json +6 -9
- package/dist/chunk-DAGT3P7M.mjs.map +0 -1
- package/dist/wire-C7ZtlBKe.d.cts +0 -489
- package/dist/wire-C7ZtlBKe.d.ts +0 -489
package/dist/sql/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Client } from '@libsql/client';
|
|
2
|
-
import { a as StoreDef, R as RangeDTO, K as KeyDTO, e as DomainQueryResult,
|
|
2
|
+
import { a as StoreDef, R as RangeDTO, K as KeyDTO, e as DomainQueryResult, T as TxOp, I as IStorage, S as StoreName, b as IReadOnlyStore, c as StoreType, d as IReadWriteStore, D as DomainQuery, f as IStorageTransaction } from '../wire-CiFcjo6_.cjs';
|
|
3
3
|
import '@bitcobblers/wod-wiki-core';
|
|
4
4
|
import '@bitcobblers/wod-wiki-lang';
|
|
5
5
|
|
|
@@ -30,9 +30,14 @@ declare class StorageError extends Error {
|
|
|
30
30
|
|
|
31
31
|
type SqlValue = string | number | null;
|
|
32
32
|
type SqlRow = Record<string, unknown>;
|
|
33
|
+
interface SqlStatement {
|
|
34
|
+
sql: string;
|
|
35
|
+
params?: SqlValue[];
|
|
36
|
+
}
|
|
33
37
|
interface SqlConnection {
|
|
34
38
|
query(sql: string, params?: SqlValue[]): Promise<SqlRow[]>;
|
|
35
39
|
exec(sql: string, params?: SqlValue[]): Promise<void>;
|
|
40
|
+
batch?(statements: SqlStatement[]): Promise<SqlRow[][]>;
|
|
36
41
|
}
|
|
37
42
|
interface Database extends SqlConnection {
|
|
38
43
|
readonly dialect: 'sqlite' | 'postgres';
|
|
@@ -57,8 +62,6 @@ declare function libsqlDatabase(client: LibsqlClient): Database;
|
|
|
57
62
|
* is typed `ArrayBuffer | string`; strings pass through, and only the exact
|
|
58
63
|
* two-key tag revives.
|
|
59
64
|
*/
|
|
60
|
-
/** Serialize a row; when `binaryField` is set and holds an ArrayBuffer, it is
|
|
61
|
-
* replaced by the lossless tag (shallow copy — the input row is untouched). */
|
|
62
65
|
declare function encodeRowJson(value: unknown, binaryField?: string): string;
|
|
63
66
|
/** Parse a row; when `binaryField` is set, a tagged value there revives as an
|
|
64
67
|
* ArrayBuffer. Every other value — including tag-shaped data in other fields
|
|
@@ -76,55 +79,34 @@ declare function decodeRowJson<T = unknown>(text: string, binaryField?: string):
|
|
|
76
79
|
declare const SCHEMA_TABLE = "_wodwiki_schema";
|
|
77
80
|
declare function storeDefinition(name: string): StoreDef;
|
|
78
81
|
declare function getRow(conn: SqlConnection, def: StoreDef, key: KeyDTO): Promise<unknown | undefined>;
|
|
82
|
+
declare function getRows(conn: SqlConnection, def: StoreDef, keys: KeyDTO[]): Promise<(unknown | undefined)[]>;
|
|
83
|
+
declare function listRowsFromIndexMany(conn: SqlConnection, def: StoreDef, indexName: string, keys: KeyDTO[]): Promise<unknown[][]>;
|
|
79
84
|
declare function listRows(conn: SqlConnection, def: StoreDef, range?: RangeDTO, count?: number, indexName?: string): Promise<unknown[]>;
|
|
80
85
|
declare function countRows(conn: SqlConnection, def: StoreDef, range?: RangeDTO, indexName?: string): Promise<number>;
|
|
86
|
+
declare function putRows(conn: SqlConnection, def: StoreDef, values: unknown[], changedKeys?: KeyDTO[]): Promise<KeyDTO[]>;
|
|
81
87
|
declare function putRow(conn: SqlConnection, def: StoreDef, value: unknown): Promise<KeyDTO>;
|
|
82
88
|
declare function deleteRows(conn: SqlConnection, def: StoreDef, keyOrRange: KeyDTO | RangeDTO): Promise<void>;
|
|
83
|
-
declare function clearRows(conn: SqlConnection, def: StoreDef): Promise<void>;
|
|
89
|
+
declare function clearRows(conn: SqlConnection, def: StoreDef, wipe?: boolean): Promise<void>;
|
|
84
90
|
/** Wipe all user stores; system stores (memberships) survive and the internal
|
|
85
91
|
* schema metadata table is not a store, so the version guard is untouched. */
|
|
86
92
|
declare function wipeRows(conn: SqlConnection): Promise<void>;
|
|
87
|
-
/** Internal schema metadata is not exposed or cleared by the store API.
|
|
93
|
+
/** Internal schema metadata is not exposed or cleared by the store API.
|
|
94
|
+
*
|
|
95
|
+
* Version policy: the SQL DDL derives entirely from STORE_DEFS, so two
|
|
96
|
+
* schema versions are compatible iff their derived table sets are equal.
|
|
97
|
+
* - stored > DB_VERSION → refuse (never downgrade).
|
|
98
|
+
* - stored < DB_VERSION with EQUAL table sets (v29 → v30: defs unchanged,
|
|
99
|
+
* only browser-local sync stores were added) → bump the guard in place;
|
|
100
|
+
* no row is read, rewritten or erased.
|
|
101
|
+
* - stored < DB_VERSION with different tables → fail closed with the same
|
|
102
|
+
* actionable schema_incompatible error as before.
|
|
103
|
+
* - no guard + store tables → foreign database, refuse. */
|
|
88
104
|
declare function initializeStore(db: Database): Promise<void>;
|
|
89
105
|
|
|
90
|
-
/**
|
|
91
|
-
* Domain read models + incremental projection refresh over the shared SQL
|
|
92
|
-
* store tables.
|
|
93
|
-
*
|
|
94
|
-
* Three derived tables, rebuilt from the per-store source tables (source of
|
|
95
|
-
* truth) when incompatible and refreshed per affected key inside the same
|
|
96
|
-
* transaction as every mutation:
|
|
97
|
-
* domain_note — note metadata selector + original payload
|
|
98
|
-
* query_block — one occurrence row per block_index row, parent resolved by
|
|
99
|
-
* exact noteId or sourcePath (ambiguous/orphan → NULL parent,
|
|
100
|
-
* which gates ALL domain reads with 409 until healed)
|
|
101
|
-
* segment_head — MAX(version) per segment id taken BEFORE the isHistory
|
|
102
|
-
* filter, so a superseded revision can never resurrect
|
|
103
|
-
*
|
|
104
|
-
* Rebuilds are guarded: a `_wodwiki_schema` row stamps the projection
|
|
105
|
-
* version, so repeated ready()/boot cycles skip the rebuild while the
|
|
106
|
-
* derived shape is already compatible.
|
|
107
|
-
*
|
|
108
|
-
* Query plans are fixed and fully validated: every value is a bound parameter,
|
|
109
|
-
* names come from this module, counts run before paging, and text matching is
|
|
110
|
-
* a literal substring over JS-lowercased materialized columns (Unicode-exact;
|
|
111
|
-
* instr/strpos never treat % or _ as wildcards).
|
|
112
|
-
*
|
|
113
|
-
* `journal` is materialized in JS (sourceMatches from the wql package — the
|
|
114
|
-
* exact predicate the app applies) so no SQL regex re-implementation exists.
|
|
115
|
-
*/
|
|
106
|
+
/** Current Note and actionable-block SQL projections. */
|
|
116
107
|
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
* refreshes derived rows — all on the caller's transaction. Returns one entry
|
|
120
|
-
* per op: the put key, or null for delete/clear. `opts.deferProjection`
|
|
121
|
-
* skips the per-key refresh pass; the caller MUST rebuild derived state in
|
|
122
|
-
* the same transaction afterwards (see rebuildProjections) or reads will
|
|
123
|
-
* operate on stale projections behind a valid guard.
|
|
124
|
-
*/
|
|
125
|
-
declare function refreshDomain(conn: SqlConnection, ops: TxOp[], opts?: {
|
|
126
|
-
deferProjection?: boolean;
|
|
127
|
-
}): Promise<(KeyDTO | null)[]>;
|
|
108
|
+
/** Checks precede mutations; pins and projections commit with source rows. */
|
|
109
|
+
declare function refreshDomain(conn: SqlConnection, ops: TxOp[]): Promise<(KeyDTO | null)[]>;
|
|
128
110
|
/**
|
|
129
111
|
* Rebuild the derived tables from the source-of-truth store rows and stamp
|
|
130
112
|
* the projection guard. Caller owns the transaction (atomicity) — the guard
|
|
@@ -141,20 +123,7 @@ declare const MAX_BODY: number;
|
|
|
141
123
|
* SqlStorage.queryDomain share this entry). */
|
|
142
124
|
declare function handleDomainQuery(db: Database, body: unknown): Promise<DomainQueryResult>;
|
|
143
125
|
|
|
144
|
-
/**
|
|
145
|
-
* SqlStorage — IStorage over the shared SQL store operations (server or
|
|
146
|
-
* browser). Transactions buffer writes and flush them as one refreshDomain
|
|
147
|
-
* pass inside a real SQL transaction: throw → rollback, success → commit,
|
|
148
|
-
* and buffered reads overlay committed rows (read-your-writes). Projections
|
|
149
|
-
* refresh inside the same transaction; bulkTransaction() batches a migration
|
|
150
|
-
* into one rebuild instead of per-key refreshes. verifyWritable() proves the
|
|
151
|
-
* backend accepts writes with a real INSERT that is rolled back.
|
|
152
|
-
*
|
|
153
|
-
* BrowserTursoStorage opens a direct-to-Turso backend via @libsql/client/web;
|
|
154
|
-
* the constructor only builds the client (HTTP is connectionless), `ready()`
|
|
155
|
-
* initializes schema + projections and every operation awaits it, so a
|
|
156
|
-
* transient init failure never wedges the backend — ready() retries.
|
|
157
|
-
*/
|
|
126
|
+
/** SQL storage with atomic, change-scoped projections and buffered reads. */
|
|
158
127
|
|
|
159
128
|
interface SqlStorageOptions {
|
|
160
129
|
/** Rows in user-owned stores are stamped with this userId on write. Omit
|
|
@@ -182,11 +151,7 @@ declare class SqlStorage implements IStorage {
|
|
|
182
151
|
readwrite<K extends StoreName>(store: K): IReadWriteStore<StoreType<K>>;
|
|
183
152
|
queryDomain(query: DomainQuery): Promise<DomainQueryResult | undefined>;
|
|
184
153
|
transaction<K extends StoreName, R>(stores: K[], mode: 'readonly' | 'readwrite', fn: (tx: IStorageTransaction) => Promise<R>): Promise<R>;
|
|
185
|
-
/**
|
|
186
|
-
* Bulk load for migration destinations: like transaction(), but the
|
|
187
|
-
* buffered writes skip per-key projection refresh and derived state is
|
|
188
|
-
* rebuilt once — everything inside one SQL transaction (throw → rollback).
|
|
189
|
-
*/
|
|
154
|
+
/** Bulk writes use the same affected-key refresh as ordinary transactions. */
|
|
190
155
|
bulkTransaction<K extends StoreName, R>(stores: K[], fn: (tx: IStorageTransaction) => Promise<R>): Promise<R>;
|
|
191
156
|
private runTx;
|
|
192
157
|
/**
|
|
@@ -217,4 +182,149 @@ declare class BrowserTursoStorage extends SqlStorage {
|
|
|
217
182
|
constructor(settings: TursoSettings, options?: SqlStorageOptions);
|
|
218
183
|
}
|
|
219
184
|
|
|
220
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Turso backup/sync protocol — the local-first sync tables layered on top of
|
|
187
|
+
* any libsql database. The browser data plane is always IndexedDB; a saved
|
|
188
|
+
* Turso target is a backup/sync destination, never the read/write provider.
|
|
189
|
+
*
|
|
190
|
+
* Design: the remote holds ONE op log (`_wodwiki_sync_ops`) plus a state row
|
|
191
|
+
* (`_wodwiki_sync_state`) carrying {schema, rev}. Every committed local
|
|
192
|
+
* transaction group appends one idempotent op-group (uuid op_id,
|
|
193
|
+
* INSERT-or-ignore per op); `rev` is assigned monotonically per NEWLY applied
|
|
194
|
+
* group — a retried group is recognized by its first op id, consumes no rev
|
|
195
|
+
* and appends nothing. Pulls replay ops with rev > cursor in deterministic
|
|
196
|
+
* (rev, idx) order. Tombstones (delete/clear/wipe) are ordinary ops —
|
|
197
|
+
* deletions propagate.
|
|
198
|
+
*
|
|
199
|
+
* Fetch atomicity: a pull window is a set of WHOLE revision groups. The rev
|
|
200
|
+
* boundary is chosen with a bounded subquery, then every row of those revs is
|
|
201
|
+
* returned — a group is never split, so the cursor never skips the rest of
|
|
202
|
+
* a group (one pathologically large group can exceed the nominal row bound;
|
|
203
|
+
* that is the unavoidable atomicity ceiling).
|
|
204
|
+
*
|
|
205
|
+
* ponytail: op-log replay pulls — a fresh browser replays the whole log
|
|
206
|
+
* (O(history)) instead of reading per-store current-state tables. Upgrade
|
|
207
|
+
* path: add per-store state tables rev-indexed by primary key and collapse
|
|
208
|
+
* the log into them periodically; the wire format (op_id, rev, mutation)
|
|
209
|
+
* stays unchanged.
|
|
210
|
+
*
|
|
211
|
+
* Never-downgrade rule: the state row's `schema` is the managing client's
|
|
212
|
+
* DB_VERSION at init time. Uploads verify `schema` INSIDE the write
|
|
213
|
+
* transaction (a stale probe must not append current payloads to an old or
|
|
214
|
+
* newer remote); an older remote log is migrated forward by the caller
|
|
215
|
+
* (transform payloads, never just rewrite the marker), a newer remote is
|
|
216
|
+
* never rewritten — the client refuses and keeps working locally.
|
|
217
|
+
*/
|
|
218
|
+
|
|
219
|
+
declare const SYNC_OPS_TABLE = "_wodwiki_sync_ops";
|
|
220
|
+
declare const SYNC_STATE_TABLE = "_wodwiki_sync_state";
|
|
221
|
+
/** Pre-migration op log copies live here — the recoverable old source,
|
|
222
|
+
* retained after a schema migration replaces the active log. */
|
|
223
|
+
declare const SYNC_ARCHIVE_TABLE = "_wodwiki_sync_ops_archive";
|
|
224
|
+
/** One committed local transaction group, as journaled in IndexedDB. */
|
|
225
|
+
type SyncMutation = {
|
|
226
|
+
op: 'put';
|
|
227
|
+
store: StoreName;
|
|
228
|
+
key: KeyDTO;
|
|
229
|
+
value: unknown;
|
|
230
|
+
} | {
|
|
231
|
+
op: 'delete';
|
|
232
|
+
store: StoreName;
|
|
233
|
+
key: KeyDTO;
|
|
234
|
+
} | {
|
|
235
|
+
op: 'clear';
|
|
236
|
+
store: StoreName;
|
|
237
|
+
} | {
|
|
238
|
+
op: 'wipe';
|
|
239
|
+
};
|
|
240
|
+
interface SyncEntry {
|
|
241
|
+
/** Idempotency key — one uuid per committed local transaction group. */
|
|
242
|
+
id: string;
|
|
243
|
+
at: number;
|
|
244
|
+
ops: SyncMutation[];
|
|
245
|
+
}
|
|
246
|
+
interface StoredSyncOp {
|
|
247
|
+
opId: string;
|
|
248
|
+
rev: number;
|
|
249
|
+
idx: number;
|
|
250
|
+
at: number;
|
|
251
|
+
mutation: SyncMutation;
|
|
252
|
+
}
|
|
253
|
+
type SyncTargetKind =
|
|
254
|
+
/** No tables at all (beyond libsql internals): fresh database. */
|
|
255
|
+
'empty'
|
|
256
|
+
/** Tables exist but neither sync state nor a legacy schema guard — not
|
|
257
|
+
* ours; initializing over unknown data is refused by the caller. */
|
|
258
|
+
| {
|
|
259
|
+
kind: 'unknown';
|
|
260
|
+
}
|
|
261
|
+
/** Legacy direct-Turso SqlStorage layout (`_wodwiki_schema` guard). */
|
|
262
|
+
| {
|
|
263
|
+
kind: 'legacy';
|
|
264
|
+
schemaVersion: number;
|
|
265
|
+
}
|
|
266
|
+
/** This sync protocol's layout; payload = state row. */
|
|
267
|
+
| {
|
|
268
|
+
kind: 'current';
|
|
269
|
+
schemaVersion: number;
|
|
270
|
+
rev: number;
|
|
271
|
+
};
|
|
272
|
+
/** Classify the sync target WITHOUT mutating it. */
|
|
273
|
+
declare function probeSyncTarget(db: Database): Promise<SyncTargetKind>;
|
|
274
|
+
/** Create the sync tables + state row. Only for empty targets; existing
|
|
275
|
+
* state (current, legacy or unknown) is never overwritten by init. */
|
|
276
|
+
declare function initSyncTarget(db: Database, schemaVersion: number): Promise<void>;
|
|
277
|
+
interface ApplySyncBatchOptions {
|
|
278
|
+
/** Schema the caller verified at probe time — rechecked INSIDE the write
|
|
279
|
+
* transaction so a schema change between probe and append aborts. */
|
|
280
|
+
expectSchema?: number;
|
|
281
|
+
/** Bump the state schema version in the SAME transaction (remote-schema
|
|
282
|
+
* migrations append converted payloads + move the marker atomically). */
|
|
283
|
+
toSchemaVersion?: number;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Append op-groups to the log in ONE SQL transaction, idempotently by
|
|
287
|
+
* group: a group whose first op id already exists is skipped entirely and
|
|
288
|
+
* consumes no rev (retries never duplicate, never allocate revisions).
|
|
289
|
+
* Returns the number of NEW groups committed — ack semantics for the
|
|
290
|
+
* uploader's watermark.
|
|
291
|
+
*/
|
|
292
|
+
declare function applySyncBatch(db: Database, entries: SyncEntry[], opts?: ApplySyncBatchOptions): Promise<number>;
|
|
293
|
+
/**
|
|
294
|
+
* Replay window: ops with rev > afterRev, in deterministic commit order.
|
|
295
|
+
* The window covers WHOLE revision groups: up to `limit` distinct revs are
|
|
296
|
+
* chosen (bounded subquery), then every row of those revs is returned, so a
|
|
297
|
+
* cursor advanced to the last returned rev never splits a group. A single
|
|
298
|
+
* group larger than `limit` is returned whole — the atomicity ceiling.
|
|
299
|
+
*/
|
|
300
|
+
declare function fetchSyncOps(db: Database, afterRev: number, limit: number): Promise<StoredSyncOp[]>;
|
|
301
|
+
/**
|
|
302
|
+
* Schema-migration replacement for the active op log, in ONE transaction:
|
|
303
|
+
* every previous op is copied into the archive table (the recoverable old
|
|
304
|
+
* source — nothing is destroyed), the active log is cleared, and the
|
|
305
|
+
* converted snapshot groups become the entire replayable history at
|
|
306
|
+
* rev 1..n with the schema marker moved forward atomically. Fresh browsers
|
|
307
|
+
* replay converted payloads only, so pulls never touch legacy row shapes.
|
|
308
|
+
*/
|
|
309
|
+
declare function replaceSyncLog(db: Database, entries: SyncEntry[], toSchemaVersion: number): Promise<void>;
|
|
310
|
+
/** Migration-only raw replay: parsed op payloads WITHOUT store/key validation
|
|
311
|
+
* (old logs reference era store names like `segments`). Only
|
|
312
|
+
* migrateRemoteToCurrent consumes this — pulls use fetchSyncOps, which
|
|
313
|
+
* validates every row against the current schema. Binary cells revive. */
|
|
314
|
+
interface RawSyncOp {
|
|
315
|
+
op: string;
|
|
316
|
+
store: string;
|
|
317
|
+
key: unknown;
|
|
318
|
+
value?: unknown;
|
|
319
|
+
}
|
|
320
|
+
declare function readRawSyncOps(db: Database): Promise<RawSyncOp[]>;
|
|
321
|
+
/**
|
|
322
|
+
* Import rows from a legacy direct-Turso (SqlStorage) database into op-
|
|
323
|
+
* groups. Reads the per-store JSON `data` columns directly — the legacy
|
|
324
|
+
* schema guard may hold an older version SqlStorage refuses to open, so the
|
|
325
|
+
* rows are lifted raw, never through SqlStorage. Missing tables contribute
|
|
326
|
+
* no rows; nothing is written or deleted remotely.
|
|
327
|
+
*/
|
|
328
|
+
declare function readLegacyRows(db: Database, table: string): Promise<unknown[]>;
|
|
329
|
+
|
|
330
|
+
export { type ApplySyncBatchOptions, BrowserTursoStorage, type Database, type LibsqlClient, MAX_BODY, type RawSyncOp, SCHEMA_TABLE, SYNC_ARCHIVE_TABLE, SYNC_OPS_TABLE, SYNC_STATE_TABLE, type SqlConnection, type SqlRow, type SqlStatement, SqlStorage, type SqlStorageOptions, type SqlValue, StorageError, type StoredSyncOp, type SyncEntry, type SyncMutation, type SyncTargetKind, type TursoSettings, applySyncBatch, clearRows, countRows, decodeRowJson, deleteRows, encodeRowJson, fetchSyncOps, getRow, getRows, handleDomainQuery, initSyncTarget, initializeDomain, initializeStore, libsqlDatabase, listRows, listRowsFromIndexMany, probeSyncTarget, putRow, putRows, readLegacyRows, readRawSyncOps, rebuildProjections, refreshDomain, replaceSyncLog, storeDefinition, validateTursoUrl, wipeDomain, wipeRows };
|
package/dist/sql/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Client } from '@libsql/client';
|
|
2
|
-
import { a as StoreDef, R as RangeDTO, K as KeyDTO, e as DomainQueryResult,
|
|
2
|
+
import { a as StoreDef, R as RangeDTO, K as KeyDTO, e as DomainQueryResult, T as TxOp, I as IStorage, S as StoreName, b as IReadOnlyStore, c as StoreType, d as IReadWriteStore, D as DomainQuery, f as IStorageTransaction } from '../wire-CiFcjo6_.js';
|
|
3
3
|
import '@bitcobblers/wod-wiki-core';
|
|
4
4
|
import '@bitcobblers/wod-wiki-lang';
|
|
5
5
|
|
|
@@ -30,9 +30,14 @@ declare class StorageError extends Error {
|
|
|
30
30
|
|
|
31
31
|
type SqlValue = string | number | null;
|
|
32
32
|
type SqlRow = Record<string, unknown>;
|
|
33
|
+
interface SqlStatement {
|
|
34
|
+
sql: string;
|
|
35
|
+
params?: SqlValue[];
|
|
36
|
+
}
|
|
33
37
|
interface SqlConnection {
|
|
34
38
|
query(sql: string, params?: SqlValue[]): Promise<SqlRow[]>;
|
|
35
39
|
exec(sql: string, params?: SqlValue[]): Promise<void>;
|
|
40
|
+
batch?(statements: SqlStatement[]): Promise<SqlRow[][]>;
|
|
36
41
|
}
|
|
37
42
|
interface Database extends SqlConnection {
|
|
38
43
|
readonly dialect: 'sqlite' | 'postgres';
|
|
@@ -57,8 +62,6 @@ declare function libsqlDatabase(client: LibsqlClient): Database;
|
|
|
57
62
|
* is typed `ArrayBuffer | string`; strings pass through, and only the exact
|
|
58
63
|
* two-key tag revives.
|
|
59
64
|
*/
|
|
60
|
-
/** Serialize a row; when `binaryField` is set and holds an ArrayBuffer, it is
|
|
61
|
-
* replaced by the lossless tag (shallow copy — the input row is untouched). */
|
|
62
65
|
declare function encodeRowJson(value: unknown, binaryField?: string): string;
|
|
63
66
|
/** Parse a row; when `binaryField` is set, a tagged value there revives as an
|
|
64
67
|
* ArrayBuffer. Every other value — including tag-shaped data in other fields
|
|
@@ -76,55 +79,34 @@ declare function decodeRowJson<T = unknown>(text: string, binaryField?: string):
|
|
|
76
79
|
declare const SCHEMA_TABLE = "_wodwiki_schema";
|
|
77
80
|
declare function storeDefinition(name: string): StoreDef;
|
|
78
81
|
declare function getRow(conn: SqlConnection, def: StoreDef, key: KeyDTO): Promise<unknown | undefined>;
|
|
82
|
+
declare function getRows(conn: SqlConnection, def: StoreDef, keys: KeyDTO[]): Promise<(unknown | undefined)[]>;
|
|
83
|
+
declare function listRowsFromIndexMany(conn: SqlConnection, def: StoreDef, indexName: string, keys: KeyDTO[]): Promise<unknown[][]>;
|
|
79
84
|
declare function listRows(conn: SqlConnection, def: StoreDef, range?: RangeDTO, count?: number, indexName?: string): Promise<unknown[]>;
|
|
80
85
|
declare function countRows(conn: SqlConnection, def: StoreDef, range?: RangeDTO, indexName?: string): Promise<number>;
|
|
86
|
+
declare function putRows(conn: SqlConnection, def: StoreDef, values: unknown[], changedKeys?: KeyDTO[]): Promise<KeyDTO[]>;
|
|
81
87
|
declare function putRow(conn: SqlConnection, def: StoreDef, value: unknown): Promise<KeyDTO>;
|
|
82
88
|
declare function deleteRows(conn: SqlConnection, def: StoreDef, keyOrRange: KeyDTO | RangeDTO): Promise<void>;
|
|
83
|
-
declare function clearRows(conn: SqlConnection, def: StoreDef): Promise<void>;
|
|
89
|
+
declare function clearRows(conn: SqlConnection, def: StoreDef, wipe?: boolean): Promise<void>;
|
|
84
90
|
/** Wipe all user stores; system stores (memberships) survive and the internal
|
|
85
91
|
* schema metadata table is not a store, so the version guard is untouched. */
|
|
86
92
|
declare function wipeRows(conn: SqlConnection): Promise<void>;
|
|
87
|
-
/** Internal schema metadata is not exposed or cleared by the store API.
|
|
93
|
+
/** Internal schema metadata is not exposed or cleared by the store API.
|
|
94
|
+
*
|
|
95
|
+
* Version policy: the SQL DDL derives entirely from STORE_DEFS, so two
|
|
96
|
+
* schema versions are compatible iff their derived table sets are equal.
|
|
97
|
+
* - stored > DB_VERSION → refuse (never downgrade).
|
|
98
|
+
* - stored < DB_VERSION with EQUAL table sets (v29 → v30: defs unchanged,
|
|
99
|
+
* only browser-local sync stores were added) → bump the guard in place;
|
|
100
|
+
* no row is read, rewritten or erased.
|
|
101
|
+
* - stored < DB_VERSION with different tables → fail closed with the same
|
|
102
|
+
* actionable schema_incompatible error as before.
|
|
103
|
+
* - no guard + store tables → foreign database, refuse. */
|
|
88
104
|
declare function initializeStore(db: Database): Promise<void>;
|
|
89
105
|
|
|
90
|
-
/**
|
|
91
|
-
* Domain read models + incremental projection refresh over the shared SQL
|
|
92
|
-
* store tables.
|
|
93
|
-
*
|
|
94
|
-
* Three derived tables, rebuilt from the per-store source tables (source of
|
|
95
|
-
* truth) when incompatible and refreshed per affected key inside the same
|
|
96
|
-
* transaction as every mutation:
|
|
97
|
-
* domain_note — note metadata selector + original payload
|
|
98
|
-
* query_block — one occurrence row per block_index row, parent resolved by
|
|
99
|
-
* exact noteId or sourcePath (ambiguous/orphan → NULL parent,
|
|
100
|
-
* which gates ALL domain reads with 409 until healed)
|
|
101
|
-
* segment_head — MAX(version) per segment id taken BEFORE the isHistory
|
|
102
|
-
* filter, so a superseded revision can never resurrect
|
|
103
|
-
*
|
|
104
|
-
* Rebuilds are guarded: a `_wodwiki_schema` row stamps the projection
|
|
105
|
-
* version, so repeated ready()/boot cycles skip the rebuild while the
|
|
106
|
-
* derived shape is already compatible.
|
|
107
|
-
*
|
|
108
|
-
* Query plans are fixed and fully validated: every value is a bound parameter,
|
|
109
|
-
* names come from this module, counts run before paging, and text matching is
|
|
110
|
-
* a literal substring over JS-lowercased materialized columns (Unicode-exact;
|
|
111
|
-
* instr/strpos never treat % or _ as wildcards).
|
|
112
|
-
*
|
|
113
|
-
* `journal` is materialized in JS (sourceMatches from the wql package — the
|
|
114
|
-
* exact predicate the app applies) so no SQL regex re-implementation exists.
|
|
115
|
-
*/
|
|
106
|
+
/** Current Note and actionable-block SQL projections. */
|
|
116
107
|
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
* refreshes derived rows — all on the caller's transaction. Returns one entry
|
|
120
|
-
* per op: the put key, or null for delete/clear. `opts.deferProjection`
|
|
121
|
-
* skips the per-key refresh pass; the caller MUST rebuild derived state in
|
|
122
|
-
* the same transaction afterwards (see rebuildProjections) or reads will
|
|
123
|
-
* operate on stale projections behind a valid guard.
|
|
124
|
-
*/
|
|
125
|
-
declare function refreshDomain(conn: SqlConnection, ops: TxOp[], opts?: {
|
|
126
|
-
deferProjection?: boolean;
|
|
127
|
-
}): Promise<(KeyDTO | null)[]>;
|
|
108
|
+
/** Checks precede mutations; pins and projections commit with source rows. */
|
|
109
|
+
declare function refreshDomain(conn: SqlConnection, ops: TxOp[]): Promise<(KeyDTO | null)[]>;
|
|
128
110
|
/**
|
|
129
111
|
* Rebuild the derived tables from the source-of-truth store rows and stamp
|
|
130
112
|
* the projection guard. Caller owns the transaction (atomicity) — the guard
|
|
@@ -141,20 +123,7 @@ declare const MAX_BODY: number;
|
|
|
141
123
|
* SqlStorage.queryDomain share this entry). */
|
|
142
124
|
declare function handleDomainQuery(db: Database, body: unknown): Promise<DomainQueryResult>;
|
|
143
125
|
|
|
144
|
-
/**
|
|
145
|
-
* SqlStorage — IStorage over the shared SQL store operations (server or
|
|
146
|
-
* browser). Transactions buffer writes and flush them as one refreshDomain
|
|
147
|
-
* pass inside a real SQL transaction: throw → rollback, success → commit,
|
|
148
|
-
* and buffered reads overlay committed rows (read-your-writes). Projections
|
|
149
|
-
* refresh inside the same transaction; bulkTransaction() batches a migration
|
|
150
|
-
* into one rebuild instead of per-key refreshes. verifyWritable() proves the
|
|
151
|
-
* backend accepts writes with a real INSERT that is rolled back.
|
|
152
|
-
*
|
|
153
|
-
* BrowserTursoStorage opens a direct-to-Turso backend via @libsql/client/web;
|
|
154
|
-
* the constructor only builds the client (HTTP is connectionless), `ready()`
|
|
155
|
-
* initializes schema + projections and every operation awaits it, so a
|
|
156
|
-
* transient init failure never wedges the backend — ready() retries.
|
|
157
|
-
*/
|
|
126
|
+
/** SQL storage with atomic, change-scoped projections and buffered reads. */
|
|
158
127
|
|
|
159
128
|
interface SqlStorageOptions {
|
|
160
129
|
/** Rows in user-owned stores are stamped with this userId on write. Omit
|
|
@@ -182,11 +151,7 @@ declare class SqlStorage implements IStorage {
|
|
|
182
151
|
readwrite<K extends StoreName>(store: K): IReadWriteStore<StoreType<K>>;
|
|
183
152
|
queryDomain(query: DomainQuery): Promise<DomainQueryResult | undefined>;
|
|
184
153
|
transaction<K extends StoreName, R>(stores: K[], mode: 'readonly' | 'readwrite', fn: (tx: IStorageTransaction) => Promise<R>): Promise<R>;
|
|
185
|
-
/**
|
|
186
|
-
* Bulk load for migration destinations: like transaction(), but the
|
|
187
|
-
* buffered writes skip per-key projection refresh and derived state is
|
|
188
|
-
* rebuilt once — everything inside one SQL transaction (throw → rollback).
|
|
189
|
-
*/
|
|
154
|
+
/** Bulk writes use the same affected-key refresh as ordinary transactions. */
|
|
190
155
|
bulkTransaction<K extends StoreName, R>(stores: K[], fn: (tx: IStorageTransaction) => Promise<R>): Promise<R>;
|
|
191
156
|
private runTx;
|
|
192
157
|
/**
|
|
@@ -217,4 +182,149 @@ declare class BrowserTursoStorage extends SqlStorage {
|
|
|
217
182
|
constructor(settings: TursoSettings, options?: SqlStorageOptions);
|
|
218
183
|
}
|
|
219
184
|
|
|
220
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Turso backup/sync protocol — the local-first sync tables layered on top of
|
|
187
|
+
* any libsql database. The browser data plane is always IndexedDB; a saved
|
|
188
|
+
* Turso target is a backup/sync destination, never the read/write provider.
|
|
189
|
+
*
|
|
190
|
+
* Design: the remote holds ONE op log (`_wodwiki_sync_ops`) plus a state row
|
|
191
|
+
* (`_wodwiki_sync_state`) carrying {schema, rev}. Every committed local
|
|
192
|
+
* transaction group appends one idempotent op-group (uuid op_id,
|
|
193
|
+
* INSERT-or-ignore per op); `rev` is assigned monotonically per NEWLY applied
|
|
194
|
+
* group — a retried group is recognized by its first op id, consumes no rev
|
|
195
|
+
* and appends nothing. Pulls replay ops with rev > cursor in deterministic
|
|
196
|
+
* (rev, idx) order. Tombstones (delete/clear/wipe) are ordinary ops —
|
|
197
|
+
* deletions propagate.
|
|
198
|
+
*
|
|
199
|
+
* Fetch atomicity: a pull window is a set of WHOLE revision groups. The rev
|
|
200
|
+
* boundary is chosen with a bounded subquery, then every row of those revs is
|
|
201
|
+
* returned — a group is never split, so the cursor never skips the rest of
|
|
202
|
+
* a group (one pathologically large group can exceed the nominal row bound;
|
|
203
|
+
* that is the unavoidable atomicity ceiling).
|
|
204
|
+
*
|
|
205
|
+
* ponytail: op-log replay pulls — a fresh browser replays the whole log
|
|
206
|
+
* (O(history)) instead of reading per-store current-state tables. Upgrade
|
|
207
|
+
* path: add per-store state tables rev-indexed by primary key and collapse
|
|
208
|
+
* the log into them periodically; the wire format (op_id, rev, mutation)
|
|
209
|
+
* stays unchanged.
|
|
210
|
+
*
|
|
211
|
+
* Never-downgrade rule: the state row's `schema` is the managing client's
|
|
212
|
+
* DB_VERSION at init time. Uploads verify `schema` INSIDE the write
|
|
213
|
+
* transaction (a stale probe must not append current payloads to an old or
|
|
214
|
+
* newer remote); an older remote log is migrated forward by the caller
|
|
215
|
+
* (transform payloads, never just rewrite the marker), a newer remote is
|
|
216
|
+
* never rewritten — the client refuses and keeps working locally.
|
|
217
|
+
*/
|
|
218
|
+
|
|
219
|
+
declare const SYNC_OPS_TABLE = "_wodwiki_sync_ops";
|
|
220
|
+
declare const SYNC_STATE_TABLE = "_wodwiki_sync_state";
|
|
221
|
+
/** Pre-migration op log copies live here — the recoverable old source,
|
|
222
|
+
* retained after a schema migration replaces the active log. */
|
|
223
|
+
declare const SYNC_ARCHIVE_TABLE = "_wodwiki_sync_ops_archive";
|
|
224
|
+
/** One committed local transaction group, as journaled in IndexedDB. */
|
|
225
|
+
type SyncMutation = {
|
|
226
|
+
op: 'put';
|
|
227
|
+
store: StoreName;
|
|
228
|
+
key: KeyDTO;
|
|
229
|
+
value: unknown;
|
|
230
|
+
} | {
|
|
231
|
+
op: 'delete';
|
|
232
|
+
store: StoreName;
|
|
233
|
+
key: KeyDTO;
|
|
234
|
+
} | {
|
|
235
|
+
op: 'clear';
|
|
236
|
+
store: StoreName;
|
|
237
|
+
} | {
|
|
238
|
+
op: 'wipe';
|
|
239
|
+
};
|
|
240
|
+
interface SyncEntry {
|
|
241
|
+
/** Idempotency key — one uuid per committed local transaction group. */
|
|
242
|
+
id: string;
|
|
243
|
+
at: number;
|
|
244
|
+
ops: SyncMutation[];
|
|
245
|
+
}
|
|
246
|
+
interface StoredSyncOp {
|
|
247
|
+
opId: string;
|
|
248
|
+
rev: number;
|
|
249
|
+
idx: number;
|
|
250
|
+
at: number;
|
|
251
|
+
mutation: SyncMutation;
|
|
252
|
+
}
|
|
253
|
+
type SyncTargetKind =
|
|
254
|
+
/** No tables at all (beyond libsql internals): fresh database. */
|
|
255
|
+
'empty'
|
|
256
|
+
/** Tables exist but neither sync state nor a legacy schema guard — not
|
|
257
|
+
* ours; initializing over unknown data is refused by the caller. */
|
|
258
|
+
| {
|
|
259
|
+
kind: 'unknown';
|
|
260
|
+
}
|
|
261
|
+
/** Legacy direct-Turso SqlStorage layout (`_wodwiki_schema` guard). */
|
|
262
|
+
| {
|
|
263
|
+
kind: 'legacy';
|
|
264
|
+
schemaVersion: number;
|
|
265
|
+
}
|
|
266
|
+
/** This sync protocol's layout; payload = state row. */
|
|
267
|
+
| {
|
|
268
|
+
kind: 'current';
|
|
269
|
+
schemaVersion: number;
|
|
270
|
+
rev: number;
|
|
271
|
+
};
|
|
272
|
+
/** Classify the sync target WITHOUT mutating it. */
|
|
273
|
+
declare function probeSyncTarget(db: Database): Promise<SyncTargetKind>;
|
|
274
|
+
/** Create the sync tables + state row. Only for empty targets; existing
|
|
275
|
+
* state (current, legacy or unknown) is never overwritten by init. */
|
|
276
|
+
declare function initSyncTarget(db: Database, schemaVersion: number): Promise<void>;
|
|
277
|
+
interface ApplySyncBatchOptions {
|
|
278
|
+
/** Schema the caller verified at probe time — rechecked INSIDE the write
|
|
279
|
+
* transaction so a schema change between probe and append aborts. */
|
|
280
|
+
expectSchema?: number;
|
|
281
|
+
/** Bump the state schema version in the SAME transaction (remote-schema
|
|
282
|
+
* migrations append converted payloads + move the marker atomically). */
|
|
283
|
+
toSchemaVersion?: number;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Append op-groups to the log in ONE SQL transaction, idempotently by
|
|
287
|
+
* group: a group whose first op id already exists is skipped entirely and
|
|
288
|
+
* consumes no rev (retries never duplicate, never allocate revisions).
|
|
289
|
+
* Returns the number of NEW groups committed — ack semantics for the
|
|
290
|
+
* uploader's watermark.
|
|
291
|
+
*/
|
|
292
|
+
declare function applySyncBatch(db: Database, entries: SyncEntry[], opts?: ApplySyncBatchOptions): Promise<number>;
|
|
293
|
+
/**
|
|
294
|
+
* Replay window: ops with rev > afterRev, in deterministic commit order.
|
|
295
|
+
* The window covers WHOLE revision groups: up to `limit` distinct revs are
|
|
296
|
+
* chosen (bounded subquery), then every row of those revs is returned, so a
|
|
297
|
+
* cursor advanced to the last returned rev never splits a group. A single
|
|
298
|
+
* group larger than `limit` is returned whole — the atomicity ceiling.
|
|
299
|
+
*/
|
|
300
|
+
declare function fetchSyncOps(db: Database, afterRev: number, limit: number): Promise<StoredSyncOp[]>;
|
|
301
|
+
/**
|
|
302
|
+
* Schema-migration replacement for the active op log, in ONE transaction:
|
|
303
|
+
* every previous op is copied into the archive table (the recoverable old
|
|
304
|
+
* source — nothing is destroyed), the active log is cleared, and the
|
|
305
|
+
* converted snapshot groups become the entire replayable history at
|
|
306
|
+
* rev 1..n with the schema marker moved forward atomically. Fresh browsers
|
|
307
|
+
* replay converted payloads only, so pulls never touch legacy row shapes.
|
|
308
|
+
*/
|
|
309
|
+
declare function replaceSyncLog(db: Database, entries: SyncEntry[], toSchemaVersion: number): Promise<void>;
|
|
310
|
+
/** Migration-only raw replay: parsed op payloads WITHOUT store/key validation
|
|
311
|
+
* (old logs reference era store names like `segments`). Only
|
|
312
|
+
* migrateRemoteToCurrent consumes this — pulls use fetchSyncOps, which
|
|
313
|
+
* validates every row against the current schema. Binary cells revive. */
|
|
314
|
+
interface RawSyncOp {
|
|
315
|
+
op: string;
|
|
316
|
+
store: string;
|
|
317
|
+
key: unknown;
|
|
318
|
+
value?: unknown;
|
|
319
|
+
}
|
|
320
|
+
declare function readRawSyncOps(db: Database): Promise<RawSyncOp[]>;
|
|
321
|
+
/**
|
|
322
|
+
* Import rows from a legacy direct-Turso (SqlStorage) database into op-
|
|
323
|
+
* groups. Reads the per-store JSON `data` columns directly — the legacy
|
|
324
|
+
* schema guard may hold an older version SqlStorage refuses to open, so the
|
|
325
|
+
* rows are lifted raw, never through SqlStorage. Missing tables contribute
|
|
326
|
+
* no rows; nothing is written or deleted remotely.
|
|
327
|
+
*/
|
|
328
|
+
declare function readLegacyRows(db: Database, table: string): Promise<unknown[]>;
|
|
329
|
+
|
|
330
|
+
export { type ApplySyncBatchOptions, BrowserTursoStorage, type Database, type LibsqlClient, MAX_BODY, type RawSyncOp, SCHEMA_TABLE, SYNC_ARCHIVE_TABLE, SYNC_OPS_TABLE, SYNC_STATE_TABLE, type SqlConnection, type SqlRow, type SqlStatement, SqlStorage, type SqlStorageOptions, type SqlValue, StorageError, type StoredSyncOp, type SyncEntry, type SyncMutation, type SyncTargetKind, type TursoSettings, applySyncBatch, clearRows, countRows, decodeRowJson, deleteRows, encodeRowJson, fetchSyncOps, getRow, getRows, handleDomainQuery, initSyncTarget, initializeDomain, initializeStore, libsqlDatabase, listRows, listRowsFromIndexMany, probeSyncTarget, putRow, putRows, readLegacyRows, readRawSyncOps, rebuildProjections, refreshDomain, replaceSyncLog, storeDefinition, validateTursoUrl, wipeDomain, wipeRows };
|