@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.
@@ -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, E 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-C7ZtlBKe.cjs';
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
- * Applies ops in order, collecting projection impact per affected key, then
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
- export { BrowserTursoStorage, type Database, type LibsqlClient, MAX_BODY, SCHEMA_TABLE, type SqlConnection, type SqlRow, SqlStorage, type SqlStorageOptions, type SqlValue, StorageError, type TursoSettings, clearRows, countRows, decodeRowJson, deleteRows, encodeRowJson, getRow, handleDomainQuery, initializeDomain, initializeStore, libsqlDatabase, listRows, putRow, rebuildProjections, refreshDomain, storeDefinition, validateTursoUrl, wipeDomain, wipeRows };
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 };
@@ -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, E 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-C7ZtlBKe.js';
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
- * Applies ops in order, collecting projection impact per affected key, then
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
- export { BrowserTursoStorage, type Database, type LibsqlClient, MAX_BODY, SCHEMA_TABLE, type SqlConnection, type SqlRow, SqlStorage, type SqlStorageOptions, type SqlValue, StorageError, type TursoSettings, clearRows, countRows, decodeRowJson, deleteRows, encodeRowJson, getRow, handleDomainQuery, initializeDomain, initializeStore, libsqlDatabase, listRows, putRow, rebuildProjections, refreshDomain, storeDefinition, validateTursoUrl, wipeDomain, wipeRows };
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 };