@cosmicdrift/kumiko-framework 0.328.1 → 0.329.0
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/dist/api/index.d.ts +1 -1
- package/dist/api/index.js +1 -1
- package/dist/api/websocket-route.d.ts +1 -0
- package/dist/api/websocket-route.js +37 -6
- package/package.json +4 -4
- package/src/changes.json +6 -0
package/dist/api/index.d.ts
CHANGED
|
@@ -29,4 +29,4 @@ export { createSseBroker } from "./sse-broker.js";
|
|
|
29
29
|
export { createSseRoute, SSE_HEARTBEAT_INTERVAL_MS } from "./sse-route.js";
|
|
30
30
|
export { generateToken } from "./tokens.js";
|
|
31
31
|
export type { KumikoServeEnv, KumikoServerWebSocket, KumikoWebSocketData, WebSocketUpgradeServer, } from "./websocket-route.js";
|
|
32
|
-
export { createKumikoWebSocketHandler, isWebSocketUpgradeRequest, kumikoWebSocketHandler, WEBSOCKET_BACKPRESSURE_LIMIT_BYTES, WEBSOCKET_DEFAULT_MAX_CONNECTIONS_PER_USER, WEBSOCKET_HEARTBEAT_INTERVAL_MS, WEBSOCKET_MAX_PAYLOAD_BYTES, WEBSOCKET_REVALIDATION_FAILURE_LIMIT, WEBSOCKET_ROUTE_PATH_PREFIX, } from "./websocket-route.js";
|
|
32
|
+
export { createKumikoWebSocketHandler, isWebSocketUpgradeRequest, kumikoWebSocketHandler, WEBSOCKET_BACKPRESSURE_LIMIT_BYTES, WEBSOCKET_DEFAULT_MAX_CONNECTIONS_PER_USER, WEBSOCKET_HEARTBEAT_INTERVAL_MS, WEBSOCKET_MAX_PAYLOAD_BYTES, WEBSOCKET_PENDING_BUFFER_LIMIT_BYTES, WEBSOCKET_REVALIDATION_FAILURE_LIMIT, WEBSOCKET_ROUTE_PATH_PREFIX, } from "./websocket-route.js";
|
package/dist/api/index.js
CHANGED
|
@@ -17,4 +17,4 @@ export { buildServer, makeDispatchSystemQuery, makeDispatchSystemWrite } from ".
|
|
|
17
17
|
export { createSseBroker } from "./sse-broker.js";
|
|
18
18
|
export { createSseRoute, SSE_HEARTBEAT_INTERVAL_MS } from "./sse-route.js";
|
|
19
19
|
export { generateToken } from "./tokens.js";
|
|
20
|
-
export { createKumikoWebSocketHandler, isWebSocketUpgradeRequest, kumikoWebSocketHandler, WEBSOCKET_BACKPRESSURE_LIMIT_BYTES, WEBSOCKET_DEFAULT_MAX_CONNECTIONS_PER_USER, WEBSOCKET_HEARTBEAT_INTERVAL_MS, WEBSOCKET_MAX_PAYLOAD_BYTES, WEBSOCKET_REVALIDATION_FAILURE_LIMIT, WEBSOCKET_ROUTE_PATH_PREFIX, } from "./websocket-route.js";
|
|
20
|
+
export { createKumikoWebSocketHandler, isWebSocketUpgradeRequest, kumikoWebSocketHandler, WEBSOCKET_BACKPRESSURE_LIMIT_BYTES, WEBSOCKET_DEFAULT_MAX_CONNECTIONS_PER_USER, WEBSOCKET_HEARTBEAT_INTERVAL_MS, WEBSOCKET_MAX_PAYLOAD_BYTES, WEBSOCKET_PENDING_BUFFER_LIMIT_BYTES, WEBSOCKET_REVALIDATION_FAILURE_LIMIT, WEBSOCKET_ROUTE_PATH_PREFIX, } from "./websocket-route.js";
|
|
@@ -4,6 +4,7 @@ import { type AuthSessionChecker, type TenantLifecycleStatusResolver } from "./a
|
|
|
4
4
|
import { type RequestContextData } from "./request-context.js";
|
|
5
5
|
export { WEBSOCKET_BACKPRESSURE_LIMIT_BYTES, WEBSOCKET_DEFAULT_MAX_CONNECTIONS_PER_USER, WEBSOCKET_DEFAULT_MAX_MESSAGE_BYTES, WEBSOCKET_MAX_PAYLOAD_BYTES, WEBSOCKET_ROUTE_PATH_PREFIX, };
|
|
6
6
|
export declare const WEBSOCKET_HEARTBEAT_INTERVAL_MS = 25000;
|
|
7
|
+
export declare const WEBSOCKET_PENDING_BUFFER_LIMIT_BYTES: number;
|
|
7
8
|
export declare const WEBSOCKET_REVALIDATION_FAILURE_LIMIT = 3;
|
|
8
9
|
export type KumikoWebSocketData = {
|
|
9
10
|
readonly handlers: WebSocketSessionHandlers;
|
|
@@ -9,6 +9,9 @@ const CLOSE_POLICY_VIOLATION = 1008;
|
|
|
9
9
|
const CLOSE_MESSAGE_TOO_BIG = 1009;
|
|
10
10
|
const CLOSE_INTERNAL_ERROR = 1011;
|
|
11
11
|
const CLOSE_TRY_AGAIN_LATER = 1013;
|
|
12
|
+
// Frames queue behind a slow onOpen or onMessage; without a byte cap a client
|
|
13
|
+
// could grow that queue unbounded.
|
|
14
|
+
export const WEBSOCKET_PENDING_BUFFER_LIMIT_BYTES = 1024 * 1024;
|
|
12
15
|
// Consecutive failed session-store checks before a socket is closed: rides out a
|
|
13
16
|
// short store blip, but a socket must not stay unverified forever.
|
|
14
17
|
export const WEBSOCKET_REVALIDATION_FAILURE_LIMIT = 3;
|
|
@@ -87,6 +90,7 @@ function stateOf(ws) {
|
|
|
87
90
|
revalidating: false,
|
|
88
91
|
chain: Promise.resolve(),
|
|
89
92
|
closed: false,
|
|
93
|
+
pendingMessageBytes: 0,
|
|
90
94
|
controller: new AbortController(),
|
|
91
95
|
};
|
|
92
96
|
connectionStates.set(ws.data, state);
|
|
@@ -112,22 +116,25 @@ function runInRequestContext(ws, run) {
|
|
|
112
116
|
const { requestContextData } = ws.data;
|
|
113
117
|
return requestContextData ? requestContext.run(requestContextData, run) : run();
|
|
114
118
|
}
|
|
115
|
-
function enqueue(ws, run) {
|
|
119
|
+
function enqueue(ws, run, onSettled) {
|
|
116
120
|
const state = stateOf(ws);
|
|
117
121
|
// skip: the socket is gone, nothing may start after close
|
|
118
122
|
if (state.closed)
|
|
119
123
|
return;
|
|
120
124
|
const step = async () => {
|
|
121
|
-
// skip: queued behind a slower handler and the socket closed meanwhile
|
|
122
|
-
if (state.closed)
|
|
123
|
-
return;
|
|
124
125
|
try {
|
|
126
|
+
// skip: queued behind a slower handler and the socket closed meanwhile
|
|
127
|
+
if (state.closed)
|
|
128
|
+
return;
|
|
125
129
|
await run();
|
|
126
130
|
}
|
|
127
131
|
catch (error) {
|
|
128
132
|
reportHandlerFailure(error);
|
|
129
133
|
ws.close(CLOSE_INTERNAL_ERROR, "internal error");
|
|
130
134
|
}
|
|
135
|
+
finally {
|
|
136
|
+
onSettled?.();
|
|
137
|
+
}
|
|
131
138
|
};
|
|
132
139
|
state.chain = state.chain.then(() => runInRequestContext(ws, step));
|
|
133
140
|
}
|
|
@@ -208,9 +215,33 @@ export function createKumikoWebSocketHandler(options = {}) {
|
|
|
208
215
|
});
|
|
209
216
|
return;
|
|
210
217
|
}
|
|
218
|
+
const state = stateOf(ws);
|
|
219
|
+
// skip: overflowed or closed socket, late frames are dropped
|
|
220
|
+
if (state.closed)
|
|
221
|
+
return;
|
|
222
|
+
state.pendingMessageBytes += byteLength;
|
|
223
|
+
const pendingLimit = Math.max(WEBSOCKET_PENDING_BUFFER_LIMIT_BYTES, ws.data.maxMessageBytes);
|
|
224
|
+
if (state.pendingMessageBytes > pendingLimit) {
|
|
225
|
+
// Marks the socket closed right away so already-queued frames never start.
|
|
226
|
+
state.closed = true;
|
|
227
|
+
ws.close(CLOSE_TRY_AGAIN_LATER, "try again later");
|
|
228
|
+
log.warn("websocket pending buffer over cap", {
|
|
229
|
+
pendingBytes: state.pendingMessageBytes,
|
|
230
|
+
limitBytes: pendingLimit,
|
|
231
|
+
});
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
211
234
|
const { onMessage } = ws.data.handlers;
|
|
212
|
-
if (onMessage)
|
|
213
|
-
|
|
235
|
+
if (onMessage) {
|
|
236
|
+
// Copy now: queued behind a slow handler, Bun may have reused the buffer by then.
|
|
237
|
+
const data = toMessageData(message);
|
|
238
|
+
enqueue(ws, () => onMessage(data, toConnection(ws)), () => {
|
|
239
|
+
state.pendingMessageBytes -= byteLength;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
state.pendingMessageBytes -= byteLength;
|
|
244
|
+
}
|
|
214
245
|
},
|
|
215
246
|
close(ws, code, reason) {
|
|
216
247
|
const state = stateOf(ws);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.329.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -206,8 +206,8 @@
|
|
|
206
206
|
"./package.json": "./package.json"
|
|
207
207
|
},
|
|
208
208
|
"dependencies": {
|
|
209
|
-
"@cosmicdrift/kumiko-http": "0.
|
|
210
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
209
|
+
"@cosmicdrift/kumiko-http": "0.329.0",
|
|
210
|
+
"@cosmicdrift/kumiko-types": "0.329.0",
|
|
211
211
|
"bullmq": "^5.76.7",
|
|
212
212
|
"bun-types": "^1.3.13",
|
|
213
213
|
"hono": "^4.13.1",
|
|
@@ -223,7 +223,7 @@
|
|
|
223
223
|
"zod": "^4.4.3"
|
|
224
224
|
},
|
|
225
225
|
"devDependencies": {
|
|
226
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
226
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.329.0",
|
|
227
227
|
"bun-types": "^1.3.13",
|
|
228
228
|
"pino-pretty": "^13.1.3"
|
|
229
229
|
},
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.329.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "WebSocket routes cap the bytes queued behind slow handlers",
|
|
6
|
+
"detail": "Frames that arrive while `onOpen` or an earlier `onMessage` is still running are queued in order. Their summed size is now limited to 1 MiB (`WEBSOCKET_PENDING_BUFFER_LIMIT_BYTES`, or the route's `maxMessageBytes` if larger); beyond that the socket closes with code 1013 and the queued frames are dropped."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.328.1",
|
|
4
10
|
"type": "improvement",
|