velocious 1.0.496 → 1.0.498

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 (42) hide show
  1. package/build/configuration-types.js +21 -0
  2. package/build/configuration.js +48 -0
  3. package/build/database/column-types.js +19 -0
  4. package/build/database/drivers/sqlite/index.native.js +14 -2
  5. package/build/database/record/index.js +9 -0
  6. package/build/src/configuration-types.d.ts +68 -0
  7. package/build/src/configuration-types.d.ts.map +1 -1
  8. package/build/src/configuration-types.js +20 -1
  9. package/build/src/configuration.d.ts +6 -0
  10. package/build/src/configuration.d.ts.map +1 -1
  11. package/build/src/configuration.js +45 -1
  12. package/build/src/database/column-types.d.ts +8 -0
  13. package/build/src/database/column-types.d.ts.map +1 -0
  14. package/build/src/database/column-types.js +18 -0
  15. package/build/src/database/drivers/sqlite/index.native.d.ts +8 -0
  16. package/build/src/database/drivers/sqlite/index.native.d.ts.map +1 -1
  17. package/build/src/database/drivers/sqlite/index.native.js +14 -4
  18. package/build/src/database/record/index.d.ts +8 -0
  19. package/build/src/database/record/index.d.ts.map +1 -1
  20. package/build/src/database/record/index.js +9 -1
  21. package/build/src/sync/sync-api-client.d.ts +4 -1
  22. package/build/src/sync/sync-api-client.d.ts.map +1 -1
  23. package/build/src/sync/sync-api-client.js +9 -2
  24. package/build/src/sync/sync-client-types.d.ts +88 -10
  25. package/build/src/sync/sync-client-types.d.ts.map +1 -1
  26. package/build/src/sync/sync-client-types.js +1 -1
  27. package/build/src/sync/sync-client.d.ts +38 -11
  28. package/build/src/sync/sync-client.d.ts.map +1 -1
  29. package/build/src/sync/sync-client.js +193 -29
  30. package/build/sync/sync-api-client.js +7 -1
  31. package/build/sync/sync-client-types.js +34 -5
  32. package/build/sync/sync-client.js +223 -27
  33. package/build/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +1 -1
  35. package/src/configuration-types.js +21 -0
  36. package/src/configuration.js +48 -0
  37. package/src/database/column-types.js +19 -0
  38. package/src/database/drivers/sqlite/index.native.js +14 -2
  39. package/src/database/record/index.js +9 -0
  40. package/src/sync/sync-api-client.js +7 -1
  41. package/src/sync/sync-client-types.js +34 -5
  42. package/src/sync/sync-client.js +223 -27
@@ -485,7 +485,10 @@ export default class SyncApiClient {
485
485
  }
486
486
 
487
487
  /**
488
- * Builds backend-safe queued sync data without mutating caller data.
488
+ * Builds backend-safe queued sync data without mutating caller data. The default
489
+ * (no explicit `data`) is the resource's attributes minus local-only attributes,
490
+ * with booleans coerced and Date values serialized to ISO strings, so apps don't
491
+ * need per-model tracked-payload builders.
489
492
  * @param {{resource: ?, data?: Record<string, unknown>, localOnlyAttributes?: string[], booleanAttributes?: string[], normalizeData?: (data: Record<string, unknown>) => Record<string, unknown>}} args - Data args.
490
493
  * @returns {Record<string, unknown>} Queued data.
491
494
  */
@@ -498,6 +501,9 @@ export default class SyncApiClient {
498
501
  for (const attributeName of args.booleanAttributes || []) {
499
502
  if (Object.hasOwn(syncData, attributeName)) syncData[attributeName] = this.optionalBooleanSyncValue(syncData[attributeName], attributeName)
500
503
  }
504
+ for (const [attributeName, value] of Object.entries(syncData)) {
505
+ if (value instanceof Date) syncData[attributeName] = value.toISOString()
506
+ }
501
507
 
502
508
  return syncData
503
509
  }
@@ -17,24 +17,53 @@
17
17
  * @property {import("./sync-api-client-types.js").SyncResourceConfig["afterApply"]} [afterApply] - Post-apply hook.
18
18
  * @property {string[]} [booleanAttributes] - Attributes coerced through sync boolean parsing when queueing.
19
19
  * @property {string[]} [localOnlyAttributes] - Attributes stripped from queued payloads.
20
- * @property {(args: {operation: "create" | "update" | "destroy", record: ?}) => string} [syncType] - Maps a mutation operation to a sync type. Defaults to the operation name with destroy mapped to "delete".
20
+ * @property {"upsert" | ((args: {operation: "create" | "update" | "destroy", record: ?}) => string)} [syncType] - Maps a mutation operation to a sync type. The "upsert" flag queues creates and updates as "update" rows (the server upserts by resource id) and destroys as "delete". Defaults to the operation name with destroy mapped to "delete".
21
21
  * @property {(args: {operation: "create" | "update" | "destroy", record: ?}) => Record<string, ?>} [trackedData] - Custom queued-payload builder for tracked mutations.
22
22
  * @property {boolean | {operations: Array<"create" | "update" | "destroy">}} [track] - Enables automatic mutation tracking through model lifecycle callbacks.
23
23
  */
24
24
 
25
25
  /**
26
- * Declarative sync client configuration.
26
+ * Model-level client sync declaration read from `static sync` by
27
+ * `SyncClient.fromConfiguration(...)`. `true` opts the model in with all
28
+ * defaults; an object customizes the derived resource config.
29
+ * @typedef {object} ModelSyncDeclarationConfig
30
+ * @property {import("./sync-api-client-types.js").SyncResourceConfig["afterApply"]} [afterApply] - Post-apply hook.
31
+ * @property {import("./sync-api-client-types.js").SyncResourceConfig["attributes"]} [attributes] - Pull-apply attribute mapper. Required for resources that receive pulled changes.
32
+ * @property {string[]} [booleanAttributes] - Extra boolean attributes merged with the boolean columns derived from column types.
33
+ * @property {import("./sync-api-client-types.js").SyncResourceConfig["findRecord"]} [findRecord] - Custom pull-apply record resolver.
34
+ * @property {import("./sync-api-client-types.js").SyncResourceConfig["findRecordForDelete"]} [findRecordForDelete] - Custom pull-apply delete resolver.
35
+ * @property {string[]} [localOnlyAttributes] - Extra local-only attributes merged with the derived primary key, createdAt/updatedAt, and sync bookkeeping attributes.
36
+ * @property {"upsert" | ((args: {operation: "create" | "update" | "destroy", record: ?}) => string)} [syncType] - Sync type flag or mapper (see SyncClientResourceConfig).
37
+ * @property {boolean | Array<"create" | "update" | "destroy"> | {operations: Array<"create" | "update" | "destroy">}} [track] - Enables automatic mutation tracking; an array is shorthand for {operations}.
38
+ * @property {(args: {operation: "create" | "update" | "destroy", record: ?}) => Record<string, ?>} [trackedData] - Custom queued-payload builder for tracked mutations.
39
+ */
40
+
41
+ /** @typedef {boolean | ModelSyncDeclarationConfig} ModelSyncDeclaration */
42
+
43
+ /**
44
+ * Options for building a sync client. Everything else — resources, transport
45
+ * POSTers, auth, connectivity, batch size — is derived from the configuration's
46
+ * registered models (`static sync`) and its `sync.client` block.
47
+ * @typedef {object} SyncClientOptions
48
+ * @property {import("../configuration.js").default} [configuration] - Configuration owning the registered models, the `sync.client` block, and the scope-store database. Defaults to the current configuration.
49
+ * @property {(args: {scope: SerializedSyncScope}) => string | null | Promise<string | null>} [legacyCursor] - Seeds a newly declared scope's cursor (e.g. from a pre-scope cursor store) so devices don't re-pull everything.
50
+ * @property {import("./sync-scope-store.js").default} [scopeStore] - Scope store override (tests).
51
+ * @property {?} [syncModel] - Pending-sync model override. Defaults to the registered "Sync" model.
52
+ */
53
+
54
+ /**
55
+ * Internal derived sync client configuration built by the SyncClient
56
+ * constructor — not an app-facing API.
27
57
  * @typedef {object} SyncClientConfig
28
58
  * @property {() => string | Promise<string>} authenticationToken - Resolves the auth token sent with sync requests.
29
59
  * @property {number} [batchSize] - Max syncs per request.
30
- * @property {import("../configuration.js").default} [configuration] - Configuration owning the scope-store database. Defaults to the current configuration.
60
+ * @property {import("../configuration.js").default} configuration - Configuration owning the scope-store database.
31
61
  * @property {() => boolean | Promise<boolean>} [isOnline] - Connectivity gate for pulls and replays. Defaults to always online.
32
62
  * @property {(args: {scope: SerializedSyncScope}) => string | null | Promise<string | null>} [legacyCursor] - Seeds a newly declared scope's cursor (e.g. from a pre-scope cursor store) so devices don't re-pull everything.
33
63
  * @property {(error: Error) => void} [onError] - Reports background replay/pull failures. Defaults to rethrowing.
34
64
  * @property {(payload: import("./sync-api-client-types.js").SyncChangesRequest & {scope: SerializedSyncScope}) => Promise<import("./sync-api-client-types.js").SyncChangesResponse>} postChanges - Posts one changes request.
35
65
  * @property {(payload: {authenticationToken: string, syncs: Array<Record<string, ?>>}) => Promise<import("./sync-api-client-types.js").SyncReplayResponse>} postReplay - Posts one replay request.
36
- * @property {Record<string, SyncClientResourceConfig>} resources - Declarative resource policies keyed by resource/model name.
37
- * @property {import("./sync-scope-store.js").default} [scopeStore] - Scope store override.
66
+ * @property {Record<string, SyncClientResourceConfig>} resources - Derived resource policies keyed by resource/model name.
38
67
  * @property {?} syncModel - Local pending-sync model class.
39
68
  */
40
69
 
@@ -1,8 +1,8 @@
1
1
  // @ts-check
2
2
 
3
- import {forcedFunction} from "typanic"
4
-
5
3
  import Configuration from "../configuration.js"
4
+ import {isBooleanColumnType} from "../database/column-types.js"
5
+ import restArgsError from "../utils/rest-args-error.js"
6
6
 
7
7
  import {serializedScopeFromQuery} from "./query-scope.js"
8
8
  import SyncApiClient from "./sync-api-client.js"
@@ -14,44 +14,85 @@ let clientCounter = 0
14
14
  /** @type {{create: "afterCreate", update: "afterUpdate", destroy: "afterDestroy"}} */
15
15
  const TRACKED_CALLBACK_NAMES = {create: "afterCreate", destroy: "afterDestroy", update: "afterUpdate"}
16
16
 
17
+ /** Attribute names treated as client-local sync bookkeeping when deriving localOnlyAttributes. */
18
+ const LOCAL_BOOKKEEPING_ATTRIBUTE_NAMES = ["createdAt", "updatedAt", "lastSyncChangeAt"]
19
+
20
+ /** @type {WeakMap<Configuration, SyncClient>} */
21
+ const syncClientsByConfiguration = new WeakMap()
22
+
17
23
  /**
18
24
  * Declarative client-side sync driver.
19
25
  *
20
- * Apps configure resources, transport, auth, and connectivity once; Velocious
21
- * owns scope persistence, per-scope cursors, pull paging/apply, local queueing,
22
- * and online-gated replay. Declare sync interest from queries:
26
+ * Everything is derived from the app's Velocious configuration: models declare
27
+ * `static sync`, transport/auth/connectivity come from the `sync.client`
28
+ * configuration block, and Velocious owns scope persistence, per-scope cursors,
29
+ * pull paging/apply, local queueing, and online-gated replay. Declare sync
30
+ * interest from queries:
23
31
  *
24
- * const syncClient = new SyncClient({...})
25
- * syncClient.setCurrent()
26
- * await syncClient.sync(Event.where({partnerId}))
32
+ * await syncClient().start()
33
+ * await syncClient().sync(Event.where({partnerId}))
27
34
  */
28
35
  export default class SyncClient {
29
36
  /**
30
- * Creates a declarative sync client.
31
- * @param {import("./sync-client-types.js").SyncClientConfig} config - Sync client configuration.
37
+ * Builds the sync client by deriving everything from the app's Velocious
38
+ * configuration: every registered model declaring `static sync` becomes a
39
+ * resource with booleanAttributes derived from column types and
40
+ * localOnlyAttributes derived from the primary key, createdAt/updatedAt, and
41
+ * sync bookkeeping columns; the pending-sync model is the registered "Sync"
42
+ * model; transport, auth, connectivity, and error reporting come from the
43
+ * `sync.client` configuration block, with the framework owning the
44
+ * `${mountPath}/changes` and `${mountPath}/replay` POSTers.
45
+ * @param {import("./sync-client-types.js").SyncClientOptions} [options] - Optional overrides.
32
46
  */
33
- constructor(config) {
34
- if (!config || typeof config !== "object") throw new Error("SyncClient requires a configuration object")
47
+ constructor(options = {}) {
48
+ const {configuration = Configuration.current(), legacyCursor, scopeStore, syncModel, ...restOptions} = options
35
49
 
36
- forcedFunction(config.postChanges, "SyncClient config.postChanges")
37
- forcedFunction(config.postReplay, "SyncClient config.postReplay")
38
- forcedFunction(config.authenticationToken, "SyncClient config.authenticationToken")
50
+ restArgsError(restOptions)
39
51
 
40
- if (!config.syncModel) throw new Error("SyncClient requires config.syncModel")
41
- if (!config.resources || typeof config.resources !== "object" || Object.keys(config.resources).length === 0) {
42
- throw new Error("SyncClient requires at least one entry in config.resources")
52
+ const clientConfiguration = configuration.getSyncConfiguration().client
53
+
54
+ if (!clientConfiguration) {
55
+ throw new Error("SyncClient requires a sync.client configuration block: new Configuration({sync: {client: {authenticationToken, transport}}})")
43
56
  }
44
57
 
45
- for (const [resourceType, resource] of Object.entries(config.resources)) {
46
- if (!resource || typeof resource !== "object" || !resource.modelClass) {
47
- throw new Error(`SyncClient resource ${resourceType} must declare a modelClass`)
48
- }
58
+ const modelClasses = configuration.getModelClasses()
59
+ /** @type {Record<string, import("./sync-client-types.js").SyncClientResourceConfig>} */
60
+ const resources = {}
61
+
62
+ for (const modelClass of Object.values(modelClasses)) {
63
+ if (!modelClass.sync) continue
64
+
65
+ const resourceType = modelClass.getModelName()
66
+
67
+ resources[resourceType] = resourceConfigFromSyncDeclaration({declaration: modelClass.sync, modelClass, resourceType})
68
+ }
69
+
70
+ if (Object.keys(resources).length === 0) {
71
+ throw new Error("SyncClient found no registered models declaring static sync - declare `static sync = true` (or a sync declaration object) on the models that should sync")
49
72
  }
50
73
 
51
- this.config = config
74
+ const resolvedSyncModel = syncModel || modelClasses.Sync
75
+
76
+ if (!resolvedSyncModel) {
77
+ throw new Error("SyncClient requires a registered \"Sync\" model for pending local sync rows (or pass options.syncModel)")
78
+ }
79
+
80
+ /** @type {import("./sync-client-types.js").SyncClientConfig} */
81
+ this.config = {
82
+ authenticationToken: clientConfiguration.authenticationToken,
83
+ batchSize: clientConfiguration.batchSize,
84
+ configuration,
85
+ isOnline: clientConfiguration.isOnline,
86
+ legacyCursor,
87
+ onError: clientConfiguration.onError,
88
+ postChanges: transportPoster({path: `${clientConfiguration.mountPath}/changes`, transport: clientConfiguration.transport}),
89
+ postReplay: transportPoster({path: `${clientConfiguration.mountPath}/replay`, transport: clientConfiguration.transport}),
90
+ resources,
91
+ syncModel: resolvedSyncModel
92
+ }
52
93
  this._clientNumber = ++clientCounter
53
94
  /** @type {import("./sync-scope-store.js").default | null} */
54
- this._scopeStore = config.scopeStore || null
95
+ this._scopeStore = scopeStore || null
55
96
  /** @type {Promise<void> | null} */
56
97
  this._scheduledReplay = null
57
98
  /** @type {Record<string, import("./sync-api-client-types.js").SyncResourceConfig> | null} */
@@ -171,6 +212,17 @@ export default class SyncClient {
171
212
  return /** @type {SyncClient} */ (currentSyncClient())
172
213
  }
173
214
 
215
+ /**
216
+ * Builds a sync client derived from the given configuration. Alias for
217
+ * `new SyncClient({configuration, ...options})`.
218
+ * @param {Configuration} [configuration] - Configuration owning the registered models and the sync.client block. Defaults to the current configuration.
219
+ * @param {Omit<import("./sync-client-types.js").SyncClientOptions, "configuration">} [options] - Optional overrides.
220
+ * @returns {SyncClient} Sync client derived from the configuration.
221
+ */
222
+ static fromConfiguration(configuration = Configuration.current(), options = {}) {
223
+ return new SyncClient({...options, configuration})
224
+ }
225
+
174
226
  /**
175
227
  * Declares (or re-activates) a sync scope from a model query and pulls it when online.
176
228
  * @param {import("../database/query/model-class-query.js").default<?>} query - Query declaring the sync scope.
@@ -364,7 +416,7 @@ export default class SyncClient {
364
416
  * @returns {import("./sync-scope-store.js").default} Scope store.
365
417
  */
366
418
  scopeStore() {
367
- this._scopeStore ||= new SyncScopeStore({configuration: this.config.configuration || Configuration.current()})
419
+ this._scopeStore ||= new SyncScopeStore({configuration: this.config.configuration})
368
420
 
369
421
  return this._scopeStore
370
422
  }
@@ -390,13 +442,16 @@ export default class SyncClient {
390
442
  }
391
443
 
392
444
  /**
393
- * Resolves the sync type for a mutation through the resource config.
445
+ * Resolves the sync type for a mutation through the resource config. The
446
+ * "upsert" flag queues creates and updates as "update" rows (the server
447
+ * upserts by resource id) and destroys as "delete" rows.
394
448
  * @param {{operation: "create" | "update" | "destroy", record: ?, resourceConfig: import("./sync-client-types.js").SyncClientResourceConfig}} args - Mutation args.
395
449
  * @returns {string} Sync type.
396
450
  */
397
451
  defaultSyncType({operation, record, resourceConfig}) {
398
- if (resourceConfig.syncType) return resourceConfig.syncType({operation, record})
452
+ if (typeof resourceConfig.syncType === "function") return resourceConfig.syncType({operation, record})
399
453
  if (operation === "destroy") return "delete"
454
+ if (resourceConfig.syncType === "upsert") return "update"
400
455
 
401
456
  return operation
402
457
  }
@@ -423,6 +478,147 @@ export default class SyncClient {
423
478
  }
424
479
  }
425
480
 
481
+ /**
482
+ * Builds one resource config from a model's `static sync` declaration plus its
483
+ * derived column metadata.
484
+ * @param {{declaration: import("./sync-client-types.js").ModelSyncDeclaration, modelClass: ?, resourceType: string}} args - Declaration args.
485
+ * @returns {import("./sync-client-types.js").SyncClientResourceConfig} Derived resource config.
486
+ */
487
+ function resourceConfigFromSyncDeclaration({declaration, modelClass, resourceType}) {
488
+ const normalizedDeclaration = declaration === true ? {} : declaration
489
+
490
+ if (!normalizedDeclaration || typeof normalizedDeclaration !== "object" || Array.isArray(normalizedDeclaration)) {
491
+ throw new Error(`${resourceType} static sync must be true or a sync declaration object, got: ${String(declaration)}`)
492
+ }
493
+
494
+ const {afterApply, attributes, booleanAttributes, findRecord, findRecordForDelete, localOnlyAttributes, syncType, track, trackedData, ...restDeclaration} = normalizedDeclaration
495
+ const unknownKeys = Object.keys(restDeclaration)
496
+
497
+ if (unknownKeys.length > 0) {
498
+ throw new Error(`${resourceType} static sync received unknown keys: ${unknownKeys.join(", ")} (supported: afterApply, attributes, booleanAttributes, findRecord, findRecordForDelete, localOnlyAttributes, syncType, track, trackedData)`)
499
+ }
500
+ if (syncType !== undefined && typeof syncType !== "function" && syncType !== "upsert") {
501
+ throw new Error(`${resourceType} static sync syncType must be a function or the string "upsert", got: ${String(syncType)}`)
502
+ }
503
+
504
+ const derived = derivedSyncAttributes({modelClass, resourceType})
505
+
506
+ return {
507
+ afterApply,
508
+ attributes,
509
+ booleanAttributes: mergedAttributeNames(derived.booleanAttributes, booleanAttributes),
510
+ findRecord,
511
+ findRecordForDelete,
512
+ localOnlyAttributes: mergedAttributeNames(derived.localOnlyAttributes, localOnlyAttributes),
513
+ modelClass,
514
+ syncType,
515
+ track: normalizedTrack(track),
516
+ trackedData
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Derives boolean and local-only attribute names from a model's column metadata:
522
+ * booleans from boolean column types; local-only from the primary key,
523
+ * createdAt/updatedAt, and sync bookkeeping columns.
524
+ * @param {{modelClass: ?, resourceType: string}} args - Derivation args.
525
+ * @returns {{booleanAttributes: string[], localOnlyAttributes: string[]}} Derived attribute names.
526
+ */
527
+ function derivedSyncAttributes({modelClass, resourceType}) {
528
+ if (
529
+ typeof modelClass.getColumnNames !== "function" ||
530
+ typeof modelClass.getColumnNameToAttributeNameMap !== "function" ||
531
+ typeof modelClass.getColumnTypeByName !== "function" ||
532
+ typeof modelClass.primaryKey !== "function" ||
533
+ typeof modelClass.hasPrimaryKey !== "function"
534
+ ) {
535
+ throw new Error(`${resourceType} static sync requires a Velocious model class with column metadata (getColumnNames, getColumnNameToAttributeNameMap, getColumnTypeByName, primaryKey, hasPrimaryKey)`)
536
+ }
537
+
538
+ const columnNameToAttributeName = modelClass.getColumnNameToAttributeNameMap()
539
+ /** @type {string[]} */
540
+ const booleanAttributes = []
541
+ /** @type {string[]} */
542
+ const localOnlyAttributes = []
543
+
544
+ if (modelClass.hasPrimaryKey()) {
545
+ const primaryKeyColumn = modelClass.primaryKey()
546
+
547
+ localOnlyAttributes.push(columnNameToAttributeName[primaryKeyColumn] || primaryKeyColumn)
548
+ }
549
+
550
+ for (const columnName of modelClass.getColumnNames()) {
551
+ const attributeName = columnNameToAttributeName[columnName] || columnName
552
+ const columnType = modelClass.getColumnTypeByName(columnName)
553
+
554
+ if (LOCAL_BOOKKEEPING_ATTRIBUTE_NAMES.includes(attributeName) && !localOnlyAttributes.includes(attributeName)) {
555
+ localOnlyAttributes.push(attributeName)
556
+ }
557
+ if (columnType && isBooleanColumnType(columnType)) {
558
+ booleanAttributes.push(attributeName)
559
+ }
560
+ }
561
+
562
+ return {booleanAttributes, localOnlyAttributes}
563
+ }
564
+
565
+ /**
566
+ * Merges derived attribute names with declared extras into a sorted, duplicate-free list.
567
+ * @param {string[]} derived - Derived attribute names.
568
+ * @param {string[] | undefined} declared - Declared extra attribute names.
569
+ * @returns {string[]} Merged attribute names.
570
+ */
571
+ function mergedAttributeNames(derived, declared) {
572
+ return [...new Set([...derived, ...(declared || [])])].sort()
573
+ }
574
+
575
+ /**
576
+ * Normalizes a declaration's track value: an operations array is shorthand for
577
+ * the {operations} form.
578
+ * @param {import("./sync-client-types.js").ModelSyncDeclarationConfig["track"]} track - Declared track value.
579
+ * @returns {import("./sync-client-types.js").SyncClientResourceConfig["track"]} Normalized track value.
580
+ */
581
+ function normalizedTrack(track) {
582
+ if (Array.isArray(track)) return {operations: track}
583
+
584
+ return track
585
+ }
586
+
587
+ /**
588
+ * Builds a framework-owned sync endpoint POSTer over the configured transport.
589
+ * @param {{path: string, transport: import("../configuration-types.js").VelociousSyncClientTransport}} args - Poster args.
590
+ * @returns {(payload: Record<string, ?>) => Promise<?>} Sync endpoint POSTer.
591
+ */
592
+ function transportPoster({path, transport}) {
593
+ return async (payload) => {
594
+ const response = await transport.post(path, payload)
595
+
596
+ if (!response || typeof response.json !== "function") {
597
+ throw new Error(`sync.client transport.post must resolve to a response with a json() method for ${path} (like the frontend-model websocket client)`)
598
+ }
599
+
600
+ return await response.json()
601
+ }
602
+ }
603
+
604
+ /**
605
+ * Lazily builds (and memoizes per configuration) the sync client derived from the
606
+ * app's Velocious configuration and registers it as the current sync client.
607
+ * @param {Configuration} [configuration] - Configuration owning the registered models and the sync.client block. Defaults to the current configuration.
608
+ * @returns {SyncClient} Memoized sync client for the configuration.
609
+ */
610
+ export function syncClient(configuration = Configuration.current()) {
611
+ let client = syncClientsByConfiguration.get(configuration)
612
+
613
+ if (!client) {
614
+ client = SyncClient.fromConfiguration(configuration)
615
+ syncClientsByConfiguration.set(configuration, client)
616
+ client.setCurrent()
617
+ }
618
+
619
+ return client
620
+ }
621
+
426
622
  /**
427
623
  * Declares a sync scope on the current sync client.
428
624
  * @param {import("../database/query/model-class-query.js").default<?>} query - Query declaring the sync scope.