velocious 1.0.599 → 1.0.601

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 (95) hide show
  1. package/README.md +18 -9
  2. package/build/background-jobs/adapter-client.js +45 -0
  3. package/build/background-jobs/adapter.js +139 -0
  4. package/build/background-jobs/job-registry.js +1 -1
  5. package/build/background-jobs/job.js +20 -129
  6. package/build/background-jobs/main.js +71 -28
  7. package/build/background-jobs/perform-job.js +23 -0
  8. package/build/background-jobs/platform-job.js +156 -0
  9. package/build/background-jobs/runtime.js +156 -0
  10. package/build/background-jobs/sql-adapter.js +20 -0
  11. package/build/background-jobs/store.js +3 -1
  12. package/build/background-jobs/types.js +10 -0
  13. package/build/background-jobs/web/controller.js +5 -2
  14. package/build/background-jobs/worker.js +6 -8
  15. package/build/configuration-types.js +7 -2
  16. package/build/configuration.js +224 -45
  17. package/build/environment-handlers/base.js +20 -0
  18. package/build/environment-handlers/node.js +27 -20
  19. package/build/frontend-model-controller.js +16 -20
  20. package/build/jobs/prune-terminal-background-jobs.js +2 -3
  21. package/build/src/background-jobs/adapter-client.d.ts +41 -0
  22. package/build/src/background-jobs/adapter-client.d.ts.map +1 -0
  23. package/build/src/background-jobs/adapter-client.js +39 -0
  24. package/build/src/background-jobs/adapter.d.ts +165 -0
  25. package/build/src/background-jobs/adapter.d.ts.map +1 -0
  26. package/build/src/background-jobs/adapter.js +121 -0
  27. package/build/src/background-jobs/job-registry.d.ts +1 -1
  28. package/build/src/background-jobs/job-registry.d.ts.map +1 -1
  29. package/build/src/background-jobs/job-registry.js +2 -2
  30. package/build/src/background-jobs/job.d.ts +6 -69
  31. package/build/src/background-jobs/job.d.ts.map +1 -1
  32. package/build/src/background-jobs/job.js +17 -116
  33. package/build/src/background-jobs/main.d.ts +13 -3
  34. package/build/src/background-jobs/main.d.ts.map +1 -1
  35. package/build/src/background-jobs/main.js +66 -30
  36. package/build/src/background-jobs/perform-job.d.ts +16 -0
  37. package/build/src/background-jobs/perform-job.d.ts.map +1 -0
  38. package/build/src/background-jobs/perform-job.js +22 -0
  39. package/build/src/background-jobs/platform-job.d.ts +110 -0
  40. package/build/src/background-jobs/platform-job.d.ts.map +1 -0
  41. package/build/src/background-jobs/platform-job.js +138 -0
  42. package/build/src/background-jobs/runtime.d.ts +78 -0
  43. package/build/src/background-jobs/runtime.d.ts.map +1 -0
  44. package/build/src/background-jobs/runtime.js +132 -0
  45. package/build/src/background-jobs/sql-adapter.d.ts +13 -0
  46. package/build/src/background-jobs/sql-adapter.d.ts.map +1 -0
  47. package/build/src/background-jobs/sql-adapter.js +18 -0
  48. package/build/src/background-jobs/store.d.ts +2 -1
  49. package/build/src/background-jobs/store.d.ts.map +1 -1
  50. package/build/src/background-jobs/store.js +4 -2
  51. package/build/src/background-jobs/types.d.ts +41 -0
  52. package/build/src/background-jobs/types.d.ts.map +1 -1
  53. package/build/src/background-jobs/types.js +11 -1
  54. package/build/src/background-jobs/web/controller.d.ts.map +1 -1
  55. package/build/src/background-jobs/web/controller.js +5 -3
  56. package/build/src/background-jobs/worker.d.ts.map +1 -1
  57. package/build/src/background-jobs/worker.js +7 -8
  58. package/build/src/configuration-types.d.ts +20 -4
  59. package/build/src/configuration-types.d.ts.map +1 -1
  60. package/build/src/configuration-types.js +7 -3
  61. package/build/src/configuration.d.ts +53 -4
  62. package/build/src/configuration.d.ts.map +1 -1
  63. package/build/src/configuration.js +209 -44
  64. package/build/src/environment-handlers/base.d.ts +17 -0
  65. package/build/src/environment-handlers/base.d.ts.map +1 -1
  66. package/build/src/environment-handlers/base.js +19 -1
  67. package/build/src/environment-handlers/node.d.ts +18 -0
  68. package/build/src/environment-handlers/node.d.ts.map +1 -1
  69. package/build/src/environment-handlers/node.js +25 -20
  70. package/build/src/frontend-model-controller.d.ts +1 -4
  71. package/build/src/frontend-model-controller.d.ts.map +1 -1
  72. package/build/src/frontend-model-controller.js +16 -20
  73. package/build/src/jobs/prune-terminal-background-jobs.d.ts.map +1 -1
  74. package/build/src/jobs/prune-terminal-background-jobs.js +3 -4
  75. package/build/tsconfig.tsbuildinfo +1 -1
  76. package/package.json +4 -4
  77. package/src/background-jobs/adapter-client.js +45 -0
  78. package/src/background-jobs/adapter.js +139 -0
  79. package/src/background-jobs/job-registry.js +1 -1
  80. package/src/background-jobs/job.js +20 -129
  81. package/src/background-jobs/main.js +71 -28
  82. package/src/background-jobs/perform-job.js +23 -0
  83. package/src/background-jobs/platform-job.js +156 -0
  84. package/src/background-jobs/runtime.js +156 -0
  85. package/src/background-jobs/sql-adapter.js +20 -0
  86. package/src/background-jobs/store.js +3 -1
  87. package/src/background-jobs/types.js +10 -0
  88. package/src/background-jobs/web/controller.js +5 -2
  89. package/src/background-jobs/worker.js +6 -8
  90. package/src/configuration-types.js +7 -2
  91. package/src/configuration.js +224 -45
  92. package/src/environment-handlers/base.js +20 -0
  93. package/src/environment-handlers/node.js +27 -20
  94. package/src/frontend-model-controller.js +16 -20
  95. package/src/jobs/prune-terminal-background-jobs.js +2 -3
package/README.md CHANGED
@@ -775,22 +775,20 @@ Use `await FrontendModelBase.waitForIdle()` when a test harness or app lifecycle
775
775
 
776
776
  Frontend-model HTTP requests always use `credentials: "include"` so shared custom commands can set session cookies without app-level transport overrides.
777
777
 
778
- Unexpected frontend-model endpoint failures return their original message by default with `errorType: "internal_error"` and a server-generated `correlationId` shared with the matching framework-error report. Set `secureFrontendModelErrors: true` to return only explicitly safe messages and otherwise `errorMessage: "Request failed."`. Expected application failures can use `VelociousError.safe(message, {errorType, details, code})`; generated frontend-model callers preserve the server's safe error fields. See [docs/frontend-models.md](docs/frontend-models.md#error-payloads).
778
+ Unexpected frontend-model endpoint failures return their original message and full stack trace by default in every environment, including production. Responses use `errorType: "internal_error"`, a server-generated `correlationId` shared with the matching framework-error report, and the established `debugErrorClass`, `debugErrorMessage`, and `debugBacktrace` fields. Expected application failures can use `VelociousError.safe(message, {errorType, details, code})`; generated frontend-model callers preserve the server's safe error fields without adding irrelevant debug fields. See [docs/frontend-models.md](docs/frontend-models.md#error-payloads).
779
779
  Invalid client query descriptors, such as unknown `select`, `where`, `search`, `joins`, `preload`, `group`, `sort`, `pluck`, or Ransack attributes, return the specific frontend-model query error message with `velocious.code: "frontend-model-query-error"` and are not emitted as framework errors.
780
780
  Invalid frontend-model write attributes and attachment names, including attributes rejected by `permittedParams()`, return the specific safe error message with `velocious.code: "frontend-model-attribute-error"` and are not emitted as framework errors.
781
- In `development` and `test`, Velocious also includes `debugErrorClass`, `debugErrorMessage`, and `debugBacktrace` fields so browser/system-test failures are easier to diagnose without exposing those details in production.
782
- Other non-production environments, such as `staging`, keep the same client-safe default unless you explicitly opt in with `exposeInternalErrorsToClients: true`:
781
+ To mask unexpected internal details, explicitly opt out for the application configuration:
783
782
 
784
783
  ```js
785
784
  const configuration = new Configuration({
786
- environment: "staging",
787
- exposeInternalErrorsToClients: true
785
+ exposeInternalErrorsToClients: false
788
786
  })
789
787
  ```
790
788
 
791
- This opt-in is ignored in `production`; production frontend-model responses never include internal exception details.
789
+ With this opt-out, built-in commands, custom commands, and sync replay failures return `errorMessage: "Request failed."` and omit the debug message and stack fields in every environment. `secureFrontendModelErrors: true` remains a deprecated compatibility alias when `exposeInternalErrorsToClients` is omitted; an explicit `exposeInternalErrorsToClients` value always wins.
792
790
 
793
- Backends can append client-safe metadata to frontend-model error responses with `configuration.addClientErrorPayloadReporter(...)`. Reporters receive the caught `error`, the current `request`, a safe `requestDetails` snapshot, and a small `context` object, and should only return fields that are safe for clients to see. Frontend-model endpoint failures include `context.frontendModelEndpoint`, `action`, `commandType`, `model`, `requestId`, and `expectedError`. This is useful for attaching an error-reporting URL while keeping the normal production error message generic:
791
+ Backends can append client-safe metadata to frontend-model error responses with `configuration.addClientErrorPayloadReporter(...)`. Reporters receive the caught `error`, the current `request`, a safe `requestDetails` snapshot, and a small `context` object, and should only return fields that are safe for clients to see. Frontend-model endpoint failures include `context.frontendModelEndpoint`, `action`, `commandType`, `model`, `requestId`, and `expectedError`. When exposure is disabled, Velocious strips the established debug fields even if a reporter supplies them. This is useful for attaching an error-reporting URL while keeping an opted-out error message generic:
794
792
 
795
793
  ```js
796
794
  configuration.addClientErrorPayloadReporter(async ({error, requestDetails, context}) => {
@@ -1919,7 +1917,7 @@ configuration.getErrorEvents().on("all-error", ({error, errorType}) => {
1919
1917
  })
1920
1918
  ```
1921
1919
 
1922
- Genuinely unexpected frontend-model command failures reach this bus too. The frontend-model controller catches them to return an `internal_error` response with the original message by default (or `Request failed.` when `secureFrontendModelErrors` is enabled) and a correlation ID, then emits them as `framework-error`/`all-error` with the same correlation ID and `context.frontendModelEndpoint === true`. Expected user-flow errors are excluded: validation failures are forwarded with their real message (for example `Name can't be blank`), invalid client query descriptors are returned as frontend-model query errors, and `error.velocious`-annotated / `safeToExpose` errors keep their expected-error status. A raw `errorType` property alone is not considered safe and does not suppress reporting.
1920
+ Genuinely unexpected frontend-model command failures reach this bus too. The frontend-model controller catches them to return an `internal_error` response with the original message and stack trace by default (or `Request failed.` without debug fields when `exposeInternalErrorsToClients: false`) and a correlation ID, then emits them as `framework-error`/`all-error` with the same correlation ID and `context.frontendModelEndpoint === true`. Expected user-flow errors are excluded: validation failures are forwarded with their real message (for example `Name can't be blank`), invalid client query descriptors are returned as frontend-model query errors, and `error.velocious`-annotated / `safeToExpose` errors keep their expected-error status without irrelevant debug fields. A raw `errorType` property alone is not considered safe and does not suppress reporting.
1923
1921
 
1924
1922
  Unexpected inbound decoded WebSocket dispatch failures emit one `framework-error` and one matching `all-error`. Established expected client-flow errors remain excluded from both events.
1925
1923
 
@@ -2264,6 +2262,7 @@ You can configure the main host/port in your configuration:
2264
2262
  export default new Configuration({
2265
2263
  // ...
2266
2264
  backgroundJobs: {
2265
+ mode: "background",
2267
2266
  host: "127.0.0.1",
2268
2267
  port: 7331,
2269
2268
  databaseIdentifier: "default",
@@ -2280,6 +2279,16 @@ export default new Configuration({
2280
2279
  })
2281
2280
  ```
2282
2281
 
2282
+ `backgroundJobs.mode` is separate from a job's `executionMode`. The default
2283
+ `"background"` mode preserves the Node SQL queue, TCP main/worker transport, and
2284
+ per-job `"pooled"` execution default. `"inline"` is a platform-neutral,
2285
+ non-durable application mode: `performLater` performs immediately and rejects
2286
+ queue/scheduling/retry/execution options whose guarantees require durable state.
2287
+ Custom persistence can be supplied as a `BackgroundJobsAdapter` instance or
2288
+ synchronous factory. See [runtime modes and adapters](docs/background-jobs.md#runtime-modes-and-adapters)
2289
+ for the contract, lifecycle, Node TCP/wake behavior, the explicit
2290
+ `platform-job.js` browser/Expo entry, and SQL-only compatibility boundaries.
2291
+
2283
2292
  Or via env vars:
2284
2293
 
2285
2294
  ```
@@ -2357,7 +2366,7 @@ Queue a job:
2357
2366
  await MyJob.performLater("a", "b")
2358
2367
  ```
2359
2368
 
2360
- Jobs use `executionMode: "forked"` by default. This runs the job in a separate attached Node child process. To run inline:
2369
+ Durably queued jobs use `executionMode: "pooled"` by default. To run one inside the worker process instead:
2361
2370
 
2362
2371
  ```js
2363
2372
  await MyJob.performLaterWithOptions({
@@ -0,0 +1,45 @@
1
+ // @ts-check
2
+
3
+ /** Platform-neutral producer client for a configured adapter. */
4
+ export default class BackgroundJobsAdapterClient {
5
+ /**
6
+ * Creates an adapter-backed producer.
7
+ * @param {{configuration: import("../configuration.js").default}} args - Client options.
8
+ */
9
+ constructor({configuration}) {
10
+ this.configuration = configuration
11
+ }
12
+
13
+ /**
14
+ * Enqueues a job through the configured adapter.
15
+ * @param {{jobName: string, args: Array<ReturnType<typeof JSON.parse>>, options?: import("./types.js").BackgroundJobOptions}} args - Job request.
16
+ * @returns {Promise<string>} - Job id.
17
+ */
18
+ async enqueue(args) {
19
+ const adapter = await this.configuration.acquireReadyBackgroundJobsAdapter()
20
+
21
+ return await adapter.enqueue(args)
22
+ }
23
+
24
+ /**
25
+ * Replaces a stable schedule through the configured adapter.
26
+ * @param {{scheduleKey: string, jobName: string, args: Array<ReturnType<typeof JSON.parse>>, options?: import("./types.js").BackgroundJobOptions}} args - Replacement request.
27
+ * @returns {Promise<import("./types.js").BackgroundJobReplacementResult>} - Replacement result.
28
+ */
29
+ async replaceScheduled(args) {
30
+ const adapter = await this.configuration.acquireReadyBackgroundJobsAdapter()
31
+
32
+ return await adapter.replaceScheduled(args)
33
+ }
34
+
35
+ /**
36
+ * Cancels a stable schedule through the configured adapter.
37
+ * @param {{scheduleKey: string}} args - Cancellation request.
38
+ * @returns {Promise<import("./types.js").BackgroundJobCancellationResult>} - Cancellation result.
39
+ */
40
+ async cancelScheduled({scheduleKey}) {
41
+ const adapter = await this.configuration.acquireReadyBackgroundJobsAdapter()
42
+
43
+ return await adapter.cancelScheduled(scheduleKey)
44
+ }
45
+ }
@@ -0,0 +1,139 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Platform-neutral persistence and lifecycle contract used by the background-jobs
5
+ * runtime. Adapters own durable queue state; transport and job execution remain
6
+ * separate concerns.
7
+ */
8
+ export default class BackgroundJobsAdapter {
9
+ /**
10
+ * Ensures the adapter can accept work.
11
+ * @returns {Promise<void>} - Resolves when ready.
12
+ */
13
+ async ensureReady() { throw new Error("BackgroundJobsAdapter#ensureReady is not implemented") }
14
+
15
+ /**
16
+ * Closes adapter-owned resources.
17
+ * @returns {Promise<void>} - Resolves after close.
18
+ */
19
+ async close() {}
20
+
21
+ /**
22
+ * Reports adapter health.
23
+ * @returns {Promise<import("./types.js").BackgroundJobsHealth>} - Adapter health.
24
+ */
25
+ async health() {
26
+ return {ready: true}
27
+ }
28
+
29
+ /**
30
+ * Ensures framework-owned persistence during a migration lifecycle. Non-SQL
31
+ * adapters may leave this as a no-op.
32
+ * @param {{dbs: Record<string, import("../database/drivers/base.js").default>}} _args - Migrated databases.
33
+ * @returns {Promise<void>} - Resolves when complete.
34
+ */
35
+ async ensureFrameworkSchema(_args) {}
36
+
37
+ /**
38
+ * Reconciles configured queue limits.
39
+ * @returns {Promise<void>} - Resolves after reconciliation.
40
+ */
41
+ async reconcileQueueConcurrency() { throw new Error("BackgroundJobsAdapter#reconcileQueueConcurrency is not implemented") }
42
+
43
+ /**
44
+ * Enqueues a job.
45
+ * @param {{jobName: string, args: Array<ReturnType<typeof JSON.parse>>, options?: import("./types.js").BackgroundJobOptions}} _args - Job request.
46
+ * @returns {Promise<string>} - Job id.
47
+ */
48
+ async enqueue(_args) { throw new Error("BackgroundJobsAdapter#enqueue is not implemented") }
49
+
50
+ /**
51
+ * Replaces the owner of a stable schedule key.
52
+ * @param {{scheduleKey: string, jobName: string, args: Array<ReturnType<typeof JSON.parse>>, options?: import("./types.js").BackgroundJobOptions}} _args - Replacement request.
53
+ * @returns {Promise<import("./types.js").BackgroundJobReplacementResult>} - Replacement result.
54
+ */
55
+ async replaceScheduled(_args) { throw new Error("BackgroundJobsAdapter#replaceScheduled is not implemented") }
56
+
57
+ /**
58
+ * Cancels the owner of a stable schedule key.
59
+ * @param {string} _scheduleKey - Stable schedule key.
60
+ * @returns {Promise<import("./types.js").BackgroundJobCancellationResult>} - Cancellation result.
61
+ */
62
+ async cancelScheduled(_scheduleKey) { throw new Error("BackgroundJobsAdapter#cancelScheduled is not implemented") }
63
+
64
+ /**
65
+ * Finds the next eligible job.
66
+ * @param {{executionMode?: import("./types.js").BackgroundJobExecutionMode | import("./types.js").BackgroundJobExecutionMode[]}} [_args] - Dequeue filters.
67
+ * @returns {Promise<import("./types.js").BackgroundJobRow | null>} - Next eligible job.
68
+ */
69
+ async nextAvailableJob(_args = {}) { throw new Error("BackgroundJobsAdapter#nextAvailableJob is not implemented") }
70
+
71
+ /**
72
+ * Finds the soonest future job.
73
+ * @returns {Promise<import("./types.js").BackgroundJobRow | null>} - Soonest future job.
74
+ */
75
+ async nextScheduledJob() { throw new Error("BackgroundJobsAdapter#nextScheduledJob is not implemented") }
76
+
77
+ /**
78
+ * Reads one job.
79
+ * @param {string} _jobId - Job id.
80
+ * @returns {Promise<import("./types.js").BackgroundJobRow | null>} - Job row.
81
+ */
82
+ async getJob(_jobId) { throw new Error("BackgroundJobsAdapter#getJob is not implemented") }
83
+
84
+ /**
85
+ * Starts a job by claiming its durable handoff.
86
+ * @param {{jobId: string, workerId?: string}} _args - Handoff request.
87
+ * @returns {Promise<import("./types.js").BackgroundJobHandoff | null>} - Claimed handoff.
88
+ */
89
+ async markHandedOff(_args) { throw new Error("BackgroundJobsAdapter#markHandedOff is not implemented") }
90
+
91
+ /**
92
+ * Marks a handed-off job successful.
93
+ * @param {{jobId: string, handoffId?: string, workerId?: string, handedOffAtMs?: number}} _args - Completion report.
94
+ * @returns {Promise<boolean>} - Whether the fenced report was accepted.
95
+ */
96
+ async markCompleted(_args) { throw new Error("BackgroundJobsAdapter#markCompleted is not implemented") }
97
+
98
+ /**
99
+ * Returns a handed-off job to its schedule.
100
+ * @param {{jobId: string, delayMs: number, handoffId?: string, workerId?: string, handedOffAtMs?: number}} _args - Reschedule report.
101
+ * @returns {Promise<boolean>} - Whether the fenced report was accepted.
102
+ */
103
+ async markRescheduled(_args) { throw new Error("BackgroundJobsAdapter#markRescheduled is not implemented") }
104
+
105
+ /**
106
+ * Returns a handed-off job to the queue.
107
+ * @param {{jobId: string, handoffId: string}} _args - Handoff release.
108
+ * @returns {Promise<void>} - Resolves after the job is returned.
109
+ */
110
+ async markReturnedToQueue(_args) { throw new Error("BackgroundJobsAdapter#markReturnedToQueue is not implemented") }
111
+
112
+ /**
113
+ * Finds active handoffs for a worker.
114
+ * @param {{workerId: string}} _args - Worker identity.
115
+ * @returns {Promise<Array<{jobId: string, handoffId: string}>>} - Active worker handoffs.
116
+ */
117
+ async handedOffJobsForWorker(_args) { throw new Error("BackgroundJobsAdapter#handedOffJobsForWorker is not implemented") }
118
+
119
+ /**
120
+ * Marks a handed-off job failed or retryable.
121
+ * @param {{jobId: string, error: ReturnType<typeof JSON.parse>, handoffId?: string, workerId?: string, handedOffAtMs?: number}} _args - Failure report.
122
+ * @returns {Promise<import("./types.js").BackgroundJobRow | null>} - Updated job when accepted.
123
+ */
124
+ async markFailed(_args) { throw new Error("BackgroundJobsAdapter#markFailed is not implemented") }
125
+
126
+ /**
127
+ * Reclaims expired handoffs.
128
+ * @param {{orphanedAfterMs?: number}} [_args] - Sweep options.
129
+ * @returns {Promise<import("./types.js").BackgroundJobRow[]>} - Newly orphaned jobs.
130
+ */
131
+ async markOrphanedJobs(_args = {}) { throw new Error("BackgroundJobsAdapter#markOrphanedJobs is not implemented") }
132
+
133
+ /**
134
+ * Prunes terminal jobs past their retention windows.
135
+ * @param {{completedTtlMs?: number | null, failedTtlMs?: number | null, batchSize?: number}} [_args] - Retention options.
136
+ * @returns {Promise<number>} - Deleted rows.
137
+ */
138
+ async pruneTerminalJobs(_args = {}) { throw new Error("BackgroundJobsAdapter#pruneTerminalJobs is not implemented") }
139
+ }
@@ -3,7 +3,7 @@
3
3
  import fs from "fs/promises"
4
4
  import path from "path"
5
5
  import toImportSpecifier from "../utils/to-import-specifier.js"
6
- import VelociousJob from "./job.js"
6
+ import VelociousJob from "./platform-job.js"
7
7
 
8
8
  export default class BackgroundJobRegistry {
9
9
  /**
@@ -1,101 +1,31 @@
1
1
  // @ts-check
2
2
 
3
- import BackgroundJobsClient from "./client.js"
4
- import BackgroundJobRescheduleSignal from "./reschedule-signal.js"
3
+ import configurationResolver from "../configuration-resolver.js"
4
+ import PlatformVelociousJob from "./platform-job.js"
5
+ import {
6
+ cancelScheduledBackgroundJobForConfiguration,
7
+ enqueueBackgroundJobForConfiguration,
8
+ replaceScheduledBackgroundJobForConfiguration
9
+ } from "./runtime.js"
5
10
 
6
11
  /**
7
- * Base class for background jobs.
8
- *
9
- * `TArgs` is the tuple of arguments the subclass's `perform` accepts, so a job that
10
- * needs arguments declares them as required and typed — for example
11
- * `class RunBuildJob extends VelociousJob<[string]>` with `async perform(buildId)`.
12
- * The default empty tuple keeps argument-less jobs (`extends VelociousJob`,
13
- * `async perform()`) working unchanged.
12
+ * Node background-job entry. It preserves lazy configuration discovery for
13
+ * fresh producer processes while the explicit platform entry stays free of
14
+ * Node-only configuration resolution.
14
15
  * @template {Array<ReturnType<typeof JSON.parse>>} [TArgs=[]]
16
+ * @augments {PlatformVelociousJob<TArgs>}
15
17
  */
16
- export default class VelociousJob {
17
- /**
18
- * Database identifiers checked out while this job performs. Set an explicit
19
- * list to avoid holding unrelated configured database connections, or `[]`
20
- * when the job establishes any connections it needs itself. Left undefined,
21
- * jobs retain the existing behavior of checking out every active database.
22
- * @type {string[] | undefined}
23
- */
24
- static databaseIdentifiers = undefined
25
-
26
- /**
27
- * Queue this job class runs on. Subclasses set e.g. `static queue = "builds"`
28
- * to route onto a queue with its own cluster-wide concurrency cap (configured
29
- * via `backgroundJobs.queues`). The `{queue}` enqueue option overrides it.
30
- * Left undefined, jobs run on the `"default"` queue.
31
- * @type {string | undefined}
32
- */
33
- static queue = undefined
34
-
35
- /**
36
- * Optional process title shown for the runner while this job executes.
37
- * Velocious sets `process.title` to this for the duration of the job — so
38
- * `ps`/`top`/`htop` identify what a runner is doing — and restores the
39
- * runner's base title when the job finishes. Left undefined, the runner falls
40
- * back to `velocious job-runner: <JobName>`. Set e.g.
41
- * `static processTitle = "velocious media transcoder"` to give a job a
42
- * custom, human-readable title.
43
- * @type {string | undefined}
44
- */
45
- static processTitle = undefined
46
-
47
- /**
48
- * Stops this performance and reschedules the same logical job row. This is
49
- * normal control flow: it does not count as a failure or consume a retry.
50
- * @param {number} delayMs - Non-negative safe-integer delay in milliseconds.
51
- * @returns {never} - This method never returns.
52
- */
53
- rescheduleIn(delayMs) {
54
- if (!Number.isSafeInteger(delayMs) || delayMs < 0) {
55
- throw new TypeError("background job reschedule delayMs must be a non-negative safe integer")
56
- }
57
-
58
- throw new BackgroundJobRescheduleSignal(delayMs)
59
- }
60
-
61
- /**
62
- * Runs job name.
63
- * @returns {string} - Job name.
64
- */
65
- static jobName() {
66
- return this.name
67
- }
68
-
69
- /**
70
- * Folds this job class's static `queue` into the enqueue options unless the
71
- * caller already specified one.
72
- * @param {import("./types.js").BackgroundJobOptions | undefined} options - Job options.
73
- * @returns {import("./types.js").BackgroundJobOptions} - Options including the resolved queue.
74
- */
75
- static _withQueue(options) {
76
- const merged = options ? {...options} : {}
77
-
78
- if (merged.queue === undefined && typeof this.queue === "string" && this.queue.length > 0) {
79
- merged.queue = this.queue
80
- }
81
-
82
- return merged
83
- }
84
-
18
+ export default class VelociousJob extends PlatformVelociousJob {
85
19
  /**
86
20
  * Runs perform later.
87
21
  * @param {...ReturnType<typeof JSON.parse>} args - Job args.
88
22
  * @returns {Promise<string>} - Job id.
89
23
  */
90
24
  static async performLater(...args) {
25
+ const configuration = await configurationResolver()
91
26
  const {jobArgs, jobOptions} = this._splitArgsAndOptions(args)
92
- const client = new BackgroundJobsClient()
93
27
 
94
- return await client.enqueue({
95
- jobName: this.jobName(),
96
- args: jobArgs,
97
- options: this._withQueue(jobOptions)
98
- })
28
+ return await enqueueBackgroundJobForConfiguration({configuration, JobClass: this, jobArgs, jobOptions})
99
29
  }
100
30
 
101
31
  /**
@@ -106,13 +36,9 @@ export default class VelociousJob {
106
36
  * @returns {Promise<string>} - Job id.
107
37
  */
108
38
  static async performLaterWithOptions({args, options}) {
109
- const client = new BackgroundJobsClient()
39
+ const configuration = await configurationResolver()
110
40
 
111
- return await client.enqueue({
112
- jobName: this.jobName(),
113
- args,
114
- options: this._withQueue(options)
115
- })
41
+ return await enqueueBackgroundJobForConfiguration({configuration, JobClass: this, jobArgs: args, jobOptions: options})
116
42
  }
117
43
 
118
44
  /**
@@ -124,14 +50,9 @@ export default class VelociousJob {
124
50
  * @returns {Promise<import("./types.js").BackgroundJobReplacementResult>} - Replacement result.
125
51
  */
126
52
  static async replaceScheduled({scheduleKey, args, options}) {
127
- const client = new BackgroundJobsClient()
53
+ const configuration = await configurationResolver()
128
54
 
129
- return await client.replaceScheduled({
130
- scheduleKey,
131
- jobName: this.jobName(),
132
- args,
133
- options: this._withQueue(options)
134
- })
55
+ return await replaceScheduledBackgroundJobForConfiguration({configuration, JobClass: this, scheduleKey, jobArgs: args, jobOptions: options})
135
56
  }
136
57
 
137
58
  /**
@@ -140,38 +61,8 @@ export default class VelociousJob {
140
61
  * @returns {Promise<import("./types.js").BackgroundJobCancellationResult>} - Cancellation result.
141
62
  */
142
63
  static async cancelScheduled(scheduleKey) {
143
- const client = new BackgroundJobsClient()
144
-
145
- return await client.cancelScheduled({scheduleKey})
146
- }
64
+ const configuration = await configurationResolver()
147
65
 
148
- /**
149
- * Runs split args and options.
150
- * @param {Array<ReturnType<typeof JSON.parse>>} args - Job args.
151
- * @returns {{jobArgs: Array<ReturnType<typeof JSON.parse>>, jobOptions: import("./types.js").BackgroundJobOptions}} - Split args and options.
152
- */
153
- static _splitArgsAndOptions(args) {
154
- if (args.length === 0) {
155
- return {jobArgs: [], jobOptions: {}}
156
- }
157
-
158
- const lastArg = args[args.length - 1]
159
- const isOptionsArg = lastArg && typeof lastArg === "object" && !Array.isArray(lastArg) && "jobOptions" in lastArg
160
-
161
- if (isOptionsArg) {
162
- const {jobOptions} = /** @type {{jobOptions: import("./types.js").BackgroundJobOptions}} */ (lastArg)
163
- return {jobArgs: args.slice(0, -1), jobOptions: jobOptions || {}}
164
- }
165
-
166
- return {jobArgs: args, jobOptions: {}}
167
- }
168
-
169
- /**
170
- * Override in subclasses.
171
- * @param {TArgs} _args - Job args (the tuple this job class was parameterized with).
172
- * @returns {Promise<void>} - Resolves when complete.
173
- */
174
- async perform(..._args) {
175
- throw new Error("perform not implemented")
66
+ return await cancelScheduledBackgroundJobForConfiguration({configuration, scheduleKey})
176
67
  }
177
68
  }
@@ -3,7 +3,6 @@
3
3
  import net from "net"
4
4
  import JsonSocket from "./json-socket.js"
5
5
  import BackgroundJobsScheduler from "./scheduler.js"
6
- import BackgroundJobsStore from "./store.js"
7
6
  import Logger from "../logger.js"
8
7
  import PruneTerminalBackgroundJobsJob from "../jobs/prune-terminal-background-jobs.js"
9
8
  import VelociousError from "../velocious-error.js"
@@ -78,7 +77,8 @@ export default class BackgroundJobsMain {
78
77
  // long is treated as wedged/dead: its leases are released and it is dropped.
79
78
  this.workerStaleTimeoutMs = typeof workerStaleTimeoutMs === "number" && workerStaleTimeoutMs >= 1 ? workerStaleTimeoutMs : WORKER_STALE_TIMEOUT_MS
80
79
  this.workerLivenessSweepMs = typeof workerLivenessSweepMs === "number" && workerLivenessSweepMs >= 1 ? workerLivenessSweepMs : WORKER_LIVENESS_SWEEP_MS
81
- this.store = new BackgroundJobsStore({configuration, databaseIdentifier: config.databaseIdentifier})
80
+ /** @type {import("./adapter.js").default | undefined} */
81
+ this.adapter = undefined
82
82
  this.logger = new Logger(this)
83
83
  /**
84
84
  * Narrows the runtime value to the documented type.
@@ -146,6 +146,24 @@ export default class BackgroundJobsMain {
146
146
  this._beaconClient = undefined
147
147
  }
148
148
 
149
+ /**
150
+ * Compatibility alias for integrations that inspect the active main store.
151
+ * @returns {import("./adapter.js").default} - Adapter acquired by start.
152
+ */
153
+ get store() {
154
+ if (!this.adapter) throw new Error("Background jobs main has not acquired its adapter")
155
+
156
+ return this.adapter
157
+ }
158
+
159
+ /**
160
+ * Preserves the historical subclass seam while keeping one adapter reference.
161
+ * @param {import("./adapter.js").default} adapter - Adapter to assign.
162
+ */
163
+ set store(adapter) {
164
+ this.adapter = adapter
165
+ }
166
+
149
167
  /**
150
168
  * Runs start.
151
169
  * @returns {Promise<void>} - Resolves when listening.
@@ -154,19 +172,24 @@ export default class BackgroundJobsMain {
154
172
  this._stopped = false
155
173
  this.stopPromise = undefined
156
174
  this.configuration.setCurrent()
157
- await this.configuration.initialize({type: "background-jobs-main"})
158
- await this.configuration.connectBeacon({peerType: "background-jobs-main"})
159
- await this.store.ensureReady()
160
- // Queue-cap changes are reconciled against the persisted backlog here, at
161
- // main-process startup — the explicit lifecycle for applying queue
162
- // configuration changes. The store serializes the adoption/release UPDATEs
163
- // across processes with a database advisory lock, so concurrently started
164
- // mains cannot interleave them.
165
- await this.store.reconcileQueueConcurrency()
166
- const server = net.createServer((socket) => this._handleConnection(socket))
167
- this.server = server
168
175
 
169
176
  try {
177
+ await this.configuration.initialize({type: "background-jobs-main"})
178
+ await this.configuration.connectBeacon({peerType: "background-jobs-main"})
179
+
180
+ if (!this.adapter) {
181
+ this.adapter = await this.configuration.acquireReadyBackgroundJobsAdapter()
182
+ }
183
+
184
+ // Queue-cap changes are reconciled against the persisted backlog here, at
185
+ // main-process startup — the explicit lifecycle for applying queue
186
+ // configuration changes. The store serializes the adoption/release UPDATEs
187
+ // across processes with a database advisory lock, so concurrently started
188
+ // mains cannot interleave them.
189
+ await this.store.reconcileQueueConcurrency()
190
+ const server = net.createServer((socket) => this._handleConnection(socket))
191
+ this.server = server
192
+
170
193
  await new Promise((resolve, reject) => {
171
194
  server.once("error", reject)
172
195
  server.listen(this.port, this.host, () => resolve(undefined))
@@ -221,7 +244,16 @@ export default class BackgroundJobsMain {
221
244
  // but this drain covers it).
222
245
  await this._drain()
223
246
  } catch (error) {
224
- await this.stop()
247
+ try {
248
+ await this.stop()
249
+ } catch (cleanupError) {
250
+ throw new AggregateError(
251
+ [error, cleanupError],
252
+ "Background jobs main startup and cleanup failed",
253
+ {cause: cleanupError}
254
+ )
255
+ }
256
+
225
257
  throw error
226
258
  }
227
259
  }
@@ -243,20 +275,27 @@ export default class BackgroundJobsMain {
243
275
  async _stop() {
244
276
  this._stopped = true
245
277
 
246
- await shutdownLifecycle({
247
- onStopped: this.onStopped,
248
- shutdown: async () => {
249
- this._closeWorkers()
250
- this._clearTimers()
251
- this._disconnectBeaconHandlers()
252
- await this.scheduler?.stop()
253
- try {
254
- await this._drainWorkerHandoffAdoptions()
255
- } finally {
256
- await this._stopBeaconAndServer()
278
+ try {
279
+ await shutdownLifecycle({
280
+ onStopped: this.onStopped,
281
+ shutdown: async () => {
282
+ this._closeWorkers()
283
+ this._clearTimers()
284
+ this._disconnectBeaconHandlers()
285
+ try {
286
+ await this.scheduler?.stop()
287
+ } finally {
288
+ try {
289
+ await this._drainWorkerHandoffAdoptions()
290
+ } finally {
291
+ await this._stopBeaconAndServer()
292
+ }
293
+ }
257
294
  }
258
- }
259
- })
295
+ })
296
+ } finally {
297
+ this.adapter = undefined
298
+ }
260
299
  }
261
300
 
262
301
  /**
@@ -318,7 +357,11 @@ export default class BackgroundJobsMain {
318
357
  try {
319
358
  await this._closeServer()
320
359
  } finally {
321
- if (this.closeDatabaseConnectionsOnStop) await this.configuration.closeDatabaseConnections()
360
+ if (this.closeDatabaseConnectionsOnStop) {
361
+ await this.configuration.closeDatabaseConnections()
362
+ } else {
363
+ await this.configuration.closeBackgroundJobsAdapter()
364
+ }
322
365
  }
323
366
  }
324
367
 
@@ -0,0 +1,23 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Performs a job class inside its declared database-connection scope.
5
+ * @param {object} args - Performance options.
6
+ * @param {import("../configuration.js").default} args.configuration - Active configuration.
7
+ * @param {typeof import("./platform-job.js").default} args.JobClass - Job class.
8
+ * @param {Array<ReturnType<typeof JSON.parse>>} args.jobArgs - Job arguments.
9
+ * @param {string} args.name - Connection-scope label.
10
+ * @returns {Promise<void>} - Resolves after performance.
11
+ */
12
+ export default async function performBackgroundJob({configuration, JobClass, jobArgs, name}) {
13
+ const jobInstance = new JobClass()
14
+ /**
15
+ * Narrows the generic subclass's runtime method to serialized job arguments.
16
+ * @type {(...args: Array<ReturnType<typeof JSON.parse>>) => Promise<void>}
17
+ */
18
+ const perform = jobInstance.perform
19
+
20
+ await configuration.withConnections({databaseIdentifiers: JobClass.databaseIdentifiers, name}, async () => {
21
+ await perform.apply(jobInstance, jobArgs)
22
+ })
23
+ }