velocious 1.0.700 → 1.0.702
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 -0
- package/build/configuration-types.js +73 -3
- package/build/configuration.js +113 -20
- package/build/frontend-model-resource/base-resource.js +150 -2
- package/build/frontend-models/resource-definition.js +47 -0
- package/build/src/configuration-types.d.ts +168 -7
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +68 -4
- package/build/src/configuration.d.ts +22 -2
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +110 -20
- package/build/src/frontend-model-resource/base-resource.d.ts +76 -3
- package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/base-resource.js +127 -3
- package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
- package/build/src/frontend-models/resource-definition.js +43 -1
- package/build/src/sync/local-mutation-log.d.ts +6 -3
- package/build/src/sync/local-mutation-log.d.ts.map +1 -1
- package/build/src/sync/local-mutation-log.js +11 -4
- package/build/src/sync/peer-mutation-bundle.d.ts +26 -0
- package/build/src/sync/peer-mutation-bundle.d.ts.map +1 -1
- package/build/src/sync/peer-mutation-bundle.js +7 -1
- package/build/src/sync/sync-api-client.d.ts +3 -1
- package/build/src/sync/sync-api-client.d.ts.map +1 -1
- package/build/src/sync/sync-api-client.js +46 -8
- package/build/src/sync/sync-client-types.d.ts +24 -12
- package/build/src/sync/sync-client-types.d.ts.map +1 -1
- package/build/src/sync/sync-client-types.js +7 -5
- package/build/src/sync/sync-client.d.ts +101 -0
- package/build/src/sync/sync-client.d.ts.map +1 -1
- package/build/src/sync/sync-client.js +236 -24
- package/build/src/sync/sync-offline-session.d.ts +328 -0
- package/build/src/sync/sync-offline-session.d.ts.map +1 -0
- package/build/src/sync/sync-offline-session.js +617 -0
- package/build/src/sync/sync-resource-base.d.ts +26 -28
- package/build/src/sync/sync-resource-base.d.ts.map +1 -1
- package/build/src/sync/sync-resource-base.js +11 -45
- package/build/sync/local-mutation-log.js +10 -3
- package/build/sync/peer-mutation-bundle.js +6 -0
- package/build/sync/sync-api-client.js +53 -7
- package/build/sync/sync-client-types.js +6 -4
- package/build/sync/sync-client.js +264 -21
- package/build/sync/sync-offline-session.js +713 -0
- package/build/sync/sync-resource-base.js +10 -53
- package/package.json +1 -1
- package/src/configuration-types.js +73 -3
- package/src/configuration.js +113 -20
- package/src/frontend-model-resource/base-resource.js +150 -2
- package/src/frontend-models/resource-definition.js +47 -0
- package/src/sync/local-mutation-log.js +10 -3
- package/src/sync/peer-mutation-bundle.js +6 -0
- package/src/sync/sync-api-client.js +53 -7
- package/src/sync/sync-client-types.js +6 -4
- package/src/sync/sync-client.js +264 -21
- package/src/sync/sync-offline-session.js +713 -0
- package/src/sync/sync-resource-base.js +10 -53
|
@@ -10,6 +10,7 @@ import VelociousWebsocketClient from "../http-client/websocket-client.js"
|
|
|
10
10
|
|
|
11
11
|
import {serializedScopeFromQuery} from "./query-scope.js"
|
|
12
12
|
import SyncApiClient from "./sync-api-client.js"
|
|
13
|
+
import SyncOfflineSession from "./sync-offline-session.js"
|
|
13
14
|
import SyncRealtimeBridge from "./sync-realtime-bridge.js"
|
|
14
15
|
import SyncScopeStore from "./sync-scope-store.js"
|
|
15
16
|
import {currentSyncClient, setCurrentSyncClient} from "./sync-client-registry.js"
|
|
@@ -121,7 +122,7 @@ export default class SyncClient {
|
|
|
121
122
|
const metadataModelClass = tenantHandle
|
|
122
123
|
? tenantHandle.metadataModelClass({databaseIdentifier: /** @type {string} */ (databaseIdentifier), modelClass})
|
|
123
124
|
: modelClass
|
|
124
|
-
const resourceConfig = resourceConfigFromSyncDeclaration({declaration: modelClass.sync, metadataModelClass, modelClass, resourceType})
|
|
125
|
+
const resourceConfig = resourceConfigFromSyncDeclaration({declaration: modelClass.sync, metadataModelClass, modelClass, offlineConfigured: Boolean(clientConfiguration.offline), resourceType})
|
|
125
126
|
|
|
126
127
|
if (databaseIdentity && resourceConfig.conflictTracking) {
|
|
127
128
|
resourceConfig.conflictTracking = {
|
|
@@ -153,6 +154,8 @@ export default class SyncClient {
|
|
|
153
154
|
isOnline: clientConfiguration.isOnline,
|
|
154
155
|
legacyCursor,
|
|
155
156
|
onError: clientConfiguration.onError,
|
|
157
|
+
offline: clientConfiguration.offline,
|
|
158
|
+
offlinePost: transportPathPoster({requestContext: capturedRequestContext, transport: clientConfiguration.transport}),
|
|
156
159
|
postChanges: transportPoster({path: `${clientConfiguration.mountPath}/changes`, requestContext: capturedRequestContext, transport: clientConfiguration.transport}),
|
|
157
160
|
postReplay: transportPoster({path: `${clientConfiguration.mountPath}/replay`, requestContext: capturedRequestContext, transport: clientConfiguration.transport}),
|
|
158
161
|
realtime: clientConfiguration.realtime,
|
|
@@ -170,6 +173,8 @@ export default class SyncClient {
|
|
|
170
173
|
: null
|
|
171
174
|
/** @type {SyncRealtimeBridge | null} */
|
|
172
175
|
this._realtimeBridge = null
|
|
176
|
+
/** @type {SyncOfflineSession | null} */
|
|
177
|
+
this._offlineSession = null
|
|
173
178
|
/** @type {import("./sync-client-types.js").SyncClientSharedConnection | null | undefined} Shared app-lifetime websocket connection (undefined until first resolved, null when none is configured). */
|
|
174
179
|
this._syncConnection = undefined
|
|
175
180
|
/** @type {Promise<void> | null} */
|
|
@@ -1089,6 +1094,7 @@ export default class SyncClient {
|
|
|
1089
1094
|
baseVersion: baseVersion === undefined ? this.baseVersionFor({operation, record: resource, resourceConfig}) : baseVersion,
|
|
1090
1095
|
conflictTracking: resourceConfig.conflictTracking,
|
|
1091
1096
|
data: queuedData,
|
|
1097
|
+
offlineSession: this.config.offline ? this.offlineSession() : undefined,
|
|
1092
1098
|
operation,
|
|
1093
1099
|
resource,
|
|
1094
1100
|
resourceType: resource.constructor.getModelName(),
|
|
@@ -1137,19 +1143,28 @@ export default class SyncClient {
|
|
|
1137
1143
|
|
|
1138
1144
|
await SyncApiClient.singleFlight(`velocious-sync-client-replay-${this._clientNumber}`, async () => await this.withTenantOperation(async (operation) => {
|
|
1139
1145
|
this._throwIfLifecycleAborted(signal)
|
|
1140
|
-
for (const [resourceType, resourceConfig] of Object.entries(this.config.resources)) {
|
|
1141
|
-
if (!resourceConfig.conflictTracking) continue
|
|
1142
1146
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1147
|
+
if (this.config.offline) {
|
|
1148
|
+
// Long-offline session configured: the signed offline session owns the
|
|
1149
|
+
// conflict-tracked mutation logs, so the background drain replays them
|
|
1150
|
+
// through the signed replay endpoint instead of the legacy unsigned
|
|
1151
|
+
// one. One owner per log — never both.
|
|
1152
|
+
await this._replayOfflinePending({signal})
|
|
1153
|
+
} else {
|
|
1154
|
+
for (const [resourceType, resourceConfig] of Object.entries(this.config.resources)) {
|
|
1155
|
+
if (!resourceConfig.conflictTracking) continue
|
|
1156
|
+
|
|
1157
|
+
await SyncApiClient.replayConflictTrackedSyncs({
|
|
1158
|
+
applyConflict: async ({record, result}) => await this.applyConflictReplayResult({record, result, resourceType}),
|
|
1159
|
+
authenticationToken: await this.config.authenticationToken(),
|
|
1160
|
+
batchSize: this.config.batchSize,
|
|
1161
|
+
conflictTracking: resourceConfig.conflictTracking,
|
|
1162
|
+
postReplay: this.config.postReplay,
|
|
1163
|
+
remoteGeneration: (identity) => this._remoteGenerations.get(identity) || 0,
|
|
1164
|
+
resourceType,
|
|
1165
|
+
signal
|
|
1166
|
+
})
|
|
1167
|
+
}
|
|
1153
1168
|
}
|
|
1154
1169
|
|
|
1155
1170
|
this._throwIfLifecycleAborted(signal)
|
|
@@ -1256,6 +1271,203 @@ export default class SyncClient {
|
|
|
1256
1271
|
})
|
|
1257
1272
|
}
|
|
1258
1273
|
|
|
1274
|
+
/**
|
|
1275
|
+
* Returns the framework-owned long-offline session built from
|
|
1276
|
+
* `sync.client.offline`, creating it on first use. Fails when the offline
|
|
1277
|
+
* configuration block is missing.
|
|
1278
|
+
* @returns {SyncOfflineSession} - The client's offline session.
|
|
1279
|
+
*/
|
|
1280
|
+
offlineSession() {
|
|
1281
|
+
if (!this.config.offline) {
|
|
1282
|
+
throw new Error("SyncClient has no sync.client.offline configuration - configure deviceIdentityStorage, enrollDevice, and grantScopes to use the long-offline session")
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
if (!this._offlineSession) {
|
|
1286
|
+
this._offlineSession = new SyncOfflineSession({
|
|
1287
|
+
authenticationToken: this.config.authenticationToken,
|
|
1288
|
+
config: this.config.offline,
|
|
1289
|
+
mutationLogs: this.offlineMutationLogs(),
|
|
1290
|
+
post: this.config.offlinePost
|
|
1291
|
+
})
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
return this._offlineSession
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* Collects the durable mutation logs the offline session signs, replays,
|
|
1299
|
+
* re-signs, and exports: the configured `sync.client.offline.mutationLog`
|
|
1300
|
+
* when given, plus every conflict-tracking log declared by the synced models.
|
|
1301
|
+
* @returns {Array<import("./local-mutation-log.js").default>} - Session mutation logs.
|
|
1302
|
+
*/
|
|
1303
|
+
offlineMutationLogs() {
|
|
1304
|
+
/** @type {Array<import("./local-mutation-log.js").default>} */
|
|
1305
|
+
const logs = []
|
|
1306
|
+
|
|
1307
|
+
if (this.config.offline?.mutationLog) logs.push(this.config.offline.mutationLog)
|
|
1308
|
+
|
|
1309
|
+
for (const resourceConfig of Object.values(this.config.resources)) {
|
|
1310
|
+
const mutationLog = resourceConfig.conflictTracking?.mutationLog
|
|
1311
|
+
|
|
1312
|
+
if (!mutationLog) continue
|
|
1313
|
+
if (logs.some((log) => log === mutationLog || log.storageKey === mutationLog.storageKey)) continue
|
|
1314
|
+
|
|
1315
|
+
logs.push(mutationLog)
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
if (logs.length === 0) throw new Error("SyncClient offline session found no durable mutation log - declare conflictTracking on a synced model or configure sync.client.offline.mutationLog")
|
|
1319
|
+
|
|
1320
|
+
return logs
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Starts the long-offline session: resolves the device signing identity
|
|
1325
|
+
* (generating and enrolling a key pair when the app-owned storage has none)
|
|
1326
|
+
* and bootstraps a live offline grant through the built-in
|
|
1327
|
+
* `/frontend-models/sync/bootstrap` endpoint. Idempotent.
|
|
1328
|
+
* @returns {Promise<import("./sync-offline-session.js").SyncOfflineSessionSummary>} - Session summary (actor ids, live grant id, expiry).
|
|
1329
|
+
*/
|
|
1330
|
+
async startOfflineSession() {
|
|
1331
|
+
return await this._runLifecycleWork(async (signal) => {
|
|
1332
|
+
this._throwIfLifecycleAborted(signal)
|
|
1333
|
+
|
|
1334
|
+
const summary = await this.offlineSession().start()
|
|
1335
|
+
|
|
1336
|
+
this._throwIfLifecycleAborted(signal)
|
|
1337
|
+
|
|
1338
|
+
return summary
|
|
1339
|
+
})
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Stops the long-offline session. The device identity and retained grants
|
|
1344
|
+
* stay in the app-owned durable storage; pending mutations stay in the
|
|
1345
|
+
* durable mutation logs and replay with the grant they were signed under.
|
|
1346
|
+
* @returns {Promise<void>} - Resolves after the session is stopped.
|
|
1347
|
+
*/
|
|
1348
|
+
async stopOfflineSession() {
|
|
1349
|
+
if (!this._offlineSession) return
|
|
1350
|
+
|
|
1351
|
+
await this._offlineSession.stop()
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Bootstraps a fresh offline grant and re-signs this device's pending
|
|
1356
|
+
* mutations under it (the replay idempotency key is grant-id-free, so
|
|
1357
|
+
* re-signing never duplicates an intent).
|
|
1358
|
+
* @returns {Promise<import("./sync-offline-session.js").SyncOfflineSessionSummary>} - Refreshed session summary.
|
|
1359
|
+
*/
|
|
1360
|
+
async refreshOfflineGrant() {
|
|
1361
|
+
return await this._runLifecycleWork(async (signal) => {
|
|
1362
|
+
this._throwIfLifecycleAborted(signal)
|
|
1363
|
+
|
|
1364
|
+
const summary = await this.offlineSession().refreshGrant()
|
|
1365
|
+
|
|
1366
|
+
this._throwIfLifecycleAborted(signal)
|
|
1367
|
+
|
|
1368
|
+
return summary
|
|
1369
|
+
})
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* Drains the durable mutation logs through the built-in signed replay
|
|
1374
|
+
* endpoint (`/frontend-models/sync/replay`): ensures a live grant (refreshing
|
|
1375
|
+
* an expired one), batches pending signed mutations with the signed grant
|
|
1376
|
+
* each was signed under, and maps per-mutation results back onto the log
|
|
1377
|
+
* records as synced/conflict/rejected. Structured conflicts stay reachable
|
|
1378
|
+
* through the existing conflict contract (`syncResult.conflict` on the record
|
|
1379
|
+
* and `resolveConflict(...)`).
|
|
1380
|
+
* @param {object} [options] - Replay options.
|
|
1381
|
+
* @param {number} [options.batchSize] - Max signed mutations per request.
|
|
1382
|
+
* @returns {Promise<import("./sync-offline-session.js").SyncOfflineReplayResult>} - Mapped replay result.
|
|
1383
|
+
*/
|
|
1384
|
+
async replayOfflinePending(options = {}) {
|
|
1385
|
+
return await this._runLifecycleWork(async (signal) => await this._replayOfflinePending({...options, signal}))
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* Signed replay implementation bound to one lifecycle generation.
|
|
1390
|
+
* @param {object} [options] - Replay options.
|
|
1391
|
+
* @param {number} [options.batchSize] - Max signed mutations per request.
|
|
1392
|
+
* @param {AbortSignal} [options.signal] - Lifecycle cancellation signal.
|
|
1393
|
+
* @returns {Promise<import("./sync-offline-session.js").SyncOfflineReplayResult>} - Mapped replay result.
|
|
1394
|
+
*/
|
|
1395
|
+
async _replayOfflinePending(options = {}) {
|
|
1396
|
+
const {signal, ...replayOptions} = options
|
|
1397
|
+
const lifecycleSignal = signal ?? this._lifecycleAbortController.signal
|
|
1398
|
+
|
|
1399
|
+
this._throwIfLifecycleAborted(lifecycleSignal)
|
|
1400
|
+
|
|
1401
|
+
const result = await this.offlineSession().replayPending({...replayOptions, signal: lifecycleSignal})
|
|
1402
|
+
|
|
1403
|
+
this._throwIfLifecycleAborted(lifecycleSignal)
|
|
1404
|
+
|
|
1405
|
+
for (const entry of result.results) {
|
|
1406
|
+
if (entry.status !== "conflict" || !entry.serverResult) continue
|
|
1407
|
+
|
|
1408
|
+
await this.applyOfflineConflictReplayResult(entry)
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
return result
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/**
|
|
1415
|
+
* Applies the authoritative server version carried by a signed replay
|
|
1416
|
+
* conflict result through the same conflict contract the online replay uses.
|
|
1417
|
+
* @param {{mutation: import("./device-identity.js").SyncMutation, serverResult: Record<string, ReturnType<typeof JSON.parse>> | null, status: string}} entry - Mapped replay result.
|
|
1418
|
+
* @returns {Promise<void>} - Resolves after the conflict server version is applied.
|
|
1419
|
+
*/
|
|
1420
|
+
async applyOfflineConflictReplayResult(entry) {
|
|
1421
|
+
if (!entry.serverResult) return
|
|
1422
|
+
|
|
1423
|
+
const response = entry.serverResult.response
|
|
1424
|
+
|
|
1425
|
+
if (!response || typeof response !== "object" || Array.isArray(response)) return
|
|
1426
|
+
|
|
1427
|
+
const conflict = /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (response).conflict
|
|
1428
|
+
|
|
1429
|
+
if (!conflict || !Object.hasOwn(conflict, "serverModel")) return
|
|
1430
|
+
|
|
1431
|
+
await this.applyConflictReplayResult({
|
|
1432
|
+
record: /** @type {import("./local-mutation-log.js").LocalMutationLogRecord} */ ({mutation: {payload: entry.mutation.payload}}),
|
|
1433
|
+
resourceType: entry.mutation.model,
|
|
1434
|
+
result: /** @type {import("./sync-api-client-types.js").SyncReplayItem} */ ({conflict})
|
|
1435
|
+
})
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* Exports the durable non-terminal mutations of the offline session's logs
|
|
1440
|
+
* as one signed peer bundle for device-to-device transfer.
|
|
1441
|
+
* @param {object} [options] - Export options.
|
|
1442
|
+
* @param {import("./local-mutation-log.js").LocalMutationStatus[]} [options.statuses] - Statuses to export.
|
|
1443
|
+
* @param {() => Date} [options.now] - Export clock.
|
|
1444
|
+
* @returns {Promise<import("./peer-mutation-bundle.js").PeerMutationBundle>} - Signed peer bundle.
|
|
1445
|
+
*/
|
|
1446
|
+
async exportPeerBundle(options = {}) {
|
|
1447
|
+
return await this._runLifecycleWork(async (signal) => {
|
|
1448
|
+
this._throwIfLifecycleAborted(signal)
|
|
1449
|
+
|
|
1450
|
+
return await this.offlineSession().exportBundle(options)
|
|
1451
|
+
})
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* Imports a peer bundle into the local mutation log: every entry is verified
|
|
1456
|
+
* against the configured backend public key before it is appended, and
|
|
1457
|
+
* entries already present by idempotency key are skipped as duplicates.
|
|
1458
|
+
* @param {object} args - Import args.
|
|
1459
|
+
* @param {import("./peer-mutation-bundle.js").PeerMutationBundle} args.bundle - Peer bundle to import.
|
|
1460
|
+
* @param {import("./local-mutation-log.js").default} [args.mutationLog] - Target log. Defaults to the session's first log.
|
|
1461
|
+
* @returns {Promise<import("./peer-mutation-bundle.js").PeerMutationBundleImportResult>} - Import result (imported/rejected/skipped).
|
|
1462
|
+
*/
|
|
1463
|
+
async importPeerBundle({bundle, mutationLog}) {
|
|
1464
|
+
return await this._runLifecycleWork(async (signal) => {
|
|
1465
|
+
this._throwIfLifecycleAborted(signal)
|
|
1466
|
+
|
|
1467
|
+
return await this.offlineSession().importBundle({bundle, mutationLog})
|
|
1468
|
+
})
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1259
1471
|
/**
|
|
1260
1472
|
* Records an authoritative remote observation so an in-flight acknowledgement
|
|
1261
1473
|
* cannot rebase a successor across that observation.
|
|
@@ -1605,10 +1817,10 @@ function safeConflictVersion(value) {
|
|
|
1605
1817
|
/**
|
|
1606
1818
|
* Builds one resource config from a model's `static sync` declaration plus its
|
|
1607
1819
|
* derived column metadata.
|
|
1608
|
-
* @param {{declaration: import("./sync-client-types.js").ModelSyncDeclaration, metadataModelClass: ReturnType<typeof JSON.parse>, modelClass: ReturnType<typeof JSON.parse>, resourceType: string}} args - Declaration args.
|
|
1820
|
+
* @param {{declaration: import("./sync-client-types.js").ModelSyncDeclaration, metadataModelClass: ReturnType<typeof JSON.parse>, modelClass: ReturnType<typeof JSON.parse>, offlineConfigured?: boolean, resourceType: string}} args - Declaration args.
|
|
1609
1821
|
* @returns {import("./sync-client-types.js").SyncClientResourceConfig} Derived resource config.
|
|
1610
1822
|
*/
|
|
1611
|
-
function resourceConfigFromSyncDeclaration({declaration, metadataModelClass, modelClass, resourceType}) {
|
|
1823
|
+
function resourceConfigFromSyncDeclaration({declaration, metadataModelClass, modelClass, offlineConfigured = false, resourceType}) {
|
|
1612
1824
|
const normalizedDeclaration = declaration === true ? {} : declaration
|
|
1613
1825
|
|
|
1614
1826
|
if (!normalizedDeclaration || typeof normalizedDeclaration !== "object" || Array.isArray(normalizedDeclaration)) {
|
|
@@ -1632,7 +1844,7 @@ function resourceConfigFromSyncDeclaration({declaration, metadataModelClass, mod
|
|
|
1632
1844
|
|
|
1633
1845
|
const derived = derivedSyncAttributes({modelClass: metadataModelClass, resourceType})
|
|
1634
1846
|
|
|
1635
|
-
if (conflictTracking) validateConflictTracking({conflictTracking, derived, resourceType})
|
|
1847
|
+
if (conflictTracking) validateConflictTracking({conflictTracking, derived, offlineConfigured, resourceType})
|
|
1636
1848
|
|
|
1637
1849
|
return {
|
|
1638
1850
|
afterApply,
|
|
@@ -1656,19 +1868,26 @@ function resourceConfigFromSyncDeclaration({declaration, metadataModelClass, mod
|
|
|
1656
1868
|
|
|
1657
1869
|
/**
|
|
1658
1870
|
* Validates one resource's durable conflict-tracking declaration.
|
|
1659
|
-
* @param {{conflictTracking: import("./sync-client-types.js").SyncClientConflictTrackingConfig, derived: {booleanAttributes: string[], localOnlyAttributes: string[]}, resourceType: string}} args - Validation args.
|
|
1871
|
+
* @param {{conflictTracking: import("./sync-client-types.js").SyncClientConflictTrackingConfig, derived: {booleanAttributes: string[], localOnlyAttributes: string[]}, offlineConfigured: boolean, resourceType: string}} args - Validation args.
|
|
1660
1872
|
* @returns {void}
|
|
1661
1873
|
*/
|
|
1662
|
-
function validateConflictTracking({conflictTracking, derived, resourceType}) {
|
|
1663
|
-
const
|
|
1874
|
+
function validateConflictTracking({conflictTracking, derived, offlineConfigured, resourceType}) {
|
|
1875
|
+
const identityFields = {
|
|
1664
1876
|
actorDeviceId: conflictTracking.actorDeviceId,
|
|
1665
1877
|
actorUserId: conflictTracking.actorUserId,
|
|
1666
1878
|
offlineGrantId: conflictTracking.offlineGrantId,
|
|
1667
1879
|
policyHash: conflictTracking.policyHash
|
|
1668
1880
|
}
|
|
1669
1881
|
|
|
1670
|
-
for (const [key, value] of Object.entries(
|
|
1671
|
-
|
|
1882
|
+
for (const [key, value] of Object.entries(identityFields)) {
|
|
1883
|
+
// Grants and policy hashes rotate at runtime: a declaration may provide a
|
|
1884
|
+
// static string, a function resolving the value at queue time, or omit the
|
|
1885
|
+
// key entirely when a `sync.client.offline` session supplies it.
|
|
1886
|
+
if (value === undefined && offlineConfigured) continue
|
|
1887
|
+
if (typeof value === "function") continue
|
|
1888
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1889
|
+
throw new Error(`${resourceType} conflictTracking.${key} must be a non-empty string, a function resolving one, or omitted when sync.client.offline supplies it`)
|
|
1890
|
+
}
|
|
1672
1891
|
}
|
|
1673
1892
|
if (!conflictTracking.mutationLog || typeof conflictTracking.mutationLog.append !== "function") throw new Error(`${resourceType} conflictTracking.mutationLog must be a LocalMutationLog`)
|
|
1674
1893
|
if (typeof conflictTracking.clientMutationId !== "function") throw new Error(`${resourceType} conflictTracking.clientMutationId must be a function`)
|
|
@@ -1744,6 +1963,30 @@ function normalizedTrack(track) {
|
|
|
1744
1963
|
return track
|
|
1745
1964
|
}
|
|
1746
1965
|
|
|
1966
|
+
/**
|
|
1967
|
+
* Builds a framework-owned POSTer over the configured transport for a path
|
|
1968
|
+
* chosen at call time (used by the long-offline session's bootstrap/replay
|
|
1969
|
+
* endpoints, which live outside the sync.api mount path).
|
|
1970
|
+
* @param {{requestContext: import("../remote-request-context.js").RemoteRequestContext, transport: import("../configuration-types.js").VelociousSyncClientTransport}} args - Poster args.
|
|
1971
|
+
* @returns {(path: string, payload: Record<string, ReturnType<typeof JSON.parse>>, options?: {signal?: AbortSignal}) => Promise<ReturnType<typeof JSON.parse>>} Path-selecting sync endpoint POSTer.
|
|
1972
|
+
*/
|
|
1973
|
+
function transportPathPoster({requestContext, transport}) {
|
|
1974
|
+
return async (path, payload, options = {}) => {
|
|
1975
|
+
const requestPayload = mergeRemoteRequestContext({
|
|
1976
|
+
context: requestContext,
|
|
1977
|
+
label: "Sync client request context",
|
|
1978
|
+
params: payload
|
|
1979
|
+
})
|
|
1980
|
+
const response = await transport.post(path, requestPayload, {signal: options.signal})
|
|
1981
|
+
|
|
1982
|
+
if (!response || typeof response.json !== "function") {
|
|
1983
|
+
throw new Error(`sync.client transport.post must resolve to a response with a json() method for ${path} (like the frontend-model websocket client)`)
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
return await response.json()
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1747
1990
|
/**
|
|
1748
1991
|
* Builds a framework-owned sync endpoint POSTer over the configured transport.
|
|
1749
1992
|
* @param {{path: string, requestContext: import("../remote-request-context.js").RemoteRequestContext, transport: import("../configuration-types.js").VelociousSyncClientTransport}} args - Poster args.
|