velocious 1.0.672 → 1.0.674
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 +1 -1
- package/build/database/drivers/base.js +52 -18
- package/build/database/drivers/mssql/index.js +48 -3
- package/build/frontend-model-controller.js +10 -2
- package/build/frontend-model-resource/base-resource.js +21 -2
- package/build/src/database/drivers/base.d.ts +23 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +51 -21
- package/build/src/database/drivers/mssql/index.d.ts +22 -0
- package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/index.js +45 -4
- package/build/src/frontend-model-controller.d.ts +5 -0
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +10 -3
- package/build/src/frontend-model-resource/base-resource.d.ts +10 -0
- package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/base-resource.js +19 -3
- package/build/src/sync/local-mutation-log.d.ts +12 -0
- package/build/src/sync/local-mutation-log.d.ts.map +1 -1
- package/build/src/sync/local-mutation-log.js +45 -1
- package/build/src/sync/sync-api-client.d.ts +6 -1
- package/build/src/sync/sync-api-client.d.ts.map +1 -1
- package/build/src/sync/sync-api-client.js +7 -2
- package/build/src/sync/sync-client.d.ts +60 -0
- package/build/src/sync/sync-client.d.ts.map +1 -1
- package/build/src/sync/sync-client.js +172 -3
- package/build/src/sync/sync-coordinator-types.d.ts +190 -0
- package/build/src/sync/sync-coordinator-types.d.ts.map +1 -0
- package/build/src/sync/sync-coordinator-types.js +65 -0
- package/build/src/sync/sync-coordinator.d.ts +246 -0
- package/build/src/sync/sync-coordinator.d.ts.map +1 -0
- package/build/src/sync/sync-coordinator.js +683 -0
- package/build/src/sync/sync-envelope-replay-service.d.ts +90 -2
- package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
- package/build/src/sync/sync-envelope-replay-service.js +101 -9
- package/build/src/sync/sync-realtime-bridge.d.ts.map +1 -1
- package/build/src/sync/sync-realtime-bridge.js +8 -1
- package/build/sync/local-mutation-log.js +53 -0
- package/build/sync/sync-api-client.js +7 -1
- package/build/sync/sync-client.js +190 -2
- package/build/sync/sync-coordinator-types.js +73 -0
- package/build/sync/sync-coordinator.js +738 -0
- package/build/sync/sync-envelope-replay-service.js +107 -8
- package/build/sync/sync-realtime-bridge.js +9 -0
- package/package.json +1 -1
- package/src/database/drivers/base.js +52 -18
- package/src/database/drivers/mssql/index.js +48 -3
- package/src/frontend-model-controller.js +10 -2
- package/src/frontend-model-resource/base-resource.js +21 -2
- package/src/sync/local-mutation-log.js +53 -0
- package/src/sync/sync-api-client.js +7 -1
- package/src/sync/sync-client.js +190 -2
- package/src/sync/sync-coordinator-types.js +73 -0
- package/src/sync/sync-coordinator.js +738 -0
- package/src/sync/sync-envelope-replay-service.js +107 -8
- package/src/sync/sync-realtime-bridge.js +9 -0
package/src/sync/sync-client.js
CHANGED
|
@@ -180,6 +180,8 @@ export default class SyncClient {
|
|
|
180
180
|
this._scopeStore = scopeStore || null
|
|
181
181
|
/** @type {Promise<void> | null} */
|
|
182
182
|
this._scheduledReplay = null
|
|
183
|
+
/** @type {((reason: "mutation" | "realtime") => Promise<void>) | null} */
|
|
184
|
+
this._coordinatorTrigger = null
|
|
183
185
|
/** @type {Record<string, import("./sync-api-client-types.js").SyncResourceConfig> | null} */
|
|
184
186
|
this._pullResourceConfigs = null
|
|
185
187
|
/** @type {Array<{callback: (record: ReturnType<typeof JSON.parse>) => Promise<void> | void, callbackName: "afterCreate" | "afterUpdate" | "afterDestroy" | "beforeUpdate" | "beforeDestroy", modelClass: ReturnType<typeof JSON.parse>}>} */
|
|
@@ -200,6 +202,7 @@ export default class SyncClient {
|
|
|
200
202
|
this._lifecycleTransitionCount = 0
|
|
201
203
|
/** @type {Promise<void>} */
|
|
202
204
|
this._lifecycleTransitionPromise = Promise.resolve()
|
|
205
|
+
this._trackingGeneration = 0
|
|
203
206
|
this._started = false
|
|
204
207
|
}
|
|
205
208
|
|
|
@@ -212,7 +215,10 @@ export default class SyncClient {
|
|
|
212
215
|
* @returns {Promise<void>}
|
|
213
216
|
*/
|
|
214
217
|
async start() {
|
|
218
|
+
const trackingGeneration = this._trackingGeneration
|
|
219
|
+
|
|
215
220
|
await this._lifecycleTransitionPromise
|
|
221
|
+
if (trackingGeneration !== this._trackingGeneration) return
|
|
216
222
|
this.assertTenantReady()
|
|
217
223
|
if (this._started) return
|
|
218
224
|
|
|
@@ -257,6 +263,8 @@ export default class SyncClient {
|
|
|
257
263
|
* @returns {Promise<void>}
|
|
258
264
|
*/
|
|
259
265
|
stop(options = {}) {
|
|
266
|
+
this._trackingGeneration += 1
|
|
267
|
+
|
|
260
268
|
for (const {callback, callbackName, modelClass} of this._trackedCallbacks) {
|
|
261
269
|
modelClass.unregisterLifecycleCallback(callbackName, callback)
|
|
262
270
|
}
|
|
@@ -639,6 +647,33 @@ export default class SyncClient {
|
|
|
639
647
|
setCurrentSyncClient(this)
|
|
640
648
|
}
|
|
641
649
|
|
|
650
|
+
/**
|
|
651
|
+
* Attaches the one reusable coordinator that owns mutation and reconnect
|
|
652
|
+
* cycles for this client. The returned detach only removes the same owner.
|
|
653
|
+
* @param {(reason: "mutation" | "realtime") => Promise<void>} trigger - Coordinator trigger.
|
|
654
|
+
* @returns {() => void} Idempotent detach callback.
|
|
655
|
+
*/
|
|
656
|
+
attachCoordinator(trigger) {
|
|
657
|
+
if (typeof trigger !== "function") throw new Error("SyncClient coordinator trigger must be a function")
|
|
658
|
+
if (this._coordinatorTrigger) throw new Error("SyncClient already has an attached coordinator")
|
|
659
|
+
|
|
660
|
+
this._coordinatorTrigger = trigger
|
|
661
|
+
|
|
662
|
+
return () => {
|
|
663
|
+
if (this._coordinatorTrigger === trigger) this._coordinatorTrigger = null
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Routes framework-owned work through the attached coordinator, or returns
|
|
669
|
+
* null so the legacy direct scheduling owner may run.
|
|
670
|
+
* @param {"mutation" | "realtime"} reason - Trigger reason.
|
|
671
|
+
* @returns {Promise<void> | null} Coordinator flight, or null without an owner.
|
|
672
|
+
*/
|
|
673
|
+
requestCoordinatorSync(reason) {
|
|
674
|
+
return this._coordinatorTrigger ? this._coordinatorTrigger(reason) : null
|
|
675
|
+
}
|
|
676
|
+
|
|
642
677
|
/**
|
|
643
678
|
* Returns the app's current sync client.
|
|
644
679
|
* @returns {SyncClient} Current sync client.
|
|
@@ -892,6 +927,26 @@ export default class SyncClient {
|
|
|
892
927
|
await this._subscribeUserScopePromise
|
|
893
928
|
}
|
|
894
929
|
|
|
930
|
+
/**
|
|
931
|
+
* Declares and activates the server-enumerated user scope without starting
|
|
932
|
+
* realtime or pulling. Use this from SyncCoordinator.prepare() so the
|
|
933
|
+
* coordinator remains the sole network ordering owner.
|
|
934
|
+
* @returns {Promise<void>}
|
|
935
|
+
*/
|
|
936
|
+
async activateUserScope() {
|
|
937
|
+
await this._runLifecycleWork(async (signal) => await this._activateUserScope(signal))
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Activates the user scope under the current lifecycle generation.
|
|
942
|
+
* @param {AbortSignal} signal - Lifecycle cancellation signal.
|
|
943
|
+
* @returns {Promise<void>} - Resolves after the scope is durable.
|
|
944
|
+
*/
|
|
945
|
+
async _activateUserScope(signal) {
|
|
946
|
+
await this.scopeStore().findOrCreateScope(await this.userScope())
|
|
947
|
+
this._throwIfLifecycleAborted(signal)
|
|
948
|
+
}
|
|
949
|
+
|
|
895
950
|
/**
|
|
896
951
|
* Declares and activates the user scope for every pullable resource, then
|
|
897
952
|
* subscribes realtime and pulls.
|
|
@@ -902,8 +957,7 @@ export default class SyncClient {
|
|
|
902
957
|
this._userScopeState = "subscribing"
|
|
903
958
|
|
|
904
959
|
try {
|
|
905
|
-
await this.
|
|
906
|
-
this._throwIfLifecycleAborted(signal)
|
|
960
|
+
await this._activateUserScope(signal)
|
|
907
961
|
|
|
908
962
|
await this.subscribeRealtime()
|
|
909
963
|
this._throwIfLifecycleAborted(signal)
|
|
@@ -1087,6 +1141,7 @@ export default class SyncClient {
|
|
|
1087
1141
|
if (!resourceConfig.conflictTracking) continue
|
|
1088
1142
|
|
|
1089
1143
|
await SyncApiClient.replayConflictTrackedSyncs({
|
|
1144
|
+
applyConflict: async ({record, result}) => await this.applyConflictReplayResult({record, result, resourceType}),
|
|
1090
1145
|
authenticationToken: await this.config.authenticationToken(),
|
|
1091
1146
|
batchSize: this.config.batchSize,
|
|
1092
1147
|
conflictTracking: resourceConfig.conflictTracking,
|
|
@@ -1110,6 +1165,97 @@ export default class SyncClient {
|
|
|
1110
1165
|
this._throwIfLifecycleAborted(signal)
|
|
1111
1166
|
}
|
|
1112
1167
|
|
|
1168
|
+
/**
|
|
1169
|
+
* Applies an authoritative serverModel returned by a conflict through the
|
|
1170
|
+
* same tenant-bound, tracking-suppressed applier used by pull/realtime.
|
|
1171
|
+
* A conflict result without serverModel has no authoritative state to apply
|
|
1172
|
+
* and remains diagnostic-only.
|
|
1173
|
+
* @param {{record: import("./local-mutation-log.js").LocalMutationLogRecord, resourceType: string, result: import("./sync-api-client-types.js").SyncReplayItem}} args - Conflict result.
|
|
1174
|
+
* @returns {Promise<void>}
|
|
1175
|
+
*/
|
|
1176
|
+
async applyConflictReplayResult({record, resourceType, result}) {
|
|
1177
|
+
const conflict = result.conflict
|
|
1178
|
+
|
|
1179
|
+
if (!conflict || !Object.hasOwn(conflict, "serverModel")) return
|
|
1180
|
+
|
|
1181
|
+
const serverModel = conflict.serverModel
|
|
1182
|
+
|
|
1183
|
+
if (serverModel !== null && (typeof serverModel !== "object" || Array.isArray(serverModel))) {
|
|
1184
|
+
throw new Error(`Sync conflict serverModel for ${resourceType} must be an object or null`)
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const resourceId = record.mutation.payload?.resourceId
|
|
1188
|
+
|
|
1189
|
+
if (typeof resourceId !== "string" || resourceId.length === 0) throw new Error(`Sync conflict for ${resourceType} is missing resourceId`)
|
|
1190
|
+
|
|
1191
|
+
const sync = SyncApiClient.syncEnvelopeFromPayload({
|
|
1192
|
+
data: serverModel,
|
|
1193
|
+
resourceId,
|
|
1194
|
+
resourceType,
|
|
1195
|
+
syncType: serverModel === null ? "delete" : "update"
|
|
1196
|
+
})
|
|
1197
|
+
|
|
1198
|
+
await this.remoteApplySync({source: "conflict server version"})(sync)
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/**
|
|
1202
|
+
* Returns queue counts and privacy-safe conflicts for coordinator/UI status.
|
|
1203
|
+
* Full local/server payloads remain only in the durable mutation log.
|
|
1204
|
+
* @returns {Promise<import("./sync-coordinator-types.js").SyncClientInspection>} Durable sync state.
|
|
1205
|
+
*/
|
|
1206
|
+
async inspectSyncState() {
|
|
1207
|
+
return await this._runLifecycleWork(async (signal) => {
|
|
1208
|
+
this._throwIfLifecycleAborted(signal)
|
|
1209
|
+
let pendingCount = 0
|
|
1210
|
+
let rejectedCount = 0
|
|
1211
|
+
/** @type {import("./sync-coordinator-types.js").SyncConflictDiagnostic[]} */
|
|
1212
|
+
const conflicts = []
|
|
1213
|
+
|
|
1214
|
+
for (const [resourceType, resourceConfig] of Object.entries(this.config.resources)) {
|
|
1215
|
+
if (!resourceConfig.conflictTracking) continue
|
|
1216
|
+
|
|
1217
|
+
for (const record of await resourceConfig.conflictTracking.mutationLog.records()) {
|
|
1218
|
+
if (record.mutation.model !== resourceType) continue
|
|
1219
|
+
if (["pending", "applied-locally", "peer-applied"].includes(record.status)) pendingCount += 1
|
|
1220
|
+
if (record.status === "rejected") rejectedCount += 1
|
|
1221
|
+
if (record.status === "conflict") conflicts.push(syncConflictDiagnostic(record))
|
|
1222
|
+
}
|
|
1223
|
+
this._throwIfLifecycleAborted(signal)
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
pendingCount += await this.withTenantOperation(async (operation) => {
|
|
1227
|
+
const syncModel = operation ? operation.modelClass(this.config.syncModel) : this.config.syncModel
|
|
1228
|
+
const pendingRows = await syncModel.preload({resource: true}).where({state: "pending"}).order("created_at").toArray()
|
|
1229
|
+
|
|
1230
|
+
return pendingRows.length
|
|
1231
|
+
})
|
|
1232
|
+
this._throwIfLifecycleAborted(signal)
|
|
1233
|
+
|
|
1234
|
+
return {conflicts, pendingCount, rejectedCount}
|
|
1235
|
+
})
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Resolves a durable conflict on its declared resource log. Retry-local is
|
|
1240
|
+
* scheduled through the current coordinator when attached.
|
|
1241
|
+
* @param {{recordId: string, resolution: "keep-server" | "retry-local", resourceType: string}} args - Resolution.
|
|
1242
|
+
* @returns {Promise<import("./local-mutation-log.js").LocalMutationLogRecord>} - Resolved durable record.
|
|
1243
|
+
*/
|
|
1244
|
+
async resolveConflict({recordId, resolution, resourceType}) {
|
|
1245
|
+
return await this._runLifecycleWork(async (signal) => {
|
|
1246
|
+
const conflictTracking = this.config.resources[resourceType]?.conflictTracking
|
|
1247
|
+
|
|
1248
|
+
if (!conflictTracking) throw new Error(`No conflict-tracked sync resource configured for: ${resourceType}`)
|
|
1249
|
+
|
|
1250
|
+
const record = await conflictTracking.mutationLog.resolveConflict({id: recordId, resolution})
|
|
1251
|
+
|
|
1252
|
+
this._throwIfLifecycleAborted(signal)
|
|
1253
|
+
if (resolution === "retry-local") this.scheduleReplay()
|
|
1254
|
+
|
|
1255
|
+
return record
|
|
1256
|
+
})
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1113
1259
|
/**
|
|
1114
1260
|
* Records an authoritative remote observation so an in-flight acknowledgement
|
|
1115
1261
|
* cannot rebase a successor across that observation.
|
|
@@ -1192,6 +1338,13 @@ export default class SyncClient {
|
|
|
1192
1338
|
* @returns {void}
|
|
1193
1339
|
*/
|
|
1194
1340
|
scheduleReplay() {
|
|
1341
|
+
const coordinatorRun = this.requestCoordinatorSync("mutation")
|
|
1342
|
+
|
|
1343
|
+
if (coordinatorRun) {
|
|
1344
|
+
this._scheduledReplay = coordinatorRun
|
|
1345
|
+
return
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1195
1348
|
this._scheduledReplay = (async () => {
|
|
1196
1349
|
try {
|
|
1197
1350
|
await this.replayPending()
|
|
@@ -1414,6 +1567,41 @@ export default class SyncClient {
|
|
|
1414
1567
|
}
|
|
1415
1568
|
}
|
|
1416
1569
|
|
|
1570
|
+
/**
|
|
1571
|
+
* Builds privacy-safe conflict metadata from a durable record.
|
|
1572
|
+
* @param {import("./local-mutation-log.js").LocalMutationLogRecord} record - Durable conflict record.
|
|
1573
|
+
* @returns {import("./sync-coordinator-types.js").SyncConflictDiagnostic} - Safe diagnostic.
|
|
1574
|
+
*/
|
|
1575
|
+
function syncConflictDiagnostic(record) {
|
|
1576
|
+
const conflict = record.syncResult?.conflict
|
|
1577
|
+
const conflictObject = conflict && !(conflict instanceof Date) && typeof conflict === "object" && !Array.isArray(conflict)
|
|
1578
|
+
? /** @type {Record<string, unknown>} */ (conflict)
|
|
1579
|
+
: {}
|
|
1580
|
+
const resourceId = record.mutation.payload?.resourceId
|
|
1581
|
+
|
|
1582
|
+
if (typeof resourceId !== "string" || resourceId.length === 0) throw new Error(`Sync conflict ${record.id} is missing a safe resourceId`)
|
|
1583
|
+
|
|
1584
|
+
return {
|
|
1585
|
+
baseVersion: safeConflictVersion(conflictObject.baseVersion ?? record.mutation.baseVersion ?? null),
|
|
1586
|
+
clientMutationId: record.mutation.clientMutationId,
|
|
1587
|
+
localVersion: safeConflictVersion(conflictObject.localVersion ?? null),
|
|
1588
|
+
recordId: record.id,
|
|
1589
|
+
resourceId,
|
|
1590
|
+
resourceType: record.mutation.model,
|
|
1591
|
+
serverVersion: safeConflictVersion(conflictObject.serverVersion ?? null),
|
|
1592
|
+
versionAttribute: typeof conflictObject.versionAttribute === "string" ? conflictObject.versionAttribute : null
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* Restricts diagnostic versions to safe scalar values.
|
|
1598
|
+
* @param {unknown} value - Version candidate.
|
|
1599
|
+
* @returns {string | number | null} - Safe scalar version.
|
|
1600
|
+
*/
|
|
1601
|
+
function safeConflictVersion(value) {
|
|
1602
|
+
return value === null || typeof value === "string" || typeof value === "number" ? value : null
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1417
1605
|
/**
|
|
1418
1606
|
* Builds one resource config from a model's `static sync` declaration plus its
|
|
1419
1607
|
* derived column metadata.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/* eslint-disable jsdoc/no-undefined-types -- ReadonlyArray is a TypeScript utility type used by checkJs. */
|
|
3
|
+
|
|
4
|
+
/** @typedef {"backoff" | "conflicted" | "failed" | "idle" | "offline" | "pending" | "stopped" | "syncing"} SyncCoordinatorState */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Privacy-safe conflict metadata for status surfaces. Mutation attributes,
|
|
8
|
+
* authoritative record data, credentials, and rejection reasons are excluded.
|
|
9
|
+
* @typedef {object} SyncConflictDiagnostic
|
|
10
|
+
* @property {string | number | null} baseVersion - Version the local mutation was based on.
|
|
11
|
+
* @property {string} clientMutationId - Durable client idempotency identifier.
|
|
12
|
+
* @property {string | number | null} localVersion - Optional client version returned by the conflict contract.
|
|
13
|
+
* @property {string} recordId - Durable local mutation-log record id.
|
|
14
|
+
* @property {string} resourceId - Stable resource identifier.
|
|
15
|
+
* @property {string} resourceType - Stable resource/model type.
|
|
16
|
+
* @property {string | number | null} serverVersion - Authoritative server version.
|
|
17
|
+
* @property {string | null} versionAttribute - Authoritative version attribute name.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Safe classified failure metadata. Applications own the classifier and must
|
|
22
|
+
* return only user-safe codes/messages; the original Error is never persisted.
|
|
23
|
+
* @typedef {object} SyncCoordinatorFailure
|
|
24
|
+
* @property {number} attempt - Consecutive cycle attempt number.
|
|
25
|
+
* @property {string} at - ISO timestamp of the failure.
|
|
26
|
+
* @property {string} code - Stable safe error code.
|
|
27
|
+
* @property {string} [message] - Optional safe display message.
|
|
28
|
+
* @property {boolean} retryable - Whether automatic retry is allowed.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Immutable coordinator status snapshot.
|
|
33
|
+
* @typedef {object} SyncCoordinatorStatus
|
|
34
|
+
* @property {ReadonlyArray<SyncConflictDiagnostic>} conflicts - Privacy-safe unresolved conflicts.
|
|
35
|
+
* @property {SyncCoordinatorFailure | null} failure - Last unresolved cycle failure.
|
|
36
|
+
* @property {string | null} lastSuccessAt - Last successful online cycle timestamp.
|
|
37
|
+
* @property {string | null} nextRetryAt - Scheduled automatic retry timestamp.
|
|
38
|
+
* @property {number} pendingCount - Durable pending mutation count.
|
|
39
|
+
* @property {number} rejectedCount - Durable permanently rejected mutation count.
|
|
40
|
+
* @property {SyncCoordinatorState} state - Observable lifecycle state.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Durable sync state reported by SyncClient.
|
|
45
|
+
* @typedef {object} SyncClientInspection
|
|
46
|
+
* @property {ReadonlyArray<SyncConflictDiagnostic>} conflicts - Privacy-safe conflict diagnostics.
|
|
47
|
+
* @property {number} pendingCount - Durable pending mutation count.
|
|
48
|
+
* @property {number} rejectedCount - Durable rejected mutation count.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Application connectivity signal adapter. SyncClient remains the authority
|
|
53
|
+
* for the current online check; this adapter only notifies the coordinator.
|
|
54
|
+
* @typedef {object} SyncCoordinatorConnectivity
|
|
55
|
+
* @property {(listener: (online: boolean) => void) => (() => void)} subscribe - Subscribes to connectivity changes.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Injected timer ownership for deterministic retry tests and teardown.
|
|
60
|
+
* @typedef {object} SyncCoordinatorScheduler
|
|
61
|
+
* @property {(timer: unknown) => void} clearTimeout - Clears a scheduled retry.
|
|
62
|
+
* @property {(callback: () => void, delayMs: number) => unknown} setTimeout - Schedules a retry.
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Optional durable status metadata store. Conflict payloads remain owned by
|
|
67
|
+
* LocalMutationLog; this store receives only privacy-safe snapshots.
|
|
68
|
+
* @typedef {object} SyncCoordinatorStatusStore
|
|
69
|
+
* @property {() => Promise<SyncCoordinatorStatus | null> | SyncCoordinatorStatus | null} load - Loads the last safe snapshot.
|
|
70
|
+
* @property {(status: SyncCoordinatorStatus) => Promise<void> | void} save - Persists one safe snapshot.
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
export {}
|