@syncular/client 0.4.0 → 0.5.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/README.md +4 -4
- package/dist/apply.d.ts +5 -1
- package/dist/apply.js +6 -4
- package/dist/client.d.ts +49 -2
- package/dist/client.js +527 -259
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/invalidation.d.ts +81 -48
- package/dist/invalidation.js +130 -42
- package/dist/reactive-store.d.ts +74 -0
- package/dist/reactive-store.js +576 -0
- package/dist/schema.js +1 -0
- package/dist/state.d.ts +9 -0
- package/dist/state.js +29 -0
- package/dist/window.d.ts +6 -1
- package/dist/window.js +0 -0
- package/dist/worker-entry.js +78 -52
- package/dist/worker-host.d.ts +8 -4
- package/dist/worker-host.js +26 -6
- package/dist/worker-protocol.d.ts +12 -14
- package/package.json +3 -3
- package/src/apply.ts +18 -5
- package/src/client.ts +685 -311
- package/src/index.ts +1 -0
- package/src/invalidation.ts +216 -62
- package/src/reactive-store.ts +695 -0
- package/src/schema.ts +3 -0
- package/src/state.ts +32 -0
- package/src/window.ts +0 -0
- package/src/worker-entry.ts +83 -54
- package/src/worker-host.ts +44 -8
- package/src/worker-protocol.ts +20 -13
package/README.md
CHANGED
|
@@ -73,9 +73,9 @@ it holds the lock, spawns the worker, and runs the core (the single-tab
|
|
|
73
73
|
path, unchanged — the lock IS the exactly-one-core invariant, and a worker
|
|
74
74
|
is *never* spawned without it). Every other tab is a **follower**: it opens
|
|
75
75
|
a `BroadcastChannel` to the leader and proxies the whole logical API over
|
|
76
|
-
it (`req`/`res`), while the leader fans its worker events —
|
|
77
|
-
|
|
78
|
-
to all followers (`event`). Queries forward to the leader's one DB; rows
|
|
76
|
+
it (`req`/`res`), while the leader fans its worker events — exact revisioned
|
|
77
|
+
change batches plus presence / conflict / sync-needed / synced / upgrading —
|
|
78
|
+
out to all followers (`event`). Queries forward to the leader's one DB; rows
|
|
79
79
|
(including `bytes` columns) ride back through structured clone.
|
|
80
80
|
|
|
81
81
|
**Promotion.** When the leader tab closes, its lock releases. Followers are
|
|
@@ -224,7 +224,7 @@ any divergence from the contract.
|
|
|
224
224
|
`init`, `call`, `ready`, `result`, `error`, `event` — every API method
|
|
225
225
|
multiplexes over `call` (typed end-to-end from the single `WorkerApi`
|
|
226
226
|
shape in `worker-protocol.ts`); `event` carries `sync-needed`,
|
|
227
|
-
`conflict` and `synced`. Query-result blobs transfer (not copy) when
|
|
227
|
+
`change`, `conflict` and `synced`. Query-result blobs transfer (not copy) when
|
|
228
228
|
they own their buffer.
|
|
229
229
|
|
|
230
230
|
## Package layout
|
package/dist/apply.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { type CommitFrame, type RowsSegment, type RowValue, type ScopeMap } from
|
|
|
7
7
|
import type { ClientDatabase } from './database.js';
|
|
8
8
|
import type { EncryptionConfig } from './encryption.js';
|
|
9
9
|
import { type CompiledClientSchema, type CompiledClientTable } from './schema.js';
|
|
10
|
+
/** Lets the client wrap the physical write in its revision transaction. */
|
|
11
|
+
export type ApplyTransaction = <T>(fn: () => T) => T;
|
|
10
12
|
export declare function upsertLocalRow(db: ClientDatabase, table: CompiledClientTable, values: readonly RowValue[], syncVersion: number): void;
|
|
11
13
|
export declare function deleteLocalRow(db: ClientDatabase, table: CompiledClientTable, rowId: string): void;
|
|
12
14
|
/**
|
|
@@ -21,7 +23,7 @@ export declare function deleteLocalRow(db: ClientDatabase, table: CompiledClient
|
|
|
21
23
|
* (`client.decrypt_failed`) aborts before any local write, so the commit
|
|
22
24
|
* never half-applies.
|
|
23
25
|
*/
|
|
24
|
-
export declare function applyCommitFrame(db: ClientDatabase, schema: CompiledClientSchema, frame: CommitFrame, encryption?: EncryptionConfig): Promise<void>;
|
|
26
|
+
export declare function applyCommitFrame(db: ClientDatabase, schema: CompiledClientSchema, frame: CommitFrame, encryption?: EncryptionConfig, transaction?: ApplyTransaction): Promise<void>;
|
|
25
27
|
/**
|
|
26
28
|
* §5.2: the segment's column table must match the generated schema for
|
|
27
29
|
* (table, schemaVersion) — order, names, types, nullability. A mismatch is
|
|
@@ -65,6 +67,7 @@ export interface SqliteSegmentDescriptor {
|
|
|
65
67
|
export declare function applySqliteSegment(db: ClientDatabase, schema: CompiledClientSchema, table: CompiledClientTable, bytes: Uint8Array, descriptor: SqliteSegmentDescriptor, options: {
|
|
66
68
|
readonly clearFirst: boolean;
|
|
67
69
|
readonly effective: ScopeMap;
|
|
70
|
+
readonly transaction?: ApplyTransaction;
|
|
68
71
|
}): number;
|
|
69
72
|
/**
|
|
70
73
|
* Apply a decoded rows segment: each block in one local transaction
|
|
@@ -77,4 +80,5 @@ export declare function applySqliteSegment(db: ClientDatabase, schema: CompiledC
|
|
|
77
80
|
export declare function applyRowsSegment(db: ClientDatabase, schema: CompiledClientSchema, table: CompiledClientTable, segment: RowsSegment, options: {
|
|
78
81
|
readonly clearFirst: boolean;
|
|
79
82
|
readonly effective: ScopeMap;
|
|
83
|
+
readonly transaction?: ApplyTransaction;
|
|
80
84
|
}, encryption?: EncryptionConfig): Promise<number>;
|
package/dist/apply.js
CHANGED
|
@@ -32,7 +32,7 @@ export function deleteLocalRow(db, table, rowId) {
|
|
|
32
32
|
* (`client.decrypt_failed`) aborts before any local write, so the commit
|
|
33
33
|
* never half-applies.
|
|
34
34
|
*/
|
|
35
|
-
export async function applyCommitFrame(db, schema, frame, encryption) {
|
|
35
|
+
export async function applyCommitFrame(db, schema, frame, encryption, transaction = (fn) => db.transaction(fn)) {
|
|
36
36
|
const resolved = [];
|
|
37
37
|
for (const change of frame.changes) {
|
|
38
38
|
const tableName = frame.tables[change.tableIndex];
|
|
@@ -62,7 +62,7 @@ export async function applyCommitFrame(db, schema, frame, encryption) {
|
|
|
62
62
|
rowVersion: change.rowVersion,
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
|
-
|
|
65
|
+
transaction(() => {
|
|
66
66
|
for (const change of resolved) {
|
|
67
67
|
if (change.op === 'delete') {
|
|
68
68
|
deleteLocalRow(db, change.table, change.rowId);
|
|
@@ -241,7 +241,7 @@ export function applySqliteSegment(db, schema, table, bytes, descriptor, options
|
|
|
241
241
|
}
|
|
242
242
|
// 3. One transaction: fresh-bootstrap clear, then replace-or-upsert.
|
|
243
243
|
const names = table.columns.map((column) => quoteIdent(column.name));
|
|
244
|
-
return db.transaction(() => {
|
|
244
|
+
return (options.transaction ?? ((fn) => db.transaction(fn)))(() => {
|
|
245
245
|
if (options.clearFirst) {
|
|
246
246
|
deleteScopedRows(db, table, options.effective);
|
|
247
247
|
}
|
|
@@ -287,7 +287,9 @@ export async function applyRowsSegment(db, schema, table, segment, options, encr
|
|
|
287
287
|
}
|
|
288
288
|
const clearThisBlock = first && options.clearFirst;
|
|
289
289
|
first = false;
|
|
290
|
-
|
|
290
|
+
if (!clearThisBlock && rows.length === 0)
|
|
291
|
+
continue;
|
|
292
|
+
(options.transaction ?? ((fn) => db.transaction(fn)))(() => {
|
|
291
293
|
if (clearThisBlock) {
|
|
292
294
|
deleteScopedRows(db, table, options.effective);
|
|
293
295
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { type RowValue, type ScopeMap, type WakeReason } from '@syncular/core';
|
|
|
11
11
|
import { type BlobRef, type BlobTransport, type CachedBlob } from './blob.js';
|
|
12
12
|
import type { ClientDatabase, SqlRow, SqlValue } from './database.js';
|
|
13
13
|
import type { EncryptionConfig } from './encryption.js';
|
|
14
|
-
import { type InvalidationListener } from './invalidation.js';
|
|
14
|
+
import { type ClientChangeListener, type CommandResult, type InvalidationListener, type LocalRevision, type SyncIntent, type SyncStatusSnapshot } from './invalidation.js';
|
|
15
15
|
import { type LeaderLock } from './leader-lock.js';
|
|
16
16
|
import { type OutboxCommit, type OutboxOperation } from './outbox.js';
|
|
17
17
|
import { type ClientSchema } from './schema.js';
|
|
@@ -136,7 +136,10 @@ export interface SyncClientConfig {
|
|
|
136
136
|
readonly limits?: SyncClientLimits;
|
|
137
137
|
readonly now?: () => number;
|
|
138
138
|
/** §8: hello `requiresSync` or a wake-up — run a pull soon. */
|
|
139
|
-
readonly onSyncNeeded?: (reason: 'hello' | WakeReason) => void;
|
|
139
|
+
readonly onSyncNeeded?: (reason: 'startup' | 'hello' | WakeReason) => void;
|
|
140
|
+
/** Exact core-owned scheduling intent. Hosts consume this to run an
|
|
141
|
+
* event-driven retry deadline without polling or inferring sync state. */
|
|
142
|
+
readonly onSyncIntent?: (intent: SyncIntent) => void;
|
|
140
143
|
readonly onConflict?: (conflict: ConflictRecord) => void;
|
|
141
144
|
/**
|
|
142
145
|
* §7.4.5: the schema-bump `upgrading` state changed. `true` when a reset
|
|
@@ -191,6 +194,30 @@ export interface WindowState {
|
|
|
191
194
|
*/
|
|
192
195
|
readonly pending: readonly string[];
|
|
193
196
|
}
|
|
197
|
+
/** One generated/raw query's required window units (SPEC §7.5). */
|
|
198
|
+
export interface WindowCoverage {
|
|
199
|
+
readonly base: WindowBase;
|
|
200
|
+
readonly units: readonly string[];
|
|
201
|
+
}
|
|
202
|
+
export interface WindowUnitRef {
|
|
203
|
+
readonly baseKey: string;
|
|
204
|
+
readonly unit: string;
|
|
205
|
+
}
|
|
206
|
+
export interface CoverageSnapshot {
|
|
207
|
+
readonly complete: boolean;
|
|
208
|
+
readonly pending: readonly WindowUnitRef[];
|
|
209
|
+
readonly missing: readonly WindowUnitRef[];
|
|
210
|
+
}
|
|
211
|
+
export interface QueryReadSpec {
|
|
212
|
+
readonly sql: string;
|
|
213
|
+
readonly params?: readonly SqlValue[];
|
|
214
|
+
readonly coverage?: readonly WindowCoverage[];
|
|
215
|
+
}
|
|
216
|
+
export interface QuerySnapshot<Row = SqlRow> {
|
|
217
|
+
readonly revision: LocalRevision;
|
|
218
|
+
readonly rows: readonly Row[];
|
|
219
|
+
readonly coverage: CoverageSnapshot;
|
|
220
|
+
}
|
|
194
221
|
/**
|
|
195
222
|
* True iff `unit` is windowed-in AND its bootstrap completed (§4.8 I3):
|
|
196
223
|
* registered and not pending. A unit with zero server rows still becomes
|
|
@@ -215,6 +242,14 @@ export declare class SyncClient {
|
|
|
215
242
|
* internals read `this.#db` directly and skip this method by design.
|
|
216
243
|
*/
|
|
217
244
|
query(sql: string, params?: readonly SqlValue[]): SqlRow[];
|
|
245
|
+
/** Current durable local observer revision (SPEC §7.5). */
|
|
246
|
+
get localRevision(): LocalRevision;
|
|
247
|
+
/**
|
|
248
|
+
* Read rows, window answerability, and revision from one SQLite snapshot.
|
|
249
|
+
* Reactive integrations use this instead of composing `query()` and
|
|
250
|
+
* `windowState()` across separate worker/IPC calls.
|
|
251
|
+
*/
|
|
252
|
+
querySnapshot<Row = SqlRow>(spec: QueryReadSpec): QuerySnapshot<Row>;
|
|
218
253
|
/**
|
|
219
254
|
* Subscribe to fine-grained invalidation. The callback fires ONCE per
|
|
220
255
|
* apply batch (never per row, I1) with the `{tables, scopeKeys}` touched
|
|
@@ -227,6 +262,10 @@ export declare class SyncClient {
|
|
|
227
262
|
* per-row scopes (COMMIT changes) or a scope map (segments/purge).
|
|
228
263
|
*/
|
|
229
264
|
onInvalidate(listener: InvalidationListener): () => void;
|
|
265
|
+
/** Subscribe to exact revisioned observer transactions (SPEC §7.5). */
|
|
266
|
+
onChange(listener: ClientChangeListener): () => void;
|
|
267
|
+
/** One call for the complete status domain used by reactive hosts. */
|
|
268
|
+
statusSnapshot(): SyncStatusSnapshot;
|
|
230
269
|
/**
|
|
231
270
|
* Stage a blob for attachment (§5.9.7): hash the bytes into the content
|
|
232
271
|
* address, cache them locally, and queue the upload (flushed before the
|
|
@@ -315,6 +354,8 @@ export declare class SyncClient {
|
|
|
315
354
|
* re-registers realtime at round end (§8.7). No socket cycle needed.
|
|
316
355
|
*/
|
|
317
356
|
setWindow(base: WindowBase, units: readonly string[]): Promise<void>;
|
|
357
|
+
/** Exact core command result consumed by automatic host loops (§7.5). */
|
|
358
|
+
setWindowCommand(base: WindowBase, units: readonly string[]): Promise<CommandResult<void>>;
|
|
318
359
|
/**
|
|
319
360
|
* The completeness oracle (§4.8 I3): which units of a base are windowed-in
|
|
320
361
|
* locally, which of those are still bootstrap-pending, and thereby the
|
|
@@ -332,6 +373,8 @@ export declare class SyncClient {
|
|
|
332
373
|
* Returns the generated `clientCommitId`.
|
|
333
374
|
*/
|
|
334
375
|
mutate(mutations: readonly MutationInput[]): string;
|
|
376
|
+
/** Host-facing mutation result with explicit network work intent (§7.5). */
|
|
377
|
+
mutateCommand(mutations: readonly MutationInput[]): CommandResult<string>;
|
|
335
378
|
/**
|
|
336
379
|
* Partial-update convenience over the §6.1 full-row wire: read the
|
|
337
380
|
* current LOCAL row, merge `partial` over it, and record one full-row
|
|
@@ -343,6 +386,10 @@ export declare class SyncClient {
|
|
|
343
386
|
patch(table: string, rowId: string, partial: Readonly<Record<string, unknown>>, options?: {
|
|
344
387
|
readonly baseVersion?: number;
|
|
345
388
|
}): string;
|
|
389
|
+
/** Host-facing patch result with explicit network work intent (§7.5). */
|
|
390
|
+
patchCommand(table: string, rowId: string, partial: Readonly<Record<string, unknown>>, options?: {
|
|
391
|
+
readonly baseVersion?: number;
|
|
392
|
+
}): CommandResult<string>;
|
|
346
393
|
/**
|
|
347
394
|
* One combined push+pull round (§1.5, §7.2). The core owns one loop: a
|
|
348
395
|
* concurrent `sync()` while one is already outstanding is rejected loudly
|