velocious 1.0.701 → 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.
Files changed (56) hide show
  1. package/README.md +1 -0
  2. package/build/configuration-types.js +72 -2
  3. package/build/configuration.js +61 -3
  4. package/build/frontend-model-resource/base-resource.js +150 -2
  5. package/build/frontend-models/resource-definition.js +47 -0
  6. package/build/src/configuration-types.d.ts +166 -5
  7. package/build/src/configuration-types.d.ts.map +1 -1
  8. package/build/src/configuration-types.js +67 -3
  9. package/build/src/configuration.d.ts +11 -2
  10. package/build/src/configuration.d.ts.map +1 -1
  11. package/build/src/configuration.js +61 -4
  12. package/build/src/frontend-model-resource/base-resource.d.ts +76 -3
  13. package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
  14. package/build/src/frontend-model-resource/base-resource.js +127 -3
  15. package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
  16. package/build/src/frontend-models/resource-definition.js +43 -1
  17. package/build/src/sync/local-mutation-log.d.ts +6 -3
  18. package/build/src/sync/local-mutation-log.d.ts.map +1 -1
  19. package/build/src/sync/local-mutation-log.js +11 -4
  20. package/build/src/sync/peer-mutation-bundle.d.ts +26 -0
  21. package/build/src/sync/peer-mutation-bundle.d.ts.map +1 -1
  22. package/build/src/sync/peer-mutation-bundle.js +7 -1
  23. package/build/src/sync/sync-api-client.d.ts +3 -1
  24. package/build/src/sync/sync-api-client.d.ts.map +1 -1
  25. package/build/src/sync/sync-api-client.js +46 -8
  26. package/build/src/sync/sync-client-types.d.ts +24 -12
  27. package/build/src/sync/sync-client-types.d.ts.map +1 -1
  28. package/build/src/sync/sync-client-types.js +7 -5
  29. package/build/src/sync/sync-client.d.ts +101 -0
  30. package/build/src/sync/sync-client.d.ts.map +1 -1
  31. package/build/src/sync/sync-client.js +236 -24
  32. package/build/src/sync/sync-offline-session.d.ts +328 -0
  33. package/build/src/sync/sync-offline-session.d.ts.map +1 -0
  34. package/build/src/sync/sync-offline-session.js +617 -0
  35. package/build/src/sync/sync-resource-base.d.ts +26 -28
  36. package/build/src/sync/sync-resource-base.d.ts.map +1 -1
  37. package/build/src/sync/sync-resource-base.js +11 -45
  38. package/build/sync/local-mutation-log.js +10 -3
  39. package/build/sync/peer-mutation-bundle.js +6 -0
  40. package/build/sync/sync-api-client.js +53 -7
  41. package/build/sync/sync-client-types.js +6 -4
  42. package/build/sync/sync-client.js +264 -21
  43. package/build/sync/sync-offline-session.js +713 -0
  44. package/build/sync/sync-resource-base.js +10 -53
  45. package/package.json +1 -1
  46. package/src/configuration-types.js +72 -2
  47. package/src/configuration.js +61 -3
  48. package/src/frontend-model-resource/base-resource.js +150 -2
  49. package/src/frontend-models/resource-definition.js +47 -0
  50. package/src/sync/local-mutation-log.js +10 -3
  51. package/src/sync/peer-mutation-bundle.js +6 -0
  52. package/src/sync/sync-api-client.js +53 -7
  53. package/src/sync/sync-client-types.js +6 -4
  54. package/src/sync/sync-client.js +264 -21
  55. package/src/sync/sync-offline-session.js +713 -0
  56. 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
- await SyncApiClient.replayConflictTrackedSyncs({
1144
- applyConflict: async ({record, result}) => await this.applyConflictReplayResult({record, result, resourceType}),
1145
- authenticationToken: await this.config.authenticationToken(),
1146
- batchSize: this.config.batchSize,
1147
- conflictTracking: resourceConfig.conflictTracking,
1148
- postReplay: this.config.postReplay,
1149
- remoteGeneration: (identity) => this._remoteGenerations.get(identity) || 0,
1150
- resourceType,
1151
- signal
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 requiredStrings = {
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(requiredStrings)) {
1671
- if (typeof value !== "string" || value.length === 0) throw new Error(`${resourceType} conflictTracking.${key} must be a non-empty string`)
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.