velocious 1.0.574 → 1.0.575
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 +17 -3
- package/build/background-jobs/client.js +71 -3
- package/build/background-jobs/job.js +30 -0
- package/build/background-jobs/main.js +100 -14
- package/build/background-jobs/store.js +344 -41
- package/build/background-jobs/types.js +25 -1
- package/build/background-jobs/web/controller.js +1 -0
- package/build/database/advisory-lock-runner.js +27 -17
- package/build/database/drivers/mssql/index.js +98 -3
- package/build/database/record/index.js +10 -8
- package/build/database/table-data/table-column.js +2 -0
- package/build/frontend-models/resource-definition.js +1 -138
- package/build/src/background-jobs/client.d.ts +30 -0
- package/build/src/background-jobs/client.d.ts.map +1 -1
- package/build/src/background-jobs/client.js +64 -4
- package/build/src/background-jobs/job.d.ts +19 -0
- package/build/src/background-jobs/job.d.ts.map +1 -1
- package/build/src/background-jobs/job.js +27 -1
- package/build/src/background-jobs/main.d.ts +41 -0
- package/build/src/background-jobs/main.d.ts.map +1 -1
- package/build/src/background-jobs/main.js +95 -14
- package/build/src/background-jobs/store.d.ts +136 -0
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +314 -42
- package/build/src/background-jobs/types.d.ts +86 -2
- package/build/src/background-jobs/types.d.ts.map +1 -1
- package/build/src/background-jobs/types.js +26 -2
- package/build/src/background-jobs/web/controller.d.ts.map +1 -1
- package/build/src/background-jobs/web/controller.js +2 -1
- package/build/src/database/advisory-lock-runner.d.ts +27 -14
- package/build/src/database/advisory-lock-runner.d.ts.map +1 -1
- package/build/src/database/advisory-lock-runner.js +27 -18
- package/build/src/database/drivers/mssql/index.d.ts +32 -0
- package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/index.js +86 -4
- package/build/src/database/record/index.d.ts +12 -8
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +11 -9
- package/build/src/database/table-data/table-column.d.ts.map +1 -1
- package/build/src/database/table-data/table-column.js +4 -1
- package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
- package/build/src/frontend-models/resource-definition.js +2 -124
- package/build/src/sync/local-mutation-log.d.ts +7 -1
- package/build/src/sync/local-mutation-log.d.ts.map +1 -1
- package/build/src/sync/local-mutation-log.js +18 -7
- package/build/src/sync/peer-mutation-bundle.d.ts.map +1 -1
- package/build/src/sync/peer-mutation-bundle.js +9 -7
- package/build/src/sync/signed-sync-envelope-replay-service.d.ts.map +1 -1
- package/build/src/sync/signed-sync-envelope-replay-service.js +3 -1
- package/build/src/sync/sync-envelope-replay-service.d.ts +69 -0
- package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
- package/build/src/sync/sync-envelope-replay-service.js +179 -18
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +17 -22
- package/build/src/utils/sha256-hex.d.ts +10 -0
- package/build/src/utils/sha256-hex.d.ts.map +1 -0
- package/build/src/utils/sha256-hex.js +126 -0
- package/build/sync/local-mutation-log.js +19 -5
- package/build/sync/peer-mutation-bundle.js +8 -6
- package/build/sync/signed-sync-envelope-replay-service.js +2 -0
- package/build/sync/sync-envelope-replay-service.js +196 -17
- package/build/testing/test-runner.js +17 -22
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/utils/sha256-hex.js +141 -0
- package/package.json +1 -1
- package/src/background-jobs/client.js +71 -3
- package/src/background-jobs/job.js +30 -0
- package/src/background-jobs/main.js +100 -14
- package/src/background-jobs/store.js +344 -41
- package/src/background-jobs/types.js +25 -1
- package/src/background-jobs/web/controller.js +1 -0
- package/src/database/advisory-lock-runner.js +27 -17
- package/src/database/drivers/mssql/index.js +98 -3
- package/src/database/record/index.js +10 -8
- package/src/database/table-data/table-column.js +2 -0
- package/src/frontend-models/resource-definition.js +1 -138
- package/src/sync/local-mutation-log.js +19 -5
- package/src/sync/peer-mutation-bundle.js +8 -6
- package/src/sync/signed-sync-envelope-replay-service.js +2 -0
- package/src/sync/sync-envelope-replay-service.js +196 -17
- package/src/testing/test-runner.js +17 -22
- 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/
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
/**
|