@abndnce/pulsar-client 0.0.8 → 0.0.9

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.d.mts CHANGED
@@ -39,9 +39,14 @@ declare function connectNostr(_relay: string, _pubkey: string): Promise<PulsarCl
39
39
  */
40
40
  /**
41
41
  * Wait for an RTCDataChannel to enter the "open" state.
42
- * Rejects if it closes or errors before opening.
42
+ *
43
+ * Also monitors the RTCPeerConnection for failure/closure, and
44
+ * accepts an optional AbortSignal for cancellation.
45
+ *
46
+ * Rejects if the channel closes, the peer connection fails, or
47
+ * the timeout elapses before the channel opens.
43
48
  */
44
- declare function waitForDataChannelOpen(channel: RTCDataChannel, timeoutMs?: number): Promise<void>;
49
+ declare function waitForDataChannelOpen(channel: RTCDataChannel, pc: RTCPeerConnection, timeoutMs?: number, signal?: AbortSignal): Promise<void>;
45
50
  /**
46
51
  * Create a data channel on the given `pc` with the Pulsar socket label
47
52
  * convention (`socket/<hostname>:<port>`) and wait for it to open.
@@ -49,34 +54,47 @@ declare function waitForDataChannelOpen(channel: RTCDataChannel, timeoutMs?: num
49
54
  * This is the low-level primitive used by `connect()` and
50
55
  * `libcurlTransport()`.
51
56
  */
52
- declare function openSocketChannel(pc: RTCPeerConnection, hostname: string, port: number, timeoutMs?: number): Promise<RTCDataChannel>;
57
+ declare function openSocketChannel(pc: RTCPeerConnection, hostname: string, port: number, timeoutMs?: number, signal?: AbortSignal): Promise<RTCDataChannel>;
53
58
  //#endregion
54
59
  //#region lib/tunnel.d.ts
55
60
  /**
56
- * A minimal WebSocket-like wrapper around an RTCDataChannel.
61
+ * WebSocket-compatible wrapper around an RTCDataChannel.
57
62
  *
58
- * libcurl.js expects its transport factory to return objects
59
- * with `send()`, `close()`, `onopen`, `onmessage`, `onclose`,
60
- * and `onerror` which is exactly the RTCDataChannel API, so
61
- * the wrapper is thin.
63
+ * libcurl.js compares `readyState` against `WebSocket.OPEN` (numeric 1)
64
+ * and expects static constants (`CONNECTING`, `OPEN`, `CLOSING`, `CLOSED`).
65
+ * Using `EventTarget` ensures `addEventListener` / `removeEventListener`
66
+ * work, which libcurl's poll loop depends on.
62
67
  */
63
- declare class DataChannelSocket {
68
+ declare class DataChannelSocket extends EventTarget {
69
+ static readonly CONNECTING = 0;
70
+ static readonly OPEN = 1;
71
+ static readonly CLOSING = 2;
72
+ static readonly CLOSED = 3;
73
+ readonly CONNECTING = 0;
74
+ readonly OPEN = 1;
75
+ readonly CLOSING = 2;
76
+ readonly CLOSED = 3;
77
+ readonly url: string;
78
+ readonly protocol = "";
79
+ readonly extensions = "";
80
+ binaryType: string;
81
+ onopen: ((event: Event) => void) | null;
82
+ onclose: ((event: CloseEvent) => void) | null;
83
+ onerror: ((event: Event) => void) | null;
84
+ onmessage: ((event: MessageEvent) => void) | null;
64
85
  private _channel;
65
86
  private _closed;
66
- onopen: (() => void) | null;
67
- onmessage: ((event: {
68
- data: ArrayBuffer | string;
69
- }) => void) | null;
70
- onclose: (() => void) | null;
71
- onerror: ((event: {
72
- error?: string;
73
- }) => void) | null;
74
- constructor(channel: RTCDataChannel);
75
- get readyState(): string;
76
- get binaryType(): string;
77
- set binaryType(_: string);
78
- send(data: ArrayBuffer | string | ArrayBufferView): void;
87
+ private _closeDispatched;
88
+ private _readyState;
89
+ constructor(pc: RTCPeerConnection, destination: string);
90
+ private _open;
91
+ get readyState(): number;
92
+ get bufferedAmount(): number;
93
+ send(data: string | ArrayBufferLike | ArrayBufferView): void;
79
94
  close(): void;
95
+ private _dispatch;
96
+ private _dispatchError;
97
+ private _dispatchClose;
80
98
  }
81
99
  /**
82
100
  * Create a libcurl.js transport factory from an existing WebRTC
package/dist/index.mjs CHANGED
@@ -91,10 +91,19 @@ async function connectNostr(_relay, _pubkey) {
91
91
  */
92
92
  /**
93
93
  * Wait for an RTCDataChannel to enter the "open" state.
94
- * Rejects if it closes or errors before opening.
94
+ *
95
+ * Also monitors the RTCPeerConnection for failure/closure, and
96
+ * accepts an optional AbortSignal for cancellation.
97
+ *
98
+ * Rejects if the channel closes, the peer connection fails, or
99
+ * the timeout elapses before the channel opens.
95
100
  */
96
- function waitForDataChannelOpen(channel, timeoutMs = 1e4) {
101
+ function waitForDataChannelOpen(channel, pc, timeoutMs = 1e4, signal) {
97
102
  return new Promise((resolve, reject) => {
103
+ if (signal?.aborted) {
104
+ reject(new DOMException("The operation was aborted.", "AbortError"));
105
+ return;
106
+ }
98
107
  if (channel.readyState === "open") {
99
108
  resolve();
100
109
  return;
@@ -105,22 +114,39 @@ function waitForDataChannelOpen(channel, timeoutMs = 1e4) {
105
114
  }, timeoutMs);
106
115
  const cleanup = () => {
107
116
  clearTimeout(timeout);
108
- channel.onopen = null;
109
- channel.onclose = null;
110
- channel.onerror = null;
117
+ channel.removeEventListener("open", onOpen);
118
+ channel.removeEventListener("close", onClose);
119
+ channel.removeEventListener("error", onError);
120
+ pc.removeEventListener("connectionstatechange", onStateChange);
121
+ signal?.removeEventListener("abort", onAbort);
111
122
  };
112
- channel.onopen = () => {
123
+ const onOpen = () => {
113
124
  cleanup();
114
125
  resolve();
115
126
  };
116
- channel.onclose = () => {
127
+ const onClose = () => {
117
128
  cleanup();
118
129
  reject(/* @__PURE__ */ new Error("DataChannel closed before opening"));
119
130
  };
120
- channel.onerror = (e) => {
131
+ const onError = () => {
121
132
  cleanup();
122
- reject(/* @__PURE__ */ new Error(`DataChannel error before opening: ${e}`));
133
+ reject(/* @__PURE__ */ new Error("DataChannel error before opening"));
134
+ };
135
+ const onStateChange = () => {
136
+ if (pc.connectionState === "failed" || pc.connectionState === "closed") {
137
+ cleanup();
138
+ reject(/* @__PURE__ */ new Error(`Peer connection ${pc.connectionState}`));
139
+ }
123
140
  };
141
+ const onAbort = () => {
142
+ cleanup();
143
+ reject(new DOMException("The operation was aborted.", "AbortError"));
144
+ };
145
+ channel.addEventListener("open", onOpen, { once: true });
146
+ channel.addEventListener("close", onClose, { once: true });
147
+ channel.addEventListener("error", onError, { once: true });
148
+ pc.addEventListener("connectionstatechange", onStateChange);
149
+ signal?.addEventListener("abort", onAbort, { once: true });
124
150
  });
125
151
  }
126
152
  /**
@@ -130,60 +156,111 @@ function waitForDataChannelOpen(channel, timeoutMs = 1e4) {
130
156
  * This is the low-level primitive used by `connect()` and
131
157
  * `libcurlTransport()`.
132
158
  */
133
- function openSocketChannel(pc, hostname, port, timeoutMs) {
159
+ function openSocketChannel(pc, hostname, port, timeoutMs, signal) {
134
160
  const label = `${SOCKET_PREFIX}${hostname}:${port}`;
135
161
  const channel = pc.createDataChannel(label, { ordered: true });
136
- return waitForDataChannelOpen(channel, timeoutMs).then(() => channel);
162
+ return waitForDataChannelOpen(channel, pc, timeoutMs, signal).then(() => channel);
137
163
  }
138
164
  //#endregion
139
165
  //#region lib/tunnel.ts
140
166
  /**
141
- * A minimal WebSocket-like wrapper around an RTCDataChannel.
167
+ * WebSocket-compatible wrapper around an RTCDataChannel.
142
168
  *
143
- * libcurl.js expects its transport factory to return objects
144
- * with `send()`, `close()`, `onopen`, `onmessage`, `onclose`,
145
- * and `onerror` which is exactly the RTCDataChannel API, so
146
- * the wrapper is thin.
169
+ * libcurl.js compares `readyState` against `WebSocket.OPEN` (numeric 1)
170
+ * and expects static constants (`CONNECTING`, `OPEN`, `CLOSING`, `CLOSED`).
171
+ * Using `EventTarget` ensures `addEventListener` / `removeEventListener`
172
+ * work, which libcurl's poll loop depends on.
147
173
  */
148
- var DataChannelSocket = class {
149
- _channel;
150
- _closed = false;
174
+ var DataChannelSocket = class DataChannelSocket extends EventTarget {
175
+ static CONNECTING = 0;
176
+ static OPEN = 1;
177
+ static CLOSING = 2;
178
+ static CLOSED = 3;
179
+ CONNECTING = 0;
180
+ OPEN = 1;
181
+ CLOSING = 2;
182
+ CLOSED = 3;
183
+ url;
184
+ protocol = "";
185
+ extensions = "";
186
+ binaryType = "arraybuffer";
151
187
  onopen = null;
152
- onmessage = null;
153
188
  onclose = null;
154
189
  onerror = null;
155
- constructor(channel) {
190
+ onmessage = null;
191
+ _channel = null;
192
+ _closed = false;
193
+ _closeDispatched = false;
194
+ _readyState = DataChannelSocket.CONNECTING;
195
+ constructor(pc, destination) {
196
+ super();
197
+ this.url = `wss://pulsar-tunnel.local/${destination}`;
198
+ const channel = pc.createDataChannel(`${SOCKET_PREFIX}${destination}`, { ordered: true });
199
+ channel.binaryType = "arraybuffer";
156
200
  this._channel = channel;
157
- channel.onopen = () => {
158
- if (!this._closed) this.onopen?.();
159
- };
201
+ this._open(channel, pc);
202
+ }
203
+ async _open(channel, pc) {
204
+ try {
205
+ await waitForDataChannelOpen(channel, pc);
206
+ } catch (error) {
207
+ if (!this._closed) {
208
+ console.error(`[DataChannelSocket] Failed to open channel: ${error instanceof Error ? error.message : String(error)}`);
209
+ this._dispatchError();
210
+ }
211
+ this._dispatchClose();
212
+ return;
213
+ }
214
+ if (this._closed) {
215
+ channel.close();
216
+ this._dispatchClose();
217
+ return;
218
+ }
219
+ this._channel = channel;
220
+ this._readyState = DataChannelSocket.OPEN;
160
221
  channel.onmessage = (event) => {
161
- if (!this._closed) this.onmessage?.({ data: event.data });
222
+ this._dispatch(new MessageEvent("message", { data: event.data }));
162
223
  };
163
224
  channel.onclose = () => {
164
- this._closed = true;
165
- this.onclose?.();
225
+ this._readyState = DataChannelSocket.CLOSED;
226
+ this._dispatchClose();
166
227
  };
167
- channel.onerror = (event) => {
168
- this.onerror?.({ error: String(event) });
228
+ channel.onerror = () => {
229
+ this._dispatchError();
169
230
  };
231
+ this._dispatch(new Event("open"));
170
232
  }
171
233
  get readyState() {
172
- return this._channel.readyState;
234
+ return this._readyState;
173
235
  }
174
- get binaryType() {
175
- return "arraybuffer";
236
+ get bufferedAmount() {
237
+ return this._channel?.bufferedAmount ?? 0;
176
238
  }
177
- set binaryType(_) {}
178
239
  send(data) {
179
- if (this._closed || this._channel.readyState !== "open") return;
180
- this._channel.send(data);
240
+ if (this._readyState !== DataChannelSocket.OPEN || !this._channel) throw new Error("DataChannelSocket is not open");
241
+ if (typeof data === "string") this._channel.send(data);
242
+ else this._channel.send(data);
181
243
  }
182
244
  close() {
245
+ if (this._closed) return;
183
246
  this._closed = true;
184
- try {
185
- this._channel.close();
186
- } catch {}
247
+ if (this._channel && this._channel.readyState !== "closed") this._channel.close();
248
+ else this._dispatchClose();
249
+ }
250
+ _dispatch(event) {
251
+ const handlerName = `on${event.type}`;
252
+ const handler = this[handlerName];
253
+ if (handler) handler(event);
254
+ this.dispatchEvent(event);
255
+ }
256
+ _dispatchError() {
257
+ this._dispatch(new Event("error"));
258
+ }
259
+ _dispatchClose() {
260
+ if (this._closeDispatched) return;
261
+ this._closeDispatched = true;
262
+ this._readyState = DataChannelSocket.CLOSED;
263
+ this._dispatch(new CloseEvent("close"));
187
264
  }
188
265
  };
189
266
  /**
@@ -218,7 +295,7 @@ function libcurlTransport(pc) {
218
295
  }
219
296
  if (!dest) throw new Error(`libcurl transport: no destination found in URL "${url}"`);
220
297
  if (dest.lastIndexOf(":") === -1) throw new Error(`libcurl transport: invalid destination "${dest}" — expected "hostname:port"`);
221
- return new DataChannelSocket(pc.createDataChannel(`${SOCKET_PREFIX}${dest}`, { ordered: true }));
298
+ return new DataChannelSocket(pc, dest);
222
299
  };
223
300
  }
224
301
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@abndnce/pulsar-client",
3
3
  "type": "module",
4
- "version": "0.0.8",
4
+ "version": "0.0.9",
5
5
  "homepage": "https://github.com/abndnce/pulsar",
6
6
  "license": "MIT",
7
7
  "exports": {