@rivetkit/virtual-websocket 0.0.0-pr.4600.db261bc

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/mod.cjs ADDED
@@ -0,0 +1,262 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/mod.ts
21
+ var mod_exports = {};
22
+ __export(mod_exports, {
23
+ VirtualWebSocket: () => VirtualWebSocket
24
+ });
25
+ module.exports = __toCommonJS(mod_exports);
26
+
27
+ // src/virtual-websocket.ts
28
+ var VirtualWebSocket = class {
29
+ static CONNECTING = 0;
30
+ static OPEN = 1;
31
+ static CLOSING = 2;
32
+ static CLOSED = 3;
33
+ CONNECTING = 0;
34
+ OPEN = 1;
35
+ CLOSING = 2;
36
+ CLOSED = 3;
37
+ #options;
38
+ #listeners = /* @__PURE__ */ new Map();
39
+ #onopen = null;
40
+ #onclose = null;
41
+ #onerror = null;
42
+ #onmessage = null;
43
+ constructor(options) {
44
+ this.#options = options;
45
+ }
46
+ // UniversalWebSocket properties
47
+ get readyState() {
48
+ return this.#options.getReadyState();
49
+ }
50
+ /**
51
+ * Binary type for message data. Only "arraybuffer" is supported.
52
+ * Setting to "blob" will throw an error.
53
+ */
54
+ get binaryType() {
55
+ return "arraybuffer";
56
+ }
57
+ set binaryType(value) {
58
+ if (value !== "arraybuffer") {
59
+ throw new DOMException(
60
+ 'VirtualWebSocket only supports binaryType "arraybuffer"',
61
+ "NotSupportedError"
62
+ );
63
+ }
64
+ }
65
+ get bufferedAmount() {
66
+ return 0;
67
+ }
68
+ get extensions() {
69
+ return "";
70
+ }
71
+ get protocol() {
72
+ return "";
73
+ }
74
+ get url() {
75
+ return "";
76
+ }
77
+ // UniversalWebSocket methods
78
+ send(data) {
79
+ const state = this.readyState;
80
+ if (state === this.CONNECTING) {
81
+ throw new DOMException(
82
+ "WebSocket is still in CONNECTING state",
83
+ "InvalidStateError"
84
+ );
85
+ }
86
+ if (state === this.CLOSING || state === this.CLOSED) {
87
+ return;
88
+ }
89
+ this.#options.onSend(data);
90
+ }
91
+ close(code = 1e3, reason = "") {
92
+ const state = this.readyState;
93
+ if (state === this.CLOSED || state === this.CLOSING) {
94
+ return;
95
+ }
96
+ if (code !== 1e3 && (code < 3e3 || code > 4999) && code !== 1001 && code !== 1002 && code !== 1003 && code !== 1007 && code !== 1008 && code !== 1009 && code !== 1010 && code !== 1011) {
97
+ throw new DOMException("Invalid close code", "InvalidAccessError");
98
+ }
99
+ if (new TextEncoder().encode(reason).length > 123) {
100
+ throw new DOMException("Close reason too long", "SyntaxError");
101
+ }
102
+ this.#options.onClose(code, reason);
103
+ }
104
+ /** @experimental Immediate close without close frame */
105
+ terminate() {
106
+ if (this.#options.onTerminate) {
107
+ this.#options.onTerminate();
108
+ } else {
109
+ this.#options.onClose(1006, "Abnormal Closure");
110
+ }
111
+ }
112
+ addEventListener(type, listener) {
113
+ if (!this.#listeners.has(type)) {
114
+ this.#listeners.set(type, []);
115
+ }
116
+ this.#listeners.get(type).push(listener);
117
+ }
118
+ removeEventListener(type, listener) {
119
+ const listeners = this.#listeners.get(type);
120
+ if (listeners) {
121
+ const index = listeners.indexOf(listener);
122
+ if (index !== -1) {
123
+ listeners.splice(index, 1);
124
+ }
125
+ }
126
+ }
127
+ dispatchEvent(event) {
128
+ this.#dispatch(event.type, event);
129
+ return true;
130
+ }
131
+ // on* property getters/setters
132
+ get onopen() {
133
+ return this.#onopen;
134
+ }
135
+ set onopen(fn) {
136
+ this.#onopen = fn;
137
+ }
138
+ get onclose() {
139
+ return this.#onclose;
140
+ }
141
+ set onclose(fn) {
142
+ this.#onclose = fn;
143
+ }
144
+ get onerror() {
145
+ return this.#onerror;
146
+ }
147
+ set onerror(fn) {
148
+ this.#onerror = fn;
149
+ }
150
+ get onmessage() {
151
+ return this.#onmessage;
152
+ }
153
+ set onmessage(fn) {
154
+ this.#onmessage = fn;
155
+ }
156
+ // Helper methods to trigger events from the other side
157
+ triggerOpen() {
158
+ const event = {
159
+ type: "open",
160
+ target: this,
161
+ currentTarget: this
162
+ };
163
+ this.#dispatch("open", event);
164
+ }
165
+ triggerMessage(data, rivetMessageIndex) {
166
+ const event = {
167
+ type: "message",
168
+ data,
169
+ rivetMessageIndex,
170
+ target: this,
171
+ currentTarget: this
172
+ };
173
+ this.#dispatch("message", event);
174
+ }
175
+ triggerClose(code, reason, wasClean) {
176
+ const event = {
177
+ type: "close",
178
+ code,
179
+ reason,
180
+ wasClean: wasClean ?? code === 1e3,
181
+ target: this,
182
+ currentTarget: this
183
+ };
184
+ this.#dispatch("close", event);
185
+ }
186
+ triggerError(error) {
187
+ const event = {
188
+ type: "error",
189
+ target: this,
190
+ currentTarget: this,
191
+ error,
192
+ message: error instanceof Error ? error.message : String(error)
193
+ };
194
+ this.#dispatch("error", event);
195
+ }
196
+ #dispatch(type, event) {
197
+ const listeners = this.#listeners.get(type);
198
+ if (listeners && listeners.length > 0) {
199
+ for (const listener of listeners) {
200
+ try {
201
+ listener(event);
202
+ } catch (err) {
203
+ console.error("Error in WebSocket event listener:", err);
204
+ }
205
+ }
206
+ }
207
+ switch (type) {
208
+ case "open":
209
+ if (this.#onopen) {
210
+ try {
211
+ this.#onopen(event);
212
+ } catch (err) {
213
+ console.error(
214
+ "Error in WebSocket onopen handler:",
215
+ err
216
+ );
217
+ }
218
+ }
219
+ break;
220
+ case "close":
221
+ if (this.#onclose) {
222
+ try {
223
+ this.#onclose(event);
224
+ } catch (err) {
225
+ console.error(
226
+ "Error in WebSocket onclose handler:",
227
+ err
228
+ );
229
+ }
230
+ }
231
+ break;
232
+ case "error":
233
+ if (this.#onerror) {
234
+ try {
235
+ this.#onerror(event);
236
+ } catch (err) {
237
+ console.error(
238
+ "Error in WebSocket onerror handler:",
239
+ err
240
+ );
241
+ }
242
+ }
243
+ break;
244
+ case "message":
245
+ if (this.#onmessage) {
246
+ try {
247
+ this.#onmessage(event);
248
+ } catch (err) {
249
+ console.error(
250
+ "Error in WebSocket onmessage handler:",
251
+ err
252
+ );
253
+ }
254
+ }
255
+ break;
256
+ }
257
+ }
258
+ };
259
+ // Annotate the CommonJS export names for ESM import in node:
260
+ 0 && (module.exports = {
261
+ VirtualWebSocket
262
+ });
package/dist/mod.d.cts ADDED
@@ -0,0 +1,112 @@
1
+ interface RivetEvent {
2
+ type: string;
3
+ target?: any;
4
+ currentTarget?: any;
5
+ /**
6
+ * @experimental
7
+ * Gateway ID for hibernatable websockets (provided by engine envoy)
8
+ **/
9
+ rivetGatewayId?: ArrayBuffer;
10
+ /**
11
+ * @experimental
12
+ * Request ID for hibernatable websockets (provided by engine envoy)
13
+ **/
14
+ rivetRequestId?: ArrayBuffer;
15
+ }
16
+ interface RivetMessageEvent extends RivetEvent {
17
+ data: any;
18
+ /**
19
+ * @experimental
20
+ * Message index for hibernatable websockets (provided by engine envoy)
21
+ **/
22
+ rivetMessageIndex?: number;
23
+ }
24
+ interface RivetCloseEvent extends RivetEvent {
25
+ code: number;
26
+ reason: string;
27
+ wasClean: boolean;
28
+ }
29
+ /**
30
+ * Common WebSocket interface that can be implemented by different WebSocket-like classes
31
+ * This is compatible with the standard WebSocket API but allows for custom implementations
32
+ */
33
+ interface UniversalWebSocket {
34
+ readonly CONNECTING: 0;
35
+ readonly OPEN: 1;
36
+ readonly CLOSING: 2;
37
+ readonly CLOSED: 3;
38
+ readonly readyState: 0 | 1 | 2 | 3;
39
+ binaryType: "arraybuffer" | "blob";
40
+ readonly bufferedAmount: number;
41
+ readonly extensions: string;
42
+ readonly protocol: string;
43
+ readonly url: string;
44
+ send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
45
+ close(code?: number, reason?: string): void;
46
+ addEventListener(type: string, listener: (event: any) => void | Promise<void>): void;
47
+ removeEventListener(type: string, listener: (event: any) => void | Promise<void>): void;
48
+ dispatchEvent(event: RivetEvent): boolean;
49
+ onopen?: ((event: RivetEvent) => void | Promise<void>) | null;
50
+ onclose?: ((event: RivetCloseEvent) => void | Promise<void>) | null;
51
+ onerror?: ((event: RivetEvent) => void | Promise<void>) | null;
52
+ onmessage?: ((event: RivetMessageEvent) => void | Promise<void>) | null;
53
+ }
54
+
55
+ interface VirtualWebSocketOptions {
56
+ /** Get the current ready state */
57
+ getReadyState: () => 0 | 1 | 2 | 3;
58
+ /** Called when send() is invoked */
59
+ onSend: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void;
60
+ /** Called when close() is invoked */
61
+ onClose: (code: number, reason: string) => void;
62
+ /** Called when terminate() is invoked (immediate close without close frame) */
63
+ onTerminate?: () => void;
64
+ }
65
+ /**
66
+ * Virtual WebSocket implementation that dispatches events and delegates
67
+ * send/close to callbacks. Used to create linked WebSocket pairs.
68
+ */
69
+ declare class VirtualWebSocket implements UniversalWebSocket {
70
+ #private;
71
+ static readonly CONNECTING: 0;
72
+ static readonly OPEN: 1;
73
+ static readonly CLOSING: 2;
74
+ static readonly CLOSED: 3;
75
+ readonly CONNECTING: 0;
76
+ readonly OPEN: 1;
77
+ readonly CLOSING: 2;
78
+ readonly CLOSED: 3;
79
+ constructor(options: VirtualWebSocketOptions);
80
+ get readyState(): 0 | 1 | 2 | 3;
81
+ /**
82
+ * Binary type for message data. Only "arraybuffer" is supported.
83
+ * Setting to "blob" will throw an error.
84
+ */
85
+ get binaryType(): "arraybuffer" | "blob";
86
+ set binaryType(value: "arraybuffer" | "blob");
87
+ get bufferedAmount(): number;
88
+ get extensions(): string;
89
+ get protocol(): string;
90
+ get url(): string;
91
+ send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
92
+ close(code?: number, reason?: string): void;
93
+ /** @experimental Immediate close without close frame */
94
+ terminate(): void;
95
+ addEventListener(type: string, listener: (ev: any) => void | Promise<void>): void;
96
+ removeEventListener(type: string, listener: (ev: any) => void | Promise<void>): void;
97
+ dispatchEvent(event: RivetEvent): boolean;
98
+ get onopen(): ((event: RivetEvent) => void | Promise<void>) | null;
99
+ set onopen(fn: ((event: RivetEvent) => void | Promise<void>) | null);
100
+ get onclose(): ((event: RivetCloseEvent) => void | Promise<void>) | null;
101
+ set onclose(fn: ((event: RivetCloseEvent) => void | Promise<void>) | null);
102
+ get onerror(): ((event: RivetEvent) => void | Promise<void>) | null;
103
+ set onerror(fn: ((event: RivetEvent) => void | Promise<void>) | null);
104
+ get onmessage(): ((event: RivetMessageEvent) => void | Promise<void>) | null;
105
+ set onmessage(fn: ((event: RivetMessageEvent) => void | Promise<void>) | null);
106
+ triggerOpen(): void;
107
+ triggerMessage(data: any, rivetMessageIndex?: number): void;
108
+ triggerClose(code: number, reason: string, wasClean?: boolean): void;
109
+ triggerError(error: unknown): void;
110
+ }
111
+
112
+ export { type RivetCloseEvent, type RivetEvent, type RivetMessageEvent, type UniversalWebSocket, VirtualWebSocket, type VirtualWebSocketOptions };
package/dist/mod.d.ts ADDED
@@ -0,0 +1,112 @@
1
+ interface RivetEvent {
2
+ type: string;
3
+ target?: any;
4
+ currentTarget?: any;
5
+ /**
6
+ * @experimental
7
+ * Gateway ID for hibernatable websockets (provided by engine envoy)
8
+ **/
9
+ rivetGatewayId?: ArrayBuffer;
10
+ /**
11
+ * @experimental
12
+ * Request ID for hibernatable websockets (provided by engine envoy)
13
+ **/
14
+ rivetRequestId?: ArrayBuffer;
15
+ }
16
+ interface RivetMessageEvent extends RivetEvent {
17
+ data: any;
18
+ /**
19
+ * @experimental
20
+ * Message index for hibernatable websockets (provided by engine envoy)
21
+ **/
22
+ rivetMessageIndex?: number;
23
+ }
24
+ interface RivetCloseEvent extends RivetEvent {
25
+ code: number;
26
+ reason: string;
27
+ wasClean: boolean;
28
+ }
29
+ /**
30
+ * Common WebSocket interface that can be implemented by different WebSocket-like classes
31
+ * This is compatible with the standard WebSocket API but allows for custom implementations
32
+ */
33
+ interface UniversalWebSocket {
34
+ readonly CONNECTING: 0;
35
+ readonly OPEN: 1;
36
+ readonly CLOSING: 2;
37
+ readonly CLOSED: 3;
38
+ readonly readyState: 0 | 1 | 2 | 3;
39
+ binaryType: "arraybuffer" | "blob";
40
+ readonly bufferedAmount: number;
41
+ readonly extensions: string;
42
+ readonly protocol: string;
43
+ readonly url: string;
44
+ send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
45
+ close(code?: number, reason?: string): void;
46
+ addEventListener(type: string, listener: (event: any) => void | Promise<void>): void;
47
+ removeEventListener(type: string, listener: (event: any) => void | Promise<void>): void;
48
+ dispatchEvent(event: RivetEvent): boolean;
49
+ onopen?: ((event: RivetEvent) => void | Promise<void>) | null;
50
+ onclose?: ((event: RivetCloseEvent) => void | Promise<void>) | null;
51
+ onerror?: ((event: RivetEvent) => void | Promise<void>) | null;
52
+ onmessage?: ((event: RivetMessageEvent) => void | Promise<void>) | null;
53
+ }
54
+
55
+ interface VirtualWebSocketOptions {
56
+ /** Get the current ready state */
57
+ getReadyState: () => 0 | 1 | 2 | 3;
58
+ /** Called when send() is invoked */
59
+ onSend: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void;
60
+ /** Called when close() is invoked */
61
+ onClose: (code: number, reason: string) => void;
62
+ /** Called when terminate() is invoked (immediate close without close frame) */
63
+ onTerminate?: () => void;
64
+ }
65
+ /**
66
+ * Virtual WebSocket implementation that dispatches events and delegates
67
+ * send/close to callbacks. Used to create linked WebSocket pairs.
68
+ */
69
+ declare class VirtualWebSocket implements UniversalWebSocket {
70
+ #private;
71
+ static readonly CONNECTING: 0;
72
+ static readonly OPEN: 1;
73
+ static readonly CLOSING: 2;
74
+ static readonly CLOSED: 3;
75
+ readonly CONNECTING: 0;
76
+ readonly OPEN: 1;
77
+ readonly CLOSING: 2;
78
+ readonly CLOSED: 3;
79
+ constructor(options: VirtualWebSocketOptions);
80
+ get readyState(): 0 | 1 | 2 | 3;
81
+ /**
82
+ * Binary type for message data. Only "arraybuffer" is supported.
83
+ * Setting to "blob" will throw an error.
84
+ */
85
+ get binaryType(): "arraybuffer" | "blob";
86
+ set binaryType(value: "arraybuffer" | "blob");
87
+ get bufferedAmount(): number;
88
+ get extensions(): string;
89
+ get protocol(): string;
90
+ get url(): string;
91
+ send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
92
+ close(code?: number, reason?: string): void;
93
+ /** @experimental Immediate close without close frame */
94
+ terminate(): void;
95
+ addEventListener(type: string, listener: (ev: any) => void | Promise<void>): void;
96
+ removeEventListener(type: string, listener: (ev: any) => void | Promise<void>): void;
97
+ dispatchEvent(event: RivetEvent): boolean;
98
+ get onopen(): ((event: RivetEvent) => void | Promise<void>) | null;
99
+ set onopen(fn: ((event: RivetEvent) => void | Promise<void>) | null);
100
+ get onclose(): ((event: RivetCloseEvent) => void | Promise<void>) | null;
101
+ set onclose(fn: ((event: RivetCloseEvent) => void | Promise<void>) | null);
102
+ get onerror(): ((event: RivetEvent) => void | Promise<void>) | null;
103
+ set onerror(fn: ((event: RivetEvent) => void | Promise<void>) | null);
104
+ get onmessage(): ((event: RivetMessageEvent) => void | Promise<void>) | null;
105
+ set onmessage(fn: ((event: RivetMessageEvent) => void | Promise<void>) | null);
106
+ triggerOpen(): void;
107
+ triggerMessage(data: any, rivetMessageIndex?: number): void;
108
+ triggerClose(code: number, reason: string, wasClean?: boolean): void;
109
+ triggerError(error: unknown): void;
110
+ }
111
+
112
+ export { type RivetCloseEvent, type RivetEvent, type RivetMessageEvent, type UniversalWebSocket, VirtualWebSocket, type VirtualWebSocketOptions };
package/dist/mod.js ADDED
@@ -0,0 +1,235 @@
1
+ // src/virtual-websocket.ts
2
+ var VirtualWebSocket = class {
3
+ static CONNECTING = 0;
4
+ static OPEN = 1;
5
+ static CLOSING = 2;
6
+ static CLOSED = 3;
7
+ CONNECTING = 0;
8
+ OPEN = 1;
9
+ CLOSING = 2;
10
+ CLOSED = 3;
11
+ #options;
12
+ #listeners = /* @__PURE__ */ new Map();
13
+ #onopen = null;
14
+ #onclose = null;
15
+ #onerror = null;
16
+ #onmessage = null;
17
+ constructor(options) {
18
+ this.#options = options;
19
+ }
20
+ // UniversalWebSocket properties
21
+ get readyState() {
22
+ return this.#options.getReadyState();
23
+ }
24
+ /**
25
+ * Binary type for message data. Only "arraybuffer" is supported.
26
+ * Setting to "blob" will throw an error.
27
+ */
28
+ get binaryType() {
29
+ return "arraybuffer";
30
+ }
31
+ set binaryType(value) {
32
+ if (value !== "arraybuffer") {
33
+ throw new DOMException(
34
+ 'VirtualWebSocket only supports binaryType "arraybuffer"',
35
+ "NotSupportedError"
36
+ );
37
+ }
38
+ }
39
+ get bufferedAmount() {
40
+ return 0;
41
+ }
42
+ get extensions() {
43
+ return "";
44
+ }
45
+ get protocol() {
46
+ return "";
47
+ }
48
+ get url() {
49
+ return "";
50
+ }
51
+ // UniversalWebSocket methods
52
+ send(data) {
53
+ const state = this.readyState;
54
+ if (state === this.CONNECTING) {
55
+ throw new DOMException(
56
+ "WebSocket is still in CONNECTING state",
57
+ "InvalidStateError"
58
+ );
59
+ }
60
+ if (state === this.CLOSING || state === this.CLOSED) {
61
+ return;
62
+ }
63
+ this.#options.onSend(data);
64
+ }
65
+ close(code = 1e3, reason = "") {
66
+ const state = this.readyState;
67
+ if (state === this.CLOSED || state === this.CLOSING) {
68
+ return;
69
+ }
70
+ if (code !== 1e3 && (code < 3e3 || code > 4999) && code !== 1001 && code !== 1002 && code !== 1003 && code !== 1007 && code !== 1008 && code !== 1009 && code !== 1010 && code !== 1011) {
71
+ throw new DOMException("Invalid close code", "InvalidAccessError");
72
+ }
73
+ if (new TextEncoder().encode(reason).length > 123) {
74
+ throw new DOMException("Close reason too long", "SyntaxError");
75
+ }
76
+ this.#options.onClose(code, reason);
77
+ }
78
+ /** @experimental Immediate close without close frame */
79
+ terminate() {
80
+ if (this.#options.onTerminate) {
81
+ this.#options.onTerminate();
82
+ } else {
83
+ this.#options.onClose(1006, "Abnormal Closure");
84
+ }
85
+ }
86
+ addEventListener(type, listener) {
87
+ if (!this.#listeners.has(type)) {
88
+ this.#listeners.set(type, []);
89
+ }
90
+ this.#listeners.get(type).push(listener);
91
+ }
92
+ removeEventListener(type, listener) {
93
+ const listeners = this.#listeners.get(type);
94
+ if (listeners) {
95
+ const index = listeners.indexOf(listener);
96
+ if (index !== -1) {
97
+ listeners.splice(index, 1);
98
+ }
99
+ }
100
+ }
101
+ dispatchEvent(event) {
102
+ this.#dispatch(event.type, event);
103
+ return true;
104
+ }
105
+ // on* property getters/setters
106
+ get onopen() {
107
+ return this.#onopen;
108
+ }
109
+ set onopen(fn) {
110
+ this.#onopen = fn;
111
+ }
112
+ get onclose() {
113
+ return this.#onclose;
114
+ }
115
+ set onclose(fn) {
116
+ this.#onclose = fn;
117
+ }
118
+ get onerror() {
119
+ return this.#onerror;
120
+ }
121
+ set onerror(fn) {
122
+ this.#onerror = fn;
123
+ }
124
+ get onmessage() {
125
+ return this.#onmessage;
126
+ }
127
+ set onmessage(fn) {
128
+ this.#onmessage = fn;
129
+ }
130
+ // Helper methods to trigger events from the other side
131
+ triggerOpen() {
132
+ const event = {
133
+ type: "open",
134
+ target: this,
135
+ currentTarget: this
136
+ };
137
+ this.#dispatch("open", event);
138
+ }
139
+ triggerMessage(data, rivetMessageIndex) {
140
+ const event = {
141
+ type: "message",
142
+ data,
143
+ rivetMessageIndex,
144
+ target: this,
145
+ currentTarget: this
146
+ };
147
+ this.#dispatch("message", event);
148
+ }
149
+ triggerClose(code, reason, wasClean) {
150
+ const event = {
151
+ type: "close",
152
+ code,
153
+ reason,
154
+ wasClean: wasClean ?? code === 1e3,
155
+ target: this,
156
+ currentTarget: this
157
+ };
158
+ this.#dispatch("close", event);
159
+ }
160
+ triggerError(error) {
161
+ const event = {
162
+ type: "error",
163
+ target: this,
164
+ currentTarget: this,
165
+ error,
166
+ message: error instanceof Error ? error.message : String(error)
167
+ };
168
+ this.#dispatch("error", event);
169
+ }
170
+ #dispatch(type, event) {
171
+ const listeners = this.#listeners.get(type);
172
+ if (listeners && listeners.length > 0) {
173
+ for (const listener of listeners) {
174
+ try {
175
+ listener(event);
176
+ } catch (err) {
177
+ console.error("Error in WebSocket event listener:", err);
178
+ }
179
+ }
180
+ }
181
+ switch (type) {
182
+ case "open":
183
+ if (this.#onopen) {
184
+ try {
185
+ this.#onopen(event);
186
+ } catch (err) {
187
+ console.error(
188
+ "Error in WebSocket onopen handler:",
189
+ err
190
+ );
191
+ }
192
+ }
193
+ break;
194
+ case "close":
195
+ if (this.#onclose) {
196
+ try {
197
+ this.#onclose(event);
198
+ } catch (err) {
199
+ console.error(
200
+ "Error in WebSocket onclose handler:",
201
+ err
202
+ );
203
+ }
204
+ }
205
+ break;
206
+ case "error":
207
+ if (this.#onerror) {
208
+ try {
209
+ this.#onerror(event);
210
+ } catch (err) {
211
+ console.error(
212
+ "Error in WebSocket onerror handler:",
213
+ err
214
+ );
215
+ }
216
+ }
217
+ break;
218
+ case "message":
219
+ if (this.#onmessage) {
220
+ try {
221
+ this.#onmessage(event);
222
+ } catch (err) {
223
+ console.error(
224
+ "Error in WebSocket onmessage handler:",
225
+ err
226
+ );
227
+ }
228
+ }
229
+ break;
230
+ }
231
+ }
232
+ };
233
+ export {
234
+ VirtualWebSocket
235
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@rivetkit/virtual-websocket",
3
+ "version": "0.0.0-pr.4600.db261bc",
4
+ "description": "Virtual WebSocket implementation for creating linked WebSocket pairs",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "files": [
8
+ "dist",
9
+ "src",
10
+ "package.json"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "import": {
15
+ "types": "./dist/mod.d.ts",
16
+ "default": "./dist/mod.js"
17
+ },
18
+ "require": {
19
+ "types": "./dist/mod.d.cts",
20
+ "default": "./dist/mod.cjs"
21
+ }
22
+ }
23
+ },
24
+ "scripts": {
25
+ "build": "tsup src/mod.ts",
26
+ "check-types": "tsc --noEmit"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^22.13.1",
30
+ "tsup": "^8.4.0",
31
+ "typescript": "^5.7.3"
32
+ },
33
+ "stableVersion": "0.8.0"
34
+ }
@@ -0,0 +1,70 @@
1
+ // Define minimal event interfaces to avoid conflicts between different WebSocket implementations
2
+ export interface RivetEvent {
3
+ type: string;
4
+ target?: any;
5
+ currentTarget?: any;
6
+ /**
7
+ * @experimental
8
+ * Gateway ID for hibernatable websockets (provided by engine envoy)
9
+ **/
10
+ rivetGatewayId?: ArrayBuffer;
11
+ /**
12
+ * @experimental
13
+ * Request ID for hibernatable websockets (provided by engine envoy)
14
+ **/
15
+ rivetRequestId?: ArrayBuffer;
16
+ }
17
+
18
+ export interface RivetMessageEvent extends RivetEvent {
19
+ data: any;
20
+ /**
21
+ * @experimental
22
+ * Message index for hibernatable websockets (provided by engine envoy)
23
+ **/
24
+ rivetMessageIndex?: number;
25
+ }
26
+
27
+ export interface RivetCloseEvent extends RivetEvent {
28
+ code: number;
29
+ reason: string;
30
+ wasClean: boolean;
31
+ }
32
+
33
+ /**
34
+ * Common WebSocket interface that can be implemented by different WebSocket-like classes
35
+ * This is compatible with the standard WebSocket API but allows for custom implementations
36
+ */
37
+ export interface UniversalWebSocket {
38
+ // WebSocket readyState values
39
+ readonly CONNECTING: 0;
40
+ readonly OPEN: 1;
41
+ readonly CLOSING: 2;
42
+ readonly CLOSED: 3;
43
+
44
+ // Properties
45
+ readonly readyState: 0 | 1 | 2 | 3;
46
+ binaryType: "arraybuffer" | "blob";
47
+ readonly bufferedAmount: number;
48
+ readonly extensions: string;
49
+ readonly protocol: string;
50
+ readonly url: string;
51
+
52
+ // Methods
53
+ send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
54
+ close(code?: number, reason?: string): void;
55
+ addEventListener(
56
+ type: string,
57
+ listener: (event: any) => void | Promise<void>,
58
+ ): void;
59
+ removeEventListener(
60
+ type: string,
61
+ listener: (event: any) => void | Promise<void>,
62
+ ): void;
63
+ dispatchEvent(event: RivetEvent): boolean;
64
+
65
+ // Event handlers (optional)
66
+ onopen?: ((event: RivetEvent) => void | Promise<void>) | null;
67
+ onclose?: ((event: RivetCloseEvent) => void | Promise<void>) | null;
68
+ onerror?: ((event: RivetEvent) => void | Promise<void>) | null;
69
+ onmessage?: ((event: RivetMessageEvent) => void | Promise<void>) | null;
70
+ }
package/src/mod.ts ADDED
@@ -0,0 +1,11 @@
1
+ export type {
2
+ RivetEvent,
3
+ RivetMessageEvent,
4
+ RivetCloseEvent,
5
+ UniversalWebSocket,
6
+ } from "./interface";
7
+
8
+ export {
9
+ VirtualWebSocket,
10
+ type VirtualWebSocketOptions,
11
+ } from "./virtual-websocket";
@@ -0,0 +1,315 @@
1
+ import type {
2
+ RivetCloseEvent,
3
+ RivetEvent,
4
+ RivetMessageEvent,
5
+ UniversalWebSocket,
6
+ } from "./interface";
7
+
8
+ export interface VirtualWebSocketOptions {
9
+ /** Get the current ready state */
10
+ getReadyState: () => 0 | 1 | 2 | 3;
11
+ /** Called when send() is invoked */
12
+ onSend: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void;
13
+ /** Called when close() is invoked */
14
+ onClose: (code: number, reason: string) => void;
15
+ /** Called when terminate() is invoked (immediate close without close frame) */
16
+ onTerminate?: () => void;
17
+ }
18
+
19
+ /**
20
+ * Virtual WebSocket implementation that dispatches events and delegates
21
+ * send/close to callbacks. Used to create linked WebSocket pairs.
22
+ */
23
+ export class VirtualWebSocket implements UniversalWebSocket {
24
+ static readonly CONNECTING = 0 as const;
25
+ static readonly OPEN = 1 as const;
26
+ static readonly CLOSING = 2 as const;
27
+ static readonly CLOSED = 3 as const;
28
+
29
+ readonly CONNECTING = 0 as const;
30
+ readonly OPEN = 1 as const;
31
+ readonly CLOSING = 2 as const;
32
+ readonly CLOSED = 3 as const;
33
+
34
+ #options: VirtualWebSocketOptions;
35
+ #listeners: Map<string, ((ev: any) => void | Promise<void>)[]> =
36
+ new Map();
37
+ #onopen: ((event: RivetEvent) => void | Promise<void>) | null = null;
38
+ #onclose:
39
+ | ((event: RivetCloseEvent) => void | Promise<void>)
40
+ | null = null;
41
+ #onerror: ((event: RivetEvent) => void | Promise<void>) | null = null;
42
+ #onmessage:
43
+ | ((event: RivetMessageEvent) => void | Promise<void>)
44
+ | null = null;
45
+
46
+ constructor(options: VirtualWebSocketOptions) {
47
+ this.#options = options;
48
+ }
49
+
50
+ // UniversalWebSocket properties
51
+ get readyState(): 0 | 1 | 2 | 3 {
52
+ return this.#options.getReadyState();
53
+ }
54
+
55
+ /**
56
+ * Binary type for message data. Only "arraybuffer" is supported.
57
+ * Setting to "blob" will throw an error.
58
+ */
59
+ get binaryType(): "arraybuffer" | "blob" {
60
+ return "arraybuffer";
61
+ }
62
+
63
+ set binaryType(value: "arraybuffer" | "blob") {
64
+ if (value !== "arraybuffer") {
65
+ throw new DOMException(
66
+ 'VirtualWebSocket only supports binaryType "arraybuffer"',
67
+ "NotSupportedError",
68
+ );
69
+ }
70
+ }
71
+
72
+ get bufferedAmount(): number {
73
+ return 0;
74
+ }
75
+
76
+ get extensions(): string {
77
+ return "";
78
+ }
79
+
80
+ get protocol(): string {
81
+ return "";
82
+ }
83
+
84
+ get url(): string {
85
+ return "";
86
+ }
87
+
88
+ // UniversalWebSocket methods
89
+ send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void {
90
+ const state = this.readyState;
91
+ if (state === this.CONNECTING) {
92
+ throw new DOMException(
93
+ "WebSocket is still in CONNECTING state",
94
+ "InvalidStateError",
95
+ );
96
+ }
97
+ if (state === this.CLOSING || state === this.CLOSED) {
98
+ return;
99
+ }
100
+ this.#options.onSend(data);
101
+ }
102
+
103
+ close(code = 1000, reason = ""): void {
104
+ const state = this.readyState;
105
+ if (state === this.CLOSED || state === this.CLOSING) {
106
+ return;
107
+ }
108
+ // Validate close code per WebSocket spec
109
+ // 1005 and 1006 are reserved and cannot be set by application code
110
+ if (
111
+ code !== 1000 &&
112
+ (code < 3000 || code > 4999) &&
113
+ code !== 1001 &&
114
+ code !== 1002 &&
115
+ code !== 1003 &&
116
+ code !== 1007 &&
117
+ code !== 1008 &&
118
+ code !== 1009 &&
119
+ code !== 1010 &&
120
+ code !== 1011
121
+ ) {
122
+ throw new DOMException("Invalid close code", "InvalidAccessError");
123
+ }
124
+ // Validate reason length (must be ≤ 123 bytes UTF-8 encoded)
125
+ if (new TextEncoder().encode(reason).length > 123) {
126
+ throw new DOMException("Close reason too long", "SyntaxError");
127
+ }
128
+ this.#options.onClose(code, reason);
129
+ }
130
+
131
+ /** @experimental Immediate close without close frame */
132
+ terminate(): void {
133
+ if (this.#options.onTerminate) {
134
+ this.#options.onTerminate();
135
+ } else {
136
+ // Fallback to close with abnormal closure code
137
+ this.#options.onClose(1006, "Abnormal Closure");
138
+ }
139
+ }
140
+
141
+ addEventListener(
142
+ type: string,
143
+ listener: (ev: any) => void | Promise<void>,
144
+ ): void {
145
+ if (!this.#listeners.has(type)) {
146
+ this.#listeners.set(type, []);
147
+ }
148
+ this.#listeners.get(type)!.push(listener);
149
+ }
150
+
151
+ removeEventListener(
152
+ type: string,
153
+ listener: (ev: any) => void | Promise<void>,
154
+ ): void {
155
+ const listeners = this.#listeners.get(type);
156
+ if (listeners) {
157
+ const index = listeners.indexOf(listener);
158
+ if (index !== -1) {
159
+ listeners.splice(index, 1);
160
+ }
161
+ }
162
+ }
163
+
164
+ dispatchEvent(event: RivetEvent): boolean {
165
+ this.#dispatch(event.type, event);
166
+ return true;
167
+ }
168
+
169
+ // on* property getters/setters
170
+ get onopen(): ((event: RivetEvent) => void | Promise<void>) | null {
171
+ return this.#onopen;
172
+ }
173
+ set onopen(fn: ((event: RivetEvent) => void | Promise<void>) | null) {
174
+ this.#onopen = fn;
175
+ }
176
+
177
+ get onclose():
178
+ | ((event: RivetCloseEvent) => void | Promise<void>)
179
+ | null {
180
+ return this.#onclose;
181
+ }
182
+ set onclose(
183
+ fn: ((event: RivetCloseEvent) => void | Promise<void>) | null,
184
+ ) {
185
+ this.#onclose = fn;
186
+ }
187
+
188
+ get onerror(): ((event: RivetEvent) => void | Promise<void>) | null {
189
+ return this.#onerror;
190
+ }
191
+ set onerror(fn: ((event: RivetEvent) => void | Promise<void>) | null) {
192
+ this.#onerror = fn;
193
+ }
194
+
195
+ get onmessage():
196
+ | ((event: RivetMessageEvent) => void | Promise<void>)
197
+ | null {
198
+ return this.#onmessage;
199
+ }
200
+ set onmessage(
201
+ fn: ((event: RivetMessageEvent) => void | Promise<void>) | null,
202
+ ) {
203
+ this.#onmessage = fn;
204
+ }
205
+
206
+ // Helper methods to trigger events from the other side
207
+ triggerOpen(): void {
208
+ const event = {
209
+ type: "open",
210
+ target: this,
211
+ currentTarget: this,
212
+ } as unknown as RivetEvent;
213
+ this.#dispatch("open", event);
214
+ }
215
+
216
+ triggerMessage(data: any, rivetMessageIndex?: number): void {
217
+ const event = {
218
+ type: "message",
219
+ data,
220
+ rivetMessageIndex,
221
+ target: this,
222
+ currentTarget: this,
223
+ } as unknown as RivetMessageEvent;
224
+ this.#dispatch("message", event);
225
+ }
226
+
227
+ triggerClose(code: number, reason: string, wasClean?: boolean): void {
228
+ const event = {
229
+ type: "close",
230
+ code,
231
+ reason,
232
+ wasClean: wasClean ?? code === 1000,
233
+ target: this,
234
+ currentTarget: this,
235
+ } as unknown as RivetCloseEvent;
236
+ this.#dispatch("close", event);
237
+ }
238
+
239
+ triggerError(error: unknown): void {
240
+ const event = {
241
+ type: "error",
242
+ target: this,
243
+ currentTarget: this,
244
+ error,
245
+ message: error instanceof Error ? error.message : String(error),
246
+ } as unknown as RivetEvent;
247
+ this.#dispatch("error", event);
248
+ }
249
+
250
+ #dispatch(type: string, event: any): void {
251
+ // Dispatch to addEventListener listeners
252
+ const listeners = this.#listeners.get(type);
253
+ if (listeners && listeners.length > 0) {
254
+ for (const listener of listeners) {
255
+ try {
256
+ listener(event);
257
+ } catch (err) {
258
+ console.error("Error in WebSocket event listener:", err);
259
+ }
260
+ }
261
+ }
262
+
263
+ // Dispatch to on* property handlers
264
+ switch (type) {
265
+ case "open":
266
+ if (this.#onopen) {
267
+ try {
268
+ this.#onopen(event);
269
+ } catch (err) {
270
+ console.error(
271
+ "Error in WebSocket onopen handler:",
272
+ err,
273
+ );
274
+ }
275
+ }
276
+ break;
277
+ case "close":
278
+ if (this.#onclose) {
279
+ try {
280
+ this.#onclose(event);
281
+ } catch (err) {
282
+ console.error(
283
+ "Error in WebSocket onclose handler:",
284
+ err,
285
+ );
286
+ }
287
+ }
288
+ break;
289
+ case "error":
290
+ if (this.#onerror) {
291
+ try {
292
+ this.#onerror(event);
293
+ } catch (err) {
294
+ console.error(
295
+ "Error in WebSocket onerror handler:",
296
+ err,
297
+ );
298
+ }
299
+ }
300
+ break;
301
+ case "message":
302
+ if (this.#onmessage) {
303
+ try {
304
+ this.#onmessage(event);
305
+ } catch (err) {
306
+ console.error(
307
+ "Error in WebSocket onmessage handler:",
308
+ err,
309
+ );
310
+ }
311
+ }
312
+ break;
313
+ }
314
+ }
315
+ }