velocious 1.0.605 → 1.0.607

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 (68) hide show
  1. package/README.md +51 -1
  2. package/build/background-jobs/job-semantics.js +145 -0
  3. package/build/background-jobs/local-adapter.js +161 -0
  4. package/build/background-jobs/local-dispatcher.js +418 -0
  5. package/build/background-jobs/local-job-registry.js +59 -0
  6. package/build/background-jobs/local-store.js +1219 -0
  7. package/build/background-jobs/main.js +6 -1
  8. package/build/background-jobs/store.js +33 -97
  9. package/build/background-jobs/types.js +26 -1
  10. package/build/configuration-types.js +3 -1
  11. package/build/configuration.js +16 -1
  12. package/build/database/drivers/sqlite/index.js +46 -41
  13. package/build/environment-handlers/browser.js +10 -0
  14. package/build/src/background-jobs/job-semantics.d.ts +65 -0
  15. package/build/src/background-jobs/job-semantics.d.ts.map +1 -0
  16. package/build/src/background-jobs/job-semantics.js +125 -0
  17. package/build/src/background-jobs/local-adapter.d.ts +161 -0
  18. package/build/src/background-jobs/local-adapter.d.ts.map +1 -0
  19. package/build/src/background-jobs/local-adapter.js +141 -0
  20. package/build/src/background-jobs/local-dispatcher.d.ts +185 -0
  21. package/build/src/background-jobs/local-dispatcher.d.ts.map +1 -0
  22. package/build/src/background-jobs/local-dispatcher.js +383 -0
  23. package/build/src/background-jobs/local-job-registry.d.ts +26 -0
  24. package/build/src/background-jobs/local-job-registry.d.ts.map +1 -0
  25. package/build/src/background-jobs/local-job-registry.js +51 -0
  26. package/build/src/background-jobs/local-store.d.ts +364 -0
  27. package/build/src/background-jobs/local-store.d.ts.map +1 -0
  28. package/build/src/background-jobs/local-store.js +1078 -0
  29. package/build/src/background-jobs/main.d.ts.map +1 -1
  30. package/build/src/background-jobs/main.js +8 -2
  31. package/build/src/background-jobs/store.d.ts +0 -9
  32. package/build/src/background-jobs/store.d.ts.map +1 -1
  33. package/build/src/background-jobs/store.js +18 -88
  34. package/build/src/background-jobs/types.d.ts +97 -2
  35. package/build/src/background-jobs/types.d.ts.map +1 -1
  36. package/build/src/background-jobs/types.js +27 -2
  37. package/build/src/configuration-types.d.ts +9 -2
  38. package/build/src/configuration-types.d.ts.map +1 -1
  39. package/build/src/configuration-types.js +4 -2
  40. package/build/src/configuration.d.ts +5 -0
  41. package/build/src/configuration.d.ts.map +1 -1
  42. package/build/src/configuration.js +15 -2
  43. package/build/src/database/drivers/sqlite/index.d.ts +8 -0
  44. package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
  45. package/build/src/database/drivers/sqlite/index.js +47 -41
  46. package/build/src/environment-handlers/browser.d.ts +9 -0
  47. package/build/src/environment-handlers/browser.d.ts.map +1 -1
  48. package/build/src/environment-handlers/browser.js +10 -1
  49. package/build/src/testing/test-runner.d.ts +42 -10
  50. package/build/src/testing/test-runner.d.ts.map +1 -1
  51. package/build/src/testing/test-runner.js +135 -35
  52. package/build/testing/test-runner.js +141 -33
  53. package/build/tsconfig.tsbuildinfo +1 -1
  54. package/package.json +1 -1
  55. package/scripts/test-browser.js +5 -1
  56. package/src/background-jobs/job-semantics.js +145 -0
  57. package/src/background-jobs/local-adapter.js +161 -0
  58. package/src/background-jobs/local-dispatcher.js +418 -0
  59. package/src/background-jobs/local-job-registry.js +59 -0
  60. package/src/background-jobs/local-store.js +1219 -0
  61. package/src/background-jobs/main.js +6 -1
  62. package/src/background-jobs/store.js +33 -97
  63. package/src/background-jobs/types.js +26 -1
  64. package/src/configuration-types.js +3 -1
  65. package/src/configuration.js +16 -1
  66. package/src/database/drivers/sqlite/index.js +46 -41
  67. package/src/environment-handlers/browser.js +10 -0
  68. package/src/testing/test-runner.js +141 -33
@@ -0,0 +1,145 @@
1
+ // @ts-check
2
+
3
+ import VelociousError from "../velocious-error.js"
4
+
5
+ export const DEFAULT_BACKGROUND_JOB_EXECUTION_MODE = "pooled"
6
+ export const DEFAULT_BACKGROUND_JOB_MAX_RETRIES = 10
7
+ export const DEFAULT_BACKGROUND_JOB_QUEUE = "default"
8
+ export const QUEUE_CONCURRENCY_KEY_PREFIX = "queue:"
9
+ /** @type {import("./types.js").BackgroundJobExecutionMode[]} */
10
+ export const BACKGROUND_JOB_EXECUTION_MODES = ["inline", "forked", "pooled", "spawned"]
11
+
12
+ /**
13
+ * Normalizes a job queue.
14
+ * @param {import("./types.js").BackgroundJobOptions} [options] - Job options.
15
+ * @returns {string} - Queue name.
16
+ */
17
+ export function normalizeBackgroundJobQueue(options) {
18
+ const queue = options?.queue
19
+
20
+ if (typeof queue === "string" && queue.trim().length > 0) return queue.trim()
21
+
22
+ return DEFAULT_BACKGROUND_JOB_QUEUE
23
+ }
24
+
25
+ /**
26
+ * Normalizes an explicit execution mode while retaining the Node default.
27
+ * @param {Omit<import("./types.js").BackgroundJobOptions, "executionMode"> & {executionMode?: string}} options - Job options.
28
+ * @param {import("./types.js").BackgroundJobExecutionMode} [defaultExecutionMode] - Default mode.
29
+ * @param {import("./types.js").BackgroundJobExecutionMode[]} [supportedExecutionModes] - Modes accepted by the caller.
30
+ * @returns {import("./types.js").BackgroundJobExecutionMode} - Execution mode.
31
+ */
32
+ export function normalizeBackgroundJobExecutionMode(options = {}, defaultExecutionMode = DEFAULT_BACKGROUND_JOB_EXECUTION_MODE, supportedExecutionModes = BACKGROUND_JOB_EXECUTION_MODES) {
33
+ if ("forked" in options) {
34
+ throw new Error("The background job `forked` option was removed; pass `executionMode` (\"inline\", \"forked\", \"pooled\", or \"spawned\") instead")
35
+ }
36
+
37
+ const requestedExecutionMode = options.executionMode || defaultExecutionMode
38
+ /** @type {import("./types.js").BackgroundJobExecutionMode | undefined} */
39
+ let executionMode
40
+
41
+ for (const candidate of BACKGROUND_JOB_EXECUTION_MODES) {
42
+ if (candidate === requestedExecutionMode) executionMode = candidate
43
+ }
44
+
45
+ if (!executionMode) throw new Error(`Invalid background job executionMode: ${requestedExecutionMode}`)
46
+
47
+ if (!supportedExecutionModes.includes(executionMode)) {
48
+ throw new Error(`Background job executionMode "${executionMode}" is not supported by the local background-jobs adapter`)
49
+ }
50
+
51
+ return executionMode
52
+ }
53
+
54
+ /**
55
+ * Validates and normalizes a retry cap.
56
+ * @param {number | null | undefined} maxRetries - Requested retry cap.
57
+ * @returns {number} - Retry cap.
58
+ */
59
+ export function normalizeBackgroundJobMaxRetries(maxRetries) {
60
+ if (typeof maxRetries === "number" && Number.isFinite(maxRetries) && maxRetries >= 0) {
61
+ return Math.floor(maxRetries)
62
+ }
63
+
64
+ return DEFAULT_BACKGROUND_JOB_MAX_RETRIES
65
+ }
66
+
67
+ /**
68
+ * Validates an enqueue eligibility timestamp.
69
+ * @param {number | undefined} scheduledAtMs - Requested timestamp.
70
+ * @param {number} defaultScheduledAtMs - Default timestamp.
71
+ * @returns {number} - Eligibility timestamp.
72
+ */
73
+ export function normalizeBackgroundJobScheduledAtMs(scheduledAtMs, defaultScheduledAtMs) {
74
+ if (scheduledAtMs === undefined) return defaultScheduledAtMs
75
+ if (Number.isSafeInteger(scheduledAtMs) && scheduledAtMs >= 0) return scheduledAtMs
76
+
77
+ throw VelociousError.safe("background job scheduledAtMs must be a non-negative safe integer")
78
+ }
79
+
80
+ /**
81
+ * Validates a reschedule delay and resolves it at persistence time.
82
+ * @param {number} delayMs - Requested delay.
83
+ * @param {number} nowMs - Persistence timestamp.
84
+ * @returns {number} - New eligibility timestamp.
85
+ */
86
+ export function rescheduledBackgroundJobAtMs(delayMs, nowMs) {
87
+ if (!Number.isSafeInteger(delayMs) || delayMs < 0) {
88
+ throw VelociousError.safe("background job reschedule delayMs must be a non-negative safe integer")
89
+ }
90
+
91
+ const scheduledAtMs = nowMs + delayMs
92
+
93
+ if (!Number.isSafeInteger(scheduledAtMs)) {
94
+ throw VelociousError.safe("background job reschedule scheduledAtMs must be a safe integer")
95
+ }
96
+
97
+ return scheduledAtMs
98
+ }
99
+
100
+ /**
101
+ * Returns the shared failure backoff.
102
+ * @param {number} retryCount - One-based failed attempt count.
103
+ * @returns {number} - Backoff in milliseconds.
104
+ */
105
+ export function retryDelayMs(retryCount) {
106
+ const scheduleSeconds = [10, 60, 600, 3600]
107
+
108
+ if (retryCount <= scheduleSeconds.length) return scheduleSeconds[retryCount - 1] * 1000
109
+
110
+ return (retryCount - 3) * 60 * 60 * 1000
111
+ }
112
+
113
+ /**
114
+ * Resolves explicit or queue-derived concurrency.
115
+ * @param {object} args - Resolution arguments.
116
+ * @param {import("./types.js").BackgroundJobOptions} args.options - Job options.
117
+ * @param {string} args.queue - Normalized queue.
118
+ * @param {Record<string, {maxConcurrent?: number, priority?: number}>} args.queues - Queue configuration.
119
+ * @returns {import("./types.js").ResolvedBackgroundJobConcurrency | null} - Concurrency contract.
120
+ */
121
+ export function normalizeBackgroundJobConcurrency({options = {}, queue, queues}) {
122
+ const key = options.concurrencyKey
123
+ const cap = options.maxConcurrency
124
+
125
+ if (key !== undefined || cap !== undefined) {
126
+ if (typeof key !== "string" || key.length === 0 || !Number.isInteger(cap) || Number(cap) <= 0) {
127
+ throw new Error("background job concurrencyKey and maxConcurrency must be paired; concurrencyKey must be non-empty and maxConcurrency must be a positive integer")
128
+ }
129
+ if (key.startsWith(QUEUE_CONCURRENCY_KEY_PREFIX)) {
130
+ throw new Error(`background job concurrencyKey must not start with the reserved "${QUEUE_CONCURRENCY_KEY_PREFIX}" prefix, which is reserved for queue-derived concurrency caps`)
131
+ }
132
+
133
+ return {concurrencyKey: key, maxConcurrency: Number(cap), queueDerived: false}
134
+ }
135
+
136
+ const queueCap = queues[queue]?.maxConcurrent
137
+
138
+ if (!Number.isInteger(queueCap) || Number(queueCap) <= 0) return null
139
+
140
+ return {
141
+ concurrencyKey: `${QUEUE_CONCURRENCY_KEY_PREFIX}${queue}`,
142
+ maxConcurrency: Number(queueCap),
143
+ queueDerived: true
144
+ }
145
+ }
@@ -0,0 +1,161 @@
1
+ // @ts-check
2
+
3
+ import BackgroundJobsAdapter from "./adapter.js"
4
+ import LocalBackgroundJobsDispatcher from "./local-dispatcher.js"
5
+ import LocalBackgroundJobRegistry from "./local-job-registry.js"
6
+ import LocalBackgroundJobsStore, {localBackgroundJobsClock} from "./local-store.js"
7
+
8
+ /** Durable local SQLite adapter with an owned in-process dispatcher. */
9
+ export default class LocalBackgroundJobsAdapter extends BackgroundJobsAdapter {
10
+ /**
11
+ * Creates a local adapter for one configuration and database.
12
+ * @param {object} args - Adapter options.
13
+ * @param {import("../configuration.js").default} args.configuration - Owning configuration.
14
+ * @param {import("./types.js").LocalBackgroundJobsClock} [args.clock] - Injectable clock.
15
+ * @param {string} [args.databaseIdentifier] - Local database identifier.
16
+ */
17
+ constructor({configuration, clock = localBackgroundJobsClock(), databaseIdentifier}) {
18
+ super()
19
+ this.clock = clock
20
+ this.configuration = configuration
21
+ this.registry = new LocalBackgroundJobRegistry({jobClasses: configuration.getBackgroundJobClasses()})
22
+ this.store = new LocalBackgroundJobsStore({
23
+ clock,
24
+ configuration,
25
+ databaseIdentifier,
26
+ onCommittedEnqueue: () => this.dispatcher.wake()
27
+ })
28
+ this.dispatcher = new LocalBackgroundJobsDispatcher({clock, configuration, registry: this.registry, store: this.store})
29
+ }
30
+
31
+ /**
32
+ * Ensures that local persistence and dispatch are ready.
33
+ * @returns {Promise<void>} - Resolves when local dispatch is ready.
34
+ */
35
+ async ensureReady() { await this.dispatcher.start() }
36
+
37
+ /**
38
+ * Stops local dispatch gracefully.
39
+ * @returns {Promise<void>} - Resolves after graceful local shutdown.
40
+ */
41
+ async close() {
42
+ await this.dispatcher.stop()
43
+ this.store.resetReadiness()
44
+ }
45
+
46
+ /**
47
+ * Reports local dispatcher health.
48
+ * @returns {Promise<import("./types.js").BackgroundJobsHealth>} - Local adapter health.
49
+ */
50
+ async health() { return {ready: this.dispatcher.isReady()} }
51
+
52
+ /**
53
+ * Reconciles configuration-derived queue concurrency caps.
54
+ * @returns {Promise<void>} - Resolves after queue cap reconciliation.
55
+ */
56
+ async reconcileQueueConcurrency() { await this.store.reconcileQueueConcurrency() }
57
+
58
+ /**
59
+ * Enqueues one statically registered local job.
60
+ * @param {{jobName: string, args: Array<ReturnType<typeof JSON.parse>>, options?: import("./types.js").BackgroundJobOptions}} args - Enqueue request.
61
+ * @returns {Promise<string>} - Durable local job id.
62
+ */
63
+ async enqueue(args) {
64
+ await this.ensureReady()
65
+ this.registry.resolve(args.jobName)
66
+ return await this.store.enqueue(args)
67
+ }
68
+
69
+ /**
70
+ * Rejects stable-key cancellation, which is outside the local adapter contract.
71
+ * @param {string} _scheduleKey - Unsupported stable key.
72
+ * @returns {Promise<import("./types.js").BackgroundJobCancellationResult>} - Never resolves.
73
+ */
74
+ async cancelScheduled(_scheduleKey) { throw new Error("cancelScheduled is not supported by the local background-jobs adapter") }
75
+
76
+ /**
77
+ * Rejects stable-key replacement, which is outside the local adapter contract.
78
+ * @param {{scheduleKey: string, jobName: string, args: Array<ReturnType<typeof JSON.parse>>, options?: import("./types.js").BackgroundJobOptions}} _args - Unsupported request.
79
+ * @returns {Promise<import("./types.js").BackgroundJobReplacementResult>} - Never resolves.
80
+ */
81
+ async replaceScheduled(_args) { throw new Error("replaceScheduled is not supported by the local background-jobs adapter") }
82
+
83
+ /**
84
+ * Finds the next eligible local job.
85
+ * @returns {Promise<import("./types.js").BackgroundJobRow | null>} - Next eligible job.
86
+ */
87
+ async nextAvailableJob() { return await this.store.nextAvailableJob() }
88
+
89
+ /**
90
+ * Finds the next future local job.
91
+ * @returns {Promise<import("./types.js").BackgroundJobRow | null>} - Next future job.
92
+ */
93
+ async nextScheduledJob() { return await this.store.nextScheduledJob() }
94
+
95
+ /**
96
+ * Finds a local job by id.
97
+ * @param {string} jobId - Job id.
98
+ * @returns {Promise<import("./types.js").BackgroundJobRow | null>} - Persisted job.
99
+ */
100
+ async getJob(jobId) { return await this.store.getJob(jobId) }
101
+
102
+ /**
103
+ * Lists local jobs in insertion order.
104
+ * @returns {Promise<import("./types.js").BackgroundJobRow[]>} - Local jobs.
105
+ */
106
+ async listJobs() { return await this.store.listJobs() }
107
+
108
+ /**
109
+ * Claims one queued local job.
110
+ * @param {{jobId: string, workerId?: string}} args - Claim request.
111
+ * @returns {Promise<import("./types.js").BackgroundJobHandoff | null>} - Handoff.
112
+ */
113
+ async markHandedOff(args) { return await this.store.markHandedOff(args) }
114
+
115
+ /**
116
+ * Finds active local handoffs owned by one worker.
117
+ * @param {{workerId: string}} args - Worker identity.
118
+ * @returns {Promise<Array<{jobId: string, handoffId: string}>>} - Active worker handoffs.
119
+ */
120
+ async handedOffJobsForWorker(args) { return await this.store.handedOffJobsForWorker(args) }
121
+
122
+ /**
123
+ * Returns an exact active local handoff to the queue.
124
+ * @param {{jobId: string, handoffId: string}} args - Handoff release.
125
+ * @returns {Promise<void>} - Resolves after the fenced release.
126
+ */
127
+ async markReturnedToQueue(args) { await this.store.markReturnedToQueue(args) }
128
+
129
+ /**
130
+ * Acknowledges successful local job completion.
131
+ * @param {{jobId: string, handoffId?: string}} args - Completion report.
132
+ * @returns {Promise<boolean>} - Whether accepted.
133
+ */
134
+ async markCompleted(args) { return await this.store.markCompleted(args) }
135
+
136
+ /**
137
+ * Acknowledges an explicit local reschedule.
138
+ * @param {{jobId: string, delayMs: number, handoffId?: string}} args - Reschedule report.
139
+ * @returns {Promise<boolean>} - Whether accepted.
140
+ */
141
+ async markRescheduled(args) { return await this.store.markRescheduled(args) }
142
+
143
+ /**
144
+ * Acknowledges a failed local performance.
145
+ * @param {{jobId: string, error: ReturnType<typeof JSON.parse>, handoffId?: string}} args - Failure report.
146
+ * @returns {Promise<import("./types.js").BackgroundJobRow | null>} - Transition.
147
+ */
148
+ async markFailed(args) { return await this.store.markFailed(args) }
149
+
150
+ /**
151
+ * Coalesces a dispatcher wake.
152
+ * @returns {void} - No return value.
153
+ */
154
+ wake() { this.dispatcher.wake() }
155
+
156
+ /**
157
+ * Waits until current local work has been acknowledged.
158
+ * @returns {Promise<void>} - Resolves after all current work is acknowledged.
159
+ */
160
+ async waitForIdle() { await this.dispatcher.waitForIdle() }
161
+ }