sonic-ws 2.2.0 → 2.4.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/README.md +28 -4
- package/bin/sonicws.mjs +159 -0
- package/bundled/bundle.js +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.js +10 -3
- package/dist/native/wrapper.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/ws/Connection.d.ts +15 -3
- package/dist/ws/Connection.js +1 -1
- package/dist/ws/PacketProcessor.js +1 -1
- package/dist/ws/client/core/ClientCore.d.ts +20 -8
- package/dist/ws/client/core/ClientCore.js +1 -1
- package/dist/ws/client/node/ClientNode.d.ts +3 -2
- package/dist/ws/client/node/ClientNode.js +1 -1
- package/dist/ws/debug/PacketLogger.d.ts +30 -0
- package/dist/ws/debug/PacketLogger.js +7 -0
- package/dist/ws/packets/PacketType.d.ts +4 -1
- package/dist/ws/packets/Packets.d.ts +10 -1
- package/dist/ws/packets/Packets.js +1 -1
- package/dist/ws/server/SonicWSConnection.d.ts +14 -2
- package/dist/ws/server/SonicWSConnection.js +1 -1
- package/dist/ws/server/SonicWSServer.d.ts +18 -7
- package/dist/ws/server/SonicWSServer.js +1 -1
- package/dist/ws/util/JSONUtil.js +7 -0
- package/dist/ws/util/packets/BatchHelper.js +1 -1
- package/dist/ws/util/packets/ControlProtocol.d.ts +7 -1
- package/dist/ws/util/packets/ControlProtocol.js +1 -1
- package/dist/ws/util/packets/PacketHolder.js +1 -1
- package/dist/ws/util/packets/PacketUtils.d.ts +50 -12
- package/dist/ws/util/packets/PacketUtils.js +1 -1
- package/dist/ws/util/packets/VariantPermutation.d.ts +83 -0
- package/dist/ws/util/packets/VariantPermutation.js +7 -0
- package/dist/ws/util/packets/{PacketManifest.d.ts → metadata/PacketManifest.d.ts} +1 -1
- package/dist/ws/util/packets/{PacketManifest.js → metadata/PacketManifest.js} +1 -1
- package/dist/ws/util/packets/metadata/SchemaValidation.d.ts +17 -0
- package/dist/ws/util/packets/metadata/SchemaValidation.js +7 -0
- package/package.json +10 -4
- package/dist/ws/util/packets/JSONUtil.js +0 -7
- /package/dist/ws/util/{packets/JSONUtil.d.ts → JSONUtil.d.ts} +0 -0
- /package/dist/ws/util/packets/{ConstructorRegistry.d.ts → metadata/ConstructorRegistry.d.ts} +0 -0
- /package/dist/ws/util/packets/{ConstructorRegistry.js → metadata/ConstructorRegistry.js} +0 -0
package/README.md
CHANGED
|
@@ -15,6 +15,8 @@ It is designed for real-time applications such as games, dashboards, collaborati
|
|
|
15
15
|
- Batch, validate, compress, and rate-limit packets with minimal boilerplate.
|
|
16
16
|
- Reconnect with bounded state recovery and opt-in missed-packet replay.
|
|
17
17
|
- Use validated RPC, server-side rooms, and pluggable scaling adapters.
|
|
18
|
+
- Inspect, encode, decode, size, validate, and generate types from packet manifests with the CLI.
|
|
19
|
+
- Drop replaceable updates under backpressure without silently dropping reliable messages.
|
|
18
20
|
- Code is much more readable than every other socket library*
|
|
19
21
|
|
|
20
22
|
<details>
|
|
@@ -72,7 +74,7 @@ Whether you're making a real-time game, a dashboard, a distributed system, or an
|
|
|
72
74
|
|
|
73
75
|
You define the packets each side is allowed to send. SonicWS exchanges that schema during the connection handshake, assigns compact numeric packet IDs, and validates incoming data before calling your listeners.
|
|
74
76
|
|
|
75
|
-
The TypeScript
|
|
77
|
+
The TypeScript, Python, and native Rust packages handle connections, packet definitions, and their language-specific public APIs. The shared Rust core handles the wire format:
|
|
76
78
|
|
|
77
79
|
- Primitive and object packet encoding/decoding
|
|
78
80
|
- Packet validation and range checks
|
|
@@ -80,7 +82,7 @@ The TypeScript/python layer handles connections, packet definitions, middleware,
|
|
|
80
82
|
- Object framing, batching, and raw DEFLATE compression
|
|
81
83
|
- TypeScript/Python-side JSON conversion transported through reserved wire type 16 as raw bytes
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
Node and browsers run the core through WebAssembly, and Python loads its packaged WASM core through `wasmtime`. The native Rust package links the core directly and adds a Tokio WebSocket client/server runtime. Protocol behavior therefore comes from the same codec on every supported runtime.
|
|
84
86
|
|
|
85
87
|
Automatic browser-file serving at `/SonicWS/bundle.js` and
|
|
86
88
|
`/SonicWS/bundle.wasm` is supported only by the Node.js server, where it can be
|
|
@@ -107,6 +109,10 @@ Python* (*unpublished currently):
|
|
|
107
109
|
```sh
|
|
108
110
|
pip install sonic-ws
|
|
109
111
|
```
|
|
112
|
+
Rust:
|
|
113
|
+
```toml
|
|
114
|
+
sonic-ws = { path = "projects/rust" }
|
|
115
|
+
```
|
|
110
116
|
|
|
111
117
|
## BUILDING AND TESTING
|
|
112
118
|
|
|
@@ -116,7 +122,8 @@ From the repository root, use the build dispatcher:
|
|
|
116
122
|
|
|
117
123
|
```sh
|
|
118
124
|
./build.sh all # Build every project
|
|
119
|
-
./build.sh rust #
|
|
125
|
+
./build.sh rust # Shared core and native Rust runtime
|
|
126
|
+
./build.sh core # Shared codec core only
|
|
120
127
|
./build.sh ts # Complete Node/browser package
|
|
121
128
|
./build.sh py # Python wheel and sdist
|
|
122
129
|
./build.sh test # Run all test suites
|
|
@@ -135,7 +142,7 @@ npm run test_node # Node end-to-end packet tests
|
|
|
135
142
|
npm run test_web # Headless browser/WASM end-to-end tests
|
|
136
143
|
```
|
|
137
144
|
|
|
138
|
-
The workspace is split into `projects/core`, `projects/ts`, and `projects/py`.
|
|
145
|
+
The workspace is split into `projects/core`, `projects/rust`, `projects/ts`, and `projects/py`.
|
|
139
146
|
Each project owns its source, tests, and packaging configuration. Shared browser
|
|
140
147
|
artifacts remain at `bundled/bundle.js` and `bundled/bundle.wasm`.
|
|
141
148
|
|
|
@@ -149,6 +156,13 @@ Full API documentation:
|
|
|
149
156
|
|
|
150
157
|
- [TypeScript / Node / browser](docs/ts/README.md)
|
|
151
158
|
- [Python](docs/py/README.md)
|
|
159
|
+
- [Native Rust](projects/rust/README.md)
|
|
160
|
+
- [Protocol version 25](docs/protocol.md)
|
|
161
|
+
- [Production defaults](docs/defaults.md)
|
|
162
|
+
- [Authentication and recovery](docs/authentication.md)
|
|
163
|
+
- [Inspector, generated types, conformance, and benchmarks](docs/tooling.md)
|
|
164
|
+
- [Backpressure and delivery](docs/backpressure.md)
|
|
165
|
+
- [Runnable examples](examples/README.md)
|
|
152
166
|
|
|
153
167
|
Packet schemas can now map the existing single-type wire format directly to application objects:
|
|
154
168
|
|
|
@@ -167,6 +181,12 @@ This remains a homogeneous `VARINT` packet. Schema mapping, row-major `autoFlatt
|
|
|
167
181
|
|
|
168
182
|
Clients can opt into capped exponential-backoff reconnect. Packets marked `replay: true` are retained in a bounded per-session buffer; successful recovery also restores server-side `state` and room membership. RPC request payloads use ordinary packet definitions, so validation and compact encoding still apply. Room broadcasts work locally and can be forwarded across processes through an adapter. Long-polling fallback is intentionally outside SonicWS's scope.
|
|
169
183
|
|
|
184
|
+
## WHERE SONICWS FITS
|
|
185
|
+
|
|
186
|
+
SonicWS is strongest for schema-known realtime traffic: numeric movement updates, repeated entity snapshots, compact enums, bounded RPC payloads, and other messages where validation and wire size matter. Positional sends are available as the lowest-allocation hot path, while schema mapping trades some CPU for cleaner application objects.
|
|
187
|
+
|
|
188
|
+
Arbitrary chat strings and already-compressed or highly dynamic JSON may see little or no size improvement over ordinary JSON WebSockets. SonicWS should be selected for its explicit packet contract, validation, lifecycle behavior, and compact structured traffic—not because every possible payload encodes faster or smaller than JSON.
|
|
189
|
+
|
|
170
190
|
## KNOWN ISSUES
|
|
171
191
|
|
|
172
192
|
|
|
@@ -175,6 +195,10 @@ Clients can opt into capped exponential-backoff reconnect. Packets marked `repla
|
|
|
175
195
|
- Publish the Rust core as a standalone, documented crate
|
|
176
196
|
- Add first-class Go bindings
|
|
177
197
|
|
|
198
|
+
## API AND PROTOCOL STABILITY
|
|
199
|
+
|
|
200
|
+
SonicWS uses semantic package versions and an explicit wire-protocol version. Incompatible wire changes require a protocol-version change and fail during handshake instead of silently decoding with different semantics. Public APIs may still evolve between major package versions; pin the package version for production deployments and run the shared conformance corpus when upgrading.
|
|
201
|
+
|
|
178
202
|
## LICENSE
|
|
179
203
|
This project is source-available.
|
|
180
204
|
|
package/bin/sonicws.mjs
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
readFile,
|
|
5
|
+
writeFile
|
|
6
|
+
} from "node:fs/promises";
|
|
7
|
+
import {
|
|
8
|
+
deflateRawSync
|
|
9
|
+
} from "node:zlib";
|
|
10
|
+
import {
|
|
11
|
+
LoadPacketManifest,
|
|
12
|
+
ValidatePacketSchema
|
|
13
|
+
} from "../dist/index.js";
|
|
14
|
+
import {
|
|
15
|
+
PacketHolder
|
|
16
|
+
} from "../dist/ws/util/packets/PacketHolder.js";
|
|
17
|
+
import {
|
|
18
|
+
processPacket
|
|
19
|
+
} from "../dist/ws/util/packets/PacketUtils.js";
|
|
20
|
+
|
|
21
|
+
function usage(message) {
|
|
22
|
+
if (message) console.error(message);
|
|
23
|
+
console.error(`Usage:
|
|
24
|
+
sonicws inspect <manifest.swsm>
|
|
25
|
+
sonicws validate <manifest.swsm>
|
|
26
|
+
sonicws types <manifest.swsm> [output.d.ts]
|
|
27
|
+
sonicws encode <manifest.swsm> <client|server> <tag> <json>
|
|
28
|
+
sonicws decode <manifest.swsm> <client|server> <tag> <hex>
|
|
29
|
+
sonicws size <manifest.swsm> <client|server> <tag> <json>`);
|
|
30
|
+
process.exit(message ? 2 : 0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const [, , command, filename, direction, tag, input] = process.argv;
|
|
34
|
+
if (!command || command === "help" || command === "--help") usage();
|
|
35
|
+
if (!filename) usage("A packet manifest is required");
|
|
36
|
+
const manifest = LoadPacketManifest(new Uint8Array(await readFile(filename)));
|
|
37
|
+
const select = name => name === "client" ? manifest.clientPackets : name === "server" ? manifest.serverPackets : usage("Direction must be client or server");
|
|
38
|
+
|
|
39
|
+
if (command === "inspect") {
|
|
40
|
+
const summarize = packets => packets.map((packet, index) => ({
|
|
41
|
+
key: index + 1,
|
|
42
|
+
tag: packet.tag,
|
|
43
|
+
type: packet.type,
|
|
44
|
+
schema: packet.fields,
|
|
45
|
+
dataMin: packet.dataMin,
|
|
46
|
+
dataMax: packet.dataMax,
|
|
47
|
+
batchingMs: packet.dataBatching,
|
|
48
|
+
compressed: packet.gzipCompression,
|
|
49
|
+
replay: packet.replay,
|
|
50
|
+
}));
|
|
51
|
+
console.log(JSON.stringify({
|
|
52
|
+
protocol: 24,
|
|
53
|
+
clientPackets: summarize(manifest.clientPackets),
|
|
54
|
+
serverPackets: summarize(manifest.serverPackets)
|
|
55
|
+
}, null, 2));
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (command === "validate") {
|
|
60
|
+
const client = ValidatePacketSchema(manifest.clientPackets, {
|
|
61
|
+
direction: "client",
|
|
62
|
+
warnUnbounded: true
|
|
63
|
+
});
|
|
64
|
+
const server = ValidatePacketSchema(manifest.serverPackets, {
|
|
65
|
+
direction: "server"
|
|
66
|
+
});
|
|
67
|
+
console.log(JSON.stringify({
|
|
68
|
+
client,
|
|
69
|
+
server,
|
|
70
|
+
valid: client.errors.length + server.errors.length === 0
|
|
71
|
+
}, null, 2));
|
|
72
|
+
process.exit(client.errors.length + server.errors.length ? 1 : 0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (command === "types") {
|
|
76
|
+
const scalar = packetType => ({
|
|
77
|
+
0: "undefined",
|
|
78
|
+
1: "Uint8Array",
|
|
79
|
+
2: "string",
|
|
80
|
+
3: "string",
|
|
81
|
+
4: "unknown",
|
|
82
|
+
5: "number",
|
|
83
|
+
6: "number",
|
|
84
|
+
7: "number",
|
|
85
|
+
8: "number",
|
|
86
|
+
9: "number",
|
|
87
|
+
10: "number",
|
|
88
|
+
11: "number",
|
|
89
|
+
12: "number",
|
|
90
|
+
13: "number",
|
|
91
|
+
14: "boolean",
|
|
92
|
+
16: "unknown",
|
|
93
|
+
17: "string",
|
|
94
|
+
})[packetType] ?? "unknown";
|
|
95
|
+
const payload = (packet, packets, group = true) => {
|
|
96
|
+
if (group && packet.isParent) {
|
|
97
|
+
const variants = packets.filter(child => child.parent === packet.tag && child.variant)
|
|
98
|
+
.map(child => `{ variant: ${JSON.stringify(child.variant)}; payload: ${payload(child, packets, false)} }`);
|
|
99
|
+
return [`{ variant: ""; payload: undefined }`, ...variants].join(" | ");
|
|
100
|
+
}
|
|
101
|
+
const types = Array.isArray(packet.type) ? packet.type : [packet.type];
|
|
102
|
+
if (packet.fields) {
|
|
103
|
+
const record = `{ ${packet.fields.map((field, index) => `${JSON.stringify(field)}: ${scalar(types[Math.min(index, types.length - 1)])}`).join("; ")} }`;
|
|
104
|
+
return packet.autoFlatten ? `${record}[]` : record;
|
|
105
|
+
}
|
|
106
|
+
if (packet.type === 0) return "undefined";
|
|
107
|
+
const value = scalar(types[0]);
|
|
108
|
+
return packet.dataMax === 1 ? value : `${value}[]`;
|
|
109
|
+
};
|
|
110
|
+
const table = (name, packets) => [
|
|
111
|
+
`export interface ${name} {`,
|
|
112
|
+
...packets.map(packet => ` ${JSON.stringify(packet.tag)}: ${payload(packet, packets)};`),
|
|
113
|
+
"}",
|
|
114
|
+
].join("\n");
|
|
115
|
+
const generated = `// Generated by sonicws types; protocol 25.\n${table("ClientPackets", manifest.clientPackets)}\n\n${table("ServerPackets", manifest.serverPackets)}\n\ntype PacketArguments<T> = T extends undefined ? [] : [value: T];\nexport interface TypedSonicWS {\n send<K extends keyof ClientPackets>(tag: K, ...value: PacketArguments<ClientPackets[K]>): Promise<void>;\n sendSafe<K extends keyof ClientPackets>(tag: K, ...value: PacketArguments<ClientPackets[K]>): Promise<boolean>;\n on<K extends keyof ServerPackets>(tag: K, listener: (value: ServerPackets[K]) => void): void;\n}\n`;
|
|
116
|
+
if (direction) await writeFile(direction, generated);
|
|
117
|
+
else process.stdout.write(generated);
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!direction || !tag || input === undefined) usage(`Missing arguments for ${command}`);
|
|
122
|
+
const packets = select(direction);
|
|
123
|
+
const holder = new PacketHolder(packets);
|
|
124
|
+
const packet = holder.getPacket(tag);
|
|
125
|
+
|
|
126
|
+
if (command === "decode") {
|
|
127
|
+
if (!/^(?:[0-9a-fA-F]{2})*$/.test(input)) usage("Encoded data must be even-length hexadecimal");
|
|
128
|
+
let bytes = Uint8Array.from(Buffer.from(input, "hex"));
|
|
129
|
+
if (bytes[0] === holder.getKey(tag)) bytes = bytes.slice(1);
|
|
130
|
+
const decoded = await packet.listen(bytes, null);
|
|
131
|
+
if (typeof decoded === "string") throw new Error(decoded);
|
|
132
|
+
console.log(JSON.stringify(decoded[0], null, 2));
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let value;
|
|
137
|
+
try {
|
|
138
|
+
value = JSON.parse(input);
|
|
139
|
+
} catch {
|
|
140
|
+
usage("Value must be valid JSON");
|
|
141
|
+
}
|
|
142
|
+
const values = packet.fields || packet.autoFlatten ? [value] : Array.isArray(value) ? value : [value];
|
|
143
|
+
const [key, encoded] = await processPacket(holder, tag, values, [false, [], undefined], 0);
|
|
144
|
+
const frame = Buffer.from([key, ...encoded]);
|
|
145
|
+
if (command === "encode") {
|
|
146
|
+
console.log(frame.toString("hex"));
|
|
147
|
+
} else if (command === "size") {
|
|
148
|
+
const json = Buffer.from(JSON.stringify({
|
|
149
|
+
event: tag,
|
|
150
|
+
data: value
|
|
151
|
+
}));
|
|
152
|
+
console.log(JSON.stringify({
|
|
153
|
+
tag,
|
|
154
|
+
sonicBytes: frame.length,
|
|
155
|
+
rawDeflateBytes: 1 + deflateRawSync(encoded).length,
|
|
156
|
+
jsonBytes: json.length,
|
|
157
|
+
reductionPercent: 100 * (1 - frame.length / json.length)
|
|
158
|
+
}, null, 2));
|
|
159
|
+
} else usage(`Unknown command: ${command}`);
|