velocious 1.0.643 → 1.0.645

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 (279) hide show
  1. package/README.md +21 -10
  2. package/build/authorization/ability.js +36 -13
  3. package/build/background-jobs/adapter.js +9 -0
  4. package/build/background-jobs/main.js +35 -10
  5. package/build/background-jobs/store.js +217 -38
  6. package/build/background-jobs/types.js +15 -0
  7. package/build/configuration-types.js +1 -1
  8. package/build/configuration.js +45 -9
  9. package/build/database/drivers/base-column.js +8 -0
  10. package/build/database/drivers/mssql/sql/update.js +1 -3
  11. package/build/database/drivers/mysql/sql/update.js +1 -3
  12. package/build/database/drivers/pgsql/column.js +22 -0
  13. package/build/database/drivers/pgsql/sql/update.js +1 -3
  14. package/build/database/drivers/sqlite/sql/alter-table.js +11 -1
  15. package/build/database/drivers/sqlite/sql/update.js +1 -3
  16. package/build/database/query/index.js +2 -2
  17. package/build/database/query/model-class-query.js +26 -10
  18. package/build/database/query/preloader/has-many.js +4 -1
  19. package/build/database/query/query-data.js +2 -1
  20. package/build/database/query/update-base.js +14 -0
  21. package/build/database/query/with-count.js +3 -1
  22. package/build/database/record/acts-as-list.js +37 -13
  23. package/build/database/record/attachments/store.js +336 -26
  24. package/build/database/record/auditing.js +6 -3
  25. package/build/database/record/index.js +197 -42
  26. package/build/database/record/instance-relationships/belongs-to.js +2 -1
  27. package/build/database/record/validators/uniqueness.js +8 -5
  28. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -8
  29. package/build/frontend-model-controller.js +136 -53
  30. package/build/frontend-model-resource/base-resource.js +87 -37
  31. package/build/frontend-model-resource/velocious-attachment-resource.js +98 -31
  32. package/build/frontend-models/base.js +307 -59
  33. package/build/frontend-models/preloader.js +6 -3
  34. package/build/frontend-models/query.js +15 -6
  35. package/build/frontend-models/resource-definition.js +21 -2
  36. package/build/frontend-models/use-destroyed-event.js +1 -1
  37. package/build/frontend-models/use-model-class-event.js +2 -2
  38. package/build/frontend-models/use-updated-event.js +1 -1
  39. package/build/frontend-models/websocket-channel.js +256 -93
  40. package/build/frontend-models/websocket-publishers.js +218 -18
  41. package/build/http-server/client/websocket-session.js +13 -1
  42. package/build/http-server/websocket-channel.js +15 -2
  43. package/build/src/authorization/ability.d.ts +9 -3
  44. package/build/src/authorization/ability.d.ts.map +1 -1
  45. package/build/src/authorization/ability.js +33 -14
  46. package/build/src/background-jobs/adapter.d.ts +6 -0
  47. package/build/src/background-jobs/adapter.d.ts.map +1 -1
  48. package/build/src/background-jobs/adapter.js +9 -1
  49. package/build/src/background-jobs/main.d.ts +6 -0
  50. package/build/src/background-jobs/main.d.ts.map +1 -1
  51. package/build/src/background-jobs/main.js +34 -11
  52. package/build/src/background-jobs/store.d.ts +66 -12
  53. package/build/src/background-jobs/store.d.ts.map +1 -1
  54. package/build/src/background-jobs/store.js +191 -35
  55. package/build/src/background-jobs/types.d.ts +55 -0
  56. package/build/src/background-jobs/types.d.ts.map +1 -1
  57. package/build/src/background-jobs/types.js +16 -1
  58. package/build/src/configuration-types.d.ts +3 -3
  59. package/build/src/configuration-types.d.ts.map +1 -1
  60. package/build/src/configuration-types.js +2 -2
  61. package/build/src/configuration.d.ts +18 -8
  62. package/build/src/configuration.d.ts.map +1 -1
  63. package/build/src/configuration.js +38 -7
  64. package/build/src/database/drivers/base-column.d.ts +5 -0
  65. package/build/src/database/drivers/base-column.d.ts.map +1 -1
  66. package/build/src/database/drivers/base-column.js +8 -1
  67. package/build/src/database/drivers/mssql/sql/update.d.ts.map +1 -1
  68. package/build/src/database/drivers/mssql/sql/update.js +2 -4
  69. package/build/src/database/drivers/mysql/sql/update.d.ts.map +1 -1
  70. package/build/src/database/drivers/mysql/sql/update.js +2 -4
  71. package/build/src/database/drivers/pgsql/column.d.ts +5 -0
  72. package/build/src/database/drivers/pgsql/column.d.ts.map +1 -1
  73. package/build/src/database/drivers/pgsql/column.js +19 -1
  74. package/build/src/database/drivers/pgsql/sql/update.d.ts.map +1 -1
  75. package/build/src/database/drivers/pgsql/sql/update.js +2 -4
  76. package/build/src/database/drivers/sqlite/sql/alter-table.d.ts.map +1 -1
  77. package/build/src/database/drivers/sqlite/sql/alter-table.js +6 -2
  78. package/build/src/database/drivers/sqlite/sql/update.d.ts.map +1 -1
  79. package/build/src/database/drivers/sqlite/sql/update.js +2 -4
  80. package/build/src/database/query/index.d.ts +4 -4
  81. package/build/src/database/query/index.d.ts.map +1 -1
  82. package/build/src/database/query/index.js +3 -3
  83. package/build/src/database/query/model-class-query.d.ts +8 -2
  84. package/build/src/database/query/model-class-query.d.ts.map +1 -1
  85. package/build/src/database/query/model-class-query.js +25 -11
  86. package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
  87. package/build/src/database/query/preloader/has-many.js +4 -2
  88. package/build/src/database/query/query-data.d.ts.map +1 -1
  89. package/build/src/database/query/query-data.js +3 -2
  90. package/build/src/database/query/update-base.d.ts +7 -0
  91. package/build/src/database/query/update-base.d.ts.map +1 -1
  92. package/build/src/database/query/update-base.js +13 -1
  93. package/build/src/database/query/with-count.d.ts.map +1 -1
  94. package/build/src/database/query/with-count.js +3 -2
  95. package/build/src/database/record/acts-as-list.d.ts.map +1 -1
  96. package/build/src/database/record/acts-as-list.js +35 -14
  97. package/build/src/database/record/attachments/store.d.ts +67 -0
  98. package/build/src/database/record/attachments/store.d.ts.map +1 -1
  99. package/build/src/database/record/attachments/store.js +301 -25
  100. package/build/src/database/record/auditing.d.ts.map +1 -1
  101. package/build/src/database/record/auditing.js +7 -4
  102. package/build/src/database/record/index.d.ts +78 -26
  103. package/build/src/database/record/index.d.ts.map +1 -1
  104. package/build/src/database/record/index.js +171 -42
  105. package/build/src/database/record/instance-relationships/belongs-to.d.ts.map +1 -1
  106. package/build/src/database/record/instance-relationships/belongs-to.js +3 -2
  107. package/build/src/database/record/validators/uniqueness.d.ts.map +1 -1
  108. package/build/src/database/record/validators/uniqueness.js +8 -6
  109. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +4 -4
  110. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
  111. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -12
  112. package/build/src/frontend-model-controller.d.ts +29 -8
  113. package/build/src/frontend-model-controller.d.ts.map +1 -1
  114. package/build/src/frontend-model-controller.js +122 -51
  115. package/build/src/frontend-model-resource/base-resource.d.ts +51 -24
  116. package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
  117. package/build/src/frontend-model-resource/base-resource.js +83 -37
  118. package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts +45 -26
  119. package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts.map +1 -1
  120. package/build/src/frontend-model-resource/velocious-attachment-resource.js +89 -31
  121. package/build/src/frontend-models/base.d.ts +53 -20
  122. package/build/src/frontend-models/base.d.ts.map +1 -1
  123. package/build/src/frontend-models/base.js +275 -59
  124. package/build/src/frontend-models/preloader.d.ts.map +1 -1
  125. package/build/src/frontend-models/preloader.js +6 -4
  126. package/build/src/frontend-models/query.d.ts +2 -2
  127. package/build/src/frontend-models/query.d.ts.map +1 -1
  128. package/build/src/frontend-models/query.js +14 -7
  129. package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
  130. package/build/src/frontend-models/resource-definition.js +20 -3
  131. package/build/src/frontend-models/use-destroyed-event.d.ts +1 -1
  132. package/build/src/frontend-models/use-destroyed-event.d.ts.map +1 -1
  133. package/build/src/frontend-models/use-destroyed-event.js +2 -2
  134. package/build/src/frontend-models/use-model-class-event.d.ts +2 -2
  135. package/build/src/frontend-models/use-model-class-event.d.ts.map +1 -1
  136. package/build/src/frontend-models/use-model-class-event.js +3 -3
  137. package/build/src/frontend-models/use-updated-event.d.ts +1 -1
  138. package/build/src/frontend-models/use-updated-event.d.ts.map +1 -1
  139. package/build/src/frontend-models/use-updated-event.js +2 -2
  140. package/build/src/frontend-models/websocket-channel.d.ts +60 -41
  141. package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
  142. package/build/src/frontend-models/websocket-channel.js +225 -85
  143. package/build/src/frontend-models/websocket-publishers.d.ts +9 -0
  144. package/build/src/frontend-models/websocket-publishers.d.ts.map +1 -1
  145. package/build/src/frontend-models/websocket-publishers.js +190 -19
  146. package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
  147. package/build/src/http-server/client/websocket-session.js +11 -2
  148. package/build/src/http-server/websocket-channel.d.ts +20 -8
  149. package/build/src/http-server/websocket-channel.d.ts.map +1 -1
  150. package/build/src/http-server/websocket-channel.js +15 -3
  151. package/build/src/sync/assets/cache.d.ts +288 -0
  152. package/build/src/sync/assets/cache.d.ts.map +1 -0
  153. package/build/src/sync/assets/cache.js +883 -0
  154. package/build/src/sync/assets/types.d.ts +199 -0
  155. package/build/src/sync/assets/types.d.ts.map +1 -0
  156. package/build/src/sync/assets/types.js +56 -0
  157. package/build/src/sync/sync-api-client.d.ts.map +1 -1
  158. package/build/src/sync/sync-api-client.js +4 -3
  159. package/build/src/sync/sync-client.d.ts.map +1 -1
  160. package/build/src/sync/sync-client.js +3 -2
  161. package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
  162. package/build/src/sync/sync-envelope-replay-service.js +6 -5
  163. package/build/src/sync/sync-publisher.d.ts.map +1 -1
  164. package/build/src/sync/sync-publisher.js +4 -3
  165. package/build/src/testing/browser-frontend-model-event-hook-scenarios.d.ts +2 -2
  166. package/build/src/testing/browser-frontend-model-event-hook-scenarios.d.ts.map +1 -1
  167. package/build/src/testing/browser-frontend-model-event-hook-scenarios.js +3 -3
  168. package/build/src/testing/test-profiler.d.ts +4 -4
  169. package/build/src/testing/test-profiler.d.ts.map +1 -1
  170. package/build/src/testing/test-profiler.js +3 -3
  171. package/build/src/testing/test-runner.d.ts +279 -117
  172. package/build/src/testing/test-runner.d.ts.map +1 -1
  173. package/build/src/testing/test-runner.js +503 -933
  174. package/build/src/testing/test.d.ts +11 -72
  175. package/build/src/testing/test.d.ts.map +1 -1
  176. package/build/src/testing/test.js +131 -284
  177. package/build/src/testing/velocious-attempt-executor.d.ts +54 -0
  178. package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -0
  179. package/build/src/testing/velocious-attempt-executor.js +408 -0
  180. package/build/src/testing/velocious-runner-reporter.d.ts +67 -0
  181. package/build/src/testing/velocious-runner-reporter.d.ts.map +1 -0
  182. package/build/src/testing/velocious-runner-reporter.js +240 -0
  183. package/build/src/testing/velocious-suite-hook-executor.d.ts +19 -0
  184. package/build/src/testing/velocious-suite-hook-executor.d.ts.map +1 -0
  185. package/build/src/testing/velocious-suite-hook-executor.js +41 -0
  186. package/build/src/testing/velocious-test-arguments.d.ts +39 -0
  187. package/build/src/testing/velocious-test-arguments.d.ts.map +1 -0
  188. package/build/src/testing/velocious-test-arguments.js +55 -0
  189. package/build/src/utils/model-primary-key.d.ts +73 -0
  190. package/build/src/utils/model-primary-key.d.ts.map +1 -0
  191. package/build/src/utils/model-primary-key.js +144 -0
  192. package/build/src/utils/sha256-bytes-hex.d.ts +7 -0
  193. package/build/src/utils/sha256-bytes-hex.d.ts.map +1 -0
  194. package/build/src/utils/sha256-bytes-hex.js +117 -0
  195. package/build/src/utils/sha256-hex.d.ts +2 -4
  196. package/build/src/utils/sha256-hex.d.ts.map +1 -1
  197. package/build/src/utils/sha256-hex.js +6 -120
  198. package/build/sync/assets/cache.js +998 -0
  199. package/build/sync/assets/types.js +62 -0
  200. package/build/sync/sync-api-client.js +3 -2
  201. package/build/sync/sync-client.js +2 -1
  202. package/build/sync/sync-envelope-replay-service.js +5 -4
  203. package/build/sync/sync-publisher.js +3 -2
  204. package/build/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
  205. package/build/testing/test-profiler.js +2 -2
  206. package/build/testing/test-runner.js +496 -990
  207. package/build/testing/test.js +164 -306
  208. package/build/testing/velocious-attempt-executor.js +441 -0
  209. package/build/testing/velocious-runner-reporter.js +264 -0
  210. package/build/testing/velocious-suite-hook-executor.js +43 -0
  211. package/build/testing/velocious-test-arguments.js +64 -0
  212. package/build/tsconfig.tsbuildinfo +1 -1
  213. package/build/utils/model-primary-key.js +171 -0
  214. package/build/utils/sha256-bytes-hex.js +132 -0
  215. package/build/utils/sha256-hex.js +7 -135
  216. package/package.json +5 -4
  217. package/src/authorization/ability.js +36 -13
  218. package/src/background-jobs/adapter.js +9 -0
  219. package/src/background-jobs/main.js +35 -10
  220. package/src/background-jobs/store.js +217 -38
  221. package/src/background-jobs/types.js +15 -0
  222. package/src/configuration-types.js +1 -1
  223. package/src/configuration.js +45 -9
  224. package/src/database/drivers/base-column.js +8 -0
  225. package/src/database/drivers/mssql/sql/update.js +1 -3
  226. package/src/database/drivers/mysql/sql/update.js +1 -3
  227. package/src/database/drivers/pgsql/column.js +22 -0
  228. package/src/database/drivers/pgsql/sql/update.js +1 -3
  229. package/src/database/drivers/sqlite/sql/alter-table.js +11 -1
  230. package/src/database/drivers/sqlite/sql/update.js +1 -3
  231. package/src/database/query/index.js +2 -2
  232. package/src/database/query/model-class-query.js +26 -10
  233. package/src/database/query/preloader/has-many.js +4 -1
  234. package/src/database/query/query-data.js +2 -1
  235. package/src/database/query/update-base.js +14 -0
  236. package/src/database/query/with-count.js +3 -1
  237. package/src/database/record/acts-as-list.js +37 -13
  238. package/src/database/record/attachments/store.js +336 -26
  239. package/src/database/record/auditing.js +6 -3
  240. package/src/database/record/index.js +197 -42
  241. package/src/database/record/instance-relationships/belongs-to.js +2 -1
  242. package/src/database/record/validators/uniqueness.js +8 -5
  243. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -8
  244. package/src/frontend-model-controller.js +136 -53
  245. package/src/frontend-model-resource/base-resource.js +87 -37
  246. package/src/frontend-model-resource/velocious-attachment-resource.js +98 -31
  247. package/src/frontend-models/base.js +307 -59
  248. package/src/frontend-models/preloader.js +6 -3
  249. package/src/frontend-models/query.js +15 -6
  250. package/src/frontend-models/resource-definition.js +21 -2
  251. package/src/frontend-models/use-destroyed-event.js +1 -1
  252. package/src/frontend-models/use-model-class-event.js +2 -2
  253. package/src/frontend-models/use-updated-event.js +1 -1
  254. package/src/frontend-models/websocket-channel.js +256 -93
  255. package/src/frontend-models/websocket-publishers.js +218 -18
  256. package/src/http-server/client/websocket-session.js +13 -1
  257. package/src/http-server/websocket-channel.js +15 -2
  258. package/src/sync/assets/cache.js +998 -0
  259. package/src/sync/assets/types.js +62 -0
  260. package/src/sync/sync-api-client.js +3 -2
  261. package/src/sync/sync-client.js +2 -1
  262. package/src/sync/sync-envelope-replay-service.js +5 -4
  263. package/src/sync/sync-publisher.js +3 -2
  264. package/src/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
  265. package/src/testing/test-profiler.js +2 -2
  266. package/src/testing/test-runner.js +496 -990
  267. package/src/testing/test.js +164 -306
  268. package/src/testing/velocious-attempt-executor.js +441 -0
  269. package/src/testing/velocious-runner-reporter.js +264 -0
  270. package/src/testing/velocious-suite-hook-executor.js +43 -0
  271. package/src/testing/velocious-test-arguments.js +64 -0
  272. package/src/utils/model-primary-key.js +171 -0
  273. package/src/utils/sha256-bytes-hex.js +132 -0
  274. package/src/utils/sha256-hex.js +7 -135
  275. package/build/src/testing/testing-package-adapter.d.ts +0 -9
  276. package/build/src/testing/testing-package-adapter.d.ts.map +0 -1
  277. package/build/src/testing/testing-package-adapter.js +0 -69
  278. package/build/testing/testing-package-adapter.js +0 -76
  279. package/src/testing/testing-package-adapter.js +0 -76
@@ -56,6 +56,20 @@ import {
56
56
  * @property {{failureMessage: string, name: string}} [advisoryLock] - Session lock held around the transaction.
57
57
  */
58
58
 
59
+ /**
60
+ * BackgroundJobConcurrencyCountRow type.
61
+ * @typedef {object} BackgroundJobConcurrencyCountRow
62
+ * @property {number | string} active_count - Persisted or aggregated active count.
63
+ * @property {string} concurrency_key - Durable cap identity.
64
+ */
65
+
66
+ /**
67
+ * BackgroundJobQueuedConcurrency type.
68
+ * @typedef {object} BackgroundJobQueuedConcurrency
69
+ * @property {string | null} concurrencyKey - Current concurrency key for queued work.
70
+ * @property {number | null} maxConcurrency - Current concurrency cap for queued work.
71
+ */
72
+
59
73
  const MIGRATIONS_TABLE = "velocious_internal_migrations"
60
74
  const MIGRATION_SCOPE = "background_jobs"
61
75
  const MIGRATION_VERSION = "20250215000000"
@@ -89,6 +103,8 @@ const SCHEDULE_KEYS_TABLE = "background_job_schedule_keys"
89
103
  const CONCURRENCY_TABLE = "background_job_concurrency"
90
104
  const COUNTS_REVISION_TABLE = "background_job_count_revisions"
91
105
  const COUNTS_REVISION_KEY = "counts"
106
+ const CONCURRENCY_RECONCILIATION_LOCK = "background-jobs:queue-concurrency-reconcile"
107
+ const CONCURRENCY_REPAIR_SAMPLE_LIMIT = 10
92
108
  export const BACKGROUND_JOB_COUNTS_CHANNEL = "velocious-background-job-counts"
93
109
  export const BACKGROUND_JOB_COUNT_BUCKETS = ["all", "queued", "handed_off", "completed", "failed", "orphaned"]
94
110
  const COUNTED_JOB_STATUSES = BACKGROUND_JOB_COUNT_BUCKETS.slice(1)
@@ -222,8 +238,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
222
238
  await this.ensureReady()
223
239
 
224
240
  await this._withDb(async (db) => {
225
- const lockName = "background-jobs:queue-concurrency-reconcile"
226
- const acquired = await db.acquireAdvisoryLock(lockName)
241
+ const acquired = await db.acquireAdvisoryLock(CONCURRENCY_RECONCILIATION_LOCK)
227
242
 
228
243
  if (!acquired) throw new Error("Failed to acquire background job queue-concurrency reconcile lock")
229
244
 
@@ -236,7 +251,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
236
251
  // the counts (adoption itself is idempotent).
237
252
  this._queueConcurrencyReconciled = true
238
253
  } finally {
239
- await db.releaseAdvisoryLock(lockName)
254
+ await db.releaseAdvisoryLock(CONCURRENCY_RECONCILIATION_LOCK)
240
255
  }
241
256
  })
242
257
 
@@ -246,6 +261,44 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
246
261
  ])
247
262
  }
248
263
 
264
+ /**
265
+ * Repairs durable active-count drift while a main process remains live. The
266
+ * initial snapshot is read-only; only suspected mismatches take their
267
+ * counter lock and re-count inside the serialized transaction path.
268
+ * @returns {Promise<import("./types.js").BackgroundJobConcurrencyReconciliation>} - Repair summary.
269
+ */
270
+ async reconcileActiveConcurrency() {
271
+ const databaseIdentifier = this.getDatabaseIdentifier()
272
+ const startedAtMs = Date.now()
273
+
274
+ await this.ensureReady()
275
+
276
+ const result = await this._serializedConnectionMutation(
277
+ async (db) => await this._reconcileConcurrency(db),
278
+ {
279
+ advisoryLock: {
280
+ failureMessage: "Failed to acquire background job active-concurrency reconcile lock",
281
+ name: CONCURRENCY_RECONCILIATION_LOCK
282
+ }
283
+ }
284
+ )
285
+
286
+ if (result.repairedCount > 0) {
287
+ await this.logger.warn(() => [
288
+ "Repaired background jobs active-concurrency count drift",
289
+ {
290
+ databaseIdentifier,
291
+ durationMs: Date.now() - startedAtMs,
292
+ repairedCount: result.repairedCount,
293
+ repairs: result.repairs,
294
+ repairsTruncatedCount: result.repairsTruncatedCount
295
+ }
296
+ ])
297
+ }
298
+
299
+ return result
300
+ }
301
+
249
302
  /**
250
303
  * Runs enqueue.
251
304
  * @param {object} args - Options.
@@ -949,8 +1002,11 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
949
1002
  const handedOffAtMs = Date.now()
950
1003
 
951
1004
  return await this._serializedCountMutation(async (db) => {
952
- const queuedJob = await this._getJobRowById(db, jobId)
953
- if (!queuedJob || queuedJob.status !== "queued") return null
1005
+ const selectedJob = await this._getJobRowById(db, jobId)
1006
+ if (!selectedJob || selectedJob.status !== "queued") return null
1007
+ const queuedJob = await this._reconcileQueuedJobConcurrency(db, selectedJob)
1008
+
1009
+ if (!queuedJob) return null
954
1010
  if (queuedJob.concurrencyKey && !(await this._reserveConcurrency(db, queuedJob.concurrencyKey))) return null
955
1011
  const affectedRows = await this._updateAffectedRows(db, {
956
1012
  tableName: JOBS_TABLE,
@@ -960,7 +1016,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
960
1016
  handoff_id: handoffId,
961
1017
  worker_id: workerId || null
962
1018
  },
963
- conditions: {id: jobId, status: "queued"}
1019
+ conditions: {concurrency_key: queuedJob.concurrencyKey, id: jobId, status: "queued"}
964
1020
  })
965
1021
 
966
1022
  if (affectedRows !== 1) {
@@ -969,7 +1025,16 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
969
1025
  }
970
1026
 
971
1027
  await this._recordStatusTransition(db, "queued", "handed_off")
972
- return {handedOffAtMs, handoffId}
1028
+ /** @type {import("./types.js").BackgroundJobRow} */
1029
+ const handedOffJob = {
1030
+ ...queuedJob,
1031
+ handedOffAtMs,
1032
+ handoffId,
1033
+ status: "handed_off",
1034
+ workerId: workerId || null
1035
+ }
1036
+
1037
+ return {handedOffAtMs, handoffId, job: handedOffJob}
973
1038
  })
974
1039
  }
975
1040
 
@@ -2355,6 +2420,41 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2355
2420
  })
2356
2421
  }
2357
2422
 
2423
+ /**
2424
+ * Applies the active generation's queue policy immediately before handoff.
2425
+ * Explicit concurrency remains owned by the enqueue request.
2426
+ * @param {import("../database/drivers/base.js").default} db - Database connection.
2427
+ * @param {import("./types.js").BackgroundJobRow} job - Queued job snapshot.
2428
+ * @returns {Promise<import("./types.js").BackgroundJobRow | null>} - Reconciled job, or null when its queued-state fence lost.
2429
+ */
2430
+ async _reconcileQueuedJobConcurrency(db, job) {
2431
+ if (job.concurrencyKey && !job.concurrencyKey.startsWith(QUEUE_CONCURRENCY_KEY_PREFIX)) {
2432
+ return job
2433
+ }
2434
+
2435
+ const concurrency = this._resolveConcurrency({}, job.queue)
2436
+ /** @type {BackgroundJobQueuedConcurrency} */
2437
+ const current = concurrency
2438
+ ? {concurrencyKey: concurrency.concurrencyKey, maxConcurrency: concurrency.maxConcurrency}
2439
+ : {concurrencyKey: null, maxConcurrency: null}
2440
+
2441
+ if (concurrency) await this._ensureQueueConcurrencyKey(db, concurrency)
2442
+ if (job.concurrencyKey === current.concurrencyKey && job.maxConcurrency === current.maxConcurrency) return job
2443
+
2444
+ const affectedRows = await this._updateAffectedRows(db, {
2445
+ tableName: JOBS_TABLE,
2446
+ data: {
2447
+ concurrency_key: current.concurrencyKey,
2448
+ max_concurrency: current.maxConcurrency
2449
+ },
2450
+ conditions: {concurrency_key: job.concurrencyKey, id: job.id, status: "queued"}
2451
+ })
2452
+
2453
+ if (affectedRows !== 1) return null
2454
+
2455
+ return {...job, concurrencyKey: current.concurrencyKey, maxConcurrency: current.maxConcurrency}
2456
+ }
2457
+
2358
2458
  /**
2359
2459
  * Reads the configured max concurrency for a queue from the background-jobs config.
2360
2460
  * @param {string} queue - Queue name.
@@ -2763,14 +2863,19 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2763
2863
  /**
2764
2864
  * Rebuilds durable counts from active handoffs.
2765
2865
  * @param {import("../database/drivers/base.js").default} db - Database connection.
2766
- * @returns {Promise<void>} - Resolves when reconciled.
2866
+ * @param {{insideTransaction?: boolean}} [options] - Reuse an enclosing transaction.
2867
+ * @returns {Promise<import("./types.js").BackgroundJobConcurrencyReconciliation>} - Repair summary.
2767
2868
  */
2768
- async _reconcileConcurrency(db) {
2769
- if (!(await db.tableExists(CONCURRENCY_TABLE))) return
2869
+ async _reconcileConcurrency(db, {insideTransaction = false} = {}) {
2870
+ if (!(await db.tableExists(CONCURRENCY_TABLE))) {
2871
+ return {candidateCount: 0, checkedCount: 0, repairedCount: 0, repairs: [], repairsTruncatedCount: 0}
2872
+ }
2873
+
2770
2874
  const activeRows = await db
2771
2875
  .newQuery()
2772
2876
  .from(JOBS_TABLE)
2773
2877
  .select("concurrency_key")
2878
+ .select("COUNT(*) AS active_count")
2774
2879
  .where({status: "handed_off"})
2775
2880
  .where(`${db.quoteColumn("concurrency_key")} IS NOT NULL`)
2776
2881
  .group("concurrency_key")
@@ -2779,18 +2884,49 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2779
2884
  .newQuery()
2780
2885
  .from(CONCURRENCY_TABLE)
2781
2886
  .select("concurrency_key")
2887
+ .select("active_count")
2782
2888
  .where(`${db.quoteColumn("active_count")} != 0`)
2783
2889
  .results()
2784
- const concurrencyKeys = new Set(
2785
- [...activeRows, ...staleRows].map((row) =>
2786
- String(/** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (row).concurrency_key)
2787
- )
2788
- )
2890
+ /** @type {Map<string, number>} */
2891
+ const activeCounts = new Map()
2892
+ /** @type {Map<string, number>} */
2893
+ const persistedCounts = new Map()
2789
2894
 
2790
- for (const concurrencyKey of [...concurrencyKeys].sort()) {
2791
- await this._transactionResult(db, async () => {
2792
- await this._reconcileConcurrencyKey(db, concurrencyKey)
2793
- })
2895
+ for (const rawRow of activeRows) {
2896
+ const row = /** @type {BackgroundJobConcurrencyCountRow} */ (rawRow)
2897
+ activeCounts.set(row.concurrency_key, this._validatedConcurrencyCount(row.active_count, row.concurrency_key))
2898
+ }
2899
+
2900
+ for (const rawRow of staleRows) {
2901
+ const row = /** @type {BackgroundJobConcurrencyCountRow} */ (rawRow)
2902
+ persistedCounts.set(row.concurrency_key, this._validatedConcurrencyCount(row.active_count, row.concurrency_key))
2903
+ }
2904
+
2905
+ const concurrencyKeys = [...new Set([...activeCounts.keys(), ...persistedCounts.keys()])].sort()
2906
+ const candidateKeys = concurrencyKeys.filter((concurrencyKey) => {
2907
+ return (activeCounts.get(concurrencyKey) || 0) !== (persistedCounts.get(concurrencyKey) || 0)
2908
+ })
2909
+ /** @type {import("./types.js").BackgroundJobConcurrencyRepair[]} */
2910
+ const repairs = []
2911
+ let repairedCount = 0
2912
+
2913
+ for (const concurrencyKey of candidateKeys) {
2914
+ const repair = insideTransaction
2915
+ ? await this._reconcileConcurrencyKey(db, concurrencyKey)
2916
+ : await this._transactionResult(db, async () => await this._reconcileConcurrencyKey(db, concurrencyKey))
2917
+
2918
+ if (!repair) continue
2919
+
2920
+ repairedCount++
2921
+ if (repairs.length < CONCURRENCY_REPAIR_SAMPLE_LIMIT) repairs.push(repair)
2922
+ }
2923
+
2924
+ return {
2925
+ candidateCount: candidateKeys.length,
2926
+ checkedCount: concurrencyKeys.length,
2927
+ repairedCount,
2928
+ repairs,
2929
+ repairsTruncatedCount: repairedCount - repairs.length
2794
2930
  }
2795
2931
  }
2796
2932
 
@@ -2799,29 +2935,57 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2799
2935
  * lock order used by handoff and completion transitions.
2800
2936
  * @param {import("../database/drivers/base.js").default} db - Database connection.
2801
2937
  * @param {string} concurrencyKey - Counter key.
2802
- * @returns {Promise<void>} - Resolves when reconciled.
2938
+ * @returns {Promise<import("./types.js").BackgroundJobConcurrencyRepair | null>} - Applied repair.
2803
2939
  */
2804
2940
  async _reconcileConcurrencyKey(db, concurrencyKey) {
2805
2941
  await this._lockConcurrencyRow(db, concurrencyKey)
2942
+ const persistedRows = await db
2943
+ .newQuery()
2944
+ .from(CONCURRENCY_TABLE)
2945
+ .select("active_count")
2946
+ .select("concurrency_key")
2947
+ .where({concurrency_key: concurrencyKey})
2948
+ .limit(1)
2949
+ .results()
2950
+
2951
+ if (!persistedRows[0]) throw new Error(`Missing background job concurrency counter for ${concurrencyKey}`)
2952
+
2953
+ const persistedRow = /** @type {BackgroundJobConcurrencyCountRow} */ (persistedRows[0])
2954
+ const previousActiveCount = this._validatedConcurrencyCount(persistedRow.active_count, concurrencyKey)
2806
2955
  const rows = await db
2807
2956
  .newQuery()
2808
2957
  .from(JOBS_TABLE)
2809
2958
  .select("COUNT(*) AS active_count")
2810
2959
  .where({concurrency_key: concurrencyKey, status: "handed_off"})
2811
2960
  .results()
2812
- const activeCount = this._normalizeNumber(
2813
- /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (rows[0] || {}).active_count
2814
- )
2961
+ const countRow = /** @type {{active_count: number | string}} */ (rows[0])
2962
+ const activeCount = this._validatedConcurrencyCount(countRow.active_count, concurrencyKey)
2815
2963
 
2816
- if (activeCount === null || !Number.isSafeInteger(activeCount) || activeCount < 0) {
2817
- throw new Error(`Invalid reconciled background job concurrency count for ${concurrencyKey}: ${activeCount}`)
2818
- }
2964
+ if (activeCount === previousActiveCount) return null
2819
2965
 
2820
2966
  await db.update({
2821
2967
  tableName: CONCURRENCY_TABLE,
2822
2968
  data: {active_count: activeCount},
2823
2969
  conditions: {concurrency_key: concurrencyKey}
2824
2970
  })
2971
+
2972
+ return {activeCount, concurrencyKey, previousActiveCount}
2973
+ }
2974
+
2975
+ /**
2976
+ * Validates a database count before it participates in reconciliation.
2977
+ * @param {number | string} value - Raw count.
2978
+ * @param {string} concurrencyKey - Counter key for diagnostics.
2979
+ * @returns {number} - Safe non-negative count.
2980
+ */
2981
+ _validatedConcurrencyCount(value, concurrencyKey) {
2982
+ const count = this._normalizeNumber(value)
2983
+
2984
+ if (count === null || !Number.isSafeInteger(count) || count < 0) {
2985
+ throw new Error(`Invalid reconciled background job concurrency count for ${concurrencyKey}: ${count}`)
2986
+ }
2987
+
2988
+ return count
2825
2989
  }
2826
2990
 
2827
2991
  /**
@@ -2836,11 +3000,12 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2836
3000
  * while a backlog exists otherwise leaves persisted rows stale: pre-cap jobs
2837
3001
  * keep a null key and bypass the cap, post-removal jobs stay capped under a
2838
3002
  * now-unconfigured key, and a changed numeric cap stays stale until the next
2839
- * enqueue. Bring the durable state in line with config: sync each configured
2840
- * queue's stored cap, adopt not-yet-keyed non-terminal jobs onto their queue
2841
- * key, and release non-terminal jobs from queue keys whose queue is no
2842
- * longer capped. Runs before {@link _reconcileConcurrency} so the rebuilt
2843
- * active counts reflect the adopted/released keys.
3003
+ * enqueue. Bring queued durable state in line with config: sync each configured
3004
+ * queue's stored cap, adopt not-yet-keyed queued jobs onto their queue key,
3005
+ * and release queued jobs from queue keys whose queue is no longer capped.
3006
+ * Existing handoffs retain the policy and reservation they started with, so
3007
+ * reconciliation cannot race their completion/retry transitions. Runs before
3008
+ * {@link _reconcileConcurrency} so any pre-existing active counts are exact.
2844
3009
  * @param {import("../database/drivers/base.js").default} db - Database connection.
2845
3010
  * @returns {Promise<void>} - Resolves when reconciled.
2846
3011
  */
@@ -2853,7 +3018,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2853
3018
  const keyColumn = db.quoteColumn("concurrency_key")
2854
3019
  const capColumn = db.quoteColumn("max_concurrency")
2855
3020
  const queueColumn = db.quoteColumn("queue")
2856
- const nonTerminal = `${db.quoteColumn("status")} IN (${db.quote("queued")}, ${db.quote("handed_off")})`
3021
+ const queued = `${db.quoteColumn("status")} = ${db.quote("queued")}`
2857
3022
  /** @type {Set<string>} */
2858
3023
  const cappedQueues = new Set()
2859
3024
 
@@ -2868,7 +3033,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2868
3033
  await this._ensureQueueConcurrencyKey(db, {concurrencyKey, maxConcurrency: cap})
2869
3034
  await db.query(
2870
3035
  `UPDATE ${jobsTable} SET ${keyColumn} = ${db.quote(concurrencyKey)}, ${capColumn} = ${Number(cap)} ` +
2871
- `WHERE ${queueColumn} = ${db.quote(queue)} AND ${keyColumn} IS NULL AND ${nonTerminal}`
3036
+ `WHERE ${queueColumn} = ${db.quote(queue)} AND ${keyColumn} IS NULL AND ${queued}`
2872
3037
  )
2873
3038
  }
2874
3039
 
@@ -2887,7 +3052,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2887
3052
 
2888
3053
  await db.query(
2889
3054
  `UPDATE ${jobsTable} SET ${keyColumn} = NULL, ${capColumn} = NULL ` +
2890
- `WHERE ${keyColumn} = ${db.quote(concurrencyKey)} AND ${nonTerminal}`
3055
+ `WHERE ${keyColumn} = ${db.quote(concurrencyKey)} AND ${queued}`
2891
3056
  )
2892
3057
  }
2893
3058
  }
@@ -3025,15 +3190,29 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
3025
3190
  }
3026
3191
 
3027
3192
  /**
3028
- * Admits transactions to the process-local FIFO before they check out a
3029
- * connection. Cross-process ordering remains the responsibility of durable
3030
- * row/advisory locks and unique constraints acquired around the callback.
3193
+ * Runs a serialized callback inside one transaction.
3031
3194
  * @template T
3032
3195
  * @param {(db: import("../database/drivers/base.js").default) => Promise<T>} callback - Transaction callback.
3033
3196
  * @param {BackgroundJobTransactionSerializationOptions} [options] - Serialization options.
3034
3197
  * @returns {Promise<T>} Callback result.
3035
3198
  */
3036
3199
  async _serializedTransactionMutation(callback, options = {}) {
3200
+ return await this._serializedConnectionMutation(
3201
+ async (db) => await this._transactionResult(db, async () => await callback(db)),
3202
+ options
3203
+ )
3204
+ }
3205
+
3206
+ /**
3207
+ * Admits mutation callbacks to the process-local FIFO before they check out a
3208
+ * connection. Cross-process ordering remains the responsibility of durable
3209
+ * row/advisory locks and unique constraints acquired around the callback.
3210
+ * @template T
3211
+ * @param {(db: import("../database/drivers/base.js").default) => Promise<T>} callback - Connection callback.
3212
+ * @param {BackgroundJobTransactionSerializationOptions} [options] - Serialization options.
3213
+ * @returns {Promise<T>} Callback result.
3214
+ */
3215
+ async _serializedConnectionMutation(callback, options = {}) {
3037
3216
  const identifier = this.getDatabaseIdentifier() || "default"
3038
3217
  const previous = transactionMutationChains.get(identifier) || Promise.resolve()
3039
3218
  let resolveRun = () => {}
@@ -3057,7 +3236,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
3057
3236
  }
3058
3237
 
3059
3238
  try {
3060
- return await this._transactionResult(db, async () => await callback(db))
3239
+ return await callback(db)
3061
3240
  } finally {
3062
3241
  if (advisoryLock) await db.releaseAdvisoryLock(advisoryLock.name)
3063
3242
  }
@@ -51,6 +51,20 @@
51
51
  * @property {number} maxConcurrency - Positive cap.
52
52
  * @property {boolean} queueDerived - Whether queue configuration owns the cap.
53
53
  */
54
+ /**
55
+ * @typedef {object} BackgroundJobConcurrencyRepair
56
+ * @property {number} activeCount - Exact handed-off job count persisted by the repair.
57
+ * @property {string} concurrencyKey - Durable cap identity.
58
+ * @property {number} previousActiveCount - Persisted count replaced by the repair.
59
+ */
60
+ /**
61
+ * @typedef {object} BackgroundJobConcurrencyReconciliation
62
+ * @property {number} candidateCount - Snapshot mismatches rechecked under their counter locks.
63
+ * @property {number} checkedCount - Active or nonzero durable counters compared in the initial snapshot.
64
+ * @property {number} repairedCount - Counters whose persisted values were changed.
65
+ * @property {BackgroundJobConcurrencyRepair[]} repairs - Bounded deterministic sample of applied repairs.
66
+ * @property {number} repairsTruncatedCount - Applied repairs omitted from the sample.
67
+ */
54
68
  /**
55
69
  * @typedef {object} PreparedLocalBackgroundJob
56
70
  * @property {string} argsDigest - Fixed-width digest of the serialized arguments.
@@ -78,6 +92,7 @@
78
92
  * @typedef {object} BackgroundJobHandoff
79
93
  * @property {string} handoffId - Unique handoff lease id.
80
94
  * @property {number} handedOffAtMs - Time handed to a worker in ms.
95
+ * @property {BackgroundJobRow} [job] - Exact committed job snapshot when the adapter changes dispatch data during the claim.
81
96
  */
82
97
  /**
83
98
  * @typedef {object} BackgroundJobHandoffSnapshot
@@ -450,7 +450,7 @@
450
450
  * @property {string[]} [builtInMemberCommands] - Built-in member command names (`find`, `update`, `destroy`, `attach`, `download`, `url`).
451
451
  * @property {string} [modelName] - Frontend model name override.
452
452
  * @property {string[]} [relationships] - Relationship names to expose in frontend models. Type and target model are inferred from the backend model's registered relationships.
453
- * @property {string} [primaryKey] - Primary key attribute name.
453
+ * @property {string | string[]} [primaryKey] - Primary key attribute name or ordered composite primary-key attribute names.
454
454
  * @property {FrontendModelResourceServerConfiguration} [server] - Optional legacy backend behavior overrides for built-in frontend actions.
455
455
  * @property {FrontendModelResourceSyncConfiguration | boolean} [sync] - Optional safe local/offline sync policy metadata. `policy` participates in the hash but is not exposed to generated frontend config/manifest.
456
456
  */
@@ -229,6 +229,9 @@ export default class VelociousConfiguration {
229
229
  * @type {Set<import("./database/drivers/base.js").default>} */
230
230
  _advisoryLockConnections = new Set()
231
231
 
232
+ /** @type {Map<string, number>} */
233
+ _schemaCacheGenerationsByReuseKey = new Map()
234
+
232
235
  /**
233
236
  * Runs current.
234
237
  * @returns {VelociousConfiguration} - The current.
@@ -403,6 +406,13 @@ export default class VelociousConfiguration {
403
406
  * @type {Set<Promise<void>>} */
404
407
  this._localBroadcastDeliveries = new Set()
405
408
 
409
+ /**
410
+ * Latest local broadcast delivery per subscription. Chaining subsequent
411
+ * deliveries preserves lifecycle event order without coupling separate
412
+ * subscribers to one another.
413
+ * @type {WeakMap<import("./http-server/websocket-channel.js").default, Promise<void>>} */
414
+ this._localBroadcastDeliveryTails = new WeakMap()
415
+
406
416
  /**
407
417
  * Stores the websocket sessions value.
408
418
  * @type {Set<import("./http-server/client/websocket-session.js").default>} - Live websocket sessions, including paused sessions within the grace window.
@@ -1137,6 +1147,11 @@ export default class VelociousConfiguration {
1137
1147
  * @returns {void} - No return value.
1138
1148
  */
1139
1149
  clearSchemaCachesForReuseKey(reuseKey) {
1150
+ this._schemaCacheGenerationsByReuseKey.set(
1151
+ reuseKey,
1152
+ this.schemaCacheGenerationForReuseKey(reuseKey) + 1
1153
+ )
1154
+
1140
1155
  for (const pool of Object.values(this.databasePools)) {
1141
1156
  if (pool.getConfigurationReuseKey() === reuseKey) {
1142
1157
  pool.clearSchemaCache()
@@ -1144,6 +1159,15 @@ export default class VelociousConfiguration {
1144
1159
  }
1145
1160
  }
1146
1161
 
1162
+ /**
1163
+ * Returns the current schema-cache generation for one physical database.
1164
+ * @param {string} reuseKey - Connection reuse key identifying the shared database.
1165
+ * @returns {number} - Current schema-cache generation.
1166
+ */
1167
+ schemaCacheGenerationForReuseKey(reuseKey) {
1168
+ return this._schemaCacheGenerationsByReuseKey.get(reuseKey) || 0
1169
+ }
1170
+
1147
1171
  /**
1148
1172
  * Invalidates record metadata owned by one closed/deleted physical tenant
1149
1173
  * database while preserving every other tenant generation.
@@ -3249,7 +3273,7 @@ export default class VelociousConfiguration {
3249
3273
  * @param {string} name - Channel name.
3250
3274
  * @param {Record<string, ReturnType<typeof JSON.parse>>} broadcastParams - Params passed to each subscription's `matches()`.
3251
3275
  * @param {ReturnType<typeof JSON.parse>} body - Message body delivered via `sendMessage()`.
3252
- * @param {{eventId?: string}} [meta] - Optional event metadata for replay tracking.
3276
+ * @param {import("./http-server/websocket-channel.js").WebsocketBroadcastMetadata} [meta] - Optional event metadata for replay tracking.
3253
3277
  * @returns {void}
3254
3278
  */
3255
3279
  _broadcastToChannelLocal(name, broadcastParams, body, meta) {
@@ -3273,25 +3297,37 @@ export default class VelociousConfiguration {
3273
3297
 
3274
3298
  if (!matches) continue
3275
3299
 
3300
+ const deliveryMetadata = {
3301
+ broadcastParams,
3302
+ ...(meta?.eventId ? {eventId: meta.eventId} : {})
3303
+ }
3304
+ const previousDelivery = this._localBroadcastDeliveryTails.get(subscription)
3276
3305
  const delivery = this.withoutCurrentConnectionContexts(() => {
3277
- return Promise
3278
- .resolve()
3279
- .then(() => this._deliverWebsocketChannelBroadcast(subscription, body, {eventId: meta?.eventId}))
3306
+ return (previousDelivery || Promise.resolve())
3307
+ .then(() => this._deliverWebsocketChannelBroadcast(subscription, body, deliveryMetadata))
3280
3308
  .catch((error) => {
3281
3309
  console.error(`broadcastToChannel: ${name} subscription ${subscription.subscriptionId} deliverBroadcast threw`, error)
3282
3310
  })
3283
3311
  })
3284
3312
 
3313
+ this._localBroadcastDeliveryTails.set(subscription, delivery)
3314
+
3285
3315
  // Keep the fire-and-forget delivery (never awaited at broadcast time) but
3286
3316
  // track it so `awaitPendingBroadcasts` can drain it before settling. Remove
3287
3317
  // on settle; the failure handler also satisfies the promise so a rejected
3288
3318
  // delivery never becomes an unhandled rejection.
3289
3319
  this._localBroadcastDeliveries.add(delivery)
3290
3320
 
3291
- delivery.then(
3292
- () => { this._localBroadcastDeliveries.delete(delivery) },
3293
- () => { this._localBroadcastDeliveries.delete(delivery) }
3294
- )
3321
+ /**
3322
+ * Removes a settled delivery from local tracking.
3323
+ * @returns {void}
3324
+ */
3325
+ const forgetDelivery = () => {
3326
+ this._localBroadcastDeliveries.delete(delivery)
3327
+ if (this._localBroadcastDeliveryTails.get(subscription) === delivery) this._localBroadcastDeliveryTails.delete(subscription)
3328
+ }
3329
+
3330
+ delivery.then(forgetDelivery, forgetDelivery)
3295
3331
  }
3296
3332
  }
3297
3333
 
@@ -3314,7 +3350,7 @@ export default class VelociousConfiguration {
3314
3350
  * Runs deliver websocket channel broadcast.
3315
3351
  * @param {import("./http-server/websocket-channel.js").default} subscription - Channel subscription.
3316
3352
  * @param {import("./http-server/websocket-channel.js").WebsocketJsonValue} body - Broadcast body.
3317
- * @param {{eventId?: string}} meta - Broadcast metadata.
3353
+ * @param {import("./http-server/websocket-channel.js").WebsocketBroadcastMetadata} meta - Broadcast metadata.
3318
3354
  * @returns {void | Promise<void>} Broadcast delivery result.
3319
3355
  */
3320
3356
  _deliverWebsocketChannelBroadcast(subscription, body, meta) {
@@ -171,6 +171,14 @@ export default class VelociousDatabaseDriversBaseColumn {
171
171
  return match[1].toLowerCase()
172
172
  }
173
173
 
174
+ /**
175
+ * Returns the database-native type name used in SQL casts.
176
+ * @returns {string} - Database-native type name.
177
+ */
178
+ getDatabaseType() {
179
+ return this.getType()
180
+ }
181
+
174
182
  /**
175
183
  * Runs get type.
176
184
  * @abstract
@@ -20,9 +20,7 @@ export default class VelociousDatabaseConnectionDriversMssqlSqlUpdate extends Up
20
20
  for (let columnName in this.conditions) {
21
21
  if (count > 0) sql += " AND "
22
22
 
23
- sql += this.getOptions().quoteColumnName(columnName)
24
- sql += " = "
25
- sql += this.formatValue(this.conditions[columnName])
23
+ sql += this.formatCondition(columnName, this.conditions[columnName])
26
24
  count++
27
25
  }
28
26
 
@@ -22,9 +22,7 @@ export default class VelociousDatabaseConnectionDriversMysqlSqlUpdate extends Up
22
22
  for (let columnName in this.conditions) {
23
23
  if (count > 0) sql += " AND "
24
24
 
25
- sql += this.getOptions().quoteColumnName(columnName)
26
- sql += " = "
27
- sql += this.formatValue(this.conditions[columnName])
25
+ sql += this.formatCondition(columnName, this.conditions[columnName])
28
26
  count++
29
27
  }
30
28
 
@@ -33,6 +33,28 @@ export default class VelociousDatabaseDriversPgsqlColumn extends BaseColumn {
33
33
  return digg(this, "data", "column_default")
34
34
  }
35
35
 
36
+ /**
37
+ * Returns the concrete PostgreSQL type name used in SQL casts.
38
+ * @returns {string} - Schema-qualified domain or UDT name, or the ordinary data type.
39
+ */
40
+ getDatabaseType() {
41
+ const domainName = this.data.domain_name
42
+ const domainSchema = this.data.domain_schema
43
+ const dataType = this.data.data_type
44
+ const udtName = this.data.udt_name
45
+ const udtSchema = this.data.udt_schema
46
+
47
+ if (typeof domainName === "string" && typeof domainSchema === "string") {
48
+ return `${this.getDriver().quoteColumn(domainSchema)}.${this.getDriver().quoteColumn(domainName)}`
49
+ }
50
+
51
+ if ((dataType === "USER-DEFINED" || dataType === "ARRAY") && typeof udtName === "string" && typeof udtSchema === "string") {
52
+ return `${this.getDriver().quoteColumn(udtSchema)}.${this.getDriver().quoteColumn(udtName)}`
53
+ }
54
+
55
+ return this.getType()
56
+ }
57
+
36
58
  getMaxLength() {
37
59
  return digg(this, "data", "character_maximum_length")
38
60
  }
@@ -22,9 +22,7 @@ export default class VelociousDatabaseConnectionDriversPgsqlSqlUpdate extends Up
22
22
  for (let columnName in this.conditions) {
23
23
  if (count > 0) sql += " AND "
24
24
 
25
- sql += this.getOptions().quoteColumnName(columnName)
26
- sql += " = "
27
- sql += this.formatValue(this.conditions[columnName])
25
+ sql += this.formatCondition(columnName, this.conditions[columnName])
28
26
  count++
29
27
  }
30
28
 
@@ -110,7 +110,17 @@ export default class VelociousDatabaseConnectionDriversSqliteSqlAlterTable exten
110
110
  if (alterColumn.getDefault() === undefined) alterColumn.setDefault(currentColumn.getDefault())
111
111
  if (!alterColumn.getIndex()) alterColumn.setIndex(currentColumn.getIndex())
112
112
  if (!alterColumn.getForeignKey()) alterColumn.setForeignKey(currentColumn.getForeignKey())
113
- if (alterColumn.getMaxLength() === undefined) alterColumn.setMaxLength(currentColumn.getMaxLength())
113
+ const alterColumnType = alterColumn.getType()
114
+
115
+ if (
116
+ alterColumn.getMaxLength() === undefined
117
+ && (
118
+ alterColumnType === undefined
119
+ || (alterColumnType !== "text" && alterColumnType === currentColumn.getType())
120
+ )
121
+ ) {
122
+ alterColumn.setMaxLength(currentColumn.getMaxLength())
123
+ }
114
124
  alterColumn.setPrimaryKey(alterColumn.getPrimaryKey() || currentColumn.getPrimaryKey())
115
125
  if (!alterColumn.getType()) alterColumn.setType(currentColumn.getType())
116
126