@syncular/client 0.15.47 → 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 +59 -0
- package/dist/bun-database.d.ts +5 -0
- package/dist/bun-database.js +5 -0
- package/dist/client.d.ts +17 -20
- package/dist/client.js +152 -60
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- 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/remote.d.ts +2 -0
- package/dist/remote.js +10 -2
- package/dist/sync-scheduler.d.ts +23 -0
- package/dist/sync-scheduler.js +122 -0
- package/dist/window.d.ts +5 -0
- package/dist/window.js +39 -0
- package/dist/worker-entry.js +8 -19
- 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/bun-database.ts +11 -0
- package/src/client.ts +203 -63
- package/src/index.ts +1 -0
- package/src/outbox.ts +39 -8
- package/src/reactive-store.ts +226 -56
- package/src/realtime-supervisor.ts +2 -11
- package/src/remote.ts +11 -2
- package/src/sync-scheduler.ts +154 -0
- package/src/window.ts +65 -0
- package/src/worker-entry.ts +10 -21
- 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/dist/remote.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Database-less SSP2 producer (§6.10). It prepares and sends ordinary commits
|
|
3
3
|
* through the existing push path without creating a local replica or outbox.
|
|
4
4
|
*/
|
|
5
|
-
import { decodeMessage, decodeRemoteOperationResponse, decodeRemoteOperationRealtimeMessage, encodeMessage, encodeRemoteOperationRequest, encodeRemoteOperationRealtimeMessage, encodeRow,
|
|
5
|
+
import { decodeMessage, decodeRemoteOperationResponse, decodeRemoteOperationRealtimeMessage, encodeMessage, encodeRemoteOperationRequest, encodeRemoteOperationRealtimeMessage, encodeRow, } from '@syncular/core';
|
|
6
6
|
import { encryptRowValues } from './encryption.js';
|
|
7
7
|
import { ClientSyncError } from './errors.js';
|
|
8
8
|
import { compileClientSchema, recordToRowValues, } from './schema.js';
|
|
@@ -71,10 +71,14 @@ export class SyncRemoteClient {
|
|
|
71
71
|
#operationSocketGeneration = 0;
|
|
72
72
|
#watches = new Map();
|
|
73
73
|
#encryption;
|
|
74
|
+
#logEpoch;
|
|
74
75
|
constructor(config) {
|
|
75
76
|
if (config.clientId.length === 0) {
|
|
76
77
|
throw invalid('SyncRemoteClient clientId must be non-empty');
|
|
77
78
|
}
|
|
79
|
+
if (config.logEpoch !== undefined && config.logEpoch.length === 0) {
|
|
80
|
+
throw invalid('SyncRemoteClient logEpoch must be non-empty');
|
|
81
|
+
}
|
|
78
82
|
this.#schema =
|
|
79
83
|
config.schema === undefined
|
|
80
84
|
? undefined
|
|
@@ -84,6 +88,7 @@ export class SyncRemoteClient {
|
|
|
84
88
|
this.#operations = config.operations;
|
|
85
89
|
this.#operationRealtime = config.operationRealtime;
|
|
86
90
|
this.#encryption = config.encryption;
|
|
91
|
+
this.#logEpoch = config.logEpoch;
|
|
87
92
|
}
|
|
88
93
|
async prepareCommit(input) {
|
|
89
94
|
const schema = this.#schema;
|
|
@@ -137,13 +142,16 @@ export class SyncRemoteClient {
|
|
|
137
142
|
return {
|
|
138
143
|
requestId: input.requestId,
|
|
139
144
|
bytes: encodeMessage({
|
|
140
|
-
wireVersion:
|
|
145
|
+
wireVersion: this.#logEpoch === undefined ? 1 : 2,
|
|
141
146
|
msgKind: 'request',
|
|
142
147
|
frames: [
|
|
143
148
|
{
|
|
144
149
|
type: 'REQ_HEADER',
|
|
145
150
|
clientId: this.#clientId,
|
|
146
151
|
schemaVersion: schema.version,
|
|
152
|
+
...(this.#logEpoch !== undefined
|
|
153
|
+
? { logEpoch: this.#logEpoch }
|
|
154
|
+
: {}),
|
|
147
155
|
},
|
|
148
156
|
{
|
|
149
157
|
type: 'PUSH_COMMIT',
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { WakeReason } from '@syncular/core';
|
|
2
|
+
import type { SecurityLifecycle } from './client.js';
|
|
3
|
+
import type { SyncIntent } from './invalidation.js';
|
|
4
|
+
export interface SyncSchedulerClient {
|
|
5
|
+
readonly syncNeeded: boolean;
|
|
6
|
+
readonly securityLifecycle: SecurityLifecycle;
|
|
7
|
+
syncUntilIdle(maxRounds?: number): Promise<unknown>;
|
|
8
|
+
onSyncNeeded(listener: (reason: 'startup' | 'hello' | WakeReason) => void): () => void;
|
|
9
|
+
onSyncIntent(listener: (intent: SyncIntent) => void): () => void;
|
|
10
|
+
}
|
|
11
|
+
export interface SyncSchedulerOptions {
|
|
12
|
+
readonly maxRounds?: number;
|
|
13
|
+
readonly onError?: (error: unknown) => void;
|
|
14
|
+
readonly now?: () => number;
|
|
15
|
+
readonly queueMicrotask?: (callback: () => void) => void;
|
|
16
|
+
readonly schedule?: (callback: () => void, delayMs: number) => () => void;
|
|
17
|
+
}
|
|
18
|
+
export interface SyncScheduler {
|
|
19
|
+
readonly stopped: boolean;
|
|
20
|
+
stop(): void;
|
|
21
|
+
}
|
|
22
|
+
/** Install the event-driven single-flight host loop for a direct client. */
|
|
23
|
+
export declare function installSyncScheduler(client: SyncSchedulerClient, options?: SyncSchedulerOptions): SyncScheduler;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/** Install the event-driven single-flight host loop for a direct client. */
|
|
2
|
+
export function installSyncScheduler(client, options = {}) {
|
|
3
|
+
const now = options.now ?? Date.now;
|
|
4
|
+
const enqueue = options.queueMicrotask ?? globalThis.queueMicrotask;
|
|
5
|
+
const schedule = options.schedule ??
|
|
6
|
+
((callback, delayMs) => {
|
|
7
|
+
const timer = globalThis.setTimeout(callback, delayMs);
|
|
8
|
+
return () => globalThis.clearTimeout(timer);
|
|
9
|
+
});
|
|
10
|
+
let stopped = false;
|
|
11
|
+
let running = false;
|
|
12
|
+
let immediatePending = false;
|
|
13
|
+
let immediateQueued = false;
|
|
14
|
+
let backgroundReady = false;
|
|
15
|
+
let backgroundDue = Number.POSITIVE_INFINITY;
|
|
16
|
+
let cancelBackground;
|
|
17
|
+
const report = (error) => {
|
|
18
|
+
if (options.onError !== undefined) {
|
|
19
|
+
options.onError(error);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const root = globalThis;
|
|
23
|
+
if (root.reportError !== undefined)
|
|
24
|
+
root.reportError(error);
|
|
25
|
+
else
|
|
26
|
+
console.error(error);
|
|
27
|
+
};
|
|
28
|
+
const clearBackground = () => {
|
|
29
|
+
cancelBackground?.();
|
|
30
|
+
cancelBackground = undefined;
|
|
31
|
+
backgroundReady = false;
|
|
32
|
+
backgroundDue = Number.POSITIVE_INFINITY;
|
|
33
|
+
};
|
|
34
|
+
const queueImmediate = () => {
|
|
35
|
+
immediatePending = true;
|
|
36
|
+
if (stopped || running || immediateQueued)
|
|
37
|
+
return;
|
|
38
|
+
immediateQueued = true;
|
|
39
|
+
enqueue(() => {
|
|
40
|
+
immediateQueued = false;
|
|
41
|
+
if (stopped || !immediatePending)
|
|
42
|
+
return;
|
|
43
|
+
immediatePending = false;
|
|
44
|
+
run();
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
const run = () => {
|
|
48
|
+
if (stopped || running)
|
|
49
|
+
return;
|
|
50
|
+
if (client.securityLifecycle === 'preflight') {
|
|
51
|
+
immediatePending = false;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
running = true;
|
|
55
|
+
backgroundReady = false;
|
|
56
|
+
void client
|
|
57
|
+
.syncUntilIdle(options.maxRounds)
|
|
58
|
+
.catch((error) => {
|
|
59
|
+
if (!stopped)
|
|
60
|
+
report(error);
|
|
61
|
+
})
|
|
62
|
+
.finally(() => {
|
|
63
|
+
running = false;
|
|
64
|
+
if (stopped)
|
|
65
|
+
return;
|
|
66
|
+
if (immediatePending || backgroundReady) {
|
|
67
|
+
queueImmediate();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (cancelBackground !== undefined && backgroundDue <= now()) {
|
|
71
|
+
clearBackground();
|
|
72
|
+
queueImmediate();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
const consume = (intent) => {
|
|
77
|
+
if (stopped)
|
|
78
|
+
return;
|
|
79
|
+
if (intent.kind === 'none') {
|
|
80
|
+
clearBackground();
|
|
81
|
+
immediatePending = false;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (intent.kind === 'interactive') {
|
|
85
|
+
clearBackground();
|
|
86
|
+
queueImmediate();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (immediatePending || immediateQueued)
|
|
90
|
+
return;
|
|
91
|
+
clearBackground();
|
|
92
|
+
backgroundDue = now() + Math.max(0, intent.delayMs);
|
|
93
|
+
cancelBackground = schedule(() => {
|
|
94
|
+
cancelBackground = undefined;
|
|
95
|
+
backgroundDue = Number.POSITIVE_INFINITY;
|
|
96
|
+
backgroundReady = true;
|
|
97
|
+
if (!running)
|
|
98
|
+
queueImmediate();
|
|
99
|
+
}, Math.max(0, intent.delayMs));
|
|
100
|
+
};
|
|
101
|
+
const unsubscribeNeeded = client.onSyncNeeded(() => {
|
|
102
|
+
consume({ kind: 'interactive' });
|
|
103
|
+
});
|
|
104
|
+
const unsubscribeIntent = client.onSyncIntent(consume);
|
|
105
|
+
if (client.syncNeeded && client.securityLifecycle === 'active') {
|
|
106
|
+
consume({ kind: 'interactive' });
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
get stopped() {
|
|
110
|
+
return stopped;
|
|
111
|
+
},
|
|
112
|
+
stop() {
|
|
113
|
+
if (stopped)
|
|
114
|
+
return;
|
|
115
|
+
stopped = true;
|
|
116
|
+
clearBackground();
|
|
117
|
+
immediatePending = false;
|
|
118
|
+
unsubscribeNeeded();
|
|
119
|
+
unsubscribeIntent();
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
package/dist/window.d.ts
CHANGED
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { type ScopeMap } from '@syncular/core';
|
|
16
16
|
import type { ClientDatabase } from './database.js';
|
|
17
|
+
export type TimeBucketUnit = 'month';
|
|
18
|
+
/** Derive the immutable UTC scope value stored when a row is created. */
|
|
19
|
+
export declare function creationTimeBucket(createdAtMs: number, unit: TimeBucketUnit): string;
|
|
20
|
+
/** Return a rolling UTC month window ordered from oldest to newest. */
|
|
21
|
+
export declare function last(count: number, unit: TimeBucketUnit, nowMs?: number): string[];
|
|
17
22
|
/**
|
|
18
23
|
* A window base: one table, one variable whose values are the window
|
|
19
24
|
* units, and any FIXED scopes every unit shares (other variables pinned
|
package/dist/window.js
CHANGED
|
@@ -13,6 +13,45 @@
|
|
|
13
13
|
* transaction and the invalidation choke point.
|
|
14
14
|
*/
|
|
15
15
|
import { canonicalScopeJson } from '@syncular/core';
|
|
16
|
+
import { ClientSyncError } from './errors.js';
|
|
17
|
+
const MAX_TIME_BUCKET_MS = 253_402_300_799_999;
|
|
18
|
+
function monthBucket(year, month) {
|
|
19
|
+
return `${year.toString().padStart(4, '0')}-${month.toString().padStart(2, '0')}`;
|
|
20
|
+
}
|
|
21
|
+
/** Derive the immutable UTC scope value stored when a row is created. */
|
|
22
|
+
export function creationTimeBucket(createdAtMs, unit) {
|
|
23
|
+
if (unit !== 'month' ||
|
|
24
|
+
!Number.isSafeInteger(createdAtMs) ||
|
|
25
|
+
createdAtMs < 0 ||
|
|
26
|
+
createdAtMs > MAX_TIME_BUCKET_MS) {
|
|
27
|
+
throw new ClientSyncError('sync.invalid_request', 'creationTimeBucket requires a supported unit and a UTC timestamp from 1970 through 9999');
|
|
28
|
+
}
|
|
29
|
+
const date = new Date(createdAtMs);
|
|
30
|
+
return monthBucket(date.getUTCFullYear(), date.getUTCMonth() + 1);
|
|
31
|
+
}
|
|
32
|
+
/** Return a rolling UTC month window ordered from oldest to newest. */
|
|
33
|
+
export function last(count, unit, nowMs = Date.now()) {
|
|
34
|
+
if (unit !== 'month' ||
|
|
35
|
+
!Number.isSafeInteger(count) ||
|
|
36
|
+
count < 1 ||
|
|
37
|
+
count > 1_200 ||
|
|
38
|
+
!Number.isSafeInteger(nowMs) ||
|
|
39
|
+
nowMs < 0 ||
|
|
40
|
+
nowMs > MAX_TIME_BUCKET_MS) {
|
|
41
|
+
throw new ClientSyncError('sync.invalid_request', 'last requires a supported unit, a count from 1 through 1200, and a UTC timestamp from 1970 through 9999');
|
|
42
|
+
}
|
|
43
|
+
const date = new Date(nowMs);
|
|
44
|
+
const current = date.getUTCFullYear() * 12 + date.getUTCMonth();
|
|
45
|
+
if (current - (count - 1) < 1970 * 12) {
|
|
46
|
+
throw new ClientSyncError('sync.invalid_request', 'last requires every returned UTC month to fall from 1970 through 9999');
|
|
47
|
+
}
|
|
48
|
+
const units = [];
|
|
49
|
+
for (let offset = count - 1; offset >= 0; offset -= 1) {
|
|
50
|
+
const value = current - offset;
|
|
51
|
+
units.push(monthBucket(Math.floor(value / 12), (value % 12) + 1));
|
|
52
|
+
}
|
|
53
|
+
return units;
|
|
54
|
+
}
|
|
16
55
|
/**
|
|
17
56
|
* A stable, server-opaque key for a window base — table + variable +
|
|
18
57
|
* canonical fixed scopes. Two `setWindow` calls with the same base
|
package/dist/worker-entry.js
CHANGED
|
@@ -91,7 +91,7 @@ export function startSyncWorker(overrides = {}) {
|
|
|
91
91
|
autoSyncScheduled = false;
|
|
92
92
|
if (closed ||
|
|
93
93
|
client === undefined ||
|
|
94
|
-
client.securityLifecycle === 'preflight') {
|
|
94
|
+
client.securityLifecycle() === 'preflight') {
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
97
|
const running = client;
|
|
@@ -113,7 +113,7 @@ export function startSyncWorker(overrides = {}) {
|
|
|
113
113
|
if (!autoSync ||
|
|
114
114
|
closed ||
|
|
115
115
|
client === undefined ||
|
|
116
|
-
client.securityLifecycle === 'preflight' ||
|
|
116
|
+
client.securityLifecycle() === 'preflight' ||
|
|
117
117
|
intent.kind === 'none') {
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
@@ -147,9 +147,6 @@ export function startSyncWorker(overrides = {}) {
|
|
|
147
147
|
autoSyncScheduled = true;
|
|
148
148
|
queueMicrotask(runAutoSync);
|
|
149
149
|
}
|
|
150
|
-
function consumeEffects(effects) {
|
|
151
|
-
consumeSyncIntent(effects.sync);
|
|
152
|
-
}
|
|
153
150
|
function requireClient() {
|
|
154
151
|
if (client === undefined) {
|
|
155
152
|
throw new ClientSyncError(WORKER_FAILED_CODE, 'the worker received a call before init completed');
|
|
@@ -280,12 +277,12 @@ export function startSyncWorker(overrides = {}) {
|
|
|
280
277
|
// `start()` may discover persisted subscriptions/outbox work. Its callback
|
|
281
278
|
// fires before this worker publishes the initialized client, so consume
|
|
282
279
|
// the durable state once here as well; coalescing makes this a single task.
|
|
283
|
-
if (started.syncNeeded)
|
|
280
|
+
if (started.statusSnapshot().syncNeeded)
|
|
284
281
|
consumeSyncIntent({ kind: 'interactive' });
|
|
285
282
|
return { clientId: started.clientId };
|
|
286
283
|
}
|
|
287
284
|
const api = {
|
|
288
|
-
securityLifecycle: () => requireClient().securityLifecycle,
|
|
285
|
+
securityLifecycle: () => requireClient().securityLifecycle(),
|
|
289
286
|
beginSecurityPreflight: async () => {
|
|
290
287
|
if (backgroundTimer !== undefined)
|
|
291
288
|
clearTimeout(backgroundTimer);
|
|
@@ -304,20 +301,16 @@ export function startSyncWorker(overrides = {}) {
|
|
|
304
301
|
subscribe: (input) => requireClient().subscribe(input),
|
|
305
302
|
unsubscribe: (id) => requireClient().unsubscribe(id),
|
|
306
303
|
setWindow: async (base, units) => {
|
|
307
|
-
|
|
308
|
-
consumeEffects(result.effects);
|
|
304
|
+
await requireClient().setWindowCommand(base, units);
|
|
309
305
|
},
|
|
310
306
|
windowState: (base) => requireClient().windowState(base),
|
|
311
307
|
mutate: (mutations) => {
|
|
312
|
-
|
|
313
|
-
consumeEffects(result.effects);
|
|
314
|
-
return result.value;
|
|
308
|
+
return requireClient().mutateCommand(mutations).value;
|
|
315
309
|
},
|
|
316
310
|
patch: (table, rowId, partial, options) => {
|
|
317
311
|
// Same §8.4 rule as `mutate`: a local write must push without the app
|
|
318
312
|
// orchestrating sync, so consume the core's immediate intent.
|
|
319
313
|
const result = requireClient().patchCommand(table, rowId, partial, options);
|
|
320
|
-
consumeEffects(result.effects);
|
|
321
314
|
return result.value;
|
|
322
315
|
},
|
|
323
316
|
purgeLocalData: (input) => requireClient().purgeLocalData(input),
|
|
@@ -343,15 +336,11 @@ export function startSyncWorker(overrides = {}) {
|
|
|
343
336
|
realtime: snapshot.host.realtime,
|
|
344
337
|
});
|
|
345
338
|
},
|
|
346
|
-
conflicts: () => requireClient().conflicts,
|
|
347
|
-
rejections: () => requireClient().rejections,
|
|
339
|
+
conflicts: () => requireClient().conflicts(),
|
|
340
|
+
rejections: () => requireClient().rejections(),
|
|
348
341
|
commitOutcome: (clientCommitId) => requireClient().commitOutcome(clientCommitId),
|
|
349
342
|
commitOutcomes: (query) => requireClient().commitOutcomes(query),
|
|
350
343
|
resolveCommitOutcome: (input) => requireClient().resolveCommitOutcome(input),
|
|
351
|
-
schemaFloor: () => requireClient().schemaFloor,
|
|
352
|
-
leaseState: () => requireClient().leaseState,
|
|
353
|
-
upgrading: () => requireClient().upgrading,
|
|
354
|
-
syncNeeded: () => requireClient().syncNeeded,
|
|
355
344
|
pendingCommits: () => requireClient().pendingCommits(),
|
|
356
345
|
subscriptions: () => requireClient().subscriptions(),
|
|
357
346
|
subscription: (id) => requireClient().subscription(id),
|
package/dist/worker-host.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PromiseMethods } from './client.js';
|
|
1
2
|
/**
|
|
2
3
|
* Main-thread side of the worker mode and the
|
|
3
4
|
* multi-tab topology.
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
*/
|
|
24
25
|
import type { WakeReason } from '@syncular/core';
|
|
25
26
|
import type { BlobRef, CachedBlob } from './blob.js';
|
|
26
|
-
import type { ConflictRecord,
|
|
27
|
+
import type { ConflictRecord, MutationInput, PresencePeer, QueryReadSpec, QuerySnapshot, RejectionRecord, SecurityLifecycle, SubscribeInput, SyncClientLimits, SyncSummary, WindowState } from './client.js';
|
|
27
28
|
import type { SqlRow, SqlValue } from './database.js';
|
|
28
29
|
import { ClientDiagnosticsEmitter, type ClientDiagnosticsListener, type ClientDiagnosticsRequest, type ClientDiagnosticsSnapshot } from './diagnostics.js';
|
|
29
30
|
import type { EncryptionKeyringConfig } from './encryption.js';
|
|
@@ -38,7 +39,7 @@ import type { CommitOutcome, CommitOutcomeQuery, ResolveCommitOutcomeInput } fro
|
|
|
38
39
|
import type { ClientSchema } from './schema.js';
|
|
39
40
|
import type { SubscriptionRecord } from './state.js';
|
|
40
41
|
import type { WindowBase } from './window.js';
|
|
41
|
-
import { type SyncWorkerEvent, type WorkerDatabaseInit, type WorkerEndpoints, type WorkerErrorShape, type WorkerSecurityActivation } from './worker-protocol.js';
|
|
42
|
+
import { type SyncWorkerEvent, type WorkerApi, type WorkerDatabaseInit, type WorkerEndpoints, type WorkerErrorShape, type WorkerSecurityActivation } from './worker-protocol.js';
|
|
42
43
|
/** Classify startup without echoing a chunk URL or bundler text to UI/logs. */
|
|
43
44
|
export declare function workerStartupError(message: unknown): ClientSyncError;
|
|
44
45
|
export type HandleRole = 'leader' | 'follower';
|
|
@@ -132,7 +133,7 @@ interface LeaderCore {
|
|
|
132
133
|
* promise. `role` is `'leader'` (owns the worker) or `'follower'` (proxies to
|
|
133
134
|
* the leader over the channel). Constructed via {@link createSyncClientHandle}.
|
|
134
135
|
*/
|
|
135
|
-
export declare class SyncClientHandle {
|
|
136
|
+
export declare class SyncClientHandle implements PromiseMethods<WorkerApi> {
|
|
136
137
|
#private;
|
|
137
138
|
/** True only for a leader handle. Kept for the pre-multiTab contract. */
|
|
138
139
|
get isLeader(): boolean;
|
|
@@ -214,11 +215,7 @@ export declare class SyncClientHandle {
|
|
|
214
215
|
commitOutcome(clientCommitId: string): Promise<CommitOutcome | undefined>;
|
|
215
216
|
commitOutcomes(query?: CommitOutcomeQuery): Promise<readonly CommitOutcome[]>;
|
|
216
217
|
resolveCommitOutcome(input: ResolveCommitOutcomeInput): Promise<CommitOutcome>;
|
|
217
|
-
schemaFloor(): Promise<SchemaFloor | undefined>;
|
|
218
|
-
leaseState(): Promise<LeaseState | undefined>;
|
|
219
218
|
/** §7.4.5: true while a schema-bump reset + first re-bootstrap runs. */
|
|
220
|
-
upgrading(): Promise<boolean>;
|
|
221
|
-
syncNeeded(): Promise<boolean>;
|
|
222
219
|
pendingCommits(): Promise<OutboxCommit[]>;
|
|
223
220
|
subscriptions(): Promise<SubscriptionRecord[]>;
|
|
224
221
|
subscription(id: string): Promise<SubscriptionRecord | undefined>;
|
package/dist/worker-host.js
CHANGED
|
@@ -104,12 +104,12 @@ export class SyncClientHandle {
|
|
|
104
104
|
ref: this,
|
|
105
105
|
clientId: () => this.#clientId,
|
|
106
106
|
role: () => this.#role,
|
|
107
|
-
outbox: async () => (await this.
|
|
107
|
+
outbox: async () => (await this.statusSnapshot()).outbox,
|
|
108
108
|
subscriptions: () => this.subscriptions(),
|
|
109
109
|
conflicts: async () => (await this.conflicts()).length,
|
|
110
110
|
rejections: async () => (await this.rejections()).length,
|
|
111
|
-
syncNeeded: () => this.
|
|
112
|
-
upgrading: () => this.
|
|
111
|
+
syncNeeded: async () => (await this.statusSnapshot()).syncNeeded,
|
|
112
|
+
upgrading: async () => (await this.statusSnapshot()).upgrading,
|
|
113
113
|
onInvalidate: (listener) => this.onInvalidate(listener),
|
|
114
114
|
});
|
|
115
115
|
}
|
|
@@ -310,19 +310,7 @@ export class SyncClientHandle {
|
|
|
310
310
|
resolveCommitOutcome(input) {
|
|
311
311
|
return this.#call('resolveCommitOutcome', [input]);
|
|
312
312
|
}
|
|
313
|
-
schemaFloor() {
|
|
314
|
-
return this.#call('schemaFloor', []);
|
|
315
|
-
}
|
|
316
|
-
leaseState() {
|
|
317
|
-
return this.#call('leaseState', []);
|
|
318
|
-
}
|
|
319
313
|
/** §7.4.5: true while a schema-bump reset + first re-bootstrap runs. */
|
|
320
|
-
upgrading() {
|
|
321
|
-
return this.#call('upgrading', []);
|
|
322
|
-
}
|
|
323
|
-
syncNeeded() {
|
|
324
|
-
return this.#call('syncNeeded', []);
|
|
325
|
-
}
|
|
326
314
|
pendingCommits() {
|
|
327
315
|
return this.#call('pendingCommits', []);
|
|
328
316
|
}
|
|
@@ -18,15 +18,14 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import type { WakeReason } from '@syncular/core';
|
|
20
20
|
import type { BlobRef, CachedBlob } from './blob.js';
|
|
21
|
-
import type { ConflictRecord,
|
|
21
|
+
import type { ConflictRecord, ClientSnapshotMethods, MutationInput, PresencePeer, QueryReadSpec, QuerySnapshot, SecurityLifecycle, SubscribeInput, SyncClientLimits, SyncSummary, WindowState } from './client.js';
|
|
22
22
|
import type { SqlRow, SqlValue } from './database.js';
|
|
23
|
-
import type {
|
|
23
|
+
import type { ClientDiagnosticsSnapshot } from './diagnostics.js';
|
|
24
24
|
import type { EncryptionKeyringConfig } from './encryption.js';
|
|
25
|
-
import type { ClientChangeBatch, LocalRevision
|
|
25
|
+
import type { ClientChangeBatch, LocalRevision } from './invalidation.js';
|
|
26
26
|
import type { LocalDataPurgeInput, LocalDataPurgeResult } from './local-purge.js';
|
|
27
27
|
import type { LocalDataRebootstrapInput, LocalDataRebootstrapResult } from './local-rebootstrap.js';
|
|
28
28
|
import type { OutboxCommit } from './outbox.js';
|
|
29
|
-
import type { CommitOutcome, CommitOutcomeQuery, ResolveCommitOutcomeInput } from './outcomes.js';
|
|
30
29
|
import type { ClientSchema } from './schema.js';
|
|
31
30
|
import type { SubscriptionRecord } from './state.js';
|
|
32
31
|
import type { WindowBase } from './window.js';
|
|
@@ -88,7 +87,7 @@ export interface WorkerInitResult {
|
|
|
88
87
|
export interface WorkerSecurityActivation {
|
|
89
88
|
readonly encryption?: EncryptionKeyringConfig;
|
|
90
89
|
}
|
|
91
|
-
export interface WorkerApi {
|
|
90
|
+
export interface WorkerApi extends Omit<ClientSnapshotMethods, 'querySnapshot'> {
|
|
92
91
|
securityLifecycle(): SecurityLifecycle;
|
|
93
92
|
beginSecurityPreflight(): Promise<void>;
|
|
94
93
|
activateSecurity(options?: WorkerSecurityActivation): Promise<void>;
|
|
@@ -112,19 +111,6 @@ export interface WorkerApi {
|
|
|
112
111
|
query(sql: string, params?: readonly SqlValue[]): SqlRow[];
|
|
113
112
|
querySnapshot(spec: QueryReadSpec): QuerySnapshot;
|
|
114
113
|
localRevision(): LocalRevision;
|
|
115
|
-
statusSnapshot(): SyncStatusSnapshot;
|
|
116
|
-
diagnosticsSnapshot(request?: ClientDiagnosticsRequest): ClientDiagnosticsSnapshot;
|
|
117
|
-
conflicts(): readonly ConflictRecord[];
|
|
118
|
-
rejections(): readonly RejectionRecord[];
|
|
119
|
-
commitOutcome(clientCommitId: string): CommitOutcome | undefined;
|
|
120
|
-
commitOutcomes(query?: CommitOutcomeQuery): readonly CommitOutcome[];
|
|
121
|
-
resolveCommitOutcome(input: ResolveCommitOutcomeInput): CommitOutcome;
|
|
122
|
-
schemaFloor(): SchemaFloor | undefined;
|
|
123
|
-
/** §7.3.5: the opaque auth-lease state, or undefined. */
|
|
124
|
-
leaseState(): LeaseState | undefined;
|
|
125
|
-
/** §7.4.5: true while a schema-bump reset + first re-bootstrap runs. */
|
|
126
|
-
upgrading(): boolean;
|
|
127
|
-
syncNeeded(): boolean;
|
|
128
114
|
pendingCommits(): OutboxCommit[];
|
|
129
115
|
subscriptions(): SubscriptionRecord[];
|
|
130
116
|
subscription(id: string): SubscriptionRecord | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Syncular TypeScript client core — offline-first sync over SQLite (WASM/OPFS, Bun, Node)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -34,14 +34,6 @@
|
|
|
34
34
|
"default": "./dist/index.js"
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
|
-
"./realtime-supervisor-observation": {
|
|
38
|
-
"bun": "./src/realtime-supervisor-observation.ts",
|
|
39
|
-
"browser": "./dist/realtime-supervisor-observation.js",
|
|
40
|
-
"import": {
|
|
41
|
-
"types": "./dist/realtime-supervisor-observation.d.ts",
|
|
42
|
-
"default": "./dist/realtime-supervisor-observation.js"
|
|
43
|
-
}
|
|
44
|
-
},
|
|
45
37
|
"./bun": {
|
|
46
38
|
"bun": "./src/bun-database.ts",
|
|
47
39
|
"browser": "./dist/bun-database.js",
|
|
@@ -97,9 +89,9 @@
|
|
|
97
89
|
},
|
|
98
90
|
"dependencies": {
|
|
99
91
|
"@sqlite.org/sqlite-wasm": "^3.53.0-build1",
|
|
100
|
-
"@syncular/core": "0.
|
|
92
|
+
"@syncular/core": "0.16.1"
|
|
101
93
|
},
|
|
102
94
|
"devDependencies": {
|
|
103
|
-
"@syncular/server": "0.
|
|
95
|
+
"@syncular/server": "0.16.1"
|
|
104
96
|
}
|
|
105
97
|
}
|
package/src/bun-database.ts
CHANGED
|
@@ -15,6 +15,12 @@ import {
|
|
|
15
15
|
type SqlValue,
|
|
16
16
|
} from './database';
|
|
17
17
|
|
|
18
|
+
declare module 'bun:sqlite' {
|
|
19
|
+
interface Database {
|
|
20
|
+
clearQueryCache(): void;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
type BunParam = string | number | bigint | Uint8Array | null;
|
|
19
25
|
|
|
20
26
|
function coerceParams(params: readonly SqlValue[]): BunParam[] {
|
|
@@ -34,6 +40,11 @@ export class BunClientDatabase implements ClientDatabase {
|
|
|
34
40
|
|
|
35
41
|
exec(sql: string, params: readonly SqlValue[] = []): void {
|
|
36
42
|
this.db.query(sql).run(...coerceParams(params));
|
|
43
|
+
// `Database.query()` caches prepared statements. Clear that cache after
|
|
44
|
+
// schema DDL so a reset does not reprepare every later row upsert.
|
|
45
|
+
if (/^\s*(?:CREATE|DROP|ALTER)\b/i.test(sql)) {
|
|
46
|
+
this.db.clearQueryCache();
|
|
47
|
+
}
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
query(sql: string, params: readonly SqlValue[] = []): SqlRow[] {
|