@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 +262 -0
- package/dist/mod.d.cts +112 -0
- package/dist/mod.d.ts +112 -0
- package/dist/mod.js +235 -0
- package/package.json +34 -0
- package/src/interface.ts +70 -0
- package/src/mod.ts +11 -0
- package/src/virtual-websocket.ts +315 -0
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
|
+
}
|
package/src/interface.ts
ADDED
|
@@ -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,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
|
+
}
|