crossws 0.4.6 → 0.4.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/dist/index.mjs CHANGED
@@ -16,19 +16,8 @@ function createWebSocketProxy(target) {
16
16
  return { headers: { "sec-websocket-protocol": accepted } };
17
17
  },
18
18
  open(peer) {
19
- let ws;
20
- try {
21
- const url = _resolveTarget(options.target, peer);
22
- const protocols = _resolveProtocols(peer, options.forwardProtocol);
23
- const wsOptions = _resolveWsOptions(options.headers, peer);
24
- ws = wsOptions ? new WebSocketCtor(url, protocols, wsOptions) : new WebSocketCtor(url, protocols);
25
- ws.binaryType = "arraybuffer";
26
- } catch {
27
- _safeClose(peer, 1011, "Upstream setup failed");
28
- return;
29
- }
30
19
  const state = {
31
- ws,
20
+ ws: void 0,
32
21
  buffer: [],
33
22
  bufferSize: 0,
34
23
  open: false,
@@ -41,26 +30,20 @@ function createWebSocketProxy(target) {
41
30
  _cleanupState(upstreams, peer.id, state);
42
31
  _safeClose(peer, 1011, "Upstream connect timeout");
43
32
  }, timeoutMs);
44
- ws.addEventListener("open", () => {
45
- _clearTimeout(state);
46
- state.open = true;
47
- for (const data of state.buffer) ws.send(data);
48
- state.buffer.length = 0;
49
- state.bufferSize = 0;
50
- });
51
- ws.addEventListener("message", (event) => {
52
- _safeSend(peer, event.data);
53
- });
54
- ws.addEventListener("close", (event) => {
55
- if (upstreams.get(peer.id) !== state) return;
33
+ let resolved;
34
+ try {
35
+ resolved = _resolveTarget(options.target, peer);
36
+ } catch {
56
37
  _cleanupState(upstreams, peer.id, state);
57
- _safeClose(peer, _remapIncomingCode(event.code), event.reason);
58
- });
59
- ws.addEventListener("error", () => {
38
+ _safeClose(peer, 1011, "Upstream setup failed");
39
+ return;
40
+ }
41
+ if (resolved instanceof Promise) resolved.then((url) => _dialUpstream(upstreams, peer, state, url, options, WebSocketCtor), () => {
60
42
  if (upstreams.get(peer.id) !== state) return;
61
43
  _cleanupState(upstreams, peer.id, state);
62
- _safeClose(peer, 1011, "Upstream error");
44
+ _safeClose(peer, 1011, "Upstream setup failed");
63
45
  });
46
+ else _dialUpstream(upstreams, peer, state, resolved, options, WebSocketCtor);
64
47
  },
65
48
  message(peer, message) {
66
49
  const state = upstreams.get(peer.id);
@@ -68,7 +51,7 @@ function createWebSocketProxy(target) {
68
51
  const raw = typeof message.rawData === "string" ? message.rawData : message.uint8Array();
69
52
  if (state.open) {
70
53
  try {
71
- state.ws.send(raw);
54
+ state.ws?.send(raw);
72
55
  } catch {}
73
56
  return;
74
57
  }
@@ -88,7 +71,7 @@ function createWebSocketProxy(target) {
88
71
  _clearTimeout(state);
89
72
  upstreams.delete(peer.id);
90
73
  try {
91
- state.ws.close(_normalizeOutgoingCode(details.code), _truncateReason(details.reason));
74
+ state.ws?.close(_normalizeOutgoingCode(details.code), _truncateReason(details.reason));
92
75
  } catch {}
93
76
  },
94
77
  error(peer) {
@@ -97,16 +80,56 @@ function createWebSocketProxy(target) {
97
80
  _clearTimeout(state);
98
81
  upstreams.delete(peer.id);
99
82
  try {
100
- state.ws.close(1011, "Peer error");
83
+ state.ws?.close(1011, "Peer error");
101
84
  } catch {}
102
85
  }
103
86
  };
104
87
  }
88
+ function _dialUpstream(upstreams, peer, state, url, options, WebSocketCtor) {
89
+ if (upstreams.get(peer.id) !== state) return;
90
+ let ws;
91
+ try {
92
+ const protocols = _resolveProtocols(peer, options.forwardProtocol);
93
+ const wsOptions = _resolveWsOptions(options.headers, peer);
94
+ ws = wsOptions ? new WebSocketCtor(url, protocols, wsOptions) : new WebSocketCtor(url, protocols);
95
+ ws.binaryType = "arraybuffer";
96
+ } catch {
97
+ _cleanupState(upstreams, peer.id, state);
98
+ _safeClose(peer, 1011, "Upstream setup failed");
99
+ return;
100
+ }
101
+ state.ws = ws;
102
+ ws.addEventListener("open", () => {
103
+ if (upstreams.get(peer.id) !== state) return;
104
+ _clearTimeout(state);
105
+ state.open = true;
106
+ try {
107
+ for (const data of state.buffer) ws.send(data);
108
+ } catch {} finally {
109
+ state.buffer.length = 0;
110
+ state.bufferSize = 0;
111
+ }
112
+ });
113
+ ws.addEventListener("message", (event) => {
114
+ if (upstreams.get(peer.id) !== state) return;
115
+ _safeSend(peer, event.data);
116
+ });
117
+ ws.addEventListener("close", (event) => {
118
+ if (upstreams.get(peer.id) !== state) return;
119
+ _cleanupState(upstreams, peer.id, state);
120
+ _safeClose(peer, _remapIncomingCode(event.code), event.reason);
121
+ });
122
+ ws.addEventListener("error", () => {
123
+ if (upstreams.get(peer.id) !== state) return;
124
+ _cleanupState(upstreams, peer.id, state);
125
+ _safeClose(peer, 1011, "Upstream error");
126
+ });
127
+ }
105
128
  function _cleanupState(upstreams, id, state) {
106
129
  _clearTimeout(state);
107
130
  upstreams.delete(id);
108
131
  try {
109
- state.ws.close();
132
+ state.ws?.close();
110
133
  } catch {}
111
134
  }
112
135
  function _clearTimeout(state) {
@@ -117,8 +140,12 @@ function _clearTimeout(state) {
117
140
  }
118
141
  function _resolveTarget(target, peer) {
119
142
  const raw = typeof target === "function" ? target(peer) : target;
143
+ if (_isThenable(raw)) return Promise.resolve(raw).then((value) => value instanceof URL ? value : new URL(value));
120
144
  return raw instanceof URL ? raw : new URL(raw);
121
145
  }
146
+ function _isThenable(value) {
147
+ return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
148
+ }
122
149
  function _resolveWsOptions(headers, peer) {
123
150
  if (!headers) return;
124
151
  const resolved = typeof headers === "function" ? headers(peer) : headers;
@@ -0,0 +1,2 @@
1
+ import { PostgresClientLike, RedisClientLike, SyncAdapter, SyncDriver, SyncDriverName, SyncDriverOptions, SyncMessage, broadcastChannel, cluster, decodeEnvelope, encodeEnvelope, pgsql, redis, setupPrimaryCluster, syncDrivers } from "./_chunks/adapter.mjs";
2
+ export { type PostgresClientLike, type RedisClientLike, type SyncAdapter, type SyncDriver, SyncDriverName, SyncDriverOptions, type SyncMessage, broadcastChannel, cluster, decodeEnvelope, encodeEnvelope, pgsql, redis, setupPrimaryCluster, syncDrivers };
package/dist/sync.mjs ADDED
@@ -0,0 +1,200 @@
1
+ function broadcastChannel(opts) {
2
+ return ({ id }) => {
3
+ const channel = new BroadcastChannel(opts.channel);
4
+ return {
5
+ subscribe(deliver) {
6
+ channel.addEventListener("message", (event) => {
7
+ const envelope = event.data;
8
+ const msg = envelope?.msg;
9
+ if (!envelope || envelope.id === id || typeof msg?.topic !== "string" || typeof msg.namespace !== "string" || !(typeof msg.data === "string" || msg.data instanceof Uint8Array)) return;
10
+ deliver(msg);
11
+ });
12
+ },
13
+ publish(msg) {
14
+ channel.postMessage({
15
+ id,
16
+ msg
17
+ });
18
+ },
19
+ close() {
20
+ channel.close();
21
+ }
22
+ };
23
+ };
24
+ }
25
+ function encodeEnvelope(id, msg) {
26
+ const binary = msg.data instanceof Uint8Array;
27
+ return JSON.stringify({
28
+ id,
29
+ msg: {
30
+ namespace: msg.namespace,
31
+ topic: msg.topic,
32
+ binary,
33
+ data: binary ? toBase64(msg.data) : msg.data
34
+ }
35
+ });
36
+ }
37
+ function decodeEnvelope(raw) {
38
+ try {
39
+ const parsed = JSON.parse(raw);
40
+ if (!parsed || typeof parsed.id !== "string" || !parsed.msg || typeof parsed.msg.topic !== "string" || typeof parsed.msg.namespace !== "string" || typeof parsed.msg.data !== "string") return;
41
+ return {
42
+ id: parsed.id,
43
+ msg: {
44
+ namespace: parsed.msg.namespace,
45
+ topic: parsed.msg.topic,
46
+ data: parsed.msg.binary ? fromBase64(parsed.msg.data) : parsed.msg.data
47
+ }
48
+ };
49
+ } catch {
50
+ return;
51
+ }
52
+ }
53
+ function toBase64(data) {
54
+ let binary = "";
55
+ for (const byte of data) binary += String.fromCharCode(byte);
56
+ return btoa(binary);
57
+ }
58
+ function fromBase64(data) {
59
+ const binary = atob(data);
60
+ const out = new Uint8Array(binary.length);
61
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
62
+ return out;
63
+ }
64
+ function redis(opts) {
65
+ const channel = opts.channel;
66
+ const isNodeRedis = opts.connector === void 0 ? typeof opts.client.pSubscribe === "function" : opts.connector === "node-redis";
67
+ return ({ id }) => {
68
+ const subscriber = opts.client.duplicate();
69
+ let started = false;
70
+ let onMessage;
71
+ return {
72
+ async subscribe(deliver) {
73
+ started = true;
74
+ const handle = (raw) => {
75
+ const envelope = decodeEnvelope(raw);
76
+ if (!envelope || envelope.id === id) return;
77
+ deliver(envelope.msg);
78
+ };
79
+ if (isNodeRedis) {
80
+ await subscriber.connect?.();
81
+ await subscriber.subscribe(channel, (raw) => handle(raw));
82
+ } else {
83
+ onMessage = (ch, raw) => {
84
+ if (ch === channel) handle(raw);
85
+ };
86
+ subscriber.on?.("message", onMessage);
87
+ await subscriber.subscribe(channel);
88
+ }
89
+ },
90
+ async publish(msg) {
91
+ await opts.client.publish(channel, encodeEnvelope(id, msg));
92
+ },
93
+ async close() {
94
+ if (!started) return;
95
+ if (onMessage) {
96
+ subscriber.off?.("message", onMessage);
97
+ onMessage = void 0;
98
+ }
99
+ try {
100
+ await subscriber.quit?.();
101
+ } catch (error) {
102
+ console.error("[crossws] sync redis close failed:", error);
103
+ }
104
+ }
105
+ };
106
+ };
107
+ }
108
+ function pgsql(opts) {
109
+ const channel = opts.channel;
110
+ if (new TextEncoder().encode(channel).length > 63) throw new Error(`[crossws] pgsql sync channel name must be at most 63 bytes (got ${channel.length} chars): ${channel}`);
111
+ const isPostgresJs = opts.connector === void 0 ? typeof opts.client.listen === "function" : opts.connector === "postgres.js";
112
+ if (!isPostgresJs && "idleCount" in opts.client && "totalCount" in opts.client && typeof opts.client.query === "function") throw new Error("[crossws] pgsql sync requires a dedicated `Client`, not a `Pool` (pool connections rotate, so LISTEN/NOTIFY can't deliver). Pass `new Client()` (node-postgres) or a `postgres()` instance (postgres.js).");
113
+ const quotedChannel = `"${channel.replace(/"/g, "\"\"")}"`;
114
+ return ({ id }) => {
115
+ let unlisten;
116
+ let onNotification;
117
+ return {
118
+ async subscribe(deliver) {
119
+ const handle = (raw) => {
120
+ const envelope = decodeEnvelope(raw);
121
+ if (!envelope || envelope.id === id) return;
122
+ deliver(envelope.msg);
123
+ };
124
+ if (isPostgresJs) unlisten = (await opts.client.listen(channel, (payload) => handle(payload)))?.unlisten;
125
+ else {
126
+ onNotification = (msg) => {
127
+ if (msg.channel === channel && msg.payload !== void 0) handle(msg.payload);
128
+ };
129
+ opts.client.on("notification", onNotification);
130
+ await opts.client.query(`LISTEN ${quotedChannel}`);
131
+ }
132
+ },
133
+ async publish(msg) {
134
+ const payload = encodeEnvelope(id, msg);
135
+ if (isPostgresJs) await opts.client.notify(channel, payload);
136
+ else await opts.client.query("SELECT pg_notify($1, $2)", [channel, payload]);
137
+ },
138
+ async close() {
139
+ if (isPostgresJs) await unlisten?.();
140
+ else if (onNotification) {
141
+ opts.client.removeListener?.("notification", onNotification);
142
+ onNotification = void 0;
143
+ await opts.client.query?.(`UNLISTEN ${quotedChannel}`);
144
+ }
145
+ }
146
+ };
147
+ };
148
+ }
149
+ const CLUSTER_MESSAGE = "crossws:sync";
150
+ function isClusterEnvelope(message) {
151
+ return typeof message === "object" && message !== null && typeof message[CLUSTER_MESSAGE] === "string" && typeof message.env === "string";
152
+ }
153
+ let relayInstalled = false;
154
+ async function setupPrimaryCluster() {
155
+ const cluster = (await import("node:cluster")).default;
156
+ if (!cluster.isPrimary || relayInstalled) return;
157
+ relayInstalled = true;
158
+ cluster.on("message", (_worker, message) => {
159
+ if (!isClusterEnvelope(message)) return;
160
+ for (const id in cluster.workers) cluster.workers[id]?.send(message);
161
+ });
162
+ }
163
+ function cluster(opts) {
164
+ const channel = opts.channel;
165
+ return ({ id }) => {
166
+ const proc = globalThis.process;
167
+ let onMessage;
168
+ return {
169
+ subscribe(deliver) {
170
+ if (typeof proc?.send !== "function") throw new Error("[crossws] cluster sync must run in a worker forked by node:cluster (process.send is unavailable). Call setupPrimaryCluster() in the primary process and start your server in the workers.");
171
+ onMessage = (message) => {
172
+ if (!isClusterEnvelope(message) || message[CLUSTER_MESSAGE] !== channel) return;
173
+ const envelope = decodeEnvelope(message.env);
174
+ if (!envelope || envelope.id === id) return;
175
+ deliver(envelope.msg);
176
+ };
177
+ proc.on("message", onMessage);
178
+ },
179
+ publish(msg) {
180
+ proc?.send?.({
181
+ [CLUSTER_MESSAGE]: channel,
182
+ env: encodeEnvelope(id, msg)
183
+ });
184
+ },
185
+ close() {
186
+ if (onMessage) {
187
+ proc?.off?.("message", onMessage);
188
+ onMessage = void 0;
189
+ }
190
+ }
191
+ };
192
+ };
193
+ }
194
+ const syncDrivers = [
195
+ "broadcastChannel",
196
+ "redis",
197
+ "pgsql",
198
+ "cluster"
199
+ ];
200
+ export { broadcastChannel, cluster, decodeEnvelope, encodeEnvelope, pgsql, redis, setupPrimaryCluster, syncDrivers };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crossws",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Cross-platform WebSocket Servers for Node.js, Deno, Bun and Cloudflare Workers",
5
5
  "homepage": "https://crossws.h3.dev",
6
6
  "license": "MIT",
@@ -19,6 +19,7 @@
19
19
  "types": "./dist/index.d.mts",
20
20
  "exports": {
21
21
  ".": "./dist/index.mjs",
22
+ "./sync": "./dist/sync.mjs",
22
23
  "./adapters/bun": "./dist/adapters/bun.mjs",
23
24
  "./adapters/bunny": "./dist/adapters/bunny.mjs",
24
25
  "./adapters/deno": "./dist/adapters/deno.mjs",
@@ -68,14 +69,14 @@
68
69
  "typecheck": "tsgo --noEmit --skipLibCheck"
69
70
  },
70
71
  "devDependencies": {
71
- "@cloudflare/workers-types": "^4.20260608.1",
72
+ "@cloudflare/workers-types": "^4.20260629.1",
72
73
  "@types/bun": "^1.3.14",
73
74
  "@types/deno": "^2.7.0",
74
- "@types/node": "^25.9.2",
75
- "@types/web": "^0.0.350",
75
+ "@types/node": "^26.0.1",
76
+ "@types/web": "^0.0.351",
76
77
  "@types/ws": "^8.18.1",
77
- "@typescript/native-preview": "7.0.0-dev.20260608.1",
78
- "@vitest/coverage-v8": "^4.1.8",
78
+ "@typescript/native-preview": "7.0.0-dev.20260629.1",
79
+ "@vitest/coverage-v8": "^4.1.9",
79
80
  "automd": "^0.4.3",
80
81
  "changelogen": "^0.6.2",
81
82
  "consola": "^3.4.2",
@@ -86,16 +87,16 @@
86
87
  "h3": "2.0.1-rc.22",
87
88
  "jiti": "^2.7.0",
88
89
  "listhen": "^1.10.0",
89
- "obuild": "^0.4.36",
90
- "oxfmt": "^0.53.0",
91
- "oxlint": "^1.68.0",
92
- "srvx": "^0.11.16",
90
+ "obuild": "^0.4.37",
91
+ "oxfmt": "^0.56.0",
92
+ "oxlint": "^1.71.0",
93
+ "srvx": "^0.11.17",
93
94
  "typescript": "^6.0.3",
94
95
  "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.57.0",
95
96
  "unbuild": "^3.6.1",
96
- "undici": "^8.4.1",
97
- "vitest": "^4.1.8",
98
- "wrangler": "^4.98.0",
97
+ "undici": "^8.5.0",
98
+ "vitest": "^4.1.9",
99
+ "wrangler": "^4.105.0",
99
100
  "ws": "^8.21.0"
100
101
  },
101
102
  "peerDependencies": {
@@ -109,19 +110,5 @@
109
110
  "resolutions": {
110
111
  "crossws": "workspace:*"
111
112
  },
112
- "packageManager": "pnpm@11.5.2",
113
- "pnpm": {
114
- "ignoredBuiltDependencies": [
115
- "@parcel/watcher",
116
- "esbuild",
117
- "sharp",
118
- "workerd"
119
- ],
120
- "onlyBuiltDependencies": [
121
- "@parcel/watcher",
122
- "esbuild",
123
- "sharp",
124
- "workerd"
125
- ]
126
- }
113
+ "packageManager": "pnpm@11.7.0"
127
114
  }
package/sync.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./dist/sync.mjs";
2
+ export { default } from "./dist/sync.mjs";
@@ -1,24 +0,0 @@
1
- import { createRequire } from "node:module";
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
9
- var __copyProps = (to, from, except, desc) => {
10
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
- key = keys[i];
12
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
- get: ((k) => from[k]).bind(null, key),
14
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
- });
16
- }
17
- return to;
18
- };
19
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
- value: mod,
21
- enumerable: true
22
- }) : target, mod));
23
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
24
- export { __commonJSMin, __require, __toESM };