@syncular/client 0.30.10 → 0.30.11
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 +8 -0
- package/dist/apply.js +17 -25
- package/dist/blob.js +13 -39
- package/dist/client.js +118 -117
- package/dist/failed-overlay.d.ts +6 -2
- package/dist/failed-overlay.js +82 -15
- package/dist/outbox.js +11 -21
- package/dist/outcomes.d.ts +8 -0
- package/dist/outcomes.js +18 -27
- package/dist/previous-version.js +14 -28
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +50 -85
- package/dist/state.d.ts +6 -9
- package/dist/state.js +66 -32
- package/dist/wasm-database.js +40 -24
- package/dist/worker-host.js +37 -11
- package/package.json +3 -3
- package/src/apply.ts +17 -25
- package/src/blob.ts +17 -39
- package/src/client.ts +222 -163
- package/src/failed-overlay.ts +155 -17
- package/src/outbox.ts +11 -21
- package/src/outcomes.ts +35 -31
- package/src/previous-version.ts +14 -28
- package/src/schema.ts +91 -96
- package/src/state.ts +90 -33
- package/src/wasm-database.ts +56 -29
- package/src/worker-host.ts +34 -12
package/src/client.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
failedOverlayCommits,
|
|
4
4
|
retainFailedRows,
|
|
5
5
|
restoreFailedBases,
|
|
6
|
+
uniqueConflicts,
|
|
6
7
|
} from './failed-overlay';
|
|
7
8
|
import {
|
|
8
9
|
ProgressEmitter,
|
|
@@ -219,6 +220,7 @@ import {
|
|
|
219
220
|
getSubscription,
|
|
220
221
|
loadSubscriptions,
|
|
221
222
|
pruneUnknownSubscriptions,
|
|
223
|
+
saveSubscriptionScopeSchema,
|
|
222
224
|
resetSubscriptionsForBump,
|
|
223
225
|
type SubscriptionRecord,
|
|
224
226
|
saveSubscription,
|
|
@@ -947,7 +949,7 @@ export class SyncClient {
|
|
|
947
949
|
const subscriptions = pruneUnknownSubscriptions(
|
|
948
950
|
this.#db,
|
|
949
951
|
loadSubscriptions(this.#db),
|
|
950
|
-
|
|
952
|
+
this.#schema,
|
|
951
953
|
);
|
|
952
954
|
this.#started = true;
|
|
953
955
|
// A persisted active subscription needs one catch-up round on every open:
|
|
@@ -1000,6 +1002,7 @@ export class SyncClient {
|
|
|
1000
1002
|
String(this.#schema.version),
|
|
1001
1003
|
);
|
|
1002
1004
|
setLocalSchemaDescriptor(this.#db, this.#schema);
|
|
1005
|
+
saveSubscriptionScopeSchema(this.#db, this.#schema);
|
|
1003
1006
|
});
|
|
1004
1007
|
return;
|
|
1005
1008
|
}
|
|
@@ -1011,6 +1014,7 @@ export class SyncClient {
|
|
|
1011
1014
|
this.#db.transaction(() => {
|
|
1012
1015
|
ensureLocalSyncedSchema(this.#db, this.#schema);
|
|
1013
1016
|
setLocalSchemaDescriptor(this.#db, this.#schema);
|
|
1017
|
+
saveSubscriptionScopeSchema(this.#db, this.#schema);
|
|
1014
1018
|
});
|
|
1015
1019
|
return;
|
|
1016
1020
|
}
|
|
@@ -1050,6 +1054,12 @@ export class SyncClient {
|
|
|
1050
1054
|
),
|
|
1051
1055
|
);
|
|
1052
1056
|
}
|
|
1057
|
+
pruneUnknownSubscriptions(
|
|
1058
|
+
this.#db,
|
|
1059
|
+
loadSubscriptions(this.#db),
|
|
1060
|
+
this.#schema,
|
|
1061
|
+
true,
|
|
1062
|
+
);
|
|
1053
1063
|
dropAndRecreateSyncedTables(this.#db, this.#schema);
|
|
1054
1064
|
resetSubscriptionsForBump(this.#db);
|
|
1055
1065
|
setMeta(
|
|
@@ -1058,6 +1068,7 @@ export class SyncClient {
|
|
|
1058
1068
|
String(this.#schema.version),
|
|
1059
1069
|
);
|
|
1060
1070
|
setLocalSchemaDescriptor(this.#db, this.#schema);
|
|
1071
|
+
saveSubscriptionScopeSchema(this.#db, this.#schema);
|
|
1061
1072
|
});
|
|
1062
1073
|
// Whole-DB reset: every synced table's rows changed (I1 eviction-shaped).
|
|
1063
1074
|
for (const table of this.#schema.tables.values()) batch.table(table.name);
|
|
@@ -1403,13 +1414,16 @@ export class SyncClient {
|
|
|
1403
1414
|
previous.tables.join('\0') !== tables.join('\0')
|
|
1404
1415
|
) {
|
|
1405
1416
|
this.#queryFailures.delete(owner.id);
|
|
1406
|
-
this.#queryFailures.set(
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1417
|
+
this.#queryFailures.set(
|
|
1418
|
+
owner.id,
|
|
1419
|
+
definedObject({
|
|
1420
|
+
id: owner.id,
|
|
1421
|
+
tables,
|
|
1422
|
+
code,
|
|
1423
|
+
sqliteCode: sqliteCode,
|
|
1424
|
+
atMs: this.#now(),
|
|
1425
|
+
}),
|
|
1426
|
+
);
|
|
1413
1427
|
for (const id of this.#queryFailures.keys()) {
|
|
1414
1428
|
if (this.#queryFailures.size <= MAX_DIAGNOSTIC_QUERY_FAILURES)
|
|
1415
1429
|
break;
|
|
@@ -1798,22 +1812,18 @@ export class SyncClient {
|
|
|
1798
1812
|
}
|
|
1799
1813
|
return a.id.localeCompare(b.id);
|
|
1800
1814
|
});
|
|
1801
|
-
return {
|
|
1815
|
+
return definedObject({
|
|
1802
1816
|
version: CLIENT_DIAGNOSTICS_VERSION,
|
|
1803
1817
|
capturedAtMs,
|
|
1804
|
-
host: {
|
|
1818
|
+
host: definedObject({
|
|
1805
1819
|
kind: 'direct',
|
|
1806
1820
|
role: 'single',
|
|
1807
1821
|
connectivity,
|
|
1808
1822
|
realtime: this.#realtimeState,
|
|
1809
1823
|
realtimePolicy: this.#realtimePolicy,
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
...(this.#realtimeRetryDelayMs !== undefined
|
|
1814
|
-
? { realtimeRetryDelayMs: this.#realtimeRetryDelayMs }
|
|
1815
|
-
: {}),
|
|
1816
|
-
},
|
|
1824
|
+
realtimeReasonCode: this.#realtimeReasonCode,
|
|
1825
|
+
realtimeRetryDelayMs: this.#realtimeRetryDelayMs,
|
|
1826
|
+
}),
|
|
1817
1827
|
securityLifecycle: this.#securityLifecycle,
|
|
1818
1828
|
schema: {
|
|
1819
1829
|
currentVersion: this.#config.schema.version,
|
|
@@ -1837,25 +1847,21 @@ export class SyncClient {
|
|
|
1837
1847
|
),
|
|
1838
1848
|
subscriptionsTruncated:
|
|
1839
1849
|
allSubscriptions.length > MAX_DIAGNOSTIC_EXPECTED_SUBSCRIPTIONS,
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
? { lastChange: this.#lastChange }
|
|
1843
|
-
: {}),
|
|
1850
|
+
lastRound: this.#lastRound,
|
|
1851
|
+
lastChange: this.#lastChange,
|
|
1844
1852
|
storage: this.#diagnosticStorage(),
|
|
1845
1853
|
queryFailures: [...this.#queryFailures.values()],
|
|
1846
|
-
};
|
|
1854
|
+
});
|
|
1847
1855
|
}
|
|
1848
1856
|
|
|
1849
1857
|
#diagnosticLease(nowMs: number): ClientDiagnosticsSnapshot['lease'] {
|
|
1850
1858
|
const lease = this.#leaseState;
|
|
1851
1859
|
if (lease?.errorCode !== undefined) {
|
|
1852
|
-
return {
|
|
1860
|
+
return definedObject({
|
|
1853
1861
|
state: 'stopped',
|
|
1854
1862
|
errorCode: this.#diagnosticCode(lease.errorCode),
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
: {}),
|
|
1858
|
-
};
|
|
1863
|
+
expiresAtMs: lease.expiresAtMs,
|
|
1864
|
+
});
|
|
1859
1865
|
}
|
|
1860
1866
|
if (lease?.expiresAtMs === undefined) return { state: 'none' };
|
|
1861
1867
|
return {
|
|
@@ -1874,18 +1880,16 @@ export class SyncClient {
|
|
|
1874
1880
|
);
|
|
1875
1881
|
const outboxBytes = Number(
|
|
1876
1882
|
this.#db.query(
|
|
1877
|
-
'SELECT COALESCE(SUM(LENGTH(operations)),
|
|
1883
|
+
'SELECT COALESCE(SUM(LENGTH(operations)),0)AS bytes FROM _syncular_outbox',
|
|
1878
1884
|
)[0]?.bytes ?? 0,
|
|
1879
1885
|
);
|
|
1880
1886
|
const outcome = this.#db.query(
|
|
1881
|
-
|
|
1882
|
-
COALESCE(SUM(LENGTH(results) + COALESCE(LENGTH(operations), 0)), 0) AS bytes
|
|
1883
|
-
FROM _syncular_commit_outcomes`,
|
|
1887
|
+
'SELECT COUNT(*)AS entries,COALESCE(SUM(LENGTH(results)+ COALESCE(LENGTH(operations),0)),0)AS bytes FROM _syncular_commit_outcomes',
|
|
1884
1888
|
)[0];
|
|
1885
1889
|
const blobBytes = this.#hasBlobs
|
|
1886
1890
|
? Number(
|
|
1887
1891
|
this.#db.query(
|
|
1888
|
-
'SELECT COALESCE(SUM(byte_length),
|
|
1892
|
+
'SELECT COALESCE(SUM(byte_length),0)AS bytes FROM _syncular_blobs',
|
|
1889
1893
|
)[0]?.bytes ?? 0,
|
|
1890
1894
|
)
|
|
1891
1895
|
: 0;
|
|
@@ -2323,7 +2327,7 @@ export class SyncClient {
|
|
|
2323
2327
|
return this.#applyBatch(
|
|
2324
2328
|
(batch) => {
|
|
2325
2329
|
restoreFailedBases(this.#db, this.#schema);
|
|
2326
|
-
this.#db.exec('DELETE FROM _syncular_failed_rows WHERE commit_id
|
|
2330
|
+
this.#db.exec('DELETE FROM _syncular_failed_rows WHERE commit_id=?', [
|
|
2327
2331
|
input.clientCommitId,
|
|
2328
2332
|
]);
|
|
2329
2333
|
const resolved = persistCommitOutcomeResolution(
|
|
@@ -2476,14 +2480,17 @@ export class SyncClient {
|
|
|
2476
2480
|
}
|
|
2477
2481
|
return;
|
|
2478
2482
|
}
|
|
2479
|
-
saveSubscription(
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2483
|
+
saveSubscription(
|
|
2484
|
+
this.#db,
|
|
2485
|
+
definedObject({
|
|
2486
|
+
id: input.id,
|
|
2487
|
+
table: input.table,
|
|
2488
|
+
scopes: input.scopes,
|
|
2489
|
+
params: input.params,
|
|
2490
|
+
cursor: -1,
|
|
2491
|
+
status: 'active',
|
|
2492
|
+
}),
|
|
2493
|
+
);
|
|
2487
2494
|
this.#setSyncNeeded(true);
|
|
2488
2495
|
this.#emitSyncIntent({ kind: 'interactive' });
|
|
2489
2496
|
this.#emitDiagnostics();
|
|
@@ -2549,14 +2556,17 @@ export class SyncClient {
|
|
|
2549
2556
|
// Re-entry cancels any deferred eviction for this sub id.
|
|
2550
2557
|
deletePendingEviction(this.#db, subId);
|
|
2551
2558
|
insertWindowUnit(this.#db, baseKey, unit, subId);
|
|
2552
|
-
saveSubscription(
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2559
|
+
saveSubscription(
|
|
2560
|
+
this.#db,
|
|
2561
|
+
definedObject({
|
|
2562
|
+
id: subId,
|
|
2563
|
+
table: base.table,
|
|
2564
|
+
scopes: unitScopes(base, unit),
|
|
2565
|
+
params: base.params,
|
|
2566
|
+
cursor: -1,
|
|
2567
|
+
status: 'active',
|
|
2568
|
+
}),
|
|
2569
|
+
);
|
|
2560
2570
|
});
|
|
2561
2571
|
this.#needsPull = true;
|
|
2562
2572
|
batch.window(baseKey, base.table, unit);
|
|
@@ -2732,14 +2742,12 @@ export class SyncClient {
|
|
|
2732
2742
|
const operations: OutboxOperation[] = mutations.map((mutation) => {
|
|
2733
2743
|
const table = this.#table(mutation.table);
|
|
2734
2744
|
if (mutation.op === 'delete') {
|
|
2735
|
-
return {
|
|
2745
|
+
return definedObject({
|
|
2736
2746
|
table: mutation.table,
|
|
2737
2747
|
rowId: mutation.rowId,
|
|
2738
2748
|
op: 'delete',
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
: {}),
|
|
2742
|
-
};
|
|
2749
|
+
baseVersion: mutation.baseVersion,
|
|
2750
|
+
});
|
|
2743
2751
|
}
|
|
2744
2752
|
const json: Record<string, JsonRowValue> = {};
|
|
2745
2753
|
if (mutation.op === 'patch') {
|
|
@@ -2752,7 +2760,7 @@ export class SyncClient {
|
|
|
2752
2760
|
const pkColumn = table.columns[table.primaryKeyIndex] as RowColumn;
|
|
2753
2761
|
const pkValue = requireRowId(normalized.get(pkColumn.name));
|
|
2754
2762
|
const local = this.#db.query(
|
|
2755
|
-
`SELECT * FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}
|
|
2763
|
+
`SELECT * FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}=?`,
|
|
2756
2764
|
[pkValue],
|
|
2757
2765
|
)[0];
|
|
2758
2766
|
if (!local)
|
|
@@ -2813,15 +2821,13 @@ export class SyncClient {
|
|
|
2813
2821
|
});
|
|
2814
2822
|
}
|
|
2815
2823
|
const pkValue = requireRowId(json[table.primaryKey]);
|
|
2816
|
-
return {
|
|
2824
|
+
return definedObject({
|
|
2817
2825
|
table: mutation.table,
|
|
2818
2826
|
rowId: pkValue,
|
|
2819
2827
|
op: 'upsert',
|
|
2820
|
-
|
|
2821
|
-
? { baseVersion: mutation.baseVersion }
|
|
2822
|
-
: {}),
|
|
2828
|
+
baseVersion: mutation.baseVersion,
|
|
2823
2829
|
values: json,
|
|
2824
|
-
};
|
|
2830
|
+
});
|
|
2825
2831
|
});
|
|
2826
2832
|
this.#applyBatch((batch) => {
|
|
2827
2833
|
this.#db.transaction(() => {
|
|
@@ -2847,10 +2853,7 @@ export class SyncClient {
|
|
|
2847
2853
|
continue;
|
|
2848
2854
|
}
|
|
2849
2855
|
this.#db.exec(
|
|
2850
|
-
|
|
2851
|
-
SELECT ?, ? WHERE EXISTS (
|
|
2852
|
-
SELECT 1 FROM _syncular_blobs WHERE blob_id = ?
|
|
2853
|
-
)`,
|
|
2856
|
+
'INSERT OR IGNORE INTO _syncular_blob_commit_refs(commit_id,blob_id)SELECT ?,? WHERE EXISTS(SELECT 1 FROM _syncular_blobs WHERE blob_id=?)',
|
|
2854
2857
|
[clientCommitId, blobId, blobId],
|
|
2855
2858
|
);
|
|
2856
2859
|
}
|
|
@@ -2956,7 +2959,8 @@ export class SyncClient {
|
|
|
2956
2959
|
// mirror row is touched and unconditionally.
|
|
2957
2960
|
this.#dropPreviousVersion();
|
|
2958
2961
|
|
|
2959
|
-
const
|
|
2962
|
+
const priorRejections = [...this.#rejections];
|
|
2963
|
+
const priorConflicts = [...this.#conflicts];
|
|
2960
2964
|
try {
|
|
2961
2965
|
return this.#applyBatch((batch) => {
|
|
2962
2966
|
const targetsByTable = this.#localPurgeTargetsByTable(purge);
|
|
@@ -2969,6 +2973,14 @@ export class SyncClient {
|
|
|
2969
2973
|
),
|
|
2970
2974
|
);
|
|
2971
2975
|
for (const commit of droppedRetained) {
|
|
2976
|
+
batch.conflicts();
|
|
2977
|
+
batch.rejections();
|
|
2978
|
+
this.#conflicts = this.#conflicts.filter(
|
|
2979
|
+
(record) => record.clientCommitId !== commit.clientCommitId,
|
|
2980
|
+
);
|
|
2981
|
+
this.#rejections = this.#rejections.filter(
|
|
2982
|
+
(record) => record.clientCommitId !== commit.clientCommitId,
|
|
2983
|
+
);
|
|
2972
2984
|
for (const operation of commit.operations)
|
|
2973
2985
|
batch.table(operation.table);
|
|
2974
2986
|
batch.outcomes();
|
|
@@ -3038,7 +3050,7 @@ export class SyncClient {
|
|
|
3038
3050
|
for (let offset = 0; offset < values.length; offset += 400) {
|
|
3039
3051
|
const chunk = values.slice(offset, offset + 400);
|
|
3040
3052
|
this.#db.exec(
|
|
3041
|
-
`DELETE FROM ${quoteIdent(tableName)} WHERE ${quoteIdent(table.primaryKey)} IN
|
|
3053
|
+
`DELETE FROM ${quoteIdent(tableName)} WHERE ${quoteIdent(table.primaryKey)} IN(${chunk.map(() => '?').join(', ')})`,
|
|
3042
3054
|
chunk,
|
|
3043
3055
|
);
|
|
3044
3056
|
}
|
|
@@ -3086,7 +3098,8 @@ export class SyncClient {
|
|
|
3086
3098
|
} catch (error) {
|
|
3087
3099
|
// SQLite rolls back through #applyBatch; mirror that rollback for the
|
|
3088
3100
|
// in-memory rejection cache before surfacing the storage failure.
|
|
3089
|
-
this.#rejections
|
|
3101
|
+
this.#rejections = priorRejections;
|
|
3102
|
+
this.#conflicts = priorConflicts;
|
|
3090
3103
|
throw error;
|
|
3091
3104
|
}
|
|
3092
3105
|
}
|
|
@@ -3192,7 +3205,7 @@ export class SyncClient {
|
|
|
3192
3205
|
params.push(...selector.values);
|
|
3193
3206
|
}
|
|
3194
3207
|
for (const row of this.#db.query(
|
|
3195
|
-
`SELECT CAST(${quoteIdent(target.table.primaryKey)} AS TEXT)
|
|
3208
|
+
`SELECT CAST(${quoteIdent(target.table.primaryKey)} AS TEXT)AS id FROM ${quoteIdent(target.table.name)} WHERE ${clauses.join(' AND ')}`,
|
|
3196
3209
|
params,
|
|
3197
3210
|
)) {
|
|
3198
3211
|
if (typeof row.id === 'string') ids.add(row.id);
|
|
@@ -3251,7 +3264,7 @@ export class SyncClient {
|
|
|
3251
3264
|
// Pin before the first encryption await: mutations can append while a
|
|
3252
3265
|
// round is encoding, and belong to the next request.
|
|
3253
3266
|
const bounds = this.#db.query(
|
|
3254
|
-
'SELECT COUNT(*)
|
|
3267
|
+
'SELECT COUNT(*)AS count,MAX(seq)AS last_seq FROM _syncular_outbox',
|
|
3255
3268
|
)[0]!;
|
|
3256
3269
|
const pendingCount = bounds.count as number;
|
|
3257
3270
|
const throughSeq = (bounds.last_seq as number | null) ?? 0;
|
|
@@ -3288,7 +3301,7 @@ export class SyncClient {
|
|
|
3288
3301
|
const selector = this.#encryption?.keyIdColumns?.[table.name];
|
|
3289
3302
|
if (selector === undefined) return undefined;
|
|
3290
3303
|
const row = this.#db.query(
|
|
3291
|
-
`SELECT ${quoteIdent(selector)} AS value FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}
|
|
3304
|
+
`SELECT ${quoteIdent(selector)} AS value FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}=?`,
|
|
3292
3305
|
[rowId],
|
|
3293
3306
|
)[0];
|
|
3294
3307
|
const value = row?.value;
|
|
@@ -3401,16 +3414,16 @@ export class SyncClient {
|
|
|
3401
3414
|
const error = classifySqliteFailure(caught).error;
|
|
3402
3415
|
const completedAtMs = this.#now();
|
|
3403
3416
|
const code = (error as { code?: unknown }).code;
|
|
3404
|
-
this.#progress.update(
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3417
|
+
this.#progress.update(
|
|
3418
|
+
definedObject({
|
|
3419
|
+
state: 'failed',
|
|
3420
|
+
errorCode:
|
|
3421
|
+
typeof code === 'string'
|
|
3422
|
+
? this.#diagnosticCode(code)
|
|
3423
|
+
: 'client.unknown_failure',
|
|
3424
|
+
retryDelayMs: this.#roundRetryDelayMs,
|
|
3425
|
+
}),
|
|
3426
|
+
);
|
|
3414
3427
|
this.#lastRound = {
|
|
3415
3428
|
status: 'failed',
|
|
3416
3429
|
startedAtMs,
|
|
@@ -3515,12 +3528,12 @@ export class SyncClient {
|
|
|
3515
3528
|
);
|
|
3516
3529
|
const limits = this.#config.limits;
|
|
3517
3530
|
const frames: RequestFrame[] = [
|
|
3518
|
-
{
|
|
3531
|
+
definedObject({
|
|
3519
3532
|
type: 'REQ_HEADER',
|
|
3520
3533
|
clientId: this.#clientId,
|
|
3521
3534
|
schemaVersion: this.#schema.version,
|
|
3522
|
-
|
|
3523
|
-
},
|
|
3535
|
+
logEpoch: logEpoch,
|
|
3536
|
+
}),
|
|
3524
3537
|
...pushFrames,
|
|
3525
3538
|
{
|
|
3526
3539
|
type: 'PULL_HEADER',
|
|
@@ -3530,17 +3543,16 @@ export class SyncClient {
|
|
|
3530
3543
|
accept: this.#acceptMask(),
|
|
3531
3544
|
},
|
|
3532
3545
|
...subs.map(
|
|
3533
|
-
(sub): RequestFrame =>
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
}),
|
|
3546
|
+
(sub): RequestFrame =>
|
|
3547
|
+
definedObject({
|
|
3548
|
+
type: 'SUBSCRIPTION',
|
|
3549
|
+
id: sub.id,
|
|
3550
|
+
table: sub.table,
|
|
3551
|
+
scopes: sub.scopes,
|
|
3552
|
+
params: sub.params,
|
|
3553
|
+
cursor: sub.cursor,
|
|
3554
|
+
bootstrapState: sub.bootstrapState,
|
|
3555
|
+
}),
|
|
3544
3556
|
),
|
|
3545
3557
|
];
|
|
3546
3558
|
const requestBytes = encodeMessage({
|
|
@@ -4074,12 +4086,10 @@ export class SyncClient {
|
|
|
4074
4086
|
// version this client sends. The §7.4.2 trigger-2 convergence runs
|
|
4075
4087
|
// when the APP updates (recreating the client with a new generated
|
|
4076
4088
|
// schema), which fires the boot-time §7.4.1 marker check instead.
|
|
4077
|
-
const schemaFloor: SchemaFloor = {
|
|
4089
|
+
const schemaFloor: SchemaFloor = definedObject({
|
|
4078
4090
|
requiredSchemaVersion: header.requiredSchemaVersion,
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
: {}),
|
|
4082
|
-
};
|
|
4091
|
+
latestSchemaVersion: header.latestSchemaVersion,
|
|
4092
|
+
});
|
|
4083
4093
|
this.#setSchemaFloor(schemaFloor);
|
|
4084
4094
|
return {
|
|
4085
4095
|
...summary,
|
|
@@ -4507,7 +4517,7 @@ export class SyncClient {
|
|
|
4507
4517
|
for (const result of frame.results) {
|
|
4508
4518
|
const operation = commit.operations[result.opIndex];
|
|
4509
4519
|
if (result.status === 'conflict') {
|
|
4510
|
-
const conflict: ConflictRecord = {
|
|
4520
|
+
const conflict: ConflictRecord = definedObject({
|
|
4511
4521
|
clientCommitId: frame.clientCommitId,
|
|
4512
4522
|
opIndex: result.opIndex,
|
|
4513
4523
|
table: operation?.table ?? '',
|
|
@@ -4520,23 +4530,23 @@ export class SyncClient {
|
|
|
4520
4530
|
operation?.table,
|
|
4521
4531
|
result.conflictColumns,
|
|
4522
4532
|
),
|
|
4523
|
-
|
|
4524
|
-
};
|
|
4533
|
+
operation: operation,
|
|
4534
|
+
});
|
|
4525
4535
|
this.#conflicts.push(conflict);
|
|
4526
4536
|
outcomeResults.push({ status: 'conflict', conflict });
|
|
4527
4537
|
batch.conflicts();
|
|
4528
4538
|
summary.conflicts.push(conflict);
|
|
4529
4539
|
} else if (result.status === 'error') {
|
|
4530
4540
|
const details = rejectionDetails?.get(result.opIndex);
|
|
4531
|
-
const rejection: RejectionRecord = {
|
|
4541
|
+
const rejection: RejectionRecord = definedObject({
|
|
4532
4542
|
clientCommitId: frame.clientCommitId,
|
|
4533
4543
|
opIndex: result.opIndex,
|
|
4534
4544
|
code: result.code,
|
|
4535
4545
|
message: result.message,
|
|
4536
4546
|
retryable: result.retryable,
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
};
|
|
4547
|
+
details: details,
|
|
4548
|
+
operation: operation,
|
|
4549
|
+
});
|
|
4540
4550
|
this.#rejections.push(rejection);
|
|
4541
4551
|
outcomeResults.push({ status: 'error', rejection });
|
|
4542
4552
|
batch.rejections();
|
|
@@ -4574,7 +4584,7 @@ export class SyncClient {
|
|
|
4574
4584
|
#outboxCommitExists(clientCommitId: string): boolean {
|
|
4575
4585
|
return (
|
|
4576
4586
|
this.#db.query(
|
|
4577
|
-
'SELECT 1 FROM _syncular_outbox WHERE client_commit_id
|
|
4587
|
+
'SELECT 1 FROM _syncular_outbox WHERE client_commit_id=? LIMIT 1',
|
|
4578
4588
|
[clientCommitId],
|
|
4579
4589
|
).length > 0
|
|
4580
4590
|
);
|
|
@@ -4627,7 +4637,14 @@ export class SyncClient {
|
|
|
4627
4637
|
(fn) =>
|
|
4628
4638
|
this.#applyBatch((batch) => {
|
|
4629
4639
|
this.#recordCommitChanges(batch, frame);
|
|
4630
|
-
|
|
4640
|
+
const result = fn();
|
|
4641
|
+
if (
|
|
4642
|
+
this.#retainFailedCommits &&
|
|
4643
|
+
this.#db.query('SELECT 1 FROM _syncular_failed_rows LIMIT 1')
|
|
4644
|
+
.length > 0
|
|
4645
|
+
)
|
|
4646
|
+
this.#replayOutbox();
|
|
4647
|
+
return result;
|
|
4631
4648
|
}),
|
|
4632
4649
|
);
|
|
4633
4650
|
summary.commitsApplied += 1;
|
|
@@ -4660,9 +4677,7 @@ export class SyncClient {
|
|
|
4660
4677
|
);
|
|
4661
4678
|
if (mappings.length === 0) return false;
|
|
4662
4679
|
const row = this.#db.query(
|
|
4663
|
-
`SELECT ${mappings.map(([, column]) => quoteIdent(column)).join(', ')}
|
|
4664
|
-
FROM ${quoteIdent(table.name)}
|
|
4665
|
-
WHERE ${quoteIdent(table.primaryKey)} = ?`,
|
|
4680
|
+
`SELECT ${mappings.map(([, column]) => quoteIdent(column)).join(', ')} FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}=?`,
|
|
4666
4681
|
[rowId],
|
|
4667
4682
|
)[0];
|
|
4668
4683
|
if (row === undefined) return false;
|
|
@@ -4694,8 +4709,7 @@ export class SyncClient {
|
|
|
4694
4709
|
}
|
|
4695
4710
|
return (
|
|
4696
4711
|
this.#db.query(
|
|
4697
|
-
`SELECT 1 FROM ${quoteIdent(table.name)}
|
|
4698
|
-
WHERE ${clauses.join(' AND ')} LIMIT 1`,
|
|
4712
|
+
`SELECT 1 FROM ${quoteIdent(table.name)} WHERE ${clauses.join(' AND ')} LIMIT 1`,
|
|
4699
4713
|
params,
|
|
4700
4714
|
).length > 0
|
|
4701
4715
|
);
|
|
@@ -4733,18 +4747,19 @@ export class SyncClient {
|
|
|
4733
4747
|
) {
|
|
4734
4748
|
const registered = getWindowUnitBySubId(this.#db, sub.id);
|
|
4735
4749
|
this.#applyBatch((batch) => {
|
|
4736
|
-
saveSubscription(
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
:
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4750
|
+
saveSubscription(
|
|
4751
|
+
this.#db,
|
|
4752
|
+
definedObject({
|
|
4753
|
+
id: sub.id,
|
|
4754
|
+
table: sub.table,
|
|
4755
|
+
scopes: sub.scopes,
|
|
4756
|
+
params: sub.params,
|
|
4757
|
+
cursor: sub.cursor,
|
|
4758
|
+
effectiveScopes: sub.effectiveScopes,
|
|
4759
|
+
status: 'failed',
|
|
4760
|
+
reasonCode: 'sync.scope_revoked',
|
|
4761
|
+
}),
|
|
4762
|
+
);
|
|
4748
4763
|
if (registered !== undefined) {
|
|
4749
4764
|
batch.window(registered.baseKey, sub.table, registered.unit);
|
|
4750
4765
|
}
|
|
@@ -4847,16 +4862,19 @@ export class SyncClient {
|
|
|
4847
4862
|
wasPending && nextCursor >= 0 && bootstrapState === undefined;
|
|
4848
4863
|
const registered = getWindowUnitBySubId(this.#db, sub.id);
|
|
4849
4864
|
this.#applyBatch((batch) => {
|
|
4850
|
-
saveSubscription(
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4865
|
+
saveSubscription(
|
|
4866
|
+
this.#db,
|
|
4867
|
+
definedObject({
|
|
4868
|
+
id: sub.id,
|
|
4869
|
+
table: sub.table,
|
|
4870
|
+
scopes: sub.scopes,
|
|
4871
|
+
params: sub.params,
|
|
4872
|
+
cursor: nextCursor,
|
|
4873
|
+
bootstrapState: bootstrapState,
|
|
4874
|
+
effectiveScopes: start.effectiveScopes,
|
|
4875
|
+
status: 'active',
|
|
4876
|
+
}),
|
|
4877
|
+
);
|
|
4860
4878
|
if (completed && registered !== undefined) {
|
|
4861
4879
|
// A zero-row bootstrap is a window-domain transition, not a fake
|
|
4862
4880
|
// row/table change (SPEC §4.8 / §7.5).
|
|
@@ -4871,18 +4889,19 @@ export class SyncClient {
|
|
|
4871
4889
|
// with cursor = -1 on the next pull. Staleness, not a purge.
|
|
4872
4890
|
const registered = getWindowUnitBySubId(this.#db, sub.id);
|
|
4873
4891
|
this.#applyBatch((batch) => {
|
|
4874
|
-
saveSubscription(
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
:
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4892
|
+
saveSubscription(
|
|
4893
|
+
this.#db,
|
|
4894
|
+
definedObject({
|
|
4895
|
+
id: sub.id,
|
|
4896
|
+
table: sub.table,
|
|
4897
|
+
scopes: sub.scopes,
|
|
4898
|
+
params: sub.params,
|
|
4899
|
+
cursor: -1,
|
|
4900
|
+
effectiveScopes: sub.effectiveScopes,
|
|
4901
|
+
status: 'active',
|
|
4902
|
+
reasonCode: start.reasonCode,
|
|
4903
|
+
}),
|
|
4904
|
+
);
|
|
4886
4905
|
if (registered !== undefined) {
|
|
4887
4906
|
batch.window(registered.baseKey, sub.table, registered.unit);
|
|
4888
4907
|
}
|
|
@@ -4915,6 +4934,14 @@ export class SyncClient {
|
|
|
4915
4934
|
),
|
|
4916
4935
|
);
|
|
4917
4936
|
for (const commit of droppedRetained) {
|
|
4937
|
+
batch.conflicts();
|
|
4938
|
+
batch.rejections();
|
|
4939
|
+
this.#conflicts = this.#conflicts.filter(
|
|
4940
|
+
(record) => record.clientCommitId !== commit.clientCommitId,
|
|
4941
|
+
);
|
|
4942
|
+
this.#rejections = this.#rejections.filter(
|
|
4943
|
+
(record) => record.clientCommitId !== commit.clientCommitId,
|
|
4944
|
+
);
|
|
4918
4945
|
for (const operation of commit.operations)
|
|
4919
4946
|
batch.table(operation.table);
|
|
4920
4947
|
batch.outcomes();
|
|
@@ -4976,18 +5003,19 @@ export class SyncClient {
|
|
|
4976
5003
|
}
|
|
4977
5004
|
}
|
|
4978
5005
|
}
|
|
4979
|
-
saveSubscription(
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
:
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
5006
|
+
saveSubscription(
|
|
5007
|
+
this.#db,
|
|
5008
|
+
definedObject({
|
|
5009
|
+
id: sub.id,
|
|
5010
|
+
table: sub.table,
|
|
5011
|
+
scopes: sub.scopes,
|
|
5012
|
+
params: sub.params,
|
|
5013
|
+
cursor: nextCursor,
|
|
5014
|
+
effectiveScopes: lastEffective,
|
|
5015
|
+
status: failed ? 'failed' : 'revoked',
|
|
5016
|
+
reasonCode: start.reasonCode,
|
|
5017
|
+
}),
|
|
5018
|
+
);
|
|
4991
5019
|
if (registered !== undefined) {
|
|
4992
5020
|
batch.window(registered.baseKey, sub.table, registered.unit);
|
|
4993
5021
|
}
|
|
@@ -5010,7 +5038,7 @@ export class SyncClient {
|
|
|
5010
5038
|
quoteIdent(SYNC_VERSION_COLUMN),
|
|
5011
5039
|
];
|
|
5012
5040
|
const row = this.#db.query(
|
|
5013
|
-
`SELECT ${columns.join(', ')} FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}
|
|
5041
|
+
`SELECT ${columns.join(', ')} FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}=?`,
|
|
5014
5042
|
[operation.rowId],
|
|
5015
5043
|
)[0];
|
|
5016
5044
|
if (row === undefined) return { opIndex, existed: false };
|
|
@@ -5074,7 +5102,7 @@ export class SyncClient {
|
|
|
5074
5102
|
const table = this.#schema.tables.get(operation.table);
|
|
5075
5103
|
if (table === undefined) continue;
|
|
5076
5104
|
const row = this.#db.query(
|
|
5077
|
-
`SELECT ${quoteIdent(SYNC_VERSION_COLUMN)} AS v FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}
|
|
5105
|
+
`SELECT ${quoteIdent(SYNC_VERSION_COLUMN)} AS v FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}=?`,
|
|
5078
5106
|
[operation.rowId],
|
|
5079
5107
|
)[0];
|
|
5080
5108
|
if (row !== undefined && row.v === OPTIMISTIC_VERSION) {
|
|
@@ -5130,6 +5158,7 @@ export class SyncClient {
|
|
|
5130
5158
|
#applyOperationsLocally(
|
|
5131
5159
|
operations: readonly OutboxOperation[],
|
|
5132
5160
|
batch?: ChangeAccumulator,
|
|
5161
|
+
replay = false,
|
|
5133
5162
|
): void {
|
|
5134
5163
|
for (const op of operations) {
|
|
5135
5164
|
const table = this.#table(op.table);
|
|
@@ -5149,7 +5178,7 @@ export class SyncClient {
|
|
|
5149
5178
|
const opValues = op.values ?? {};
|
|
5150
5179
|
const full = table.columns.every((column) => column.name in opValues);
|
|
5151
5180
|
const local = this.#db.query(
|
|
5152
|
-
`SELECT * FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}
|
|
5181
|
+
`SELECT * FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(table.primaryKey)}=?`,
|
|
5153
5182
|
[op.rowId],
|
|
5154
5183
|
)[0];
|
|
5155
5184
|
if (local === undefined && !full) {
|
|
@@ -5180,6 +5209,21 @@ export class SyncClient {
|
|
|
5180
5209
|
local === undefined
|
|
5181
5210
|
? OPTIMISTIC_VERSION
|
|
5182
5211
|
: (local[SYNC_VERSION_COLUMN] as number);
|
|
5212
|
+
if (
|
|
5213
|
+
replay &&
|
|
5214
|
+
table.indexes.some((index) => index.unique) &&
|
|
5215
|
+
uniqueConflicts(
|
|
5216
|
+
this.#db,
|
|
5217
|
+
table,
|
|
5218
|
+
Object.fromEntries(
|
|
5219
|
+
table.columns.map((column, index) => [
|
|
5220
|
+
column.name,
|
|
5221
|
+
values[index] ?? null,
|
|
5222
|
+
]),
|
|
5223
|
+
),
|
|
5224
|
+
).length
|
|
5225
|
+
)
|
|
5226
|
+
continue;
|
|
5183
5227
|
upsertLocalRow(this.#db, table, values, version, false);
|
|
5184
5228
|
}
|
|
5185
5229
|
}
|
|
@@ -5196,7 +5240,7 @@ export class SyncClient {
|
|
|
5196
5240
|
restoreFailedBases(this.#db, this.#schema);
|
|
5197
5241
|
restoreFailedBases(this.#db, this.#schema, true);
|
|
5198
5242
|
for (const commit of pending)
|
|
5199
|
-
this.#applyOperationsLocally(commit.operations, batch);
|
|
5243
|
+
this.#applyOperationsLocally(commit.operations, batch, true);
|
|
5200
5244
|
});
|
|
5201
5245
|
});
|
|
5202
5246
|
}
|
|
@@ -5236,6 +5280,21 @@ export class SyncClient {
|
|
|
5236
5280
|
}
|
|
5237
5281
|
}
|
|
5238
5282
|
|
|
5283
|
+
type DefinedObject<T> = {
|
|
5284
|
+
[K in keyof T as undefined extends T[K] ? never : K]: T[K];
|
|
5285
|
+
} & {
|
|
5286
|
+
[K in keyof T as undefined extends T[K] ? K : never]?: Exclude<
|
|
5287
|
+
T[K],
|
|
5288
|
+
undefined
|
|
5289
|
+
>;
|
|
5290
|
+
};
|
|
5291
|
+
function definedObject<const T extends object>(record: T): DefinedObject<T> {
|
|
5292
|
+
for (const key of Object.keys(record) as (keyof T)[]) {
|
|
5293
|
+
if (record[key] === undefined) delete record[key];
|
|
5294
|
+
}
|
|
5295
|
+
return record as DefinedObject<T>;
|
|
5296
|
+
}
|
|
5297
|
+
|
|
5239
5298
|
function requireRowId(value: unknown): string {
|
|
5240
5299
|
if (typeof value !== 'string' || !value.length)
|
|
5241
5300
|
throw invalidRequest('a mutation requires a non-empty string primary key');
|