velocious 1.0.574 → 1.0.576

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 (82) hide show
  1. package/README.md +17 -3
  2. package/build/background-jobs/client.js +71 -3
  3. package/build/background-jobs/job.js +30 -0
  4. package/build/background-jobs/main.js +100 -14
  5. package/build/background-jobs/store.js +344 -41
  6. package/build/background-jobs/types.js +25 -1
  7. package/build/background-jobs/web/controller.js +1 -0
  8. package/build/database/advisory-lock-runner.js +27 -17
  9. package/build/database/drivers/mssql/index.js +98 -3
  10. package/build/database/record/index.js +10 -8
  11. package/build/database/table-data/table-column.js +2 -0
  12. package/build/frontend-models/resource-definition.js +1 -138
  13. package/build/src/background-jobs/client.d.ts +30 -0
  14. package/build/src/background-jobs/client.d.ts.map +1 -1
  15. package/build/src/background-jobs/client.js +64 -4
  16. package/build/src/background-jobs/job.d.ts +19 -0
  17. package/build/src/background-jobs/job.d.ts.map +1 -1
  18. package/build/src/background-jobs/job.js +27 -1
  19. package/build/src/background-jobs/main.d.ts +41 -0
  20. package/build/src/background-jobs/main.d.ts.map +1 -1
  21. package/build/src/background-jobs/main.js +95 -14
  22. package/build/src/background-jobs/store.d.ts +136 -0
  23. package/build/src/background-jobs/store.d.ts.map +1 -1
  24. package/build/src/background-jobs/store.js +314 -42
  25. package/build/src/background-jobs/types.d.ts +86 -2
  26. package/build/src/background-jobs/types.d.ts.map +1 -1
  27. package/build/src/background-jobs/types.js +26 -2
  28. package/build/src/background-jobs/web/controller.d.ts.map +1 -1
  29. package/build/src/background-jobs/web/controller.js +2 -1
  30. package/build/src/database/advisory-lock-runner.d.ts +27 -14
  31. package/build/src/database/advisory-lock-runner.d.ts.map +1 -1
  32. package/build/src/database/advisory-lock-runner.js +27 -18
  33. package/build/src/database/drivers/mssql/index.d.ts +32 -0
  34. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  35. package/build/src/database/drivers/mssql/index.js +86 -4
  36. package/build/src/database/record/index.d.ts +12 -8
  37. package/build/src/database/record/index.d.ts.map +1 -1
  38. package/build/src/database/record/index.js +11 -9
  39. package/build/src/database/table-data/table-column.d.ts.map +1 -1
  40. package/build/src/database/table-data/table-column.js +4 -1
  41. package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
  42. package/build/src/frontend-models/resource-definition.js +2 -124
  43. package/build/src/sync/local-mutation-log.d.ts +7 -1
  44. package/build/src/sync/local-mutation-log.d.ts.map +1 -1
  45. package/build/src/sync/local-mutation-log.js +18 -7
  46. package/build/src/sync/peer-mutation-bundle.d.ts.map +1 -1
  47. package/build/src/sync/peer-mutation-bundle.js +9 -7
  48. package/build/src/sync/signed-sync-envelope-replay-service.d.ts.map +1 -1
  49. package/build/src/sync/signed-sync-envelope-replay-service.js +3 -1
  50. package/build/src/sync/sync-envelope-replay-service.d.ts +69 -0
  51. package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
  52. package/build/src/sync/sync-envelope-replay-service.js +186 -18
  53. package/build/src/testing/test-runner.d.ts.map +1 -1
  54. package/build/src/testing/test-runner.js +17 -22
  55. package/build/src/utils/sha256-hex.d.ts +10 -0
  56. package/build/src/utils/sha256-hex.d.ts.map +1 -0
  57. package/build/src/utils/sha256-hex.js +126 -0
  58. package/build/sync/local-mutation-log.js +19 -5
  59. package/build/sync/peer-mutation-bundle.js +8 -6
  60. package/build/sync/signed-sync-envelope-replay-service.js +2 -0
  61. package/build/sync/sync-envelope-replay-service.js +205 -17
  62. package/build/testing/test-runner.js +17 -22
  63. package/build/tsconfig.tsbuildinfo +1 -1
  64. package/build/utils/sha256-hex.js +141 -0
  65. package/package.json +1 -1
  66. package/src/background-jobs/client.js +71 -3
  67. package/src/background-jobs/job.js +30 -0
  68. package/src/background-jobs/main.js +100 -14
  69. package/src/background-jobs/store.js +344 -41
  70. package/src/background-jobs/types.js +25 -1
  71. package/src/background-jobs/web/controller.js +1 -0
  72. package/src/database/advisory-lock-runner.js +27 -17
  73. package/src/database/drivers/mssql/index.js +98 -3
  74. package/src/database/record/index.js +10 -8
  75. package/src/database/table-data/table-column.js +2 -0
  76. package/src/frontend-models/resource-definition.js +1 -138
  77. package/src/sync/local-mutation-log.js +19 -5
  78. package/src/sync/peer-mutation-bundle.js +8 -6
  79. package/src/sync/signed-sync-envelope-replay-service.js +2 -0
  80. package/src/sync/sync-envelope-replay-service.js +205 -17
  81. package/src/testing/test-runner.js +17 -22
  82. package/src/utils/sha256-hex.js +141 -0
package/README.md CHANGED
@@ -11,11 +11,11 @@
11
11
  * External packages (engines) that contribute data models, frontend-model resources and migrations to a consuming app (see [docs/packages.md](docs/packages.md))
12
12
  * Controllers and views for HTTP endpoints
13
13
  * Frontend-model transport for creating, updating, querying, and subscribing to query-filtered lifecycle events over HTTP/WebSocket, with structured per-attribute validation error responses and one-budget WebSocket startup controls (see [docs/frontend-models.md](docs/frontend-models.md) and [docs/websocket-channels.md](docs/websocket-channels.md))
14
- * Client-side offline sync mutation logs and frontend-model optimistic queueing primitives (see [docs/offline-sync.md](docs/offline-sync.md))
14
+ * Client-side offline sync mutation logs and frontend-model optimistic queueing primitives (see the [shared-resource sync developer guide](docs/shared-resource-sync-guide.md) and [offline sync architecture](docs/offline-sync.md))
15
15
  * Declarative client sync scopes with per-scope cursors, automatic mutation tracking, realtime delivery whose per-recipient authorization hook receives the complete persisted sync envelope, and `sync`/`pull` progress reporting for "X of Y" import screens (see [docs/sync-client.md](docs/sync-client.md) and [docs/offline-sync.md](docs/offline-sync.md))
16
16
  * Reactive `useLiveQuery(Model.where(...))` queries that stay current from committed local model changes across local writes, pulls, and realtime (see [docs/live-queries.md](docs/live-queries.md))
17
- * Server-side sync envelope replay orchestration for app-owned sync receivers (see [docs/sync-envelope-replay-service.md](docs/sync-envelope-replay-service.md))
18
- * AwesomeTasks-shaped offline sync proof using routed resources, domain commands, signed offline grants, and peer-forwarded mutations (see [docs/awesome-tasks-offline-sync-proof.md](docs/awesome-tasks-offline-sync-proof.md))
17
+ * Server-side sync envelope replay orchestration for app-owned sync receivers, including allowlisted authoritative values for conflict resolution (see [docs/sync-envelope-replay-service.md](docs/sync-envelope-replay-service.md))
18
+ * AwesomeTasks-shaped offline sync proof using routed resources, domain commands, signed offline grants, and peer-forwarded mutations (see the [developer guide](docs/shared-resource-sync-guide.md) and [proof](docs/awesome-tasks-offline-sync-proof.md))
19
19
  * SQLite web persistence that automatically prefers OPFS, then IndexedDB, and migrates legacy persisted bytes when possible (see [docs/sqlite-web-persistence.md](docs/sqlite-web-persistence.md))
20
20
  * Expo / Metro compatibility guidance and a real Expo export check (see [docs/expo-metro-compatibility.md](docs/expo-metro-compatibility.md))
21
21
  * Gap-less positional lists with automatic reordering via `actsAsList`, including models with numeric, string, or UUID primary keys (see [docs/acts-as-list.md](docs/acts-as-list.md))
@@ -2249,6 +2249,20 @@ await MyJob.performLaterWithOptions({
2249
2249
 
2250
2250
  Until `scheduledAtMs` is reached, the job remains queued but is not eligible for dispatch. The event-driven dispatcher arms its timer for the earliest future job and wakes at that timestamp. Omitting `scheduledAtMs` keeps the immediate-enqueue behavior.
2251
2251
 
2252
+ Use a durable stable key when the same logical one-off schedule must be moved or cancelled without retaining its transient job id:
2253
+
2254
+ ```js
2255
+ const result = await MyJob.replaceScheduled({
2256
+ scheduleKey: `event:${eventId}:reminder:24h`,
2257
+ args: [eventId, reminderRevision],
2258
+ options: {scheduledAtMs: reminderAtMs}
2259
+ })
2260
+
2261
+ await MyJob.cancelScheduled(`event:${eventId}:reminder:24h`)
2262
+ ```
2263
+
2264
+ A queued owner is atomically cancelled during replacement/cancellation. A `previousStatus` or cancellation `outcome` of `"handed_off"` means the worker may already be running; Velocious removes or replaces key ownership but does not claim that JavaScript stopped. Store a generation/revision in application state, pass it to the job, and re-check it immediately before irreversible effects. Stable keys and full result shapes are documented in [Scheduling One-Off Background Jobs](docs/scheduled-background-job-enqueue.md#replacing-or-cancelling-a-logical-schedule).
2265
+
2252
2266
  Set `deduplicateWhileQueued: true` to coalesce an enqueue onto the earliest identical queued job with the same job name, arguments, and queue when that existing job is scheduled no later than the new request. A retry backed off into the future does not suppress a new immediate enqueue, while repeated immediate triggers and equal or later schedules still coalesce.
2253
2267
 
2254
2268
  Select a non-default runtime explicitly with `options: {executionMode: "inline" | "forked" | "spawned"}`.
@@ -13,6 +13,17 @@ export default class BackgroundJobsClient {
13
13
  this.configurationPromise = configuration ? Promise.resolve(configuration) : configurationResolver()
14
14
  }
15
15
 
16
+ /**
17
+ * Builds a one-shot client socket request from the resolved configuration.
18
+ * @returns {Promise<BackgroundJobsSocketRequest>} - Socket request.
19
+ */
20
+ async _request() {
21
+ const configuration = await this.configurationPromise
22
+ const {host, port} = configuration.getBackgroundJobsConfig()
23
+
24
+ return new BackgroundJobsSocketRequest({host, port, role: "client"})
25
+ }
26
+
16
27
  /**
17
28
  * Runs enqueue.
18
29
  * @param {object} args - Options.
@@ -22,9 +33,7 @@ export default class BackgroundJobsClient {
22
33
  * @returns {Promise<string>} - Job id.
23
34
  */
24
35
  async enqueue({jobName, args, options}) {
25
- const configuration = await this.configurationPromise
26
- const {host, port} = configuration.getBackgroundJobsConfig()
27
- const request = new BackgroundJobsSocketRequest({host, port, role: "client"})
36
+ const request = await this._request()
28
37
 
29
38
  return await request.run({
30
39
  onConnect: (jsonSocket) => {
@@ -47,4 +56,63 @@ export default class BackgroundJobsClient {
47
56
  }
48
57
  })
49
58
  }
59
+
60
+ /**
61
+ * Atomically replaces the queued owner of a stable schedule key.
62
+ * @param {object} args - Options.
63
+ * @param {string} args.scheduleKey - Stable logical schedule key.
64
+ * @param {string} args.jobName - Job name.
65
+ * @param {Array<?>} args.args - Job args.
66
+ * @param {import("./types.js").BackgroundJobOptions} [args.options] - Job options.
67
+ * @returns {Promise<import("./types.js").BackgroundJobReplacementResult>} - Replacement result.
68
+ */
69
+ async replaceScheduled({scheduleKey, jobName, args, options}) {
70
+ const request = await this._request()
71
+
72
+ return await request.run({
73
+ onConnect: (jsonSocket) => {
74
+ jsonSocket.send({type: "replace-scheduled", scheduleKey, jobName, args, options})
75
+ },
76
+ onMessage: ({message, resolve, reject}) => {
77
+ if (message?.type === "schedule-replaced") {
78
+ resolve({
79
+ jobId: message.jobId,
80
+ previousJobId: message.previousJobId,
81
+ previousStatus: message.previousStatus
82
+ })
83
+ return
84
+ }
85
+
86
+ if (message?.type === "replace-scheduled-error") {
87
+ reject(new Error(message.error || "Failed to replace scheduled job"))
88
+ }
89
+ }
90
+ })
91
+ }
92
+
93
+ /**
94
+ * Cancels or detaches the current owner of a stable schedule key.
95
+ * @param {object} args - Options.
96
+ * @param {string} args.scheduleKey - Stable logical schedule key.
97
+ * @returns {Promise<import("./types.js").BackgroundJobCancellationResult>} - Cancellation result.
98
+ */
99
+ async cancelScheduled({scheduleKey}) {
100
+ const request = await this._request()
101
+
102
+ return await request.run({
103
+ onConnect: (jsonSocket) => {
104
+ jsonSocket.send({type: "cancel-scheduled", scheduleKey})
105
+ },
106
+ onMessage: ({message, resolve, reject}) => {
107
+ if (message?.type === "schedule-cancelled") {
108
+ resolve({jobId: message.jobId, outcome: message.outcome})
109
+ return
110
+ }
111
+
112
+ if (message?.type === "cancel-scheduled-error") {
113
+ reject(new Error(message.error || "Failed to cancel scheduled job"))
114
+ }
115
+ }
116
+ })
117
+ }
50
118
  }
@@ -100,6 +100,36 @@ export default class VelociousJob {
100
100
  })
101
101
  }
102
102
 
103
+ /**
104
+ * Atomically replaces this job class's queued owner for a stable schedule key.
105
+ * @param {object} args - Options.
106
+ * @param {string} args.scheduleKey - Stable logical schedule key.
107
+ * @param {Array<?>} args.args - Job args.
108
+ * @param {import("./types.js").BackgroundJobOptions} [args.options] - Job options.
109
+ * @returns {Promise<import("./types.js").BackgroundJobReplacementResult>} - Replacement result.
110
+ */
111
+ static async replaceScheduled({scheduleKey, args, options}) {
112
+ const client = new BackgroundJobsClient()
113
+
114
+ return await client.replaceScheduled({
115
+ scheduleKey,
116
+ jobName: this.jobName(),
117
+ args,
118
+ options: this._withQueue(options)
119
+ })
120
+ }
121
+
122
+ /**
123
+ * Cancels or detaches the current owner of a stable schedule key.
124
+ * @param {string} scheduleKey - Stable logical schedule key.
125
+ * @returns {Promise<import("./types.js").BackgroundJobCancellationResult>} - Cancellation result.
126
+ */
127
+ static async cancelScheduled(scheduleKey) {
128
+ const client = new BackgroundJobsClient()
129
+
130
+ return await client.cancelScheduled({scheduleKey})
131
+ }
132
+
103
133
  /**
104
134
  * Runs split args and options.
105
135
  * @param {Array<?>} args - Job args.
@@ -538,6 +538,16 @@ export default class BackgroundJobsMain {
538
538
  _handleClientSocketMessage({jsonSocket, message}) {
539
539
  if (message?.type === "enqueue") {
540
540
  this._handleEnqueue({jsonSocket, message})
541
+ return
542
+ }
543
+
544
+ if (message?.type === "replace-scheduled") {
545
+ this._handleReplaceScheduled({jsonSocket, message})
546
+ return
547
+ }
548
+
549
+ if (message?.type === "cancel-scheduled") {
550
+ this._handleCancelScheduled({jsonSocket, message})
541
551
  }
542
552
  }
543
553
 
@@ -749,23 +759,99 @@ export default class BackgroundJobsMain {
749
759
  this._notifyEnqueued()
750
760
  await this._drain()
751
761
  } catch (error) {
752
- if (error instanceof VelociousError && error.safeToExpose) {
753
- jsonSocket.send({type: "enqueue-error", error: error.message})
754
- return
755
- }
756
-
757
- const normalizedError = error instanceof Error ? error : new Error(String(error))
758
- const payload = {
762
+ this._handleClientMutationError({
759
763
  context: {jobName: message.jobName, stage: "background-job-enqueue"},
760
- error: normalizedError
761
- }
762
- const errorEvents = this.configuration.getErrorEvents()
764
+ error,
765
+ fallbackMessage: "Failed to enqueue job",
766
+ jsonSocket,
767
+ logMessage: "Failed to enqueue background job:",
768
+ responseType: "enqueue-error"
769
+ })
770
+ }
771
+ }
772
+
773
+ /**
774
+ * Handles a stable-key replacement request and re-arms dispatch afterward.
775
+ * @param {object} args - Options.
776
+ * @param {JsonSocket} args.jsonSocket - JSON socket.
777
+ * @param {import("./types.js").BackgroundJobReplaceScheduledMessage} args.message - Message.
778
+ * @returns {Promise<void>} - Resolves when handled.
779
+ */
780
+ async _handleReplaceScheduled({jsonSocket, message}) {
781
+ try {
782
+ const result = await this.store.replaceScheduled({
783
+ scheduleKey: message.scheduleKey,
784
+ jobName: message.jobName,
785
+ args: message.args || [],
786
+ options: message.options || {}
787
+ })
788
+
789
+ jsonSocket.send({type: "schedule-replaced", ...result})
790
+ this._notifyEnqueued()
791
+ await this._drain()
792
+ } catch (error) {
793
+ this._handleClientMutationError({
794
+ context: {jobName: message.jobName, scheduleKey: message.scheduleKey, stage: "background-job-replace-scheduled"},
795
+ error,
796
+ fallbackMessage: "Failed to replace scheduled job",
797
+ jsonSocket,
798
+ logMessage: "Failed to replace scheduled background job:",
799
+ responseType: "replace-scheduled-error"
800
+ })
801
+ }
802
+ }
803
+
804
+ /**
805
+ * Handles a stable-key cancellation request and re-arms dispatch afterward.
806
+ * @param {object} args - Options.
807
+ * @param {JsonSocket} args.jsonSocket - JSON socket.
808
+ * @param {import("./types.js").BackgroundJobCancelScheduledMessage} args.message - Message.
809
+ * @returns {Promise<void>} - Resolves when handled.
810
+ */
811
+ async _handleCancelScheduled({jsonSocket, message}) {
812
+ try {
813
+ const result = await this.store.cancelScheduled(message.scheduleKey)
814
+
815
+ jsonSocket.send({type: "schedule-cancelled", ...result})
816
+ this._notifyEnqueued()
817
+ await this._drain()
818
+ } catch (error) {
819
+ this._handleClientMutationError({
820
+ context: {scheduleKey: message.scheduleKey, stage: "background-job-cancel-scheduled"},
821
+ error,
822
+ fallbackMessage: "Failed to cancel scheduled job",
823
+ jsonSocket,
824
+ logMessage: "Failed to cancel scheduled background job:",
825
+ responseType: "cancel-scheduled-error"
826
+ })
827
+ }
828
+ }
763
829
 
764
- this.logger.error(() => ["Failed to enqueue background job:", normalizedError])
765
- errorEvents.emit("framework-error", payload)
766
- errorEvents.emit("all-error", {...payload, errorType: "framework-error"})
767
- jsonSocket.send({type: "enqueue-error", error: "Failed to enqueue job"})
830
+ /**
831
+ * Returns safe validation failures and reports unexpected client mutations.
832
+ * @param {object} args - Options.
833
+ * @param {Record<string, ?>} args.context - Framework-error context.
834
+ * @param {?} args.error - Mutation failure.
835
+ * @param {string} args.fallbackMessage - Client-safe fallback message.
836
+ * @param {JsonSocket} args.jsonSocket - JSON socket.
837
+ * @param {string} args.logMessage - Error log prefix.
838
+ * @param {"enqueue-error" | "replace-scheduled-error" | "cancel-scheduled-error"} args.responseType - Response type.
839
+ * @returns {void}
840
+ */
841
+ _handleClientMutationError({context, error, fallbackMessage, jsonSocket, logMessage, responseType}) {
842
+ if (error instanceof VelociousError && error.safeToExpose) {
843
+ jsonSocket.send({type: responseType, error: error.message})
844
+ return
768
845
  }
846
+
847
+ const normalizedError = error instanceof Error ? error : new Error(String(error))
848
+ const payload = {context, error: normalizedError}
849
+ const errorEvents = this.configuration.getErrorEvents()
850
+
851
+ this.logger.error(() => [logMessage, normalizedError])
852
+ errorEvents.emit("framework-error", payload)
853
+ errorEvents.emit("all-error", {...payload, errorType: "framework-error"})
854
+ jsonSocket.send({type: responseType, error: fallbackMessage})
769
855
  }
770
856
 
771
857
  /**