velocious 1.0.514 → 1.0.516

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 (255) hide show
  1. package/README.md +4 -3
  2. package/build/background-jobs/cron-expression.js +9 -9
  3. package/build/background-jobs/forked-runner-child.js +1 -1
  4. package/build/background-jobs/job-runner.js +21 -15
  5. package/build/background-jobs/json-socket.js +4 -0
  6. package/build/background-jobs/main.js +180 -18
  7. package/build/background-jobs/status-reporter.js +12 -5
  8. package/build/background-jobs/store.js +132 -50
  9. package/build/background-jobs/types.js +11 -3
  10. package/build/background-jobs/worker.js +9 -5
  11. package/build/beacon/client.js +13 -6
  12. package/build/configuration.js +54 -29
  13. package/build/controller.js +9 -3
  14. package/build/database/drivers/sqlite/base.js +1 -1
  15. package/build/database/drivers/sqlite/index.js +15 -14
  16. package/build/database/migrations-ledger.js +16 -16
  17. package/build/database/pool/async-tracked-multi-connection.js +16 -0
  18. package/build/database/pool/base.js +26 -0
  19. package/build/database/query/where-model-class-hash.js +2 -0
  20. package/build/database/query/with-count.js +1 -1
  21. package/build/database/record/acts-as-list.js +2 -2
  22. package/build/database/record/auditing.js +23 -6
  23. package/build/database/record/index.js +7 -7
  24. package/build/database/record/validators/uniqueness.js +3 -3
  25. package/build/database/tenants/data-copier.js +23 -23
  26. package/build/database/tenants/schema-cloner.js +27 -27
  27. package/build/environment-handlers/browser.js +0 -1
  28. package/build/environment-handlers/node/cli/commands/background-jobs-runner.js +2 -1
  29. package/build/environment-handlers/node/cli/commands/lint/relationships.js +2 -2
  30. package/build/environment-handlers/node.js +1 -1
  31. package/build/frontend-model-controller.js +26 -18
  32. package/build/frontend-model-resource/base-resource.js +4 -4
  33. package/build/frontend-models/base.js +5 -5
  34. package/build/frontend-models/query.js +12 -12
  35. package/build/http-server/client/index.js +61 -15
  36. package/build/http-server/client/request-runner.js +7 -1
  37. package/build/http-server/client/response.js +17 -1
  38. package/build/http-server/client/websocket-session.js +13 -13
  39. package/build/http-server/server-client.js +96 -0
  40. package/build/http-server/websocket-channel.js +12 -11
  41. package/build/http-server/websocket-connection.js +12 -11
  42. package/build/http-server/websocket-events-host.js +1 -1
  43. package/build/http-server/worker-handler/in-process.js +48 -9
  44. package/build/http-server/worker-handler/index.js +67 -7
  45. package/build/http-server/worker-handler/worker-thread.js +68 -0
  46. package/build/src/background-jobs/cron-expression.d.ts +2 -2
  47. package/build/src/background-jobs/cron-expression.js +10 -10
  48. package/build/src/background-jobs/forked-runner-child.js +2 -2
  49. package/build/src/background-jobs/job-runner.d.ts +5 -1
  50. package/build/src/background-jobs/job-runner.d.ts.map +1 -1
  51. package/build/src/background-jobs/job-runner.js +22 -16
  52. package/build/src/background-jobs/json-socket.d.ts +4 -0
  53. package/build/src/background-jobs/json-socket.d.ts.map +1 -1
  54. package/build/src/background-jobs/json-socket.js +5 -1
  55. package/build/src/background-jobs/main.d.ts +62 -10
  56. package/build/src/background-jobs/main.d.ts.map +1 -1
  57. package/build/src/background-jobs/main.js +171 -19
  58. package/build/src/background-jobs/status-reporter.d.ts +6 -2
  59. package/build/src/background-jobs/status-reporter.d.ts.map +1 -1
  60. package/build/src/background-jobs/status-reporter.js +13 -6
  61. package/build/src/background-jobs/store.d.ts +48 -25
  62. package/build/src/background-jobs/store.d.ts.map +1 -1
  63. package/build/src/background-jobs/store.js +123 -50
  64. package/build/src/background-jobs/types.d.ts +36 -3
  65. package/build/src/background-jobs/types.d.ts.map +1 -1
  66. package/build/src/background-jobs/types.js +12 -4
  67. package/build/src/background-jobs/worker.d.ts +7 -5
  68. package/build/src/background-jobs/worker.d.ts.map +1 -1
  69. package/build/src/background-jobs/worker.js +10 -6
  70. package/build/src/beacon/client.d.ts +6 -3
  71. package/build/src/beacon/client.d.ts.map +1 -1
  72. package/build/src/beacon/client.js +14 -7
  73. package/build/src/configuration-types.d.ts +1 -1
  74. package/build/src/configuration.d.ts +42 -29
  75. package/build/src/configuration.d.ts.map +1 -1
  76. package/build/src/configuration.js +52 -30
  77. package/build/src/controller.d.ts +3 -0
  78. package/build/src/controller.d.ts.map +1 -1
  79. package/build/src/controller.js +9 -4
  80. package/build/src/database/drivers/sqlite/base.js +2 -2
  81. package/build/src/database/drivers/sqlite/index.d.ts +7 -6
  82. package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
  83. package/build/src/database/drivers/sqlite/index.js +16 -15
  84. package/build/src/database/migrations-ledger.d.ts +16 -16
  85. package/build/src/database/migrations-ledger.js +17 -17
  86. package/build/src/database/pool/async-tracked-multi-connection.d.ts +0 -11
  87. package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
  88. package/build/src/database/pool/async-tracked-multi-connection.js +16 -1
  89. package/build/src/database/pool/base.d.ts +21 -0
  90. package/build/src/database/pool/base.d.ts.map +1 -1
  91. package/build/src/database/pool/base.js +24 -1
  92. package/build/src/database/query/where-model-class-hash.d.ts.map +1 -1
  93. package/build/src/database/query/where-model-class-hash.js +3 -1
  94. package/build/src/database/query/with-count.js +2 -2
  95. package/build/src/database/record/acts-as-list.js +3 -3
  96. package/build/src/database/record/auditing.d.ts +1 -1
  97. package/build/src/database/record/auditing.d.ts.map +1 -1
  98. package/build/src/database/record/auditing.js +24 -7
  99. package/build/src/database/record/index.d.ts +7 -7
  100. package/build/src/database/record/index.js +8 -8
  101. package/build/src/database/record/validators/uniqueness.d.ts +3 -3
  102. package/build/src/database/record/validators/uniqueness.js +4 -4
  103. package/build/src/database/tenants/data-copier.d.ts +18 -18
  104. package/build/src/database/tenants/data-copier.js +24 -24
  105. package/build/src/database/tenants/schema-cloner.d.ts +27 -27
  106. package/build/src/database/tenants/schema-cloner.js +28 -28
  107. package/build/src/environment-handlers/browser.d.ts +9 -0
  108. package/build/src/environment-handlers/browser.d.ts.map +1 -1
  109. package/build/src/environment-handlers/browser.js +1 -2
  110. package/build/src/environment-handlers/node/cli/commands/background-jobs-runner.d.ts.map +1 -1
  111. package/build/src/environment-handlers/node/cli/commands/background-jobs-runner.js +3 -2
  112. package/build/src/environment-handlers/node/cli/commands/lint/relationships.js +3 -3
  113. package/build/src/environment-handlers/node.js +2 -2
  114. package/build/src/frontend-model-controller.d.ts +9 -8
  115. package/build/src/frontend-model-controller.d.ts.map +1 -1
  116. package/build/src/frontend-model-controller.js +26 -19
  117. package/build/src/frontend-model-resource/base-resource.js +5 -5
  118. package/build/src/frontend-models/base.d.ts +4 -4
  119. package/build/src/frontend-models/base.d.ts.map +1 -1
  120. package/build/src/frontend-models/base.js +6 -6
  121. package/build/src/frontend-models/query.d.ts +7 -7
  122. package/build/src/frontend-models/query.js +13 -13
  123. package/build/src/http-server/client/index.d.ts +23 -1
  124. package/build/src/http-server/client/index.d.ts.map +1 -1
  125. package/build/src/http-server/client/index.js +55 -17
  126. package/build/src/http-server/client/request-runner.d.ts.map +1 -1
  127. package/build/src/http-server/client/request-runner.js +8 -2
  128. package/build/src/http-server/client/response.d.ts +11 -1
  129. package/build/src/http-server/client/response.d.ts.map +1 -1
  130. package/build/src/http-server/client/response.js +16 -2
  131. package/build/src/http-server/client/websocket-session.d.ts +13 -13
  132. package/build/src/http-server/client/websocket-session.js +14 -14
  133. package/build/src/http-server/server-client.d.ts +13 -0
  134. package/build/src/http-server/server-client.d.ts.map +1 -1
  135. package/build/src/http-server/server-client.js +94 -1
  136. package/build/src/http-server/websocket-channel.d.ts +12 -11
  137. package/build/src/http-server/websocket-channel.d.ts.map +1 -1
  138. package/build/src/http-server/websocket-channel.js +13 -12
  139. package/build/src/http-server/websocket-connection.d.ts +12 -11
  140. package/build/src/http-server/websocket-connection.d.ts.map +1 -1
  141. package/build/src/http-server/websocket-connection.js +13 -12
  142. package/build/src/http-server/websocket-events-host.d.ts +1 -1
  143. package/build/src/http-server/websocket-events-host.js +2 -2
  144. package/build/src/http-server/worker-handler/in-process.d.ts +2 -0
  145. package/build/src/http-server/worker-handler/in-process.d.ts.map +1 -1
  146. package/build/src/http-server/worker-handler/in-process.js +42 -11
  147. package/build/src/http-server/worker-handler/index.d.ts +21 -0
  148. package/build/src/http-server/worker-handler/index.d.ts.map +1 -1
  149. package/build/src/http-server/worker-handler/index.js +61 -7
  150. package/build/src/http-server/worker-handler/worker-thread.d.ts +30 -0
  151. package/build/src/http-server/worker-handler/worker-thread.d.ts.map +1 -1
  152. package/build/src/http-server/worker-handler/worker-thread.js +61 -1
  153. package/build/src/sync/device-identity.d.ts +2 -2
  154. package/build/src/sync/device-identity.d.ts.map +1 -1
  155. package/build/src/sync/device-identity.js +4 -4
  156. package/build/src/sync/local-mutation-log.d.ts +0 -1
  157. package/build/src/sync/local-mutation-log.d.ts.map +1 -1
  158. package/build/src/sync/local-mutation-log.js +1 -2
  159. package/build/src/sync/query-scope.d.ts +5 -0
  160. package/build/src/sync/query-scope.d.ts.map +1 -1
  161. package/build/src/sync/query-scope.js +7 -2
  162. package/build/src/sync/sync-client-types.d.ts +2 -2
  163. package/build/src/sync/sync-client-types.d.ts.map +1 -1
  164. package/build/src/sync/sync-client-types.js +1 -1
  165. package/build/src/sync/sync-client.d.ts +17 -2
  166. package/build/src/sync/sync-client.d.ts.map +1 -1
  167. package/build/src/sync/sync-client.js +28 -14
  168. package/build/src/sync/sync-realtime-bridge.d.ts +6 -2
  169. package/build/src/sync/sync-realtime-bridge.d.ts.map +1 -1
  170. package/build/src/sync/sync-realtime-bridge.js +14 -3
  171. package/build/src/sync/sync-resource-base.d.ts +14 -3
  172. package/build/src/sync/sync-resource-base.d.ts.map +1 -1
  173. package/build/src/sync/sync-resource-base.js +20 -4
  174. package/build/src/sync/sync-scope-store.d.ts +3 -3
  175. package/build/src/sync/sync-scope-store.d.ts.map +1 -1
  176. package/build/src/sync/sync-scope-store.js +7 -5
  177. package/build/src/sync/sync-websocket-channel.d.ts +10 -0
  178. package/build/src/sync/sync-websocket-channel.d.ts.map +1 -1
  179. package/build/src/sync/sync-websocket-channel.js +20 -3
  180. package/build/src/tenants/tenant-iterator.d.ts +8 -8
  181. package/build/src/tenants/tenant-iterator.js +9 -9
  182. package/build/src/tenants/tenant.d.ts +6 -6
  183. package/build/src/tenants/tenant.js +7 -7
  184. package/build/src/testing/test-runner.d.ts +16 -0
  185. package/build/src/testing/test-runner.d.ts.map +1 -1
  186. package/build/src/testing/test-runner.js +79 -19
  187. package/build/sync/device-identity.js +3 -3
  188. package/build/sync/local-mutation-log.js +0 -1
  189. package/build/sync/query-scope.js +6 -1
  190. package/build/sync/sync-client-types.js +1 -1
  191. package/build/sync/sync-client.js +28 -15
  192. package/build/sync/sync-realtime-bridge.js +13 -2
  193. package/build/sync/sync-resource-base.js +20 -3
  194. package/build/sync/sync-scope-store.js +6 -4
  195. package/build/sync/sync-websocket-channel.js +18 -2
  196. package/build/tenants/tenant-iterator.js +8 -8
  197. package/build/tenants/tenant.js +6 -6
  198. package/build/testing/test-runner.js +86 -20
  199. package/package.json +2 -2
  200. package/src/background-jobs/cron-expression.js +9 -9
  201. package/src/background-jobs/forked-runner-child.js +1 -1
  202. package/src/background-jobs/job-runner.js +21 -15
  203. package/src/background-jobs/json-socket.js +4 -0
  204. package/src/background-jobs/main.js +180 -18
  205. package/src/background-jobs/status-reporter.js +12 -5
  206. package/src/background-jobs/store.js +132 -50
  207. package/src/background-jobs/types.js +11 -3
  208. package/src/background-jobs/worker.js +9 -5
  209. package/src/beacon/client.js +13 -6
  210. package/src/configuration.js +54 -29
  211. package/src/controller.js +9 -3
  212. package/src/database/drivers/sqlite/base.js +1 -1
  213. package/src/database/drivers/sqlite/index.js +15 -14
  214. package/src/database/migrations-ledger.js +16 -16
  215. package/src/database/pool/async-tracked-multi-connection.js +16 -0
  216. package/src/database/pool/base.js +26 -0
  217. package/src/database/query/where-model-class-hash.js +2 -0
  218. package/src/database/query/with-count.js +1 -1
  219. package/src/database/record/acts-as-list.js +2 -2
  220. package/src/database/record/auditing.js +23 -6
  221. package/src/database/record/index.js +7 -7
  222. package/src/database/record/validators/uniqueness.js +3 -3
  223. package/src/database/tenants/data-copier.js +23 -23
  224. package/src/database/tenants/schema-cloner.js +27 -27
  225. package/src/environment-handlers/browser.js +0 -1
  226. package/src/environment-handlers/node/cli/commands/background-jobs-runner.js +2 -1
  227. package/src/environment-handlers/node/cli/commands/lint/relationships.js +2 -2
  228. package/src/environment-handlers/node.js +1 -1
  229. package/src/frontend-model-controller.js +26 -18
  230. package/src/frontend-model-resource/base-resource.js +4 -4
  231. package/src/frontend-models/base.js +5 -5
  232. package/src/frontend-models/query.js +12 -12
  233. package/src/http-server/client/index.js +61 -15
  234. package/src/http-server/client/request-runner.js +7 -1
  235. package/src/http-server/client/response.js +17 -1
  236. package/src/http-server/client/websocket-session.js +13 -13
  237. package/src/http-server/server-client.js +96 -0
  238. package/src/http-server/websocket-channel.js +12 -11
  239. package/src/http-server/websocket-connection.js +12 -11
  240. package/src/http-server/websocket-events-host.js +1 -1
  241. package/src/http-server/worker-handler/in-process.js +48 -9
  242. package/src/http-server/worker-handler/index.js +67 -7
  243. package/src/http-server/worker-handler/worker-thread.js +68 -0
  244. package/src/sync/device-identity.js +3 -3
  245. package/src/sync/local-mutation-log.js +0 -1
  246. package/src/sync/query-scope.js +6 -1
  247. package/src/sync/sync-client-types.js +1 -1
  248. package/src/sync/sync-client.js +28 -15
  249. package/src/sync/sync-realtime-bridge.js +13 -2
  250. package/src/sync/sync-resource-base.js +20 -3
  251. package/src/sync/sync-scope-store.js +6 -4
  252. package/src/sync/sync-websocket-channel.js +18 -2
  253. package/src/tenants/tenant-iterator.js +8 -8
  254. package/src/tenants/tenant.js +6 -6
  255. package/src/testing/test-runner.js +86 -20
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import EventEmitter from "../utils/event-emitter.js"
4
+ import {createReadStream} from "node:fs"
4
5
  import Logger from "../logger.js"
5
6
 
6
7
  /**
@@ -205,6 +206,101 @@ export default class ServerClient {
205
206
  })
206
207
  }
207
208
 
209
+ /**
210
+ * Streams a file to the socket while respecting socket write backpressure.
211
+ * @param {string} filePath - File path.
212
+ * @param {boolean} [sendBody] - Whether to read and send the file body.
213
+ * @returns {Promise<"completed" | "aborted">} - Transfer result.
214
+ */
215
+ async sendFile(filePath, sendBody = true) {
216
+ if (this.socket.destroyed || this.socket.writableEnded || this.socket.writable === false) return "aborted"
217
+ if (!sendBody) return "completed"
218
+
219
+ const readStream = createReadStream(filePath)
220
+ let aborted = false
221
+ const abort = () => {
222
+ aborted = true
223
+ readStream.destroy()
224
+ }
225
+
226
+ this.socket.once("close", abort)
227
+ this.socket.once("error", abort)
228
+
229
+ try {
230
+ for await (const chunk of readStream) {
231
+ if (aborted || !await this.writeFileChunk(chunk)) return "aborted"
232
+ }
233
+
234
+ return aborted ? "aborted" : "completed"
235
+ } catch (error) {
236
+ this.logger.error(() => [`Socket ${this.clientCount} file response failed`, filePath, error])
237
+ if (!this.socket.destroyed) this.socket.destroy()
238
+
239
+ return "aborted"
240
+ } finally {
241
+ this.socket.off("close", abort)
242
+ this.socket.off("error", abort)
243
+ readStream.destroy()
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Writes one file chunk and waits for both write acceptance and drain when required.
249
+ * @param {Buffer | Uint8Array} chunk - File chunk.
250
+ * @returns {Promise<boolean>} - Whether the chunk was accepted before the socket aborted.
251
+ */
252
+ writeFileChunk(chunk) {
253
+ return new Promise((resolve) => {
254
+ let callbackCompleted = false
255
+ let drained = false
256
+ let settled = false
257
+
258
+ const cleanup = () => {
259
+ this.socket.off("close", onAbort)
260
+ this.socket.off("error", onAbort)
261
+ this.socket.off("drain", onDrain)
262
+ }
263
+ const finish = (/** @type {boolean} */ result) => {
264
+ if (settled) return
265
+
266
+ settled = true
267
+ cleanup()
268
+ resolve(result)
269
+ }
270
+ const finishIfReady = () => {
271
+ if (callbackCompleted && drained) finish(true)
272
+ }
273
+ const onAbort = () => finish(false)
274
+ const onDrain = () => {
275
+ drained = true
276
+ finishIfReady()
277
+ }
278
+
279
+ this.socket.once("close", onAbort)
280
+ this.socket.once("error", onAbort)
281
+ this.socket.once("drain", onDrain)
282
+
283
+ try {
284
+ const accepted = this.socket.write(chunk, (error) => {
285
+ if (error) {
286
+ finish(false)
287
+ return
288
+ }
289
+
290
+ callbackCompleted = true
291
+ finishIfReady()
292
+ })
293
+
294
+ drained = accepted
295
+ if (accepted) this.socket.off("drain", onDrain)
296
+ finishIfReady()
297
+ } catch (error) {
298
+ this.logger.error(() => [`Socket ${this.clientCount} file write failed`, error])
299
+ finish(false)
300
+ }
301
+ })
302
+ }
303
+
208
304
  /**
209
305
  * Runs set worker.
210
306
  * @param {import("worker_threads").Worker} newWorker - New worker.
@@ -20,7 +20,7 @@
20
20
  export default class VelociousWebsocketChannel {
21
21
  /**
22
22
  * Runs constructor.
23
- * @param {object} args
23
+ * @param {object} args - Session, channel parameters, and client subscription identifier.
24
24
  * @param {string} args.subscriptionId - Client-assigned id, unique within the session.
25
25
  * @param {WebsocketParams} args.params - Subscribe params.
26
26
  * @param {import("./client/websocket-session.js").default} args.session - Owning session.
@@ -36,7 +36,7 @@ export default class VelociousWebsocketChannel {
36
36
  * Subscribe-time auth. Default is `false` (deny). Channel authors
37
37
  * MUST override to allow subscriptions. Returning a Promise defers
38
38
  * the `channel-subscribed` confirmation until it resolves.
39
- * @returns {boolean | Promise<boolean>}
39
+ * @returns {boolean | Promise<boolean>} - Whether the subscription is authorized.
40
40
  */
41
41
  canSubscribe() { return false }
42
42
 
@@ -44,14 +44,14 @@ export default class VelociousWebsocketChannel {
44
44
  * Optional — called once after `canSubscribe` resolves truthy and
45
45
  * before `channel-subscribed` is sent to the client. Use for
46
46
  * initial snapshot delivery.
47
- * @returns {void | Promise<void>}
47
+ * @returns {void | Promise<void>} - Completes after subscription setup.
48
48
  */
49
49
  subscribed() {}
50
50
 
51
51
  /**
52
52
  * Optional — called once when the subscription ends. Fires on
53
53
  * client-initiated `channel-unsubscribe` or on session teardown.
54
- * @returns {void | Promise<void>}
54
+ * @returns {void | Promise<void>} - Completes after subscription teardown.
55
55
  */
56
56
  unsubscribed() {}
57
57
 
@@ -60,14 +60,14 @@ export default class VelociousWebsocketChannel {
60
60
  * moved into the paused/grace registry. Either `onResume` fires
61
61
  * on successful client reconnect, or `unsubscribed()` fires when
62
62
  * the grace window expires.
63
- * @returns {void | Promise<void>}
63
+ * @returns {void | Promise<void>} - Completes after disconnect handling.
64
64
  */
65
65
  onDisconnect() {}
66
66
 
67
67
  /**
68
68
  * Called after a client reconnect + `session-resume` rebinds this
69
69
  * subscription to a new socket.
70
- * @returns {void | Promise<void>}
70
+ * @returns {void | Promise<void>} - Completes after resume handling.
71
71
  */
72
72
  onResume() {}
73
73
 
@@ -76,7 +76,7 @@ export default class VelociousWebsocketChannel {
76
76
  * sign-in / locale change). Override to react to session-level
77
77
  * metadata updates.
78
78
  * @param {WebsocketParams} _metadata - Updated metadata.
79
- * @returns {void | Promise<void>}
79
+ * @returns {void | Promise<void>} - Completes after metadata-change handling.
80
80
  */
81
81
  onMetadataChanged(_metadata) {}
82
82
 
@@ -101,9 +101,9 @@ export default class VelociousWebsocketChannel {
101
101
  * Delivers a matched broadcast to this subscriber. Subclasses can
102
102
  * override when the outbound body must be tailored to subscription
103
103
  * params before sending.
104
- * @param {WebsocketJsonValue} body
104
+ * @param {WebsocketJsonValue} body - Broadcast payload offered to this subscription.
105
105
  * @param {{eventId?: string}} [meta] - Optional event metadata.
106
- * @returns {void | Promise<void>}
106
+ * @returns {void | Promise<void>} - Completes after broadcast delivery.
107
107
  */
108
108
  deliverBroadcast(body, meta) {
109
109
  this.sendMessage(body, meta)
@@ -113,7 +113,7 @@ export default class VelociousWebsocketChannel {
113
113
  * Sends a `channel-message` frame to THIS subscriber only.
114
114
  * When `meta.eventId` is provided, the client receives it so it
115
115
  * can track its checkpoint for `lastEventId` replay on reconnect.
116
- * @param {WebsocketJsonValue} body
116
+ * @param {WebsocketJsonValue} body - Channel payload to send to the subscribed client.
117
117
  * @param {{eventId?: string}} [meta] - Optional event metadata.
118
118
  * @returns {void}
119
119
  */
@@ -132,6 +132,7 @@ export default class VelociousWebsocketChannel {
132
132
 
133
133
  /**
134
134
  * Runs is closed.
135
- * @returns {boolean} */
135
+ * @returns {boolean} - Whether the channel is closed.
136
+ */
136
137
  isClosed() { return this._closed }
137
138
  }
@@ -12,7 +12,7 @@
12
12
  export default class VelociousWebsocketConnection {
13
13
  /**
14
14
  * Runs constructor.
15
- * @param {object} args
15
+ * @param {object} args - Owning session, connection parameters, and client identifier.
16
16
  * @param {string} args.connectionId - Client-assigned id, unique within the session.
17
17
  * @param {Record<string, ?>} args.params - Opaque params from the `connection-open` message.
18
18
  * @param {import("./client/websocket-session.js").default} args.session - Owning session.
@@ -28,7 +28,7 @@ export default class VelociousWebsocketConnection {
28
28
  * Called once after the session registers this connection and before
29
29
  * any `onMessage` fires. Returning a Promise defers the first
30
30
  * `connection-opened` message to the client until it resolves.
31
- * @returns {void | Promise<void>}
31
+ * @returns {void | Promise<void>} - Completes after connection setup.
32
32
  */
33
33
  onConnect() {}
34
34
 
@@ -36,8 +36,8 @@ export default class VelociousWebsocketConnection {
36
36
  * Called for each `connection-message` the client sends to this
37
37
  * specific connection. Messages arriving before `onConnect` has
38
38
  * resolved are queued and delivered in order once it finishes.
39
- * @param {?} body
40
- * @returns {void | Promise<void>}
39
+ * @param {?} body - Client-sent payload for this connection.
40
+ * @returns {void | Promise<void>} - Completes after message handling.
41
41
  */
42
42
  onMessage(body) { void body }
43
43
 
@@ -47,14 +47,14 @@ export default class VelociousWebsocketConnection {
47
47
  * itself survives; either `onResume` fires on a successful
48
48
  * client reconnect, or `onClose("grace_expired")` fires when the
49
49
  * grace window expires.
50
- * @returns {void | Promise<void>}
50
+ * @returns {void | Promise<void>} - Completes after disconnect handling.
51
51
  */
52
52
  onDisconnect() {}
53
53
 
54
54
  /**
55
55
  * Called after a client reconnect + `session-resume` rebinds this
56
56
  * connection to a new socket.
57
- * @returns {void | Promise<void>}
57
+ * @returns {void | Promise<void>} - Completes after resume handling.
58
58
  */
59
59
  onResume() {}
60
60
 
@@ -64,15 +64,15 @@ export default class VelociousWebsocketConnection {
64
64
  * (server-initiated `close()`), `session_destroyed` (socket dropped
65
65
  * and nothing to resume; grace path did not apply), `grace_expired`
66
66
  * (paused session's grace window ran out without resume), `error`.
67
- * @param {"client_close" | "server_close" | "session_destroyed" | "grace_expired" | "error"} reason
68
- * @returns {void | Promise<void>}
67
+ * @param {"client_close" | "server_close" | "session_destroyed" | "grace_expired" | "error"} reason - Lifecycle reason for permanent connection teardown.
68
+ * @returns {void | Promise<void>} - Completes after close handling.
69
69
  */
70
70
  onClose(reason) { void reason }
71
71
 
72
72
  /**
73
73
  * Sends a `connection-message` frame to the client side of this
74
74
  * connection. Throws if the connection has already been closed.
75
- * @param {?} body
75
+ * @param {?} body - Connection payload to send to the client.
76
76
  * @returns {void}
77
77
  */
78
78
  sendMessage(body) {
@@ -90,7 +90,7 @@ export default class VelociousWebsocketConnection {
90
90
  /**
91
91
  * Closes this connection from the server side. Fires `onClose`
92
92
  * locally and notifies the client with `{type: "connection-closed"}`.
93
- * @param {"server_close" | "error"} [reason]
93
+ * @param {"server_close" | "error"} [reason] - Reason reported to the close hook and client.
94
94
  * @returns {Promise<void>}
95
95
  */
96
96
  async close(reason = "server_close") {
@@ -111,7 +111,8 @@ export default class VelociousWebsocketConnection {
111
111
 
112
112
  /**
113
113
  * Runs is closed.
114
- * @returns {boolean} */
114
+ * @returns {boolean} - Whether the connection is closed.
115
+ */
115
116
  isClosed() {
116
117
  return this._closed
117
118
  }
@@ -120,7 +120,7 @@ export class VelociousHttpServerWebsocketEventsHost {
120
120
  * @param {object} args - Options.
121
121
  * @param {?} args.body - Event body.
122
122
  * @param {string} args.channel - Channel name.
123
- * @returns {Promise<{createdAt: string, id: string} | null>}
123
+ * @returns {Promise<{createdAt: string, id: string} | null>} - Persisted event metadata when storage is enabled.
124
124
  */
125
125
  async _persistV2EventIfNeeded({body, channel}) {
126
126
  return await this._persistChannelEventIfNeeded({channel, payload: body})
@@ -26,6 +26,9 @@ export default class VelociousHttpServerInProcessHandler {
26
26
  * @type {Record<number, {httpClient: Client, serverClient: import("../server-client.js").default}>} */
27
27
  this.clients = {}
28
28
 
29
+ /** @type {Set<Promise<void>>} */
30
+ this.pendingClientCloseCleanups = new Set()
31
+
29
32
  this.logger = new Logger(this)
30
33
  this.workerCount = workerCount
31
34
  this.unregisterFromEventsHost = websocketEventsHost.register(/** @type {?} */ (this))
@@ -53,17 +56,48 @@ export default class VelociousHttpServerInProcessHandler {
53
56
  remoteAddress: serverClient.remoteAddress
54
57
  })
55
58
 
59
+ let deliveryQueue = Promise.resolve()
60
+ const enqueueDelivery = (/** @type {() => Promise<void>} */ delivery) => {
61
+ deliveryQueue = deliveryQueue
62
+ .catch(() => {})
63
+ .then(delivery)
64
+
65
+ return deliveryQueue
66
+ }
67
+
56
68
  httpClient.events.on("output", (output) => {
57
69
  if (output !== null && output !== undefined) {
58
- void serverClient.send(output).catch((error) => {
70
+ void enqueueDelivery(() => serverClient.send(output)).catch((error) => {
59
71
  this.logger.error(() => ["Failed to deliver client output", {clientCount}, error])
60
72
  })
61
73
  }
62
74
  })
63
75
 
76
+ httpClient.events.on("file", ({filePath, sendBody, settle}) => {
77
+ void enqueueDelivery(async () => {
78
+ await settle(await serverClient.sendFile(filePath, sendBody))
79
+ }).catch((error) => {
80
+ this.logger.error(() => ["Failed to deliver file response", {clientCount, filePath}, error])
81
+ void settle("aborted")
82
+ })
83
+ })
84
+
64
85
  httpClient.events.on("close", () => {
65
- void serverClient.end()
66
- delete this.clients[clientCount]
86
+ void enqueueDelivery(() => serverClient.end())
87
+ .finally(() => delete this.clients[clientCount])
88
+ })
89
+
90
+ serverClient.events.on("close", () => {
91
+ const cleanup = httpClient.abortPendingFileResponses()
92
+ .catch((error) => {
93
+ this.logger.warn("Failed to abort file responses after client close", error)
94
+ })
95
+ .finally(() => {
96
+ this.pendingClientCloseCleanups.delete(cleanup)
97
+ delete this.clients[clientCount]
98
+ })
99
+
100
+ this.pendingClientCloseCleanups.add(cleanup)
67
101
  })
68
102
 
69
103
  this.clients[clientCount] = {httpClient, serverClient}
@@ -90,14 +124,19 @@ export default class VelociousHttpServerInProcessHandler {
90
124
  async stop() {
91
125
  this._stopping = true
92
126
 
93
- for (const {serverClient} of Object.values(this.clients)) {
94
- try {
95
- void serverClient.end()
96
- } catch (error) {
97
- this.logger.warn("Failed to close client during shutdown", error)
98
- }
127
+ for (const {httpClient, serverClient} of Object.values(this.clients)) {
128
+ await Promise.all([
129
+ httpClient.abortPendingFileResponses().catch((error) => {
130
+ this.logger.warn("Failed to abort file responses during shutdown", error)
131
+ }),
132
+ serverClient.end().catch((error) => {
133
+ this.logger.warn("Failed to close client during shutdown", error)
134
+ })
135
+ ])
99
136
  }
100
137
 
138
+ await Promise.all(this.pendingClientCloseCleanups)
139
+
101
140
  this.clients = {}
102
141
  this.unregisterFromEventsHost?.()
103
142
  }
@@ -58,6 +58,9 @@ export default class VelociousHttpServerWorker {
58
58
  * Narrows the runtime value to the documented type.
59
59
  * @type {Map<number, {resolve: (snapshot: Record<string, ?>) => void}>} */
60
60
  this._debugSnapshotRequests = new Map()
61
+
62
+ /** @type {Map<number, Promise<void>>} */
63
+ this._clientDeliveryQueues = new Map()
61
64
  }
62
65
 
63
66
  start() {
@@ -99,9 +102,25 @@ export default class VelociousHttpServerWorker {
99
102
  client.listen()
100
103
 
101
104
  this.clients[clientCount] = client
105
+ client.events.on("close", () => {
106
+ this.handleClientAbort(clientCount)
107
+ })
102
108
  this.worker.postMessage({command: "newClient", clientCount, remoteAddress: client.remoteAddress})
103
109
  }
104
110
 
111
+ /**
112
+ * Propagates a parent-side socket close and clears all parent state for the client.
113
+ * @param {number} clientCount - Client count.
114
+ * @returns {void}
115
+ */
116
+ handleClientAbort(clientCount) {
117
+ if (!this.clients[clientCount] && !this._clientDeliveryQueues.has(clientCount)) return
118
+
119
+ delete this.clients[clientCount]
120
+ this._clientDeliveryQueues.delete(clientCount)
121
+ this.worker?.postMessage({command: "clientAbort", clientCount})
122
+ }
123
+
105
124
  /**
106
125
  * On worker error.
107
126
  * @param {?} error - Error instance.
@@ -121,10 +140,10 @@ export default class VelociousHttpServerWorker {
121
140
  onWorkerExit = (code) => {
122
141
  this._hasExited = true
123
142
  this.workerStarted = false
143
+ this._closeAllClients()
124
144
 
125
145
  if (code !== 0 && !this._stopping) {
126
146
  this.logger.error(`Velocious worker ${this.workerCount} exited unexpectedly with code ${code}`)
127
- void this._closeAllClients()
128
147
  throw new Error(`Client worker stopped with exit code ${code}`)
129
148
  } else {
130
149
  this.logger.debug(() => `Client worker stopped with exit code ${code}`)
@@ -160,6 +179,9 @@ export default class VelociousHttpServerWorker {
160
179
  * @param {string} data.command - Command.
161
180
  * @param {number} [data.clientCount] - Client count.
162
181
  * @param {string | Uint8Array} [data.output] - Output.
182
+ * @param {string} [data.filePath] - File path.
183
+ * @param {boolean} [data.sendBody] - Whether to send the file body.
184
+ * @param {number} [data.transferId] - File transfer id.
163
185
  * @param {string} [data.channel] - Channel name.
164
186
  * @param {number} [data.requestId] - Debug request id.
165
187
  * @param {Record<string, ?>} [data.snapshot] - Worker debug snapshot.
@@ -197,7 +219,7 @@ export default class VelociousHttpServerWorker {
197
219
  if (output !== null && output !== undefined) {
198
220
  const outputLength = typeof output === "string" ? output.length : output.byteLength
199
221
 
200
- void client.send(output).then(() => {
222
+ void this.enqueueClientDelivery(client.clientCount, () => client.send(output)).then(() => {
201
223
  this.logger.debug(() => ["Client output delivered", {
202
224
  clientCount,
203
225
  outputLength,
@@ -210,6 +232,25 @@ export default class VelociousHttpServerWorker {
210
232
  }, error])
211
233
  })
212
234
  }
235
+ } else if (command == "clientFile") {
236
+ const {clientCount, filePath, sendBody, transferId} = data
237
+ const client = typeof clientCount === "number" ? this.clients[clientCount] : undefined
238
+
239
+ if (typeof transferId !== "number") throw new Error("clientFile transferId must be a number")
240
+
241
+ if (!client || typeof filePath !== "string") {
242
+ this.worker?.postMessage({command: "clientFileResult", result: "aborted", transferId})
243
+ return
244
+ }
245
+
246
+ void this.enqueueClientDelivery(client.clientCount, async () => {
247
+ const result = await client.sendFile(filePath, sendBody !== false)
248
+
249
+ this.worker?.postMessage({command: "clientFileResult", result, transferId})
250
+ }).catch((error) => {
251
+ this.logger.error(() => ["Failed to deliver file response", {clientCount: client.clientCount, filePath}, error])
252
+ this.worker?.postMessage({command: "clientFileResult", result: "aborted", transferId})
253
+ })
213
254
  } else if (command == "clientClose") {
214
255
  const {clientCount} = data
215
256
  const client = typeof clientCount === "number" ? this.clients[clientCount] : undefined
@@ -219,11 +260,8 @@ export default class VelociousHttpServerWorker {
219
260
  return
220
261
  }
221
262
 
222
- void client.end()
223
-
224
- if (typeof clientCount === "number") {
225
- delete this.clients[clientCount]
226
- }
263
+ void this.enqueueClientDelivery(client.clientCount, () => client.end())
264
+ .finally(() => delete this.clients[client.clientCount])
227
265
  } else if (command == "debugSnapshot") {
228
266
  const {requestId, snapshot} = data
229
267
  if (typeof requestId !== "number") throw new Error("debugSnapshot requestId must be a number")
@@ -257,6 +295,28 @@ export default class VelociousHttpServerWorker {
257
295
  }
258
296
  }
259
297
 
298
+ /**
299
+ * Preserves socket output ordering for one client.
300
+ * @param {number} clientCount - Client count.
301
+ * @param {() => Promise<void>} delivery - Delivery operation.
302
+ * @returns {Promise<void>} - Queued delivery.
303
+ */
304
+ enqueueClientDelivery(clientCount, delivery) {
305
+ const previous = this._clientDeliveryQueues.get(clientCount) || Promise.resolve()
306
+ const queued = previous
307
+ .catch(() => {})
308
+ .then(delivery)
309
+
310
+ this._clientDeliveryQueues.set(clientCount, queued)
311
+ const clearQueue = () => {
312
+ if (this._clientDeliveryQueues.get(clientCount) === queued) this._clientDeliveryQueues.delete(clientCount)
313
+ }
314
+
315
+ void queued.then(clearQueue, clearQueue)
316
+
317
+ return queued
318
+ }
319
+
260
320
  /**
261
321
  * Runs get debug snapshot.
262
322
  * @returns {Promise<Record<string, ?>>} - Worker-local debug snapshot.
@@ -42,6 +42,10 @@ export default class VelociousHttpServerWorkerHandlerWorkerThread {
42
42
  this.parentPort = parentPort
43
43
  this.workerData = workerData
44
44
  this.workerCount = workerCount
45
+ this.fileTransferCount = 0
46
+
47
+ /** @type {Map<number, {clientCount: number, settle: (result: "completed" | "aborted") => Promise<void>}>} */
48
+ this.fileTransfers = new Map()
45
49
 
46
50
  parentPort.on("message", errorLogger(this.onCommand))
47
51
 
@@ -98,6 +102,8 @@ export default class VelociousHttpServerWorkerHandlerWorkerThread {
98
102
  * @param {string} [data.createdAt] - Event creation time.
99
103
  * @param {string} [data.eventId] - Event identifier.
100
104
  * @param {number} [data.requestId] - Debug request id.
105
+ * @param {number} [data.transferId] - File transfer id.
106
+ * @param {"completed" | "aborted"} [data.result] - File transfer result.
101
107
  * @param {?} [data.payload] - Payload data.
102
108
  * @param {Record<string, ?>} [data.broadcastParams] - V2 broadcast filter params.
103
109
  * @param {?} [data.body] - V2 broadcast body.
@@ -111,6 +117,10 @@ export default class VelociousHttpServerWorkerHandlerWorkerThread {
111
117
  this.handleNewClient(data)
112
118
  } else if (command == "clientWrite") {
113
119
  await this.handleClientWrite(data)
120
+ } else if (command == "clientFileResult") {
121
+ await this.handleClientFileResult(data)
122
+ } else if (command == "clientAbort") {
123
+ await this.handleClientAbort(data)
114
124
  } else if (command == "websocketEvent") {
115
125
  await this.handleWebsocketEvent(data)
116
126
  } else if (command == "websocketV2Broadcast") {
@@ -148,6 +158,13 @@ export default class VelociousHttpServerWorkerHandlerWorkerThread {
148
158
  this.parentPort.postMessage({command: "clientOutput", clientCount, output})
149
159
  })
150
160
 
161
+ client.events.on("file", ({filePath, sendBody, settle}) => {
162
+ const transferId = ++this.fileTransferCount
163
+
164
+ this.fileTransfers.set(transferId, {clientCount, settle})
165
+ this.parentPort.postMessage({command: "clientFile", clientCount, filePath, sendBody, transferId})
166
+ })
167
+
151
168
  client.events.on("close", (output) => {
152
169
  this.logger.debugLowLevel(() => "Close received from client in worker - forwarding to worker parent")
153
170
  this.parentPort.postMessage({command: "clientClose", clientCount, output})
@@ -156,6 +173,54 @@ export default class VelociousHttpServerWorkerHandlerWorkerThread {
156
173
  this.clients[clientCount] = client
157
174
  }
158
175
 
176
+ /**
177
+ * Settles a file response after the parent finishes socket delivery.
178
+ * @param {object} data - File result message.
179
+ * @param {number} [data.transferId] - File transfer id.
180
+ * @param {"completed" | "aborted"} [data.result] - File transfer result.
181
+ * @returns {Promise<void>} - Resolves after the worker-side completion callback settles.
182
+ */
183
+ async handleClientFileResult(data) {
184
+ const {result, transferId} = data
185
+
186
+ if (typeof transferId !== "number") throw new Error("transferId must be a number")
187
+ if (result !== "completed" && result !== "aborted") throw new Error(`Unknown file transfer result: ${result}`)
188
+
189
+ const transfer = this.fileTransfers.get(transferId)
190
+
191
+ if (!transfer) return
192
+
193
+ this.fileTransfers.delete(transferId)
194
+ await transfer.settle(result)
195
+ }
196
+
197
+ /**
198
+ * Aborts file responses belonging to a closed parent-side socket.
199
+ * @param {object} data - Client abort message.
200
+ * @param {number} [data.clientCount] - Client count.
201
+ * @returns {Promise<void>} - Resolves after pending completion callbacks settle.
202
+ */
203
+ async handleClientAbort(data) {
204
+ const {clientCount} = data
205
+
206
+ if (typeof clientCount !== "number") throw new Error("clientCount must be a number")
207
+
208
+ const settlements = []
209
+ const client = this.clients[clientCount]
210
+
211
+ if (client) settlements.push(client.abortPendingFileResponses())
212
+
213
+ for (const [transferId, transfer] of this.fileTransfers) {
214
+ if (transfer.clientCount !== clientCount) continue
215
+
216
+ this.fileTransfers.delete(transferId)
217
+ settlements.push(transfer.settle("aborted"))
218
+ }
219
+
220
+ delete this.clients[clientCount]
221
+ await Promise.all(settlements)
222
+ }
223
+
159
224
  /**
160
225
  * Runs handle client write.
161
226
  * @param {object} data - Data payload.
@@ -238,6 +303,9 @@ export default class VelociousHttpServerWorkerHandlerWorkerThread {
238
303
  * @returns {Promise<void>} Resolves after worker shutdown has been requested.
239
304
  */
240
305
  async handleShutdown() {
306
+ await Promise.all(Object.values(this.clients).map((client) => client.abortPendingFileResponses()))
307
+ this.fileTransfers.clear()
308
+
241
309
  if (this.configuration?.closeDatabaseConnections) {
242
310
  await this.configuration.closeDatabaseConnections()
243
311
  }
@@ -5,7 +5,7 @@ const ECDSA_P256_SHA256_SIGNATURE_PREFIX = "ecdsa-p256-sha256-"
5
5
 
6
6
  /**
7
7
  * JSON Web Key used by the sync signing helpers.
8
- * @typedef {JsonWebKey} SyncJsonWebKey
8
+ * @typedef {import("node:crypto").webcrypto.JsonWebKey} SyncJsonWebKey
9
9
  */
10
10
 
11
11
  /**
@@ -223,7 +223,7 @@ function signedMutationSignatureValue({deviceCertificate, mutation}) {
223
223
  */
224
224
  async function signStableJson({privateKey, value}) {
225
225
  const key = await cryptoSubtle().importKey("jwk", privateKey, {name: "ECDSA", namedCurve: "P-256"}, false, ["sign"])
226
- const signature = await cryptoSubtle().sign({hash: "SHA-256", name: "ECDSA"}, key, /** @type {BufferSource} */ (utf8(stableJsonStringify(value))))
226
+ const signature = await cryptoSubtle().sign({hash: "SHA-256", name: "ECDSA"}, key, /** @type {Uint8Array<ArrayBuffer>} */ (utf8(stableJsonStringify(value))))
227
227
 
228
228
  return `${ECDSA_P256_SHA256_SIGNATURE_PREFIX}${base64UrlEncode(new Uint8Array(signature))}`
229
229
  }
@@ -240,7 +240,7 @@ async function verifyStableJsonSignature({publicKey, signature, value}) {
240
240
  const key = await cryptoSubtle().importKey("jwk", publicKey, {name: "ECDSA", namedCurve: "P-256"}, false, ["verify"])
241
241
  const signatureBytes = base64UrlDecode(signatureWithoutPrefix(signature))
242
242
 
243
- return await cryptoSubtle().verify({hash: "SHA-256", name: "ECDSA"}, key, /** @type {BufferSource} */ (signatureBytes), /** @type {BufferSource} */ (utf8(stableJsonStringify(value))))
243
+ return await cryptoSubtle().verify({hash: "SHA-256", name: "ECDSA"}, key, /** @type {Uint8Array<ArrayBuffer>} */ (signatureBytes), /** @type {Uint8Array<ArrayBuffer>} */ (utf8(stableJsonStringify(value))))
244
244
  }
245
245
 
246
246
  /**
@@ -20,7 +20,6 @@ const STORAGE_KEY_LOCKS = new Map()
20
20
  * Implementations should store each mutation log record as its own row/entry.
21
21
  * Native apps should back this with SQLite and indexes on storage key, status,
22
22
  * and sequence. Avoid storing the whole log as one JSON blob.
23
- *
24
23
  * @typedef {object} LocalMutationLogStorage
25
24
  * @property {(storageKey: string, record: LocalMutationLogRecord) => Promise<void> | void} appendRecord - Appends one log record.
26
25
  * @property {(storageKey: string, ids: string[]) => Promise<void> | void} deleteRecords - Deletes log records by id.
@@ -50,13 +50,18 @@ export function serializedScopeFromQuery(query) {
50
50
  * its own local identity and cursor — a user scope's empty-conditions cursor
51
51
  * never leaks across accounts on a shared device, while the same user
52
52
  * reconnecting keeps continuity. Owner-less scopes keep their pre-owner key.
53
+ *
54
+ * A null `resourceType` is the all-types scope (the user scope): one scope
55
+ * covering every resource type the server authorizes for the caller, rather
56
+ * than one scope per type. It keys as an empty resource type, so it never
57
+ * collides with a type-declared scope.
53
58
  * @param {import("./sync-client-types.js").SerializedSyncScope} scope - Serialized sync scope.
54
59
  * @returns {string} Stable scope key.
55
60
  */
56
61
  export function scopeKey(scope) {
57
62
  const ownerPrefix = scope.owner === undefined || scope.owner === null ? "" : `owner=${stableJsonStringify(scope.owner)}|`
58
63
 
59
- return `${ownerPrefix}${scope.resourceType}:${stableJsonStringify(scope.conditions)}`
64
+ return `${ownerPrefix}${scope.resourceType ?? ""}:${stableJsonStringify(scope.conditions)}`
60
65
  }
61
66
 
62
67
  /**