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.
Files changed (42) hide show
  1. package/README.md +28 -4
  2. package/bin/sonicws.mjs +159 -0
  3. package/bundled/bundle.js +1 -1
  4. package/dist/index.d.ts +9 -4
  5. package/dist/index.js +10 -3
  6. package/dist/native/wrapper.js +1 -1
  7. package/dist/version.d.ts +1 -1
  8. package/dist/version.js +1 -1
  9. package/dist/ws/Connection.d.ts +15 -3
  10. package/dist/ws/Connection.js +1 -1
  11. package/dist/ws/PacketProcessor.js +1 -1
  12. package/dist/ws/client/core/ClientCore.d.ts +20 -8
  13. package/dist/ws/client/core/ClientCore.js +1 -1
  14. package/dist/ws/client/node/ClientNode.d.ts +3 -2
  15. package/dist/ws/client/node/ClientNode.js +1 -1
  16. package/dist/ws/debug/PacketLogger.d.ts +30 -0
  17. package/dist/ws/debug/PacketLogger.js +7 -0
  18. package/dist/ws/packets/PacketType.d.ts +4 -1
  19. package/dist/ws/packets/Packets.d.ts +10 -1
  20. package/dist/ws/packets/Packets.js +1 -1
  21. package/dist/ws/server/SonicWSConnection.d.ts +14 -2
  22. package/dist/ws/server/SonicWSConnection.js +1 -1
  23. package/dist/ws/server/SonicWSServer.d.ts +18 -7
  24. package/dist/ws/server/SonicWSServer.js +1 -1
  25. package/dist/ws/util/JSONUtil.js +7 -0
  26. package/dist/ws/util/packets/BatchHelper.js +1 -1
  27. package/dist/ws/util/packets/ControlProtocol.d.ts +7 -1
  28. package/dist/ws/util/packets/ControlProtocol.js +1 -1
  29. package/dist/ws/util/packets/PacketHolder.js +1 -1
  30. package/dist/ws/util/packets/PacketUtils.d.ts +50 -12
  31. package/dist/ws/util/packets/PacketUtils.js +1 -1
  32. package/dist/ws/util/packets/VariantPermutation.d.ts +83 -0
  33. package/dist/ws/util/packets/VariantPermutation.js +7 -0
  34. package/dist/ws/util/packets/{PacketManifest.d.ts → metadata/PacketManifest.d.ts} +1 -1
  35. package/dist/ws/util/packets/{PacketManifest.js → metadata/PacketManifest.js} +1 -1
  36. package/dist/ws/util/packets/metadata/SchemaValidation.d.ts +17 -0
  37. package/dist/ws/util/packets/metadata/SchemaValidation.js +7 -0
  38. package/package.json +10 -4
  39. package/dist/ws/util/packets/JSONUtil.js +0 -7
  40. /package/dist/ws/util/{packets/JSONUtil.d.ts → JSONUtil.d.ts} +0 -0
  41. /package/dist/ws/util/packets/{ConstructorRegistry.d.ts → metadata/ConstructorRegistry.d.ts} +0 -0
  42. /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/python layer handles connections, packet definitions, middleware, and the public API. The Rust core handles the wire format:
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
- The same Rust implementation runs through WASM in Node, browsers, and Python. Python loads its packaged WASM core through `wasmtime`, while Node uses the packaged Node-target module. Protocol behavior therefore does not depend on the operating system or connected runtime.
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 # Rust core only
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
 
@@ -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}`);