velocious 1.0.626 → 1.0.627
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 +14 -2
- package/build/database/pool/async-tracked-multi-connection.js +46 -9
- package/build/database/pool/base.js +3 -0
- package/build/database/pool/checkout-timeout-error.js +16 -0
- package/build/http-client/websocket-client.js +52 -0
- package/build/http-server/client/index.js +6 -0
- package/build/http-server/client/websocket-session.js +30 -0
- package/build/http-server/index.js +202 -38
- package/build/http-server/worker-handler/index.js +15 -1
- package/build/http-server/worker-handler/worker-thread.js +8 -0
- package/build/src/database/pool/async-tracked-multi-connection.d.ts +22 -2
- package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
- package/build/src/database/pool/async-tracked-multi-connection.js +44 -10
- package/build/src/database/pool/base.d.ts +15 -0
- package/build/src/database/pool/base.d.ts.map +1 -1
- package/build/src/database/pool/base.js +4 -1
- package/build/src/database/pool/checkout-timeout-error.d.ts +13 -0
- package/build/src/database/pool/checkout-timeout-error.d.ts.map +1 -0
- package/build/src/database/pool/checkout-timeout-error.js +15 -0
- package/build/src/http-client/websocket-client.d.ts +18 -0
- package/build/src/http-client/websocket-client.d.ts.map +1 -1
- package/build/src/http-client/websocket-client.js +48 -1
- package/build/src/http-server/client/index.d.ts.map +1 -1
- package/build/src/http-server/client/index.js +7 -1
- package/build/src/http-server/client/websocket-session.d.ts +6 -0
- package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
- package/build/src/http-server/client/websocket-session.js +27 -1
- package/build/src/http-server/index.d.ts +82 -10
- package/build/src/http-server/index.d.ts.map +1 -1
- package/build/src/http-server/index.js +186 -33
- package/build/src/http-server/worker-handler/index.d.ts +28 -1
- package/build/src/http-server/worker-handler/index.d.ts.map +1 -1
- package/build/src/http-server/worker-handler/index.js +23 -2
- package/build/src/http-server/worker-handler/worker-thread.d.ts.map +1 -1
- package/build/src/http-server/worker-handler/worker-thread.js +7 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/database/pool/async-tracked-multi-connection.js +46 -9
- package/src/database/pool/base.js +3 -0
- package/src/database/pool/checkout-timeout-error.js +16 -0
- package/src/http-client/websocket-client.js +52 -0
- package/src/http-server/client/index.js +6 -0
- package/src/http-server/client/websocket-session.js +30 -0
- package/src/http-server/index.js +202 -38
- package/src/http-server/worker-handler/index.js +15 -1
- package/src/http-server/worker-handler/worker-thread.js +8 -0
package/README.md
CHANGED
|
@@ -1827,7 +1827,7 @@ database: {
|
|
|
1827
1827
|
}
|
|
1828
1828
|
```
|
|
1829
1829
|
|
|
1830
|
-
`pool.max` caps live async-tracked connections for that pool and defaults to `10` when omitted. When the cap is reached, new checkouts wait until a matching checked-in connection can be handed over or capacity is freed. Set `pool.max` to `null` only when a process is deliberately allowed to open an unbounded number of database connections. The built-in debug endpoint reports each in-use connection's `checkedOutForMs`, each idle connection's `idleForMs`,
|
|
1830
|
+
`pool.max` caps live async-tracked connections for that pool and defaults to `10` when omitted. When the cap is reached, new checkouts wait until a matching checked-in connection can be handed over or capacity is freed. Set `pool.max` to `null` only when a process is deliberately allowed to open an unbounded number of database connections. The built-in debug endpoint reports each in-use connection's `checkedOutForMs`, each idle connection's `idleForMs`, queued `pendingCheckouts[].waitingForMs`, and matching idle capacity plus checkout-drain state so production diagnostics can distinguish long-held checkouts, pool-capacity waits, and an invariant violation where compatible capacity is unexpectedly idle.
|
|
1831
1831
|
|
|
1832
1832
|
Debug snapshots also expose cumulative connection-creation, checkout-wait and
|
|
1833
1833
|
timeout, idle-reap, and peak-live-connection telemetry. Opt-in test profiles can
|
|
@@ -2028,7 +2028,7 @@ await client.close()
|
|
|
2028
2028
|
|
|
2029
2029
|
For long-lived Node clients, the constructor also accepts opt-in liveness options (all default off, so browser/Expo usage is unchanged): `webSocketImplementation` (inject Node's `ws`, since the global/undici WebSocket exposes neither protocol ping nor an unref-able socket), `heartbeatIntervalMs` (a ping heartbeat that drops a socket whose peer stops ponging, so a client notices a vanished server), and `unref` (unref the underlying socket so an idle connection can't keep the process alive on its own). See [docs/websocket-channels.md](docs/websocket-channels.md).
|
|
2030
2030
|
|
|
2031
|
-
`await client.close()` is a final graceful shutdown that releases resumable server-session state; unexpected transport drops first attempt to resume that state. A successful resume retains the existing server-side connection and channel instances. If the server instead rejects the old session with `session-gone`, SnapReq promotes the already-established fresh session, reopens still-live one-to-one connection handles, and re-subscribes still-live channel handles. Those public handles remain usable and channel readiness resolves on the fresh session; explicitly closed handles stay closed. See [the WebSocket channel lifecycle guarantees](docs/websocket-channels.md#lifecycle-guarantees-phase-1b).
|
|
2031
|
+
`await client.close()` is a final graceful shutdown that releases resumable server-session state; unexpected transport drops first attempt to resume that state. On a multi-worker server, the client automatically puts the prior session identity in the reconnect upgrade URL so the host can route it to its owning worker; routing is session-based and never source-IP-based. A successful resume retains the existing server-side connection and channel instances. If the server instead rejects the old session with `session-gone`, SnapReq promotes the already-established fresh session, reopens still-live one-to-one connection handles, and re-subscribes still-live channel handles. Those public handles remain usable and channel readiness resolves on the fresh session; explicitly closed handles stay closed. See [the WebSocket channel lifecycle guarantees](docs/websocket-channels.md#lifecycle-guarantees-phase-1b).
|
|
2032
2032
|
|
|
2033
2033
|
## Subscribe to events
|
|
2034
2034
|
|
|
@@ -2701,6 +2701,18 @@ It exposes `GET /api/stats`, `/api/jobs`, `/api/jobs/:id`, `/api/schedule` and `
|
|
|
2701
2701
|
npx velocious server --host 0.0.0.0 --port 8082
|
|
2702
2702
|
```
|
|
2703
2703
|
|
|
2704
|
+
Threaded servers default to `os.availableParallelism()` HTTP workers. Pass
|
|
2705
|
+
`--workers` or configure `httpServer.workers` to override that count. Ordinary
|
|
2706
|
+
connections are distributed round-robin even behind a loopback reverse proxy,
|
|
2707
|
+
while resumable WebSockets return to their session's worker. Each worker owns a
|
|
2708
|
+
separate configuration and database pools, so per-worker limits multiply across
|
|
2709
|
+
the effective worker count (for example, four workers with `pool.max: 10` can
|
|
2710
|
+
open 40 connections for that pool). The debug snapshot exposes the configured
|
|
2711
|
+
and effective counts; default in-process mode uses one effective handler.
|
|
2712
|
+
Only requests carrying a resumable-session query wait for upgrade headers before
|
|
2713
|
+
worker assignment; ordinary and malformed requests continue directly to the
|
|
2714
|
+
request parser.
|
|
2715
|
+
|
|
2704
2716
|
When the server runs in the `development` environment, Velocious watches application `src/` trees and hot-reloads by recycling HTTP workers after `.js`/`.mjs`/`.cjs`/`.json`/`.ejs` changes. That picks up edited controllers, models, resources, routes, and views without a manual server restart while keeping production/test behavior unchanged.
|
|
2705
2717
|
|
|
2706
2718
|
Starting the HTTP server creates `tmp/server.lock` under the configured application directory before Beacon, workers, or the TCP listener start. A second server for the same app fails fast with the lock owner details instead of partially starting. Normal shutdown removes the lock; stale locks with a dead local PID are reclaimed automatically, while locks from another host or unreadable metadata should be removed manually only after confirming no server is running. See [docs/http-server.md](docs/http-server.md#server-lock).
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { AsyncLocalStorage } from "async_hooks"
|
|
4
4
|
import BasePool, { POOL_CONFIGURATION_KEY } from "./base.js"
|
|
5
|
+
import DatabasePoolCheckoutTimeoutError from "./checkout-timeout-error.js"
|
|
5
6
|
import { currentTestProfileContext } from "../../testing/test-profile-context.js"
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -101,6 +102,9 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
101
102
|
* @type {Promise<void> | undefined} */
|
|
102
103
|
pendingCheckoutDrainPromise = undefined
|
|
103
104
|
|
|
105
|
+
/** Whether a caller requested another pass through the pending checkout queue. */
|
|
106
|
+
pendingCheckoutDrainRequested = false
|
|
107
|
+
|
|
104
108
|
/**
|
|
105
109
|
* Idle connection reaper timer.
|
|
106
110
|
* @type {ReturnType<typeof setTimeout> | undefined} */
|
|
@@ -602,18 +606,45 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
602
606
|
* @returns {Promise<void>} - Resolves when pending checkouts have been drained as far as possible.
|
|
603
607
|
*/
|
|
604
608
|
async drainPendingCheckouts() {
|
|
605
|
-
|
|
606
|
-
await this.pendingCheckoutDrainPromise
|
|
607
|
-
return
|
|
608
|
-
}
|
|
609
|
+
this.pendingCheckoutDrainRequested = true
|
|
609
610
|
|
|
610
|
-
this.pendingCheckoutDrainPromise
|
|
611
|
+
if (!this.pendingCheckoutDrainPromise) this.startPendingCheckoutDrain()
|
|
612
|
+
await this.pendingCheckoutDrainPromise
|
|
613
|
+
}
|
|
611
614
|
|
|
615
|
+
/**
|
|
616
|
+
* Starts the single checkout-drain owner. The shared promise is cleared before
|
|
617
|
+
* it settles, closing the resolved-promise/stale-field interval in which a new
|
|
618
|
+
* request could otherwise be lost.
|
|
619
|
+
* @returns {void}
|
|
620
|
+
*/
|
|
621
|
+
startPendingCheckoutDrain() {
|
|
622
|
+
const {promise, reject, resolve} = Promise.withResolvers()
|
|
623
|
+
|
|
624
|
+
this.pendingCheckoutDrainPromise = promise
|
|
625
|
+
void this.runRequestedPendingCheckoutDrains({reject, resolve})
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Runs drain passes until every request observed during the active pass has
|
|
630
|
+
* received a later pass.
|
|
631
|
+
* @param {{reject: (reason?: ReturnType<typeof JSON.parse>) => void, resolve: (value?: void) => void}} deferred - Shared drain settlement.
|
|
632
|
+
* @returns {Promise<void>}
|
|
633
|
+
*/
|
|
634
|
+
async runRequestedPendingCheckoutDrains({reject, resolve}) {
|
|
612
635
|
try {
|
|
613
|
-
|
|
614
|
-
|
|
636
|
+
while (this.pendingCheckoutDrainRequested) {
|
|
637
|
+
this.pendingCheckoutDrainRequested = false
|
|
638
|
+
await this.drainPendingCheckoutsActual()
|
|
639
|
+
}
|
|
640
|
+
} catch (error) {
|
|
615
641
|
this.pendingCheckoutDrainPromise = undefined
|
|
642
|
+
reject(error)
|
|
643
|
+
return
|
|
616
644
|
}
|
|
645
|
+
|
|
646
|
+
this.pendingCheckoutDrainPromise = undefined
|
|
647
|
+
resolve()
|
|
617
648
|
}
|
|
618
649
|
|
|
619
650
|
/**
|
|
@@ -726,13 +757,13 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
726
757
|
/**
|
|
727
758
|
* Runs pending checkout timeout error.
|
|
728
759
|
* @param {PendingCheckout} checkout - Timed-out checkout.
|
|
729
|
-
* @returns {
|
|
760
|
+
* @returns {DatabasePoolCheckoutTimeoutError} - Timeout error.
|
|
730
761
|
*/
|
|
731
762
|
pendingCheckoutTimeoutError(checkout) {
|
|
732
763
|
const checkoutName = checkout.options.name ? ` Checkout name: ${JSON.stringify(checkout.options.name)}.` : ""
|
|
733
764
|
const diagnostics = this.pendingCheckoutTimeoutDiagnostics(checkout)
|
|
734
765
|
|
|
735
|
-
return new
|
|
766
|
+
return new DatabasePoolCheckoutTimeoutError(`Timed out after ${checkout.timeoutMillis}ms waiting for database connection checkout from pool "${this.identifier}".${checkoutName} ${diagnostics}`)
|
|
736
767
|
}
|
|
737
768
|
|
|
738
769
|
/**
|
|
@@ -1255,7 +1286,13 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
|
|
|
1255
1286
|
connections,
|
|
1256
1287
|
connectionsBeingSpawned: this.connectionsBeingSpawned,
|
|
1257
1288
|
idleCount: this.connections.length + [...this.lifecycleRetainedConnections.values()].filter((connection) => connection.getIdSeq() === undefined).length,
|
|
1289
|
+
idleMatchingPendingCheckoutCount: this.connections.filter((connection) => {
|
|
1290
|
+
return !this.connectionHasOpenTransaction(connection)
|
|
1291
|
+
&& this.pendingCheckouts.some((checkout) => this.connectionMatchesReuseKey(connection, checkout.reuseKey))
|
|
1292
|
+
}).length,
|
|
1258
1293
|
inUseCount: Object.keys(this.connectionsInUse).length,
|
|
1294
|
+
pendingCheckoutDrainActive: Boolean(this.pendingCheckoutDrainPromise),
|
|
1295
|
+
pendingCheckoutDrainRequested: this.pendingCheckoutDrainRequested,
|
|
1259
1296
|
pendingCheckouts: this.pendingCheckoutDebugSnapshots(now),
|
|
1260
1297
|
pendingCheckoutCount: this.pendingCheckouts.length,
|
|
1261
1298
|
telemetry: {...this.telemetry}
|
|
@@ -40,10 +40,13 @@ import sha256Hex from "../../utils/sha256-hex.js"
|
|
|
40
40
|
* @property {Array<Record<string, ReturnType<typeof JSON.parse>>>} connections - Live connection snapshots.
|
|
41
41
|
* @property {number} connectionsBeingSpawned - Number of in-progress connection spawns.
|
|
42
42
|
* @property {number} idleCount - Number of idle connections.
|
|
43
|
+
* @property {number} [idleMatchingPendingCheckoutCount] - Idle connections that can satisfy at least one pending checkout.
|
|
43
44
|
* @property {string} identifier - Database identifier.
|
|
44
45
|
* @property {number} inUseCount - Number of checked-out connections.
|
|
45
46
|
* @property {Array<DatabasePoolPendingCheckoutDebugSnapshot>} [pendingCheckouts] - Waiting checkout snapshots.
|
|
46
47
|
* @property {number} pendingCheckoutCount - Number of queued checkout requests.
|
|
48
|
+
* @property {boolean} [pendingCheckoutDrainActive] - Whether a checkout drain pass is active.
|
|
49
|
+
* @property {boolean} [pendingCheckoutDrainRequested] - Whether another checkout drain pass was requested.
|
|
47
50
|
* @property {string} poolClass - Pool class name.
|
|
48
51
|
* @property {{connectionCreationCount: number, connectionCreationFailureCount: number, connectionCreationMaxMs: number, connectionCreationTotalMs: number, checkoutTimeoutCount: number, checkoutWaitCount: number, checkoutWaitMaxMs: number, checkoutWaitTotalMs: number, idleReapCount: number, idleReapDisposalCount: number, idleReapFailureCount: number, idleReapMaxMs: number, idleReapTotalMs: number, peakLiveConnections: number}} [telemetry] - Cumulative pool lifecycle telemetry.
|
|
49
52
|
*/
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** Stable framework error for a database pool checkout that exceeded its wait limit. */
|
|
4
|
+
export default class DatabasePoolCheckoutTimeoutError extends Error {
|
|
5
|
+
/**
|
|
6
|
+
* Builds a database pool checkout timeout error.
|
|
7
|
+
* @param {string} message - Detailed sanitized pool timeout message.
|
|
8
|
+
* @param {{cause?: unknown}} [args] - Error options.
|
|
9
|
+
*/
|
|
10
|
+
constructor(message, {cause} = {}) {
|
|
11
|
+
super(message, cause === undefined ? undefined : {cause})
|
|
12
|
+
|
|
13
|
+
this.name = "DatabasePoolCheckoutTimeoutError"
|
|
14
|
+
this.code = "VELOCIOUS_DATABASE_POOL_CHECKOUT_TIMEOUT"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -4,6 +4,7 @@ import SnapReqWebSocketClient from "snapreq/websocket"
|
|
|
4
4
|
import {deserializeFrontendModelTransportValue} from "../frontend-models/transport-serialization.js"
|
|
5
5
|
|
|
6
6
|
const DEFAULT_URL = "ws://127.0.0.1:3006/websocket"
|
|
7
|
+
const SESSION_ROUTING_PARAMETER = "velociousSessionId"
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Velocious's WebSocket client. The cross-platform connection/session/channel
|
|
@@ -28,6 +29,57 @@ export default class VelociousWebsocketClient extends SnapReqWebSocketClient {
|
|
|
28
29
|
this.runningReconnectTasks = new Set()
|
|
29
30
|
/** @type {Promise<void> | null} */
|
|
30
31
|
this.gracefulClosePromise = null
|
|
32
|
+
this.routingBaseUrl = this.url
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Restores a persisted session before opening the socket so the host can route
|
|
37
|
+
* the HTTP upgrade to the worker that owns its paused state.
|
|
38
|
+
* @returns {Promise<void>}
|
|
39
|
+
*/
|
|
40
|
+
async _restoreSessionIdForRouting() {
|
|
41
|
+
// SnapReq initializes these internal session fields in its constructor, but
|
|
42
|
+
// its declaration does not expose that definite-assignment lifecycle here.
|
|
43
|
+
const routingState = /** @type {{_sessionId: string | null, _sessionStore: {get: () => string | null | undefined | Promise<string | null | undefined>} | undefined, _sessionStoreRestored: boolean}} */ (/** @type {unknown} */ (this))
|
|
44
|
+
|
|
45
|
+
if (routingState._sessionId || routingState._sessionStoreRestored || !routingState._sessionStore) return
|
|
46
|
+
|
|
47
|
+
routingState._sessionStoreRestored = true
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const storedId = await routingState._sessionStore.get()
|
|
51
|
+
|
|
52
|
+
if (typeof storedId === "string" && storedId.length > 0) routingState._sessionId = storedId
|
|
53
|
+
} catch (error) {
|
|
54
|
+
this._debug("sessionStore.get failed", error)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Builds the WebSocket URL carrying only the current resumable session routing hint.
|
|
60
|
+
* @returns {string} - WebSocket URL.
|
|
61
|
+
*/
|
|
62
|
+
_sessionRoutingUrl() {
|
|
63
|
+
const url = new URL(this.routingBaseUrl)
|
|
64
|
+
|
|
65
|
+
if (this._sessionId) {
|
|
66
|
+
url.searchParams.set(SESSION_ROUTING_PARAMETER, this._sessionId)
|
|
67
|
+
} else {
|
|
68
|
+
url.searchParams.delete(SESSION_ROUTING_PARAMETER)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return url.toString()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Restores routing state before delegating socket creation to SnapReq.
|
|
76
|
+
* @param {Parameters<SnapReqWebSocketClient["_connect"]>[0]} [options] - Connect options.
|
|
77
|
+
* @returns {Promise<void>} - Resolves when the session is ready.
|
|
78
|
+
*/
|
|
79
|
+
async _connect(options) {
|
|
80
|
+
await this._restoreSessionIdForRouting()
|
|
81
|
+
this.url = this._sessionRoutingUrl()
|
|
82
|
+
await super._connect(options)
|
|
31
83
|
}
|
|
32
84
|
|
|
33
85
|
/**
|
|
@@ -314,6 +314,12 @@ export default class VeoliciousHttpServerClient {
|
|
|
314
314
|
this.websocketSession = undefined
|
|
315
315
|
this.events.emit("close")
|
|
316
316
|
})
|
|
317
|
+
this.websocketSession.events.on("ownershipClaimed", ({sessionId}) => {
|
|
318
|
+
this.events.emit("websocketSessionOwned", {sessionId})
|
|
319
|
+
})
|
|
320
|
+
this.websocketSession.events.on("ownershipReleased", ({sessionId}) => {
|
|
321
|
+
this.events.emit("websocketSessionReleased", {sessionId})
|
|
322
|
+
})
|
|
317
323
|
this.state = "websocket"
|
|
318
324
|
this.events.emit("output", response)
|
|
319
325
|
void this.websocketSession.initializeChannel()
|
|
@@ -215,6 +215,9 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
215
215
|
*/
|
|
216
216
|
this._resumeIdentityPromise = undefined
|
|
217
217
|
|
|
218
|
+
/** @type {string | null} */
|
|
219
|
+
this._claimedSessionId = null
|
|
220
|
+
|
|
218
221
|
/**
|
|
219
222
|
* Accumulates payloads for a fragmented websocket message per
|
|
220
223
|
* RFC 6455. Non-null while mid-fragment; cleared when the frame
|
|
@@ -266,6 +269,7 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
266
269
|
* @returns {void}
|
|
267
270
|
*/
|
|
268
271
|
sendSessionEstablished() {
|
|
272
|
+
this._claimOwnership()
|
|
269
273
|
this.sendJson({
|
|
270
274
|
type: "session-established",
|
|
271
275
|
sessionId: this.sessionId,
|
|
@@ -313,6 +317,7 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
313
317
|
}
|
|
314
318
|
|
|
315
319
|
destroy() {
|
|
320
|
+
this._releaseOwnership()
|
|
316
321
|
this._stopHeartbeat()
|
|
317
322
|
this._resetFragmentBuffer()
|
|
318
323
|
this._clearBufferedFrameChunks()
|
|
@@ -325,6 +330,25 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
325
330
|
this.events.removeAllListeners()
|
|
326
331
|
}
|
|
327
332
|
|
|
333
|
+
/** Claims this session id for host-side reconnect routing. */
|
|
334
|
+
_claimOwnership() {
|
|
335
|
+
if (this._claimedSessionId === this.sessionId) return
|
|
336
|
+
if (this._claimedSessionId) this._releaseOwnership()
|
|
337
|
+
|
|
338
|
+
this._claimedSessionId = this.sessionId
|
|
339
|
+
this.events.emit("ownershipClaimed", {sessionId: this.sessionId})
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Releases the currently claimed session id exactly once. */
|
|
343
|
+
_releaseOwnership() {
|
|
344
|
+
const sessionId = this._claimedSessionId
|
|
345
|
+
|
|
346
|
+
if (!sessionId) return
|
|
347
|
+
|
|
348
|
+
this._claimedSessionId = null
|
|
349
|
+
this.events.emit("ownershipReleased", {sessionId})
|
|
350
|
+
}
|
|
351
|
+
|
|
328
352
|
/**
|
|
329
353
|
* Runs has subscription.
|
|
330
354
|
* @param {string} channel - Channel name.
|
|
@@ -1339,6 +1363,7 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
1339
1363
|
}
|
|
1340
1364
|
|
|
1341
1365
|
this._stopHeartbeat()
|
|
1366
|
+
this._releaseOwnership()
|
|
1342
1367
|
this.configuration._websocketSessions.delete(this)
|
|
1343
1368
|
void this._runMessageHandlerClose()
|
|
1344
1369
|
void this._teardownChannel()
|
|
@@ -1355,6 +1380,7 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
1355
1380
|
*/
|
|
1356
1381
|
_finalizeGraceExpiry() {
|
|
1357
1382
|
this._stopHeartbeat()
|
|
1383
|
+
this._releaseOwnership()
|
|
1358
1384
|
this._resetFragmentBuffer()
|
|
1359
1385
|
this._clearBufferedFrameChunks()
|
|
1360
1386
|
this._abandonInboundMessages()
|
|
@@ -1499,6 +1525,9 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
1499
1525
|
|
|
1500
1526
|
this.configuration._clearPausedWebsocketSession(resumeSessionId)
|
|
1501
1527
|
|
|
1528
|
+
this._releaseOwnership()
|
|
1529
|
+
paused._releaseOwnership()
|
|
1530
|
+
|
|
1502
1531
|
// Transfer resumable state onto this (live) session. The paused
|
|
1503
1532
|
// session shell is discarded after the transfer.
|
|
1504
1533
|
for (const [connectionId, connection] of paused._connections) {
|
|
@@ -1526,6 +1555,7 @@ export default class VelociousHttpServerClientWebsocketSession {
|
|
|
1526
1555
|
paused._paused = false
|
|
1527
1556
|
paused.destroy()
|
|
1528
1557
|
|
|
1558
|
+
this._claimOwnership()
|
|
1529
1559
|
this.sendJson({type: "session-resumed", sessionId: resumeSessionId})
|
|
1530
1560
|
for (const body of queued) this.sendJson(body)
|
|
1531
1561
|
await this._fireOnResume()
|