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
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  * Concurrent multi threadded web server
4
4
  * Database framework with familiar MVC concepts
5
5
  * Database models with migrations and validations
6
- * Database models that work almost the same in frontend and backend
6
+ * Database models that work almost the same in frontend and backend, including online CRUD for [composite primary keys](docs/composite-primary-keys.md)
7
7
  * Connection-scoped advisory locks with automatic cleanup before pooled connections are reused or closed (see [docs/advisory-locks.md](docs/advisory-locks.md))
8
8
  * Built-in record auditing for model lifecycle changes (see [docs/auditing.md](docs/auditing.md))
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))
@@ -178,10 +178,10 @@ Baselines are generated against a fresh checkout (no generated dummy `configurat
178
178
  # Testing
179
179
 
180
180
  Application tests may import the testing DSL from the independent public package.
181
- `@velocious/testing` `0.0.1` uses one compatible default registry across installed copies,
182
- and the framework runner discovers these declarations while retaining its database,
183
- request, retry, profiling, and cleanup behavior. The existing Velocious facade remains
184
- supported.
181
+ `@velocious/testing` `0.0.9` is the declaration and execution engine. Compatible
182
+ installed copies share one protocol-1/schema-3 default registry. Velocious adapts each
183
+ package-owned attempt with its database, request, profiling, and cleanup behavior; the
184
+ existing Velocious facade exports the same declaration DSL and remains supported.
185
185
 
186
186
  ```js
187
187
  import {describe, expect, it} from "@velocious/testing"
@@ -713,6 +713,9 @@ User.hasOneAttachment("profilePicture", {
713
713
  `db:migrate` provisions the framework-owned attachment table before runtime
714
714
  attachment work begins. See [Backend record attachments](docs/attachments.md)
715
715
  for the complete input, storage-driver, lifecycle, and path-security contracts.
716
+ Offline-capable clients can apply synchronized attachment descriptors through
717
+ the platform-neutral [Synchronized asset cache](docs/synchronized-assets.md),
718
+ while Expo and web packages own their respective byte-storage adapters.
716
719
 
717
720
  You can also pass a driver class or instance directly on the attachment:
718
721
 
@@ -841,8 +844,14 @@ The optional model-level `sync` block is client-safe policy metadata for asset
841
844
  cache adapters. It distinguishes eager/on-demand fetching,
842
845
  durable/evictable retention, and optional/required offline availability.
843
846
  Required offline assets must be durable. Backend driver configuration never
844
- appears in generated frontend models or API manifests.
845
- Attachment metadata is exposed through the built-in `VelociousAttachment` frontend model with safe fields only: `id`, `recordType`, `recordId`, `name`, `position`, `filename`, `contentType`, `byteSize`, `createdAt`, and `updatedAt`. Storage internals such as `driver`, `storageKey`, and `contentBase64` remain hidden and non-queryable. Direct metadata queries require owner filters: `recordType`, `recordId`, and `name`.
847
+ appears in generated frontend models or API manifests. A descriptor ID keeps
848
+ its digest, byte size, and content type immutably. Cache descriptors that share
849
+ a digest must agree on byte size and content type, and eager synchronization
850
+ attempts each shared digest only once per reconciliation. On-demand resolution
851
+ rechecks the backing blob after cleanup and returns `null` instead of a stale
852
+ local URI when concurrent eviction removed it. Cleanup deferred by an active
853
+ cached resolution runs again after that digest's final guard releases.
854
+ Attachment metadata is exposed through the built-in `VelociousAttachment` frontend model with safe fields only: `id`, `recordType`, `recordId`, `name`, `position`, `filename`, `contentType`, `byteSize`, `createdAt`, and `updatedAt`. Storage internals such as `driver`, `storageKey`, and `contentBase64` remain hidden and non-queryable. Metadata collection queries require owner filters: `resourceName`, `recordType`, `recordId`, and `name`. Composite `recordId` values retain the complete canonical tuple without a 255-character limit, and key-changing saves rekey attachment ownership in the record transaction. `VelociousAttachment.find(id)` uses the member endpoint and authorizes against configured resource aliases backed by the attachment owner type.
846
855
 
847
856
  When your frontend app calls a backend on another host/port (or under a path prefix), configure transport once:
848
857
 
@@ -2237,8 +2246,10 @@ If you are using Velocious for an app, Velocious has a built-in testing framewor
2237
2246
  npx velocious test
2238
2247
  ```
2239
2248
 
2240
- Test declarations can be imported from `@velocious/testing`; the legacy
2241
- `velocious/build/src/testing/test.js` facade remains supported.
2249
+ Test declarations can be imported from `@velocious/testing`; the compatibility
2250
+ `velocious/build/src/testing/test.js` facade uses the same package registry and remains
2251
+ supported. Under Velocious, ordinary callbacks receive `testArgs`, while `it.each`
2252
+ callbacks receive their row arguments followed by `testArgs`.
2242
2253
 
2243
2254
  If you are developing on Velocious, you can run the tests with:
2244
2255
 
@@ -2790,7 +2801,7 @@ backgroundJobs: {
2790
2801
  }
2791
2802
  ```
2792
2803
 
2793
- A job with no queue runs on `"default"`; a queue with no cap is unlimited. Caps are enforced through the durable per-key concurrency mechanism (the reserved `queue:<name>` key) and hold regardless of how many worker processes run. Changing a cap and rebuilding durable active counts happen only when `background-jobs-main` starts (serialized across processes with a database advisory lock and logged with database identifier/duration); `db:migrate`, `db:tenants:*`, and routine store/application initialization with an intact jobs table never adopt queued jobs or rebuild global concurrency counts. If schema repair must recreate a missing `background_jobs` table while the migration marker and concurrency table survive, it resets the now-orphaned active counts against that newly empty table. Scheduled jobs honor a job's `static queue` too.
2804
+ A job with no queue runs on `"default"`; a queue with no cap is unlimited. Caps are enforced through the durable per-key concurrency mechanism (the reserved `queue:<name>` key) and hold regardless of how many worker processes run. Queue-policy changes are adopted by queued backlog rows when `background-jobs-main` starts; already handed-off jobs drain under their original policy. If they return, reschedule, or retry, the reporting generation releases the original reservation without changing shared policy, and the active generation applies its current queue policy immediately before the next handoff and sends that committed policy to the worker; explicit concurrency remains unchanged. Handoff persistence is fenced against concurrent policy changes. Startup also rebuilds durable active counts under a database advisory lock. The active main then checks those counts every minute, performs no counter writes while they are exact, repairs only locked mismatches, logs a bounded structured repair summary, and immediately retries dispatch so stale capacity cannot require a restart. `db:migrate`, `db:tenants:*`, and routine store/application initialization with an intact jobs table never adopt queued jobs or rebuild global concurrency counts. If schema repair must recreate a missing `background_jobs` table while the migration marker and concurrency table survive, it resets the now-orphaned active counts against that newly empty table. Scheduled jobs honor a job's `static queue` too.
2794
2805
 
2795
2806
  Set `priority` (default `0`) to dispatch a queue ahead of lower-priority ones regardless of enqueue order, so a small time-critical queue is never starved by a flood of low-priority work sharing a worker pool. Unlike Sidekiq's strict queue ordering, priority composes with the caps: a higher-priority queue already at its `maxConcurrent` is skipped and dispatch falls through to the next eligible job. See [docs/background-jobs.md](docs/background-jobs.md#queues-per-queue-concurrency-caps).
2796
2807
 
@@ -189,9 +189,10 @@ export default class VelociousAuthorizationAbility {
189
189
  * @param {string} args.action - Requested action.
190
190
  * @param {typeof import("../database/record/index.js").default} args.modelClass - Model class.
191
191
  * @param {import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>} args.query - Query.
192
+ * @param {() => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>} [args.ruleQueryFactory] - Optional factory for the queries that evaluate individual conditional rules.
192
193
  * @returns {import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>} - Authorized query.
193
194
  */
194
- applyToQuery({action, modelClass, query}) {
195
+ applyToQuery({action, modelClass, query, ruleQueryFactory}) {
195
196
  this.loadAbilitiesForModelClass(modelClass)
196
197
 
197
198
  const applicableRules = this.rulesFor({action, modelClass})
@@ -203,18 +204,18 @@ export default class VelociousAuthorizationAbility {
203
204
  }
204
205
 
205
206
  if (allowRules.some((rule) => !rule.conditions)) {
206
- this.applyDenyRules({action, denyRules, modelClass, query})
207
+ this.applyDenyRules({action, denyRules, modelClass, query, ruleQueryFactory})
207
208
  return query
208
209
  }
209
210
 
210
- const allowSqlParts = this.conditionSqlParts({action, modelClass, query, rules: allowRules})
211
+ const allowSqlParts = this.conditionSqlParts({action, modelClass, query, ruleQueryFactory, rules: allowRules})
211
212
 
212
213
  if (allowSqlParts.length === 0) {
213
214
  return query.where("1=0")
214
215
  }
215
216
 
216
217
  query.where(`(${allowSqlParts.join(" OR ")})`)
217
- this.applyDenyRules({action, denyRules, modelClass, query})
218
+ this.applyDenyRules({action, denyRules, modelClass, query, ruleQueryFactory})
218
219
 
219
220
  return query
220
221
  }
@@ -240,19 +241,20 @@ export default class VelociousAuthorizationAbility {
240
241
  * @param {string} args.action - Action.
241
242
  * @param {typeof import("../database/record/index.js").default} args.modelClass - Model class.
242
243
  * @param {import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>} args.query - Base query.
244
+ * @param {() => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>} [args.ruleQueryFactory] - Optional conditional-rule query factory.
243
245
  * @param {AbilityRuleType[]} args.rules - Rules.
244
246
  * @returns {string[]} - SQL condition parts.
245
247
  */
246
- conditionSqlParts({action, modelClass, query, rules}) {
247
- const pk = modelClass.primaryKey()
248
- const quotedBaseTable = query.driver.quoteTable(modelClass.tableName())
249
- const quotedPk = query.driver.quoteColumn(pk)
248
+ conditionSqlParts({action, modelClass, query, ruleQueryFactory, rules}) {
249
+ const primaryKey = modelClass.primaryKey()
250
+ const primaryKeyAttributes = Array.isArray(primaryKey) ? primaryKey : [primaryKey]
251
+ const quotedBaseTable = query.driver.quoteTable(query.getTableReferenceForJoin())
250
252
  const sqlParts = []
251
253
 
252
254
  for (const rule of rules) {
253
255
  if (!rule.conditions) continue
254
256
 
255
- const scopedQuery = modelClass._newQuery()
257
+ const scopedQuery = ruleQueryFactory ? ruleQueryFactory() : modelClass._newQuery()
256
258
  const resultQuery = this.applyRuleCondition({
257
259
  action,
258
260
  conditions: rule.conditions,
@@ -260,7 +262,16 @@ export default class VelociousAuthorizationAbility {
260
262
  query: scopedQuery
261
263
  })
262
264
  const finalQuery = resultQuery || scopedQuery
263
- const selectedPkSql = `${quotedBaseTable}.${quotedPk}`
265
+
266
+ if (ruleQueryFactory && finalQuery !== scopedQuery) {
267
+ const finalFroms = finalQuery.getFroms()
268
+
269
+ finalFroms.splice(0, finalFroms.length, ...scopedQuery.getFroms())
270
+ }
271
+
272
+ const quotedScopedTable = query.driver.quoteTable(finalQuery.getTableReferenceForJoin())
273
+ const primaryKeyColumns = primaryKeyAttributes.map((attributeName) => modelClass.getColumnNameForAttributeName(attributeName))
274
+ const selectedPkSql = primaryKeyColumns.map((columnName) => `${quotedScopedTable}.${query.driver.quoteColumn(columnName)}`)
264
275
 
265
276
  if (finalQuery._distinct) {
266
277
  query.distinct(true)
@@ -268,7 +279,18 @@ export default class VelociousAuthorizationAbility {
268
279
 
269
280
  finalQuery.select(selectedPkSql)
270
281
 
271
- sqlParts.push(`${quotedBaseTable}.${quotedPk} IN (${finalQuery.toSql()})`)
282
+ if (Array.isArray(primaryKey)) {
283
+ const authorizedRowsAlias = query.driver.quoteTable("velocious_authorized_rows")
284
+ const identitySql = primaryKeyColumns.map((columnName) => {
285
+ const quotedColumn = query.driver.quoteColumn(columnName)
286
+
287
+ return `${authorizedRowsAlias}.${quotedColumn} = ${quotedBaseTable}.${quotedColumn}`
288
+ }).join(" AND ")
289
+
290
+ sqlParts.push(`EXISTS (SELECT 1 FROM (${finalQuery.toSql()}) AS ${authorizedRowsAlias} WHERE ${identitySql})`)
291
+ } else {
292
+ sqlParts.push(`${quotedBaseTable}.${query.driver.quoteColumn(primaryKeyColumns[0])} IN (${finalQuery.toSql()})`)
293
+ }
272
294
  }
273
295
 
274
296
  return sqlParts
@@ -281,9 +303,10 @@ export default class VelociousAuthorizationAbility {
281
303
  * @param {AbilityRuleType[]} args.denyRules - Deny rules.
282
304
  * @param {typeof import("../database/record/index.js").default} args.modelClass - Model class.
283
305
  * @param {import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>} args.query - Query.
306
+ * @param {() => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>} [args.ruleQueryFactory] - Optional conditional-rule query factory.
284
307
  * @returns {void} - No return value.
285
308
  */
286
- applyDenyRules({action, denyRules, modelClass, query}) {
309
+ applyDenyRules({action, denyRules, modelClass, query, ruleQueryFactory}) {
287
310
  if (denyRules.length === 0) return
288
311
 
289
312
  if (denyRules.some((rule) => !rule.conditions)) {
@@ -291,7 +314,7 @@ export default class VelociousAuthorizationAbility {
291
314
  return
292
315
  }
293
316
 
294
- const denySqlParts = this.conditionSqlParts({action, modelClass, query, rules: denyRules})
317
+ const denySqlParts = this.conditionSqlParts({action, modelClass, query, ruleQueryFactory, rules: denyRules})
295
318
 
296
319
  if (denySqlParts.length > 0) {
297
320
  query.where(`NOT (${denySqlParts.join(" OR ")})`)
@@ -48,6 +48,15 @@ export default class BackgroundJobsAdapter {
48
48
  */
49
49
  async reconcileQueueConcurrency() { throw new Error("BackgroundJobsAdapter#reconcileQueueConcurrency is not implemented") }
50
50
 
51
+ /**
52
+ * Repairs drift in adapter-owned durable active concurrency counts. Adapters
53
+ * without duplicate active-count persistence can keep this no-op result.
54
+ * @returns {Promise<import("./types.js").BackgroundJobConcurrencyReconciliation>} - Repair summary.
55
+ */
56
+ async reconcileActiveConcurrency() {
57
+ return {candidateCount: 0, checkedCount: 0, repairedCount: 0, repairs: [], repairsTruncatedCount: 0}
58
+ }
59
+
51
60
  /**
52
61
  * Enqueues a job.
53
62
  * @param {{jobName: string, args: Array<ReturnType<typeof JSON.parse>>, options?: import("./types.js").BackgroundJobOptions}} _args - Job request.
@@ -2063,23 +2063,25 @@ export default class BackgroundJobsMain {
2063
2063
  handoffs.set(job.id, handoff.handoffId)
2064
2064
 
2065
2065
  try {
2066
+ const dispatchedJob = handoff.job || job
2067
+
2066
2068
  worker.send({
2067
2069
  type: "job",
2068
2070
  payload: {
2069
- id: job.id,
2070
- jobName: job.jobName,
2071
- args: job.args,
2071
+ id: dispatchedJob.id,
2072
+ jobName: dispatchedJob.jobName,
2073
+ args: dispatchedJob.args,
2072
2074
  handoffId: handoff.handoffId,
2073
2075
  workerId: worker.workerId,
2074
2076
  handedOffAtMs: handoff.handedOffAtMs,
2075
2077
  options: {
2076
- concurrencyKey: job.concurrencyKey || undefined,
2077
- executionMode: job.executionMode,
2078
- maxConcurrency: job.maxConcurrency ?? undefined,
2079
- maxRetries: job.maxRetries ?? undefined,
2080
- queue: job.queue,
2081
- scheduledAtMs: job.scheduledAtMs ?? undefined,
2082
- ...(job.timeoutMs === null ? {} : {timeoutMs: job.timeoutMs})
2078
+ concurrencyKey: dispatchedJob.concurrencyKey || undefined,
2079
+ executionMode: dispatchedJob.executionMode,
2080
+ maxConcurrency: dispatchedJob.maxConcurrency ?? undefined,
2081
+ maxRetries: dispatchedJob.maxRetries ?? undefined,
2082
+ queue: dispatchedJob.queue,
2083
+ scheduledAtMs: dispatchedJob.scheduledAtMs ?? undefined,
2084
+ ...(dispatchedJob.timeoutMs === null ? {} : {timeoutMs: dispatchedJob.timeoutMs})
2083
2085
  }
2084
2086
  }
2085
2087
  })
@@ -2354,6 +2356,29 @@ export default class BackgroundJobsMain {
2354
2356
  errorEvents.emit("framework-error", payload)
2355
2357
  errorEvents.emit("all-error", {...payload, errorType: "framework-error"})
2356
2358
  }
2359
+
2360
+ if (this.lifecycleState === "active") await this._reconcileActiveConcurrency()
2361
+ }
2362
+
2363
+ /**
2364
+ * Repairs durable admission counters on the active main's maintenance cadence
2365
+ * and immediately retries dispatch when capacity was recovered.
2366
+ * @returns {Promise<void>} - Resolves after repair and any resulting drain.
2367
+ */
2368
+ async _reconcileActiveConcurrency() {
2369
+ try {
2370
+ const result = await this.store.reconcileActiveConcurrency()
2371
+
2372
+ if (result.repairedCount > 0) await this._drain()
2373
+ } catch (error) {
2374
+ const normalizedError = error instanceof Error ? error : new Error(String(error))
2375
+ const payload = {context: {generationId: this.generationId, stage: "background-job-concurrency-reconciliation"}, error: normalizedError}
2376
+ const errorEvents = this.configuration.getErrorEvents()
2377
+
2378
+ this.logger.error(() => ["Failed to reconcile background job active-concurrency counts:", normalizedError])
2379
+ errorEvents.emit("framework-error", payload)
2380
+ errorEvents.emit("all-error", {...payload, errorType: "framework-error"})
2381
+ }
2357
2382
  }
2358
2383
 
2359
2384
  /**