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
@@ -4,9 +4,16 @@ import AuthorizationBaseResource from "../authorization/base-resource.js"
4
4
  import {frontendModelResourcesWithBuiltInsForBackendProject} from "./built-in-resources.js"
5
5
  import {frontendModelResourceDefinitionIsClass} from "./resource-definition.js"
6
6
  import {serializeFrontendModelTransportValue} from "./transport-serialization.js"
7
+ import {modelPrimaryKeyCacheKey, readModelPrimaryKeyValue} from "../utils/model-primary-key.js"
8
+
9
+ /** @typedef {{primaryKey: import("../utils/model-primary-key.js").ModelPrimaryKeyDefinition}} FrontendModelPublisherResource */
10
+ /** @typedef {Record<string, import("./query.js").FrontendModelTransportValue>} FrontendModelDestroyAuthorizationRecord */
11
+ /** @typedef {import("../database/record/index.js").default & {__frontendModelWebsocketAction?: "create" | "update", __frontendModelWebsocketDestroyAuthorizationRecord?: FrontendModelDestroyAuthorizationRecord, __frontendModelWebsocketPreviousIds?: Map<string, import("../utils/model-primary-key.js").ModelPrimaryKeyValue>}} FrontendModelWebsocketRecord */
7
12
 
8
13
  const modelClassesWithRegisteredHooks = new WeakSet()
9
14
  const channelClassRegisteredConfigurations = new WeakSet()
15
+ /** @type {WeakMap<import("../configuration.js").default, WeakMap<typeof import("../database/record/index.js").default, Map<string, FrontendModelPublisherResource>>>} */
16
+ const publisherResourcesByConfiguration = new WeakMap()
10
17
 
11
18
  /** Shared channel name for all frontend-model lifecycle subscriptions. */
12
19
  export const FRONTEND_MODELS_CHANNEL_NAME = "frontend-models"
@@ -60,7 +67,7 @@ function frontendModelResourcesFromAbilityResourcesList(abilityResources) {
60
67
  // throw `requires a static ModelClass` during ability-resource discovery.
61
68
  if (!resourceClass.ModelClass) continue
62
69
 
63
- const modelName = resourceClass.modelClass().getModelName()
70
+ const modelName = resourceClass.resourceConfig().modelName || resourceClass.modelClass().getModelName()
64
71
 
65
72
  resources[modelName] = resourceClass
66
73
  } else if (resourceClass.prototype instanceof AuthorizationBaseResource) {
@@ -113,14 +120,36 @@ export async function ensureFrontendModelWebsocketPublishersRegistered(configura
113
120
  configuration.registerWebsocketChannel(FRONTEND_MODELS_CHANNEL_NAME, FrontendModelWebsocketChannel)
114
121
  }
115
122
 
116
- for (const resourceClass of Object.values(allFrontendModels)) {
123
+ for (const [modelName, resourceClass] of Object.entries(allFrontendModels)) {
117
124
  // An abstract base resource (no static ModelClass — e.g. an app's shared
118
125
  // `BaseResource` that other resources extend) backs no model, so there is
119
126
  // nothing to publish realtime events for. Skip it instead of throwing.
120
127
  if (!resourceClass.ModelClass) continue
121
128
 
122
129
  const modelClass = resourceClass.modelClass()
123
- const modelName = modelClass.getModelName()
130
+ const resourceConfiguration = resourceClass.resourceConfig()
131
+ const configuredPrimaryKey = resourceConfiguration.primaryKey
132
+ const modelPrimaryKey = modelClass.primaryKey()
133
+ const primaryKey = configuredPrimaryKey || (Array.isArray(modelPrimaryKey)
134
+ ? modelPrimaryKey.map((columnName) => modelClass.resolveAttributeName(columnName) || columnName)
135
+ : modelClass.resolveAttributeName(modelPrimaryKey) || modelPrimaryKey)
136
+ let publisherResourcesByModelClass = publisherResourcesByConfiguration.get(configuration)
137
+
138
+ if (!publisherResourcesByModelClass) {
139
+ publisherResourcesByModelClass = new WeakMap()
140
+ publisherResourcesByConfiguration.set(configuration, publisherResourcesByModelClass)
141
+ }
142
+
143
+ let publisherResources = publisherResourcesByModelClass.get(modelClass)
144
+
145
+ if (!publisherResources) {
146
+ publisherResources = new Map()
147
+ publisherResourcesByModelClass.set(modelClass, publisherResources)
148
+ }
149
+
150
+ publisherResources.set(modelName, {
151
+ primaryKey
152
+ })
124
153
 
125
154
  // Register lifecycle hooks once per model class, not per configuration. A model class belongs to a
126
155
  // single backend project/config in production, so per-config registration only differs in tests where
@@ -132,36 +161,203 @@ export async function ensureFrontendModelWebsocketPublishersRegistered(configura
132
161
  modelClassesWithRegisteredHooks.add(modelClass)
133
162
 
134
163
  modelClass.beforeCreate((model) => {
135
- /** @type {import("../database/record/index.js").default & {__frontendModelWebsocketAction?: "create" | "update"}} */ (model).__frontendModelWebsocketAction = "create"
164
+ /** @type {FrontendModelWebsocketRecord} */ (model).__frontendModelWebsocketAction = "create"
136
165
  })
137
166
 
138
- modelClass.beforeUpdate((model) => {
139
- /** @type {import("../database/record/index.js").default & {__frontendModelWebsocketAction?: "create" | "update"}} */ (model).__frontendModelWebsocketAction = "update"
167
+ modelClass.beforeUpdate(async (model) => {
168
+ const websocketModel = /** @type {FrontendModelWebsocketRecord} */ (model)
169
+
170
+ websocketModel.__frontendModelWebsocketAction = "update"
171
+ websocketModel.__frontendModelWebsocketPreviousIds = await frontendModelPreviousResourceIdentities(model)
172
+ })
173
+
174
+ modelClass.beforeDestroy(async (model) => {
175
+ const websocketModel = /** @type {FrontendModelWebsocketRecord} */ (model)
176
+ const persistedModel = await model
177
+ .queryForModel(model.getModelClass())
178
+ .find(model._persistedPrimaryKeyValue())
179
+
180
+ if (!persistedModel) throw new Error(`Cannot capture websocket destroy authorization for missing ${model.getModelClass().name}`)
181
+
182
+ websocketModel.__frontendModelWebsocketPreviousIds = frontendModelResourceIdentities(persistedModel)
183
+ websocketModel.__frontendModelWebsocketDestroyAuthorizationRecord = frontendModelDestroyAuthorizationRecord(persistedModel)
140
184
  })
141
185
 
142
186
  modelClass.afterSave((model) => {
143
- const modelWithWebsocketAction = /** @type {import("../database/record/index.js").default & {__frontendModelWebsocketAction?: "create" | "update"}} */ (model)
187
+ const modelWithWebsocketAction = /** @type {FrontendModelWebsocketRecord} */ (model)
144
188
  const action = modelWithWebsocketAction.__frontendModelWebsocketAction
145
189
 
146
190
  if (action !== "create" && action !== "update") return
191
+ const previousIds = modelWithWebsocketAction.__frontendModelWebsocketPreviousIds
147
192
 
148
193
  void model.connection().afterCommit(async () => {
149
- broadcastFrontendModelEvent(model._getConfiguration(), modelName, {
150
- action,
151
- id: model.id(),
152
- record: model.attributes()
153
- })
194
+ broadcastFrontendModelEvents(model, action, previousIds)
154
195
  })
155
196
  delete modelWithWebsocketAction.__frontendModelWebsocketAction
197
+ delete modelWithWebsocketAction.__frontendModelWebsocketPreviousIds
156
198
  })
157
199
 
158
200
  modelClass.afterDestroy((model) => {
201
+ const websocketModel = /** @type {FrontendModelWebsocketRecord} */ (model)
202
+ const destroyAuthorizationRecord = websocketModel.__frontendModelWebsocketDestroyAuthorizationRecord
203
+ const previousIds = websocketModel.__frontendModelWebsocketPreviousIds
204
+
159
205
  void model.connection().afterCommit(async () => {
160
- broadcastFrontendModelEvent(model._getConfiguration(), modelName, {
161
- action: "destroy",
162
- id: model.id()
163
- })
206
+ broadcastFrontendModelEvents(model, "destroy", previousIds, destroyAuthorizationRecord)
164
207
  })
208
+ delete websocketModel.__frontendModelWebsocketDestroyAuthorizationRecord
209
+ delete websocketModel.__frontendModelWebsocketPreviousIds
210
+ })
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Returns every resource identity represented by the record before its pending changes or destruction.
216
+ * @param {import("../database/record/index.js").default} model - Backing model before update or destroy.
217
+ * @returns {Promise<Map<string, import("../utils/model-primary-key.js").ModelPrimaryKeyValue>>} - Previous identities by resource name.
218
+ */
219
+ async function frontendModelPreviousResourceIdentities(model) {
220
+ const publisherResources = publisherResourcesByConfiguration.get(model._getConfiguration())?.get(model.getModelClass())
221
+ /** @type {Map<string, import("../utils/model-primary-key.js").ModelPrimaryKeyValue>} */
222
+ const previousIds = new Map()
223
+
224
+ if (!publisherResources) return previousIds
225
+
226
+ for (const [modelName, {primaryKey}] of publisherResources) {
227
+ const previousId = frontendModelResourceIdentity({model, previous: true, primaryKey})
228
+
229
+ if (previousId !== null) previousIds.set(modelName, previousId)
230
+ }
231
+
232
+ if (previousIds.size === publisherResources.size) return previousIds
233
+
234
+ const persistedModel = await model
235
+ .queryForModel(model.getModelClass())
236
+ .find(model._persistedPrimaryKeyValue())
237
+
238
+ for (const [modelName, {primaryKey}] of publisherResources) {
239
+ if (previousIds.has(modelName)) continue
240
+
241
+ const persistedId = frontendModelResourceIdentity({model: persistedModel, primaryKey})
242
+
243
+ if (persistedId !== null) previousIds.set(modelName, persistedId)
244
+ }
245
+
246
+ return previousIds
247
+ }
248
+
249
+ /**
250
+ * Returns every configured resource identity represented by a persisted backing record.
251
+ * @param {import("../database/record/index.js").default} model - Fully loaded persisted backing record.
252
+ * @returns {Map<string, import("../utils/model-primary-key.js").ModelPrimaryKeyValue>} - Identities by resource name.
253
+ */
254
+ function frontendModelResourceIdentities(model) {
255
+ const publisherResources = publisherResourcesByConfiguration.get(model._getConfiguration())?.get(model.getModelClass())
256
+ /** @type {Map<string, import("../utils/model-primary-key.js").ModelPrimaryKeyValue>} */
257
+ const identities = new Map()
258
+
259
+ if (!publisherResources) return identities
260
+
261
+ for (const [modelName, {primaryKey}] of publisherResources) {
262
+ const id = frontendModelResourceIdentity({model, primaryKey})
263
+
264
+ if (id !== null) identities.set(modelName, id)
265
+ }
266
+
267
+ return identities
268
+ }
269
+
270
+ /**
271
+ * Serializes the persisted record for server-side destroy authorization. Binary values
272
+ * use a dedicated byte-array marker because the shared transport serializer otherwise
273
+ * leaves Buffers to the JSON implementation used by the worker or Beacon transport.
274
+ * @param {import("../database/record/index.js").default} model - Fully loaded persisted backing record.
275
+ * @returns {FrontendModelDestroyAuthorizationRecord} - Column-keyed transport values.
276
+ */
277
+ function frontendModelDestroyAuthorizationRecord(model) {
278
+ const serializationOptions = transportSerializationOptionsForConfiguration(model._getConfiguration())
279
+ /** @type {FrontendModelDestroyAuthorizationRecord} */
280
+ const authorizationRecord = {}
281
+
282
+ for (const [columnName, value] of Object.entries(model.rawAttributes())) {
283
+ authorizationRecord[columnName] = value instanceof Uint8Array
284
+ ? {__velociousDestroyAuthorizationType: "binary", value: Array.from(value)}
285
+ : serializeFrontendModelTransportValue(value, serializationOptions)
286
+ }
287
+
288
+ if (Object.keys(authorizationRecord).length === 0) {
289
+ throw new Error(`Cannot capture websocket destroy authorization without attributes for ${model.getModelClass().name}`)
290
+ }
291
+
292
+ return authorizationRecord
293
+ }
294
+
295
+ /**
296
+ * Reads a resource identity only when every identity attribute was loaded on the backing record.
297
+ * @param {object} args - Identity arguments.
298
+ * @param {import("../database/record/index.js").default} args.model - Backing model.
299
+ * @param {boolean} [args.previous] - Read values from before pending changes.
300
+ * @param {import("../utils/model-primary-key.js").ModelPrimaryKeyDefinition} args.primaryKey - Resource identity definition.
301
+ * @returns {import("../utils/model-primary-key.js").ModelPrimaryKeyValue | null} - Complete identity or null when unavailable.
302
+ */
303
+ function frontendModelResourceIdentity({model, previous = false, primaryKey}) {
304
+ const attributes = model.attributes()
305
+ const changes = model.changes()
306
+ /** @type {Record<string, import("../utils/model-primary-key.js").ModelPrimaryKeyScalar>} */
307
+ const identityAttributes = {}
308
+ const primaryKeyAttributes = Array.isArray(primaryKey) ? primaryKey : [primaryKey]
309
+
310
+ for (const attributeName of primaryKeyAttributes) {
311
+ const columnName = model.getModelClass().getColumnNameForAttributeName(attributeName)
312
+ let value
313
+
314
+ if (previous && Object.hasOwn(changes, columnName)) {
315
+ value = changes[columnName][0]
316
+ } else {
317
+ if (!Object.hasOwn(attributes, attributeName)) return null
318
+
319
+ value = attributes[attributeName]
320
+ }
321
+
322
+ if (typeof value !== "string" && typeof value !== "number") return null
323
+
324
+ identityAttributes[attributeName] = value
325
+ }
326
+
327
+ return readModelPrimaryKeyValue(primaryKey, (attributeName) => identityAttributes[attributeName])
328
+ }
329
+
330
+ /**
331
+ * Fans one backing-record lifecycle event out through every configured frontend-resource identity.
332
+ * @param {import("../database/record/index.js").default} model - Backing model instance.
333
+ * @param {"create" | "update" | "destroy"} action - Lifecycle action.
334
+ * @param {Map<string, import("../utils/model-primary-key.js").ModelPrimaryKeyValue>} [previousIds] - Persisted identities captured before update or destroy.
335
+ * @param {FrontendModelDestroyAuthorizationRecord} [destroyAuthorizationRecord] - Server-only pre-delete row used to authorize a destroyed record.
336
+ * @returns {void}
337
+ */
338
+ function broadcastFrontendModelEvents(model, action, previousIds, destroyAuthorizationRecord) {
339
+ const configuration = model._getConfiguration()
340
+ const publisherResources = publisherResourcesByConfiguration.get(configuration)?.get(model.getModelClass())
341
+
342
+ if (!publisherResources) return
343
+
344
+ for (const [modelName, {primaryKey}] of publisherResources) {
345
+ const previousId = previousIds?.get(modelName)
346
+ const currentId = frontendModelResourceIdentity({model, primaryKey})
347
+ const id = action === "destroy" ? previousId : currentId ?? previousId
348
+
349
+ if (id === null || id === undefined) continue
350
+
351
+ const identityChanged = action === "update"
352
+ && currentId !== null
353
+ && previousId !== undefined
354
+ && modelPrimaryKeyCacheKey(primaryKey, previousId) !== modelPrimaryKeyCacheKey(primaryKey, id)
355
+
356
+ broadcastFrontendModelEvent(configuration, modelName, {
357
+ action,
358
+ id,
359
+ ...(destroyAuthorizationRecord !== undefined ? {destroyAuthorizationRecord} : {}),
360
+ ...(identityChanged ? {previousId} : {})
165
361
  })
166
362
  }
167
363
  }
@@ -172,7 +368,7 @@ export async function ensureFrontendModelWebsocketPublishersRegistered(configura
172
368
  * transport serializer so Date/undefined/etc. survive the JSON hop.
173
369
  * @param {import("../configuration.js").default} configuration - Configuration instance.
174
370
  * @param {string} modelName - Model class name.
175
- * @param {{action: "create" | "update" | "destroy", id: ReturnType<typeof JSON.parse>, record?: Record<string, ReturnType<typeof JSON.parse>>}} event - Lifecycle event.
371
+ * @param {{action: "create" | "update" | "destroy", destroyAuthorizationRecord?: FrontendModelDestroyAuthorizationRecord, id: import("../utils/model-primary-key.js").ModelPrimaryKeyValue, previousId?: import("../utils/model-primary-key.js").ModelPrimaryKeyValue, record?: Record<string, import("./query.js").FrontendModelTransportValue>}} event - Lifecycle event.
176
372
  * @returns {void}
177
373
  */
178
374
  function broadcastFrontendModelEvent(configuration, modelName, event) {
@@ -180,8 +376,12 @@ function broadcastFrontendModelEvent(configuration, modelName, event) {
180
376
  action: event.action,
181
377
  id: event.id,
182
378
  model: modelName,
379
+ ...(event.previousId !== undefined ? {previousId: event.previousId} : {}),
183
380
  ...(event.record ? {record: serializeFrontendModelTransportValue(event.record, transportSerializationOptionsForConfiguration(configuration))} : {})
184
381
  }
185
382
 
186
- configuration.broadcastToChannel(FRONTEND_MODELS_CHANNEL_NAME, {model: modelName}, body)
383
+ configuration.broadcastToChannel(FRONTEND_MODELS_CHANNEL_NAME, {
384
+ ...(event.destroyAuthorizationRecord !== undefined ? {destroyAuthorizationRecord: event.destroyAuthorizationRecord} : {}),
385
+ model: modelName
386
+ }, body)
187
387
  }
@@ -1880,7 +1880,19 @@ export default class VelociousHttpServerClientWebsocketSession {
1880
1880
  for (const event of events) {
1881
1881
  if (subscription.isClosed()) break
1882
1882
 
1883
- subscription.sendMessage(/** @type {import("../websocket-channel.js").WebsocketJsonValue} */ (event.payload))
1883
+ if (await subscription._requiresReplayGap(event.payload)) {
1884
+ this.sendJson({
1885
+ type: "channel-replay-gap",
1886
+ subscriptionId: subscription.subscriptionId,
1887
+ lastEventId
1888
+ })
1889
+ return
1890
+ }
1891
+
1892
+ await subscription.deliverBroadcast(
1893
+ /** @type {import("../websocket-channel.js").WebsocketJsonValue} */ (event.payload),
1894
+ {eventId: event.id}
1895
+ )
1884
1896
  }
1885
1897
  }
1886
1898
 
@@ -8,6 +8,10 @@
8
8
  * WebsocketParams type.
9
9
  * @typedef {Record<string, WebsocketJsonValue>} WebsocketParams
10
10
  */
11
+ /**
12
+ * Server-side metadata accompanying a matched broadcast.
13
+ * @typedef {{broadcastParams?: WebsocketParams, eventId?: string}} WebsocketBroadcastMetadata
14
+ */
11
15
 
12
16
  /**
13
17
  * Base class for app-defined 1:N pub/sub channels.
@@ -90,6 +94,15 @@ export default class VelociousWebsocketChannel {
90
94
  */
91
95
  matches(..._broadcastArgs) { return true }
92
96
 
97
+ /**
98
+ * Whether replaying a persisted broadcast would require a client resync.
99
+ * Subclasses override this when replay storage deliberately omits metadata
100
+ * required to deliver an event safely.
101
+ * @param {WebsocketJsonValue} _body - Persisted broadcast payload.
102
+ * @returns {boolean | Promise<boolean>} - Whether the session must report a replay gap.
103
+ */
104
+ _requiresReplayGap(_body) { return false }
105
+
93
106
  /**
94
107
  * Returns sanitized diagnostics for debug snapshots.
95
108
  * Subclasses can override to expose non-sensitive routing details.
@@ -102,7 +115,7 @@ export default class VelociousWebsocketChannel {
102
115
  * override when the outbound body must be tailored to subscription
103
116
  * params before sending.
104
117
  * @param {WebsocketJsonValue} body - Broadcast payload offered to this subscription.
105
- * @param {{eventId?: string}} [meta] - Optional event metadata.
118
+ * @param {WebsocketBroadcastMetadata} [meta] - Optional server-side broadcast metadata.
106
119
  * @returns {void | Promise<void>} - Completes after broadcast delivery.
107
120
  */
108
121
  deliverBroadcast(body, meta) {
@@ -114,7 +127,7 @@ export default class VelociousWebsocketChannel {
114
127
  * When `meta.eventId` is provided, the client receives it so it
115
128
  * can track its checkpoint for `lastEventId` replay on reconnect.
116
129
  * @param {WebsocketJsonValue} body - Channel payload to send to the subscribed client.
117
- * @param {{eventId?: string}} [meta] - Optional event metadata.
130
+ * @param {WebsocketBroadcastMetadata} [meta] - Optional server-side broadcast metadata.
118
131
  * @returns {void}
119
132
  */
120
133
  sendMessage(body, meta) {