@syncular/client 0.0.6-95 → 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/src/migrate.ts
DELETED
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @syncular/client - Sync migrations (SQLite reference)
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { type Kysely, sql } from 'kysely';
|
|
6
|
-
import type { SyncClientDb } from './schema';
|
|
7
|
-
|
|
8
|
-
type SyncInternalTable =
|
|
9
|
-
| 'sync_subscription_state'
|
|
10
|
-
| 'sync_outbox_commits'
|
|
11
|
-
| 'sync_conflicts';
|
|
12
|
-
|
|
13
|
-
function toErrorMessage(error: unknown): string {
|
|
14
|
-
return error instanceof Error ? error.message : String(error);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function isMissingTableError(message: string): boolean {
|
|
18
|
-
const normalized = message.toLowerCase();
|
|
19
|
-
return (
|
|
20
|
-
normalized.includes('no such table') ||
|
|
21
|
-
(normalized.includes('relation') && normalized.includes('does not exist'))
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function isMissingColumnError(message: string): boolean {
|
|
26
|
-
const normalized = message.toLowerCase();
|
|
27
|
-
return (
|
|
28
|
-
normalized.includes('no such column') ||
|
|
29
|
-
(normalized.includes('column') && normalized.includes('does not exist'))
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function isDuplicateColumnError(message: string): boolean {
|
|
34
|
-
const normalized = message.toLowerCase();
|
|
35
|
-
return (
|
|
36
|
-
normalized.includes('duplicate column name') ||
|
|
37
|
-
(normalized.includes('column') && normalized.includes('already exists'))
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function getColumnNames<DB extends SyncClientDb>(
|
|
42
|
-
db: Kysely<DB>,
|
|
43
|
-
tableName: SyncInternalTable
|
|
44
|
-
): Promise<Set<string> | null> {
|
|
45
|
-
try {
|
|
46
|
-
const sqlite = await sql<{ name: string }>`
|
|
47
|
-
select name from pragma_table_info(${sql.val(tableName)})
|
|
48
|
-
`.execute(db);
|
|
49
|
-
return new Set(sqlite.rows.map((row) => String(row.name)));
|
|
50
|
-
} catch {
|
|
51
|
-
// Not SQLite or pragma unavailable.
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
const postgres = await sql<{ name: string }>`
|
|
56
|
-
select column_name as name
|
|
57
|
-
from information_schema.columns
|
|
58
|
-
where table_name = ${sql.val(tableName)}
|
|
59
|
-
`.execute(db);
|
|
60
|
-
return new Set(postgres.rows.map((row) => String(row.name)));
|
|
61
|
-
} catch {
|
|
62
|
-
// Introspection unavailable; caller falls back to probing.
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function hasColumn<DB extends SyncClientDb>(
|
|
69
|
-
db: Kysely<DB>,
|
|
70
|
-
tableName: SyncInternalTable,
|
|
71
|
-
columnName: string
|
|
72
|
-
): Promise<boolean> {
|
|
73
|
-
const columns = await getColumnNames(db, tableName);
|
|
74
|
-
if (columns) {
|
|
75
|
-
return columns.has(columnName);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
await sql`select ${sql.ref(columnName)} from ${sql.table(tableName)} limit 1`.execute(
|
|
80
|
-
db
|
|
81
|
-
);
|
|
82
|
-
return true;
|
|
83
|
-
} catch (error) {
|
|
84
|
-
const message = toErrorMessage(error);
|
|
85
|
-
if (isMissingTableError(message) || isMissingColumnError(message)) {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
throw error;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function addColumnIfMissing<DB extends SyncClientDb>(
|
|
93
|
-
db: Kysely<DB>,
|
|
94
|
-
tableName: SyncInternalTable,
|
|
95
|
-
columnName: string,
|
|
96
|
-
addColumn: () => Promise<void>
|
|
97
|
-
): Promise<void> {
|
|
98
|
-
if (await hasColumn(db, tableName, columnName)) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
try {
|
|
102
|
-
await addColumn();
|
|
103
|
-
} catch (error) {
|
|
104
|
-
const message = toErrorMessage(error);
|
|
105
|
-
if (isDuplicateColumnError(message)) {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function ensureClientSyncSchemaCompat<DB extends SyncClientDb>(
|
|
113
|
-
db: Kysely<DB>
|
|
114
|
-
): Promise<void> {
|
|
115
|
-
const hasTableColumn = await hasColumn(
|
|
116
|
-
db,
|
|
117
|
-
'sync_subscription_state',
|
|
118
|
-
'table'
|
|
119
|
-
);
|
|
120
|
-
if (
|
|
121
|
-
!hasTableColumn &&
|
|
122
|
-
(await hasColumn(db, 'sync_subscription_state', 'shape'))
|
|
123
|
-
) {
|
|
124
|
-
try {
|
|
125
|
-
await sql`alter table ${sql.table('sync_subscription_state')} rename column ${sql.ref('shape')} to ${sql.ref('table')}`.execute(
|
|
126
|
-
db
|
|
127
|
-
);
|
|
128
|
-
} catch {
|
|
129
|
-
await addColumnIfMissing(
|
|
130
|
-
db,
|
|
131
|
-
'sync_subscription_state',
|
|
132
|
-
'table',
|
|
133
|
-
async () => {
|
|
134
|
-
await db.schema
|
|
135
|
-
.alterTable('sync_subscription_state')
|
|
136
|
-
.addColumn('table', 'text', (col) => col.notNull().defaultTo(''))
|
|
137
|
-
.execute();
|
|
138
|
-
}
|
|
139
|
-
);
|
|
140
|
-
await sql`update ${sql.table('sync_subscription_state')}
|
|
141
|
-
set ${sql.ref('table')} = ${sql.ref('shape')}
|
|
142
|
-
where ${sql.ref('table')} = ${sql.val('')}`.execute(db);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
await addColumnIfMissing(
|
|
147
|
-
db,
|
|
148
|
-
'sync_subscription_state',
|
|
149
|
-
'bootstrap_state_json',
|
|
150
|
-
async () => {
|
|
151
|
-
await db.schema
|
|
152
|
-
.alterTable('sync_subscription_state')
|
|
153
|
-
.addColumn('bootstrap_state_json', 'text')
|
|
154
|
-
.execute();
|
|
155
|
-
}
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
await addColumnIfMissing(
|
|
159
|
-
db,
|
|
160
|
-
'sync_outbox_commits',
|
|
161
|
-
'schema_version',
|
|
162
|
-
async () => {
|
|
163
|
-
await db.schema
|
|
164
|
-
.alterTable('sync_outbox_commits')
|
|
165
|
-
.addColumn('schema_version', 'integer', (col) =>
|
|
166
|
-
col.notNull().defaultTo(1)
|
|
167
|
-
)
|
|
168
|
-
.execute();
|
|
169
|
-
}
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
await addColumnIfMissing(db, 'sync_conflicts', 'resolved_at', async () => {
|
|
173
|
-
await db.schema
|
|
174
|
-
.alterTable('sync_conflicts')
|
|
175
|
-
.addColumn('resolved_at', 'bigint')
|
|
176
|
-
.execute();
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
await addColumnIfMissing(db, 'sync_conflicts', 'resolution', async () => {
|
|
180
|
-
await db.schema
|
|
181
|
-
.alterTable('sync_conflicts')
|
|
182
|
-
.addColumn('resolution', 'text')
|
|
183
|
-
.execute();
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
await db.schema
|
|
187
|
-
.createIndex('idx_sync_outbox_commits_status_updated_at')
|
|
188
|
-
.ifNotExists()
|
|
189
|
-
.on('sync_outbox_commits')
|
|
190
|
-
.columns(['status', 'updated_at', 'created_at'])
|
|
191
|
-
.execute();
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Ensures the client sync schema exists in the database.
|
|
196
|
-
* Safe to call multiple times (idempotent).
|
|
197
|
-
*
|
|
198
|
-
* @typeParam DB - Your database type that extends SyncClientDb
|
|
199
|
-
*/
|
|
200
|
-
export async function ensureClientSyncSchema<DB extends SyncClientDb>(
|
|
201
|
-
db: Kysely<DB>
|
|
202
|
-
): Promise<void> {
|
|
203
|
-
// Schema builder doesn't need typed access - operates on raw SQL.
|
|
204
|
-
await db.schema
|
|
205
|
-
.createTable('sync_subscription_state')
|
|
206
|
-
.ifNotExists()
|
|
207
|
-
.addColumn('state_id', 'text', (col) => col.notNull())
|
|
208
|
-
.addColumn('subscription_id', 'text', (col) => col.notNull())
|
|
209
|
-
.addColumn('table', 'text', (col) => col.notNull())
|
|
210
|
-
.addColumn('scopes_json', 'text', (col) => col.notNull().defaultTo('{}'))
|
|
211
|
-
.addColumn('params_json', 'text', (col) => col.notNull())
|
|
212
|
-
.addColumn('cursor', 'bigint', (col) => col.notNull())
|
|
213
|
-
.addColumn('bootstrap_state_json', 'text')
|
|
214
|
-
.addColumn('status', 'text', (col) => col.notNull())
|
|
215
|
-
.addColumn('created_at', 'bigint', (col) => col.notNull())
|
|
216
|
-
.addColumn('updated_at', 'bigint', (col) => col.notNull())
|
|
217
|
-
.execute();
|
|
218
|
-
|
|
219
|
-
await db.schema
|
|
220
|
-
.createTable('sync_outbox_commits')
|
|
221
|
-
.ifNotExists()
|
|
222
|
-
.addColumn('id', 'text', (col) => col.primaryKey())
|
|
223
|
-
.addColumn('client_commit_id', 'text', (col) => col.notNull())
|
|
224
|
-
.addColumn('status', 'text', (col) => col.notNull())
|
|
225
|
-
.addColumn('operations_json', 'text', (col) => col.notNull())
|
|
226
|
-
.addColumn('last_response_json', 'text')
|
|
227
|
-
.addColumn('error', 'text')
|
|
228
|
-
.addColumn('created_at', 'bigint', (col) => col.notNull())
|
|
229
|
-
.addColumn('updated_at', 'bigint', (col) => col.notNull())
|
|
230
|
-
.addColumn('attempt_count', 'integer', (col) => col.notNull().defaultTo(0))
|
|
231
|
-
.addColumn('acked_commit_seq', 'bigint')
|
|
232
|
-
.addColumn('schema_version', 'integer', (col) => col.notNull().defaultTo(1))
|
|
233
|
-
.execute();
|
|
234
|
-
|
|
235
|
-
await db.schema
|
|
236
|
-
.createTable('sync_conflicts')
|
|
237
|
-
.ifNotExists()
|
|
238
|
-
.addColumn('id', 'text', (col) => col.primaryKey())
|
|
239
|
-
.addColumn('outbox_commit_id', 'text', (col) => col.notNull())
|
|
240
|
-
.addColumn('client_commit_id', 'text', (col) => col.notNull())
|
|
241
|
-
.addColumn('op_index', 'integer', (col) => col.notNull())
|
|
242
|
-
.addColumn('result_status', 'text', (col) => col.notNull())
|
|
243
|
-
.addColumn('message', 'text', (col) => col.notNull())
|
|
244
|
-
.addColumn('code', 'text')
|
|
245
|
-
.addColumn('server_version', 'bigint')
|
|
246
|
-
.addColumn('server_row_json', 'text')
|
|
247
|
-
.addColumn('created_at', 'bigint', (col) => col.notNull())
|
|
248
|
-
.addColumn('resolved_at', 'bigint')
|
|
249
|
-
.addColumn('resolution', 'text')
|
|
250
|
-
.execute();
|
|
251
|
-
|
|
252
|
-
// Apply framework-managed compatibility upgrades for legacy sync tables.
|
|
253
|
-
await ensureClientSyncSchemaCompat(db);
|
|
254
|
-
|
|
255
|
-
await db.schema
|
|
256
|
-
.createIndex('idx_sync_subscription_state_state_sub')
|
|
257
|
-
.ifNotExists()
|
|
258
|
-
.on('sync_subscription_state')
|
|
259
|
-
.columns(['state_id', 'subscription_id'])
|
|
260
|
-
.unique()
|
|
261
|
-
.execute();
|
|
262
|
-
|
|
263
|
-
await db.schema
|
|
264
|
-
.createIndex('idx_sync_subscription_state_state')
|
|
265
|
-
.ifNotExists()
|
|
266
|
-
.on('sync_subscription_state')
|
|
267
|
-
.columns(['state_id', 'updated_at'])
|
|
268
|
-
.execute();
|
|
269
|
-
|
|
270
|
-
await db.schema
|
|
271
|
-
.createIndex('idx_sync_outbox_commits_client_commit_id')
|
|
272
|
-
.ifNotExists()
|
|
273
|
-
.on('sync_outbox_commits')
|
|
274
|
-
.columns(['client_commit_id'])
|
|
275
|
-
.unique()
|
|
276
|
-
.execute();
|
|
277
|
-
|
|
278
|
-
await db.schema
|
|
279
|
-
.createIndex('idx_sync_outbox_commits_status_created_at')
|
|
280
|
-
.ifNotExists()
|
|
281
|
-
.on('sync_outbox_commits')
|
|
282
|
-
.columns(['status', 'created_at'])
|
|
283
|
-
.execute();
|
|
284
|
-
|
|
285
|
-
await db.schema
|
|
286
|
-
.createIndex('idx_sync_outbox_commits_status_updated_at')
|
|
287
|
-
.ifNotExists()
|
|
288
|
-
.on('sync_outbox_commits')
|
|
289
|
-
.columns(['status', 'updated_at', 'created_at'])
|
|
290
|
-
.execute();
|
|
291
|
-
|
|
292
|
-
await db.schema
|
|
293
|
-
.createIndex('idx_sync_conflicts_outbox_commit')
|
|
294
|
-
.ifNotExists()
|
|
295
|
-
.on('sync_conflicts')
|
|
296
|
-
.columns(['outbox_commit_id'])
|
|
297
|
-
.execute();
|
|
298
|
-
|
|
299
|
-
await db.schema
|
|
300
|
-
.createIndex('idx_sync_conflicts_resolved_at')
|
|
301
|
-
.ifNotExists()
|
|
302
|
-
.on('sync_conflicts')
|
|
303
|
-
.columns(['resolved_at'])
|
|
304
|
-
.execute();
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Drops the client sync schema from the database.
|
|
309
|
-
*
|
|
310
|
-
* @typeParam DB - Your database type that extends SyncClientDb
|
|
311
|
-
*/
|
|
312
|
-
export async function dropClientSyncSchema<DB extends SyncClientDb>(
|
|
313
|
-
db: Kysely<DB>
|
|
314
|
-
): Promise<void> {
|
|
315
|
-
await db.schema.dropTable('sync_conflicts').ifExists().execute();
|
|
316
|
-
await db.schema.dropTable('sync_outbox_commits').ifExists().execute();
|
|
317
|
-
await db.schema.dropTable('sync_subscription_state').ifExists().execute();
|
|
318
|
-
}
|
package/src/outbox.ts
DELETED
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @syncular/client - Sync outbox (commit-based)
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { SyncOperation } from '@syncular/core';
|
|
6
|
-
import { isRecord, randomId } from '@syncular/core';
|
|
7
|
-
import type { Kysely } from 'kysely';
|
|
8
|
-
import { sql } from 'kysely';
|
|
9
|
-
import type { OutboxCommitStatus, SyncClientDb } from './schema';
|
|
10
|
-
|
|
11
|
-
export interface OutboxCommit {
|
|
12
|
-
id: string;
|
|
13
|
-
client_commit_id: string;
|
|
14
|
-
status: OutboxCommitStatus;
|
|
15
|
-
operations: SyncOperation[];
|
|
16
|
-
last_response_json: string | null;
|
|
17
|
-
error: string | null;
|
|
18
|
-
created_at: number;
|
|
19
|
-
updated_at: number;
|
|
20
|
-
attempt_count: number;
|
|
21
|
-
acked_commit_seq: number | null;
|
|
22
|
-
/** Client schema version when commit was created */
|
|
23
|
-
schema_version: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function isSyncOperation(value: unknown): value is SyncOperation {
|
|
27
|
-
if (!isRecord(value)) return false;
|
|
28
|
-
if (typeof value.table !== 'string') return false;
|
|
29
|
-
if (typeof value.row_id !== 'string') return false;
|
|
30
|
-
if (value.op !== 'upsert' && value.op !== 'delete') return false;
|
|
31
|
-
if (value.payload !== null && !isRecord(value.payload)) return false;
|
|
32
|
-
if (!('base_version' in value)) return true;
|
|
33
|
-
return (
|
|
34
|
-
value.base_version === null ||
|
|
35
|
-
value.base_version === undefined ||
|
|
36
|
-
typeof value.base_version === 'number'
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function parseOperations(value: unknown): SyncOperation[] {
|
|
41
|
-
let parsed: unknown = value;
|
|
42
|
-
if (typeof value === 'string') {
|
|
43
|
-
try {
|
|
44
|
-
parsed = JSON.parse(value);
|
|
45
|
-
} catch {
|
|
46
|
-
return [];
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (!Array.isArray(parsed)) return [];
|
|
51
|
-
|
|
52
|
-
const out: SyncOperation[] = [];
|
|
53
|
-
for (const item of parsed) {
|
|
54
|
-
if (!isSyncOperation(item)) continue;
|
|
55
|
-
out.push(item);
|
|
56
|
-
}
|
|
57
|
-
return out;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export async function enqueueOutboxCommit<DB extends SyncClientDb>(
|
|
61
|
-
db: Kysely<DB>,
|
|
62
|
-
args: {
|
|
63
|
-
operations: SyncOperation[];
|
|
64
|
-
clientCommitId?: string;
|
|
65
|
-
nowMs?: number;
|
|
66
|
-
/** Client schema version (default: 1) */
|
|
67
|
-
schemaVersion?: number;
|
|
68
|
-
}
|
|
69
|
-
): Promise<{ id: string; clientCommitId: string }> {
|
|
70
|
-
const now = args.nowMs ?? Date.now();
|
|
71
|
-
const id = randomId();
|
|
72
|
-
const clientCommitId = args.clientCommitId ?? randomId();
|
|
73
|
-
const schemaVersion = args.schemaVersion ?? 1;
|
|
74
|
-
|
|
75
|
-
await sql`
|
|
76
|
-
insert into ${sql.table('sync_outbox_commits')} (
|
|
77
|
-
${sql.join([
|
|
78
|
-
sql.ref('id'),
|
|
79
|
-
sql.ref('client_commit_id'),
|
|
80
|
-
sql.ref('status'),
|
|
81
|
-
sql.ref('operations_json'),
|
|
82
|
-
sql.ref('last_response_json'),
|
|
83
|
-
sql.ref('error'),
|
|
84
|
-
sql.ref('created_at'),
|
|
85
|
-
sql.ref('updated_at'),
|
|
86
|
-
sql.ref('attempt_count'),
|
|
87
|
-
sql.ref('acked_commit_seq'),
|
|
88
|
-
sql.ref('schema_version'),
|
|
89
|
-
])}
|
|
90
|
-
) values (
|
|
91
|
-
${sql.join([
|
|
92
|
-
sql.val(id),
|
|
93
|
-
sql.val(clientCommitId),
|
|
94
|
-
sql.val('pending'),
|
|
95
|
-
sql.val(JSON.stringify(args.operations ?? [])),
|
|
96
|
-
sql.val(null),
|
|
97
|
-
sql.val(null),
|
|
98
|
-
sql.val(now),
|
|
99
|
-
sql.val(now),
|
|
100
|
-
sql.val(0),
|
|
101
|
-
sql.val(null),
|
|
102
|
-
sql.val(schemaVersion),
|
|
103
|
-
])}
|
|
104
|
-
)
|
|
105
|
-
`.execute(db);
|
|
106
|
-
|
|
107
|
-
return { id, clientCommitId };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Atomically claim and return the next sendable outbox commit.
|
|
112
|
-
*
|
|
113
|
-
* Uses SELECT then UPDATE with status check to prevent race conditions.
|
|
114
|
-
* If another tab claims the same commit, retries with a different candidate.
|
|
115
|
-
*/
|
|
116
|
-
export async function getNextSendableOutboxCommit<DB extends SyncClientDb>(
|
|
117
|
-
db: Kysely<DB>,
|
|
118
|
-
options?: { staleTimeoutMs?: number; maxRetries?: number }
|
|
119
|
-
): Promise<OutboxCommit | null> {
|
|
120
|
-
const staleTimeoutMs = options?.staleTimeoutMs ?? 30000;
|
|
121
|
-
const maxRetries = options?.maxRetries ?? 3;
|
|
122
|
-
|
|
123
|
-
// Track IDs we've already tried to claim (to avoid retrying the same one)
|
|
124
|
-
const attemptedIds = new Set<string>();
|
|
125
|
-
|
|
126
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
127
|
-
const staleThreshold = Date.now() - staleTimeoutMs;
|
|
128
|
-
const now = Date.now();
|
|
129
|
-
|
|
130
|
-
// Find a candidate to claim, excluding ones we've already tried
|
|
131
|
-
const attempted = Array.from(attemptedIds);
|
|
132
|
-
const attemptedFilter =
|
|
133
|
-
attempted.length === 0
|
|
134
|
-
? sql``
|
|
135
|
-
: sql`and ${sql.ref('id')} not in (${sql.join(
|
|
136
|
-
attempted.map((id) => sql.val(id)),
|
|
137
|
-
sql`, `
|
|
138
|
-
)})`;
|
|
139
|
-
|
|
140
|
-
const candidateResult = await sql<{ id: string }>`
|
|
141
|
-
select ${sql.ref('id')}
|
|
142
|
-
from ${sql.table('sync_outbox_commits')}
|
|
143
|
-
where (
|
|
144
|
-
${sql.ref('status')} = ${sql.val('pending')}
|
|
145
|
-
or (
|
|
146
|
-
${sql.ref('status')} = ${sql.val('sending')}
|
|
147
|
-
and ${sql.ref('updated_at')} < ${sql.val(staleThreshold)}
|
|
148
|
-
)
|
|
149
|
-
)
|
|
150
|
-
${attemptedFilter}
|
|
151
|
-
order by ${sql.ref('created_at')} asc
|
|
152
|
-
limit ${sql.val(1)}
|
|
153
|
-
`.execute(db);
|
|
154
|
-
const candidate = candidateResult.rows[0];
|
|
155
|
-
if (!candidate) return null;
|
|
156
|
-
|
|
157
|
-
attemptedIds.add(candidate.id);
|
|
158
|
-
|
|
159
|
-
// Atomically claim the commit using UPDATE...WHERE with status check
|
|
160
|
-
const claimResult = await sql`
|
|
161
|
-
update ${sql.table('sync_outbox_commits')}
|
|
162
|
-
set
|
|
163
|
-
${sql.ref('status')} = ${sql.val('sending')},
|
|
164
|
-
${sql.ref('updated_at')} = ${sql.val(now)},
|
|
165
|
-
${sql.ref('attempt_count')} = ${sql.ref('attempt_count')} + ${sql.val(1)},
|
|
166
|
-
${sql.ref('error')} = ${sql.val(null)},
|
|
167
|
-
${sql.ref('last_response_json')} = ${sql.val(null)}
|
|
168
|
-
where ${sql.ref('id')} = ${sql.val(candidate.id)}
|
|
169
|
-
and (
|
|
170
|
-
${sql.ref('status')} = ${sql.val('pending')}
|
|
171
|
-
or (
|
|
172
|
-
${sql.ref('status')} = ${sql.val('sending')}
|
|
173
|
-
and ${sql.ref('updated_at')} < ${sql.val(staleThreshold)}
|
|
174
|
-
)
|
|
175
|
-
)
|
|
176
|
-
`.execute(db);
|
|
177
|
-
|
|
178
|
-
const claimed = Number(claimResult.numAffectedRows ?? 0) > 0;
|
|
179
|
-
if (claimed) {
|
|
180
|
-
const rowResult = await sql<{
|
|
181
|
-
id: string;
|
|
182
|
-
client_commit_id: string;
|
|
183
|
-
status: OutboxCommitStatus;
|
|
184
|
-
operations_json: string;
|
|
185
|
-
last_response_json: string | null;
|
|
186
|
-
error: string | null;
|
|
187
|
-
created_at: number;
|
|
188
|
-
updated_at: number;
|
|
189
|
-
attempt_count: number;
|
|
190
|
-
acked_commit_seq: number | null;
|
|
191
|
-
schema_version: number | null;
|
|
192
|
-
}>`
|
|
193
|
-
select
|
|
194
|
-
${sql.ref('id')},
|
|
195
|
-
${sql.ref('client_commit_id')},
|
|
196
|
-
${sql.ref('status')},
|
|
197
|
-
${sql.ref('operations_json')},
|
|
198
|
-
${sql.ref('last_response_json')},
|
|
199
|
-
${sql.ref('error')},
|
|
200
|
-
${sql.ref('created_at')},
|
|
201
|
-
${sql.ref('updated_at')},
|
|
202
|
-
${sql.ref('attempt_count')},
|
|
203
|
-
${sql.ref('acked_commit_seq')},
|
|
204
|
-
${sql.ref('schema_version')}
|
|
205
|
-
from ${sql.table('sync_outbox_commits')}
|
|
206
|
-
where ${sql.ref('id')} = ${sql.val(candidate.id)}
|
|
207
|
-
`.execute(db);
|
|
208
|
-
const row = rowResult.rows[0];
|
|
209
|
-
if (!row) continue;
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
id: row.id,
|
|
213
|
-
client_commit_id: row.client_commit_id,
|
|
214
|
-
status: row.status,
|
|
215
|
-
operations: parseOperations(row.operations_json),
|
|
216
|
-
last_response_json: row.last_response_json,
|
|
217
|
-
error: row.error,
|
|
218
|
-
created_at: row.created_at,
|
|
219
|
-
updated_at: row.updated_at,
|
|
220
|
-
attempt_count: row.attempt_count,
|
|
221
|
-
acked_commit_seq: row.acked_commit_seq,
|
|
222
|
-
schema_version: row.schema_version ?? 1,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Another tab claimed it, retry with next candidate
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function markOutboxCommitSending<DB extends SyncClientDb>(
|
|
233
|
-
db: Kysely<DB>,
|
|
234
|
-
id: string
|
|
235
|
-
): Promise<void> {
|
|
236
|
-
const now = Date.now();
|
|
237
|
-
|
|
238
|
-
await sql`
|
|
239
|
-
update ${sql.table('sync_outbox_commits')}
|
|
240
|
-
set
|
|
241
|
-
${sql.ref('status')} = ${sql.val('sending')},
|
|
242
|
-
${sql.ref('updated_at')} = ${sql.val(now)},
|
|
243
|
-
${sql.ref('attempt_count')} = ${sql.ref('attempt_count')} + ${sql.val(1)},
|
|
244
|
-
${sql.ref('error')} = ${sql.val(null)},
|
|
245
|
-
${sql.ref('last_response_json')} = ${sql.val(null)}
|
|
246
|
-
where ${sql.ref('id')} = ${sql.val(id)}
|
|
247
|
-
`.execute(db);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export async function markOutboxCommitAcked<DB extends SyncClientDb>(
|
|
251
|
-
db: Kysely<DB>,
|
|
252
|
-
args: { id: string; commitSeq?: number | null; responseJson?: string | null }
|
|
253
|
-
): Promise<void> {
|
|
254
|
-
const now = Date.now();
|
|
255
|
-
|
|
256
|
-
await sql`
|
|
257
|
-
update ${sql.table('sync_outbox_commits')}
|
|
258
|
-
set
|
|
259
|
-
${sql.ref('status')} = ${sql.val('acked')},
|
|
260
|
-
${sql.ref('updated_at')} = ${sql.val(now)},
|
|
261
|
-
${sql.ref('acked_commit_seq')} = ${sql.val(args.commitSeq ?? null)},
|
|
262
|
-
${sql.ref('error')} = ${sql.val(null)},
|
|
263
|
-
${sql.ref('last_response_json')} = ${sql.val(args.responseJson ?? null)}
|
|
264
|
-
where ${sql.ref('id')} = ${sql.val(args.id)}
|
|
265
|
-
`.execute(db);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export async function markOutboxCommitFailed<DB extends SyncClientDb>(
|
|
269
|
-
db: Kysely<DB>,
|
|
270
|
-
args: { id: string; error: string; responseJson?: string | null }
|
|
271
|
-
): Promise<void> {
|
|
272
|
-
const now = Date.now();
|
|
273
|
-
|
|
274
|
-
await sql`
|
|
275
|
-
update ${sql.table('sync_outbox_commits')}
|
|
276
|
-
set
|
|
277
|
-
${sql.ref('status')} = ${sql.val('failed')},
|
|
278
|
-
${sql.ref('updated_at')} = ${sql.val(now)},
|
|
279
|
-
${sql.ref('error')} = ${sql.val(args.error)},
|
|
280
|
-
${sql.ref('last_response_json')} = ${sql.val(args.responseJson ?? null)}
|
|
281
|
-
where ${sql.ref('id')} = ${sql.val(args.id)}
|
|
282
|
-
`.execute(db);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export async function markOutboxCommitPending<DB extends SyncClientDb>(
|
|
286
|
-
db: Kysely<DB>,
|
|
287
|
-
args: { id: string; error?: string | null; responseJson?: string | null }
|
|
288
|
-
): Promise<void> {
|
|
289
|
-
const now = Date.now();
|
|
290
|
-
|
|
291
|
-
await sql`
|
|
292
|
-
update ${sql.table('sync_outbox_commits')}
|
|
293
|
-
set
|
|
294
|
-
${sql.ref('status')} = ${sql.val('pending')},
|
|
295
|
-
${sql.ref('updated_at')} = ${sql.val(now)},
|
|
296
|
-
${sql.ref('error')} = ${sql.val(args.error ?? null)},
|
|
297
|
-
${sql.ref('last_response_json')} = ${sql.val(args.responseJson ?? null)}
|
|
298
|
-
where ${sql.ref('id')} = ${sql.val(args.id)}
|
|
299
|
-
`.execute(db);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
async function deleteAckedOutboxCommits<DB extends SyncClientDb>(
|
|
303
|
-
db: Kysely<DB>
|
|
304
|
-
): Promise<number> {
|
|
305
|
-
const res = await sql`
|
|
306
|
-
delete from ${sql.table('sync_outbox_commits')}
|
|
307
|
-
where ${sql.ref('status')} = ${sql.val('acked')}
|
|
308
|
-
`.execute(db);
|
|
309
|
-
return Number(res.numAffectedRows ?? 0);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
async function deleteFailedOutboxCommits<DB extends SyncClientDb>(
|
|
313
|
-
db: Kysely<DB>
|
|
314
|
-
): Promise<number> {
|
|
315
|
-
const res = await sql`
|
|
316
|
-
delete from ${sql.table('sync_outbox_commits')}
|
|
317
|
-
where ${sql.ref('status')} = ${sql.val('failed')}
|
|
318
|
-
`.execute(db);
|
|
319
|
-
return Number(res.numAffectedRows ?? 0);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
async function clearAllOutboxCommits<DB extends SyncClientDb>(
|
|
323
|
-
db: Kysely<DB>
|
|
324
|
-
): Promise<number> {
|
|
325
|
-
const res =
|
|
326
|
-
await sql`delete from ${sql.table('sync_outbox_commits')}`.execute(db);
|
|
327
|
-
return Number(res.numAffectedRows ?? 0);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Outbox namespace - organized API for outbox operations.
|
|
332
|
-
*
|
|
333
|
-
* @example
|
|
334
|
-
* ```typescript
|
|
335
|
-
* import { outbox } from '@syncular/client';
|
|
336
|
-
*
|
|
337
|
-
* // Enqueue a new commit
|
|
338
|
-
* const { id } = await outbox.enqueue(db, { operations: [...] });
|
|
339
|
-
*
|
|
340
|
-
* // Get next sendable commit
|
|
341
|
-
* const commit = await outbox.getNextSendable(db);
|
|
342
|
-
*
|
|
343
|
-
* // Mark commit status
|
|
344
|
-
* await outbox.mark.acked(db, { id, commitSeq: 42 });
|
|
345
|
-
* await outbox.mark.failed(db, { id, error: 'Network error' });
|
|
346
|
-
* await outbox.mark.pending(db, { id });
|
|
347
|
-
* await outbox.mark.sending(db, id);
|
|
348
|
-
*
|
|
349
|
-
* // Cleanup commits
|
|
350
|
-
* await outbox.cleanup.acked(db);
|
|
351
|
-
* await outbox.cleanup.failed(db);
|
|
352
|
-
* await outbox.cleanup.all(db);
|
|
353
|
-
* ```
|
|
354
|
-
*/
|
|
355
|
-
export const outbox = {
|
|
356
|
-
/** Enqueue a new commit to the outbox */
|
|
357
|
-
enqueue: enqueueOutboxCommit,
|
|
358
|
-
|
|
359
|
-
/** Get the next sendable commit (atomically claims it) */
|
|
360
|
-
getNextSendable: getNextSendableOutboxCommit,
|
|
361
|
-
|
|
362
|
-
/** Mark commit status */
|
|
363
|
-
mark: {
|
|
364
|
-
/** Mark commit as sending (in progress) */
|
|
365
|
-
sending: markOutboxCommitSending,
|
|
366
|
-
/** Mark commit as acknowledged (successfully synced) */
|
|
367
|
-
acked: markOutboxCommitAcked,
|
|
368
|
-
/** Mark commit as failed (permanent error) */
|
|
369
|
-
failed: markOutboxCommitFailed,
|
|
370
|
-
/** Mark commit as pending (ready for retry) */
|
|
371
|
-
pending: markOutboxCommitPending,
|
|
372
|
-
},
|
|
373
|
-
|
|
374
|
-
/** Cleanup operations */
|
|
375
|
-
cleanup: {
|
|
376
|
-
/** Delete all acknowledged commits */
|
|
377
|
-
acked: deleteAckedOutboxCommits,
|
|
378
|
-
/** Delete all failed commits */
|
|
379
|
-
failed: deleteFailedOutboxCommits,
|
|
380
|
-
/** Delete all commits (clear entire outbox) */
|
|
381
|
-
all: clearAllOutboxCommits,
|
|
382
|
-
},
|
|
383
|
-
} as const;
|