ventiws 1.0.0-alpha → 1.0.0-alpha.10

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Farben
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Farben
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,138 +1,160 @@
1
- <p align="center">
2
- <img src="misc/ventiws_banner.png" alt="ventiws banner" />
3
- </p>
4
-
5
- # ventiws
6
-
7
- A WebSocket implementation for Node.js with the API of
8
- [`ws`](https://github.com/websockets/ws), delivered as a native addon whose framing
9
- engine is written in Zig. It targets `ws` 8.21.3 and reproduces that release's
10
- observable behaviour rather than its source. ventiws is not affiliated with the
11
- `ws` project and vendors none of its code; the upstream API reference is vendored
12
- as the pinned contract, credited in
13
- [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
14
-
15
- **ventiws is at `1.0.0-alpha`.** Nothing is published to npm yet, so an
16
- install builds the addon for one platform, and the surface may change between
17
- alpha releases. What "alpha" means here is bounded: the `ws` compatibility
18
- contract is tracked row by row in
19
- [COMPATIBILITY.md](COMPATIBILITY.md), and the four places ventiws knowingly
20
- differs from `ws` are listed in [CHANGELOG.md](CHANGELOG.md) under "Known
21
- divergences from `ws`".
22
-
23
- ## Install
24
-
25
- Working in the tree:
26
-
27
- ```sh
28
- git clone git@github.com:farbenbuilds/ventiws.git
29
- cd ventiws
30
- nix develop
31
- pnpm install
32
- pnpm build
33
- ```
34
-
35
- Published usage, once a release lands:
36
-
37
- ```sh
38
- pnpm add ventiws
39
- ```
40
-
41
- `nix develop` pins Node.js, pnpm, and Zig 0.16.0. The first `pnpm build` compiles
42
- the vendored C dependencies, which takes minutes and about a gigabyte; later
43
- builds are incremental.
44
-
45
- ## Usage
46
-
47
- ```ts
48
- import { WebSocket, WebSocketServer } from "ventiws";
49
-
50
- const server = new WebSocketServer({ port: 8080 });
51
-
52
- server.on("listening", () => {
53
- console.log("listening on", server.address()?.port);
54
- });
55
-
56
- server.on("connection", (socket) => {
57
- socket.on("message", (data, isBinary) => {
58
- socket.send(isBinary ? data : `echo: ${data.toString()}`);
59
- });
60
- socket.on("close", (code, reason) => {
61
- console.log("closed", code, reason.toString());
62
- });
63
- });
64
-
65
- const client = new WebSocket("ws://127.0.0.1:8080/");
66
- client.on("open", () => client.send("hello"));
67
- client.on("message", (data) => console.log("client saw", data.toString()));
68
- client.on("error", (error) => console.error(error));
69
- ```
70
-
71
- Both halves run against a real `ws` peer in this repository's suites, and the
72
- constructors are plain functions returning state records: no `class`, no `this`,
73
- no prototype.
74
-
75
- ## Status
76
-
77
- Option defaults match `ws`: `maxPayload` 100 MiB, `maxFragments` 16384, and
78
- `closeTimeout` 30000 ms. The itemised matrix, with the owner module and the
79
- evidence test behind every row, is [COMPATIBILITY.md](COMPATIBILITY.md).
80
-
81
- | Area | State |
82
- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
83
- | `WebSocketServer`: options, events, `handleUpgrade`, `shouldHandle`, `verifyClient` | Works, compared property for property against `ws` |
84
- | Server-side `WebSocket`: properties, `on*` and DOM handlers, ready states | Works, with exactly-once `close` |
85
- | `WebSocket` client: `net` and `tls`, redirects, `ws+unix:`, `unexpected-response` | Works against a real `ws` server |
86
- | `send`, fragmentation, `binaryType`, `ping`/`pong`, close codes | Works both directions; `maxPayload` and `maxFragments` are enforced per connection |
87
- | `permessage-deflate` | Works; context takeover is declined in both directions ([why](COMPATIBILITY.md#shared-with-the-engine)) |
88
- | `createWebSocketStream`, `clientTracking`, `server.options` | Works, including the `WebSocket` class option |
89
- | Engine route message cap, 64 KiB | A property of the build. [Two limits](#two-limits-worth-knowing) |
90
- | Engine route inbound ring, 64 messages | Excess is dropped and counted. Same section |
91
- | RFC 6455 Autobahn suite | Regression gate; 424 of 517 cases in the full run, 268 in the framing selection ([matrix](COMPATIBILITY.md#rfc-6455-conformance)) |
92
- | Client `wss:` to `ws:` downgrade, redirect hop limit, `origin`, `handshakeTimeout` | Implemented with no behavioural test ([outstanding](COMPATIBILITY.md#what-is-still-outstanding)) |
93
-
94
- ## Two limits worth knowing
95
-
96
- **The 64 KiB message cap.** `message_capacity` in
97
- `src/engine/server/capacities.zig` is a `comptime` constant, so it is baked into
98
- the addon: `maxPayload` sizes each connection's own ceiling and does not lift this
99
- one. 64 KiB is the largest payload the conformance suite puts on the wire, and a
100
- frame above it is closed with 1009. Raising it costs about 22 MB per live server,
101
- because the message slab, the write queue, and both staging rings scale with it.
102
-
103
- **The 64-slot inbound ring.** The engine has no hook for stopping a read when its
104
- consumer falls behind, so the ring in `src/engine/server/instance.zig` is the
105
- only place a burst can be absorbed. It holds 64 messages, and a peer delivering
106
- more before the Node main thread drains has the excess dropped and counted by
107
- `serverDroppedMessages`. `ws` applies backpressure instead, so it has no cliff.
108
-
109
- Both bind the engine route, which is µWebZockets' own listener. The codec route
110
- the public `ws` surface uses is bound by neither: its buffers grow into each
111
- connection's own `maxPayload` and `maxFragments`. `engineLimits()` is exported
112
- from the package entry, so the compiled numbers are readable at runtime.
113
-
114
- ## Commands
115
-
116
- ```sh
117
- pnpm build
118
- pnpm test
119
- pnpm test:compat
120
- pnpm test:autobahn
121
- pnpm bench
122
- pnpm typecheck
123
- pnpm lint
124
- pnpm format:check
125
- ```
126
-
127
- ## Docs and licence
128
-
129
- - [COMPATIBILITY.md](COMPATIBILITY.md): every `ws` surface item, its owner module, its status, and its evidence test.
130
- - [CODEBASE.md](CODEBASE.md): repository layout, the two routes, the boundary, and the compiled capacities.
131
- - [CODING_CONVENTION.md](CODING_CONVENTION.md): TypeScript and Zig style, the anti-OOP rules, and the module budget.
132
- - [docs/migrating.md](docs/migrating.md): moving an existing `ws` application over.
133
- - [CONTRIBUTE.md](CONTRIBUTE.md): environment setup, the script contract, and how to release.
134
-
135
- MIT. See [LICENSE](LICENSE). `ws` is copyright Einar Otto Stangvik, Arnout
136
- Kazemier, and contributors, MIT-licensed. µWebZockets is the first-party
137
- MIT-licensed protocol engine written for this project. Provenance for everything
138
- vendored or CI-only is in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
1
+ <p align="center">
2
+ <img src="misc/ventiws_banner.png" alt="ventiws banner" />
3
+ </p>
4
+
5
+ # ventiws
6
+
7
+ A WebSocket implementation for Node.js with the API of
8
+ [`ws`](https://github.com/websockets/ws), delivered as a native addon whose framing
9
+ engine is written in Zig. It targets `ws` 8.21.3 and reproduces that release's
10
+ observable behaviour rather than its source. ventiws is not affiliated with the
11
+ `ws` project and vendors none of its code; the upstream API reference is vendored
12
+ as the pinned contract, credited in
13
+ [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
14
+
15
+ **ventiws is at `1.0.0-alpha.10`.** The surface may change between alpha
16
+ releases. What "alpha" means here is bounded: the `ws` compatibility
17
+ contract is tracked row by row in
18
+ [COMPATIBILITY.md](COMPATIBILITY.md), and the four places ventiws knowingly
19
+ differs from `ws` are listed in [CHANGELOG.md](CHANGELOG.md) under "Known
20
+ divergences from `ws`".
21
+
22
+ ## Install
23
+
24
+ ```sh
25
+ pnpm add ventiws
26
+ ```
27
+
28
+ The addon arrives as a per-platform package that npm installs only on a host
29
+ it matches, so the install needs no compiler:
30
+
31
+ | Platform | Package |
32
+ | --------------------------- | ---------------------------------- |
33
+ | Linux x64, glibc | `@ventiws/binding-linux-x64-gnu` |
34
+ | Linux x64, musl (Alpine) | `@ventiws/binding-linux-x64-musl` |
35
+ | Linux arm64, glibc | `@ventiws/binding-linux-arm64-gnu` |
36
+ | macOS x64 | `@ventiws/binding-darwin-x64` |
37
+ | macOS arm64 (Apple silicon) | `@ventiws/binding-darwin-arm64` |
38
+
39
+ Any other host installs ventiws with no addon behind it, and the first
40
+ `WebSocketServer` says so by name and lists the platforms above.
41
+
42
+ **Windows is not supported yet, at either layer.** There is no published addon,
43
+ because the pinned `napi-zig` asks the build for an ABI-less
44
+ `x86_64-windows-none` that has no `ws2_32` to link. More fundamentally, the
45
+ engine cannot serve a socket there: its listener binds and the kernel accepts,
46
+ then the event loop fails `accept` and `read` with `error.Unexpected`, and the
47
+ peer is reset. Building in a checkout does not change that, so a Windows
48
+ checkout is a place to edit the code, not to run a server. The frame codec
49
+ itself is platform-independent and its suites do pass there.
50
+
51
+ Working in the tree:
52
+
53
+ ```sh
54
+ git clone git@github.com:farbenbuilds/ventiws.git
55
+ cd ventiws
56
+ direnv allow
57
+ pnpm install
58
+ pnpm build
59
+ ```
60
+
61
+ `direnv allow` runs `.envrc`, which enters the pinned Nix shell with Node.js,
62
+ pnpm, and Zig 0.16.0, and re-enters it on every later `cd`. Without
63
+ [direnv](https://direnv.net) installed, run `nix develop` once per shell
64
+ instead. The first `pnpm build` compiles the vendored C dependencies, which
65
+ takes minutes and about a gigabyte; later builds are incremental.
66
+
67
+ ## Usage
68
+
69
+ ```ts
70
+ import { WebSocket, WebSocketServer } from "ventiws";
71
+
72
+ const server = new WebSocketServer({ port: 8080 });
73
+
74
+ server.on("listening", () => {
75
+ console.log("listening on", server.address()?.port);
76
+ });
77
+
78
+ server.on("connection", (socket) => {
79
+ socket.on("message", (data, isBinary) => {
80
+ socket.send(isBinary ? data : `echo: ${data.toString()}`);
81
+ });
82
+ socket.on("close", (code, reason) => {
83
+ console.log("closed", code, reason.toString());
84
+ });
85
+ });
86
+
87
+ const client = new WebSocket("ws://127.0.0.1:8080/");
88
+ client.on("open", () => client.send("hello"));
89
+ client.on("message", (data) => console.log("client saw", data.toString()));
90
+ client.on("error", (error) => console.error(error));
91
+ ```
92
+
93
+ Both halves run against a real `ws` peer in this repository's suites, and the
94
+ constructors are plain functions returning state records: no `class`, no `this`,
95
+ no prototype.
96
+
97
+ ## Status
98
+
99
+ Option defaults match `ws`: `maxPayload` 100 MiB, `maxFragments` 16384, and
100
+ `closeTimeout` 30000 ms. The itemised matrix, with the owner module and the
101
+ evidence test behind every row, is [COMPATIBILITY.md](COMPATIBILITY.md).
102
+
103
+ | Area | State |
104
+ | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
105
+ | `WebSocketServer`: options, events, `handleUpgrade`, `shouldHandle`, `verifyClient` | Works, compared property for property against `ws` |
106
+ | Server-side `WebSocket`: properties, `on*` and DOM handlers, ready states | Works, with exactly-once `close` |
107
+ | `WebSocket` client: `net` and `tls`, redirects, `ws+unix:`, `unexpected-response` | Works against a real `ws` server |
108
+ | `send`, fragmentation, `binaryType`, `ping`/`pong`, close codes | Works both directions; `maxPayload` and `maxFragments` are enforced per connection |
109
+ | `permessage-deflate` | Works; context takeover is declined in both directions ([why](COMPATIBILITY.md#shared-with-the-engine)) |
110
+ | `createWebSocketStream`, `clientTracking`, `server.options` | Works, including the `WebSocket` class option |
111
+ | Engine route message cap, 64 KiB | A property of the build. [Two limits](#two-limits-worth-knowing) |
112
+ | Engine route inbound ring, 64 messages | Excess is dropped and counted. Same section |
113
+ | RFC 6455 Autobahn suite | Regression gate; 424 of 517 cases in the full run, 268 in the framing selection ([matrix](COMPATIBILITY.md#rfc-6455-conformance)) |
114
+ | Client `wss:` to `ws:` downgrade, redirect hop limit, `origin`, `handshakeTimeout` | Implemented with no behavioural test ([outstanding](COMPATIBILITY.md#what-is-still-outstanding)) |
115
+
116
+ ## Two limits worth knowing
117
+
118
+ **The 64 KiB message cap.** `message_capacity` in
119
+ `src/engine/server/capacities.zig` is a `comptime` constant, so it is baked into
120
+ the addon: `maxPayload` sizes each connection's own ceiling and does not lift this
121
+ one. 64 KiB is the largest payload the conformance suite puts on the wire, and a
122
+ frame above it is closed with 1009. Raising it costs about 22 MB per live server,
123
+ because the message slab, the write queue, and both staging rings scale with it.
124
+
125
+ **The 64-slot inbound ring.** The engine has no hook for stopping a read when its
126
+ consumer falls behind, so the ring in `src/engine/server/instance.zig` is the
127
+ only place a burst can be absorbed. It holds 64 messages, and a peer delivering
128
+ more before the Node main thread drains has the excess dropped and counted by
129
+ `serverDroppedMessages`. `ws` applies backpressure instead, so it has no cliff.
130
+
131
+ Both bind the engine route, which is µWebZockets' own listener. The codec route
132
+ the public `ws` surface uses is bound by neither: its buffers grow into each
133
+ connection's own `maxPayload` and `maxFragments`. `engineLimits()` is exported
134
+ from the package entry, so the compiled numbers are readable at runtime.
135
+
136
+ ## Commands
137
+
138
+ ```sh
139
+ pnpm build
140
+ pnpm test
141
+ pnpm test:compat
142
+ pnpm test:autobahn
143
+ pnpm bench
144
+ pnpm typecheck
145
+ pnpm lint
146
+ pnpm format:check
147
+ ```
148
+
149
+ ## Docs and licence
150
+
151
+ - [COMPATIBILITY.md](COMPATIBILITY.md): every `ws` surface item, its owner module, its status, and its evidence test.
152
+ - [CODEBASE.md](CODEBASE.md): repository layout, the two routes, the boundary, and the compiled capacities.
153
+ - [CODING_CONVENTION.md](CODING_CONVENTION.md): TypeScript and Zig style, the anti-OOP rules, and the module budget.
154
+ - [docs/migrating.md](docs/migrating.md): moving an existing `ws` application over.
155
+ - [CONTRIBUTE.md](CONTRIBUTE.md): environment setup, the script contract, and how to release.
156
+
157
+ MIT. See [LICENSE](LICENSE). `ws` is copyright Einar Otto Stangvik, Arnout
158
+ Kazemier, and contributors, MIT-licensed. µWebZockets is the first-party
159
+ MIT-licensed protocol engine written for this project. Provenance for everything
160
+ vendored or CI-only is in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
package/dist/index.cjs CHANGED
@@ -8,6 +8,7 @@ let node_module = require("node:module");
8
8
  let node_process = require("node:process");
9
9
  let node_path = require("node:path");
10
10
  let node_url = require("node:url");
11
+ let node_child_process = require("node:child_process");
11
12
  let node_util = require("node:util");
12
13
  let node_crypto = require("node:crypto");
13
14
  let node_https = require("node:https");
@@ -16,19 +17,95 @@ let node_stream = require("node:stream");
16
17
  function unloadableAddonMessage(path, platform, arch) {
17
18
  return `ventiws: the native addon at ${path} was found but could not be loaded, so it was built for a different ${platform}-${arch} or against a different libc. In a checkout the fix is "pnpm build:binding"; in an install, a package built for this platform. The loader's own error is the cause.`;
18
19
  }
19
- function missingAddonMessage(root, platform, arch) {
20
- const target = `${platform}-${arch}`;
21
- if (root === void 0) return `ventiws: the package layout is broken -- there is no package.json above the native addon, so the artifact for ${target} cannot be located. Reinstalling ventiws should restore it.`;
22
- return `ventiws: no native addon for ${target} was found under ${root}. An install ships the addon for one platform, so a ${target} machine needs ventiws built for ${target}, which needs the Zig toolchain (zig 0.16.0). If you are working in a checkout, the command is "pnpm build:binding".`;
20
+ function missingAddonMessage(root, platform, arch, target, published) {
21
+ const host = `${platform}-${arch}`;
22
+ if (root === void 0) return `ventiws: the package layout is broken -- there is no package.json above the native addon, so the artifact for ${host} cannot be located. Reinstalling ventiws should restore it.`;
23
+ if (target === void 0) return `ventiws: no published native addon for ${host}, so nothing was installed to load. A published ventiws ships a binary for ${published.join(", ")}. In a checkout, the fix is "pnpm build:binding", which builds the addon for this machine.`;
24
+ return `ventiws: the native addon for ${target} is not installed under ${root}. It is an optional dependency, so npm skipped it for ${host}; an install that resolves no binary for this host needs a reinstall, and a checkout needs "pnpm build:binding".`;
25
+ }
26
+ //#endregion
27
+ //#region src/binding/target.ts
28
+ const ADDON_SCOPE = "@ventiws";
29
+ const PUBLISHED_TARGETS = [
30
+ "linux-x64-gnu",
31
+ "linux-arm64-gnu",
32
+ "linux-x64-musl",
33
+ "darwin-x64",
34
+ "darwin-arm64"
35
+ ];
36
+ function bindingPackageName(target) {
37
+ return `${ADDON_SCOPE}/binding-${target}`;
38
+ }
39
+ function addonTarget(nodePlatform, nodeArch) {
40
+ if (nodePlatform === "darwin") return darwinTarget(nodeArch);
41
+ if (nodePlatform !== "linux") return void 0;
42
+ const libc = isMuslLibc() ? "musl" : "gnu";
43
+ if (nodeArch === "x64") return `linux-x64-${libc}`;
44
+ if (nodeArch === "arm64" && libc === "gnu") return "linux-arm64-gnu";
45
+ }
46
+ function darwinTarget(nodeArch) {
47
+ if (nodeArch === "x64") return "darwin-x64";
48
+ if (nodeArch === "arm64") return "darwin-arm64";
49
+ }
50
+ function isMuslLibc() {
51
+ const facts = reportFacts();
52
+ if (facts?.glibcVersionRuntime !== void 0) return false;
53
+ if (isMuslSharedObject(facts?.sharedObjects)) return true;
54
+ return probeMuslBinary();
55
+ }
56
+ function reportFacts() {
57
+ if (typeof process.report?.getReport !== "function") return void 0;
58
+ const report = asRecord(asRecord(process.report.getReport()));
59
+ if (report === void 0) return void 0;
60
+ const facts = {};
61
+ const glibc = asRecord(report.header)?.glibcVersionRuntime;
62
+ if (typeof glibc === "string") facts.glibcVersionRuntime = glibc;
63
+ const objects = report.sharedObjects;
64
+ if (Array.isArray(objects)) facts.sharedObjects = objects.filter((entry) => typeof entry === "string");
65
+ return facts;
66
+ }
67
+ function asRecord(value) {
68
+ if (typeof value === "string") return parsedJson(value);
69
+ if (typeof value !== "object" || value === null) return void 0;
70
+ return value;
71
+ }
72
+ function parsedJson(source) {
73
+ try {
74
+ return asRecord(JSON.parse(source));
75
+ } catch {
76
+ return;
77
+ }
78
+ }
79
+ function isMuslSharedObject(sharedObjects) {
80
+ if (sharedObjects === void 0) return false;
81
+ return sharedObjects.some((entry) => entry.includes("libc.musl-") || entry.includes("ld-musl-"));
82
+ }
83
+ function probeMuslBinary() {
84
+ try {
85
+ return (0, node_fs.readFileSync)("/usr/bin/ldd", "utf8").includes("musl");
86
+ } catch {
87
+ return probeLddVersion();
88
+ }
89
+ }
90
+ function probeLddVersion() {
91
+ try {
92
+ return (0, node_child_process.execFileSync)("ldd", ["--version"], { encoding: "utf8" }).includes("musl");
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+ function hostAddonTarget() {
98
+ return addonTarget(node_process.platform, node_process.arch);
23
99
  }
24
100
  //#endregion
25
101
  //#region src/binding/load.ts
26
102
  const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
27
- const candidatePaths = [[
103
+ const checkoutPath = [
28
104
  "zig-out",
29
105
  "lib",
30
106
  "ventiws.node"
31
- ], ["dist", "ventiws.node"]];
107
+ ];
108
+ const legacyPath = ["dist", "ventiws.node"];
32
109
  function findPackageRoot(start) {
33
110
  let current = start;
34
111
  while (current !== void 0) {
@@ -37,25 +114,51 @@ function findPackageRoot(start) {
37
114
  current = parent === current ? void 0 : parent;
38
115
  }
39
116
  }
40
- function resolveAddonPath() {
41
- const root = findPackageRoot((0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href)));
42
- for (const parts of candidatePaths) {
43
- if (root === void 0) continue;
44
- const candidate = (0, node_path.join)(root, ...parts);
45
- if ((0, node_fs.existsSync)(candidate)) return candidate;
117
+ function packageRoot() {
118
+ return findPackageRoot((0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href)));
119
+ }
120
+ function checkoutArtifact(root) {
121
+ if (root === void 0) return void 0;
122
+ const candidate = (0, node_path.join)(root, ...checkoutPath);
123
+ return (0, node_fs.existsSync)(candidate) ? candidate : void 0;
124
+ }
125
+ function legacyArtifact(root) {
126
+ if (root === void 0) return void 0;
127
+ const candidate = (0, node_path.join)(root, ...legacyPath);
128
+ return (0, node_fs.existsSync)(candidate) ? candidate : void 0;
129
+ }
130
+ function installedBinding(target) {
131
+ try {
132
+ return require$1.resolve(bindingPackageName(target));
133
+ } catch {
134
+ return;
46
135
  }
47
- throw new Error(missingAddonMessage(root, node_process.platform, node_process.arch));
48
136
  }
49
137
  let addon;
50
138
  function loadAddon() {
51
139
  if (addon !== void 0) return addon;
52
- const path = resolveAddonPath();
140
+ addon = resolveAddon();
141
+ return addon;
142
+ }
143
+ function resolveAddon() {
144
+ const root = packageRoot();
145
+ const target = hostAddonTarget();
146
+ const checkout = checkoutArtifact(root);
147
+ if (checkout !== void 0) return requireAddon$1(checkout);
148
+ if (target !== void 0) {
149
+ const installed = installedBinding(target);
150
+ if (installed !== void 0) return requireAddon$1(installed);
151
+ }
152
+ const legacy = legacyArtifact(root);
153
+ if (legacy !== void 0) return requireAddon$1(legacy);
154
+ throw new Error(missingAddonMessage(root, node_process.platform, node_process.arch, target, PUBLISHED_TARGETS));
155
+ }
156
+ function requireAddon$1(path) {
53
157
  try {
54
- addon = require$1(path);
158
+ return require$1(path);
55
159
  } catch (cause) {
56
160
  throw Object.assign(new Error(unloadableAddonMessage(path, node_process.platform, node_process.arch)), { cause });
57
161
  }
58
- return addon;
59
162
  }
60
163
  //#endregion
61
164
  //#region src/compat/events/registry.ts
package/dist/index.d.cts CHANGED
@@ -71,15 +71,15 @@ declare class WebSocket$1 extends EventEmitter {
71
71
  fin?: boolean | undefined;
72
72
  }, cb?: (err?: Error) => void): void;
73
73
  terminate(): void;
74
- /**
75
- * Pause the websocket causing it to stop emitting events. Some events can still be
76
- * emitted after this is called, until all buffered data is consumed. This method
77
- * is a noop if the ready state is `CONNECTING` or `CLOSED`.
74
+ /**
75
+ * Pause the websocket causing it to stop emitting events. Some events can still be
76
+ * emitted after this is called, until all buffered data is consumed. This method
77
+ * is a noop if the ready state is `CONNECTING` or `CLOSED`.
78
78
  */
79
79
  pause(): void;
80
- /**
81
- * Make a paused socket resume emitting events. This method is a noop if the ready
82
- * state is `CONNECTING` or `CLOSED`.
80
+ /**
81
+ * Make a paused socket resume emitting events. This method is a noop if the ready
82
+ * state is `CONNECTING` or `CLOSED`.
83
83
  */
84
84
  resume(): void;
85
85
  // HTML5 WebSocket events
@@ -143,42 +143,42 @@ declare class WebSocket$1 extends EventEmitter {
143
143
  declare const WebSocketAlias: typeof WebSocket$1;
144
144
  interface WebSocketAlias extends WebSocket$1 {} // eslint-disable-line @typescript-eslint/no-empty-interface
145
145
  declare namespace WebSocket$1 {
146
- /**
147
- * Data represents the raw message payload received over the WebSocket.
146
+ /**
147
+ * Data represents the raw message payload received over the WebSocket.
148
148
  */
149
149
  type RawData = Buffer | ArrayBuffer | Buffer[];
150
- /**
151
- * Data represents the message payload received over the WebSocket.
150
+ /**
151
+ * Data represents the message payload received over the WebSocket.
152
152
  */
153
153
  type Data = string | Buffer | ArrayBuffer | Buffer[];
154
- /**
155
- * CertMeta represents the accepted types for certificate & key data.
154
+ /**
155
+ * CertMeta represents the accepted types for certificate & key data.
156
156
  */
157
157
  type CertMeta = string | string[] | Buffer | Buffer[];
158
- /**
159
- * VerifyClientCallbackSync is a synchronous callback used to inspect the
160
- * incoming message. The return value (boolean) of the function determines
161
- * whether or not to accept the handshake.
158
+ /**
159
+ * VerifyClientCallbackSync is a synchronous callback used to inspect the
160
+ * incoming message. The return value (boolean) of the function determines
161
+ * whether or not to accept the handshake.
162
162
  */
163
163
  type VerifyClientCallbackSync<Request extends IncomingMessage = IncomingMessage> = (info: {
164
164
  origin: string;
165
165
  secure: boolean;
166
166
  req: Request;
167
167
  }) => boolean;
168
- /**
169
- * VerifyClientCallbackAsync is an asynchronous callback used to inspect the
170
- * incoming message. The return value (boolean) of the function determines
171
- * whether or not to accept the handshake.
168
+ /**
169
+ * VerifyClientCallbackAsync is an asynchronous callback used to inspect the
170
+ * incoming message. The return value (boolean) of the function determines
171
+ * whether or not to accept the handshake.
172
172
  */
173
173
  type VerifyClientCallbackAsync<Request extends IncomingMessage = IncomingMessage> = (info: {
174
174
  origin: string;
175
175
  secure: boolean;
176
176
  req: Request;
177
177
  }, callback: (res: boolean, code?: number, message?: string, headers?: OutgoingHttpHeaders) => void) => void;
178
- /**
179
- * FinishRequestCallback is a callback for last minute customization of the
180
- * headers. If finishRequest is set, then it has the responsibility to call
181
- * request.end() once it is done setting request headers.
178
+ /**
179
+ * FinishRequestCallback is a callback for last minute customization of the
180
+ * headers. If finishRequest is set, then it has the responsibility to call
181
+ * request.end() once it is done setting request headers.
182
182
  */
183
183
  type FinishRequestCallback = (request: ClientRequest, websocket: WebSocket$1) => void;
184
184
  interface ClientOptions extends SecureContextOptions {
package/dist/index.d.mts CHANGED
@@ -72,15 +72,15 @@ declare class WebSocket$1 extends EventEmitter {
72
72
  fin?: boolean | undefined;
73
73
  }, cb?: (err?: Error) => void): void;
74
74
  terminate(): void;
75
- /**
76
- * Pause the websocket causing it to stop emitting events. Some events can still be
77
- * emitted after this is called, until all buffered data is consumed. This method
78
- * is a noop if the ready state is `CONNECTING` or `CLOSED`.
75
+ /**
76
+ * Pause the websocket causing it to stop emitting events. Some events can still be
77
+ * emitted after this is called, until all buffered data is consumed. This method
78
+ * is a noop if the ready state is `CONNECTING` or `CLOSED`.
79
79
  */
80
80
  pause(): void;
81
- /**
82
- * Make a paused socket resume emitting events. This method is a noop if the ready
83
- * state is `CONNECTING` or `CLOSED`.
81
+ /**
82
+ * Make a paused socket resume emitting events. This method is a noop if the ready
83
+ * state is `CONNECTING` or `CLOSED`.
84
84
  */
85
85
  resume(): void;
86
86
  // HTML5 WebSocket events
@@ -144,42 +144,42 @@ declare class WebSocket$1 extends EventEmitter {
144
144
  declare const WebSocketAlias: typeof WebSocket$1;
145
145
  interface WebSocketAlias extends WebSocket$1 {} // eslint-disable-line @typescript-eslint/no-empty-interface
146
146
  declare namespace WebSocket$1 {
147
- /**
148
- * Data represents the raw message payload received over the WebSocket.
147
+ /**
148
+ * Data represents the raw message payload received over the WebSocket.
149
149
  */
150
150
  type RawData = Buffer | ArrayBuffer | Buffer[];
151
- /**
152
- * Data represents the message payload received over the WebSocket.
151
+ /**
152
+ * Data represents the message payload received over the WebSocket.
153
153
  */
154
154
  type Data = string | Buffer | ArrayBuffer | Buffer[];
155
- /**
156
- * CertMeta represents the accepted types for certificate & key data.
155
+ /**
156
+ * CertMeta represents the accepted types for certificate & key data.
157
157
  */
158
158
  type CertMeta = string | string[] | Buffer | Buffer[];
159
- /**
160
- * VerifyClientCallbackSync is a synchronous callback used to inspect the
161
- * incoming message. The return value (boolean) of the function determines
162
- * whether or not to accept the handshake.
159
+ /**
160
+ * VerifyClientCallbackSync is a synchronous callback used to inspect the
161
+ * incoming message. The return value (boolean) of the function determines
162
+ * whether or not to accept the handshake.
163
163
  */
164
164
  type VerifyClientCallbackSync<Request extends IncomingMessage = IncomingMessage> = (info: {
165
165
  origin: string;
166
166
  secure: boolean;
167
167
  req: Request;
168
168
  }) => boolean;
169
- /**
170
- * VerifyClientCallbackAsync is an asynchronous callback used to inspect the
171
- * incoming message. The return value (boolean) of the function determines
172
- * whether or not to accept the handshake.
169
+ /**
170
+ * VerifyClientCallbackAsync is an asynchronous callback used to inspect the
171
+ * incoming message. The return value (boolean) of the function determines
172
+ * whether or not to accept the handshake.
173
173
  */
174
174
  type VerifyClientCallbackAsync<Request extends IncomingMessage = IncomingMessage> = (info: {
175
175
  origin: string;
176
176
  secure: boolean;
177
177
  req: Request;
178
178
  }, callback: (res: boolean, code?: number, message?: string, headers?: OutgoingHttpHeaders) => void) => void;
179
- /**
180
- * FinishRequestCallback is a callback for last minute customization of the
181
- * headers. If finishRequest is set, then it has the responsibility to call
182
- * request.end() once it is done setting request headers.
179
+ /**
180
+ * FinishRequestCallback is a callback for last minute customization of the
181
+ * headers. If finishRequest is set, then it has the responsibility to call
182
+ * request.end() once it is done setting request headers.
183
183
  */
184
184
  type FinishRequestCallback = (request: ClientRequest, websocket: WebSocket$1) => void;
185
185
  interface ClientOptions extends SecureContextOptions {
package/dist/index.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { STATUS_CODES, createServer, request } from "node:http";
3
- import { existsSync } from "node:fs";
3
+ import { existsSync, readFileSync } from "node:fs";
4
4
  import { arch, platform } from "node:process";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
+ import { execFileSync } from "node:child_process";
7
8
  import { inspect } from "node:util";
8
9
  import { createHash, randomBytes } from "node:crypto";
9
10
  import { request as request$1 } from "node:https";
@@ -12,19 +13,95 @@ import { Duplex } from "node:stream";
12
13
  function unloadableAddonMessage(path, platform, arch) {
13
14
  return `ventiws: the native addon at ${path} was found but could not be loaded, so it was built for a different ${platform}-${arch} or against a different libc. In a checkout the fix is "pnpm build:binding"; in an install, a package built for this platform. The loader's own error is the cause.`;
14
15
  }
15
- function missingAddonMessage(root, platform, arch) {
16
- const target = `${platform}-${arch}`;
17
- if (root === void 0) return `ventiws: the package layout is broken -- there is no package.json above the native addon, so the artifact for ${target} cannot be located. Reinstalling ventiws should restore it.`;
18
- return `ventiws: no native addon for ${target} was found under ${root}. An install ships the addon for one platform, so a ${target} machine needs ventiws built for ${target}, which needs the Zig toolchain (zig 0.16.0). If you are working in a checkout, the command is "pnpm build:binding".`;
16
+ function missingAddonMessage(root, platform, arch, target, published) {
17
+ const host = `${platform}-${arch}`;
18
+ if (root === void 0) return `ventiws: the package layout is broken -- there is no package.json above the native addon, so the artifact for ${host} cannot be located. Reinstalling ventiws should restore it.`;
19
+ if (target === void 0) return `ventiws: no published native addon for ${host}, so nothing was installed to load. A published ventiws ships a binary for ${published.join(", ")}. In a checkout, the fix is "pnpm build:binding", which builds the addon for this machine.`;
20
+ return `ventiws: the native addon for ${target} is not installed under ${root}. It is an optional dependency, so npm skipped it for ${host}; an install that resolves no binary for this host needs a reinstall, and a checkout needs "pnpm build:binding".`;
21
+ }
22
+ //#endregion
23
+ //#region src/binding/target.ts
24
+ const ADDON_SCOPE = "@ventiws";
25
+ const PUBLISHED_TARGETS = [
26
+ "linux-x64-gnu",
27
+ "linux-arm64-gnu",
28
+ "linux-x64-musl",
29
+ "darwin-x64",
30
+ "darwin-arm64"
31
+ ];
32
+ function bindingPackageName(target) {
33
+ return `${ADDON_SCOPE}/binding-${target}`;
34
+ }
35
+ function addonTarget(nodePlatform, nodeArch) {
36
+ if (nodePlatform === "darwin") return darwinTarget(nodeArch);
37
+ if (nodePlatform !== "linux") return void 0;
38
+ const libc = isMuslLibc() ? "musl" : "gnu";
39
+ if (nodeArch === "x64") return `linux-x64-${libc}`;
40
+ if (nodeArch === "arm64" && libc === "gnu") return "linux-arm64-gnu";
41
+ }
42
+ function darwinTarget(nodeArch) {
43
+ if (nodeArch === "x64") return "darwin-x64";
44
+ if (nodeArch === "arm64") return "darwin-arm64";
45
+ }
46
+ function isMuslLibc() {
47
+ const facts = reportFacts();
48
+ if (facts?.glibcVersionRuntime !== void 0) return false;
49
+ if (isMuslSharedObject(facts?.sharedObjects)) return true;
50
+ return probeMuslBinary();
51
+ }
52
+ function reportFacts() {
53
+ if (typeof process.report?.getReport !== "function") return void 0;
54
+ const report = asRecord(asRecord(process.report.getReport()));
55
+ if (report === void 0) return void 0;
56
+ const facts = {};
57
+ const glibc = asRecord(report.header)?.glibcVersionRuntime;
58
+ if (typeof glibc === "string") facts.glibcVersionRuntime = glibc;
59
+ const objects = report.sharedObjects;
60
+ if (Array.isArray(objects)) facts.sharedObjects = objects.filter((entry) => typeof entry === "string");
61
+ return facts;
62
+ }
63
+ function asRecord(value) {
64
+ if (typeof value === "string") return parsedJson(value);
65
+ if (typeof value !== "object" || value === null) return void 0;
66
+ return value;
67
+ }
68
+ function parsedJson(source) {
69
+ try {
70
+ return asRecord(JSON.parse(source));
71
+ } catch {
72
+ return;
73
+ }
74
+ }
75
+ function isMuslSharedObject(sharedObjects) {
76
+ if (sharedObjects === void 0) return false;
77
+ return sharedObjects.some((entry) => entry.includes("libc.musl-") || entry.includes("ld-musl-"));
78
+ }
79
+ function probeMuslBinary() {
80
+ try {
81
+ return readFileSync("/usr/bin/ldd", "utf8").includes("musl");
82
+ } catch {
83
+ return probeLddVersion();
84
+ }
85
+ }
86
+ function probeLddVersion() {
87
+ try {
88
+ return execFileSync("ldd", ["--version"], { encoding: "utf8" }).includes("musl");
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+ function hostAddonTarget() {
94
+ return addonTarget(platform, arch);
19
95
  }
20
96
  //#endregion
21
97
  //#region src/binding/load.ts
22
98
  const require = createRequire(import.meta.url);
23
- const candidatePaths = [[
99
+ const checkoutPath = [
24
100
  "zig-out",
25
101
  "lib",
26
102
  "ventiws.node"
27
- ], ["dist", "ventiws.node"]];
103
+ ];
104
+ const legacyPath = ["dist", "ventiws.node"];
28
105
  function findPackageRoot(start) {
29
106
  let current = start;
30
107
  while (current !== void 0) {
@@ -33,25 +110,51 @@ function findPackageRoot(start) {
33
110
  current = parent === current ? void 0 : parent;
34
111
  }
35
112
  }
36
- function resolveAddonPath() {
37
- const root = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
38
- for (const parts of candidatePaths) {
39
- if (root === void 0) continue;
40
- const candidate = join(root, ...parts);
41
- if (existsSync(candidate)) return candidate;
113
+ function packageRoot() {
114
+ return findPackageRoot(dirname(fileURLToPath(import.meta.url)));
115
+ }
116
+ function checkoutArtifact(root) {
117
+ if (root === void 0) return void 0;
118
+ const candidate = join(root, ...checkoutPath);
119
+ return existsSync(candidate) ? candidate : void 0;
120
+ }
121
+ function legacyArtifact(root) {
122
+ if (root === void 0) return void 0;
123
+ const candidate = join(root, ...legacyPath);
124
+ return existsSync(candidate) ? candidate : void 0;
125
+ }
126
+ function installedBinding(target) {
127
+ try {
128
+ return require.resolve(bindingPackageName(target));
129
+ } catch {
130
+ return;
42
131
  }
43
- throw new Error(missingAddonMessage(root, platform, arch));
44
132
  }
45
133
  let addon;
46
134
  function loadAddon() {
47
135
  if (addon !== void 0) return addon;
48
- const path = resolveAddonPath();
136
+ addon = resolveAddon();
137
+ return addon;
138
+ }
139
+ function resolveAddon() {
140
+ const root = packageRoot();
141
+ const target = hostAddonTarget();
142
+ const checkout = checkoutArtifact(root);
143
+ if (checkout !== void 0) return requireAddon$1(checkout);
144
+ if (target !== void 0) {
145
+ const installed = installedBinding(target);
146
+ if (installed !== void 0) return requireAddon$1(installed);
147
+ }
148
+ const legacy = legacyArtifact(root);
149
+ if (legacy !== void 0) return requireAddon$1(legacy);
150
+ throw new Error(missingAddonMessage(root, platform, arch, target, PUBLISHED_TARGETS));
151
+ }
152
+ function requireAddon$1(path) {
49
153
  try {
50
- addon = require(path);
154
+ return require(path);
51
155
  } catch (cause) {
52
156
  throw Object.assign(new Error(unloadableAddonMessage(path, platform, arch)), { cause });
53
157
  }
54
- return addon;
55
158
  }
56
159
  //#endregion
57
160
  //#region src/compat/events/registry.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ventiws",
3
- "version": "1.0.0-alpha",
3
+ "version": "1.0.0-alpha.10",
4
4
  "description": "A drop-in replacement for ws for Node.js, built on the first-party µWebZockets Zig engine via napi-zig.",
5
5
  "homepage": "https://github.com/farbenbuilds/ventiws#readme",
6
6
  "bugs": {
@@ -18,51 +18,22 @@
18
18
  "type": "module",
19
19
  "main": "./dist/index.cjs",
20
20
  "module": "./dist/index.mjs",
21
- "types": "./dist/index.d.mts",
21
+ "types": "./dist/index.d.cts",
22
22
  "exports": {
23
23
  ".": {
24
- "types": {
25
- "import": "./dist/index.d.mts",
26
- "require": "./dist/index.d.cts"
27
- },
28
24
  "import": "./dist/index.mjs",
29
25
  "require": "./dist/index.cjs"
30
26
  },
31
27
  "./package.json": "./package.json"
32
28
  },
33
- "devDependencies": {
34
- "@types/node": "^26.6.3",
35
- "@types/ws": "8.18.1",
36
- "bumpp": "^12.3.0",
37
- "lefthook": "^2.1.14",
38
- "napi-zig": "^0.2.8",
39
- "oxfmt": "^0.70.0",
40
- "oxlint": "^1.85.0",
41
- "tsdown": "^0.23.0",
42
- "typescript": "^7.0.2",
43
- "vite": "^8.3.0",
44
- "vitest": "^5.0.2",
45
- "ws": "8.21.3"
46
- },
47
29
  "engines": {
48
30
  "node": ">=22.12.0"
49
31
  },
50
- "scripts": {
51
- "build": "pnpm run build:binding && tsdown && node scripts/finalize-exports.mjs && pnpm run typecheck:dist",
52
- "build:binding": "napi-zig build --optimize safe",
53
- "dev": "tsdown --watch",
54
- "format": "oxfmt",
55
- "format:check": "oxfmt --check",
56
- "lint": "oxlint && node scripts/check-conventions.mjs",
57
- "finalize:exports": "node scripts/finalize-exports.mjs",
58
- "lint:fix": "oxlint --fix",
59
- "test": "pnpm run build:binding && vitest run --no-file-parallelism",
60
- "test:watch": "pnpm run build:binding && vitest",
61
- "test:compat": "vitest run tests/conformance",
62
- "test:autobahn": "node tests/autobahn/run.ts",
63
- "bench": "node bench/index.ts",
64
- "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
65
- "typecheck:dist": "tsc -p tsconfig.dist-types.json",
66
- "release": "bumpp"
32
+ "optionalDependencies": {
33
+ "@ventiws/binding-darwin-arm64": "1.0.0-alpha.10",
34
+ "@ventiws/binding-darwin-x64": "1.0.0-alpha.10",
35
+ "@ventiws/binding-linux-arm64-gnu": "1.0.0-alpha.10",
36
+ "@ventiws/binding-linux-x64-gnu": "1.0.0-alpha.10",
37
+ "@ventiws/binding-linux-x64-musl": "1.0.0-alpha.10"
67
38
  }
68
39
  }
package/dist/ventiws.node DELETED
Binary file