@syncular/client 0.1.3 → 0.2.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 +227 -709
- package/dist/apply.d.ts +80 -0
- package/dist/apply.js +301 -0
- package/dist/blob.d.ts +127 -0
- package/dist/blob.js +201 -0
- package/dist/bun-database.d.ts +22 -0
- package/dist/bun-database.js +61 -0
- package/dist/client.d.ts +320 -69
- package/dist/client.js +1627 -163
- package/dist/content-type.d.ts +2 -0
- package/dist/content-type.js +2 -0
- package/dist/database.d.ts +35 -80
- package/dist/database.js +32 -947
- package/dist/encryption.d.ts +47 -0
- package/dist/encryption.js +75 -0
- package/dist/errors.d.ts +8 -22
- package/dist/errors.js +10 -207
- package/dist/http.d.ts +32 -0
- package/dist/http.js +196 -0
- package/dist/index.d.ts +28 -16
- package/dist/index.js +25 -12
- package/dist/invalidation.d.ts +69 -0
- package/dist/invalidation.js +84 -0
- package/dist/leader-lock.d.ts +28 -0
- package/dist/leader-lock.js +38 -0
- package/dist/multi-tab.d.ts +134 -0
- package/dist/multi-tab.js +399 -0
- package/dist/node-database.d.ts +41 -0
- package/dist/node-database.js +133 -0
- package/dist/outbox.d.ts +56 -0
- package/dist/outbox.js +140 -0
- package/dist/schema.d.ts +107 -0
- package/dist/schema.js +267 -0
- package/dist/state.d.ts +40 -0
- package/dist/state.js +77 -0
- package/dist/transport.d.ts +44 -0
- package/dist/transport.js +7 -0
- package/dist/wasm-database.d.ts +30 -0
- package/dist/wasm-database.js +170 -0
- package/dist/window.d.ts +64 -0
- package/dist/window.js +0 -0
- package/dist/worker-entry.d.ts +16 -2
- package/dist/worker-entry.js +300 -456
- package/dist/worker-host.d.ts +174 -0
- package/dist/worker-host.js +536 -0
- package/dist/worker-protocol.d.ts +173 -61
- package/dist/worker-protocol.js +7 -16
- package/package.json +45 -112
- package/src/apply.ts +435 -0
- package/src/blob.ts +333 -0
- package/src/bun-database.ts +75 -0
- package/src/client.ts +2247 -277
- package/src/content-type.ts +2 -0
- package/src/database.ts +63 -1389
- package/src/encryption.ts +123 -0
- package/src/errors.ts +11 -265
- package/src/http.ts +254 -0
- package/src/index.ts +25 -20
- package/src/invalidation.ts +128 -0
- package/src/leader-lock.ts +68 -0
- package/src/multi-tab.ts +550 -0
- package/src/node-database.ts +180 -0
- package/src/outbox.ts +206 -0
- package/src/schema.ts +397 -0
- package/src/state.ts +126 -0
- package/src/transport.ts +52 -0
- package/src/wasm-database.ts +279 -0
- package/src/window.ts +0 -0
- package/src/worker-entry.ts +391 -545
- package/src/worker-host.ts +800 -0
- package/src/worker-protocol.ts +204 -99
- package/dist/auth-leases.d.ts +0 -11
- package/dist/auth-leases.d.ts.map +0 -1
- package/dist/auth-leases.js +0 -140
- package/dist/auth-leases.js.map +0 -1
- package/dist/blob-limits.d.ts +0 -15
- package/dist/blob-limits.d.ts.map +0 -1
- package/dist/blob-limits.js +0 -66
- package/dist/blob-limits.js.map +0 -1
- package/dist/bridge-client.d.ts +0 -69
- package/dist/bridge-client.d.ts.map +0 -1
- package/dist/bridge-client.js +0 -282
- package/dist/bridge-client.js.map +0 -1
- package/dist/client-config.d.ts +0 -6
- package/dist/client-config.d.ts.map +0 -1
- package/dist/client-config.js +0 -18
- package/dist/client-config.js.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/command-history.d.ts +0 -35
- package/dist/command-history.d.ts.map +0 -1
- package/dist/command-history.js +0 -378
- package/dist/command-history.js.map +0 -1
- package/dist/console-diagnostics.d.ts +0 -33
- package/dist/console-diagnostics.d.ts.map +0 -1
- package/dist/console-diagnostics.js +0 -496
- package/dist/console-diagnostics.js.map +0 -1
- package/dist/crdt-yjs/index.d.ts +0 -4
- package/dist/crdt-yjs/index.d.ts.map +0 -1
- package/dist/crdt-yjs/index.js +0 -4
- package/dist/crdt-yjs/index.js.map +0 -1
- package/dist/crdt-yjs/webview-host-facade.d.ts +0 -126
- package/dist/crdt-yjs/webview-host-facade.d.ts.map +0 -1
- package/dist/crdt-yjs/webview-host-facade.js +0 -284
- package/dist/crdt-yjs/webview-host-facade.js.map +0 -1
- package/dist/crdt-yjs/yjs-document-field-adapter.d.ts +0 -153
- package/dist/crdt-yjs/yjs-document-field-adapter.d.ts.map +0 -1
- package/dist/crdt-yjs/yjs-document-field-adapter.js +0 -406
- package/dist/crdt-yjs/yjs-document-field-adapter.js.map +0 -1
- package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts +0 -73
- package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts.map +0 -1
- package/dist/crdt-yjs/yjs-prosemirror-bridge.js +0 -169
- package/dist/crdt-yjs/yjs-prosemirror-bridge.js.map +0 -1
- package/dist/database.d.ts.map +0 -1
- package/dist/database.js.map +0 -1
- package/dist/diagnostics.d.ts +0 -10
- package/dist/diagnostics.d.ts.map +0 -1
- package/dist/diagnostics.js +0 -83
- package/dist/diagnostics.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/generated-bridge.d.ts +0 -365
- package/dist/generated-bridge.d.ts.map +0 -1
- package/dist/generated-bridge.js +0 -250
- package/dist/generated-bridge.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/mutations.d.ts +0 -72
- package/dist/mutations.d.ts.map +0 -1
- package/dist/mutations.js +0 -63
- package/dist/mutations.js.map +0 -1
- package/dist/network.d.ts +0 -3
- package/dist/network.d.ts.map +0 -1
- package/dist/network.js +0 -17
- package/dist/network.js.map +0 -1
- package/dist/react/index.d.ts +0 -169
- package/dist/react/index.d.ts.map +0 -1
- package/dist/react/index.js +0 -628
- package/dist/react/index.js.map +0 -1
- package/dist/react-native/index.d.ts +0 -35
- package/dist/react-native/index.d.ts.map +0 -1
- package/dist/react-native/index.js +0 -49
- package/dist/react-native/index.js.map +0 -1
- package/dist/runtime-contract.d.ts +0 -13
- package/dist/runtime-contract.d.ts.map +0 -1
- package/dist/runtime-contract.js +0 -24
- package/dist/runtime-contract.js.map +0 -1
- package/dist/rust-client.d.ts +0 -106
- package/dist/rust-client.d.ts.map +0 -1
- package/dist/rust-client.js +0 -938
- package/dist/rust-client.js.map +0 -1
- package/dist/sentry.d.ts +0 -35
- package/dist/sentry.d.ts.map +0 -1
- package/dist/sentry.js +0 -155
- package/dist/sentry.js.map +0 -1
- package/dist/sql-safety.d.ts +0 -3
- package/dist/sql-safety.d.ts.map +0 -1
- package/dist/sql-safety.js +0 -62
- package/dist/sql-safety.js.map +0 -1
- package/dist/syncular-runtime-artifacts.json +0 -61
- package/dist/tauri/index.d.ts +0 -35
- package/dist/tauri/index.d.ts.map +0 -1
- package/dist/tauri/index.js +0 -114
- package/dist/tauri/index.js.map +0 -1
- package/dist/types.d.ts +0 -1096
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/wasm/.syncular-wasm-profile +0 -1
- package/dist/wasm/syncular-runtime-artifact.json +0 -21
- package/dist/wasm/syncular.d.ts +0 -207
- package/dist/wasm/syncular.js +0 -2341
- package/dist/wasm/syncular_bg.wasm +0 -0
- package/dist/wasm/syncular_bg.wasm.d.ts +0 -97
- package/dist/wasm-bindings/runtime-contract.d.ts +0 -22
- package/dist/wasm-bindings/runtime-contract.d.ts.map +0 -1
- package/dist/wasm-bindings/runtime-contract.js +0 -112
- package/dist/wasm-bindings/runtime-contract.js.map +0 -1
- package/dist/wasm-core/.syncular-wasm-profile +0 -1
- package/dist/wasm-core/syncular-runtime-artifact.json +0 -17
- package/dist/wasm-core/syncular.d.ts +0 -162
- package/dist/wasm-core/syncular.js +0 -1847
- package/dist/wasm-core/syncular_bg.wasm +0 -0
- package/dist/wasm-core/syncular_bg.wasm.d.ts +0 -77
- package/dist/wasm-perf/.syncular-wasm-profile +0 -1
- package/dist/wasm-perf/syncular-runtime-artifact.json +0 -21
- package/dist/wasm-perf/syncular.d.ts +0 -207
- package/dist/wasm-perf/syncular.js +0 -2341
- package/dist/wasm-perf/syncular_bg.wasm +0 -0
- package/dist/wasm-perf/syncular_bg.wasm.d.ts +0 -97
- package/dist/wasm-runtime.d.ts +0 -23
- package/dist/wasm-runtime.d.ts.map +0 -1
- package/dist/wasm-runtime.js +0 -69
- package/dist/wasm-runtime.js.map +0 -1
- package/dist/worker-client.d.ts +0 -123
- package/dist/worker-client.d.ts.map +0 -1
- package/dist/worker-client.js +0 -1735
- package/dist/worker-client.js.map +0 -1
- package/dist/worker-entry.d.ts.map +0 -1
- package/dist/worker-entry.js.map +0 -1
- package/dist/worker-protocol.d.ts.map +0 -1
- package/dist/worker-protocol.js.map +0 -1
- package/dist/worker-realtime.d.ts +0 -39
- package/dist/worker-realtime.d.ts.map +0 -1
- package/dist/worker-realtime.js +0 -677
- package/dist/worker-realtime.js.map +0 -1
- package/src/auth-leases.ts +0 -251
- package/src/blob-limits.ts +0 -98
- package/src/bridge-client.ts +0 -512
- package/src/client-config.ts +0 -29
- package/src/command-history.ts +0 -623
- package/src/console-diagnostics.ts +0 -617
- package/src/crdt-yjs/index.ts +0 -3
- package/src/crdt-yjs/webview-host-facade.ts +0 -477
- package/src/crdt-yjs/yjs-document-field-adapter.ts +0 -733
- package/src/crdt-yjs/yjs-prosemirror-bridge.ts +0 -272
- package/src/diagnostics.ts +0 -116
- package/src/generated-bridge.ts +0 -741
- package/src/mutations.ts +0 -168
- package/src/network.ts +0 -32
- package/src/react/index.ts +0 -1036
- package/src/react-native/index.ts +0 -152
- package/src/runtime-contract.ts +0 -48
- package/src/rust-client.ts +0 -1491
- package/src/sentry.ts +0 -215
- package/src/sql-safety.ts +0 -61
- package/src/tauri/index.ts +0 -211
- package/src/types.ts +0 -1397
- package/src/wasm-bindings/generated-wasm-bindings.d.ts +0 -70
- package/src/wasm-bindings/runtime-contract.ts +0 -158
- package/src/wasm-runtime.ts +0 -145
- package/src/worker-client.ts +0 -2289
- package/src/worker-realtime.ts +0 -843
package/src/worker-entry.ts
CHANGED
|
@@ -1,584 +1,430 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Worker-side bootstrap (Direction decision 2): constructs the WHOLE
|
|
3
|
+
* client core — SyncClient + transports + realtime + SQLite — inside the
|
|
4
|
+
* worker and serves the `worker-protocol` RPC over postMessage.
|
|
5
|
+
*
|
|
6
|
+
* Browsers get the defaults: opfs-sahpool persistence and the fetch/WS
|
|
7
|
+
* transports from `./http`, wired from `WorkerInitConfig.endpoints`.
|
|
8
|
+
* Tests inject a database factory (bun:sqlite) through `overrides` — the
|
|
9
|
+
* same bootstrap, the same RPC, a different SQLite.
|
|
10
|
+
*
|
|
11
|
+
* The §8.4 host loop lives HERE: wake-ups (hello/`sync` events, delta
|
|
12
|
+
* drops) coalesce into one jittered `syncUntilIdle` round in the worker;
|
|
13
|
+
* RPC-driven and auto-driven sync rounds serialize on one queue because
|
|
14
|
+
* the core owns exactly one loop.
|
|
15
|
+
*/
|
|
16
|
+
import { SyncClient } from './client';
|
|
17
|
+
import type { ClientDatabase } from './database';
|
|
18
|
+
import { ClientSyncError } from './errors';
|
|
3
19
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from './errors';
|
|
10
|
-
import {
|
|
11
|
-
dispatchGeneratedSyncularWorkerRequest,
|
|
12
|
-
generatedSyncularWorkerRequestDiagnosticSource,
|
|
13
|
-
isGeneratedSyncularAbortableWorkerRequestType,
|
|
14
|
-
isGeneratedSyncularDiagnosedSuccessWorkerRequestType,
|
|
15
|
-
} from './generated-bridge';
|
|
16
|
-
import { createSyncularRuntimeInfo } from './runtime-contract';
|
|
17
|
-
import { openSyncularRustClient, type SyncularRustClient } from './rust-client';
|
|
18
|
-
import type {
|
|
19
|
-
ResolvedSyncularClientConfig,
|
|
20
|
-
SyncularDiagnosticEvent,
|
|
21
|
-
} from './types';
|
|
22
|
-
import {
|
|
23
|
-
getSyncularRustRuntimeInfo,
|
|
24
|
-
getSyncularWasmGlueUrl,
|
|
25
|
-
getSyncularWasmUrl,
|
|
26
|
-
type SyncularWasmGlue,
|
|
27
|
-
} from './wasm-runtime';
|
|
20
|
+
httpBlobTransport,
|
|
21
|
+
httpSegmentDownloader,
|
|
22
|
+
httpSyncTransport,
|
|
23
|
+
webSocketRealtimeConnector,
|
|
24
|
+
} from './http';
|
|
28
25
|
import type {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
} from './
|
|
26
|
+
RealtimeConnector,
|
|
27
|
+
SegmentDownloader,
|
|
28
|
+
SyncTransport,
|
|
29
|
+
} from './transport';
|
|
30
|
+
import { openPersistentWasmDatabase } from './wasm-database';
|
|
34
31
|
import {
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
type MainToWorkerMessage,
|
|
33
|
+
WORKER_FAILED_CODE,
|
|
34
|
+
type WorkerApi,
|
|
35
|
+
type WorkerCallMessage,
|
|
36
|
+
type WorkerErrorShape,
|
|
37
|
+
type WorkerInitConfig,
|
|
38
|
+
type WorkerInitResult,
|
|
39
|
+
type WorkerToMainMessage,
|
|
37
40
|
} from './worker-protocol';
|
|
38
|
-
import { SyncularWorkerRealtimeController } from './worker-realtime';
|
|
39
|
-
|
|
40
|
-
let client: SyncularRustClient | undefined;
|
|
41
|
-
let openedConfig: ResolvedSyncularClientConfig | undefined;
|
|
42
|
-
let openedRuntime: SyncularWorkerRuntimeArtifact | undefined;
|
|
43
|
-
let removeRowsChangedListener: (() => void) | undefined;
|
|
44
|
-
const canceledRequests = new Set<number>();
|
|
45
|
-
const abortControllers = new Map<number, AbortController>();
|
|
46
|
-
const realtime = new SyncularWorkerRealtimeController({
|
|
47
|
-
getClient: requireClient,
|
|
48
|
-
getConfig: () => openedConfig,
|
|
49
|
-
getLocationOrigin: () =>
|
|
50
|
-
typeof self.location?.origin === 'string'
|
|
51
|
-
? self.location.origin
|
|
52
|
-
: 'http://localhost',
|
|
53
|
-
createWebSocket: (url) => new WebSocket(url),
|
|
54
|
-
postEvent: (event) => self.postMessage(event),
|
|
55
|
-
postDiagnostic,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
self.onmessage = (event: MessageEvent<SyncularWorkerRequest>) => {
|
|
59
|
-
void handleRequest(event.data);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
async function handleRequest(request: SyncularWorkerRequest): Promise<void> {
|
|
63
|
-
if (request.protocolVersion !== SYNCULAR_WORKER_PROTOCOL_VERSION) {
|
|
64
|
-
post({
|
|
65
|
-
id: request.id,
|
|
66
|
-
protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
|
|
67
|
-
ok: false,
|
|
68
|
-
error: createSyncularWorkerErrorPayload(
|
|
69
|
-
'worker.protocol_mismatch',
|
|
70
|
-
`Unsupported Syncular worker protocol ${request.protocolVersion}`,
|
|
71
|
-
{ details: { supported: SYNCULAR_WORKER_PROTOCOL_VERSION } }
|
|
72
|
-
),
|
|
73
|
-
});
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (request.type === 'cancel') {
|
|
78
|
-
canceledRequests.add(request.requestId);
|
|
79
|
-
const controller = abortControllers.get(request.requestId);
|
|
80
|
-
controller?.abort();
|
|
81
|
-
if (controller) {
|
|
82
|
-
postDiagnostic({
|
|
83
|
-
level: 'warn',
|
|
84
|
-
source: 'worker',
|
|
85
|
-
code: 'worker.request_aborted',
|
|
86
|
-
message: 'Syncular worker request aborted',
|
|
87
|
-
details: { requestId: request.requestId },
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
post({
|
|
91
|
-
id: request.id,
|
|
92
|
-
protocolVersion: request.protocolVersion,
|
|
93
|
-
ok: true,
|
|
94
|
-
});
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const startedAt = Date.now();
|
|
99
|
-
const abortController = createAbortController(request.type);
|
|
100
|
-
if (abortController) {
|
|
101
|
-
abortControllers.set(request.id, abortController);
|
|
102
|
-
}
|
|
103
|
-
try {
|
|
104
|
-
if (abortController) client?.setAbortSignal(abortController.signal);
|
|
105
|
-
const value = await dispatch(request);
|
|
106
|
-
if (canceledRequests.delete(request.id)) return;
|
|
107
|
-
post({
|
|
108
|
-
id: request.id,
|
|
109
|
-
protocolVersion: request.protocolVersion,
|
|
110
|
-
ok: true,
|
|
111
|
-
value,
|
|
112
|
-
});
|
|
113
|
-
postRequestSuccessDiagnostic(request, value, Date.now() - startedAt);
|
|
114
|
-
} catch (err) {
|
|
115
|
-
if (canceledRequests.delete(request.id)) return;
|
|
116
|
-
const source = requestDiagnosticSource(request.type);
|
|
117
|
-
const resyncRequired = errorRequiresFullRefresh(err);
|
|
118
|
-
const encodedError = encodeWorkerError(err);
|
|
119
|
-
const diagnosticCode = resyncRequired
|
|
120
|
-
? `${source}.resync_required`
|
|
121
|
-
: encodedError.code.startsWith(`${source}.`)
|
|
122
|
-
? encodedError.code
|
|
123
|
-
: `${source}.${request.type}.failed`;
|
|
124
|
-
postDiagnostic({
|
|
125
|
-
level: requestDiagnosticLevel(request.type, err),
|
|
126
|
-
source,
|
|
127
|
-
code: diagnosticCode,
|
|
128
|
-
message: resyncRequired
|
|
129
|
-
? `Syncular worker request ${request.type} requires full resync`
|
|
130
|
-
: `Syncular worker request ${request.type} failed`,
|
|
131
|
-
details: {
|
|
132
|
-
requestType: request.type,
|
|
133
|
-
durationMs: Date.now() - startedAt,
|
|
134
|
-
error: errorMessage(err),
|
|
135
|
-
errorCode: encodedError.code,
|
|
136
|
-
...(encodedError.category ? { category: encodedError.category } : {}),
|
|
137
|
-
...(encodedError.retryable != null
|
|
138
|
-
? { retryable: encodedError.retryable }
|
|
139
|
-
: {}),
|
|
140
|
-
...(encodedError.recommendedAction
|
|
141
|
-
? { recommendedAction: encodedError.recommendedAction }
|
|
142
|
-
: {}),
|
|
143
|
-
...(resyncRequired ? { resyncRequired: true } : {}),
|
|
144
|
-
...diagnosticStatus(err),
|
|
145
|
-
},
|
|
146
|
-
...requestSyncAttemptDiagnosticFields(request),
|
|
147
|
-
});
|
|
148
|
-
post({
|
|
149
|
-
id: request.id,
|
|
150
|
-
protocolVersion: request.protocolVersion,
|
|
151
|
-
ok: false,
|
|
152
|
-
error: encodedError,
|
|
153
|
-
});
|
|
154
|
-
} finally {
|
|
155
|
-
abortControllers.delete(request.id);
|
|
156
|
-
if (abortController) clearClientAbortSignal();
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async function dispatch(request: SyncularWorkerRequest): Promise<unknown> {
|
|
161
|
-
return dispatchGeneratedSyncularWorkerRequest(
|
|
162
|
-
{
|
|
163
|
-
requireClient,
|
|
164
|
-
openClient,
|
|
165
|
-
startRealtime: (options) => {
|
|
166
|
-
realtime.start(options);
|
|
167
|
-
return true;
|
|
168
|
-
},
|
|
169
|
-
stopRealtime: () => {
|
|
170
|
-
realtime.stop();
|
|
171
|
-
return true;
|
|
172
|
-
},
|
|
173
|
-
sendPresence: (action, scopeKey, metadata) => {
|
|
174
|
-
realtime.sendPresence(action, scopeKey, metadata);
|
|
175
|
-
return true;
|
|
176
|
-
},
|
|
177
|
-
runtimeInfo,
|
|
178
|
-
closeClient,
|
|
179
|
-
},
|
|
180
|
-
request
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async function openClient(
|
|
185
|
-
request: Extract<SyncularWorkerRequest, { type: 'open' }>
|
|
186
|
-
): Promise<boolean> {
|
|
187
|
-
const config = resolveSyncularClientConfig(request.config);
|
|
188
|
-
realtime.stop();
|
|
189
|
-
detachRowsChangedListener();
|
|
190
|
-
client?.close();
|
|
191
|
-
client = undefined;
|
|
192
|
-
openedConfig = undefined;
|
|
193
|
-
openedRuntime = undefined;
|
|
194
|
-
const runtime = request.runtime;
|
|
195
|
-
client = await openSyncularRustClient({
|
|
196
|
-
config,
|
|
197
|
-
module: runtime?.wasmGlueUrl
|
|
198
|
-
? loadWorkerWasmGlue(runtime.wasmGlueUrl)
|
|
199
|
-
: undefined,
|
|
200
|
-
wasmGlueUrl: runtime?.wasmGlueUrl,
|
|
201
|
-
wasmUrl: runtime?.wasmUrl,
|
|
202
|
-
});
|
|
203
|
-
attachRowsChangedListener(client);
|
|
204
|
-
openedConfig = config;
|
|
205
|
-
openedRuntime = runtime;
|
|
206
|
-
return true;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function closeClient(): boolean {
|
|
210
|
-
realtime.stop();
|
|
211
|
-
detachRowsChangedListener();
|
|
212
|
-
client?.close();
|
|
213
|
-
client = undefined;
|
|
214
|
-
openedConfig = undefined;
|
|
215
|
-
openedRuntime = undefined;
|
|
216
|
-
return true;
|
|
217
|
-
}
|
|
218
41
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
rust: await getSyncularRustRuntimeInfo(
|
|
233
|
-
openedRuntime?.wasmGlueUrl
|
|
234
|
-
? loadWorkerWasmGlue(openedRuntime.wasmGlueUrl)
|
|
235
|
-
: undefined,
|
|
236
|
-
selectedWasmUrl
|
|
237
|
-
),
|
|
238
|
-
});
|
|
42
|
+
export interface SyncWorkerOverrides {
|
|
43
|
+
/** Database factory indirection: tests inject bun:sqlite here. */
|
|
44
|
+
readonly openDatabase?: (
|
|
45
|
+
config: WorkerInitConfig,
|
|
46
|
+
) => Promise<ClientDatabase> | ClientDatabase;
|
|
47
|
+
readonly createTransport?: (config: WorkerInitConfig) => SyncTransport;
|
|
48
|
+
readonly createSegments?: (
|
|
49
|
+
config: WorkerInitConfig,
|
|
50
|
+
) => SegmentDownloader | undefined;
|
|
51
|
+
readonly createRealtime?: (
|
|
52
|
+
config: WorkerInitConfig,
|
|
53
|
+
clientId: string,
|
|
54
|
+
) => RealtimeConnector | undefined;
|
|
239
55
|
}
|
|
240
56
|
|
|
241
|
-
|
|
242
|
-
|
|
57
|
+
/** Minimal structural view of the dedicated-worker global scope. */
|
|
58
|
+
interface WorkerScope {
|
|
59
|
+
postMessage(message: unknown, transfer?: Transferable[]): void;
|
|
60
|
+
addEventListener(
|
|
61
|
+
type: 'message',
|
|
62
|
+
listener: (event: MessageEvent) => void,
|
|
63
|
+
): void;
|
|
243
64
|
}
|
|
244
65
|
|
|
245
|
-
function
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
66
|
+
function toErrorShape(error: unknown): WorkerErrorShape {
|
|
67
|
+
if (error instanceof ClientSyncError) {
|
|
68
|
+
return {
|
|
69
|
+
code: error.code,
|
|
70
|
+
message: error.message,
|
|
71
|
+
retryable: error.retryable,
|
|
72
|
+
};
|
|
251
73
|
}
|
|
252
|
-
return
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
post({
|
|
258
|
-
protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
|
|
259
|
-
type: 'rowsChanged',
|
|
260
|
-
source: event.source,
|
|
261
|
-
changedTables: event.changedTables,
|
|
262
|
-
changedRows: event.changedRows,
|
|
263
|
-
changedRowsTruncated: event.changedRowsTruncated,
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function detachRowsChangedListener(): void {
|
|
269
|
-
removeRowsChangedListener?.();
|
|
270
|
-
removeRowsChangedListener = undefined;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function post(message: SyncularWorkerOutboundMessage): void {
|
|
274
|
-
self.postMessage(message);
|
|
74
|
+
return {
|
|
75
|
+
code: WORKER_FAILED_CODE,
|
|
76
|
+
message: error instanceof Error ? error.message : String(error),
|
|
77
|
+
retryable: false,
|
|
78
|
+
};
|
|
275
79
|
}
|
|
276
80
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
81
|
+
/**
|
|
82
|
+
* ArrayBuffers safe to transfer out of a query result: whole-buffer
|
|
83
|
+
* views only (a view into a larger/pooled buffer is cloned instead —
|
|
84
|
+
* transferring would detach unrelated data), deduplicated because a
|
|
85
|
+
* buffer may only appear once in a transfer list.
|
|
86
|
+
*/
|
|
87
|
+
function queryTransferables(rows: readonly unknown[]): Transferable[] {
|
|
88
|
+
const buffers = new Set<ArrayBuffer>();
|
|
89
|
+
for (const row of rows) {
|
|
90
|
+
if (row === null || typeof row !== 'object') continue;
|
|
91
|
+
for (const value of Object.values(row)) {
|
|
92
|
+
if (
|
|
93
|
+
value instanceof Uint8Array &&
|
|
94
|
+
value.buffer instanceof ArrayBuffer &&
|
|
95
|
+
value.byteOffset === 0 &&
|
|
96
|
+
value.byteLength === value.buffer.byteLength
|
|
97
|
+
) {
|
|
98
|
+
buffers.add(value.buffer);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
282
101
|
}
|
|
283
|
-
return
|
|
102
|
+
return [...buffers];
|
|
284
103
|
}
|
|
285
104
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Register the RPC server on the current worker scope. Call this once
|
|
107
|
+
* from a worker entry script; the main-thread handle drives everything
|
|
108
|
+
* else through `init`/`call` messages.
|
|
109
|
+
*/
|
|
110
|
+
export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
|
|
111
|
+
const scope = globalThis as unknown as WorkerScope;
|
|
112
|
+
const post = (message: WorkerToMainMessage, transfer?: Transferable[]) => {
|
|
113
|
+
if (transfer !== undefined && transfer.length > 0) {
|
|
114
|
+
scope.postMessage(message, transfer);
|
|
115
|
+
} else {
|
|
116
|
+
scope.postMessage(message);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
289
119
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
120
|
+
let client: SyncClient | undefined;
|
|
121
|
+
let database: ClientDatabase | undefined;
|
|
122
|
+
let offline = false;
|
|
123
|
+
let closed = false;
|
|
124
|
+
|
|
125
|
+
// -- one sync loop: RPC-driven and auto-driven rounds serialize ----------
|
|
126
|
+
let syncChain: Promise<unknown> = Promise.resolve();
|
|
127
|
+
function serializedSync<T>(fn: () => Promise<T>): Promise<T> {
|
|
128
|
+
const next = syncChain.then(fn, fn);
|
|
129
|
+
syncChain = next.then(
|
|
130
|
+
() => undefined,
|
|
131
|
+
() => undefined,
|
|
132
|
+
);
|
|
133
|
+
return next;
|
|
295
134
|
}
|
|
296
|
-
}
|
|
297
135
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
value: unknown,
|
|
326
|
-
durationMs: number
|
|
327
|
-
): void {
|
|
328
|
-
if (!isDiagnosedSuccessRequest(request.type)) return;
|
|
329
|
-
const syncRevocation = syncScopeRevocationDetails(value);
|
|
330
|
-
postDiagnostic({
|
|
331
|
-
level: 'info',
|
|
332
|
-
source: requestDiagnosticSource(request.type),
|
|
333
|
-
code: `${requestDiagnosticSource(request.type)}.${request.type}.completed`,
|
|
334
|
-
message: `Syncular worker request ${request.type} completed`,
|
|
335
|
-
...requestSyncAttemptDiagnosticFields(request),
|
|
336
|
-
details: {
|
|
337
|
-
requestType: request.type,
|
|
338
|
-
durationMs,
|
|
339
|
-
...requestSuccessDetails(request, value),
|
|
340
|
-
},
|
|
341
|
-
});
|
|
342
|
-
if (
|
|
343
|
-
syncRevocation &&
|
|
344
|
-
(request.type === 'syncPull' ||
|
|
345
|
-
request.type === 'syncPush' ||
|
|
346
|
-
request.type === 'syncOnce')
|
|
347
|
-
) {
|
|
348
|
-
postDiagnostic({
|
|
349
|
-
level: 'warn',
|
|
350
|
-
source: 'sync',
|
|
351
|
-
code: 'sync.scope_revoked',
|
|
352
|
-
message: 'Syncular subscription scope revoked',
|
|
353
|
-
...requestSyncAttemptDiagnosticFields(request),
|
|
354
|
-
details: {
|
|
355
|
-
requestType: request.type,
|
|
356
|
-
...syncRevocation,
|
|
136
|
+
// -- §8.4 host loop: coalesced, jittered, inside the worker --------------
|
|
137
|
+
let autoSync = true;
|
|
138
|
+
let wakeJitterMs = 200;
|
|
139
|
+
let autoSyncScheduled = false;
|
|
140
|
+
function scheduleAutoSync(): void {
|
|
141
|
+
if (!autoSync || autoSyncScheduled || closed || client === undefined) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
autoSyncScheduled = true;
|
|
145
|
+
setTimeout(
|
|
146
|
+
() => {
|
|
147
|
+
autoSyncScheduled = false;
|
|
148
|
+
if (closed || client === undefined) return;
|
|
149
|
+
const running = client;
|
|
150
|
+
void serializedSync(() => running.syncUntilIdle())
|
|
151
|
+
.then((summary) => {
|
|
152
|
+
if (!closed)
|
|
153
|
+
post({ t: 'event', event: { kind: 'synced', summary } });
|
|
154
|
+
})
|
|
155
|
+
.catch((error: unknown) => {
|
|
156
|
+
if (!closed) {
|
|
157
|
+
post({
|
|
158
|
+
t: 'event',
|
|
159
|
+
event: { kind: 'synced', error: toErrorShape(error) },
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
});
|
|
357
163
|
},
|
|
358
|
-
|
|
164
|
+
Math.floor(Math.random() * wakeJitterMs),
|
|
165
|
+
);
|
|
359
166
|
}
|
|
360
|
-
}
|
|
361
167
|
|
|
362
|
-
function
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
return {};
|
|
168
|
+
function requireClient(): SyncClient {
|
|
169
|
+
if (client === undefined) {
|
|
170
|
+
throw new ClientSyncError(
|
|
171
|
+
WORKER_FAILED_CODE,
|
|
172
|
+
'the worker received a call before init completed',
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return client;
|
|
371
176
|
}
|
|
372
|
-
return syncularDiagnosticAttemptFields(request.syncAttempt);
|
|
373
|
-
}
|
|
374
177
|
|
|
375
|
-
function
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
178
|
+
function assertOnline(): void {
|
|
179
|
+
if (offline) {
|
|
180
|
+
throw new ClientSyncError(
|
|
181
|
+
'sync.transport_failed',
|
|
182
|
+
'the worker transport is offline (setOffline(true))',
|
|
183
|
+
true,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
380
187
|
|
|
381
|
-
function
|
|
382
|
-
|
|
383
|
-
)
|
|
384
|
-
|
|
385
|
-
}
|
|
188
|
+
function gateOffline<T>(inner: (request: T) => Promise<Uint8Array>) {
|
|
189
|
+
return (request: T): Promise<Uint8Array> => {
|
|
190
|
+
assertOnline();
|
|
191
|
+
return inner(request);
|
|
192
|
+
};
|
|
193
|
+
}
|
|
386
194
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
195
|
+
/** Offline gate that preserves the §5.4 `fetchUrl` capability marker. */
|
|
196
|
+
function gateSegmentsOffline(inner: SegmentDownloader): SegmentDownloader {
|
|
197
|
+
const fetchUrl = inner.fetchUrl;
|
|
198
|
+
return Object.assign(gateOffline(inner), {
|
|
199
|
+
...(fetchUrl !== undefined
|
|
200
|
+
? {
|
|
201
|
+
fetchUrl: (url: string) => {
|
|
202
|
+
assertOnline();
|
|
203
|
+
return fetchUrl(url);
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
: {}),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
394
209
|
|
|
395
|
-
function
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
return {
|
|
402
|
-
storage: openedConfig?.storage,
|
|
403
|
-
clientId: openedConfig?.clientId,
|
|
404
|
-
};
|
|
405
|
-
case 'setAuthHeaders':
|
|
406
|
-
return { headerCount: Object.keys(request.headers).length };
|
|
407
|
-
case 'setFieldEncryption':
|
|
408
|
-
return {
|
|
409
|
-
enabled: request.config != null,
|
|
410
|
-
ruleCount: request.config?.rules.length ?? 0,
|
|
411
|
-
};
|
|
412
|
-
case 'setEncryptedCrdt':
|
|
413
|
-
return {
|
|
414
|
-
enabled: request.config != null,
|
|
415
|
-
keyCount: request.config ? Object.keys(request.config.keys).length : 0,
|
|
416
|
-
};
|
|
417
|
-
case 'setBlobEncryption':
|
|
418
|
-
return {
|
|
419
|
-
enabled: request.config != null,
|
|
420
|
-
keyCount: request.config ? Object.keys(request.config.keys).length : 0,
|
|
421
|
-
};
|
|
422
|
-
case 'syncPull':
|
|
423
|
-
case 'syncPush':
|
|
424
|
-
case 'syncOnce':
|
|
425
|
-
return syncResultDetails(value);
|
|
426
|
-
case 'storeBlob':
|
|
427
|
-
return { immediate: request.options?.immediate === true };
|
|
428
|
-
case 'pruneBlobCache':
|
|
429
|
-
return { maxBytes: request.maxBytes ?? null, prunedBytes: value };
|
|
430
|
-
case 'compactStorage':
|
|
431
|
-
return objectRecord(value);
|
|
432
|
-
case 'localHealthCheck': {
|
|
433
|
-
const report = objectRecord(value);
|
|
434
|
-
const findings = Array.isArray(report.findings) ? report.findings : [];
|
|
435
|
-
return {
|
|
436
|
-
ok: report.ok === true,
|
|
437
|
-
findingCount: findings.length,
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
case 'exportLocalSupportBundle': {
|
|
441
|
-
const bundle = objectRecord(value);
|
|
442
|
-
const health = objectRecord(bundle.health);
|
|
443
|
-
const findings = Array.isArray(health.findings) ? health.findings : [];
|
|
444
|
-
return {
|
|
445
|
-
redacted: bundle.redacted === true,
|
|
446
|
-
source: bundle.source,
|
|
447
|
-
findingCount: findings.length,
|
|
448
|
-
};
|
|
210
|
+
async function init(config: WorkerInitConfig): Promise<WorkerInitResult> {
|
|
211
|
+
if (client !== undefined) {
|
|
212
|
+
throw new ClientSyncError(
|
|
213
|
+
WORKER_FAILED_CODE,
|
|
214
|
+
'the worker is already initialized',
|
|
215
|
+
);
|
|
449
216
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
217
|
+
autoSync = config.autoSync ?? true;
|
|
218
|
+
wakeJitterMs = config.wakeJitterMs ?? 200;
|
|
219
|
+
|
|
220
|
+
database =
|
|
221
|
+
overrides.openDatabase !== undefined
|
|
222
|
+
? await overrides.openDatabase(config)
|
|
223
|
+
: await defaultOpenDatabase(config);
|
|
224
|
+
const transport = gateOffline(
|
|
225
|
+
overrides.createTransport !== undefined
|
|
226
|
+
? overrides.createTransport(config)
|
|
227
|
+
: httpSyncTransport(config.endpoints.syncUrl),
|
|
228
|
+
);
|
|
229
|
+
const segments =
|
|
230
|
+
overrides.createSegments !== undefined
|
|
231
|
+
? overrides.createSegments(config)
|
|
232
|
+
: config.endpoints.segmentsUrl !== undefined
|
|
233
|
+
? httpSegmentDownloader(config.endpoints.segmentsUrl)
|
|
234
|
+
: undefined;
|
|
235
|
+
// §5.9 blob transport: only when a blobs URL is configured.
|
|
236
|
+
const blobs =
|
|
237
|
+
config.endpoints.blobsUrl !== undefined
|
|
238
|
+
? httpBlobTransport(config.endpoints.blobsUrl)
|
|
239
|
+
: undefined;
|
|
240
|
+
|
|
241
|
+
// Realtime needs the (possibly persisted) clientId for the
|
|
242
|
+
// `{clientId}` URL placeholder — start the client first, then attach
|
|
243
|
+
// the connector through a mutable slot read at connect time.
|
|
244
|
+
let realtimeConnector: RealtimeConnector | undefined;
|
|
245
|
+
const started = new SyncClient({
|
|
246
|
+
database,
|
|
247
|
+
schema: config.schema,
|
|
248
|
+
transport,
|
|
249
|
+
...(segments !== undefined
|
|
250
|
+
? { segments: gateSegmentsOffline(segments) }
|
|
251
|
+
: {}),
|
|
252
|
+
...(blobs !== undefined ? { blobs } : {}),
|
|
253
|
+
realtime: (handlers) => {
|
|
254
|
+
if (offline) {
|
|
255
|
+
throw new ClientSyncError(
|
|
256
|
+
'sync.transport_failed',
|
|
257
|
+
'the worker realtime channel is offline (setOffline(true))',
|
|
258
|
+
true,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (realtimeConnector === undefined) {
|
|
262
|
+
throw new ClientSyncError(
|
|
263
|
+
'sync.invalid_request',
|
|
264
|
+
'no realtime connector configured',
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
return realtimeConnector(handlers);
|
|
268
|
+
},
|
|
269
|
+
...(config.clientId !== undefined ? { clientId: config.clientId } : {}),
|
|
270
|
+
...(config.limits !== undefined ? { limits: config.limits } : {}),
|
|
271
|
+
onSyncNeeded: (reason) => {
|
|
272
|
+
post({ t: 'event', event: { kind: 'sync-needed', reason } });
|
|
273
|
+
scheduleAutoSync();
|
|
274
|
+
},
|
|
275
|
+
onConflict: (conflict) => {
|
|
276
|
+
post({ t: 'event', event: { kind: 'conflict', conflict } });
|
|
277
|
+
},
|
|
278
|
+
onUpgrading: (upgrading) => {
|
|
279
|
+
// §7.4.5: surface the schema-bump reset + completion to the UI thread.
|
|
280
|
+
post({ t: 'event', event: { kind: 'upgrading', upgrading } });
|
|
281
|
+
},
|
|
282
|
+
onPresence: (scopeKey) => {
|
|
283
|
+
// §8.6: surface a presence change to the UI thread.
|
|
284
|
+
post({ t: 'event', event: { kind: 'presence', scopeKey } });
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
// TODO 3.1 / I1: forward every coalesced apply-batch invalidation to the
|
|
288
|
+
// UI thread so `SyncClientHandle.onInvalidate` fires with worker parity.
|
|
289
|
+
started.onInvalidate((event) => {
|
|
290
|
+
if (!closed) post({ t: 'event', event: { kind: 'invalidate', event } });
|
|
291
|
+
});
|
|
292
|
+
await started.start();
|
|
293
|
+
realtimeConnector =
|
|
294
|
+
overrides.createRealtime !== undefined
|
|
295
|
+
? overrides.createRealtime(config, started.clientId)
|
|
296
|
+
: config.endpoints.realtimeUrl !== undefined
|
|
297
|
+
? webSocketRealtimeConnector(
|
|
298
|
+
config.endpoints.realtimeUrl.replace(
|
|
299
|
+
'{clientId}',
|
|
300
|
+
encodeURIComponent(started.clientId),
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
: undefined;
|
|
304
|
+
client = started;
|
|
305
|
+
return { clientId: started.clientId };
|
|
460
306
|
}
|
|
461
|
-
}
|
|
462
307
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
308
|
+
const api: WorkerApi = {
|
|
309
|
+
subscribe: (input) => requireClient().subscribe(input),
|
|
310
|
+
unsubscribe: (id) => requireClient().unsubscribe(id),
|
|
311
|
+
setWindow: async (base, units) => {
|
|
312
|
+
await requireClient().setWindow(base, units);
|
|
313
|
+
// A window change adds/removes value-sharded subscriptions; a widened
|
|
314
|
+
// unit needs a bootstrap pull to become visible. In autoSync mode the
|
|
315
|
+
// host loop owns rounds, so schedule one now — otherwise the new unit
|
|
316
|
+
// waits for an unrelated server wake-up (§8.4). A no-op change still
|
|
317
|
+
// schedules a harmless idempotent round.
|
|
318
|
+
scheduleAutoSync();
|
|
319
|
+
},
|
|
320
|
+
windowState: (base) => requireClient().windowState(base),
|
|
321
|
+
mutate: (mutations) => {
|
|
322
|
+
const id = requireClient().mutate(mutations);
|
|
323
|
+
// In autoSync mode the host loop owns rounds (§8.4): a local write must
|
|
324
|
+
// push without the app orchestrating sync, so schedule a jittered round
|
|
325
|
+
// to drain the outbox promptly. `mutate` is synchronous — its outbox
|
|
326
|
+
// write has committed before this line — and the round itself serializes
|
|
327
|
+
// on SyncClient's operation chain, so no ordering hack is needed here;
|
|
328
|
+
// the §8.4 jitter in `scheduleAutoSync` is the only deferral, and it
|
|
329
|
+
// coalesces bursts. (Historically this used setTimeout to dodge a
|
|
330
|
+
// nested-transaction flake; that race is fixed at the source now — the
|
|
331
|
+
// real cause was cross-operation interleaving, serialized in the core.)
|
|
332
|
+
scheduleAutoSync();
|
|
333
|
+
return id;
|
|
334
|
+
},
|
|
335
|
+
sync: () => {
|
|
336
|
+
const running = requireClient();
|
|
337
|
+
return serializedSync(() => running.sync());
|
|
338
|
+
},
|
|
339
|
+
syncUntilIdle: (maxRounds) => {
|
|
340
|
+
const running = requireClient();
|
|
341
|
+
return serializedSync(() => running.syncUntilIdle(maxRounds));
|
|
342
|
+
},
|
|
343
|
+
query: (sql, params) => requireClient().query(sql, params),
|
|
344
|
+
conflicts: () => requireClient().conflicts,
|
|
345
|
+
rejections: () => requireClient().rejections,
|
|
346
|
+
schemaFloor: () => requireClient().schemaFloor,
|
|
347
|
+
leaseState: () => requireClient().leaseState,
|
|
348
|
+
upgrading: () => requireClient().upgrading,
|
|
349
|
+
syncNeeded: () => requireClient().syncNeeded,
|
|
350
|
+
pendingCommits: () => requireClient().pendingCommits(),
|
|
351
|
+
subscriptions: () => requireClient().subscriptions(),
|
|
352
|
+
subscription: (id) => requireClient().subscription(id),
|
|
353
|
+
connectRealtime: () => requireClient().connectRealtime(),
|
|
354
|
+
disconnectRealtime: () => requireClient().disconnectRealtime(),
|
|
355
|
+
setPresence: (scopeKey, doc) => requireClient().setPresence(scopeKey, doc),
|
|
356
|
+
presence: (scopeKey) => requireClient().presence(scopeKey),
|
|
357
|
+
uploadBlob: (bytes, options) => requireClient().uploadBlob(bytes, options),
|
|
358
|
+
fetchBlob: (blobIdOrRef) => requireClient().fetchBlob(blobIdOrRef),
|
|
359
|
+
setOffline: (value) => {
|
|
360
|
+
offline = value;
|
|
361
|
+
if (offline) client?.disconnectRealtime();
|
|
362
|
+
},
|
|
363
|
+
close: async () => {
|
|
364
|
+
closed = true;
|
|
365
|
+
await client?.close();
|
|
366
|
+
database?.close();
|
|
367
|
+
client = undefined;
|
|
368
|
+
database = undefined;
|
|
369
|
+
},
|
|
480
370
|
};
|
|
481
|
-
}
|
|
482
371
|
|
|
483
|
-
function
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
const result = objectRecord(value);
|
|
488
|
-
const subscriptions = Array.isArray(result.subscriptions)
|
|
489
|
-
? result.subscriptions
|
|
490
|
-
: [];
|
|
491
|
-
const revokedSubscriptionIds = subscriptions
|
|
492
|
-
.filter((subscription): subscription is { id: string; status: string } => {
|
|
493
|
-
return (
|
|
494
|
-
subscription !== null &&
|
|
495
|
-
typeof subscription === 'object' &&
|
|
496
|
-
(subscription as { status?: unknown }).status === 'revoked' &&
|
|
497
|
-
typeof (subscription as { id?: unknown }).id === 'string'
|
|
498
|
-
);
|
|
499
|
-
})
|
|
500
|
-
.map((subscription) => subscription.id);
|
|
372
|
+
async function dispatch(message: WorkerCallMessage): Promise<unknown> {
|
|
373
|
+
const method = api[message.method] as (...args: unknown[]) => unknown;
|
|
374
|
+
return await method.apply(api, message.args as unknown[]);
|
|
375
|
+
}
|
|
501
376
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
377
|
+
function run(
|
|
378
|
+
id: number,
|
|
379
|
+
fn: () => Promise<unknown>,
|
|
380
|
+
transfer?: (value: unknown) => Transferable[],
|
|
381
|
+
): void {
|
|
382
|
+
void (async () => {
|
|
383
|
+
try {
|
|
384
|
+
const value = await fn();
|
|
385
|
+
post(
|
|
386
|
+
{ t: 'result', id, value },
|
|
387
|
+
transfer !== undefined ? transfer(value) : undefined,
|
|
388
|
+
);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
post({ t: 'error', id, error: toErrorShape(error) });
|
|
506
391
|
}
|
|
507
|
-
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function diagnosticStatus(error: unknown): Record<string, unknown> {
|
|
511
|
-
const status = syncularErrorStatus(error);
|
|
512
|
-
return status ? { status } : {};
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
function objectRecord(value: unknown): Record<string, unknown> {
|
|
516
|
-
return value && typeof value === 'object'
|
|
517
|
-
? (value as Record<string, unknown>)
|
|
518
|
-
: {};
|
|
519
|
-
}
|
|
392
|
+
})();
|
|
393
|
+
}
|
|
520
394
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
395
|
+
scope.addEventListener('message', (event) => {
|
|
396
|
+
const message = event.data as MainToWorkerMessage;
|
|
397
|
+
if (message.t === 'init') {
|
|
398
|
+
run(message.id, () => init(message.config));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (message.t === 'call') {
|
|
402
|
+
run(
|
|
403
|
+
message.id,
|
|
404
|
+
() => dispatch(message),
|
|
405
|
+
message.method === 'query'
|
|
406
|
+
? (value) => queryTransferables(value as readonly unknown[])
|
|
407
|
+
: undefined,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
524
411
|
|
|
525
|
-
|
|
526
|
-
return errorMessage(error).includes('full snapshot resync required');
|
|
412
|
+
post({ t: 'ready' });
|
|
527
413
|
}
|
|
528
414
|
|
|
529
|
-
function
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
name: error.name,
|
|
539
|
-
stack: error.stack,
|
|
540
|
-
details: error.details,
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
if (error instanceof Error) {
|
|
544
|
-
const details = syncularErrorDetails(error);
|
|
545
|
-
const classification = classifySyncularError(error, error.message, details);
|
|
546
|
-
if (!classification) {
|
|
547
|
-
return createSyncularWorkerErrorPayload('worker.failed', error.message, {
|
|
548
|
-
name: error.name,
|
|
549
|
-
stack: error.stack,
|
|
550
|
-
details,
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
return {
|
|
554
|
-
code: classification.code,
|
|
555
|
-
message: error.message,
|
|
556
|
-
category: classification.category,
|
|
557
|
-
retryable: classification.retryable,
|
|
558
|
-
recommendedAction: classification.recommendedAction,
|
|
559
|
-
name: error.name,
|
|
560
|
-
stack: error.stack,
|
|
561
|
-
details,
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
const message = String(error);
|
|
565
|
-
const classification = classifySyncularError(error, message);
|
|
566
|
-
if (!classification) {
|
|
567
|
-
return createSyncularWorkerErrorPayload('worker.failed', message);
|
|
415
|
+
async function defaultOpenDatabase(
|
|
416
|
+
config: WorkerInitConfig,
|
|
417
|
+
): Promise<ClientDatabase> {
|
|
418
|
+
if (config.database.mode !== 'persistent') {
|
|
419
|
+
throw new ClientSyncError(
|
|
420
|
+
WORKER_FAILED_CODE,
|
|
421
|
+
"database mode 'custom' requires an openDatabase override in the " +
|
|
422
|
+
'worker bootstrap (startSyncWorker({ openDatabase }))',
|
|
423
|
+
);
|
|
568
424
|
}
|
|
569
|
-
return {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
recommendedAction: classification.recommendedAction,
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function isWorkerErrorPayload(
|
|
579
|
-
value: unknown
|
|
580
|
-
): value is SyncularWorkerErrorPayload {
|
|
581
|
-
return Boolean(
|
|
582
|
-
value && typeof value === 'object' && 'code' in value && 'message' in value
|
|
583
|
-
);
|
|
425
|
+
return openPersistentWasmDatabase(config.database.name, {
|
|
426
|
+
...(config.database.directory !== undefined
|
|
427
|
+
? { directory: config.database.directory }
|
|
428
|
+
: {}),
|
|
429
|
+
});
|
|
584
430
|
}
|