velocious 1.0.596 → 1.0.598

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 (124) hide show
  1. package/README.md +18 -2
  2. package/build/background-jobs/main.js +42 -32
  3. package/build/background-jobs/pooled-runner-broker-identity.js +22 -4
  4. package/build/background-jobs/store.js +383 -9
  5. package/build/background-jobs/types.js +1 -0
  6. package/build/cli/commands/db/tenants/migrations/pending.js +45 -0
  7. package/build/configuration-types.js +3 -0
  8. package/build/configuration.js +5 -2
  9. package/build/database/drivers/sqlite/base.js +3 -3
  10. package/build/database/tenants/migration-pending-inspector.js +77 -0
  11. package/build/environment-handlers/base.js +8 -0
  12. package/build/environment-handlers/node.js +14 -0
  13. package/build/mailer/backends/resend-smtp.js +121 -0
  14. package/build/mailer/base.js +52 -22
  15. package/build/mailer/delivery-operation-store.js +152 -0
  16. package/build/mailer/delivery-operation.js +205 -0
  17. package/build/mailer/delivery.js +9 -4
  18. package/build/mailer/index.js +5 -1
  19. package/build/mailer.js +6 -1
  20. package/build/src/background-jobs/main.d.ts +15 -10
  21. package/build/src/background-jobs/main.d.ts.map +1 -1
  22. package/build/src/background-jobs/main.js +42 -34
  23. package/build/src/background-jobs/pooled-runner-broker-identity.d.ts +10 -1
  24. package/build/src/background-jobs/pooled-runner-broker-identity.d.ts.map +1 -1
  25. package/build/src/background-jobs/pooled-runner-broker-identity.js +23 -5
  26. package/build/src/background-jobs/store.d.ts +154 -0
  27. package/build/src/background-jobs/store.d.ts.map +1 -1
  28. package/build/src/background-jobs/store.js +350 -10
  29. package/build/src/background-jobs/types.d.ts +5 -0
  30. package/build/src/background-jobs/types.d.ts.map +1 -1
  31. package/build/src/background-jobs/types.js +2 -1
  32. package/build/src/cli/commands/db/tenants/migrations/pending.d.ts +15 -0
  33. package/build/src/cli/commands/db/tenants/migrations/pending.d.ts.map +1 -0
  34. package/build/src/cli/commands/db/tenants/migrations/pending.js +40 -0
  35. package/build/src/configuration-types.d.ts +20 -0
  36. package/build/src/configuration-types.d.ts.map +1 -1
  37. package/build/src/configuration-types.js +4 -1
  38. package/build/src/configuration.d.ts +2 -2
  39. package/build/src/configuration.d.ts.map +1 -1
  40. package/build/src/configuration.js +6 -3
  41. package/build/src/database/drivers/sqlite/base.js +4 -4
  42. package/build/src/database/tenants/migration-pending-inspector.d.ts +38 -0
  43. package/build/src/database/tenants/migration-pending-inspector.d.ts.map +1 -0
  44. package/build/src/database/tenants/migration-pending-inspector.js +70 -0
  45. package/build/src/environment-handlers/base.d.ts +10 -0
  46. package/build/src/environment-handlers/base.d.ts.map +1 -1
  47. package/build/src/environment-handlers/base.js +8 -1
  48. package/build/src/environment-handlers/node.d.ts +11 -0
  49. package/build/src/environment-handlers/node.d.ts.map +1 -1
  50. package/build/src/environment-handlers/node.js +14 -1
  51. package/build/src/mailer/backends/resend-smtp.d.ts +43 -0
  52. package/build/src/mailer/backends/resend-smtp.d.ts.map +1 -0
  53. package/build/src/mailer/backends/resend-smtp.js +106 -0
  54. package/build/src/mailer/base.d.ts +9 -2
  55. package/build/src/mailer/base.d.ts.map +1 -1
  56. package/build/src/mailer/base.js +41 -19
  57. package/build/src/mailer/delivery-operation-store.d.ts +52 -0
  58. package/build/src/mailer/delivery-operation-store.d.ts.map +1 -0
  59. package/build/src/mailer/delivery-operation-store.js +130 -0
  60. package/build/src/mailer/delivery-operation.d.ts +73 -0
  61. package/build/src/mailer/delivery-operation.d.ts.map +1 -0
  62. package/build/src/mailer/delivery-operation.js +185 -0
  63. package/build/src/mailer/delivery.d.ts +4 -2
  64. package/build/src/mailer/delivery.d.ts.map +1 -1
  65. package/build/src/mailer/delivery.js +9 -5
  66. package/build/src/mailer/index.d.ts +24 -1
  67. package/build/src/mailer/index.d.ts.map +1 -1
  68. package/build/src/mailer/index.js +6 -2
  69. package/build/src/mailer.d.ts +11 -13
  70. package/build/src/mailer.d.ts.map +1 -1
  71. package/build/src/mailer.js +7 -2
  72. package/build/src/sync/local-mutation-log.d.ts +5 -5
  73. package/build/src/sync/local-mutation-log.d.ts.map +1 -1
  74. package/build/src/sync/local-mutation-log.js +34 -10
  75. package/build/src/sync/stable-json.d.ts +1 -8
  76. package/build/src/sync/stable-json.d.ts.map +1 -1
  77. package/build/src/sync/stable-json.js +2 -27
  78. package/build/src/sync/sync-envelope-replay-service.d.ts +3 -2
  79. package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
  80. package/build/src/sync/sync-envelope-replay-service.js +12 -8
  81. package/build/src/testing/factory/node/definition-reload-policy.d.ts +97 -0
  82. package/build/src/testing/factory/node/definition-reload-policy.d.ts.map +1 -0
  83. package/build/src/testing/factory/node/definition-reload-policy.js +133 -0
  84. package/build/src/testing/factory/node/load-definitions.d.ts +4 -1
  85. package/build/src/testing/factory/node/load-definitions.d.ts.map +1 -1
  86. package/build/src/testing/factory/node/load-definitions.js +27 -4
  87. package/build/src/testing/test-runner.js +7 -7
  88. package/build/src/utils/stable-json.d.ts +7 -0
  89. package/build/src/utils/stable-json.d.ts.map +1 -0
  90. package/build/src/utils/stable-json.js +25 -0
  91. package/build/sync/local-mutation-log.js +37 -6
  92. package/build/sync/stable-json.js +1 -28
  93. package/build/sync/sync-envelope-replay-service.js +11 -7
  94. package/build/testing/factory/node/definition-reload-policy.js +149 -0
  95. package/build/testing/factory/node/load-definitions.js +29 -5
  96. package/build/testing/test-runner.js +6 -6
  97. package/build/tsconfig.tsbuildinfo +1 -1
  98. package/build/utils/stable-json.js +26 -0
  99. package/package.json +2 -1
  100. package/src/background-jobs/main.js +42 -32
  101. package/src/background-jobs/pooled-runner-broker-identity.js +22 -4
  102. package/src/background-jobs/store.js +383 -9
  103. package/src/background-jobs/types.js +1 -0
  104. package/src/cli/commands/db/tenants/migrations/pending.js +45 -0
  105. package/src/configuration-types.js +3 -0
  106. package/src/configuration.js +5 -2
  107. package/src/database/drivers/sqlite/base.js +3 -3
  108. package/src/database/tenants/migration-pending-inspector.js +77 -0
  109. package/src/environment-handlers/base.js +8 -0
  110. package/src/environment-handlers/node.js +14 -0
  111. package/src/mailer/backends/resend-smtp.js +121 -0
  112. package/src/mailer/base.js +52 -22
  113. package/src/mailer/delivery-operation-store.js +152 -0
  114. package/src/mailer/delivery-operation.js +205 -0
  115. package/src/mailer/delivery.js +9 -4
  116. package/src/mailer/index.js +5 -1
  117. package/src/mailer.js +6 -1
  118. package/src/sync/local-mutation-log.js +37 -6
  119. package/src/sync/stable-json.js +1 -28
  120. package/src/sync/sync-envelope-replay-service.js +11 -7
  121. package/src/testing/factory/node/definition-reload-policy.js +149 -0
  122. package/src/testing/factory/node/load-definitions.js +29 -5
  123. package/src/testing/test-runner.js +6 -6
  124. package/src/utils/stable-json.js +26 -0
package/README.md CHANGED
@@ -9,6 +9,7 @@
9
9
  * Declarative state machines for models, with typed event methods generated into the base model (see [docs/state-machine.md](docs/state-machine.md))
10
10
  * Migrations for schema changes and UTC datetime storage (see [docs/database-migrations.md](docs/database-migrations.md))
11
11
  * Tenant-selected base-model and structure generation with one immutable, fail-closed physical database context; tenant-only model metadata initializes only after that context is active (see [docs/tenant-selected-database-generation.md](docs/tenant-selected-database-generation.md))
12
+ * Read-only tenant migration deploy preflight with stable JSON output and fail-closed ledger reads (see [docs/tenant-migration-deploy-preflight.md](docs/tenant-migration-deploy-preflight.md))
12
13
  * External packages (engines) that contribute data models, frontend-model resources and migrations to a consuming app (see [docs/packages.md](docs/packages.md))
13
14
  * Optional Rampway-owned durable deployment control plane mounted through the standard routes DSL on Velocious 1.0.577 or newer (see [docs/rampway-integration.md](docs/rampway-integration.md))
14
15
  * Controllers and views for HTTP endpoints
@@ -24,7 +25,7 @@
24
25
  * Expo / Metro compatibility guidance and a real Expo export check (see [docs/expo-metro-compatibility.md](docs/expo-metro-compatibility.md))
25
26
  * 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))
26
27
  * Rails-style nested-attribute writes on frontend-model `save()` (see [docs/nested-attributes.md](docs/nested-attributes.md))
27
- * Async-aware test-data factories with inherited traits, graph-first native association autosave, metadata-aware override precedence, callbacks, sequences, and linting (see [docs/factories.md](docs/factories.md))
28
+ * Async-aware test-data factories with inherited traits, graph-first native association autosave, metadata-aware override precedence, callbacks, sequences, linting, and a process-global reload-retention budget that bounds cache-busted re-import memory (see [docs/factories.md](docs/factories.md))
28
29
  * Per-row association counts via `.withCount(...)`, including cohort-safe intersected filters, safe batching of structurally identical aggregates, and automatic IN-list chunking for large parent sets, on frontend and backend queries (see [docs/with-count.md](docs/with-count.md))
29
30
  * Consumer-defined per-row SQL aggregates/computations via `.queryData(...)`, with compatible projections sharing a roundtrip while preserving declared alias-overwrite order and automatic IN-list chunking for large parent sets, on frontend and backend queries (see [docs/query-data.md](docs/query-data.md))
30
31
  * Per-record ability checks via `.abilities(...)` on frontend queries + `record.can(action)` (see [docs/abilities.md](docs/abilities.md))
@@ -389,6 +390,19 @@ await new TasksMailer().newNotification(task, user).deliverNow()
389
390
  await new TasksMailer().newNotification(task, user).deliverLater()
390
391
  ```
391
392
 
393
+ For a provider that advertises duplicate suppression, a producer can require one stable mail operation across outbox replay and native-job retries:
394
+
395
+ ```js
396
+ await new TasksMailer().newNotification(task, user).deliverLater({
397
+ deliveryOperation: {
398
+ id: `project-command:${command.id()}`,
399
+ idempotency: "required"
400
+ }
401
+ })
402
+ ```
403
+
404
+ Required delivery fails before enqueue on unsupported backends, rejects the same id with changed rendered content, and fails closed after the provider retention window. Direct required `deliverPayload()` calls also fail before provider I/O when the background-jobs database connection is already inside a caller-owned transaction, because an outer rollback could erase the first-attempt marker. Generic SMTP remains at-least-once. Velocious includes a dedicated `ResendSmtpMailerBackend` for Resend's 24-hour `Resend-Idempotency-Key` contract; see [Mailers](docs/mailers.md#provider-backed-idempotent-background-delivery) for setup, expiry, reconciliation, and non-exactly-once guarantees.
405
+
392
406
  Build the rendered payload without sending when the app needs to store an audit copy or hand delivery to its own transport:
393
407
 
394
408
  ```js
@@ -2395,10 +2409,12 @@ const result = await MyJob.replaceScheduled({
2395
2409
  await MyJob.cancelScheduled(`event:${eventId}:reminder:24h`)
2396
2410
  ```
2397
2411
 
2398
- 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).
2412
+ A queued owner is atomically cancelled during replacement/cancellation. Its acknowledgement waits for the corresponding dispatch drain lifecycle; if another drain is already active, the request coalesces and waits for its re-drain and future-job timer re-arm instead of acknowledging early. 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).
2399
2413
 
2400
2414
  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.
2401
2415
 
2416
+ Use `options: {idempotencyKey}` when producer replay must converge on the original durable job across every state and even after terminal-job pruning. Ownership is scoped to the resolved job class name, resolved queue, and key; reusing that scope with changed canonical arguments or behavior-affecting options fails. This is distinct from queued-only deduplication, and ownership rows are intentionally retained until a future explicit reconciliation/deletion policy. See [durable idempotent enqueue](docs/background-jobs.md#durable-idempotent-enqueue).
2417
+
2402
2418
  Select a non-default runtime explicitly with `options: {executionMode: "inline" | "forked" | "spawned"}`.
2403
2419
 
2404
2420
  Inline jobs share the worker process and run concurrently up to `maxConcurrentInlineJobs`, so a single slow inline job no longer blocks the queue. A single worker can also override the configured cap explicitly:
@@ -127,6 +127,8 @@ export default class BackgroundJobsMain {
127
127
  this.scheduler = undefined
128
128
  this._draining = false
129
129
  this._redrainQueued = false
130
+ /** @type {Promise<void> | undefined} */
131
+ this._drainPromise = undefined
130
132
  this._stopped = false
131
133
  /** @type {Promise<void> | undefined} */
132
134
  this.stopPromise = undefined
@@ -645,9 +647,11 @@ export default class BackgroundJobsMain {
645
647
  /**
646
648
  * Removes a lost worker socket and releases only leases dispatched through it.
647
649
  * @param {JsonSocket} worker - Disconnected worker socket.
650
+ * @param {object} [args] - Coordination options.
651
+ * @param {boolean} [args.queueRedrain] - Queue another pass instead of awaiting the active drain.
648
652
  * @returns {Promise<void>} - Resolves after its active leases are released.
649
653
  */
650
- async _handleWorkerSocketClosed(worker) {
654
+ async _handleWorkerSocketClosed(worker, {queueRedrain = false} = {}) {
651
655
  this.workers.delete(worker)
652
656
  this.readyWorkers.delete(worker)
653
657
 
@@ -657,7 +661,7 @@ export default class BackgroundJobsMain {
657
661
  }
658
662
 
659
663
  try {
660
- await this._releaseWorkerHandoffs(worker)
664
+ await this._releaseWorkerHandoffs(worker, {queueRedrain})
661
665
  } catch (error) {
662
666
  this._reportHandoffReleaseError(error)
663
667
  this._scheduleErrorRetry()
@@ -667,9 +671,11 @@ export default class BackgroundJobsMain {
667
671
  /**
668
672
  * Releases all leases still owned by one exact worker socket.
669
673
  * @param {JsonSocket} worker - Worker socket.
674
+ * @param {object} [args] - Coordination options.
675
+ * @param {boolean} [args.queueRedrain] - Queue another pass instead of awaiting the active drain.
670
676
  * @returns {Promise<void>} - Resolves after fenced releases and dispatch wake-up.
671
677
  */
672
- async _releaseWorkerHandoffs(worker) {
678
+ async _releaseWorkerHandoffs(worker, {queueRedrain = false} = {}) {
673
679
  const handoffs = this.workerHandoffs.get(worker)
674
680
 
675
681
  if (!handoffs || handoffs.size === 0) {
@@ -683,7 +689,11 @@ export default class BackgroundJobsMain {
683
689
 
684
690
  this.workerHandoffs.delete(worker)
685
691
  this._notifyEnqueued()
686
- await this._drain()
692
+ if (queueRedrain) {
693
+ this._redrainQueued = true
694
+ } else {
695
+ await this._drain()
696
+ }
687
697
  }
688
698
 
689
699
  /**
@@ -1100,23 +1110,38 @@ export default class BackgroundJobsMain {
1100
1110
  * @returns {Promise<void>}
1101
1111
  */
1102
1112
  async _drain() {
1103
- if (!this._startDrain()) return
1113
+ if (this._stopped) return
1104
1114
 
1105
- const errored = await this._drainUntilIdle()
1115
+ if (this._drainPromise) {
1116
+ this._redrainQueued = true
1117
+ await this._drainPromise
1118
+ return
1119
+ }
1120
+
1121
+ const drainPromise = this._drainToCompletion()
1106
1122
 
1107
- await this._finishDrain({errored})
1123
+ this._drainPromise = drainPromise
1124
+ await drainPromise
1108
1125
  }
1109
1126
 
1110
1127
  /**
1111
- * Runs start drain.
1112
- * @returns {boolean} - Whether the drain should continue.
1128
+ * Runs one serialized drain lifecycle, including timer re-arming.
1129
+ * @returns {Promise<void>} - Resolves after every coalesced request is handled.
1113
1130
  */
1114
- _startDrain() {
1115
- if (this._stopped) return false
1116
- if (this._queueDrainIfAlreadyRunning()) return false
1117
-
1131
+ async _drainToCompletion() {
1118
1132
  this._draining = true
1119
- return true
1133
+
1134
+ try {
1135
+ let errored
1136
+
1137
+ do {
1138
+ errored = await this._drainUntilIdle()
1139
+ await this._finishDrain({errored})
1140
+ } while (!errored && this._redrainQueued && !this._stopped)
1141
+ } finally {
1142
+ this._draining = false
1143
+ this._drainPromise = undefined
1144
+ }
1120
1145
  }
1121
1146
 
1122
1147
  /**
@@ -1162,27 +1187,12 @@ export default class BackgroundJobsMain {
1162
1187
  }
1163
1188
  }
1164
1189
 
1165
- /**
1166
- * Runs queue drain if already running.
1167
- * @returns {boolean} - Whether another drain is already in progress.
1168
- */
1169
- _queueDrainIfAlreadyRunning() {
1170
- if (!this._draining) return false
1171
-
1172
- this._redrainQueued = true
1173
- return true
1174
- }
1175
-
1176
1190
  /**
1177
1191
  * Runs drain until idle.
1178
1192
  * @returns {Promise<boolean>} - Whether the drain hit an error.
1179
1193
  */
1180
1194
  async _drainUntilIdle() {
1181
- try {
1182
- return await this._runDrainLoop()
1183
- } finally {
1184
- this._draining = false
1185
- }
1195
+ return await this._runDrainLoop()
1186
1196
  }
1187
1197
 
1188
1198
  /**
@@ -1279,7 +1289,7 @@ export default class BackgroundJobsMain {
1279
1289
  if (!handoffs || !this.workers.has(worker)) {
1280
1290
  await this.store.markReturnedToQueue({handoffId: handoff.handoffId, jobId: job.id})
1281
1291
  this._notifyEnqueued()
1282
- await this._drain()
1292
+ this._redrainQueued = true
1283
1293
  continue
1284
1294
  }
1285
1295
 
@@ -1307,7 +1317,7 @@ export default class BackgroundJobsMain {
1307
1317
  } catch (closeError) {
1308
1318
  this.logger.warn(() => ["Failed to close worker after job send failure:", closeError])
1309
1319
  }
1310
- await this._handleWorkerSocketClosed(worker)
1320
+ await this._handleWorkerSocketClosed(worker, {queueRedrain: true})
1311
1321
  }
1312
1322
  }
1313
1323
  }
@@ -23,16 +23,18 @@ export default class PooledRunnerBrokerIdentity {
23
23
  /**
24
24
  * Prepares one identity, sharing an in-flight same-identity rotation.
25
25
  * @param {import("../testing/shared-transaction-proxy-driver.js").SharedTransactionBrokerJobConfig} config - Dispatch configuration.
26
+ * @param {boolean} [admissionReserved] - Whether the caller already reserved its active-user slot.
26
27
  * @returns {Promise<void>} - Resolves after stale connections close.
27
28
  */
28
- async prepare(config) {
29
+ async prepare(config, admissionReserved = false) {
29
30
  const identity = JSON.stringify(config)
30
31
  if (this.pending) {
31
32
  if (this.pending.identity !== identity) throw new Error("Pooled runner cannot mix shared transaction broker capabilities concurrently")
32
33
  return await this.pending.promise
33
34
  }
34
35
  if (this.activeIdentity === identity) return
35
- if (this.activeUsers > 0) throw new Error("Pooled runner cannot mix shared transaction broker capabilities concurrently")
36
+ const otherActiveUsers = this.activeUsers - (admissionReserved ? 1 : 0)
37
+ if (otherActiveUsers > 0) throw new Error("Pooled runner cannot mix shared transaction broker capabilities concurrently")
36
38
  if (this.activeIdentity === undefined) {
37
39
  this.activeIdentity = identity
38
40
  return
@@ -55,8 +57,7 @@ export default class PooledRunnerBrokerIdentity {
55
57
  * @returns {Promise<T>} - Job result.
56
58
  */
57
59
  async run(config, callback) {
58
- await this.prepare(config)
59
- this.activeUsers++
60
+ await this.admit(config)
60
61
  try {
61
62
  return await callback()
62
63
  } finally {
@@ -64,6 +65,23 @@ export default class PooledRunnerBrokerIdentity {
64
65
  }
65
66
  }
66
67
 
68
+ /**
69
+ * Atomically prepares an attempt identity and reserves its active user. Without
70
+ * this admission turn, another capability can rotate connections after `prepare`
71
+ * resolves but before `run` increments `activeUsers`.
72
+ * @param {import("../testing/shared-transaction-proxy-driver.js").SharedTransactionBrokerJobConfig} config - Dispatch configuration.
73
+ * @returns {Promise<void>} - Resolves after admission is reserved.
74
+ */
75
+ async admit(config) {
76
+ this.activeUsers++
77
+ try {
78
+ await this.prepare(config, true)
79
+ } catch (error) {
80
+ this.activeUsers--
81
+ throw error
82
+ }
83
+ }
84
+
67
85
  /**
68
86
  * Rotates retained connection state to an identity.
69
87
  * @param {string} identity - Target identity.