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.
- package/README.md +4 -3
- package/build/background-jobs/cron-expression.js +9 -9
- package/build/background-jobs/forked-runner-child.js +1 -1
- package/build/background-jobs/job-runner.js +21 -15
- package/build/background-jobs/json-socket.js +4 -0
- package/build/background-jobs/main.js +180 -18
- package/build/background-jobs/status-reporter.js +12 -5
- package/build/background-jobs/store.js +132 -50
- package/build/background-jobs/types.js +11 -3
- package/build/background-jobs/worker.js +9 -5
- package/build/beacon/client.js +13 -6
- package/build/configuration.js +54 -29
- package/build/controller.js +9 -3
- package/build/database/drivers/sqlite/base.js +1 -1
- package/build/database/drivers/sqlite/index.js +15 -14
- package/build/database/migrations-ledger.js +16 -16
- package/build/database/pool/async-tracked-multi-connection.js +16 -0
- package/build/database/pool/base.js +26 -0
- package/build/database/query/where-model-class-hash.js +2 -0
- package/build/database/query/with-count.js +1 -1
- package/build/database/record/acts-as-list.js +2 -2
- package/build/database/record/auditing.js +23 -6
- package/build/database/record/index.js +7 -7
- package/build/database/record/validators/uniqueness.js +3 -3
- package/build/database/tenants/data-copier.js +23 -23
- package/build/database/tenants/schema-cloner.js +27 -27
- package/build/environment-handlers/browser.js +0 -1
- package/build/environment-handlers/node/cli/commands/background-jobs-runner.js +2 -1
- package/build/environment-handlers/node/cli/commands/lint/relationships.js +2 -2
- package/build/environment-handlers/node.js +1 -1
- package/build/frontend-model-controller.js +26 -18
- package/build/frontend-model-resource/base-resource.js +4 -4
- package/build/frontend-models/base.js +5 -5
- package/build/frontend-models/query.js +12 -12
- package/build/http-server/client/index.js +61 -15
- package/build/http-server/client/request-runner.js +7 -1
- package/build/http-server/client/response.js +17 -1
- package/build/http-server/client/websocket-session.js +13 -13
- package/build/http-server/server-client.js +96 -0
- package/build/http-server/websocket-channel.js +12 -11
- package/build/http-server/websocket-connection.js +12 -11
- package/build/http-server/websocket-events-host.js +1 -1
- package/build/http-server/worker-handler/in-process.js +48 -9
- package/build/http-server/worker-handler/index.js +67 -7
- package/build/http-server/worker-handler/worker-thread.js +68 -0
- package/build/src/background-jobs/cron-expression.d.ts +2 -2
- package/build/src/background-jobs/cron-expression.js +10 -10
- package/build/src/background-jobs/forked-runner-child.js +2 -2
- package/build/src/background-jobs/job-runner.d.ts +5 -1
- package/build/src/background-jobs/job-runner.d.ts.map +1 -1
- package/build/src/background-jobs/job-runner.js +22 -16
- package/build/src/background-jobs/json-socket.d.ts +4 -0
- package/build/src/background-jobs/json-socket.d.ts.map +1 -1
- package/build/src/background-jobs/json-socket.js +5 -1
- package/build/src/background-jobs/main.d.ts +62 -10
- package/build/src/background-jobs/main.d.ts.map +1 -1
- package/build/src/background-jobs/main.js +171 -19
- package/build/src/background-jobs/status-reporter.d.ts +6 -2
- package/build/src/background-jobs/status-reporter.d.ts.map +1 -1
- package/build/src/background-jobs/status-reporter.js +13 -6
- package/build/src/background-jobs/store.d.ts +48 -25
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +123 -50
- package/build/src/background-jobs/types.d.ts +36 -3
- package/build/src/background-jobs/types.d.ts.map +1 -1
- package/build/src/background-jobs/types.js +12 -4
- package/build/src/background-jobs/worker.d.ts +7 -5
- package/build/src/background-jobs/worker.d.ts.map +1 -1
- package/build/src/background-jobs/worker.js +10 -6
- package/build/src/beacon/client.d.ts +6 -3
- package/build/src/beacon/client.d.ts.map +1 -1
- package/build/src/beacon/client.js +14 -7
- package/build/src/configuration-types.d.ts +1 -1
- package/build/src/configuration.d.ts +42 -29
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +52 -30
- package/build/src/controller.d.ts +3 -0
- package/build/src/controller.d.ts.map +1 -1
- package/build/src/controller.js +9 -4
- package/build/src/database/drivers/sqlite/base.js +2 -2
- package/build/src/database/drivers/sqlite/index.d.ts +7 -6
- package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.js +16 -15
- package/build/src/database/migrations-ledger.d.ts +16 -16
- package/build/src/database/migrations-ledger.js +17 -17
- package/build/src/database/pool/async-tracked-multi-connection.d.ts +0 -11
- package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
- package/build/src/database/pool/async-tracked-multi-connection.js +16 -1
- package/build/src/database/pool/base.d.ts +21 -0
- package/build/src/database/pool/base.d.ts.map +1 -1
- package/build/src/database/pool/base.js +24 -1
- package/build/src/database/query/where-model-class-hash.d.ts.map +1 -1
- package/build/src/database/query/where-model-class-hash.js +3 -1
- package/build/src/database/query/with-count.js +2 -2
- package/build/src/database/record/acts-as-list.js +3 -3
- package/build/src/database/record/auditing.d.ts +1 -1
- package/build/src/database/record/auditing.d.ts.map +1 -1
- package/build/src/database/record/auditing.js +24 -7
- package/build/src/database/record/index.d.ts +7 -7
- package/build/src/database/record/index.js +8 -8
- package/build/src/database/record/validators/uniqueness.d.ts +3 -3
- package/build/src/database/record/validators/uniqueness.js +4 -4
- package/build/src/database/tenants/data-copier.d.ts +18 -18
- package/build/src/database/tenants/data-copier.js +24 -24
- package/build/src/database/tenants/schema-cloner.d.ts +27 -27
- package/build/src/database/tenants/schema-cloner.js +28 -28
- package/build/src/environment-handlers/browser.d.ts +9 -0
- package/build/src/environment-handlers/browser.d.ts.map +1 -1
- package/build/src/environment-handlers/browser.js +1 -2
- package/build/src/environment-handlers/node/cli/commands/background-jobs-runner.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/background-jobs-runner.js +3 -2
- package/build/src/environment-handlers/node/cli/commands/lint/relationships.js +3 -3
- package/build/src/environment-handlers/node.js +2 -2
- package/build/src/frontend-model-controller.d.ts +9 -8
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +26 -19
- package/build/src/frontend-model-resource/base-resource.js +5 -5
- package/build/src/frontend-models/base.d.ts +4 -4
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +6 -6
- package/build/src/frontend-models/query.d.ts +7 -7
- package/build/src/frontend-models/query.js +13 -13
- package/build/src/http-server/client/index.d.ts +23 -1
- package/build/src/http-server/client/index.d.ts.map +1 -1
- package/build/src/http-server/client/index.js +55 -17
- package/build/src/http-server/client/request-runner.d.ts.map +1 -1
- package/build/src/http-server/client/request-runner.js +8 -2
- package/build/src/http-server/client/response.d.ts +11 -1
- package/build/src/http-server/client/response.d.ts.map +1 -1
- package/build/src/http-server/client/response.js +16 -2
- package/build/src/http-server/client/websocket-session.d.ts +13 -13
- package/build/src/http-server/client/websocket-session.js +14 -14
- package/build/src/http-server/server-client.d.ts +13 -0
- package/build/src/http-server/server-client.d.ts.map +1 -1
- package/build/src/http-server/server-client.js +94 -1
- package/build/src/http-server/websocket-channel.d.ts +12 -11
- package/build/src/http-server/websocket-channel.d.ts.map +1 -1
- package/build/src/http-server/websocket-channel.js +13 -12
- package/build/src/http-server/websocket-connection.d.ts +12 -11
- package/build/src/http-server/websocket-connection.d.ts.map +1 -1
- package/build/src/http-server/websocket-connection.js +13 -12
- package/build/src/http-server/websocket-events-host.d.ts +1 -1
- package/build/src/http-server/websocket-events-host.js +2 -2
- package/build/src/http-server/worker-handler/in-process.d.ts +2 -0
- package/build/src/http-server/worker-handler/in-process.d.ts.map +1 -1
- package/build/src/http-server/worker-handler/in-process.js +42 -11
- package/build/src/http-server/worker-handler/index.d.ts +21 -0
- package/build/src/http-server/worker-handler/index.d.ts.map +1 -1
- package/build/src/http-server/worker-handler/index.js +61 -7
- package/build/src/http-server/worker-handler/worker-thread.d.ts +30 -0
- package/build/src/http-server/worker-handler/worker-thread.d.ts.map +1 -1
- package/build/src/http-server/worker-handler/worker-thread.js +61 -1
- package/build/src/sync/device-identity.d.ts +2 -2
- package/build/src/sync/device-identity.d.ts.map +1 -1
- package/build/src/sync/device-identity.js +4 -4
- package/build/src/sync/local-mutation-log.d.ts +0 -1
- package/build/src/sync/local-mutation-log.d.ts.map +1 -1
- package/build/src/sync/local-mutation-log.js +1 -2
- package/build/src/sync/query-scope.d.ts +5 -0
- package/build/src/sync/query-scope.d.ts.map +1 -1
- package/build/src/sync/query-scope.js +7 -2
- package/build/src/sync/sync-client-types.d.ts +2 -2
- package/build/src/sync/sync-client-types.d.ts.map +1 -1
- package/build/src/sync/sync-client-types.js +1 -1
- package/build/src/sync/sync-client.d.ts +17 -2
- package/build/src/sync/sync-client.d.ts.map +1 -1
- package/build/src/sync/sync-client.js +28 -14
- package/build/src/sync/sync-realtime-bridge.d.ts +6 -2
- package/build/src/sync/sync-realtime-bridge.d.ts.map +1 -1
- package/build/src/sync/sync-realtime-bridge.js +14 -3
- package/build/src/sync/sync-resource-base.d.ts +14 -3
- package/build/src/sync/sync-resource-base.d.ts.map +1 -1
- package/build/src/sync/sync-resource-base.js +20 -4
- package/build/src/sync/sync-scope-store.d.ts +3 -3
- package/build/src/sync/sync-scope-store.d.ts.map +1 -1
- package/build/src/sync/sync-scope-store.js +7 -5
- package/build/src/sync/sync-websocket-channel.d.ts +10 -0
- package/build/src/sync/sync-websocket-channel.d.ts.map +1 -1
- package/build/src/sync/sync-websocket-channel.js +20 -3
- package/build/src/tenants/tenant-iterator.d.ts +8 -8
- package/build/src/tenants/tenant-iterator.js +9 -9
- package/build/src/tenants/tenant.d.ts +6 -6
- package/build/src/tenants/tenant.js +7 -7
- package/build/src/testing/test-runner.d.ts +16 -0
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +79 -19
- package/build/sync/device-identity.js +3 -3
- package/build/sync/local-mutation-log.js +0 -1
- package/build/sync/query-scope.js +6 -1
- package/build/sync/sync-client-types.js +1 -1
- package/build/sync/sync-client.js +28 -15
- package/build/sync/sync-realtime-bridge.js +13 -2
- package/build/sync/sync-resource-base.js +20 -3
- package/build/sync/sync-scope-store.js +6 -4
- package/build/sync/sync-websocket-channel.js +18 -2
- package/build/tenants/tenant-iterator.js +8 -8
- package/build/tenants/tenant.js +6 -6
- package/build/testing/test-runner.js +86 -20
- package/package.json +2 -2
- package/src/background-jobs/cron-expression.js +9 -9
- package/src/background-jobs/forked-runner-child.js +1 -1
- package/src/background-jobs/job-runner.js +21 -15
- package/src/background-jobs/json-socket.js +4 -0
- package/src/background-jobs/main.js +180 -18
- package/src/background-jobs/status-reporter.js +12 -5
- package/src/background-jobs/store.js +132 -50
- package/src/background-jobs/types.js +11 -3
- package/src/background-jobs/worker.js +9 -5
- package/src/beacon/client.js +13 -6
- package/src/configuration.js +54 -29
- package/src/controller.js +9 -3
- package/src/database/drivers/sqlite/base.js +1 -1
- package/src/database/drivers/sqlite/index.js +15 -14
- package/src/database/migrations-ledger.js +16 -16
- package/src/database/pool/async-tracked-multi-connection.js +16 -0
- package/src/database/pool/base.js +26 -0
- package/src/database/query/where-model-class-hash.js +2 -0
- package/src/database/query/with-count.js +1 -1
- package/src/database/record/acts-as-list.js +2 -2
- package/src/database/record/auditing.js +23 -6
- package/src/database/record/index.js +7 -7
- package/src/database/record/validators/uniqueness.js +3 -3
- package/src/database/tenants/data-copier.js +23 -23
- package/src/database/tenants/schema-cloner.js +27 -27
- package/src/environment-handlers/browser.js +0 -1
- package/src/environment-handlers/node/cli/commands/background-jobs-runner.js +2 -1
- package/src/environment-handlers/node/cli/commands/lint/relationships.js +2 -2
- package/src/environment-handlers/node.js +1 -1
- package/src/frontend-model-controller.js +26 -18
- package/src/frontend-model-resource/base-resource.js +4 -4
- package/src/frontend-models/base.js +5 -5
- package/src/frontend-models/query.js +12 -12
- package/src/http-server/client/index.js +61 -15
- package/src/http-server/client/request-runner.js +7 -1
- package/src/http-server/client/response.js +17 -1
- package/src/http-server/client/websocket-session.js +13 -13
- package/src/http-server/server-client.js +96 -0
- package/src/http-server/websocket-channel.js +12 -11
- package/src/http-server/websocket-connection.js +12 -11
- package/src/http-server/websocket-events-host.js +1 -1
- package/src/http-server/worker-handler/in-process.js +48 -9
- package/src/http-server/worker-handler/index.js +67 -7
- package/src/http-server/worker-handler/worker-thread.js +68 -0
- package/src/sync/device-identity.js +3 -3
- package/src/sync/local-mutation-log.js +0 -1
- package/src/sync/query-scope.js +6 -1
- package/src/sync/sync-client-types.js +1 -1
- package/src/sync/sync-client.js +28 -15
- package/src/sync/sync-realtime-bridge.js +13 -2
- package/src/sync/sync-resource-base.js +20 -3
- package/src/sync/sync-scope-store.js +6 -4
- package/src/sync/sync-websocket-channel.js +18 -2
- package/src/tenants/tenant-iterator.js +8 -8
- package/src/tenants/tenant.js +6 -6
- 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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 {
|
|
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 {
|
|
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.
|
package/src/sync/query-scope.js
CHANGED
|
@@ -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
|
/**
|