@series-inc/rundot-syncplay 5.23.0-beta.7
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 +610 -0
- package/dist/animation.d.ts +119 -0
- package/dist/animation.js +488 -0
- package/dist/asset-cooking.d.ts +82 -0
- package/dist/asset-cooking.js +364 -0
- package/dist/authority-room.d.ts +217 -0
- package/dist/authority-room.js +701 -0
- package/dist/bot-documents.d.ts +115 -0
- package/dist/bot-documents.js +549 -0
- package/dist/bot-primitives.d.ts +152 -0
- package/dist/bot-primitives.js +472 -0
- package/dist/browser.d.ts +57 -0
- package/dist/browser.js +53 -0
- package/dist/canonical.d.ts +12 -0
- package/dist/canonical.js +288 -0
- package/dist/certification.d.ts +455 -0
- package/dist/certification.js +1855 -0
- package/dist/chaos-certification.d.ts +56 -0
- package/dist/chaos-certification.js +1397 -0
- package/dist/cjs/animation.js +495 -0
- package/dist/cjs/asset-cooking.js +372 -0
- package/dist/cjs/authority-room.js +704 -0
- package/dist/cjs/bot-documents.js +556 -0
- package/dist/cjs/bot-primitives.js +482 -0
- package/dist/cjs/browser.js +96 -0
- package/dist/cjs/canonical.js +299 -0
- package/dist/cjs/certification.js +1860 -0
- package/dist/cjs/chaos-certification.js +1402 -0
- package/dist/cjs/collections.js +36 -0
- package/dist/cjs/collision.js +96 -0
- package/dist/cjs/commands.js +987 -0
- package/dist/cjs/config-bundle.js +491 -0
- package/dist/cjs/creator.js +30 -0
- package/dist/cjs/deterministic-primitives.js +134 -0
- package/dist/cjs/deterministic-session.js +1410 -0
- package/dist/cjs/ecs-lite.js +155 -0
- package/dist/cjs/effects-adapter.js +97 -0
- package/dist/cjs/engine-complete.js +2297 -0
- package/dist/cjs/engine-core-proof.js +70 -0
- package/dist/cjs/engine-parity-matrix.js +874 -0
- package/dist/cjs/engine-parity.js +3183 -0
- package/dist/cjs/engine-runtime.js +3141 -0
- package/dist/cjs/errors.js +22 -0
- package/dist/cjs/events.js +247 -0
- package/dist/cjs/examples/counter-duel.js +157 -0
- package/dist/cjs/examples/fixed-arena.js +368 -0
- package/dist/cjs/index.js +229 -0
- package/dist/cjs/input-authority.js +150 -0
- package/dist/cjs/input-button.js +256 -0
- package/dist/cjs/input-timeline.js +418 -0
- package/dist/cjs/instant-replay.js +66 -0
- package/dist/cjs/lag-compensation3d.js +837 -0
- package/dist/cjs/local-authority-server-runner.js +1121 -0
- package/dist/cjs/local-authority-wire.js +1497 -0
- package/dist/cjs/local-authority.js +817 -0
- package/dist/cjs/local-ws-adapter.js +150 -0
- package/dist/cjs/match-log.js +61 -0
- package/dist/cjs/math.js +157 -0
- package/dist/cjs/mock-network-adapter.js +26 -0
- package/dist/cjs/movement.js +703 -0
- package/dist/cjs/movement3d.js +1246 -0
- package/dist/cjs/multiclient-authority.js +308 -0
- package/dist/cjs/multiplayer-service.js +434 -0
- package/dist/cjs/navigation.js +347 -0
- package/dist/cjs/navmesh.js +1437 -0
- package/dist/cjs/networked-client.js +691 -0
- package/dist/cjs/networked-input-authority.js +443 -0
- package/dist/cjs/node.js +47 -0
- package/dist/cjs/number-lab.js +78 -0
- package/dist/cjs/offline-session.js +370 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pause-resume.js +43 -0
- package/dist/cjs/physics-cert.js +426 -0
- package/dist/cjs/physics2d.js +751 -0
- package/dist/cjs/physics3d.js +1996 -0
- package/dist/cjs/prediction-culling.js +203 -0
- package/dist/cjs/protocol-lab.js +1299 -0
- package/dist/cjs/r3f-render-adapter.js +209 -0
- package/dist/cjs/random.js +59 -0
- package/dist/cjs/replay-bundle.js +1598 -0
- package/dist/cjs/replay-inspect.js +236 -0
- package/dist/cjs/replay-scoring.js +68 -0
- package/dist/cjs/replay.js +356 -0
- package/dist/cjs/reports.js +181 -0
- package/dist/cjs/rollback-history.js +251 -0
- package/dist/cjs/runtime-guards.js +111 -0
- package/dist/cjs/sample-lab.js +145 -0
- package/dist/cjs/sample-scenes.js +752 -0
- package/dist/cjs/schema-artifacts.js +359 -0
- package/dist/cjs/schema-codegen.js +544 -0
- package/dist/cjs/schema-dsl-binary.js +655 -0
- package/dist/cjs/schema.js +319 -0
- package/dist/cjs/sdk-offline.js +99 -0
- package/dist/cjs/session-promotion.js +74 -0
- package/dist/cjs/session-snapshot.js +265 -0
- package/dist/cjs/session-wire.js +216 -0
- package/dist/cjs/signals.js +103 -0
- package/dist/cjs/sparse-set.js +169 -0
- package/dist/cjs/synctest.js +136 -0
- package/dist/cjs/system-lifecycle.js +286 -0
- package/dist/cjs/timers.js +61 -0
- package/dist/cjs/tools/static-checker.js +1468 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/venus-mp-budget.js +125 -0
- package/dist/cjs/ws-frame.js +255 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +11379 -0
- package/dist/collections.d.ts +9 -0
- package/dist/collections.js +30 -0
- package/dist/collision.d.ts +25 -0
- package/dist/collision.js +85 -0
- package/dist/commands.d.ts +137 -0
- package/dist/commands.js +977 -0
- package/dist/config-bundle.d.ts +89 -0
- package/dist/config-bundle.js +483 -0
- package/dist/creator.d.ts +20 -0
- package/dist/creator.js +11 -0
- package/dist/deterministic-primitives.d.ts +59 -0
- package/dist/deterministic-primitives.js +126 -0
- package/dist/deterministic-session.d.ts +197 -0
- package/dist/deterministic-session.js +1401 -0
- package/dist/ecs-lite.d.ts +26 -0
- package/dist/ecs-lite.js +152 -0
- package/dist/effects-adapter.d.ts +21 -0
- package/dist/effects-adapter.js +94 -0
- package/dist/engine-complete.d.ts +45 -0
- package/dist/engine-complete.js +2292 -0
- package/dist/engine-core-proof.d.ts +26 -0
- package/dist/engine-core-proof.js +66 -0
- package/dist/engine-parity-matrix.d.ts +300 -0
- package/dist/engine-parity-matrix.js +869 -0
- package/dist/engine-parity.d.ts +74 -0
- package/dist/engine-parity.js +3178 -0
- package/dist/engine-runtime.d.ts +351 -0
- package/dist/engine-runtime.js +3123 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +17 -0
- package/dist/events.d.ts +45 -0
- package/dist/events.js +242 -0
- package/dist/examples/counter-duel.d.ts +43 -0
- package/dist/examples/counter-duel.js +150 -0
- package/dist/examples/fixed-arena.d.ts +110 -0
- package/dist/examples/fixed-arena.js +356 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +66 -0
- package/dist/input-authority.d.ts +82 -0
- package/dist/input-authority.js +147 -0
- package/dist/input-button.d.ts +36 -0
- package/dist/input-button.js +249 -0
- package/dist/input-timeline.d.ts +65 -0
- package/dist/input-timeline.js +412 -0
- package/dist/instant-replay.d.ts +59 -0
- package/dist/instant-replay.js +62 -0
- package/dist/lag-compensation3d.d.ts +146 -0
- package/dist/lag-compensation3d.js +827 -0
- package/dist/local-authority-server-runner.d.ts +79 -0
- package/dist/local-authority-server-runner.js +1118 -0
- package/dist/local-authority-wire.d.ts +142 -0
- package/dist/local-authority-wire.js +1477 -0
- package/dist/local-authority.d.ts +160 -0
- package/dist/local-authority.js +810 -0
- package/dist/local-ws-adapter.d.ts +14 -0
- package/dist/local-ws-adapter.js +146 -0
- package/dist/match-log.d.ts +74 -0
- package/dist/match-log.js +57 -0
- package/dist/math.d.ts +41 -0
- package/dist/math.js +154 -0
- package/dist/mock-network-adapter.d.ts +13 -0
- package/dist/mock-network-adapter.js +23 -0
- package/dist/movement.d.ts +155 -0
- package/dist/movement.js +694 -0
- package/dist/movement3d.d.ts +195 -0
- package/dist/movement3d.js +1238 -0
- package/dist/multiclient-authority.d.ts +30 -0
- package/dist/multiclient-authority.js +305 -0
- package/dist/multiplayer-service.d.ts +153 -0
- package/dist/multiplayer-service.js +429 -0
- package/dist/navigation.d.ts +91 -0
- package/dist/navigation.js +338 -0
- package/dist/navmesh.d.ts +236 -0
- package/dist/navmesh.js +1423 -0
- package/dist/networked-client.d.ts +124 -0
- package/dist/networked-client.js +688 -0
- package/dist/networked-input-authority.d.ts +51 -0
- package/dist/networked-input-authority.js +439 -0
- package/dist/node.d.ts +22 -0
- package/dist/node.js +19 -0
- package/dist/number-lab.d.ts +15 -0
- package/dist/number-lab.js +75 -0
- package/dist/offline-session.d.ts +2 -0
- package/dist/offline-session.js +367 -0
- package/dist/pause-resume.d.ts +48 -0
- package/dist/pause-resume.js +40 -0
- package/dist/physics-cert.d.ts +40 -0
- package/dist/physics-cert.js +389 -0
- package/dist/physics2d.d.ts +142 -0
- package/dist/physics2d.js +741 -0
- package/dist/physics3d.d.ts +285 -0
- package/dist/physics3d.js +1979 -0
- package/dist/prediction-culling.d.ts +34 -0
- package/dist/prediction-culling.js +200 -0
- package/dist/protocol-lab.d.ts +115 -0
- package/dist/protocol-lab.js +1295 -0
- package/dist/r3f-render-adapter.d.ts +61 -0
- package/dist/r3f-render-adapter.js +203 -0
- package/dist/random.d.ts +13 -0
- package/dist/random.js +55 -0
- package/dist/replay-bundle.d.ts +150 -0
- package/dist/replay-bundle.js +1586 -0
- package/dist/replay-inspect.d.ts +60 -0
- package/dist/replay-inspect.js +228 -0
- package/dist/replay-scoring.d.ts +28 -0
- package/dist/replay-scoring.js +65 -0
- package/dist/replay.d.ts +17 -0
- package/dist/replay.js +351 -0
- package/dist/reports.d.ts +176 -0
- package/dist/reports.js +174 -0
- package/dist/rollback-history.d.ts +46 -0
- package/dist/rollback-history.js +247 -0
- package/dist/runtime-guards.d.ts +1 -0
- package/dist/runtime-guards.js +108 -0
- package/dist/sample-lab.d.ts +32 -0
- package/dist/sample-lab.js +134 -0
- package/dist/sample-scenes.d.ts +86 -0
- package/dist/sample-scenes.js +745 -0
- package/dist/schema-artifacts.d.ts +49 -0
- package/dist/schema-artifacts.js +356 -0
- package/dist/schema-codegen.d.ts +21 -0
- package/dist/schema-codegen.js +538 -0
- package/dist/schema-dsl-binary.d.ts +75 -0
- package/dist/schema-dsl-binary.js +648 -0
- package/dist/schema.d.ts +87 -0
- package/dist/schema.js +315 -0
- package/dist/sdk-offline.d.ts +34 -0
- package/dist/sdk-offline.js +96 -0
- package/dist/session-promotion.d.ts +26 -0
- package/dist/session-promotion.js +70 -0
- package/dist/session-snapshot.d.ts +95 -0
- package/dist/session-snapshot.js +255 -0
- package/dist/session-wire.d.ts +89 -0
- package/dist/session-wire.js +211 -0
- package/dist/signals.d.ts +31 -0
- package/dist/signals.js +99 -0
- package/dist/sparse-set.d.ts +34 -0
- package/dist/sparse-set.js +165 -0
- package/dist/synctest.d.ts +31 -0
- package/dist/synctest.js +133 -0
- package/dist/system-lifecycle.d.ts +72 -0
- package/dist/system-lifecycle.js +281 -0
- package/dist/timers.d.ts +17 -0
- package/dist/timers.js +53 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/static-checker.d.ts +31 -0
- package/dist/tools/static-checker.js +1462 -0
- package/dist/tools/vite-plugin.d.ts +69 -0
- package/dist/tools/vite-plugin.js +203 -0
- package/dist/types.d.ts +148 -0
- package/dist/types.js +1 -0
- package/dist/venus-mp-budget.d.ts +29 -0
- package/dist/venus-mp-budget.js +122 -0
- package/dist/ws-frame.d.ts +49 -0
- package/dist/ws-frame.js +247 -0
- package/package.json +169 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runVenusMpBudgetLab = runVenusMpBudgetLab;
|
|
4
|
+
const canonical_js_1 = require("./canonical.js");
|
|
5
|
+
const random_js_1 = require("./random.js");
|
|
6
|
+
const sample_lab_js_1 = require("./sample-lab.js");
|
|
7
|
+
function runVenusMpBudgetLab(options) {
|
|
8
|
+
validateOptions(options);
|
|
9
|
+
const random = new random_js_1.DeterministicRandom(options.seed);
|
|
10
|
+
const frames = Array.from({ length: options.framesPerMessage }, (_, frame) => ({
|
|
11
|
+
frame,
|
|
12
|
+
inputs: Array.from({ length: options.players }, () => (0, sample_lab_js_1.makeLabInput)(random)),
|
|
13
|
+
}));
|
|
14
|
+
const jsonPayload = {
|
|
15
|
+
type: 'message',
|
|
16
|
+
msgType: 'rdm.input.v1',
|
|
17
|
+
data: {
|
|
18
|
+
startFrame: 0,
|
|
19
|
+
frames,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const packedPayloadBytes = frames.reduce((sum, frame) => sum + (0, sample_lab_js_1.packLabInputFrame)(frame.inputs).byteLength, 0);
|
|
23
|
+
const packedEnvelopeBytes = JSON.stringify({
|
|
24
|
+
type: 'message',
|
|
25
|
+
msgType: 'rdm.input-packed.v1',
|
|
26
|
+
data: {
|
|
27
|
+
startFrame: 0,
|
|
28
|
+
frames: options.framesPerMessage,
|
|
29
|
+
bytes: packedPayloadBytes,
|
|
30
|
+
},
|
|
31
|
+
}).length;
|
|
32
|
+
const jsonMessageBytes = JSON.stringify(jsonPayload).length;
|
|
33
|
+
const packedMessageBytes = packedEnvelopeBytes + packedPayloadBytes;
|
|
34
|
+
const messagesPerSecondPerPlayer = options.hz / options.framesPerMessage;
|
|
35
|
+
const roomInboundMessagesPerSecond = messagesPerSecondPerPlayer * options.players;
|
|
36
|
+
const withinMaxPlayers = options.players <= options.maxPlayers;
|
|
37
|
+
const withinRateLimit = roomInboundMessagesPerSecond <= options.rateLimitPerSecond;
|
|
38
|
+
const jsonWithinMaxMessage = jsonMessageBytes <= options.maxMessageBytes;
|
|
39
|
+
const packedWithinMaxMessage = packedMessageBytes <= options.maxMessageBytes;
|
|
40
|
+
let result = 'fail';
|
|
41
|
+
if (withinMaxPlayers && withinRateLimit && (jsonWithinMaxMessage || packedWithinMaxMessage)) {
|
|
42
|
+
result = 'pass';
|
|
43
|
+
}
|
|
44
|
+
const recommendation = chooseRecommendation({
|
|
45
|
+
withinMaxPlayers,
|
|
46
|
+
withinRateLimit,
|
|
47
|
+
jsonWithinMaxMessage,
|
|
48
|
+
packedWithinMaxMessage,
|
|
49
|
+
});
|
|
50
|
+
const reportWithoutChecksum = {
|
|
51
|
+
...options,
|
|
52
|
+
messagesPerSecondPerPlayer,
|
|
53
|
+
roomInboundMessagesPerSecond,
|
|
54
|
+
jsonMessageBytes,
|
|
55
|
+
packedMessageBytes,
|
|
56
|
+
jsonWithinMaxMessage,
|
|
57
|
+
packedWithinMaxMessage,
|
|
58
|
+
withinRateLimit,
|
|
59
|
+
withinMaxPlayers,
|
|
60
|
+
recommendation,
|
|
61
|
+
result,
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
players: options.players,
|
|
65
|
+
hz: options.hz,
|
|
66
|
+
framesPerMessage: options.framesPerMessage,
|
|
67
|
+
maxPlayers: options.maxPlayers,
|
|
68
|
+
rateLimitPerSecond: options.rateLimitPerSecond,
|
|
69
|
+
maxMessageBytes: options.maxMessageBytes,
|
|
70
|
+
messagesPerSecondPerPlayer: messagesPerSecondPerPlayer.toFixed(2),
|
|
71
|
+
roomInboundMessagesPerSecond: roomInboundMessagesPerSecond.toFixed(2),
|
|
72
|
+
jsonMessageBytes,
|
|
73
|
+
packedMessageBytes,
|
|
74
|
+
jsonWithinMaxMessage,
|
|
75
|
+
packedWithinMaxMessage,
|
|
76
|
+
withinRateLimit,
|
|
77
|
+
withinMaxPlayers,
|
|
78
|
+
recommendation,
|
|
79
|
+
checksum: (0, canonical_js_1.defaultChecksum)(reportWithoutChecksum),
|
|
80
|
+
result,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function chooseRecommendation(config) {
|
|
84
|
+
const failures = [
|
|
85
|
+
!config.withinMaxPlayers,
|
|
86
|
+
!config.withinRateLimit,
|
|
87
|
+
!config.jsonWithinMaxMessage && !config.packedWithinMaxMessage,
|
|
88
|
+
].filter(Boolean).length;
|
|
89
|
+
if (failures === 0) {
|
|
90
|
+
return 'pass';
|
|
91
|
+
}
|
|
92
|
+
if (failures > 1) {
|
|
93
|
+
return 'multiple-changes';
|
|
94
|
+
}
|
|
95
|
+
if (!config.withinMaxPlayers) {
|
|
96
|
+
return 'increase-max-players';
|
|
97
|
+
}
|
|
98
|
+
if (!config.withinRateLimit) {
|
|
99
|
+
return 'increase-bundle-size';
|
|
100
|
+
}
|
|
101
|
+
return 'compact-payload';
|
|
102
|
+
}
|
|
103
|
+
function validateOptions(options) {
|
|
104
|
+
if (!Number.isInteger(options.players) || options.players < 1 || options.players > 128) {
|
|
105
|
+
throw new Error('Venus MP budget players must be between 1 and 128');
|
|
106
|
+
}
|
|
107
|
+
if (![10, 20, 30, 60].includes(options.hz)) {
|
|
108
|
+
throw new Error('Venus MP budget hz must be 10, 20, 30, or 60');
|
|
109
|
+
}
|
|
110
|
+
if (!Number.isInteger(options.framesPerMessage) || options.framesPerMessage < 1 || options.framesPerMessage > 60) {
|
|
111
|
+
throw new Error('Venus MP budget framesPerMessage must be between 1 and 60');
|
|
112
|
+
}
|
|
113
|
+
if (!Number.isInteger(options.maxPlayers) || options.maxPlayers < 1) {
|
|
114
|
+
throw new Error('Venus MP budget maxPlayers must be positive');
|
|
115
|
+
}
|
|
116
|
+
if (!Number.isInteger(options.rateLimitPerSecond) || options.rateLimitPerSecond < 1) {
|
|
117
|
+
throw new Error('Venus MP budget rateLimitPerSecond must be positive');
|
|
118
|
+
}
|
|
119
|
+
if (!Number.isInteger(options.maxMessageBytes) || options.maxMessageBytes < 1) {
|
|
120
|
+
throw new Error('Venus MP budget maxMessageBytes must be positive');
|
|
121
|
+
}
|
|
122
|
+
if (!Number.isInteger(options.seed) || options.seed < 0 || options.seed > 0xffff_ffff) {
|
|
123
|
+
throw new Error('Venus MP budget seed must be a uint32 integer');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.webSocketAcceptKey = webSocketAcceptKey;
|
|
4
|
+
exports.encodeWebSocketTextFrame = encodeWebSocketTextFrame;
|
|
5
|
+
exports.encodeWebSocketCloseFrame = encodeWebSocketCloseFrame;
|
|
6
|
+
exports.readWebSocketFrame = readWebSocketFrame;
|
|
7
|
+
exports.startWebSocketServer = startWebSocketServer;
|
|
8
|
+
exports.connectWebSocketClient = connectWebSocketClient;
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
const node_http_1 = require("node:http");
|
|
11
|
+
const canonical_js_1 = require("./canonical.js");
|
|
12
|
+
/**
|
|
13
|
+
* Minimal RFC 6455 text-frame codec plus a connection-oriented server/client.
|
|
14
|
+
*
|
|
15
|
+
* The engine ships with zero runtime dependencies, so this hand-rolls just enough of the
|
|
16
|
+
* WebSocket wire format to carry small JSON control messages over a real socket. It supports
|
|
17
|
+
* unfragmented text frames up to 65535 bytes — sufficient for input/confirmed-input traffic.
|
|
18
|
+
* `local-ws-adapter.ts` and the M0 networked-authority spike share this codec.
|
|
19
|
+
*/
|
|
20
|
+
const webSocketGuid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
21
|
+
function webSocketAcceptKey(secWebSocketKey) {
|
|
22
|
+
return (0, node_crypto_1.createHash)('sha1').update(`${secWebSocketKey}${webSocketGuid}`).digest('base64');
|
|
23
|
+
}
|
|
24
|
+
function encodeWebSocketTextFrame(text) {
|
|
25
|
+
const payload = (0, canonical_js_1.encodeUtf8)(text);
|
|
26
|
+
if (payload.byteLength > 65535) {
|
|
27
|
+
throw new Error('WebSocket text frame exceeds the 65535-byte codec limit');
|
|
28
|
+
}
|
|
29
|
+
const headerLength = payload.byteLength < 126 ? 2 : 4;
|
|
30
|
+
const frame = new Uint8Array(headerLength + payload.byteLength);
|
|
31
|
+
frame[0] = 0x81;
|
|
32
|
+
if (payload.byteLength < 126) {
|
|
33
|
+
frame[1] = payload.byteLength;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
frame[1] = 126;
|
|
37
|
+
frame[2] = Math.floor(payload.byteLength / 256);
|
|
38
|
+
frame[3] = payload.byteLength % 256;
|
|
39
|
+
}
|
|
40
|
+
frame.set(payload, headerLength);
|
|
41
|
+
return frame;
|
|
42
|
+
}
|
|
43
|
+
/** A minimal, bodyless close frame (opcode 0x8, FIN set). */
|
|
44
|
+
function encodeWebSocketCloseFrame() {
|
|
45
|
+
return new Uint8Array([0x88, 0x00]);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* NEVER throws: this runs inside raw `socket.on('data')` handlers, where an
|
|
49
|
+
* uncaught throw is not routed to the 'error' listener and takes the process
|
|
50
|
+
* down — a handful of malformed bytes from any client would be an
|
|
51
|
+
* unauthenticated crash. Malformed input is reported as `kind: 'invalid'` so
|
|
52
|
+
* the receive loop can close the connection instead.
|
|
53
|
+
*/
|
|
54
|
+
function readWebSocketFrame(bytes) {
|
|
55
|
+
if (bytes.byteLength < 2) {
|
|
56
|
+
return { kind: 'incomplete' };
|
|
57
|
+
}
|
|
58
|
+
const opcode = bytes[0] & 0x0f;
|
|
59
|
+
const masked = (bytes[1] & 0x80) !== 0;
|
|
60
|
+
// Capture the 7-bit tag BEFORE the 126 branch reassigns `length`: a 127-byte
|
|
61
|
+
// payload arrives in the 16-bit form and must not trip the 64-bit guard.
|
|
62
|
+
const lengthTag = bytes[1] & 0x7f;
|
|
63
|
+
let length = lengthTag;
|
|
64
|
+
let offset = 2;
|
|
65
|
+
if (lengthTag === 127) {
|
|
66
|
+
return { kind: 'invalid', reason: 'unsupported-64-bit-frame-length' };
|
|
67
|
+
}
|
|
68
|
+
if (lengthTag === 126) {
|
|
69
|
+
if (bytes.byteLength < 4) {
|
|
70
|
+
return { kind: 'incomplete' };
|
|
71
|
+
}
|
|
72
|
+
length = bytes[2] * 256 + bytes[3];
|
|
73
|
+
offset = 4;
|
|
74
|
+
}
|
|
75
|
+
const maskBytes = masked ? bytes.slice(offset, offset + 4) : undefined;
|
|
76
|
+
offset += masked ? 4 : 0;
|
|
77
|
+
if (bytes.byteLength < offset + length) {
|
|
78
|
+
return { kind: 'incomplete' };
|
|
79
|
+
}
|
|
80
|
+
const payload = bytes.slice(offset, offset + length);
|
|
81
|
+
if (maskBytes !== undefined) {
|
|
82
|
+
for (let index = 0; index < payload.byteLength; index += 1) {
|
|
83
|
+
payload[index] ^= maskBytes[index % 4];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
let text;
|
|
87
|
+
try {
|
|
88
|
+
text = (0, canonical_js_1.decodeUtf8)(payload);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return { kind: 'invalid', reason: 'malformed-utf8-payload' };
|
|
92
|
+
}
|
|
93
|
+
return { kind: 'frame', frame: { opcode, text, bytesRead: offset + length } };
|
|
94
|
+
}
|
|
95
|
+
async function startWebSocketServer(options) {
|
|
96
|
+
const path = options.path ?? '/';
|
|
97
|
+
const server = (0, node_http_1.createServer)((request, response) => {
|
|
98
|
+
if (request.url === '/health') {
|
|
99
|
+
response.statusCode = 200;
|
|
100
|
+
response.setHeader('content-type', 'text/plain');
|
|
101
|
+
response.end('ok');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
response.statusCode = 404;
|
|
105
|
+
response.end('not found');
|
|
106
|
+
});
|
|
107
|
+
server.on('upgrade', (request, socket, head) => {
|
|
108
|
+
if (new URL(request.url ?? '/', 'http://127.0.0.1').pathname !== path) {
|
|
109
|
+
socket.end('HTTP/1.1 404 Not Found\r\n\r\n');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const key = request.headers['sec-websocket-key'];
|
|
113
|
+
if (typeof key !== 'string') {
|
|
114
|
+
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
socket.write([
|
|
118
|
+
'HTTP/1.1 101 Switching Protocols',
|
|
119
|
+
'Upgrade: websocket',
|
|
120
|
+
'Connection: Upgrade',
|
|
121
|
+
`Sec-WebSocket-Accept: ${webSocketAcceptKey(key)}`,
|
|
122
|
+
'\r\n',
|
|
123
|
+
].join('\r\n'));
|
|
124
|
+
// The upgraded socket is a net.Socket at runtime; @types/node's upgrade signature omits a
|
|
125
|
+
// 'close' overload, so narrow it to the small surface this codec actually uses.
|
|
126
|
+
const connection = createServerConnection(socket);
|
|
127
|
+
options.onConnection(connection.handle, request);
|
|
128
|
+
if (head.byteLength > 0) {
|
|
129
|
+
connection.receive(head);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
await new Promise((resolveListen) => {
|
|
133
|
+
server.listen(options.port ?? 0, '127.0.0.1', resolveListen);
|
|
134
|
+
});
|
|
135
|
+
const address = server.address();
|
|
136
|
+
if (address === null || typeof address === 'string') {
|
|
137
|
+
throw new Error('Could not determine WebSocket server port');
|
|
138
|
+
}
|
|
139
|
+
const port = address.port;
|
|
140
|
+
return {
|
|
141
|
+
port,
|
|
142
|
+
url: `ws://127.0.0.1:${port}${path}`,
|
|
143
|
+
close() {
|
|
144
|
+
return new Promise((resolveClose, rejectClose) => {
|
|
145
|
+
server.close((error) => (error === undefined ? resolveClose() : rejectClose(error)));
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function createServerConnection(socket) {
|
|
151
|
+
let pending = new Uint8Array();
|
|
152
|
+
const messageHandlers = [];
|
|
153
|
+
const closeHandlers = [];
|
|
154
|
+
let closed = false;
|
|
155
|
+
function emitClose() {
|
|
156
|
+
if (closed) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
closed = true;
|
|
160
|
+
for (const handler of closeHandlers) {
|
|
161
|
+
handler();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function receive(chunk) {
|
|
165
|
+
pending = concatBytes(pending, chunk);
|
|
166
|
+
let result = readWebSocketFrame(pending);
|
|
167
|
+
while (result.kind === 'frame') {
|
|
168
|
+
const frame = result.frame;
|
|
169
|
+
pending = pending.slice(frame.bytesRead);
|
|
170
|
+
if (frame.opcode === 0x8) {
|
|
171
|
+
socket.end(encodeWebSocketCloseFrame());
|
|
172
|
+
emitClose();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (frame.opcode === 0x1) {
|
|
176
|
+
const text = frame.text;
|
|
177
|
+
for (const handler of messageHandlers) {
|
|
178
|
+
handler(text);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
result = readWebSocketFrame(pending);
|
|
182
|
+
}
|
|
183
|
+
if (result.kind === 'invalid') {
|
|
184
|
+
// Malformed frame: close the connection instead of crashing the process.
|
|
185
|
+
socket.end(encodeWebSocketCloseFrame());
|
|
186
|
+
emitClose();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
socket.on('data', (chunk) => receive(chunk));
|
|
190
|
+
socket.on('error', () => {
|
|
191
|
+
socket.destroy();
|
|
192
|
+
emitClose();
|
|
193
|
+
});
|
|
194
|
+
socket.on('close', emitClose);
|
|
195
|
+
const handle = {
|
|
196
|
+
send(text) {
|
|
197
|
+
if (!closed) {
|
|
198
|
+
socket.write(encodeWebSocketTextFrame(text));
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
onMessage(handler) {
|
|
202
|
+
messageHandlers.push(handler);
|
|
203
|
+
},
|
|
204
|
+
onClose(handler) {
|
|
205
|
+
closeHandlers.push(handler);
|
|
206
|
+
},
|
|
207
|
+
close() {
|
|
208
|
+
if (!closed) {
|
|
209
|
+
socket.end(encodeWebSocketCloseFrame());
|
|
210
|
+
emitClose();
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
return { handle, receive };
|
|
215
|
+
}
|
|
216
|
+
async function connectWebSocketClient(url) {
|
|
217
|
+
const socket = new WebSocket(url);
|
|
218
|
+
const messageHandlers = [];
|
|
219
|
+
const closeHandlers = [];
|
|
220
|
+
socket.addEventListener('message', (event) => {
|
|
221
|
+
const text = String(event.data);
|
|
222
|
+
for (const handler of messageHandlers) {
|
|
223
|
+
handler(text);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
socket.addEventListener('close', () => {
|
|
227
|
+
for (const handler of closeHandlers) {
|
|
228
|
+
handler();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
await new Promise((resolveOpen, rejectOpen) => {
|
|
232
|
+
socket.addEventListener('open', () => resolveOpen(), { once: true });
|
|
233
|
+
socket.addEventListener('error', () => rejectOpen(new Error(`Could not connect to ${url}`)), { once: true });
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
send(text) {
|
|
237
|
+
socket.send(text);
|
|
238
|
+
},
|
|
239
|
+
onMessage(handler) {
|
|
240
|
+
messageHandlers.push(handler);
|
|
241
|
+
},
|
|
242
|
+
onClose(handler) {
|
|
243
|
+
closeHandlers.push(handler);
|
|
244
|
+
},
|
|
245
|
+
close() {
|
|
246
|
+
socket.close();
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function concatBytes(left, right) {
|
|
251
|
+
const output = new Uint8Array(left.byteLength + right.byteLength);
|
|
252
|
+
output.set(left, 0);
|
|
253
|
+
output.set(right, left.byteLength);
|
|
254
|
+
return output;
|
|
255
|
+
}
|
package/dist/cli.d.ts
ADDED