@syncular/client 0.0.6-96 → 0.1.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 +713 -11
- package/dist/auth-leases.d.ts +11 -0
- package/dist/auth-leases.d.ts.map +1 -0
- package/dist/auth-leases.js +140 -0
- package/dist/auth-leases.js.map +1 -0
- package/dist/blob-limits.d.ts +15 -0
- package/dist/blob-limits.d.ts.map +1 -0
- package/dist/blob-limits.js +66 -0
- package/dist/blob-limits.js.map +1 -0
- package/dist/bridge-client.d.ts +69 -0
- package/dist/bridge-client.d.ts.map +1 -0
- package/dist/bridge-client.js +282 -0
- package/dist/bridge-client.js.map +1 -0
- package/dist/client-config.d.ts +6 -0
- package/dist/client-config.d.ts.map +1 -0
- package/dist/client-config.js +18 -0
- package/dist/client-config.js.map +1 -0
- package/dist/client.d.ts +65 -397
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +150 -960
- package/dist/client.js.map +1 -1
- package/dist/command-history.d.ts +35 -0
- package/dist/command-history.d.ts.map +1 -0
- package/dist/command-history.js +378 -0
- package/dist/command-history.js.map +1 -0
- package/dist/console-diagnostics.d.ts +33 -0
- package/dist/console-diagnostics.d.ts.map +1 -0
- package/dist/console-diagnostics.js +496 -0
- package/dist/console-diagnostics.js.map +1 -0
- package/dist/crdt-yjs/index.d.ts +4 -0
- package/dist/crdt-yjs/index.d.ts.map +1 -0
- package/dist/crdt-yjs/index.js +4 -0
- package/dist/crdt-yjs/index.js.map +1 -0
- package/dist/crdt-yjs/webview-host-facade.d.ts +126 -0
- package/dist/crdt-yjs/webview-host-facade.d.ts.map +1 -0
- package/dist/crdt-yjs/webview-host-facade.js +284 -0
- package/dist/crdt-yjs/webview-host-facade.js.map +1 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.d.ts +153 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.d.ts.map +1 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.js +406 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.js.map +1 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts +73 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts.map +1 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.js +169 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.js.map +1 -0
- package/dist/database.d.ts +83 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +916 -0
- package/dist/database.js.map +1 -0
- package/dist/diagnostics.d.ts +10 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +83 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +211 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated-bridge.d.ts +365 -0
- package/dist/generated-bridge.d.ts.map +1 -0
- package/dist/generated-bridge.js +250 -0
- package/dist/generated-bridge.js.map +1 -0
- package/dist/index.d.ts +13 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -25
- package/dist/index.js.map +1 -1
- package/dist/mutations.d.ts +4 -72
- package/dist/mutations.d.ts.map +1 -1
- package/dist/mutations.js +2 -568
- package/dist/mutations.js.map +1 -1
- package/dist/network.d.ts +3 -0
- package/dist/network.d.ts.map +1 -0
- package/dist/network.js +17 -0
- package/dist/network.js.map +1 -0
- package/dist/react/index.d.ts +169 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +628 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react-native/index.d.ts +35 -0
- package/dist/react-native/index.d.ts.map +1 -0
- package/dist/react-native/index.js +49 -0
- package/dist/react-native/index.js.map +1 -0
- package/dist/runtime-contract.d.ts +13 -0
- package/dist/runtime-contract.d.ts.map +1 -0
- package/dist/runtime-contract.js +24 -0
- package/dist/runtime-contract.js.map +1 -0
- package/dist/rust-client.d.ts +106 -0
- package/dist/rust-client.d.ts.map +1 -0
- package/dist/rust-client.js +936 -0
- package/dist/rust-client.js.map +1 -0
- package/dist/sentry.d.ts +35 -0
- package/dist/sentry.d.ts.map +1 -0
- package/dist/sentry.js +155 -0
- package/dist/sentry.js.map +1 -0
- package/dist/sql-safety.d.ts +3 -0
- package/dist/sql-safety.d.ts.map +1 -0
- package/dist/sql-safety.js +62 -0
- package/dist/sql-safety.js.map +1 -0
- package/dist/syncular-runtime-artifacts.json +61 -0
- package/dist/tauri/index.d.ts +35 -0
- package/dist/tauri/index.d.ts.map +1 -0
- package/dist/tauri/index.js +114 -0
- package/dist/tauri/index.js.map +1 -0
- package/dist/types.d.ts +1059 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/wasm/.syncular-wasm-profile +1 -0
- package/dist/wasm/syncular-runtime-artifact.json +21 -0
- package/dist/wasm/syncular.d.ts +207 -0
- package/dist/wasm/syncular.js +2341 -0
- package/dist/wasm/syncular_bg.wasm +0 -0
- package/dist/wasm/syncular_bg.wasm.d.ts +97 -0
- package/dist/wasm-bindings/runtime-contract.d.ts +22 -0
- package/dist/wasm-bindings/runtime-contract.d.ts.map +1 -0
- package/dist/wasm-bindings/runtime-contract.js +112 -0
- package/dist/wasm-bindings/runtime-contract.js.map +1 -0
- package/dist/wasm-core/.syncular-wasm-profile +1 -0
- package/dist/wasm-core/syncular-runtime-artifact.json +17 -0
- package/dist/wasm-core/syncular.d.ts +162 -0
- package/dist/wasm-core/syncular.js +1847 -0
- package/dist/wasm-core/syncular_bg.wasm +0 -0
- package/dist/wasm-core/syncular_bg.wasm.d.ts +77 -0
- package/dist/wasm-perf/.syncular-wasm-profile +1 -0
- package/dist/wasm-perf/syncular-runtime-artifact.json +21 -0
- package/dist/wasm-perf/syncular.d.ts +207 -0
- package/dist/wasm-perf/syncular.js +2341 -0
- package/dist/wasm-perf/syncular_bg.wasm +0 -0
- package/dist/wasm-perf/syncular_bg.wasm.d.ts +97 -0
- package/dist/wasm-runtime.d.ts +18 -0
- package/dist/wasm-runtime.d.ts.map +1 -0
- package/dist/wasm-runtime.js +28 -0
- package/dist/wasm-runtime.js.map +1 -0
- package/dist/worker-client.d.ts +123 -0
- package/dist/worker-client.d.ts.map +1 -0
- package/dist/worker-client.js +1686 -0
- package/dist/worker-client.js.map +1 -0
- package/dist/worker-entry.d.ts +2 -0
- package/dist/worker-entry.d.ts.map +1 -0
- package/dist/worker-entry.js +479 -0
- package/dist/worker-entry.js.map +1 -0
- package/dist/worker-protocol.d.ts +64 -0
- package/dist/worker-protocol.d.ts.map +1 -0
- package/dist/worker-protocol.js +16 -0
- package/dist/worker-protocol.js.map +1 -0
- package/dist/worker-realtime.d.ts +39 -0
- package/dist/worker-realtime.d.ts.map +1 -0
- package/dist/worker-realtime.js +677 -0
- package/dist/worker-realtime.js.map +1 -0
- package/package.json +124 -17
- package/src/auth-leases.ts +251 -0
- package/src/blob-limits.ts +98 -0
- package/src/bridge-client.ts +512 -0
- package/src/client-config.ts +29 -0
- package/src/client.ts +256 -1414
- package/src/command-history.ts +623 -0
- package/src/console-diagnostics.ts +617 -0
- package/src/crdt-yjs/index.ts +3 -0
- package/src/crdt-yjs/webview-host-facade.ts +477 -0
- package/src/crdt-yjs/yjs-document-field-adapter.ts +733 -0
- package/src/crdt-yjs/yjs-prosemirror-bridge.ts +272 -0
- package/src/database.ts +1342 -0
- package/src/diagnostics.ts +116 -0
- package/src/errors.ts +269 -0
- package/src/generated-bridge.ts +741 -0
- package/src/index.ts +21 -25
- package/src/mutations.ts +12 -821
- package/src/network.ts +32 -0
- package/src/react/index.ts +1036 -0
- package/src/react-native/index.ts +152 -0
- package/src/runtime-contract.ts +48 -0
- package/src/rust-client.ts +1488 -0
- package/src/sentry.ts +215 -0
- package/src/sql-safety.ts +61 -0
- package/src/tauri/index.ts +211 -0
- package/src/types.ts +1356 -0
- package/src/wasm-bindings/generated-wasm-bindings.d.ts +70 -0
- package/src/wasm-bindings/runtime-contract.ts +158 -0
- package/src/wasm-runtime.ts +76 -0
- package/src/worker-client.ts +2215 -0
- package/src/worker-entry.ts +584 -0
- package/src/worker-protocol.ts +109 -0
- package/src/worker-realtime.ts +843 -0
- package/dist/blobs/index.d.ts +0 -7
- package/dist/blobs/index.d.ts.map +0 -1
- package/dist/blobs/index.js +0 -7
- package/dist/blobs/index.js.map +0 -1
- package/dist/blobs/manager.d.ts +0 -345
- package/dist/blobs/manager.d.ts.map +0 -1
- package/dist/blobs/manager.js +0 -749
- package/dist/blobs/manager.js.map +0 -1
- package/dist/blobs/migrate.d.ts +0 -14
- package/dist/blobs/migrate.d.ts.map +0 -1
- package/dist/blobs/migrate.js +0 -59
- package/dist/blobs/migrate.js.map +0 -1
- package/dist/blobs/types.d.ts +0 -62
- package/dist/blobs/types.d.ts.map +0 -1
- package/dist/blobs/types.js +0 -5
- package/dist/blobs/types.js.map +0 -1
- package/dist/conflicts.d.ts +0 -31
- package/dist/conflicts.d.ts.map +0 -1
- package/dist/conflicts.js +0 -112
- package/dist/conflicts.js.map +0 -1
- package/dist/create-client.d.ts +0 -118
- package/dist/create-client.d.ts.map +0 -1
- package/dist/create-client.js +0 -178
- package/dist/create-client.js.map +0 -1
- package/dist/engine/SyncEngine.d.ts +0 -272
- package/dist/engine/SyncEngine.d.ts.map +0 -1
- package/dist/engine/SyncEngine.js +0 -1860
- package/dist/engine/SyncEngine.js.map +0 -1
- package/dist/engine/index.d.ts +0 -6
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js +0 -6
- package/dist/engine/index.js.map +0 -1
- package/dist/engine/types.d.ts +0 -345
- package/dist/engine/types.d.ts.map +0 -1
- package/dist/engine/types.js +0 -7
- package/dist/engine/types.js.map +0 -1
- package/dist/handlers/collection.d.ts +0 -6
- package/dist/handlers/collection.d.ts.map +0 -1
- package/dist/handlers/collection.js +0 -21
- package/dist/handlers/collection.js.map +0 -1
- package/dist/handlers/create-handler.d.ts +0 -120
- package/dist/handlers/create-handler.d.ts.map +0 -1
- package/dist/handlers/create-handler.js +0 -145
- package/dist/handlers/create-handler.js.map +0 -1
- package/dist/handlers/types.d.ts +0 -83
- package/dist/handlers/types.d.ts.map +0 -1
- package/dist/handlers/types.js +0 -5
- package/dist/handlers/types.js.map +0 -1
- package/dist/migrate.d.ts +0 -19
- package/dist/migrate.d.ts.map +0 -1
- package/dist/migrate.js +0 -238
- package/dist/migrate.js.map +0 -1
- package/dist/outbox.d.ts +0 -112
- package/dist/outbox.d.ts.map +0 -1
- package/dist/outbox.js +0 -294
- package/dist/outbox.js.map +0 -1
- package/dist/plugins/incrementing-version.d.ts +0 -34
- package/dist/plugins/incrementing-version.d.ts.map +0 -1
- package/dist/plugins/incrementing-version.js +0 -83
- package/dist/plugins/incrementing-version.js.map +0 -1
- package/dist/plugins/index.d.ts +0 -3
- package/dist/plugins/index.d.ts.map +0 -1
- package/dist/plugins/index.js +0 -3
- package/dist/plugins/index.js.map +0 -1
- package/dist/plugins/types.d.ts +0 -49
- package/dist/plugins/types.d.ts.map +0 -1
- package/dist/plugins/types.js +0 -15
- package/dist/plugins/types.js.map +0 -1
- package/dist/proxy/connection.d.ts +0 -33
- package/dist/proxy/connection.d.ts.map +0 -1
- package/dist/proxy/connection.js +0 -153
- package/dist/proxy/connection.js.map +0 -1
- package/dist/proxy/dialect.d.ts +0 -46
- package/dist/proxy/dialect.d.ts.map +0 -1
- package/dist/proxy/dialect.js +0 -58
- package/dist/proxy/dialect.js.map +0 -1
- package/dist/proxy/driver.d.ts +0 -42
- package/dist/proxy/driver.d.ts.map +0 -1
- package/dist/proxy/driver.js +0 -78
- package/dist/proxy/driver.js.map +0 -1
- package/dist/proxy/index.d.ts +0 -10
- package/dist/proxy/index.d.ts.map +0 -1
- package/dist/proxy/index.js +0 -10
- package/dist/proxy/index.js.map +0 -1
- package/dist/proxy/mutations.d.ts +0 -9
- package/dist/proxy/mutations.d.ts.map +0 -1
- package/dist/proxy/mutations.js +0 -11
- package/dist/proxy/mutations.js.map +0 -1
- package/dist/pull-engine.d.ts +0 -48
- package/dist/pull-engine.d.ts.map +0 -1
- package/dist/pull-engine.js +0 -648
- package/dist/pull-engine.js.map +0 -1
- package/dist/push-engine.d.ts +0 -18
- package/dist/push-engine.d.ts.map +0 -1
- package/dist/push-engine.js +0 -167
- package/dist/push-engine.js.map +0 -1
- package/dist/query/FingerprintCollector.d.ts +0 -18
- package/dist/query/FingerprintCollector.d.ts.map +0 -1
- package/dist/query/FingerprintCollector.js +0 -28
- package/dist/query/FingerprintCollector.js.map +0 -1
- package/dist/query/QueryContext.d.ts +0 -33
- package/dist/query/QueryContext.d.ts.map +0 -1
- package/dist/query/QueryContext.js +0 -16
- package/dist/query/QueryContext.js.map +0 -1
- package/dist/query/fingerprint.d.ts +0 -61
- package/dist/query/fingerprint.d.ts.map +0 -1
- package/dist/query/fingerprint.js +0 -91
- package/dist/query/fingerprint.js.map +0 -1
- package/dist/query/index.d.ts +0 -7
- package/dist/query/index.d.ts.map +0 -1
- package/dist/query/index.js +0 -7
- package/dist/query/index.js.map +0 -1
- package/dist/query/tracked-select.d.ts +0 -18
- package/dist/query/tracked-select.d.ts.map +0 -1
- package/dist/query/tracked-select.js +0 -90
- package/dist/query/tracked-select.js.map +0 -1
- package/dist/schema.d.ts +0 -83
- package/dist/schema.d.ts.map +0 -1
- package/dist/schema.js +0 -7
- package/dist/schema.js.map +0 -1
- package/dist/subscription-state.d.ts +0 -46
- package/dist/subscription-state.d.ts.map +0 -1
- package/dist/subscription-state.js +0 -185
- package/dist/subscription-state.js.map +0 -1
- package/dist/sync-loop.d.ts +0 -34
- package/dist/sync-loop.d.ts.map +0 -1
- package/dist/sync-loop.js +0 -253
- package/dist/sync-loop.js.map +0 -1
- package/dist/sync.d.ts +0 -32
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js +0 -55
- package/dist/sync.js.map +0 -1
- package/dist/utils/id.d.ts +0 -17
- package/dist/utils/id.d.ts.map +0 -1
- package/dist/utils/id.js +0 -46
- package/dist/utils/id.js.map +0 -1
- package/src/blobs/index.ts +0 -7
- package/src/blobs/manager.ts +0 -1027
- package/src/blobs/migrate.ts +0 -67
- package/src/blobs/types.ts +0 -84
- package/src/client.test.ts +0 -417
- package/src/conflicts.ts +0 -171
- package/src/create-client.test.ts +0 -83
- package/src/create-client.ts +0 -330
- package/src/engine/SyncEngine.test.ts +0 -254
- package/src/engine/SyncEngine.ts +0 -2375
- package/src/engine/index.ts +0 -6
- package/src/engine/types.ts +0 -419
- package/src/handlers/collection.ts +0 -36
- package/src/handlers/create-handler.ts +0 -330
- package/src/handlers/types.ts +0 -108
- package/src/migrate.ts +0 -318
- package/src/outbox.ts +0 -383
- package/src/plugins/incrementing-version.ts +0 -133
- package/src/plugins/index.ts +0 -2
- package/src/plugins/types.ts +0 -63
- package/src/proxy/connection.ts +0 -191
- package/src/proxy/dialect.ts +0 -76
- package/src/proxy/driver.ts +0 -126
- package/src/proxy/index.ts +0 -10
- package/src/proxy/mutations.ts +0 -18
- package/src/pull-engine.test.ts +0 -151
- package/src/pull-engine.ts +0 -855
- package/src/push-engine.ts +0 -216
- package/src/query/FingerprintCollector.ts +0 -29
- package/src/query/QueryContext.ts +0 -54
- package/src/query/fingerprint.ts +0 -109
- package/src/query/index.ts +0 -10
- package/src/query/tracked-select.ts +0 -139
- package/src/schema.ts +0 -94
- package/src/subscription-state.ts +0 -259
- package/src/sync-loop.ts +0 -379
- package/src/sync.ts +0 -170
- package/src/utils/id.ts +0 -55
package/README.md
CHANGED
|
@@ -1,23 +1,725 @@
|
|
|
1
1
|
# @syncular/client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Rust-owned SQLite browser client for Syncular.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This package is the TypeScript host binding over the Rust client. The browser
|
|
6
|
+
runtime is a dedicated Worker that owns the Rust WASM module and SQLite handle.
|
|
7
|
+
TypeScript keeps Kysely as the type-safe query builder; generated app code
|
|
8
|
+
supplies the DB type, schema installer, mutation helpers, subscriptions, and
|
|
9
|
+
runtime assertions.
|
|
10
|
+
|
|
11
|
+
## Generated App Entry
|
|
12
|
+
|
|
13
|
+
Configure Rust codegen to emit your browser helper into your app package:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"typescriptOutputPath": "src/generated/syncular.browser.ts",
|
|
18
|
+
"typescriptRuntimeImportPath": "@syncular/client",
|
|
19
|
+
"tables": {
|
|
20
|
+
"profiles": {
|
|
21
|
+
"serverVersionColumn": "server_version",
|
|
22
|
+
"blobColumns": ["avatar"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
App code imports the generated helper, not a table-specific API from this
|
|
29
|
+
package:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { createSyncularAppDatabase } from './generated/syncular.browser';
|
|
33
|
+
|
|
34
|
+
const syncular = await createSyncularAppDatabase({
|
|
35
|
+
config: {
|
|
36
|
+
baseUrl: '/sync',
|
|
37
|
+
actorId: 'user-1',
|
|
38
|
+
clientId: 'client-1',
|
|
39
|
+
projectId: 'project-1',
|
|
40
|
+
fileName: 'app.sqlite',
|
|
41
|
+
},
|
|
42
|
+
requestTimeoutMs: 30_000,
|
|
43
|
+
getHeaders: async () => ({
|
|
44
|
+
authorization: `Bearer ${await auth.currentAccessToken()}`,
|
|
45
|
+
}),
|
|
46
|
+
authLifecycle: {
|
|
47
|
+
refreshToken: () => auth.refreshAccessToken(),
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const rows = await syncular.db
|
|
52
|
+
.selectFrom('tasks')
|
|
53
|
+
.select(['id', 'title'])
|
|
54
|
+
.where('project_id', '=', 'project-1')
|
|
55
|
+
.execute();
|
|
56
|
+
|
|
57
|
+
await syncular.mutations.tasks.insert({
|
|
58
|
+
title: 'Typed Rust-owned write',
|
|
59
|
+
completed: 0,
|
|
60
|
+
user_id: 'user-1',
|
|
61
|
+
project_id: 'project-1',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await syncular.client.issueAuthLease({
|
|
65
|
+
schemaVersion: 1,
|
|
66
|
+
scopes: [
|
|
67
|
+
{
|
|
68
|
+
subscriptionId: 'tasks:user-1',
|
|
69
|
+
table: 'tasks',
|
|
70
|
+
values: { user_id: 'user-1' },
|
|
71
|
+
operations: ['upsert'],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await syncular.leasedMutations.tasks.update('task-1', {
|
|
77
|
+
title: 'Queued while offline',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Local mutations sync automatically by default. To opt out, pass:
|
|
81
|
+
// sync: { autoSyncAfterMutation: false }
|
|
82
|
+
|
|
83
|
+
const live = await syncular.live(
|
|
84
|
+
syncular.db
|
|
85
|
+
.selectFrom('tasks')
|
|
86
|
+
.select(['id', 'title'])
|
|
87
|
+
.where('project_id', '=', 'project-1'),
|
|
88
|
+
{
|
|
89
|
+
onChange(rows) {
|
|
90
|
+
console.log(rows);
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
live.unsubscribe();
|
|
96
|
+
await syncular.close();
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Generated helpers intentionally do not emit table constants, column constants,
|
|
100
|
+
or canned queries. Reads stay plain Kysely. Sync-aware writes go through
|
|
101
|
+
`syncular.mutations` or generated operation helpers.
|
|
102
|
+
|
|
103
|
+
Use `bootstrapPhases` when the app should become usable before every
|
|
104
|
+
subscription has finished its first snapshot. Keys can be generated table names
|
|
105
|
+
or subscription ids. Phase `0` is critical by default, phase `1` is
|
|
106
|
+
interactive by default, and higher phases continue in the background:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const syncular = await createSyncularAppDatabase({
|
|
110
|
+
config: {
|
|
111
|
+
baseUrl: '/sync',
|
|
112
|
+
actorId: 'user-1',
|
|
113
|
+
clientId: 'client-1',
|
|
114
|
+
pull: {
|
|
115
|
+
criticalBootstrapPhase: 0,
|
|
116
|
+
interactiveBootstrapPhase: 1,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
bootstrapPhases: {
|
|
120
|
+
projects: 0,
|
|
121
|
+
tasks: 1,
|
|
122
|
+
comments: 2,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const result = await syncular.client.syncOnce();
|
|
127
|
+
|
|
128
|
+
if (result.bootstrap.criticalReady) {
|
|
129
|
+
renderShell();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const unsubscribeBootstrap = syncular.client.addEventListener(
|
|
133
|
+
'bootstrapChanged',
|
|
134
|
+
(bootstrap) => {
|
|
135
|
+
if (bootstrap.interactiveReady) enableMainViews();
|
|
136
|
+
if (bootstrap.complete) enableFullDataViews();
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Do not treat missing scopes as empty data while `bootstrap.complete` is false.
|
|
142
|
+
Use `bootstrap.pendingSubscriptionIds`, `bootstrap.phases`, or generated
|
|
143
|
+
subscription ids to decide which views can render complete results.
|
|
144
|
+
|
|
145
|
+
The database owns the sync lifecycle. `createSyncularDatabase` registers
|
|
146
|
+
subscriptions, runs the initial sync, starts realtime, schedules reconnect
|
|
147
|
+
catchup, and coordinates shutdown through `close()`:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { createSyncularDatabase } from '@syncular/client';
|
|
151
|
+
|
|
152
|
+
const syncular = await createSyncularDatabase<AppDb>({
|
|
153
|
+
config: {
|
|
154
|
+
baseUrl: '/sync',
|
|
155
|
+
actorId: 'user-1',
|
|
156
|
+
clientId: 'client-1',
|
|
157
|
+
},
|
|
158
|
+
subscriptions: [
|
|
159
|
+
{
|
|
160
|
+
id: 'tasks:user-1',
|
|
161
|
+
table: 'tasks',
|
|
162
|
+
scopes: { user_id: 'user-1' },
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const unsubscribe = syncular.on('rowsChanged', (event) => {
|
|
168
|
+
console.log(event.changedTables);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const status = syncular.getStatus();
|
|
172
|
+
if (status.hasPendingMutations) showSavingIndicator();
|
|
173
|
+
|
|
174
|
+
await syncular.resumeFromBackground();
|
|
175
|
+
await syncular.close();
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The database starts realtime by default. Pass `realtime: false` only for a
|
|
179
|
+
host policy that cannot hold a websocket, and `lifecycle: { autoStart: false }`
|
|
180
|
+
to open without starting sync at all. Interval polling is still off by
|
|
181
|
+
default: websocket reconnects trigger HTTP catchup sync, and failed
|
|
182
|
+
websocket binary sync-pack applies recover through HTTP pull. Use
|
|
183
|
+
`pollIntervalMs` only for environments that explicitly need polling.
|
|
184
|
+
|
|
185
|
+
## React
|
|
186
|
+
|
|
187
|
+
React apps can import the `@syncular/client/react` subpath.
|
|
188
|
+
The adapter owns the Rust browser client lifecycle when passed `options`, or
|
|
189
|
+
can wrap an already-created managed client:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import { createSyncularReact } from '@syncular/client/react';
|
|
193
|
+
|
|
194
|
+
const {
|
|
195
|
+
SyncProvider,
|
|
196
|
+
useSyncQuery,
|
|
197
|
+
useMutations,
|
|
198
|
+
useLeasedMutations,
|
|
199
|
+
useMutation,
|
|
200
|
+
useOutboxStats,
|
|
201
|
+
usePresenceWithJoin,
|
|
202
|
+
useSyncConnection,
|
|
203
|
+
} = createSyncularReact<AppDb>();
|
|
204
|
+
|
|
205
|
+
function AppShell({ children }: { children: React.ReactNode }) {
|
|
206
|
+
return (
|
|
207
|
+
<SyncProvider
|
|
208
|
+
options={{
|
|
209
|
+
config: {
|
|
210
|
+
baseUrl: '/sync',
|
|
211
|
+
actorId: 'user-1',
|
|
212
|
+
clientId: 'client-1',
|
|
213
|
+
},
|
|
214
|
+
subscriptions: [
|
|
215
|
+
{
|
|
216
|
+
id: 'tasks:user-1',
|
|
217
|
+
table: 'tasks',
|
|
218
|
+
scopes: { user_id: 'user-1' },
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
realtime: true,
|
|
222
|
+
}}
|
|
223
|
+
>
|
|
224
|
+
{children}
|
|
225
|
+
</SyncProvider>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function TaskList() {
|
|
230
|
+
const { data: tasks } = useSyncQuery(
|
|
231
|
+
({ selectFrom }) =>
|
|
232
|
+
selectFrom('tasks')
|
|
233
|
+
.select(['id', 'title'])
|
|
234
|
+
.where('user_id', '=', 'user-1'),
|
|
235
|
+
{
|
|
236
|
+
tables: ['tasks'],
|
|
237
|
+
deps: ['user-1'],
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const presence = usePresenceWithJoin('user:user-1', {
|
|
242
|
+
metadata: { view: 'tasks' },
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const m = useMutations();
|
|
246
|
+
const leased = useLeasedMutations();
|
|
247
|
+
const createTask = (title: string) =>
|
|
248
|
+
m.tasks.insert({
|
|
249
|
+
title,
|
|
250
|
+
completed: 0,
|
|
251
|
+
user_id: 'user-1',
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const completeTask = useMutation({ table: 'tasks' });
|
|
255
|
+
const markDone = (id: string) =>
|
|
256
|
+
completeTask.mutate.update(id, { completed: 1 });
|
|
257
|
+
|
|
258
|
+
const renameOffline = (id: string, title: string) =>
|
|
259
|
+
leased.tasks.update(id, { title });
|
|
260
|
+
|
|
261
|
+
const connection = useSyncConnection();
|
|
262
|
+
const outbox = useOutboxStats();
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The React entrypoint is intentionally ergonomic and Rust-backed: reads use typed
|
|
267
|
+
Kysely selectors through `useSyncQuery`, writes use generated mutations through
|
|
268
|
+
`useMutations` / `useLeasedMutations` or table-scoped
|
|
269
|
+
`useMutation` / `useLeasedMutation`, and presence stays scoped to server scope
|
|
270
|
+
keys. When the query is a Kysely builder, `useSyncQuery` uses the runtime
|
|
271
|
+
live-query observer; promise-only queries fall back to conservative row-change
|
|
272
|
+
refresh. `SyncProvider` does not recreate an owned client just because an inline
|
|
273
|
+
`options` object changed identity; pass `optionsKey` when the app intentionally
|
|
274
|
+
needs to tear down and reopen the Rust client for a new identity or database.
|
|
275
|
+
|
|
276
|
+
Generated apps also get typed row-delta helpers for realtime/UI routing. The
|
|
277
|
+
runtime event stays generic, while app code can branch on real table columns:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
import { syncularChangedRows } from './generated/syncular.browser';
|
|
281
|
+
|
|
282
|
+
const unsubscribe = syncular.on('rowsChanged', (event) => {
|
|
283
|
+
for (const task of syncularChangedRows.tasks(event)) {
|
|
284
|
+
if (task.isDelete) {
|
|
285
|
+
removeTaskFromList(task.rowId);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (task.changed.title || task.changed.completed) {
|
|
289
|
+
refreshTaskRow(task.rowId);
|
|
290
|
+
}
|
|
291
|
+
if (task.crdt.title_yjs_state) {
|
|
292
|
+
refreshActiveEditorState(task.rowId);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The returned `syncular.db` is a read/query-builder surface. Public SQL execution
|
|
299
|
+
rejects app-table and internal-table writes, including Kysely `insertInto`,
|
|
300
|
+
`updateTable`, `deleteFrom`, schema DDL, and raw mutating SQL. This prevents
|
|
301
|
+
local rows from bypassing Syncular's outbox, conflict, encryption, blob, and
|
|
302
|
+
realtime semantics. Generated app setup uses an internal schema-write path
|
|
303
|
+
before the database handle is returned; application writes should use
|
|
304
|
+
`syncular.mutations`.
|
|
305
|
+
|
|
306
|
+
Mutations schedule `client.syncOnce()` automatically after a successful local
|
|
307
|
+
commit. The scheduler coalesces repeated writes with a short debounce and queues
|
|
308
|
+
one follow-up sync if another mutation lands while sync is already running:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
const syncular = await createSyncularAppDatabase({
|
|
312
|
+
config: {
|
|
313
|
+
baseUrl: '/sync',
|
|
314
|
+
actorId: 'user-1',
|
|
315
|
+
clientId: 'client-1',
|
|
316
|
+
},
|
|
317
|
+
sync: {
|
|
318
|
+
autoSyncAfterMutation: true, // default
|
|
319
|
+
mutationSyncDebounceMs: 25,
|
|
320
|
+
rowsChangedDebounceMs: 16,
|
|
321
|
+
autoProcessBlobUploadsAfterStore: false, // default
|
|
322
|
+
blobUploadDebounceMs: 25,
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Set `autoSyncAfterMutation: false` when an app wants to batch its own sync
|
|
328
|
+
cycles explicitly. Set `autoProcessBlobUploadsAfterStore: true` when the
|
|
329
|
+
browser should process queued blob uploads after `blobs.store()` with the same
|
|
330
|
+
debounce/backpressure model. It is disabled by default so mobile/background
|
|
331
|
+
hosts can choose when network blob work is allowed.
|
|
332
|
+
|
|
333
|
+
## Blobs
|
|
334
|
+
|
|
335
|
+
Blobs are a sidecar API on the same Rust-owned SQLite client. App data still
|
|
336
|
+
uses typed Kysely queries; binary payloads are content-addressed and staged in
|
|
337
|
+
Syncular internal blob tables:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
const avatar = await syncular.blobs.store(file, {
|
|
341
|
+
mimeType: file.type,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
await syncular.mutations.profiles.upsert(userId, {
|
|
345
|
+
avatar,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
await syncular.blobs.processUploadQueue();
|
|
349
|
+
|
|
350
|
+
const bytes = await syncular.blobs.retrieve(avatar);
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
`store()` hashes and caches bytes in Rust/WASM SQLite, then queues upload unless
|
|
354
|
+
`immediate: true` is passed. Upload/download requests use the same auth header
|
|
355
|
+
lifecycle as sync and talk to the server blob routes under `${baseUrl}/blobs`.
|
|
356
|
+
Apps can call `processUploadQueue()` manually or opt into
|
|
357
|
+
`sync.autoProcessBlobUploadsAfterStore`.
|
|
358
|
+
Columns listed in `blobColumns` are typed as `BlobRef` in generated Kysely
|
|
359
|
+
types and use generated codecs so SQLite stores JSON text while app code reads
|
|
360
|
+
and writes structured blob refs.
|
|
361
|
+
|
|
362
|
+
## Auth
|
|
363
|
+
|
|
364
|
+
App code owns authentication. Pass `getHeaders` to the generated app database
|
|
365
|
+
factory when sync requests need bearer tokens, session headers, or tenant
|
|
366
|
+
headers:
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
const syncular = await createSyncularAppDatabase({
|
|
370
|
+
config: {
|
|
371
|
+
baseUrl: '/sync',
|
|
372
|
+
actorId: 'user-1',
|
|
373
|
+
clientId: 'client-1',
|
|
374
|
+
},
|
|
375
|
+
getHeaders: async () => ({
|
|
376
|
+
authorization: `Bearer ${await auth.currentAccessToken()}`,
|
|
377
|
+
}),
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The Worker refreshes those headers after opening and before `syncPull`,
|
|
382
|
+
`syncPush`, and `syncOnce`, then forwards them into Rust. The `actorId` config
|
|
383
|
+
is used for sync identity and generated default scopes; it is not sent as an
|
|
384
|
+
implicit auth credential.
|
|
385
|
+
If Rust reports HTTP 401/403 during sync, `authLifecycle` can refresh
|
|
386
|
+
credentials and the Worker retries that sync operation once with fresh headers.
|
|
387
|
+
|
|
388
|
+
Offline auth leases are explicit. They capture bounded local intent and audit
|
|
389
|
+
provenance, but the server still rechecks current authorization when queued
|
|
390
|
+
commits replay:
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
await syncular.client.issueAuthLease({
|
|
394
|
+
schemaVersion: 1,
|
|
395
|
+
scopes: [
|
|
396
|
+
{
|
|
397
|
+
subscriptionId: 'tasks:user-1',
|
|
398
|
+
table: 'tasks',
|
|
399
|
+
values: { user_id: 'user-1' },
|
|
400
|
+
operations: ['upsert', 'delete'],
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const active = await syncular.client.activeAuthLeases('user-1');
|
|
406
|
+
await syncular.leasedMutations.tasks.update('task-1', {
|
|
407
|
+
title: 'Offline edit',
|
|
408
|
+
});
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Use normal `syncular.mutations` unless the app intentionally needs lease-backed
|
|
412
|
+
offline writes.
|
|
413
|
+
|
|
414
|
+
## Platform Bridges
|
|
415
|
+
|
|
416
|
+
`@syncular/client/tauri` and `@syncular/client/react-native` expose TypeScript
|
|
417
|
+
host bindings over a native Rust runtime. They are bridge adapters, not separate
|
|
418
|
+
JavaScript sync clients:
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
import { createSyncularTauriClient } from '@syncular/client/tauri';
|
|
422
|
+
|
|
423
|
+
const client = await createSyncularTauriClient<AppDb>({
|
|
424
|
+
invoke,
|
|
425
|
+
listen,
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const rows = await client.db.selectFrom('tasks').selectAll().execute();
|
|
429
|
+
await client.leasedMutations.tasks.update('task-1', { title: 'Offline edit' });
|
|
430
|
+
await client.resumeFromBackground();
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Bridge subpaths preserve row/field metadata on `rowsChanged` events and expose
|
|
434
|
+
the same leased mutation, auth lease, lifecycle, presence, conflict, and blob
|
|
435
|
+
client shape where the native module provides those commands. They do not
|
|
436
|
+
pretend to support live-query registration by rerunning table-level events; app
|
|
437
|
+
bridges can either use row/field metadata directly or wait for a native
|
|
438
|
+
observed-query stream. Command history remains generated-client owned, not a
|
|
439
|
+
generic bridge-level JavaScript undo stack.
|
|
440
|
+
|
|
441
|
+
## Diagnostics
|
|
442
|
+
|
|
443
|
+
Pass `diagnostics` to observe structured client, worker, auth, realtime,
|
|
444
|
+
storage, sync, and blob events. Header values and websocket URLs are not emitted.
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
const syncular = await createSyncularAppDatabase({
|
|
448
|
+
config: {
|
|
449
|
+
baseUrl: '/sync',
|
|
450
|
+
actorId: 'user-1',
|
|
451
|
+
clientId: 'client-1',
|
|
452
|
+
},
|
|
453
|
+
diagnostics(event) {
|
|
454
|
+
logger.debug(event.code, event);
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
`requestTimeoutMs` is enforced in the Worker. For long sync/blob requests the
|
|
460
|
+
Worker also aborts the Rust-owned browser fetches, including snapshot chunk
|
|
461
|
+
downloads, before dropping the timed-out response.
|
|
462
|
+
|
|
463
|
+
UI code can poll `syncular.client.connectionState()` for a cheap snapshot of the
|
|
464
|
+
Worker state: closed flag, pending request count, realtime connection state,
|
|
465
|
+
storage fallback, and the latest diagnostic/error.
|
|
466
|
+
|
|
467
|
+
## Realtime
|
|
468
|
+
|
|
469
|
+
Realtime is optional and runs inside the same dedicated Worker as Rust-owned
|
|
470
|
+
SQLite. Enable it with `realtime`:
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
const syncular = await createSyncularAppDatabase({
|
|
474
|
+
config: {
|
|
475
|
+
baseUrl: '/sync',
|
|
476
|
+
actorId: 'user-1',
|
|
477
|
+
clientId: 'client-1',
|
|
478
|
+
},
|
|
479
|
+
realtime: true,
|
|
480
|
+
});
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
The Worker connects to `${baseUrl}/realtime`, listens for server `sync`
|
|
484
|
+
wakeups, runs `syncPull()` in Rust, then emits affected live-query snapshots to
|
|
485
|
+
JS listeners. Browser WebSockets cannot send custom headers; use same-origin
|
|
486
|
+
cookie auth when possible, or pass non-sensitive server-supported params:
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
await createSyncularAppDatabase({
|
|
490
|
+
config: {
|
|
491
|
+
baseUrl: '/sync',
|
|
492
|
+
actorId: 'user-1',
|
|
493
|
+
clientId: 'client-1',
|
|
494
|
+
},
|
|
495
|
+
realtime: {
|
|
496
|
+
wsUrl: 'wss://api.example.com/sync/realtime',
|
|
497
|
+
getParams: async () => ({ token: await auth.realtimeToken() }),
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Realtime also carries presence. Scope keys match the sync scope keys exposed by
|
|
503
|
+
the server, for example `user:user-1` for a `user:{user_id}` handler scope:
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
const unsubscribePresence = syncular.client.addPresenceListener((event) => {
|
|
507
|
+
renderCollaborators(event.scopeKey, event.presence);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
syncular.client.joinPresence('user:user-1', {
|
|
511
|
+
editingTaskId: 'task-1',
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
syncular.client.updatePresenceMetadata('user:user-1', {
|
|
515
|
+
editingTaskId: 'task-2',
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
const currentPresence = syncular.client.getPresence('user:user-1');
|
|
519
|
+
renderCollaborators('user:user-1', currentPresence);
|
|
520
|
+
|
|
521
|
+
syncular.client.leavePresence('user:user-1');
|
|
522
|
+
unsubscribePresence();
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
`getPresence(scopeKey)` returns the latest in-memory snapshot for that scope.
|
|
526
|
+
The server authorizes presence against the websocket connection's current
|
|
527
|
+
subscriptions, so call `syncular.client.setSubscriptions()` and complete an
|
|
528
|
+
initial sync before joining presence.
|
|
529
|
+
|
|
530
|
+
Operational events are available on the same client surface:
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
syncular.client.addEventListener('outboxChanged', (stats) => {
|
|
534
|
+
updateSyncBadge(stats.pending + stats.sending);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
syncular.client.addEventListener('conflictsChanged', (stats) => {
|
|
538
|
+
showConflictCount(stats.unresolved);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
syncular.client.addEventListener('blobUploadFailed', ({ hash, error }) => {
|
|
542
|
+
reportBlobUploadFailure(hash, error);
|
|
543
|
+
});
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
Browser event names intentionally use the Rust-native vocabulary shared with
|
|
547
|
+
native event payloads: `rowsChanged`, `outboxChanged`, `conflictsChanged`,
|
|
548
|
+
`presenceChanged`, `blobUploadCompleted`, and `blobUploadFailed`.
|
|
549
|
+
|
|
550
|
+
## Runtime Contract
|
|
551
|
+
|
|
552
|
+
The default API always uses a Worker. `createSyncularAppDatabase()` validates
|
|
553
|
+
the runtime before returning:
|
|
554
|
+
|
|
555
|
+
- package name/version must match `@syncular/client`
|
|
556
|
+
- Worker protocol version must match the generated helper
|
|
557
|
+
- generated app schema version must match the local SQLite schema state
|
|
558
|
+
- Rust runtime must include the generated schema's required feature list
|
|
559
|
+
|
|
560
|
+
`client.runtimeInfo()` exposes the package identity, Worker protocol, resolved
|
|
561
|
+
storage mode, fallback details, Worker/WASM asset URLs, Rust crate version,
|
|
562
|
+
generated schema version, and Rust feature list.
|
|
563
|
+
|
|
564
|
+
Generated clients emit `syncularGeneratedRequiredRuntimeFeatures` from schema
|
|
565
|
+
metadata. A basic app only needs `web-owned-sqlite-core`; apps using blob
|
|
566
|
+
columns, CRDT/Yjs, or field encryption add `blobs`, `crdt-yjs`, and/or `e2ee`.
|
|
567
|
+
`createSyncularAppDatabase()` passes those requirements into the Worker open
|
|
568
|
+
path automatically.
|
|
569
|
+
|
|
570
|
+
## Storage
|
|
571
|
+
|
|
572
|
+
Omitting `config.storage` defaults to `opfsSahPool`. If that default OPFS open
|
|
573
|
+
fails because the browser cannot create the sync access handle, the Worker
|
|
574
|
+
client retries with `indexedDb` and reports the fallback via
|
|
575
|
+
`runtimeInfo().storageFallback`.
|
|
576
|
+
|
|
577
|
+
Explicit storage is never silently changed:
|
|
578
|
+
|
|
579
|
+
```ts
|
|
580
|
+
await createSyncularAppDatabase({
|
|
581
|
+
config: {
|
|
582
|
+
baseUrl: '/sync',
|
|
583
|
+
actorId: 'user-1',
|
|
584
|
+
clientId: 'client-1',
|
|
585
|
+
storage: 'indexedDb',
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
`client.compactStorage()` performs bounded local cleanup in Rust-owned SQLite:
|
|
591
|
+
acked outbox commits and resolved conflicts by age, optional failed blob upload
|
|
592
|
+
rows and inactive subscription state by age, blob cache pruning by byte budget,
|
|
593
|
+
and tombstones only when the caller supplies `maxTombstoneServerVersion`.
|
|
594
|
+
|
|
595
|
+
```ts
|
|
596
|
+
await syncular.client.compactStorage({
|
|
597
|
+
olderThanMs: 7 * 24 * 60 * 60 * 1000,
|
|
598
|
+
maxBlobCacheBytes: 256 * 1024 * 1024,
|
|
599
|
+
pruneFailedBlobUploads: true,
|
|
600
|
+
maxTombstoneServerVersion: lastServerVersionKnownSafeToDrop,
|
|
601
|
+
});
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
Tombstone cleanup is intentionally not enabled by age alone; deleting
|
|
605
|
+
soft-deleted app rows before the server/version contract says they are safe can
|
|
606
|
+
break later sync repair.
|
|
607
|
+
|
|
608
|
+
## CRDT Document Fields
|
|
609
|
+
|
|
610
|
+
Generated app clients expose schema-derived CRDT field helpers, and the
|
|
611
|
+
low-level client exposes generic `openCrdtField`, `applyCrdtFieldYjsUpdate`,
|
|
612
|
+
`materializeCrdtField`, `snapshotCrdtFieldStateVector`, and `compactCrdtField`
|
|
613
|
+
methods. Keep editor-specific code above this package: TipTap schemas,
|
|
614
|
+
ProseMirror transforms, Excalidraw save policy, selection, undo, and WebView
|
|
615
|
+
messages belong in app code or optional app adapters.
|
|
616
|
+
|
|
617
|
+
Use `@syncular/client/crdt-yjs` for app-layer editor glue above this
|
|
618
|
+
package. It connects Yjs binary update streams to Syncular's durable CRDT field
|
|
619
|
+
API, preserves pending updates across failed writes, exposes backpressure,
|
|
620
|
+
prefers queued native host writes when available, and refreshes app view models
|
|
621
|
+
from materialized Syncular state after changed-row events.
|
|
622
|
+
|
|
623
|
+
For rich editors, keep Yjs as the canonical field state. ProseMirror JSON,
|
|
624
|
+
title, preview, outline, search text, and similar values are projections that
|
|
625
|
+
apps should rebuild after a CRDT changed-row event, remote apply, or compaction.
|
|
626
|
+
The Rust-owned client persists a compact binary Yjs state and state vector per
|
|
627
|
+
document field, plus an append-only binary Yjs update log with `pending`,
|
|
628
|
+
`flushed`, and `acked` status. Use `crdtDocumentSnapshot` to inspect the
|
|
629
|
+
current compacted state/vector and queue counts, `crdtUpdateLog` for adapter
|
|
630
|
+
diagnostics, and `compactStorage({ olderThanMs, pruneCrdtUpdateLog: true })` to
|
|
631
|
+
prune old acked log entries without touching the canonical compact state.
|
|
632
|
+
|
|
633
|
+
## Assets
|
|
634
|
+
|
|
635
|
+
The `@syncular/client` package writes the full Rust WASM artifact to
|
|
636
|
+
`dist/wasm`:
|
|
637
|
+
|
|
638
|
+
- `syncular.js`
|
|
639
|
+
- `syncular_bg.wasm`
|
|
640
|
+
- `syncular-runtime-artifact.json`
|
|
641
|
+
|
|
642
|
+
It also writes the core artifact to `dist/wasm-core` and the ordered catalog to
|
|
643
|
+
`dist/syncular-runtime-artifacts.json`.
|
|
644
|
+
|
|
645
|
+
The default Worker resolves those assets relative to the package runtime.
|
|
646
|
+
Generated app code can select from that catalog without changing the public
|
|
647
|
+
query/mutation API:
|
|
648
|
+
|
|
649
|
+
```ts
|
|
650
|
+
import { resolveSyncularRuntimeArtifactCatalog } from '@syncular/client';
|
|
651
|
+
|
|
652
|
+
const catalogUrl = '/syncular/syncular-runtime-artifacts.json';
|
|
653
|
+
const catalog = await fetch(catalogUrl).then((response) => response.json());
|
|
654
|
+
|
|
655
|
+
await createSyncularAppDatabase({
|
|
656
|
+
config,
|
|
657
|
+
runtimeArtifacts: resolveSyncularRuntimeArtifactCatalog(catalog, {
|
|
658
|
+
baseUrl: catalogUrl,
|
|
659
|
+
}),
|
|
660
|
+
});
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
The first artifact containing every generated required feature is used. Custom
|
|
664
|
+
asset serving can still pass a custom `worker`; the lower-level direct Rust
|
|
665
|
+
client accepts advanced `runtime`, `module`, `wasmGlueUrl`, and `wasmUrl`
|
|
666
|
+
options. Normal generated app code should not need those lower-level paths.
|
|
667
|
+
|
|
668
|
+
Release WASM builds run a size budget check after `wasm-opt -Oz` and custom
|
|
669
|
+
section stripping. The current checked budgets are `3.25 MiB` raw and
|
|
670
|
+
`1.35 MiB` gzip. Override them only for an intentional release-size decision:
|
|
6
671
|
|
|
7
672
|
```bash
|
|
8
|
-
|
|
673
|
+
SYNCULAR_WASM_RAW_BUDGET_BYTES=3407872 \
|
|
674
|
+
SYNCULAR_WASM_GZIP_BUDGET_BYTES=1415578 \
|
|
675
|
+
bun --cwd packages/client run size:wasm:check
|
|
9
676
|
```
|
|
10
677
|
|
|
11
|
-
|
|
678
|
+
The check writes an attribution report to
|
|
679
|
+
`.context/wasm-size/syncular-wasm-size.txt` when run through
|
|
680
|
+
`packages/client` `build:wasm` or `size:wasm:check`. Release builds
|
|
681
|
+
also write a non-shipping optimized profile WASM to
|
|
682
|
+
`.context/wasm-size/syncular_bg.profile.wasm` before final custom section
|
|
683
|
+
stripping so attribution can keep symbol names when available.
|
|
684
|
+
|
|
685
|
+
The current browser client is the canonical Rust-owned SQLite runtime wrapper
|
|
686
|
+
for generated clients. The no-CRDT/no-E2EE core binding artifact has measured
|
|
687
|
+
byte savings; the current core artifact also omits blob upload/cache helpers.
|
|
688
|
+
The client package `build` runs `build:wasm:variants`, which writes both
|
|
689
|
+
artifacts plus the catalog, and generated loading can select the smallest
|
|
690
|
+
matching artifact when an app serves the catalog. Publishing separate wrapper
|
|
691
|
+
packages around the same WASM would not remove bytes.
|
|
692
|
+
|
|
693
|
+
For local measurement or app experiments:
|
|
694
|
+
|
|
695
|
+
```bash
|
|
696
|
+
bun --cwd packages/client run build:wasm:core
|
|
697
|
+
bun --cwd packages/client run build:wasm:variants
|
|
698
|
+
bun --cwd packages/client run catalog:wasm
|
|
699
|
+
bun --cwd packages/client run size:wasm:core
|
|
700
|
+
```
|
|
12
701
|
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
702
|
+
`build:wasm:core` writes `dist/wasm-core/syncular.js` and
|
|
703
|
+
`dist/wasm-core/syncular_bg.wasm` with `web-owned-sqlite-core` only. That
|
|
704
|
+
artifact does not include blob, CRDT/Yjs, or E2EE support. `catalog:wasm`
|
|
705
|
+
combines `dist/wasm-core/syncular-runtime-artifact.json` and
|
|
706
|
+
`dist/wasm/syncular-runtime-artifact.json` into the top-level
|
|
707
|
+
`dist/syncular-runtime-artifacts.json` catalog.
|
|
16
708
|
|
|
17
|
-
##
|
|
709
|
+
## Package Scripts
|
|
18
710
|
|
|
19
|
-
|
|
20
|
-
|
|
711
|
+
```bash
|
|
712
|
+
bun run build
|
|
713
|
+
bun run test
|
|
714
|
+
bun run test:wasm:auth
|
|
715
|
+
bun run test:wasm:hono
|
|
716
|
+
bun run test:wasm:variants
|
|
717
|
+
```
|
|
21
718
|
|
|
22
|
-
|
|
719
|
+
`test:wasm:hono` builds the dev WASM artifact and runs the Hono-backed browser
|
|
720
|
+
smokes for auth retry, sync protocol edge cases, realtime wakeups, and blob
|
|
721
|
+
transport behavior.
|
|
23
722
|
|
|
723
|
+
`packages/client` `build:wasm`, `size:wasm:check`, and the conformance
|
|
724
|
+
gates are the current browser runtime validation path. The old JS/wa-sqlite
|
|
725
|
+
comparison benchmark was removed with the legacy TypeScript client runtime.
|