velocious 1.0.562 → 1.0.564

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 (245) hide show
  1. package/README.md +5 -1
  2. package/build/background-jobs/cron-expression.js +12 -13
  3. package/build/background-jobs/main.js +6 -6
  4. package/build/background-jobs/runner-graceful-shutdown.js +2 -3
  5. package/build/background-jobs/scheduler.js +3 -3
  6. package/build/background-jobs/worker.js +8 -9
  7. package/build/beacon/client.js +3 -4
  8. package/build/configuration-types.js +1 -0
  9. package/build/configuration.js +44 -3
  10. package/build/database/drivers/sqlite/web-persistence.js +2 -5
  11. package/build/database/pool/async-tracked-multi-connection.js +65 -15
  12. package/build/database/pool/base.js +23 -7
  13. package/build/database/query/index.js +21 -22
  14. package/build/database/query/model-class-query.js +5 -6
  15. package/build/database/record/attachments/normalize-input.js +9 -10
  16. package/build/database/record/attachments/store.js +2 -2
  17. package/build/database/record/auditing.js +7 -8
  18. package/build/database/record/index.js +4 -5
  19. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +1 -2
  20. package/build/frontend-model-controller.js +49 -56
  21. package/build/frontend-models/base.js +17 -19
  22. package/build/frontend-models/outgoing-event-buffer.js +3 -4
  23. package/build/frontend-models/query.js +40 -44
  24. package/build/frontend-models/resource-definition.js +7 -8
  25. package/build/frontend-models/transport-serialization.js +10 -12
  26. package/build/frontend-models/websocket-channel.js +12 -13
  27. package/build/http-server/client/websocket-session.js +7 -8
  28. package/build/http-server/client-delivery-queue.js +167 -0
  29. package/build/http-server/server-client.js +9 -0
  30. package/build/http-server/websocket-event-log-store.js +4 -6
  31. package/build/http-server/websocket-events-host.js +42 -12
  32. package/build/http-server/worker-handler/in-process.js +53 -14
  33. package/build/http-server/worker-handler/index.js +88 -18
  34. package/build/http-server/worker-handler/worker-thread.js +2 -2
  35. package/build/plugins/sqljs-wasm-route.js +3 -5
  36. package/build/src/background-jobs/cron-expression.d.ts +0 -12
  37. package/build/src/background-jobs/cron-expression.d.ts.map +1 -1
  38. package/build/src/background-jobs/cron-expression.js +13 -13
  39. package/build/src/background-jobs/main.d.ts.map +1 -1
  40. package/build/src/background-jobs/main.js +7 -7
  41. package/build/src/background-jobs/runner-graceful-shutdown.d.ts +0 -6
  42. package/build/src/background-jobs/runner-graceful-shutdown.d.ts.map +1 -1
  43. package/build/src/background-jobs/runner-graceful-shutdown.js +3 -3
  44. package/build/src/background-jobs/scheduler.d.ts +0 -3
  45. package/build/src/background-jobs/scheduler.d.ts.map +1 -1
  46. package/build/src/background-jobs/scheduler.js +4 -4
  47. package/build/src/background-jobs/worker.d.ts +0 -8
  48. package/build/src/background-jobs/worker.d.ts.map +1 -1
  49. package/build/src/background-jobs/worker.js +9 -9
  50. package/build/src/beacon/client.d.ts +0 -4
  51. package/build/src/beacon/client.d.ts.map +1 -1
  52. package/build/src/beacon/client.js +4 -4
  53. package/build/src/configuration-types.d.ts +8 -0
  54. package/build/src/configuration-types.d.ts.map +1 -1
  55. package/build/src/configuration-types.js +2 -1
  56. package/build/src/configuration.d.ts +34 -1
  57. package/build/src/configuration.d.ts.map +1 -1
  58. package/build/src/configuration.js +40 -4
  59. package/build/src/database/drivers/sqlite/web-persistence.d.ts +0 -14
  60. package/build/src/database/drivers/sqlite/web-persistence.d.ts.map +1 -1
  61. package/build/src/database/drivers/sqlite/web-persistence.js +3 -3
  62. package/build/src/database/pool/async-tracked-multi-connection.d.ts +29 -1
  63. package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
  64. package/build/src/database/pool/async-tracked-multi-connection.js +57 -15
  65. package/build/src/database/pool/base.d.ts +53 -4
  66. package/build/src/database/pool/base.d.ts.map +1 -1
  67. package/build/src/database/pool/base.js +23 -6
  68. package/build/src/database/query/index.d.ts +18 -39
  69. package/build/src/database/query/index.d.ts.map +1 -1
  70. package/build/src/database/query/index.js +22 -22
  71. package/build/src/database/query/model-class-query.d.ts +0 -5
  72. package/build/src/database/query/model-class-query.d.ts.map +1 -1
  73. package/build/src/database/query/model-class-query.js +6 -6
  74. package/build/src/database/record/attachments/normalize-input.d.ts +0 -9
  75. package/build/src/database/record/attachments/normalize-input.d.ts.map +1 -1
  76. package/build/src/database/record/attachments/normalize-input.js +10 -10
  77. package/build/src/database/record/attachments/store.d.ts.map +1 -1
  78. package/build/src/database/record/attachments/store.js +2 -2
  79. package/build/src/database/record/auditing.d.ts +17 -24
  80. package/build/src/database/record/auditing.d.ts.map +1 -1
  81. package/build/src/database/record/auditing.js +8 -8
  82. package/build/src/database/record/index.d.ts +4 -4
  83. package/build/src/database/record/index.d.ts.map +1 -1
  84. package/build/src/database/record/index.js +5 -5
  85. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +0 -39
  86. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
  87. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +2 -2
  88. package/build/src/frontend-model-controller.d.ts +1 -1
  89. package/build/src/frontend-model-controller.d.ts.map +1 -1
  90. package/build/src/frontend-model-controller.js +50 -50
  91. package/build/src/frontend-models/base.d.ts.map +1 -1
  92. package/build/src/frontend-models/base.js +18 -18
  93. package/build/src/frontend-models/outgoing-event-buffer.d.ts.map +1 -1
  94. package/build/src/frontend-models/outgoing-event-buffer.js +3 -3
  95. package/build/src/frontend-models/query.d.ts +56 -37
  96. package/build/src/frontend-models/query.d.ts.map +1 -1
  97. package/build/src/frontend-models/query.js +39 -39
  98. package/build/src/frontend-models/resource-definition.d.ts +0 -7
  99. package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
  100. package/build/src/frontend-models/resource-definition.js +8 -8
  101. package/build/src/frontend-models/transport-serialization.d.ts +0 -10
  102. package/build/src/frontend-models/transport-serialization.d.ts.map +1 -1
  103. package/build/src/frontend-models/transport-serialization.js +11 -11
  104. package/build/src/frontend-models/websocket-channel.d.ts +0 -12
  105. package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
  106. package/build/src/frontend-models/websocket-channel.js +13 -13
  107. package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
  108. package/build/src/http-server/client/websocket-session.js +8 -8
  109. package/build/src/http-server/client-delivery-queue.d.ts +121 -0
  110. package/build/src/http-server/client-delivery-queue.d.ts.map +1 -0
  111. package/build/src/http-server/client-delivery-queue.js +152 -0
  112. package/build/src/http-server/server-client.d.ts +6 -0
  113. package/build/src/http-server/server-client.d.ts.map +1 -1
  114. package/build/src/http-server/server-client.js +10 -1
  115. package/build/src/http-server/websocket-event-log-store.d.ts +0 -14
  116. package/build/src/http-server/websocket-event-log-store.d.ts.map +1 -1
  117. package/build/src/http-server/websocket-event-log-store.js +5 -5
  118. package/build/src/http-server/websocket-events-host.d.ts +15 -4
  119. package/build/src/http-server/websocket-events-host.d.ts.map +1 -1
  120. package/build/src/http-server/websocket-events-host.js +39 -13
  121. package/build/src/http-server/worker-handler/in-process.d.ts +19 -1
  122. package/build/src/http-server/worker-handler/in-process.d.ts.map +1 -1
  123. package/build/src/http-server/worker-handler/in-process.js +50 -14
  124. package/build/src/http-server/worker-handler/index.d.ts +36 -4
  125. package/build/src/http-server/worker-handler/index.d.ts.map +1 -1
  126. package/build/src/http-server/worker-handler/index.js +82 -18
  127. package/build/src/http-server/worker-handler/worker-thread.js +3 -3
  128. package/build/src/plugins/sqljs-wasm-route.d.ts.map +1 -1
  129. package/build/src/plugins/sqljs-wasm-route.js +4 -4
  130. package/build/src/sync/conflict-strategy.d.ts +0 -15
  131. package/build/src/sync/conflict-strategy.d.ts.map +1 -1
  132. package/build/src/sync/conflict-strategy.js +3 -3
  133. package/build/src/sync/device-identity.d.ts +0 -45
  134. package/build/src/sync/device-identity.d.ts.map +1 -1
  135. package/build/src/sync/device-identity.js +4 -4
  136. package/build/src/sync/local-mutation-log.d.ts +0 -41
  137. package/build/src/sync/local-mutation-log.d.ts.map +1 -1
  138. package/build/src/sync/local-mutation-log.js +9 -9
  139. package/build/src/sync/offline-grant.d.ts +0 -26
  140. package/build/src/sync/offline-grant.d.ts.map +1 -1
  141. package/build/src/sync/offline-grant.js +4 -4
  142. package/build/src/sync/peer-mutation-bundle.d.ts +0 -14
  143. package/build/src/sync/peer-mutation-bundle.d.ts.map +1 -1
  144. package/build/src/sync/peer-mutation-bundle.js +3 -3
  145. package/build/src/sync/server-change-feed.d.ts.map +1 -1
  146. package/build/src/sync/server-change-feed.js +33 -33
  147. package/build/src/sync/sync-api-client.d.ts +0 -9
  148. package/build/src/sync/sync-api-client.d.ts.map +1 -1
  149. package/build/src/sync/sync-api-client.js +2 -2
  150. package/build/src/sync/sync-envelope-replay-service.d.ts +38 -21
  151. package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
  152. package/build/src/sync/sync-envelope-replay-service.js +18 -18
  153. package/build/src/sync/sync-resource-base.d.ts +0 -7
  154. package/build/src/sync/sync-resource-base.d.ts.map +1 -1
  155. package/build/src/sync/sync-resource-base.js +2 -2
  156. package/build/src/sync/sync-scope-store.d.ts +0 -9
  157. package/build/src/sync/sync-scope-store.d.ts.map +1 -1
  158. package/build/src/sync/sync-scope-store.js +10 -10
  159. package/build/src/testing/test-filter-parser.d.ts +0 -10
  160. package/build/src/testing/test-filter-parser.d.ts.map +1 -1
  161. package/build/src/testing/test-filter-parser.js +11 -11
  162. package/build/src/testing/test-runner.d.ts +25 -91
  163. package/build/src/testing/test-runner.d.ts.map +1 -1
  164. package/build/src/testing/test-runner.js +113 -98
  165. package/build/src/testing/test.d.ts +8 -0
  166. package/build/src/testing/test.d.ts.map +1 -1
  167. package/build/src/testing/test.js +9 -9
  168. package/build/src/utils/backtrace-cleaner.d.ts.map +1 -1
  169. package/build/src/utils/backtrace-cleaner.js +9 -9
  170. package/build/src/utils/model-scope.d.ts +0 -8
  171. package/build/src/utils/model-scope.d.ts.map +1 -1
  172. package/build/src/utils/model-scope.js +3 -3
  173. package/build/src/utils/ransack.d.ts +52 -19
  174. package/build/src/utils/ransack.d.ts.map +1 -1
  175. package/build/src/utils/ransack.js +26 -26
  176. package/build/sync/conflict-strategy.js +3 -6
  177. package/build/sync/device-identity.js +4 -9
  178. package/build/sync/local-mutation-log.js +9 -14
  179. package/build/sync/offline-grant.js +4 -7
  180. package/build/sync/peer-mutation-bundle.js +2 -4
  181. package/build/sync/server-change-feed.js +32 -34
  182. package/build/sync/sync-api-client.js +1 -2
  183. package/build/sync/sync-envelope-replay-service.js +17 -19
  184. package/build/sync/sync-resource-base.js +1 -2
  185. package/build/sync/sync-scope-store.js +9 -10
  186. package/build/testing/test-filter-parser.js +10 -11
  187. package/build/testing/test-runner.js +116 -108
  188. package/build/testing/test.js +8 -9
  189. package/build/tsconfig.tsbuildinfo +1 -1
  190. package/build/utils/backtrace-cleaner.js +8 -9
  191. package/build/utils/model-scope.js +3 -4
  192. package/build/utils/ransack.js +27 -34
  193. package/package.json +3 -1
  194. package/scripts/eslint-rules/typedefs-first.js +67 -0
  195. package/scripts/prewarm-chromedriver.js +26 -0
  196. package/src/background-jobs/cron-expression.js +12 -13
  197. package/src/background-jobs/main.js +6 -6
  198. package/src/background-jobs/runner-graceful-shutdown.js +2 -3
  199. package/src/background-jobs/scheduler.js +3 -3
  200. package/src/background-jobs/worker.js +8 -9
  201. package/src/beacon/client.js +3 -4
  202. package/src/configuration-types.js +1 -0
  203. package/src/configuration.js +44 -3
  204. package/src/database/drivers/sqlite/web-persistence.js +2 -5
  205. package/src/database/pool/async-tracked-multi-connection.js +65 -15
  206. package/src/database/pool/base.js +23 -7
  207. package/src/database/query/index.js +21 -22
  208. package/src/database/query/model-class-query.js +5 -6
  209. package/src/database/record/attachments/normalize-input.js +9 -10
  210. package/src/database/record/attachments/store.js +2 -2
  211. package/src/database/record/auditing.js +7 -8
  212. package/src/database/record/index.js +4 -5
  213. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +1 -2
  214. package/src/frontend-model-controller.js +49 -56
  215. package/src/frontend-models/base.js +17 -19
  216. package/src/frontend-models/outgoing-event-buffer.js +3 -4
  217. package/src/frontend-models/query.js +40 -44
  218. package/src/frontend-models/resource-definition.js +7 -8
  219. package/src/frontend-models/transport-serialization.js +10 -12
  220. package/src/frontend-models/websocket-channel.js +12 -13
  221. package/src/http-server/client/websocket-session.js +7 -8
  222. package/src/http-server/client-delivery-queue.js +167 -0
  223. package/src/http-server/server-client.js +9 -0
  224. package/src/http-server/websocket-event-log-store.js +4 -6
  225. package/src/http-server/websocket-events-host.js +42 -12
  226. package/src/http-server/worker-handler/in-process.js +53 -14
  227. package/src/http-server/worker-handler/index.js +88 -18
  228. package/src/http-server/worker-handler/worker-thread.js +2 -2
  229. package/src/plugins/sqljs-wasm-route.js +3 -5
  230. package/src/sync/conflict-strategy.js +3 -6
  231. package/src/sync/device-identity.js +4 -9
  232. package/src/sync/local-mutation-log.js +9 -14
  233. package/src/sync/offline-grant.js +4 -7
  234. package/src/sync/peer-mutation-bundle.js +2 -4
  235. package/src/sync/server-change-feed.js +32 -34
  236. package/src/sync/sync-api-client.js +1 -2
  237. package/src/sync/sync-envelope-replay-service.js +17 -19
  238. package/src/sync/sync-resource-base.js +1 -2
  239. package/src/sync/sync-scope-store.js +9 -10
  240. package/src/testing/test-filter-parser.js +10 -11
  241. package/src/testing/test-runner.js +116 -108
  242. package/src/testing/test.js +8 -9
  243. package/src/utils/backtrace-cleaner.js +8 -9
  244. package/src/utils/model-scope.js +3 -4
  245. package/src/utils/ransack.js +27 -34
@@ -0,0 +1,167 @@
1
+ /**
2
+ * @typedef {object} DeliveryTask
3
+ * @property {number} byteLength - Retained complete-buffer bytes.
4
+ * @property {boolean} countedFrame - Whether this task is an outbound frame.
5
+ * @property {() => Promise<void>} delivery - Delivery operation.
6
+ * @property {(error?: Error) => void} settle - Settles the enqueue promise.
7
+ */
8
+ // @ts-check
9
+
10
+ export class ClientDeliveryQueueOverflowError extends Error {
11
+ /**
12
+ * Builds an outbound queue overflow error.
13
+ * @param {object} args - Overflow details.
14
+ * @param {number} args.clientCount - Client identifier.
15
+ * @param {number} args.maxBytes - Configured byte high-water mark.
16
+ * @param {number} args.maxFrames - Configured frame high-water mark.
17
+ * @param {number} args.pendingBytes - Bytes retained before rejecting the frame.
18
+ * @param {number} args.pendingFrames - Frames retained before rejecting the frame.
19
+ * @param {number} args.rejectedBytes - Rejected frame size.
20
+ */
21
+ constructor({clientCount, maxBytes, maxFrames, pendingBytes, pendingFrames, rejectedBytes}) {
22
+ super(`WebSocket client ${clientCount} exceeded its outbound queue limit (${pendingFrames}/${maxFrames} frames, ${pendingBytes}/${maxBytes} bytes; rejected ${rejectedBytes} bytes)`)
23
+ this.name = "ClientDeliveryQueueOverflowError"
24
+ }
25
+ }
26
+
27
+ export default class ClientDeliveryQueue {
28
+ /**
29
+ * Builds a per-client delivery queue.
30
+ * @param {object} args - Queue options.
31
+ * @param {number} args.clientCount - Client identifier.
32
+ * @param {number} args.maxBytes - Byte high-water mark.
33
+ * @param {number} args.maxFrames - Frame high-water mark.
34
+ * @param {(error: ClientDeliveryQueueOverflowError) => void} args.onOverflow - Overflow handler.
35
+ */
36
+ constructor({clientCount, maxBytes, maxFrames, onOverflow}) {
37
+ this.clientCount = clientCount
38
+ this.maxBytes = maxBytes
39
+ this.maxFrames = maxFrames
40
+ this.onOverflow = onOverflow
41
+ /** @type {DeliveryTask[]} */
42
+ this.tasks = []
43
+ /** @type {DeliveryTask | undefined} */
44
+ this.activeTask = undefined
45
+ this.pendingBytes = 0
46
+ this.pendingFrames = 0
47
+ this.destroyed = false
48
+ }
49
+
50
+ /**
51
+ * Enqueues one complete output buffer.
52
+ * @param {object} args - Delivery details.
53
+ * @param {number} args.byteLength - Exact buffer byte length.
54
+ * @param {() => Promise<void>} args.delivery - Delivery operation.
55
+ * @returns {Promise<void>} - Settles after delivery or teardown.
56
+ */
57
+ enqueueFrame({byteLength, delivery}) {
58
+ if (this.destroyed) return Promise.resolve()
59
+
60
+ if (this.pendingFrames + 1 > this.maxFrames || this.pendingBytes + byteLength > this.maxBytes) {
61
+ const error = new ClientDeliveryQueueOverflowError({
62
+ clientCount: this.clientCount,
63
+ maxBytes: this.maxBytes,
64
+ maxFrames: this.maxFrames,
65
+ pendingBytes: this.pendingBytes,
66
+ pendingFrames: this.pendingFrames,
67
+ rejectedBytes: byteLength
68
+ })
69
+
70
+ this.onOverflow(error)
71
+ return Promise.reject(error)
72
+ }
73
+
74
+ this.pendingFrames += 1
75
+ this.pendingBytes += byteLength
76
+ return this._enqueue({byteLength, countedFrame: true, delivery})
77
+ }
78
+
79
+ /**
80
+ * Enqueues an ordering-only operation that retains no complete output frame.
81
+ * @param {() => Promise<void>} delivery - Delivery operation.
82
+ * @returns {Promise<void>} - Settles after delivery or teardown.
83
+ */
84
+ enqueueControl(delivery) {
85
+ if (this.destroyed) return Promise.resolve()
86
+
87
+ return this._enqueue({byteLength: 0, countedFrame: false, delivery})
88
+ }
89
+
90
+ /**
91
+ * Releases queued and active accounting during explicit client teardown.
92
+ * @returns {void}
93
+ */
94
+ destroy() {
95
+ if (this.destroyed) return
96
+
97
+ this.destroyed = true
98
+ const tasks = this.activeTask ? [this.activeTask, ...this.tasks] : this.tasks
99
+
100
+ this.activeTask = undefined
101
+ this.tasks = []
102
+ this.pendingBytes = 0
103
+ this.pendingFrames = 0
104
+
105
+ for (const task of tasks) task.settle()
106
+ }
107
+
108
+ /**
109
+ * Gets current retained-buffer accounting.
110
+ * @returns {{pendingBytes: number, pendingFrames: number}} - Current retained-buffer accounting.
111
+ */
112
+ snapshot() {
113
+ return {pendingBytes: this.pendingBytes, pendingFrames: this.pendingFrames}
114
+ }
115
+
116
+ /**
117
+ * Enqueues a delivery task.
118
+ * @param {Omit<DeliveryTask, "settle">} task - Task to enqueue.
119
+ * @returns {Promise<void>} - Task completion.
120
+ */
121
+ _enqueue(task) {
122
+ const promise = new Promise((resolve, reject) => {
123
+ this.tasks.push({
124
+ ...task,
125
+ settle: (error) => error ? reject(error) : resolve(undefined)
126
+ })
127
+ })
128
+
129
+ this._drain()
130
+ return promise
131
+ }
132
+
133
+ /**
134
+ * Starts the next task when idle.
135
+ * @returns {void} - No return value.
136
+ */
137
+ _drain() {
138
+ if (this.destroyed || this.activeTask) return
139
+
140
+ const task = this.tasks.shift()
141
+ if (!task) return
142
+
143
+ this.activeTask = task
144
+ void task.delivery().then(
145
+ () => this._finish(task),
146
+ (error) => this._finish(task, error)
147
+ )
148
+ }
149
+
150
+ /**
151
+ * Finishes the active delivery task.
152
+ * @param {DeliveryTask} task - Completed task.
153
+ * @param {Error} [error] - Delivery error.
154
+ * @returns {void}
155
+ */
156
+ _finish(task, error) {
157
+ if (this.destroyed || this.activeTask !== task) return
158
+
159
+ this.activeTask = undefined
160
+ if (task.countedFrame) {
161
+ this.pendingBytes -= task.byteLength
162
+ this.pendingFrames -= 1
163
+ }
164
+ task.settle(error)
165
+ this._drain()
166
+ }
167
+ }
@@ -76,6 +76,15 @@ export default class ServerClient {
76
76
  })
77
77
  }
78
78
 
79
+ /**
80
+ * Immediately destroys the socket and all transport-owned write buffers.
81
+ * @param {Error} error - Destruction reason.
82
+ * @returns {void}
83
+ */
84
+ destroy(error) {
85
+ if (!this.socket.destroyed) this.socket.destroy(error)
86
+ }
87
+
79
88
  /**
80
89
  * On socket data.
81
90
  * @param {Buffer} chunk - Chunk.
@@ -4,11 +4,6 @@ import {randomUUID} from "crypto"
4
4
  import TableData from "../database/table-data/index.js"
5
5
  import Logger from "../logger.js"
6
6
 
7
- const EVENTS_TABLE = "websocket_channel_events"
8
- const REPLAY_CHANNELS_TABLE = "websocket_replay_channels"
9
- const DEFAULT_RETENTION_MS = 10 * 60 * 1000
10
- const stores = new WeakMap()
11
-
12
7
  /**
13
8
  * WebsocketEventRow type.
14
9
  * @typedef {object} WebsocketEventRow
@@ -18,12 +13,15 @@ const stores = new WeakMap()
18
13
  * @property {string} payload_json - Serialized payload.
19
14
  * @property {number | string} sequence - Sequence number.
20
15
  */
21
-
22
16
  /**
23
17
  * WebsocketReplayChannelRow type.
24
18
  * @typedef {object} WebsocketReplayChannelRow
25
19
  * @property {string} channel - Channel name.
26
20
  */
21
+ const EVENTS_TABLE = "websocket_channel_events"
22
+ const REPLAY_CHANNELS_TABLE = "websocket_replay_channels"
23
+ const DEFAULT_RETENTION_MS = 10 * 60 * 1000
24
+ const stores = new WeakMap()
27
25
 
28
26
  /**
29
27
  * Runs the websocketEventLogStoreForConfiguration helper.
@@ -8,6 +8,10 @@ export class VelociousHttpServerWebsocketEventsHost {
8
8
  * Narrows the runtime value to the documented type.
9
9
  * @type {Set<import("./worker-handler/index.js").default>} */
10
10
  this.handlers = new Set()
11
+ /**
12
+ * Broadcast handlers grouped by the configuration that owns them.
13
+ * @type {Map<import("../configuration.js").default, Set<import("./worker-handler/index.js").default>>} */
14
+ this.broadcastHandlersByConfiguration = new Map()
11
15
  this.publishQueue = Promise.resolve()
12
16
  }
13
17
 
@@ -30,8 +34,23 @@ export class VelociousHttpServerWebsocketEventsHost {
30
34
  */
31
35
  register(handler) {
32
36
  this.handlers.add(handler)
37
+ let configurationHandlers = this.broadcastHandlersByConfiguration.get(handler.configuration)
38
+
39
+ if (!configurationHandlers) {
40
+ configurationHandlers = new Set()
41
+ this.broadcastHandlersByConfiguration.set(handler.configuration, configurationHandlers)
42
+ }
43
+
44
+ configurationHandlers.add(handler)
45
+
46
+ return () => {
47
+ this.handlers.delete(handler)
48
+ configurationHandlers.delete(handler)
33
49
 
34
- return () => this.handlers.delete(handler)
50
+ if (configurationHandlers.size === 0) {
51
+ this.broadcastHandlersByConfiguration.delete(handler.configuration)
52
+ }
53
+ }
35
54
  }
36
55
 
37
56
  /**
@@ -69,17 +88,24 @@ export class VelociousHttpServerWebsocketEventsHost {
69
88
  * @param {string} args.channel - Channel name.
70
89
  * @param {Record<string, ?>} args.broadcastParams - Routing filter params.
71
90
  * @param {?} args.body - Message body.
91
+ * @param {import("../configuration.js").default} args.configuration - Originating configuration.
72
92
  * @returns {void}
73
93
  */
74
- broadcastV2({body, broadcastParams, channel}) {
94
+ broadcastV2({body, broadcastParams, channel, configuration}) {
75
95
  // Chain onto publishQueue so persistence completes before
76
96
  // the next broadcast — without this, a subscriber that connects
77
97
  // immediately after a broadcast could miss the just-persisted
78
98
  // event when replaying from lastEventId on a slow DB.
79
99
  this._queuePublish(async () => {
80
- const persistedEvent = await this._persistV2EventIfNeeded({body, channel})
100
+ const persistedEvent = await this._persistV2EventIfNeeded({body, channel, configuration})
101
+ const dispatchedTargets = new Set()
81
102
 
82
- for (const handler of this.handlers) {
103
+ for (const handler of this.broadcastHandlersByConfiguration.get(configuration) || []) {
104
+ const dispatchKey = handler.websocketV2BroadcastDispatchKey()
105
+
106
+ if (dispatchedTargets.has(dispatchKey)) continue
107
+
108
+ dispatchedTargets.add(dispatchKey)
83
109
  handler.dispatchWebsocketV2Broadcast({
84
110
  body,
85
111
  broadcastParams,
@@ -88,18 +114,19 @@ export class VelociousHttpServerWebsocketEventsHost {
88
114
  createdAt: persistedEvent?.createdAt
89
115
  })
90
116
  }
91
- }, "Failed to persist/broadcast V2 event")
117
+ }, "Failed to persist/broadcast V2 event", configuration)
92
118
  }
93
119
 
94
120
  /**
95
121
  * Runs queue publish.
96
122
  * @param {() => Promise<void>} callback - Publish work to run in order.
97
123
  * @param {string} errorMessage - Message logged when publish work fails.
124
+ * @param {import("../configuration.js").default} [originatingConfiguration] - Configuration whose context owns the work.
98
125
  * @returns {void}
99
126
  */
100
- _queuePublish(callback, errorMessage) {
127
+ _queuePublish(callback, errorMessage, originatingConfiguration) {
101
128
  const handler = this.handlers.values().next().value
102
- const configuration = handler?.configuration
129
+ const configuration = originatingConfiguration || handler?.configuration
103
130
 
104
131
  this.publishQueue = this.publishQueue
105
132
  .then(async () => {
@@ -120,10 +147,11 @@ export class VelociousHttpServerWebsocketEventsHost {
120
147
  * @param {object} args - Options.
121
148
  * @param {?} args.body - Event body.
122
149
  * @param {string} args.channel - Channel name.
150
+ * @param {import("../configuration.js").default} args.configuration - Originating configuration.
123
151
  * @returns {Promise<{createdAt: string, id: string} | null>} - Persisted event metadata when storage is enabled.
124
152
  */
125
- async _persistV2EventIfNeeded({body, channel}) {
126
- return await this._persistChannelEventIfNeeded({channel, payload: body})
153
+ async _persistV2EventIfNeeded({body, channel, configuration}) {
154
+ return await this._persistChannelEventIfNeeded({channel, payload: body, configuration})
127
155
  }
128
156
 
129
157
  /**
@@ -142,14 +170,16 @@ export class VelociousHttpServerWebsocketEventsHost {
142
170
  * @param {object} args - Options object.
143
171
  * @param {string} args.channel - Channel name.
144
172
  * @param {?} args.payload - Payload data.
173
+ * @param {import("../configuration.js").default} [args.configuration] - Configuration owning the event store.
145
174
  * @returns {Promise<{createdAt: string, id: string} | null>} - Persisted event metadata.
146
175
  */
147
- async _persistChannelEventIfNeeded({channel, payload}) {
176
+ async _persistChannelEventIfNeeded({channel, payload, configuration}) {
148
177
  const handler = this.handlers.values().next().value
178
+ const eventConfiguration = configuration || handler?.configuration
149
179
 
150
- if (!handler?.configuration) return null
180
+ if (!eventConfiguration) return null
151
181
 
152
- const websocketEventLogStore = websocketEventLogStoreForConfiguration(handler.configuration)
182
+ const websocketEventLogStore = websocketEventLogStoreForConfiguration(eventConfiguration)
153
183
  const shouldPersist = await websocketEventLogStore.shouldPersistChannel(channel)
154
184
 
155
185
  if (!shouldPersist) return null
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import Client from "../client/index.js"
4
+ import ClientDeliveryQueue from "../client-delivery-queue.js"
4
5
  import dispatchChannelSubscribers from "./channel-subscriber-dispatch.js"
5
6
  import Logger from "../../logger.js"
6
7
  import websocketEventsHost from "../websocket-events-host.js"
@@ -23,7 +24,7 @@ export default class VelociousHttpServerInProcessHandler {
23
24
 
24
25
  /**
25
26
  * Narrows the runtime value to the documented type.
26
- * @type {Record<number, {httpClient: Client, serverClient: import("../server-client.js").default}>} */
27
+ * @type {Record<number, {deliveryQueue: ClientDeliveryQueue, httpClient: Client, serverClient: import("../server-client.js").default}>} */
27
28
  this.clients = {}
28
29
 
29
30
  /** @type {Set<Promise<void>>} */
@@ -56,25 +57,36 @@ export default class VelociousHttpServerInProcessHandler {
56
57
  remoteAddress: serverClient.remoteAddress
57
58
  })
58
59
 
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
- }
60
+ const {maxBytes, maxFrames} = this.configuration.getWebsocketOutboundQueueLimits()
61
+ const deliveryQueue = new ClientDeliveryQueue({
62
+ clientCount,
63
+ maxBytes,
64
+ maxFrames,
65
+ onOverflow: (error) => {
66
+ deliveryQueue.destroy()
67
+ serverClient.destroy(error)
68
+ this._reportOutboundQueueOverflow({clientCount, error})
69
+ }
70
+ })
67
71
 
68
- httpClient.events.on("output", (output) => {
72
+ httpClient.events.on("output", (output, {websocketFrame = false} = {}) => {
69
73
  if (output !== null && output !== undefined) {
70
- void enqueueDelivery(() => serverClient.send(output)).catch((error) => {
74
+ const delivery = () => serverClient.send(output)
75
+ const queued = websocketFrame
76
+ ? deliveryQueue.enqueueFrame({
77
+ byteLength: typeof output === "string" ? Buffer.byteLength(output) : output.byteLength,
78
+ delivery
79
+ })
80
+ : deliveryQueue.enqueueControl(delivery)
81
+
82
+ void queued.catch((error) => {
71
83
  this.logger.error(() => ["Failed to deliver client output", {clientCount}, error])
72
84
  })
73
85
  }
74
86
  })
75
87
 
76
88
  httpClient.events.on("file", ({filePath, sendBody, settle}) => {
77
- void enqueueDelivery(async () => {
89
+ void deliveryQueue.enqueueControl(async () => {
78
90
  await settle(await serverClient.sendFile(filePath, sendBody))
79
91
  }).catch((error) => {
80
92
  this.logger.error(() => ["Failed to deliver file response", {clientCount, filePath}, error])
@@ -83,11 +95,12 @@ export default class VelociousHttpServerInProcessHandler {
83
95
  })
84
96
 
85
97
  httpClient.events.on("close", () => {
86
- void enqueueDelivery(() => serverClient.end())
98
+ void deliveryQueue.enqueueControl(() => serverClient.end())
87
99
  .finally(() => delete this.clients[clientCount])
88
100
  })
89
101
 
90
102
  serverClient.events.on("close", () => {
103
+ deliveryQueue.destroy()
91
104
  const cleanup = httpClient.abortPendingFileResponses()
92
105
  .catch((error) => {
93
106
  this.logger.warn("Failed to abort file responses after client close", error)
@@ -100,7 +113,7 @@ export default class VelociousHttpServerInProcessHandler {
100
113
  this.pendingClientCloseCleanups.add(cleanup)
101
114
  })
102
115
 
103
- this.clients[clientCount] = {httpClient, serverClient}
116
+ this.clients[clientCount] = {deliveryQueue, httpClient, serverClient}
104
117
 
105
118
  // Create a message-port shim so ServerClient.onSocketData can route data
106
119
  // to the in-process HTTP Client without needing a real worker thread.
@@ -118,6 +131,24 @@ export default class VelociousHttpServerInProcessHandler {
118
131
  serverClient.listen()
119
132
  }
120
133
 
134
+ /**
135
+ * Reports a per-client outbound queue overflow.
136
+ * @param {object} args - Overflow details.
137
+ * @param {number} args.clientCount - Affected client.
138
+ * @param {Error} args.error - Overflow error.
139
+ * @returns {void}
140
+ */
141
+ _reportOutboundQueueOverflow({clientCount, error}) {
142
+ const errorPayload = {
143
+ context: {clientCount, websocketOutboundQueueOverflow: true, workerCount: this.workerCount},
144
+ error
145
+ }
146
+ const errorEvents = this.configuration.getErrorEvents()
147
+
148
+ errorEvents.emit("framework-error", errorPayload)
149
+ errorEvents.emit("all-error", {...errorPayload, errorType: "framework-error"})
150
+ }
151
+
121
152
  /**
122
153
  * Runs stop.
123
154
  * @returns {Promise<void>} */
@@ -158,6 +189,14 @@ export default class VelociousHttpServerInProcessHandler {
158
189
  return this.configuration._broadcastToChannelLocal(channel, broadcastParams, body, {eventId})
159
190
  }
160
191
 
192
+ /**
193
+ * Gets the configuration-wide V2 broadcast target shared by in-process handlers.
194
+ * @returns {import("../../configuration.js").default} - Shared configuration target.
195
+ */
196
+ websocketV2BroadcastDispatchKey() {
197
+ return this.configuration
198
+ }
199
+
161
200
  /**
162
201
  * Runs dispatch websocket event.
163
202
  * @param {object} args - Options object.
@@ -2,6 +2,7 @@
2
2
 
3
3
  import {ensureError} from "typanic"
4
4
  import Logger from "../../logger.js"
5
+ import ClientDeliveryQueue from "../client-delivery-queue.js"
5
6
  import {Worker} from "worker_threads"
6
7
  import websocketEventsHost from "../websocket-events-host.js"
7
8
 
@@ -59,7 +60,7 @@ export default class VelociousHttpServerWorker {
59
60
  * @type {Map<number, {resolve: (snapshot: Record<string, ?>) => void}>} */
60
61
  this._debugSnapshotRequests = new Map()
61
62
 
62
- /** @type {Map<number, Promise<void>>} */
63
+ /** @type {Map<number, ClientDeliveryQueue>} */
63
64
  this._clientDeliveryQueues = new Map()
64
65
  }
65
66
 
@@ -117,6 +118,7 @@ export default class VelociousHttpServerWorker {
117
118
  if (!this.clients[clientCount] && !this._clientDeliveryQueues.has(clientCount)) return
118
119
 
119
120
  delete this.clients[clientCount]
121
+ this._clientDeliveryQueues.get(clientCount)?.destroy()
120
122
  this._clientDeliveryQueues.delete(clientCount)
121
123
  this.worker?.postMessage({command: "clientAbort", clientCount})
122
124
  }
@@ -163,6 +165,10 @@ export default class VelociousHttpServerWorker {
163
165
  _closeAllClients() {
164
166
  const clients = Object.values(this.clients)
165
167
  this.clients = {}
168
+ const deliveryQueues = this._clientDeliveryQueues
169
+ this._clientDeliveryQueues = new Map()
170
+
171
+ for (const queue of deliveryQueues.values()) queue.destroy()
166
172
 
167
173
  for (const client of clients) {
168
174
  try {
@@ -182,6 +188,7 @@ export default class VelociousHttpServerWorker {
182
188
  * @param {string} [data.filePath] - File path.
183
189
  * @param {boolean} [data.sendBody] - Whether to send the file body.
184
190
  * @param {number} [data.transferId] - File transfer id.
191
+ * @param {boolean} [data.websocketFrame] - Whether output is a completed WebSocket frame.
185
192
  * @param {string} [data.channel] - Channel name.
186
193
  * @param {number} [data.requestId] - Debug request id.
187
194
  * @param {Record<string, ?>} [data.snapshot] - Worker debug snapshot.
@@ -219,7 +226,11 @@ export default class VelociousHttpServerWorker {
219
226
  if (output !== null && output !== undefined) {
220
227
  const outputLength = typeof output === "string" ? output.length : output.byteLength
221
228
 
222
- void this.enqueueClientDelivery(client.clientCount, () => client.send(output)).then(() => {
229
+ const delivery = data.websocketFrame === true
230
+ ? this.enqueueClientFrame(client, output)
231
+ : this.enqueueClientControl(client, () => client.send(output))
232
+
233
+ void delivery.then(() => {
223
234
  this.logger.debug(() => ["Client output delivered", {
224
235
  clientCount,
225
236
  outputLength,
@@ -243,7 +254,7 @@ export default class VelociousHttpServerWorker {
243
254
  return
244
255
  }
245
256
 
246
- void this.enqueueClientDelivery(client.clientCount, async () => {
257
+ void this.enqueueClientControl(client, async () => {
247
258
  const result = await client.sendFile(filePath, sendBody !== false)
248
259
 
249
260
  this.worker?.postMessage({command: "clientFileResult", result, transferId})
@@ -260,7 +271,7 @@ export default class VelociousHttpServerWorker {
260
271
  return
261
272
  }
262
273
 
263
- void this.enqueueClientDelivery(client.clientCount, () => client.end())
274
+ void this.enqueueClientControl(client, () => client.end())
264
275
  .finally(() => delete this.clients[client.clientCount])
265
276
  } else if (command == "debugSnapshot") {
266
277
  const {requestId, snapshot} = data
@@ -289,7 +300,12 @@ export default class VelociousHttpServerWorker {
289
300
  throw new Error("Worker websocket v2-broadcast channel must be a string")
290
301
  }
291
302
 
292
- websocketEventsHost.broadcastV2({body, broadcastParams: broadcastParams || {}, channel})
303
+ websocketEventsHost.broadcastV2({
304
+ body,
305
+ broadcastParams: broadcastParams || {},
306
+ channel,
307
+ configuration: this.configuration
308
+ })
293
309
  } else {
294
310
  throw new Error(`Unknown command: ${command}`)
295
311
  }
@@ -297,24 +313,70 @@ export default class VelociousHttpServerWorker {
297
313
 
298
314
  /**
299
315
  * Preserves socket output ordering for one client.
300
- * @param {number} clientCount - Client count.
316
+ * @param {import("../server-client.js").default} client - Client instance.
317
+ * @param {string | Uint8Array} output - Complete output buffer.
318
+ * @returns {Promise<void>} - Queued delivery.
319
+ */
320
+ enqueueClientFrame(client, output) {
321
+ const byteLength = typeof output === "string" ? Buffer.byteLength(output) : output.byteLength
322
+
323
+ return this._deliveryQueueFor(client).enqueueFrame({
324
+ byteLength,
325
+ delivery: () => client.send(output)
326
+ })
327
+ }
328
+
329
+ /**
330
+ * Preserves ordering for a delivery that retains no complete output frame.
331
+ * @param {import("../server-client.js").default} client - Client instance.
301
332
  * @param {() => Promise<void>} delivery - Delivery operation.
302
333
  * @returns {Promise<void>} - Queued delivery.
303
334
  */
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
- }
335
+ enqueueClientControl(client, delivery) {
336
+ return this._deliveryQueueFor(client).enqueueControl(delivery)
337
+ }
314
338
 
315
- void queued.then(clearQueue, clearQueue)
339
+ /**
340
+ * Gets or creates one client's delivery queue.
341
+ * @param {import("../server-client.js").default} client - Client instance.
342
+ * @returns {ClientDeliveryQueue} - Client-owned delivery queue.
343
+ */
344
+ _deliveryQueueFor(client) {
345
+ const existing = this._clientDeliveryQueues.get(client.clientCount)
346
+ if (existing) return existing
347
+
348
+ const {maxBytes, maxFrames} = this.configuration.getWebsocketOutboundQueueLimits()
349
+ const queue = new ClientDeliveryQueue({
350
+ clientCount: client.clientCount,
351
+ maxBytes,
352
+ maxFrames,
353
+ onOverflow: (error) => {
354
+ queue.destroy()
355
+ client.destroy(error)
356
+ this._reportOutboundQueueOverflow({clientCount: client.clientCount, error})
357
+ }
358
+ })
359
+
360
+ this._clientDeliveryQueues.set(client.clientCount, queue)
361
+ return queue
362
+ }
363
+
364
+ /**
365
+ * Reports a per-client outbound queue overflow.
366
+ * @param {object} args - Overflow details.
367
+ * @param {number} args.clientCount - Affected client.
368
+ * @param {Error} args.error - Overflow error.
369
+ * @returns {void}
370
+ */
371
+ _reportOutboundQueueOverflow({clientCount, error}) {
372
+ const errorPayload = {
373
+ context: {clientCount, websocketOutboundQueueOverflow: true, workerCount: this.workerCount},
374
+ error
375
+ }
376
+ const errorEvents = this.configuration.getErrorEvents()
316
377
 
317
- return queued
378
+ errorEvents.emit("framework-error", errorPayload)
379
+ errorEvents.emit("all-error", {...errorPayload, errorType: "framework-error"})
318
380
  }
319
381
 
320
382
  /**
@@ -410,6 +472,14 @@ export default class VelociousHttpServerWorker {
410
472
  this.worker.postMessage({body, broadcastParams, channel, command: "websocketV2Broadcast", eventId, createdAt})
411
473
  }
412
474
 
475
+ /**
476
+ * Gets this worker's isolated V2 broadcast target.
477
+ * @returns {VelociousHttpServerWorker} - This worker handler.
478
+ */
479
+ websocketV2BroadcastDispatchKey() {
480
+ return this
481
+ }
482
+
413
483
  /**
414
484
  * Runs register with events host.
415
485
  * @returns {void} */
@@ -154,8 +154,8 @@ export default class VelociousHttpServerWorkerHandlerWorkerThread {
154
154
  remoteAddress
155
155
  })
156
156
 
157
- client.events.on("output", (output) => {
158
- this.parentPort.postMessage({command: "clientOutput", clientCount, output})
157
+ client.events.on("output", (output, {websocketFrame = false} = {}) => {
158
+ this.parentPort.postMessage({command: "clientOutput", clientCount, output, websocketFrame})
159
159
  })
160
160
 
161
161
  client.events.on("file", ({filePath, sendBody, settle}) => {
@@ -2,23 +2,21 @@ import {createRequire} from "node:module"
2
2
  import path from "node:path"
3
3
  import SqlJsWasmRouteController from "./sqljs-wasm-route-controller.js"
4
4
 
5
- const require = createRequire(import.meta.url)
6
- const sqlJsEntryPath = require.resolve("sql.js")
7
- const sqlJsDistDirectory = path.dirname(sqlJsEntryPath)
8
-
9
5
  /**
10
6
  * InstallSqlJsWasmRouteArgs type.
11
7
  * @typedef {object} InstallSqlJsWasmRouteArgs
12
8
  * @property {import("../configuration.js").default} configuration - Velocious configuration instance.
13
9
  * @property {string} [routePrefix] - Route prefix used for sql.js asset serving.
14
10
  */
15
-
16
11
  /**
17
12
  * SqlJsLocateFileFromBackendArgs type.
18
13
  * @typedef {object} SqlJsLocateFileFromBackendArgs
19
14
  * @property {string} backendBaseUrl - Backend base URL (for example `https://api.example.com`).
20
15
  * @property {string} [routePrefix] - Route prefix used for sql.js asset serving.
21
16
  */
17
+ const require = createRequire(import.meta.url)
18
+ const sqlJsEntryPath = require.resolve("sql.js")
19
+ const sqlJsDistDirectory = path.dirname(sqlJsEntryPath)
22
20
 
23
21
  /**
24
22
  * Runs normalize route prefix.