@syncular/client 0.0.6-213 → 0.0.6-219
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/dist/engine/SyncEngine.d.ts +5 -1
- package/dist/engine/SyncEngine.d.ts.map +1 -1
- package/dist/engine/SyncEngine.js +86 -0
- package/dist/engine/SyncEngine.js.map +1 -1
- package/dist/engine/types.d.ts +33 -1
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/pull-engine.d.ts +12 -1
- package/dist/pull-engine.d.ts.map +1 -1
- package/dist/pull-engine.js +256 -212
- package/dist/pull-engine.js.map +1 -1
- package/package.json +3 -3
- package/src/engine/SyncEngine.ts +114 -0
- package/src/engine/types.ts +39 -0
- package/src/pull-engine.test.ts +247 -0
- package/src/pull-engine.ts +350 -245
package/src/pull-engine.ts
CHANGED
|
@@ -2,16 +2,22 @@
|
|
|
2
2
|
* @syncular/client - Sync pull engine
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
import {
|
|
6
|
+
bytesToReadableStream,
|
|
7
|
+
decodeSnapshotRows,
|
|
8
|
+
gunzipBytes,
|
|
9
|
+
readAllBytesFromStream as readAllBytesFromCoreStream,
|
|
10
|
+
type ScopeValues,
|
|
11
|
+
type SyncBootstrapApplyMode,
|
|
12
|
+
type SyncBootstrapState,
|
|
13
|
+
type SyncChange,
|
|
14
|
+
type SyncPullRequest,
|
|
15
|
+
type SyncPullResponse,
|
|
16
|
+
type SyncPullSubscriptionResponse,
|
|
17
|
+
type SyncSnapshot,
|
|
18
|
+
type SyncSubscriptionRequest,
|
|
19
|
+
type SyncTransport,
|
|
20
|
+
type SyncTransportCapabilities,
|
|
15
21
|
} from '@syncular/core';
|
|
16
22
|
import { type Kysely, sql, type Transaction } from 'kysely';
|
|
17
23
|
import {
|
|
@@ -55,15 +61,6 @@ function isGzipBytes(bytes: Uint8Array): boolean {
|
|
|
55
61
|
return bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
|
|
56
62
|
}
|
|
57
63
|
|
|
58
|
-
function bytesToReadableStream(bytes: Uint8Array): ReadableStream<Uint8Array> {
|
|
59
|
-
return new ReadableStream<Uint8Array>({
|
|
60
|
-
start(controller) {
|
|
61
|
-
controller.enqueue(bytes);
|
|
62
|
-
controller.close();
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
64
|
function concatBytes(chunks: readonly Uint8Array[]): Uint8Array {
|
|
68
65
|
if (chunks.length === 1) {
|
|
69
66
|
return chunks[0] ?? new Uint8Array();
|
|
@@ -141,9 +138,8 @@ async function maybeGunzipStream(
|
|
|
141
138
|
return replayStream.pipeThrough(new DecompressionStream('gzip'));
|
|
142
139
|
}
|
|
143
140
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
);
|
|
141
|
+
const compressedBytes = await readAllBytesFromCoreStream(replayStream);
|
|
142
|
+
return bytesToReadableStream(await gunzipBytes(compressedBytes));
|
|
147
143
|
}
|
|
148
144
|
|
|
149
145
|
async function* decodeSnapshotRowStreamBatches(
|
|
@@ -338,6 +334,25 @@ async function materializeSnapshotChunkRows(
|
|
|
338
334
|
expectedHash: string | undefined,
|
|
339
335
|
sha256Override?: (bytes: Uint8Array) => Promise<string>
|
|
340
336
|
): Promise<unknown[]> {
|
|
337
|
+
if (
|
|
338
|
+
transport.capabilities?.snapshotChunkReadMode === 'bytes' &&
|
|
339
|
+
transport.fetchSnapshotChunk
|
|
340
|
+
) {
|
|
341
|
+
let bytes = await transport.fetchSnapshotChunk(request);
|
|
342
|
+
if (isGzipBytes(bytes)) {
|
|
343
|
+
bytes = await gunzipBytes(bytes);
|
|
344
|
+
}
|
|
345
|
+
if (expectedHash) {
|
|
346
|
+
const actualHash = await computeSha256Hex(bytes, sha256Override);
|
|
347
|
+
if (actualHash !== expectedHash) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
`Snapshot chunk integrity check failed: expected sha256 ${expectedHash}, got ${actualHash}`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return decodeSnapshotRows(bytes);
|
|
354
|
+
}
|
|
355
|
+
|
|
341
356
|
const rawStream = await fetchSnapshotChunkStream(transport, request);
|
|
342
357
|
const decodedStream = await maybeGunzipStream(rawStream);
|
|
343
358
|
let streamForDecode = decodedStream;
|
|
@@ -689,6 +704,17 @@ export interface SyncPullOnceOptions {
|
|
|
689
704
|
maxSnapshotPages?: number;
|
|
690
705
|
dedupeRows?: boolean;
|
|
691
706
|
stateId?: string;
|
|
707
|
+
/**
|
|
708
|
+
* Controls how bootstrap snapshot results are committed to the local DB.
|
|
709
|
+
* - `single-transaction`: all subscriptions in one DB transaction
|
|
710
|
+
* - `per-subscription`: each subscription in its own DB transaction
|
|
711
|
+
* - `auto`: choose a sensible runtime-specific default
|
|
712
|
+
*
|
|
713
|
+
* `per-subscription` makes early subscriptions visible sooner and prevents a
|
|
714
|
+
* later large bootstrap table from hiding already-applied tables behind one
|
|
715
|
+
* long-running transaction.
|
|
716
|
+
*/
|
|
717
|
+
bootstrapApplyMode?: 'auto' | SyncBootstrapApplyMode;
|
|
692
718
|
/**
|
|
693
719
|
* Custom SHA-256 hash function for snapshot chunk integrity verification.
|
|
694
720
|
* Provide this on platforms where `crypto.subtle` is unavailable (e.g. React Native).
|
|
@@ -880,8 +906,13 @@ export async function applyPullResponse<DB extends SyncClientDb>(
|
|
|
880
906
|
clientId: options.clientId,
|
|
881
907
|
};
|
|
882
908
|
const plugins = options.plugins ?? [];
|
|
883
|
-
const requiresMaterializedSnapshots =
|
|
884
|
-
(plugin) => !!plugin.afterPull
|
|
909
|
+
const requiresMaterializedSnapshots =
|
|
910
|
+
plugins.some((plugin) => !!plugin.afterPull) ||
|
|
911
|
+
transport.capabilities?.preferMaterializedSnapshots === true;
|
|
912
|
+
const bootstrapApplyMode = resolveBootstrapApplyMode(
|
|
913
|
+
options,
|
|
914
|
+
rawResponse,
|
|
915
|
+
transport.capabilities
|
|
885
916
|
);
|
|
886
917
|
|
|
887
918
|
let responseToApply = requiresMaterializedSnapshots
|
|
@@ -895,248 +926,322 @@ export async function applyPullResponse<DB extends SyncClientDb>(
|
|
|
895
926
|
});
|
|
896
927
|
}
|
|
897
928
|
|
|
929
|
+
const subsById = new Map<string, (typeof options.subscriptions)[number]>();
|
|
930
|
+
for (const s of options.subscriptions ?? []) subsById.set(s.id, s);
|
|
931
|
+
|
|
898
932
|
await db.transaction().execute(async (trx) => {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
933
|
+
await removeUndesiredSubscriptions(
|
|
934
|
+
trx,
|
|
935
|
+
handlers,
|
|
936
|
+
existing,
|
|
937
|
+
options.subscriptions ?? [],
|
|
938
|
+
stateId
|
|
939
|
+
);
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
if (bootstrapApplyMode === 'per-subscription') {
|
|
943
|
+
for (const sub of responseToApply.subscriptions) {
|
|
944
|
+
await db.transaction().execute(async (trx) => {
|
|
945
|
+
await applySubscriptionResponse({
|
|
946
|
+
trx,
|
|
947
|
+
handlers,
|
|
948
|
+
transport,
|
|
949
|
+
options,
|
|
950
|
+
stateId,
|
|
951
|
+
existingById,
|
|
952
|
+
subsById,
|
|
953
|
+
sub,
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
} else {
|
|
958
|
+
await db.transaction().execute(async (trx) => {
|
|
959
|
+
for (const sub of responseToApply.subscriptions) {
|
|
960
|
+
await applySubscriptionResponse({
|
|
961
|
+
trx,
|
|
962
|
+
handlers,
|
|
963
|
+
transport,
|
|
964
|
+
options,
|
|
965
|
+
stateId,
|
|
966
|
+
existingById,
|
|
967
|
+
subsById,
|
|
968
|
+
sub,
|
|
969
|
+
});
|
|
920
970
|
}
|
|
971
|
+
});
|
|
972
|
+
}
|
|
921
973
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
974
|
+
return responseToApply;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function resolveBootstrapApplyMode(
|
|
978
|
+
options: SyncPullOnceOptions,
|
|
979
|
+
response: SyncPullResponse,
|
|
980
|
+
capabilities?: SyncTransportCapabilities
|
|
981
|
+
): SyncBootstrapApplyMode {
|
|
982
|
+
const mode = options.bootstrapApplyMode ?? 'auto';
|
|
983
|
+
if (mode === 'single-transaction' || mode === 'per-subscription') {
|
|
984
|
+
return mode;
|
|
985
|
+
}
|
|
986
|
+
if (!response.subscriptions.some((sub) => sub.bootstrap)) {
|
|
987
|
+
return 'single-transaction';
|
|
988
|
+
}
|
|
989
|
+
if (capabilities?.preferredBootstrapApplyMode) {
|
|
990
|
+
return capabilities.preferredBootstrapApplyMode;
|
|
991
|
+
}
|
|
992
|
+
if (
|
|
993
|
+
capabilities?.snapshotChunkReadMode === 'bytes' ||
|
|
994
|
+
capabilities?.gzipDecompressionMode === 'buffered'
|
|
995
|
+
) {
|
|
996
|
+
return 'per-subscription';
|
|
997
|
+
}
|
|
998
|
+
return 'single-transaction';
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
async function removeUndesiredSubscriptions<DB extends SyncClientDb>(
|
|
1002
|
+
trx: Transaction<DB>,
|
|
1003
|
+
handlers: ClientHandlerCollection<DB>,
|
|
1004
|
+
existing: SyncSubscriptionStateTable[],
|
|
1005
|
+
desiredSubscriptions: Array<Omit<SyncSubscriptionRequest, 'cursor'>>,
|
|
1006
|
+
stateId: string
|
|
1007
|
+
): Promise<void> {
|
|
1008
|
+
const desiredIds = new Set(
|
|
1009
|
+
desiredSubscriptions.map((subscription) => subscription.id)
|
|
1010
|
+
);
|
|
1011
|
+
|
|
1012
|
+
for (const row of existing) {
|
|
1013
|
+
if (desiredIds.has(row.subscription_id)) continue;
|
|
1014
|
+
|
|
1015
|
+
if (row.table) {
|
|
1016
|
+
try {
|
|
1017
|
+
const scopes = row.scopes_json
|
|
1018
|
+
? typeof row.scopes_json === 'string'
|
|
1019
|
+
? JSON.parse(row.scopes_json)
|
|
1020
|
+
: row.scopes_json
|
|
1021
|
+
: {};
|
|
1022
|
+
await getClientHandlerOrThrow(handlers, row.table).clearAll({
|
|
1023
|
+
trx,
|
|
1024
|
+
scopes,
|
|
1025
|
+
});
|
|
1026
|
+
} catch {
|
|
1027
|
+
// ignore missing table handler
|
|
1028
|
+
}
|
|
927
1029
|
}
|
|
928
1030
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
const latestStateRows = await sql<{
|
|
932
|
-
subscription_id: string;
|
|
933
|
-
cursor: number | string | null;
|
|
934
|
-
}>`
|
|
935
|
-
select
|
|
936
|
-
${sql.ref('subscription_id')} as subscription_id,
|
|
937
|
-
${sql.ref('cursor')} as cursor
|
|
938
|
-
from ${sql.table('sync_subscription_state')}
|
|
1031
|
+
await sql`
|
|
1032
|
+
delete from ${sql.table('sync_subscription_state')}
|
|
939
1033
|
where ${sql.ref('state_id')} = ${sql.val(stateId)}
|
|
1034
|
+
and ${sql.ref('subscription_id')} = ${sql.val(row.subscription_id)}
|
|
940
1035
|
`.execute(trx);
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
const raw = row.cursor;
|
|
944
|
-
const cursor =
|
|
945
|
-
typeof raw === 'number'
|
|
946
|
-
? raw
|
|
947
|
-
: raw === null || raw === undefined
|
|
948
|
-
? null
|
|
949
|
-
: Number(raw);
|
|
950
|
-
latestCursorBySubscriptionId.set(row.subscription_id, cursor);
|
|
951
|
-
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
952
1038
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
Number.isFinite(prevCursor) &&
|
|
973
|
-
latestCursor !== null &&
|
|
974
|
-
Number.isFinite(latestCursor)
|
|
975
|
-
? Math.max(prevCursor, latestCursor)
|
|
976
|
-
: prevCursor !== null && Number.isFinite(prevCursor)
|
|
977
|
-
? prevCursor
|
|
978
|
-
: latestCursor !== null && Number.isFinite(latestCursor)
|
|
979
|
-
? latestCursor
|
|
980
|
-
: null;
|
|
981
|
-
const staleIncrementalResponse =
|
|
982
|
-
!sub.bootstrap &&
|
|
983
|
-
effectiveCursor !== null &&
|
|
984
|
-
sub.nextCursor < effectiveCursor;
|
|
985
|
-
|
|
986
|
-
// Guard against out-of-order duplicate pull responses from older requests.
|
|
987
|
-
if (staleIncrementalResponse) {
|
|
988
|
-
continue;
|
|
989
|
-
}
|
|
1039
|
+
async function readLatestSubscriptionCursor<DB extends SyncClientDb>(
|
|
1040
|
+
trx: Transaction<DB>,
|
|
1041
|
+
stateId: string,
|
|
1042
|
+
subscriptionId: string
|
|
1043
|
+
): Promise<number | null> {
|
|
1044
|
+
const result = await sql<{ cursor: number | string | null }>`
|
|
1045
|
+
select ${sql.ref('cursor')} as cursor
|
|
1046
|
+
from ${sql.table('sync_subscription_state')}
|
|
1047
|
+
where ${sql.ref('state_id')} = ${sql.val(stateId)}
|
|
1048
|
+
and ${sql.ref('subscription_id')} = ${sql.val(subscriptionId)}
|
|
1049
|
+
limit 1
|
|
1050
|
+
`.execute(trx);
|
|
1051
|
+
const raw = result.rows[0]?.cursor;
|
|
1052
|
+
return typeof raw === 'number'
|
|
1053
|
+
? raw
|
|
1054
|
+
: raw === null || raw === undefined
|
|
1055
|
+
? null
|
|
1056
|
+
: Number(raw);
|
|
1057
|
+
}
|
|
990
1058
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1059
|
+
async function applySubscriptionResponse<DB extends SyncClientDb>(args: {
|
|
1060
|
+
trx: Transaction<DB>;
|
|
1061
|
+
handlers: ClientHandlerCollection<DB>;
|
|
1062
|
+
transport: SyncTransport;
|
|
1063
|
+
options: SyncPullOnceOptions;
|
|
1064
|
+
stateId: string;
|
|
1065
|
+
existingById: Map<string, SyncSubscriptionStateTable>;
|
|
1066
|
+
subsById: Map<string, Omit<SyncSubscriptionRequest, 'cursor'> | undefined>;
|
|
1067
|
+
sub: SyncPullSubscriptionResponse;
|
|
1068
|
+
}): Promise<void> {
|
|
1069
|
+
const {
|
|
1070
|
+
trx,
|
|
1071
|
+
handlers,
|
|
1072
|
+
transport,
|
|
1073
|
+
options,
|
|
1074
|
+
stateId,
|
|
1075
|
+
existingById,
|
|
1076
|
+
subsById,
|
|
1077
|
+
sub,
|
|
1078
|
+
} = args;
|
|
1079
|
+
const def = subsById.get(sub.id);
|
|
1080
|
+
const prev = existingById.get(sub.id);
|
|
1081
|
+
const prevCursorRaw = prev?.cursor;
|
|
1082
|
+
const prevCursor =
|
|
1083
|
+
typeof prevCursorRaw === 'number'
|
|
1084
|
+
? prevCursorRaw
|
|
1085
|
+
: prevCursorRaw === null || prevCursorRaw === undefined
|
|
1086
|
+
? null
|
|
1087
|
+
: Number(prevCursorRaw);
|
|
1088
|
+
const latestCursor = await readLatestSubscriptionCursor(trx, stateId, sub.id);
|
|
1089
|
+
const effectiveCursor =
|
|
1090
|
+
prevCursor !== null &&
|
|
1091
|
+
Number.isFinite(prevCursor) &&
|
|
1092
|
+
latestCursor !== null &&
|
|
1093
|
+
Number.isFinite(latestCursor)
|
|
1094
|
+
? Math.max(prevCursor, latestCursor)
|
|
1095
|
+
: prevCursor !== null && Number.isFinite(prevCursor)
|
|
1096
|
+
? prevCursor
|
|
1097
|
+
: latestCursor !== null && Number.isFinite(latestCursor)
|
|
1098
|
+
? latestCursor
|
|
1099
|
+
: null;
|
|
1100
|
+
const staleIncrementalResponse =
|
|
1101
|
+
!sub.bootstrap &&
|
|
1102
|
+
effectiveCursor !== null &&
|
|
1103
|
+
sub.nextCursor < effectiveCursor;
|
|
1104
|
+
|
|
1105
|
+
if (staleIncrementalResponse) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1004
1108
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1109
|
+
if (sub.status === 'revoked') {
|
|
1110
|
+
if (prev?.table) {
|
|
1111
|
+
try {
|
|
1112
|
+
const scopes = parseScopeValuesJson(prev.scopes_json);
|
|
1113
|
+
await getClientHandlerOrThrow(handlers, prev.table).clearAll({
|
|
1114
|
+
trx,
|
|
1115
|
+
scopes,
|
|
1116
|
+
});
|
|
1117
|
+
} catch {
|
|
1118
|
+
// ignore missing handler
|
|
1012
1119
|
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
await sql`
|
|
1123
|
+
delete from ${sql.table('sync_subscription_state')}
|
|
1124
|
+
where ${sql.ref('state_id')} = ${sql.val(stateId)}
|
|
1125
|
+
and ${sql.ref('subscription_id')} = ${sql.val(sub.id)}
|
|
1126
|
+
`.execute(trx);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1013
1129
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1130
|
+
const nextScopes = sub.scopes ?? def?.scopes ?? {};
|
|
1131
|
+
const previousScopes = parseScopeValuesJson(prev?.scopes_json);
|
|
1132
|
+
const scopesChanged = !scopeValuesEqual(previousScopes, nextScopes);
|
|
1017
1133
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
}
|
|
1030
|
-
} catch {
|
|
1031
|
-
// ignore missing handler
|
|
1032
|
-
}
|
|
1134
|
+
if (sub.bootstrap && prev?.table && scopesChanged) {
|
|
1135
|
+
try {
|
|
1136
|
+
const clearScopes = resolveBootstrapClearScopes(
|
|
1137
|
+
previousScopes,
|
|
1138
|
+
nextScopes
|
|
1139
|
+
);
|
|
1140
|
+
if (clearScopes !== 'none') {
|
|
1141
|
+
await getClientHandlerOrThrow(handlers, prev.table).clearAll({
|
|
1142
|
+
trx,
|
|
1143
|
+
scopes: clearScopes ?? previousScopes,
|
|
1144
|
+
});
|
|
1033
1145
|
}
|
|
1146
|
+
} catch {
|
|
1147
|
+
// ignore missing handler
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1034
1150
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
if (hasChunkRefs) {
|
|
1052
|
-
await applyChunkedSnapshot(
|
|
1053
|
-
transport,
|
|
1054
|
-
handler,
|
|
1055
|
-
trx,
|
|
1056
|
-
snapshot,
|
|
1057
|
-
sub.scopes,
|
|
1058
|
-
options.sha256
|
|
1059
|
-
);
|
|
1060
|
-
} else {
|
|
1061
|
-
await handler.applySnapshot({ trx }, snapshot);
|
|
1062
|
-
}
|
|
1151
|
+
if (sub.bootstrap) {
|
|
1152
|
+
for (const snapshot of sub.snapshots ?? []) {
|
|
1153
|
+
const handler = getClientHandlerOrThrow(handlers, snapshot.table);
|
|
1154
|
+
const hasChunkRefs =
|
|
1155
|
+
Array.isArray(snapshot.chunks) && snapshot.chunks.length > 0;
|
|
1156
|
+
|
|
1157
|
+
if (snapshot.isFirstPage && handler.onSnapshotStart) {
|
|
1158
|
+
await handler.onSnapshotStart({
|
|
1159
|
+
trx,
|
|
1160
|
+
table: snapshot.table,
|
|
1161
|
+
scopes: sub.scopes,
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1063
1164
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1165
|
+
if (hasChunkRefs) {
|
|
1166
|
+
await applyChunkedSnapshot(
|
|
1167
|
+
transport,
|
|
1168
|
+
handler,
|
|
1169
|
+
trx,
|
|
1170
|
+
snapshot,
|
|
1171
|
+
sub.scopes,
|
|
1172
|
+
options.sha256
|
|
1173
|
+
);
|
|
1073
1174
|
} else {
|
|
1074
|
-
|
|
1075
|
-
for (const commit of sub.commits) {
|
|
1076
|
-
await applyIncrementalCommitChanges(handlers, trx, {
|
|
1077
|
-
changes: commit.changes,
|
|
1078
|
-
commitSeq: commit.commitSeq ?? null,
|
|
1079
|
-
actorId: commit.actorId ?? null,
|
|
1080
|
-
createdAt: commit.createdAt ?? null,
|
|
1081
|
-
});
|
|
1082
|
-
}
|
|
1175
|
+
await handler.applySnapshot({ trx }, snapshot);
|
|
1083
1176
|
}
|
|
1084
1177
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
? serializeJsonCached(sub.bootstrapState)
|
|
1093
|
-
: null
|
|
1094
|
-
: null;
|
|
1095
|
-
|
|
1096
|
-
const table = def?.table ?? 'unknown';
|
|
1097
|
-
await sql`
|
|
1098
|
-
insert into ${sql.table('sync_subscription_state')} (
|
|
1099
|
-
${sql.join([
|
|
1100
|
-
sql.ref('state_id'),
|
|
1101
|
-
sql.ref('subscription_id'),
|
|
1102
|
-
sql.ref('table'),
|
|
1103
|
-
sql.ref('scopes_json'),
|
|
1104
|
-
sql.ref('params_json'),
|
|
1105
|
-
sql.ref('cursor'),
|
|
1106
|
-
sql.ref('bootstrap_state_json'),
|
|
1107
|
-
sql.ref('status'),
|
|
1108
|
-
sql.ref('created_at'),
|
|
1109
|
-
sql.ref('updated_at'),
|
|
1110
|
-
])}
|
|
1111
|
-
) values (
|
|
1112
|
-
${sql.join([
|
|
1113
|
-
sql.val(stateId),
|
|
1114
|
-
sql.val(sub.id),
|
|
1115
|
-
sql.val(table),
|
|
1116
|
-
sql.val(scopesJson),
|
|
1117
|
-
sql.val(paramsJson),
|
|
1118
|
-
sql.val(sub.nextCursor),
|
|
1119
|
-
sql.val(bootstrapStateJson),
|
|
1120
|
-
sql.val('active'),
|
|
1121
|
-
sql.val(now),
|
|
1122
|
-
sql.val(now),
|
|
1123
|
-
])}
|
|
1124
|
-
)
|
|
1125
|
-
on conflict (${sql.join([sql.ref('state_id'), sql.ref('subscription_id')])})
|
|
1126
|
-
do update set
|
|
1127
|
-
${sql.ref('table')} = ${sql.val(table)},
|
|
1128
|
-
${sql.ref('scopes_json')} = ${sql.val(scopesJson)},
|
|
1129
|
-
${sql.ref('params_json')} = ${sql.val(paramsJson)},
|
|
1130
|
-
${sql.ref('cursor')} = ${sql.val(sub.nextCursor)},
|
|
1131
|
-
${sql.ref('bootstrap_state_json')} = ${sql.val(bootstrapStateJson)},
|
|
1132
|
-
${sql.ref('status')} = ${sql.val('active')},
|
|
1133
|
-
${sql.ref('updated_at')} = ${sql.val(now)}
|
|
1134
|
-
`.execute(trx);
|
|
1135
|
-
latestCursorBySubscriptionId.set(sub.id, sub.nextCursor);
|
|
1178
|
+
if (snapshot.isLastPage && handler.onSnapshotEnd) {
|
|
1179
|
+
await handler.onSnapshotEnd({
|
|
1180
|
+
trx,
|
|
1181
|
+
table: snapshot.table,
|
|
1182
|
+
scopes: sub.scopes,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1136
1185
|
}
|
|
1137
|
-
}
|
|
1186
|
+
} else {
|
|
1187
|
+
for (const commit of sub.commits) {
|
|
1188
|
+
await applyIncrementalCommitChanges(handlers, trx, {
|
|
1189
|
+
changes: commit.changes,
|
|
1190
|
+
commitSeq: commit.commitSeq ?? null,
|
|
1191
|
+
actorId: commit.actorId ?? null,
|
|
1192
|
+
createdAt: commit.createdAt ?? null,
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1138
1196
|
|
|
1139
|
-
|
|
1197
|
+
const now = Date.now();
|
|
1198
|
+
const paramsJson = serializeJsonCached(def?.params ?? {});
|
|
1199
|
+
const scopesJson = serializeJsonCached(nextScopes);
|
|
1200
|
+
const bootstrapStateJson = sub.bootstrap
|
|
1201
|
+
? sub.bootstrapState
|
|
1202
|
+
? serializeJsonCached(sub.bootstrapState)
|
|
1203
|
+
: null
|
|
1204
|
+
: null;
|
|
1205
|
+
const table = def?.table ?? 'unknown';
|
|
1206
|
+
|
|
1207
|
+
await sql`
|
|
1208
|
+
insert into ${sql.table('sync_subscription_state')} (
|
|
1209
|
+
${sql.join([
|
|
1210
|
+
sql.ref('state_id'),
|
|
1211
|
+
sql.ref('subscription_id'),
|
|
1212
|
+
sql.ref('table'),
|
|
1213
|
+
sql.ref('scopes_json'),
|
|
1214
|
+
sql.ref('params_json'),
|
|
1215
|
+
sql.ref('cursor'),
|
|
1216
|
+
sql.ref('bootstrap_state_json'),
|
|
1217
|
+
sql.ref('status'),
|
|
1218
|
+
sql.ref('created_at'),
|
|
1219
|
+
sql.ref('updated_at'),
|
|
1220
|
+
])}
|
|
1221
|
+
) values (
|
|
1222
|
+
${sql.join([
|
|
1223
|
+
sql.val(stateId),
|
|
1224
|
+
sql.val(sub.id),
|
|
1225
|
+
sql.val(table),
|
|
1226
|
+
sql.val(scopesJson),
|
|
1227
|
+
sql.val(paramsJson),
|
|
1228
|
+
sql.val(sub.nextCursor),
|
|
1229
|
+
sql.val(bootstrapStateJson),
|
|
1230
|
+
sql.val('active'),
|
|
1231
|
+
sql.val(now),
|
|
1232
|
+
sql.val(now),
|
|
1233
|
+
])}
|
|
1234
|
+
)
|
|
1235
|
+
on conflict (${sql.join([sql.ref('state_id'), sql.ref('subscription_id')])})
|
|
1236
|
+
do update set
|
|
1237
|
+
${sql.ref('table')} = ${sql.val(table)},
|
|
1238
|
+
${sql.ref('scopes_json')} = ${sql.val(scopesJson)},
|
|
1239
|
+
${sql.ref('params_json')} = ${sql.val(paramsJson)},
|
|
1240
|
+
${sql.ref('cursor')} = ${sql.val(sub.nextCursor)},
|
|
1241
|
+
${sql.ref('bootstrap_state_json')} = ${sql.val(bootstrapStateJson)},
|
|
1242
|
+
${sql.ref('status')} = ${sql.val('active')},
|
|
1243
|
+
${sql.ref('updated_at')} = ${sql.val(now)}
|
|
1244
|
+
`.execute(trx);
|
|
1140
1245
|
}
|
|
1141
1246
|
|
|
1142
1247
|
export async function syncPullOnce<DB extends SyncClientDb>(
|