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
@@ -222,7 +222,11 @@ export default class BackgroundJobsMain {
222
222
  options: jobClass._withQueue(options)
223
223
  })
224
224
  this._notifyEnqueued()
225
- await this._drain()
225
+ // Persistence is the scheduler enqueue boundary. Dispatch remains
226
+ // coalesced, but a slow active drain must not make the recurring
227
+ // scheduler treat this job key as still being enqueued and suppress
228
+ // later timer occurrences.
229
+ void this._drain()
226
230
  }
227
231
  })
228
232
  await this.scheduler.start()
@@ -284,6 +288,7 @@ export default class BackgroundJobsMain {
284
288
  this._disconnectBeaconHandlers()
285
289
  try {
286
290
  await this.scheduler?.stop()
291
+ if (this._drainPromise) await this._drainPromise
287
292
  } finally {
288
293
  try {
289
294
  await this._drainWorkerHandoffAdoptions()
@@ -9,6 +9,19 @@ import BackgroundJobRecord from "./job-record.js"
9
9
  import normalizeBackgroundJobError from "./normalize-error.js"
10
10
  import {coordinateSharedTransactionConnection} from "../testing/shared-transaction-connection-coordinator.js"
11
11
  import stableJsonStringify from "../utils/stable-json.js"
12
+ import {
13
+ BACKGROUND_JOB_EXECUTION_MODES,
14
+ DEFAULT_BACKGROUND_JOB_EXECUTION_MODE,
15
+ DEFAULT_BACKGROUND_JOB_QUEUE,
16
+ QUEUE_CONCURRENCY_KEY_PREFIX,
17
+ normalizeBackgroundJobConcurrency,
18
+ normalizeBackgroundJobExecutionMode,
19
+ normalizeBackgroundJobMaxRetries,
20
+ normalizeBackgroundJobQueue,
21
+ normalizeBackgroundJobScheduledAtMs,
22
+ rescheduledBackgroundJobAtMs,
23
+ retryDelayMs
24
+ } from "./job-semantics.js"
12
25
  import {
13
26
  MAIL_DELIVERY_OPERATIONS_TABLE,
14
27
  mailDeliveryOperationForJob,
@@ -52,22 +65,7 @@ const COUNTS_REVISION_KEY = "counts"
52
65
  export const BACKGROUND_JOB_COUNTS_CHANNEL = "velocious-background-job-counts"
53
66
  export const BACKGROUND_JOB_COUNT_BUCKETS = ["all", "queued", "handed_off", "completed", "failed", "orphaned"]
54
67
  const COUNTED_JOB_STATUSES = BACKGROUND_JOB_COUNT_BUCKETS.slice(1)
55
- const DEFAULT_MAX_RETRIES = 10
56
68
  const ORPHANED_AFTER_MS = 2 * 60 * 60 * 1000
57
- /**
58
- * Execution modes.
59
- * @type {import("./types.js").BackgroundJobExecutionMode[]} */
60
- const EXECUTION_MODES = ["inline", "forked", "pooled", "spawned"]
61
- /**
62
- * Execution mode for a new enqueue that names neither `executionMode` nor the
63
- * legacy `forked` flag. Pooled routes the job to a warm, reused local runner
64
- * process — the same isolation as forked without paying a fresh process per job.
65
- * @type {import("./types.js").BackgroundJobExecutionMode} */
66
- const DEFAULT_EXECUTION_MODE = "pooled"
67
- const DEFAULT_QUEUE = "default"
68
- // Queue-derived durable concurrency keys are namespaced so they can't collide
69
- // with explicit caller-supplied concurrencyKeys.
70
- const QUEUE_CONCURRENCY_KEY_PREFIX = "queue:"
71
69
 
72
70
  /**
73
71
  * Columns the dashboard is allowed to sort job listings by, mapped to their
@@ -783,7 +781,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
783
781
  .map(([queue, priority]) => `WHEN ${db.quote(queue)} THEN ${priority}`)
784
782
  .join(" ")
785
783
 
786
- return `CASE COALESCE(${queueColumn}, ${db.quote(DEFAULT_QUEUE)}) ${whens} ELSE 0 END`
784
+ return `CASE COALESCE(${queueColumn}, ${db.quote(DEFAULT_BACKGROUND_JOB_QUEUE)}) ${whens} ELSE 0 END`
787
785
  }
788
786
 
789
787
  /**
@@ -1302,13 +1300,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1302
1300
  * @returns {number} - Delay in milliseconds.
1303
1301
  */
1304
1302
  getRetryDelayMs(retryCount) {
1305
- const scheduleSeconds = [10, 60, 600, 3600]
1306
-
1307
- if (retryCount <= scheduleSeconds.length) {
1308
- return scheduleSeconds[retryCount - 1] * 1000
1309
- }
1310
-
1311
- return (retryCount - 3) * 60 * 60 * 1000
1303
+ return retryDelayMs(retryCount)
1312
1304
  }
1313
1305
 
1314
1306
  /**
@@ -1382,11 +1374,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1382
1374
  * @returns {number} - Normalized max retries.
1383
1375
  */
1384
1376
  _normalizeMaxRetries(maxRetries) {
1385
- if (typeof maxRetries === "number" && Number.isFinite(maxRetries) && maxRetries >= 0) {
1386
- return Math.floor(maxRetries)
1387
- }
1388
-
1389
- return DEFAULT_MAX_RETRIES
1377
+ return normalizeBackgroundJobMaxRetries(maxRetries)
1390
1378
  }
1391
1379
 
1392
1380
  /**
@@ -1396,10 +1384,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1396
1384
  * @returns {number} - Dispatch timestamp.
1397
1385
  */
1398
1386
  _normalizeScheduledAtMs(scheduledAtMs, defaultScheduledAtMs) {
1399
- if (scheduledAtMs === undefined) return defaultScheduledAtMs
1400
- if (Number.isSafeInteger(scheduledAtMs) && scheduledAtMs >= 0) return scheduledAtMs
1401
-
1402
- throw VelociousError.safe("background job scheduledAtMs must be a non-negative safe integer")
1387
+ return normalizeBackgroundJobScheduledAtMs(scheduledAtMs, defaultScheduledAtMs)
1403
1388
  }
1404
1389
 
1405
1390
  /**
@@ -1408,14 +1393,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1408
1393
  * @returns {number} - Future eligibility timestamp.
1409
1394
  */
1410
1395
  _rescheduledAtMs(delayMs) {
1411
- this._validateRescheduleDelayMs(delayMs)
1412
-
1413
- const scheduledAtMs = Date.now() + delayMs
1414
- if (!Number.isSafeInteger(scheduledAtMs)) {
1415
- throw VelociousError.safe("background job reschedule scheduledAtMs must be a safe integer")
1416
- }
1417
-
1418
- return scheduledAtMs
1396
+ return rescheduledBackgroundJobAtMs(delayMs, Date.now())
1419
1397
  }
1420
1398
 
1421
1399
  /**
@@ -1424,9 +1402,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1424
1402
  * @returns {void}
1425
1403
  */
1426
1404
  _validateRescheduleDelayMs(delayMs) {
1427
- if (!Number.isSafeInteger(delayMs) || delayMs < 0) {
1428
- throw VelociousError.safe("background job reschedule delayMs must be a non-negative safe integer")
1429
- }
1405
+ rescheduledBackgroundJobAtMs(delayMs, 0)
1430
1406
  }
1431
1407
 
1432
1408
  /**
@@ -2083,14 +2059,14 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2083
2059
  // `execution_mode` is the single source of truth for a job's runtime and is
2084
2060
  // written on every enqueue; the drop-forked migration backfills any pre-existing
2085
2061
  // rows before the legacy `forked` column is removed.
2086
- const executionMode = row.execution_mode ? this._normalizeExecutionModeName(String(row.execution_mode)) : DEFAULT_EXECUTION_MODE
2062
+ const executionMode = row.execution_mode ? this._normalizeExecutionModeName(String(row.execution_mode)) : DEFAULT_BACKGROUND_JOB_EXECUTION_MODE
2087
2063
 
2088
2064
  return {
2089
2065
  id: String(row.id),
2090
2066
  jobName: String(row.job_name),
2091
2067
  args: this._parseArgs(row.args_json),
2092
2068
  executionMode,
2093
- queue: row.queue ? String(row.queue) : DEFAULT_QUEUE,
2069
+ queue: row.queue ? String(row.queue) : DEFAULT_BACKGROUND_JOB_QUEUE,
2094
2070
  scheduleKey: row.schedule_key ? String(row.schedule_key) : null,
2095
2071
  status: row.status ? String(row.status) : "queued",
2096
2072
  attempts: this._normalizeNumber(row.attempts),
@@ -2109,35 +2085,13 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2109
2085
  }
2110
2086
  }
2111
2087
 
2112
- /**
2113
- * Validates concurrency options.
2114
- * @param {import("./types.js").BackgroundJobOptions | undefined} options - Job options.
2115
- * @returns {{concurrencyKey: string, maxConcurrency: number} | null} - Normalized configuration.
2116
- */
2117
- _normalizeConcurrencyOptions(options) {
2118
- const key = options?.concurrencyKey
2119
- const cap = options?.maxConcurrency
2120
- if (key === undefined && cap === undefined) return null
2121
- if (typeof key !== "string" || key.length === 0 || !Number.isInteger(cap) || Number(cap) <= 0) {
2122
- throw new Error("background job concurrencyKey and maxConcurrency must be paired; concurrencyKey must be non-empty and maxConcurrency must be a positive integer")
2123
- }
2124
- if (key.startsWith(QUEUE_CONCURRENCY_KEY_PREFIX)) {
2125
- 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`)
2126
- }
2127
- return {concurrencyKey: key, maxConcurrency: Number(cap)}
2128
- }
2129
-
2130
2088
  /**
2131
2089
  * Normalizes a job's queue name, defaulting to "default".
2132
2090
  * @param {import("./types.js").BackgroundJobOptions | undefined} options - Job options.
2133
2091
  * @returns {string} - Queue name.
2134
2092
  */
2135
2093
  _normalizeQueue(options) {
2136
- const queue = options?.queue
2137
-
2138
- if (typeof queue === "string" && queue.trim().length > 0) return queue.trim()
2139
-
2140
- return DEFAULT_QUEUE
2094
+ return normalizeBackgroundJobQueue(options)
2141
2095
  }
2142
2096
 
2143
2097
  /**
@@ -2151,15 +2105,11 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2151
2105
  * @returns {{concurrencyKey: string, maxConcurrency: number, queueDerived: boolean} | null} - Resolved concurrency.
2152
2106
  */
2153
2107
  _resolveConcurrency(options, queue) {
2154
- const explicit = this._normalizeConcurrencyOptions(options)
2155
-
2156
- if (explicit) return {...explicit, queueDerived: false}
2157
-
2158
- const cap = this._queueMaxConcurrency(queue)
2159
-
2160
- if (cap === null) return null
2161
-
2162
- return {concurrencyKey: `${QUEUE_CONCURRENCY_KEY_PREFIX}${queue}`, maxConcurrency: cap, queueDerived: true}
2108
+ return normalizeBackgroundJobConcurrency({
2109
+ options: options || {},
2110
+ queue,
2111
+ queues: this.configuration.getBackgroundJobsConfig().queues
2112
+ })
2163
2113
  }
2164
2114
 
2165
2115
  /**
@@ -2667,21 +2617,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2667
2617
  * @returns {import("./types.js").BackgroundJobExecutionMode} - Normalized execution mode.
2668
2618
  */
2669
2619
  _normalizeExecutionMode(options) {
2670
- const executionMode = options?.executionMode
2671
-
2672
- if (executionMode) {
2673
- return this._normalizeExecutionModeName(executionMode)
2674
- }
2675
-
2676
- // The `forked` option alias was removed. Reject it loudly instead of silently
2677
- // defaulting to pooled, which would turn an explicitly inline (`forked: false`)
2678
- // or one-shot forked (`forked: true`) job into a pooled child-runner job — a
2679
- // silent semantic change for any not-yet-migrated caller.
2680
- if (options && "forked" in options) {
2681
- throw new Error("The background job `forked` option was removed; pass `executionMode` (\"inline\", \"forked\", \"pooled\", or \"spawned\") instead")
2682
- }
2683
-
2684
- return DEFAULT_EXECUTION_MODE
2620
+ return normalizeBackgroundJobExecutionMode(options || {}, DEFAULT_BACKGROUND_JOB_EXECUTION_MODE)
2685
2621
  }
2686
2622
 
2687
2623
  /**
@@ -2690,11 +2626,11 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2690
2626
  * @returns {import("./types.js").BackgroundJobExecutionMode} - Normalized execution mode.
2691
2627
  */
2692
2628
  _normalizeExecutionModeName(executionMode) {
2693
- for (const mode of EXECUTION_MODES) {
2694
- if (mode === executionMode) return mode
2695
- }
2696
-
2697
- throw new Error(`Invalid background job executionMode: ${executionMode}`)
2629
+ return normalizeBackgroundJobExecutionMode(
2630
+ {executionMode: /** @type {import("./types.js").BackgroundJobExecutionMode} */ (executionMode)},
2631
+ DEFAULT_BACKGROUND_JOB_EXECUTION_MODE,
2632
+ BACKGROUND_JOB_EXECUTION_MODES
2633
+ )
2698
2634
  }
2699
2635
 
2700
2636
  /**
@@ -3,6 +3,31 @@
3
3
  /**
4
4
  * @typedef {"inline" | "forked" | "pooled" | "spawned"} BackgroundJobExecutionMode
5
5
  */
6
+ /**
7
+ * @typedef {object} LocalBackgroundJobsClock
8
+ * @property {() => number} now - Current epoch milliseconds.
9
+ * @property {(callback: () => void, delayMs: number) => ReturnType<typeof setTimeout> | number} setTimeout - Arms a timer.
10
+ * @property {(timerId: ReturnType<typeof setTimeout> | number) => void} clearTimeout - Clears a timer.
11
+ */
12
+ /**
13
+ * @typedef {object} ResolvedBackgroundJobConcurrency
14
+ * @property {string} concurrencyKey - Durable cap identity.
15
+ * @property {number} maxConcurrency - Positive cap.
16
+ * @property {boolean} queueDerived - Whether queue configuration owns the cap.
17
+ */
18
+ /**
19
+ * @typedef {object} PreparedLocalBackgroundJob
20
+ * @property {string} argsDigest - Fixed-width digest of the serialized arguments.
21
+ * @property {string} argsJson - Serialized arguments.
22
+ * @property {ResolvedBackgroundJobConcurrency | null} concurrency - Resolved concurrency.
23
+ * @property {number} createdAtMs - Creation timestamp.
24
+ * @property {"inline"} executionMode - Local in-process execution mode.
25
+ * @property {string} jobId - Durable id.
26
+ * @property {string} jobName - Registered name.
27
+ * @property {number} maxRetries - Retry cap.
28
+ * @property {string} queue - Queue name.
29
+ * @property {number} scheduledAtMs - Eligibility timestamp.
30
+ */
6
31
  /**
7
32
  * @typedef {object} BackgroundJobsHealth
8
33
  * @property {boolean} ready - Whether the adapter can accept and process work.
@@ -20,7 +45,7 @@
20
45
  */
21
46
  /**
22
47
  * @typedef {object} BackgroundJobOptions
23
- * @property {BackgroundJobExecutionMode} [executionMode] - How the job should run. Defaults to `"pooled"` (a warm, reused local runner process). `"forked"` runs the job in a fresh `child_process.fork()` child, `"spawned"` in a detached CLI runner, and `"inline"` inside the worker process. Omit to use the default `"pooled"` mode.
48
+ * @property {BackgroundJobExecutionMode} [executionMode] - How the job should run. Node defaults to `"pooled"` (a warm, reused local runner process). Browser/Expo local dispatch defaults to and only accepts `"inline"`. `"forked"` runs a Node job in a fresh `child_process.fork()` child, and `"spawned"` in a detached CLI runner.
24
49
  * @property {number} [maxRetries] - Max retries for a failed job before it is marked failed.
25
50
  * @property {string} [queue] - Queue name. Defaults to `"default"`. When the queue has a configured cap in `backgroundJobs.queues`, that cap is enforced cluster-wide.
26
51
  * @property {string} [concurrencyKey] - Opaque non-empty key used to share a concurrency cap. Overrides any queue-derived cap.
@@ -158,14 +158,16 @@
158
158
 
159
159
  /** @typedef {"background" | "inline"} BackgroundJobsMode */
160
160
  /** @typedef {(args: {configuration: import("./configuration.js").default}) => import("./background-jobs/adapter.js").default} BackgroundJobsAdapterFactory */
161
+ /** @typedef {typeof import("./background-jobs/platform-job.js").default} BackgroundJobClass */
161
162
 
162
163
  /**
163
164
  * @typedef {object} BackgroundJobsConfiguration
164
165
  * @property {import("./background-jobs/adapter.js").default | BackgroundJobsAdapterFactory} [adapter] - Adapter instance or synchronous factory. A factory creates one adapter per configuration lifecycle; the framework closes adapters it resolves.
166
+ * @property {BackgroundJobClass[]} [jobClasses] - Static portable job classes available to Browser/Expo local dispatch. Defaults to `[]`; Node keeps its filesystem registry.
165
167
  * @property {BackgroundJobsMode} [mode] - `"background"` uses the configured adapter/transport and durable queue semantics; `"inline"` performs immediately without durable queue state. Defaults to `"background"`.
166
168
  * @property {string} [host] - Hostname for the background jobs main process.
167
169
  * @property {number} [port] - Port for the background jobs main process.
168
- * @property {string} [databaseIdentifier] - Database identifier used to store background jobs.
170
+ * @property {string} [databaseIdentifier] - Database identifier used to store background jobs. Browser/Expo local dispatch uses this existing SQLite database and defaults to `"default"`.
169
171
  * @property {number} [maxConcurrentInlineJobs] - How many `forked: false` jobs a single
170
172
  * `background-jobs-worker` process is allowed to run in parallel. Concurrency
171
173
  * is at the JS event-loop level: every concurrent job shares the worker's
@@ -1543,7 +1543,22 @@ export default class VelociousConfiguration {
1543
1543
  : 60 * 60 * 1000
1544
1544
  }
1545
1545
 
1546
- return {host, port, databaseIdentifier, maxConcurrentForkedJobs, maxConcurrentInlineJobs, mode, pooledRunnerCount, pooledRunnerConcurrency, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, dispatchStrategy, pollIntervalMs, queues, jobTimeoutMs, retention}
1546
+ const jobClasses = this.getBackgroundJobClasses()
1547
+
1548
+ return {host, port, databaseIdentifier, maxConcurrentForkedJobs, maxConcurrentInlineJobs, mode, pooledRunnerCount, pooledRunnerConcurrency, pooledRunnerMaxJobs, pooledRunnerMaxRssBytes, pooledRunnerMaxLifetimeMs, dispatchStrategy, pollIntervalMs, queues, jobClasses, jobTimeoutMs, retention}
1549
+ }
1550
+
1551
+ /**
1552
+ * Returns statically registered portable background jobs.
1553
+ * @returns {import("./configuration-types.js").BackgroundJobClass[]} - Configured job classes.
1554
+ */
1555
+ getBackgroundJobClasses() {
1556
+ const jobClasses = this._backgroundJobs?.jobClasses
1557
+
1558
+ if (jobClasses === undefined) return []
1559
+ if (!Array.isArray(jobClasses)) throw new TypeError("backgroundJobs.jobClasses must be an array")
1560
+
1561
+ return [...jobClasses]
1547
1562
  }
1548
1563
 
1549
1564
  /**
@@ -255,6 +255,35 @@ export default class VelociousDatabaseDriversSqliteNode extends Base {
255
255
  await fs.writeFile(ownerPath, payload)
256
256
  }
257
257
 
258
+ /**
259
+ * Publishes a fully initialized lock directory with one atomic rename.
260
+ * A losing candidate is removed in the same call, so concurrent acquisition
261
+ * cannot observe or delete another process's half-written owner metadata.
262
+ * @param {string} lockPath - Stable advisory-lock path.
263
+ * @returns {Promise<boolean>} - Whether this candidate became the lock owner.
264
+ */
265
+ async _publishAdvisoryLockDirectory(lockPath) {
266
+ const candidatePath = await fs.mkdtemp(`${lockPath}.pending-`)
267
+ let published = false
268
+
269
+ try {
270
+ await this._writeAdvisoryLockMetadata(candidatePath)
271
+
272
+ try {
273
+ await fs.rename(candidatePath, lockPath)
274
+ published = true
275
+ return true
276
+ } catch (error) {
277
+ const code = /** @type {Error & {code?: string}} */ (error)?.code
278
+
279
+ if (code === "EEXIST" || code === "ENOTEMPTY") return false
280
+ throw error
281
+ }
282
+ } finally {
283
+ if (!published) await fs.rm(candidatePath, {force: true, recursive: true})
284
+ }
285
+ }
286
+
258
287
  /**
259
288
  * Runs acquire advisory lock file.
260
289
  * @param {string} name - Lock name.
@@ -272,28 +301,21 @@ export default class VelociousDatabaseDriversSqliteNode extends Base {
272
301
  // mkdir succeeds, the deadline elapses, or an unexpected error is
273
302
  // re-thrown.
274
303
  while (true) {
275
- try {
276
- await fs.mkdir(lockPath)
277
- await this._writeAdvisoryLockMetadata(lockPath)
304
+ if (await this._publishAdvisoryLockDirectory(lockPath)) return true
278
305
 
279
- return true
280
- } catch (error) {
281
- if (/** @type {Error & {code?: string}} */ (error)?.code !== "EEXIST") throw error
282
-
283
- if (await this._isAdvisoryLockStale(lockPath)) {
284
- await fs.rm(lockPath, {recursive: true, force: true})
285
- continue
286
- }
306
+ if (await this._isAdvisoryLockStale(lockPath)) {
307
+ await fs.rm(lockPath, {recursive: true, force: true})
308
+ continue
309
+ }
287
310
 
288
- if (deadline !== null) {
289
- const remaining = deadline - Date.now()
311
+ if (deadline !== null) {
312
+ const remaining = deadline - Date.now()
290
313
 
291
- if (remaining <= 0) return false
314
+ if (remaining <= 0) return false
292
315
 
293
- await wait(Math.min(pollIntervalMs, remaining))
294
- } else {
295
- await wait(pollIntervalMs)
296
- }
316
+ await wait(Math.min(pollIntervalMs, remaining))
317
+ } else {
318
+ await wait(pollIntervalMs)
297
319
  }
298
320
  }
299
321
  }
@@ -308,31 +330,14 @@ export default class VelociousDatabaseDriversSqliteNode extends Base {
308
330
 
309
331
  const lockPath = this._advisoryLockPath(name)
310
332
 
311
- try {
312
- await fs.mkdir(lockPath)
313
- await this._writeAdvisoryLockMetadata(lockPath)
333
+ if (await this._publishAdvisoryLockDirectory(lockPath)) return true
314
334
 
315
- return true
316
- } catch (error) {
317
- if (/** @type {Error & {code?: string}} */ (error)?.code !== "EEXIST") throw error
318
-
319
- if (await this._isAdvisoryLockStale(lockPath)) {
320
- await fs.rm(lockPath, {recursive: true, force: true})
321
-
322
- try {
323
- await fs.mkdir(lockPath)
324
- await this._writeAdvisoryLockMetadata(lockPath)
325
-
326
- return true
327
- } catch (retryError) {
328
- if (/** @type {Error & {code?: string}} */ (retryError)?.code === "EEXIST") return false
329
-
330
- throw retryError
331
- }
332
- }
333
-
334
- return false
335
+ if (await this._isAdvisoryLockStale(lockPath)) {
336
+ await fs.rm(lockPath, {recursive: true, force: true})
337
+ return await this._publishAdvisoryLockDirectory(lockPath)
335
338
  }
339
+
340
+ return false
336
341
  }
337
342
 
338
343
  /**
@@ -2,6 +2,7 @@ import Base from "./base.js"
2
2
  import * as inflection from "inflection"
3
3
  import restArgsError from "../utils/rest-args-error.js"
4
4
  import Logger from "../logger.js"
5
+ import LocalBackgroundJobsAdapter from "../background-jobs/local-adapter.js"
5
6
 
6
7
  /**
7
8
  * Defines this typedef.
@@ -40,6 +41,15 @@ function isMigrationObject(migration) {
40
41
  }
41
42
 
42
43
  export default class VelociousEnvironmentsHandlerBrowser extends Base {
44
+ /**
45
+ * Creates the Browser/Expo local SQLite adapter and in-process dispatcher.
46
+ * @param {{configuration: import("../configuration.js").default}} args - Adapter options.
47
+ * @returns {LocalBackgroundJobsAdapter} - Local background-jobs adapter.
48
+ */
49
+ createBackgroundJobsAdapter({configuration}) {
50
+ return new LocalBackgroundJobsAdapter({configuration})
51
+ }
52
+
43
53
  /**
44
54
  * Find commands require context result.
45
55
  * @type {CommandsRequireContextType | undefined} */