@syncular/client 0.15.48 → 0.16.1
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 +42 -0
- package/dist/client.d.ts +13 -20
- package/dist/client.js +25 -43
- package/dist/outbox.d.ts +5 -1
- package/dist/outbox.js +26 -3
- package/dist/reactive-store.d.ts +12 -6
- package/dist/reactive-store.js +219 -45
- package/dist/realtime-supervisor.d.ts +1 -1
- package/dist/realtime-supervisor.js +2 -8
- package/dist/worker-entry.js +6 -10
- package/dist/worker-host.d.ts +4 -7
- package/dist/worker-host.js +3 -15
- package/dist/worker-protocol.d.ts +4 -18
- package/package.json +3 -11
- package/src/client.ts +59 -47
- package/src/outbox.ts +39 -8
- package/src/reactive-store.ts +226 -56
- package/src/realtime-supervisor.ts +2 -11
- package/src/worker-entry.ts +7 -10
- package/src/worker-host.ts +5 -22
- package/src/worker-protocol.ts +7 -33
- package/dist/realtime-supervisor-observation.d.ts +0 -8
- package/dist/realtime-supervisor-observation.js +0 -15
- package/src/realtime-supervisor-observation.ts +0 -21
package/README.md
CHANGED
|
@@ -580,3 +580,45 @@ Tests drive the real worker entry in a bun `Worker` with bun:sqlite
|
|
|
580
580
|
injected through the bootstrap's database-factory override
|
|
581
581
|
(`test/worker-rpc.test.ts`); the OPFS path itself is browser-only and is
|
|
582
582
|
exercised by `apps/demo`.
|
|
583
|
+
|
|
584
|
+
## Snapshot API migration
|
|
585
|
+
|
|
586
|
+
This source-breaking revision uses methods for application reads across the
|
|
587
|
+
direct client, worker leaders and followers, Tauri, and React Native. Replace
|
|
588
|
+
`client.conflicts`, `client.rejections`, and `client.securityLifecycle` on the
|
|
589
|
+
direct client with method calls. Replace `schemaFloor`, `leaseState`,
|
|
590
|
+
`upgrading`, and `syncNeeded` getters or bridge methods with fields from one
|
|
591
|
+
`statusSnapshot()` call:
|
|
592
|
+
|
|
593
|
+
```ts
|
|
594
|
+
const status = await client.statusSnapshot();
|
|
595
|
+
if (status.schemaFloor) showUpgradeRequired(status.schemaFloor);
|
|
596
|
+
const conflicts = await client.conflicts();
|
|
597
|
+
const outcome = await client.commitOutcome(commitId);
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
The direct client returns snapshots synchronously. Worker and native bridges
|
|
601
|
+
return promises; `await` works with both. `querySnapshot` returns rows, coverage,
|
|
602
|
+
and revision from one read. `diagnosticsSnapshot`, `commitOutcome`,
|
|
603
|
+
`commitOutcomes`, and `resolveCommitOutcome` retain their existing arguments.
|
|
604
|
+
The shared `ClientSnapshotMethods` and `PromiseMethods` types describe these
|
|
605
|
+
contracts. Key-bearing security activation stays on each concrete host type.
|
|
606
|
+
|
|
607
|
+
React uses the supplied client directly; `useSyncClient()` preserves its
|
|
608
|
+
identity. Remove imports of `normalizeClient` and the
|
|
609
|
+
`@syncular/client/realtime-supervisor-observation` forwarding utility. Pass the
|
|
610
|
+
client to `SyncProvider` and use `realtimeSupervisorSnapshot(client)` to inspect
|
|
611
|
+
an attached supervisor. Custom React clients must implement the snapshot
|
|
612
|
+
methods and method-form collection reads. See the [React migration](https://syncular.dev/platform-react/)
|
|
613
|
+
for the `onEnqueued` callback rename.
|
|
614
|
+
|
|
615
|
+
## Outbox read costs
|
|
616
|
+
|
|
617
|
+
Request encoding pins the pending count and highest local sequence before its
|
|
618
|
+
first asynchronous step. It reads keyset pages of 32 raw records, decodes only
|
|
619
|
+
the consumed prefix, and stops at the first whole commit that exceeds the
|
|
620
|
+
remaining operation budget. Mutations appended during encoding enter the next
|
|
621
|
+
request. Status and diagnostics use `COUNT(*)` without parsing pending bodies.
|
|
622
|
+
Optimistic replay still reads the remaining outbox after each response. The
|
|
623
|
+
100/1,000/10,000-commit workload and measured limits are recorded in
|
|
624
|
+
[the reliability RFC](../../docs/RFC-RELIABILITY-DX.md#9-implementation-evidence-2026-09-05).
|
package/dist/client.d.ts
CHANGED
|
@@ -231,6 +231,16 @@ export interface QuerySnapshot<Row = SqlRow> {
|
|
|
231
231
|
* complete once its bootstrap round finishes — emptiness ≠ pendency.
|
|
232
232
|
*/
|
|
233
233
|
export declare function windowComplete(state: WindowState, unit: string): boolean;
|
|
234
|
+
/** Canonical client reads, shared by synchronous cores and promise hosts. */
|
|
235
|
+
export type ClientSnapshotMethods = Pick<SyncClient, 'querySnapshot' | 'statusSnapshot' | 'diagnosticsSnapshot' | 'conflicts' | 'rejections' | 'commitOutcome' | 'commitOutcomes' | 'resolveCommitOutcome'>;
|
|
236
|
+
/** Project a method contract across an asynchronous host boundary. */
|
|
237
|
+
export type PromiseMethods<Methods> = {
|
|
238
|
+
[Key in keyof Methods]: Methods[Key] extends (...args: infer Args) => infer Result ? (...args: Args) => Promise<Awaited<Result>> : never;
|
|
239
|
+
};
|
|
240
|
+
/** A reader can execute locally or cross a worker/native boundary. */
|
|
241
|
+
export type ClientSnapshotReader = {
|
|
242
|
+
[Key in keyof ClientSnapshotMethods]: (...args: Parameters<ClientSnapshotMethods[Key]>) => ReturnType<ClientSnapshotMethods[Key]> | Promise<ReturnType<ClientSnapshotMethods[Key]>>;
|
|
243
|
+
};
|
|
234
244
|
export declare class SyncClient {
|
|
235
245
|
#private;
|
|
236
246
|
constructor(config: SyncClientConfig);
|
|
@@ -238,7 +248,7 @@ export declare class SyncClient {
|
|
|
238
248
|
start(): Promise<void>;
|
|
239
249
|
close(): Promise<void>;
|
|
240
250
|
/** Current fail-closed local-replica security state. */
|
|
241
|
-
|
|
251
|
+
securityLifecycle(): SecurityLifecycle;
|
|
242
252
|
/**
|
|
243
253
|
* Block new protected operations immediately, then wait for every already
|
|
244
254
|
* serialized database/network operation to settle before releasing key
|
|
@@ -320,8 +330,8 @@ export declare class SyncClient {
|
|
|
320
330
|
fetchBlob(blobIdOrRef: string): Promise<CachedBlob>;
|
|
321
331
|
/** Flush any queued blob uploads (§5.9.7 B4); safe to call standalone. */
|
|
322
332
|
flushBlobUploads(): Promise<void>;
|
|
323
|
-
|
|
324
|
-
|
|
333
|
+
conflicts(): readonly ConflictRecord[];
|
|
334
|
+
rejections(): readonly RejectionRecord[];
|
|
325
335
|
/** One durable final outcome by the originating client commit id. */
|
|
326
336
|
commitOutcome(clientCommitId: string): CommitOutcome | undefined;
|
|
327
337
|
/** Newest-first durable outcome journal. */
|
|
@@ -333,28 +343,11 @@ export declare class SyncClient {
|
|
|
333
343
|
* dismissed. The transition is one-way and survives restart.
|
|
334
344
|
*/
|
|
335
345
|
resolveCommitOutcome(input: ResolveCommitOutcomeInput): CommitOutcome;
|
|
336
|
-
/** Non-undefined once the server declared a schema floor (§1.6). */
|
|
337
|
-
get schemaFloor(): SchemaFloor | undefined;
|
|
338
|
-
/**
|
|
339
|
-
* §7.4.5: true while a schema-bump reset + first re-bootstrap is in
|
|
340
|
-
* flight — the app's "upgrading…" cue. Clears when the first post-reset
|
|
341
|
-
* bootstrap round reaches idle (every subscription past its fresh
|
|
342
|
-
* bootstrap).
|
|
343
|
-
*/
|
|
344
|
-
get upgrading(): boolean;
|
|
345
|
-
/**
|
|
346
|
-
* §7.3.5: the current auth-lease state (opaque). Undefined until a
|
|
347
|
-
* `LEASE` frame arrives. `errorCode` is set when a round was rejected
|
|
348
|
-
* with a request-level lease code — syncing on the lease has stopped.
|
|
349
|
-
*/
|
|
350
|
-
get leaseState(): LeaseState | undefined;
|
|
351
346
|
/** §7.3.5: remaining lease validity in ms (`expiresAtMs − now`), or
|
|
352
347
|
* `undefined` if no lease is held. Negative once expired. */
|
|
353
348
|
leaseRemainingMs(now?: number): number | undefined;
|
|
354
349
|
/** True when syncing is stopped pending a client upgrade. */
|
|
355
350
|
get stopped(): boolean;
|
|
356
|
-
/** §8: a hello/wake-up asked for a pull that has not run yet. */
|
|
357
|
-
get syncNeeded(): boolean;
|
|
358
351
|
/**
|
|
359
352
|
* §8.6 presence on a scope key: the current peers present there (a map
|
|
360
353
|
* of `actorId clientId` → peer). Empty for a key with no present peers.
|
package/dist/client.js
CHANGED
|
@@ -18,7 +18,7 @@ import { singleOwnerLock, } from './leader-lock.js';
|
|
|
18
18
|
import { compileLocalDataPurge, localDataPurgeMetaKey, localDataPurgeTargetMatches, } from './local-purge.js';
|
|
19
19
|
import { compileLocalDataRebootstrap, localDataRebootstrapMetaKey, } from './local-rebootstrap.js';
|
|
20
20
|
import { decodeLocalDataRebootstrapReceipt, encodeLocalDataRebootstrapReceipt, } from './local-rebootstrap-receipt.js';
|
|
21
|
-
import { appendOutboxCommit, deleteOutboxCommit, dropOutboxCommitsInScope, encodeOutboxCommit, listOutbox, listOutboxBeforeImages, OutboxEncodeError, replaceOutboxBeforeImages, } from './outbox.js';
|
|
21
|
+
import { appendOutboxCommit, deleteOutboxCommit, dropOutboxCommitsInScope, encodeOutboxCommit, listOutbox, iterateOutbox, countOutbox, listOutboxBeforeImages, OutboxEncodeError, replaceOutboxBeforeImages, } from './outbox.js';
|
|
22
22
|
import { activeFailureRecords, listCommitOutcomes, persistCommitOutcomeResolution, pruneCommitOutcomes, commitOutcome as readCommitOutcome, recordCommitOutcome, } from './outcomes.js';
|
|
23
23
|
import { assertReadOnlyQuery } from './query-guard.js';
|
|
24
24
|
import { compileClientSchema, dropAndRecreateSyncedTables, ensureLocalBookkeepingSchema, ensureLocalSyncedSchema, fromSqlValue, jsonToRowValue, LOCAL_SCHEMA_VERSION_KEY, normalizeRecordKeys, OPTIMISTIC_VERSION, quoteIdent, recordToRowValues, rowValueToJson, SYNC_VERSION_COLUMN, stripSyncColumns, } from './schema.js';
|
|
@@ -232,7 +232,7 @@ export class SyncClient {
|
|
|
232
232
|
// this as an exact core-owned intent so hosts never need a startup poll or
|
|
233
233
|
// an application-issued sync() call.
|
|
234
234
|
const startupWork = this.#schemaFloor === undefined &&
|
|
235
|
-
(
|
|
235
|
+
(countOutbox(this.#db) > 0 ||
|
|
236
236
|
subscriptions.some((sub) => sub.status === 'active'));
|
|
237
237
|
if (startupWork && this.#securityLifecycle === 'active') {
|
|
238
238
|
this.#needsPull = true;
|
|
@@ -247,10 +247,10 @@ export class SyncClient {
|
|
|
247
247
|
role: () => 'direct',
|
|
248
248
|
outbox: async () => this.pendingCommits().length,
|
|
249
249
|
subscriptions: async () => this.subscriptions(),
|
|
250
|
-
conflicts: async () => this.conflicts.length,
|
|
251
|
-
rejections: async () => this.rejections.length,
|
|
252
|
-
syncNeeded: async () => this.syncNeeded,
|
|
253
|
-
upgrading: async () => this.upgrading,
|
|
250
|
+
conflicts: async () => this.conflicts().length,
|
|
251
|
+
rejections: async () => this.rejections().length,
|
|
252
|
+
syncNeeded: async () => this.statusSnapshot().syncNeeded,
|
|
253
|
+
upgrading: async () => this.statusSnapshot().upgrading,
|
|
254
254
|
onInvalidate: (listener) => this.onInvalidate(listener),
|
|
255
255
|
});
|
|
256
256
|
this.#emitDiagnostics();
|
|
@@ -395,7 +395,7 @@ export class SyncClient {
|
|
|
395
395
|
}
|
|
396
396
|
}
|
|
397
397
|
/** Current fail-closed local-replica security state. */
|
|
398
|
-
|
|
398
|
+
securityLifecycle() {
|
|
399
399
|
return this.#securityLifecycle;
|
|
400
400
|
}
|
|
401
401
|
/**
|
|
@@ -439,7 +439,7 @@ export class SyncClient {
|
|
|
439
439
|
this.#encryption = options.encryption;
|
|
440
440
|
this.#securityLifecycle = 'active';
|
|
441
441
|
const startupWork = this.#schemaFloor === undefined &&
|
|
442
|
-
(
|
|
442
|
+
(countOutbox(this.#db) > 0 ||
|
|
443
443
|
loadSubscriptions(this.#db).some((sub) => sub.status === 'active'));
|
|
444
444
|
if (startupWork) {
|
|
445
445
|
this.#setSyncNeeded(true);
|
|
@@ -666,7 +666,7 @@ export class SyncClient {
|
|
|
666
666
|
replica: {
|
|
667
667
|
localRevision: getLocalRevision(this.#db).toString(),
|
|
668
668
|
syncNeeded: this.#needsPull,
|
|
669
|
-
pendingOutbox:
|
|
669
|
+
pendingOutbox: countOutbox(this.#db),
|
|
670
670
|
},
|
|
671
671
|
lease: leaseState,
|
|
672
672
|
subscriptions: allSubscriptions.slice(0, MAX_DIAGNOSTIC_EXPECTED_SUBSCRIPTIONS),
|
|
@@ -757,7 +757,7 @@ export class SyncClient {
|
|
|
757
757
|
#statusSnapshot(outboxCount) {
|
|
758
758
|
return {
|
|
759
759
|
currentSchemaVersion: this.#config.schema.version,
|
|
760
|
-
outbox: outboxCount ??
|
|
760
|
+
outbox: outboxCount ?? countOutbox(this.#db),
|
|
761
761
|
upgrading: this.#upgrading,
|
|
762
762
|
leaseState: this.#leaseState,
|
|
763
763
|
schemaFloor: this.#schemaFloor,
|
|
@@ -994,11 +994,11 @@ export class SyncClient {
|
|
|
994
994
|
}
|
|
995
995
|
await transport.upload(blobId, bytes, mediaType);
|
|
996
996
|
}
|
|
997
|
-
|
|
997
|
+
conflicts() {
|
|
998
998
|
this.#requireActive();
|
|
999
999
|
return this.#conflicts;
|
|
1000
1000
|
}
|
|
1001
|
-
|
|
1001
|
+
rejections() {
|
|
1002
1002
|
this.#requireActive();
|
|
1003
1003
|
return this.#rejections;
|
|
1004
1004
|
}
|
|
@@ -1062,27 +1062,6 @@ export class SyncClient {
|
|
|
1062
1062
|
return resolved;
|
|
1063
1063
|
});
|
|
1064
1064
|
}
|
|
1065
|
-
/** Non-undefined once the server declared a schema floor (§1.6). */
|
|
1066
|
-
get schemaFloor() {
|
|
1067
|
-
return this.#schemaFloor;
|
|
1068
|
-
}
|
|
1069
|
-
/**
|
|
1070
|
-
* §7.4.5: true while a schema-bump reset + first re-bootstrap is in
|
|
1071
|
-
* flight — the app's "upgrading…" cue. Clears when the first post-reset
|
|
1072
|
-
* bootstrap round reaches idle (every subscription past its fresh
|
|
1073
|
-
* bootstrap).
|
|
1074
|
-
*/
|
|
1075
|
-
get upgrading() {
|
|
1076
|
-
return this.#upgrading;
|
|
1077
|
-
}
|
|
1078
|
-
/**
|
|
1079
|
-
* §7.3.5: the current auth-lease state (opaque). Undefined until a
|
|
1080
|
-
* `LEASE` frame arrives. `errorCode` is set when a round was rejected
|
|
1081
|
-
* with a request-level lease code — syncing on the lease has stopped.
|
|
1082
|
-
*/
|
|
1083
|
-
get leaseState() {
|
|
1084
|
-
return this.#leaseState;
|
|
1085
|
-
}
|
|
1086
1065
|
/** §7.3.5: remaining lease validity in ms (`expiresAtMs − now`), or
|
|
1087
1066
|
* `undefined` if no lease is held. Negative once expired. */
|
|
1088
1067
|
leaseRemainingMs(now = this.#now()) {
|
|
@@ -1093,10 +1072,6 @@ export class SyncClient {
|
|
|
1093
1072
|
get stopped() {
|
|
1094
1073
|
return this.#schemaFloor !== undefined;
|
|
1095
1074
|
}
|
|
1096
|
-
/** §8: a hello/wake-up asked for a pull that has not run yet. */
|
|
1097
|
-
get syncNeeded() {
|
|
1098
|
-
return this.#needsPull;
|
|
1099
|
-
}
|
|
1100
1075
|
/**
|
|
1101
1076
|
* §8.6 presence on a scope key: the current peers present there (a map
|
|
1102
1077
|
* of `actorId clientId` → peer). Empty for a key with no present peers.
|
|
@@ -1731,21 +1706,27 @@ export class SyncClient {
|
|
|
1731
1706
|
* the encoded push frames index-aligned with the surviving `outbox`.
|
|
1732
1707
|
*/
|
|
1733
1708
|
async #encodeOutboxForPush() {
|
|
1734
|
-
|
|
1709
|
+
// Pin before the first encryption await: mutations can append while a
|
|
1710
|
+
// round is encoding, and belong to the next request.
|
|
1711
|
+
const bounds = this.#db.query('SELECT COUNT(*) AS count, MAX(seq) AS last_seq FROM _syncular_outbox')[0];
|
|
1712
|
+
const pendingCount = bounds.count;
|
|
1713
|
+
const throughSeq = bounds.last_seq ?? 0;
|
|
1735
1714
|
const pushFrames = [];
|
|
1736
1715
|
const outbox = [];
|
|
1737
1716
|
let deferred = 0;
|
|
1738
1717
|
let ops = 0;
|
|
1739
|
-
|
|
1718
|
+
let processed = 0;
|
|
1719
|
+
for (const commit of iterateOutbox(this.#db, throughSeq)) {
|
|
1740
1720
|
// §6.1 splitBatch: whole commits in commit order, stopping before the
|
|
1741
1721
|
// per-request operation cap. A first commit that alone exceeds the cap
|
|
1742
1722
|
// is sent alone — the server rejects it loudly rather than the queue
|
|
1743
1723
|
// wedging silently. Deferred commits stay queued for the next round.
|
|
1744
1724
|
if (outbox.length > 0 &&
|
|
1745
1725
|
ops + commit.operations.length > MAX_OPS_PER_REQUEST) {
|
|
1746
|
-
deferred
|
|
1747
|
-
|
|
1726
|
+
deferred = pendingCount - processed;
|
|
1727
|
+
break;
|
|
1748
1728
|
}
|
|
1729
|
+
processed += 1;
|
|
1749
1730
|
try {
|
|
1750
1731
|
pushFrames.push(
|
|
1751
1732
|
// §5.11: encrypted columns are encrypted at this encode-at-send
|
|
@@ -2003,7 +1984,8 @@ export class SyncClient {
|
|
|
2003
1984
|
last.segmentRowsApplied === 0 &&
|
|
2004
1985
|
last.bootstrapping.length === 0 &&
|
|
2005
1986
|
last.resets.length === 0 &&
|
|
2006
|
-
(last.deferredCommits ?? 0) === 0
|
|
1987
|
+
(last.deferredCommits ?? 0) === 0 &&
|
|
1988
|
+
!this.#needsPull) {
|
|
2007
1989
|
return last;
|
|
2008
1990
|
}
|
|
2009
1991
|
}
|
|
@@ -2347,7 +2329,7 @@ export class SyncClient {
|
|
|
2347
2329
|
});
|
|
2348
2330
|
break;
|
|
2349
2331
|
case 'PUSH_RESULT': {
|
|
2350
|
-
let outboxCount = responseOutboxCount ??
|
|
2332
|
+
let outboxCount = responseOutboxCount ?? countOutbox(this.#db);
|
|
2351
2333
|
this.#applyBatch((batch) => {
|
|
2352
2334
|
const drained = this.#handlePushResult(frame, commitsById, summary, batch, rejectionDetailsByCommit.get(frame.clientCommitId), frame === lastFinalPushResult);
|
|
2353
2335
|
if (drained)
|
package/dist/outbox.d.ts
CHANGED
|
@@ -38,8 +38,12 @@ export interface OutboxBeforeImage {
|
|
|
38
38
|
readonly values?: Readonly<Record<string, JsonRowValue>>;
|
|
39
39
|
}
|
|
40
40
|
export declare function appendOutboxCommit(db: ClientDatabase, clientCommitId: string, operations: readonly OutboxOperation[], nowMs: number, beforeImages?: readonly OutboxBeforeImage[]): void;
|
|
41
|
-
/** Pending commits in FIFO creation order (§7.1). */
|
|
41
|
+
/** Pending commits in FIFO creation order (§7.1). Full reads serve replay and the public listing. */
|
|
42
42
|
export declare function listOutbox(db: ClientDatabase): OutboxCommit[];
|
|
43
|
+
/** Keyset pages bound staging; laziness decodes only commits consumed by the encoder. */
|
|
44
|
+
export declare function iterateOutbox(db: ClientDatabase, throughSeq: number): Generator<OutboxCommit>;
|
|
45
|
+
/** Routine status reads never load operation bodies. */
|
|
46
|
+
export declare function countOutbox(db: ClientDatabase): number;
|
|
43
47
|
export declare function deleteOutboxCommit(db: ClientDatabase, clientCommitId: string): void;
|
|
44
48
|
export declare function listOutboxBeforeImages(db: ClientDatabase, clientCommitId: string): OutboxBeforeImage[];
|
|
45
49
|
export declare function replaceOutboxBeforeImages(db: ClientDatabase, clientCommitId: string, replacements: readonly OutboxBeforeImage[]): void;
|
package/dist/outbox.js
CHANGED
|
@@ -26,17 +26,40 @@ export function appendOutboxCommit(db, clientCommitId, operations, nowMs, before
|
|
|
26
26
|
]);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
/** Pending commits in FIFO creation order (§7.1). */
|
|
29
|
+
/** Pending commits in FIFO creation order (§7.1). Full reads serve replay and the public listing. */
|
|
30
30
|
export function listOutbox(db) {
|
|
31
31
|
return db
|
|
32
32
|
.query(`SELECT seq, client_commit_id, created_at_ms, operations
|
|
33
33
|
FROM _syncular_outbox ORDER BY seq ASC`)
|
|
34
|
-
.map(
|
|
34
|
+
.map(decodeOutboxRow);
|
|
35
|
+
}
|
|
36
|
+
function decodeOutboxRow(row) {
|
|
37
|
+
return {
|
|
35
38
|
seq: row.seq,
|
|
36
39
|
clientCommitId: row.client_commit_id,
|
|
37
40
|
createdAtMs: row.created_at_ms,
|
|
38
41
|
operations: JSON.parse(row.operations),
|
|
39
|
-
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Keyset pages bound staging; laziness decodes only commits consumed by the encoder. */
|
|
45
|
+
export function* iterateOutbox(db, throughSeq) {
|
|
46
|
+
let afterSeq = 0;
|
|
47
|
+
while (afterSeq < throughSeq) {
|
|
48
|
+
const rows = db.query(`SELECT seq, client_commit_id, created_at_ms, operations FROM _syncular_outbox
|
|
49
|
+
WHERE seq > ? AND seq <= ? ORDER BY seq ASC LIMIT 32`, [afterSeq, throughSeq]);
|
|
50
|
+
if (rows.length === 0)
|
|
51
|
+
return;
|
|
52
|
+
for (const row of rows) {
|
|
53
|
+
const commit = decodeOutboxRow(row);
|
|
54
|
+
afterSeq = commit.seq;
|
|
55
|
+
yield commit;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Routine status reads never load operation bodies. */
|
|
60
|
+
export function countOutbox(db) {
|
|
61
|
+
return db.query('SELECT COUNT(*) AS count FROM _syncular_outbox')[0]
|
|
62
|
+
.count;
|
|
40
63
|
}
|
|
41
64
|
export function deleteOutboxCommit(db, clientCommitId) {
|
|
42
65
|
db.exec('DELETE FROM _syncular_outbox_before_images WHERE client_commit_id = ?', [clientCommitId]);
|
package/dist/reactive-store.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type SyncAvailability } from './availability.js';
|
|
2
|
-
import type { CommitOutcome, QueryReadSpec, QuerySnapshot, WindowCoverage, WindowState } from './client.js';
|
|
2
|
+
import type { CommitOutcome, ClientSnapshotReader, QueryReadSpec, QuerySnapshot, WindowCoverage, WindowState } from './client.js';
|
|
3
3
|
import type { SqlValue } from './database.js';
|
|
4
4
|
import type { ClientChangeListener, SyncStatusSnapshot } from './invalidation.js';
|
|
5
5
|
import type { LeadershipState } from './multi-tab.js';
|
|
@@ -27,16 +27,14 @@ export interface LiveQueryResult<Row> {
|
|
|
27
27
|
readonly isRefreshing: boolean;
|
|
28
28
|
readonly availability: SyncAvailability;
|
|
29
29
|
}
|
|
30
|
-
export interface ReactiveQueryClient {
|
|
30
|
+
export interface ReactiveQueryClient extends Pick<ClientSnapshotReader, 'statusSnapshot' | 'commitOutcomes'> {
|
|
31
31
|
readonly currentSchemaVersion?: number;
|
|
32
32
|
onChange(listener: ClientChangeListener): () => void;
|
|
33
33
|
querySnapshot<Row = Record<string, SqlValue>>(spec: QueryReadSpec): QuerySnapshot<Row> | Promise<QuerySnapshot<Row>>;
|
|
34
|
-
statusSnapshot(): SyncStatusSnapshot | Promise<SyncStatusSnapshot>;
|
|
35
34
|
leadershipSnapshot?(): LeadershipState | undefined;
|
|
36
35
|
onLeadershipChange?(listener: (state: LeadershipState) => void): () => void;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
commitOutcomes(): readonly CommitOutcome[] | Promise<readonly CommitOutcome[]>;
|
|
36
|
+
conflicts(): readonly unknown[] | Promise<readonly unknown[]>;
|
|
37
|
+
rejections(): readonly unknown[] | Promise<readonly unknown[]>;
|
|
40
38
|
setWindow(base: WindowBase, units: readonly string[]): void | Promise<void>;
|
|
41
39
|
windowState(base: WindowBase): WindowState | Promise<WindowState>;
|
|
42
40
|
}
|
|
@@ -85,6 +83,14 @@ export declare class ReactiveClientStore {
|
|
|
85
83
|
window(base: WindowBase): ExternalStoreEntry<WindowState>;
|
|
86
84
|
setWindowClaim(owner: symbol, base: WindowBase, units: readonly string[]): Promise<void>;
|
|
87
85
|
releaseWindowClaims(owner: symbol): void;
|
|
86
|
+
/** Retained observation counts for diagnostics and resource benchmarks. */
|
|
87
|
+
cacheStats(): {
|
|
88
|
+
queries: number;
|
|
89
|
+
activeQueries: number;
|
|
90
|
+
windows: number;
|
|
91
|
+
activeWindows: number;
|
|
92
|
+
windowClaims: number;
|
|
93
|
+
};
|
|
88
94
|
start(): void;
|
|
89
95
|
dispose(): void;
|
|
90
96
|
}
|