raffel 1.0.15-next.239a2ec → 1.0.17-next.f28271f

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.
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Raffel WebSocket Client SDK — Browser entry point
3
+ *
4
+ * Uses the native browser WebSocket API. No `ws` dependency needed.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { createRaffelClient } from 'raffel/client/browser'
9
+ *
10
+ * const client = createRaffelClient({ url: 'wss://api.example.com/ws', token: 'xxx' })
11
+ *
12
+ * // RPC
13
+ * const user = await client.call('users.get', { id: '123' })
14
+ *
15
+ * // Channels
16
+ * const channel = client.subscribe('orders')
17
+ * channel.on('created', (data) => console.log(data))
18
+ *
19
+ * // Streams
20
+ * for await (const chunk of client.stream('logs.tail')) {
21
+ * console.log(chunk)
22
+ * }
23
+ * ```
24
+ */
25
+ export { createRaffelClient } from './create.js';
26
+ export type { RaffelClientOptions, RaffelClient, ClientStream, CallOptions, ClientChannel, ClientChannelMember, } from './types.js';
27
+ export { createReconnectController, getReconnectDelay } from './reconnect.js';
28
+ export type { ReconnectConfig, ReconnectState } from './reconnect.js';
29
+ export type { WebSocketLike, WebSocketConstructor } from './create.js';
30
+ //# sourceMappingURL=browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/client/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAChD,YAAY,EACV,mBAAmB,EACnB,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,aAAa,EACb,mBAAmB,GACpB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAC7E,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACrE,YAAY,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Raffel WebSocket Client SDK — Browser entry point
3
+ *
4
+ * Uses the native browser WebSocket API. No `ws` dependency needed.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { createRaffelClient } from 'raffel/client/browser'
9
+ *
10
+ * const client = createRaffelClient({ url: 'wss://api.example.com/ws', token: 'xxx' })
11
+ *
12
+ * // RPC
13
+ * const user = await client.call('users.get', { id: '123' })
14
+ *
15
+ * // Channels
16
+ * const channel = client.subscribe('orders')
17
+ * channel.on('created', (data) => console.log(data))
18
+ *
19
+ * // Streams
20
+ * for await (const chunk of client.stream('logs.tail')) {
21
+ * console.log(chunk)
22
+ * }
23
+ * ```
24
+ */
25
+ export { createRaffelClient } from './create.js';
26
+ export { createReconnectController, getReconnectDelay } from './reconnect.js';
27
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/client/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAShD,OAAO,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Raffel WebSocket Client SDK — Universal (Browser + Node.js)
3
+ *
4
+ * This is the core client implementation that works in both environments.
5
+ * It uses the standard WebSocket API (addEventListener) which is supported
6
+ * by both native browser WebSocket and the `ws` npm package.
7
+ *
8
+ * @example Browser
9
+ * ```typescript
10
+ * import { createRaffelClient } from 'raffel/client/browser'
11
+ * const client = createRaffelClient({ url: 'wss://api.example.com/ws' })
12
+ * ```
13
+ *
14
+ * @example Node.js
15
+ * ```typescript
16
+ * import { createRaffelClient } from 'raffel/client'
17
+ * const client = createRaffelClient({ url: 'ws://localhost:3000/ws' })
18
+ * ```
19
+ */
20
+ import type { RaffelClientOptions, RaffelClient } from './types.js';
21
+ export type { RaffelClientOptions, RaffelClient, ClientStream, CallOptions, ClientChannel, ClientChannelMember, } from './types.js';
22
+ export { createReconnectController, getReconnectDelay } from './reconnect.js';
23
+ export type { ReconnectConfig, ReconnectState } from './reconnect.js';
24
+ /**
25
+ * Minimal WebSocket interface that both browser native and `ws` package satisfy.
26
+ */
27
+ export interface WebSocketLike {
28
+ readonly readyState: number;
29
+ send(data: string): void;
30
+ close(code?: number, reason?: string): void;
31
+ addEventListener(type: string, listener: (event: any) => void): void;
32
+ }
33
+ /**
34
+ * Constructor type for WebSocket-like objects.
35
+ */
36
+ export interface WebSocketConstructor {
37
+ new (url: string, protocols?: string | string[]): WebSocketLike;
38
+ readonly CONNECTING: number;
39
+ readonly OPEN: number;
40
+ }
41
+ /**
42
+ * Create a Raffel WebSocket client (universal — works in browser and Node.js)
43
+ *
44
+ * @param options - Client configuration (url is required)
45
+ * @returns RaffelClient instance
46
+ */
47
+ export declare function createRaffelClient(options: RaffelClientOptions): RaffelClient;
48
+ export declare function createRaffelClient(url: string): RaffelClient;
49
+ //# sourceMappingURL=create.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/client/create.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EACV,mBAAmB,EACnB,YAAY,EAKb,MAAM,YAAY,CAAA;AAEnB,YAAY,EACV,mBAAmB,EACnB,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,aAAa,EACb,mBAAmB,GACpB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAC7E,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAoBrE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3C,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI,CAAA;CACrE;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,KAAI,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,aAAa,CAAA;IAC9D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAiCD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CAAA;AAC9E,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAAA"}
@@ -0,0 +1,543 @@
1
+ /**
2
+ * Raffel WebSocket Client SDK — Universal (Browser + Node.js)
3
+ *
4
+ * This is the core client implementation that works in both environments.
5
+ * It uses the standard WebSocket API (addEventListener) which is supported
6
+ * by both native browser WebSocket and the `ws` npm package.
7
+ *
8
+ * @example Browser
9
+ * ```typescript
10
+ * import { createRaffelClient } from 'raffel/client/browser'
11
+ * const client = createRaffelClient({ url: 'wss://api.example.com/ws' })
12
+ * ```
13
+ *
14
+ * @example Node.js
15
+ * ```typescript
16
+ * import { createRaffelClient } from 'raffel/client'
17
+ * const client = createRaffelClient({ url: 'ws://localhost:3000/ws' })
18
+ * ```
19
+ */
20
+ import { createReconnectController } from './reconnect.js';
21
+ export { createReconnectController, getReconnectDelay } from './reconnect.js';
22
+ let idCounter = 0;
23
+ function nextId() {
24
+ return `c_${++idCounter}_${Date.now().toString(36)}`;
25
+ }
26
+ /** WebSocket readyState constants (same in both browser and ws) */
27
+ const WS_CONNECTING = 0;
28
+ const WS_OPEN = 1;
29
+ /**
30
+ * Resolve the WebSocket constructor to use.
31
+ * Priority: explicit option > globalThis.WebSocket
32
+ */
33
+ function resolveWebSocket(explicit) {
34
+ const WS = explicit
35
+ ?? (typeof globalThis !== 'undefined' && globalThis.WebSocket);
36
+ if (!WS) {
37
+ throw new Error('No WebSocket implementation available. ' +
38
+ 'In Node.js < 21, pass { WebSocket } from the "ws" package. ' +
39
+ 'In browser, globalThis.WebSocket is used automatically.');
40
+ }
41
+ // WS is already a constructor — just attach constants if missing
42
+ const result = WS;
43
+ if (typeof result.CONNECTING !== 'number') {
44
+ Object.defineProperty(result, 'CONNECTING', { value: WS_CONNECTING });
45
+ }
46
+ if (typeof result.OPEN !== 'number') {
47
+ Object.defineProperty(result, 'OPEN', { value: WS_OPEN });
48
+ }
49
+ return result;
50
+ }
51
+ export function createRaffelClient(urlOrOptions) {
52
+ const opts = typeof urlOrOptions === 'string'
53
+ ? { url: urlOrOptions }
54
+ : urlOrOptions;
55
+ const { url, reconnect: autoReconnect = true, maxReconnectAttempts = Infinity, reconnectDelay = 1000, maxReconnectDelay = 30000, reconnectBackoff = 2, connectTimeout = 10000, requestTimeout = 30000, token, protocols, onConnect, onDisconnect, onReconnect, onError, } = opts;
56
+ const WS = resolveWebSocket(opts.WebSocket);
57
+ let ws = null;
58
+ let connected = false;
59
+ let intentionalClose = false;
60
+ const pendingRequests = new Map();
61
+ const pendingStreams = new Map();
62
+ const channelSubs = new Map();
63
+ // Build URL with token if provided
64
+ const wsUrl = token
65
+ ? `${url}${url.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}`
66
+ : url;
67
+ const reconnectController = createReconnectController({
68
+ maxAttempts: maxReconnectAttempts,
69
+ initialDelay: reconnectDelay,
70
+ maxDelay: maxReconnectDelay,
71
+ backoff: reconnectBackoff,
72
+ }, () => connect(), onReconnect);
73
+ function connect() {
74
+ if (ws && (ws.readyState === WS.CONNECTING || ws.readyState === WS.OPEN)) {
75
+ return;
76
+ }
77
+ const connectTimer = setTimeout(() => {
78
+ if (ws && ws.readyState === WS.CONNECTING) {
79
+ ws.close();
80
+ }
81
+ }, connectTimeout);
82
+ ws = new WS(wsUrl, protocols);
83
+ ws.addEventListener('open', () => {
84
+ clearTimeout(connectTimer);
85
+ connected = true;
86
+ reconnectController.reset();
87
+ onConnect?.();
88
+ });
89
+ ws.addEventListener('message', (event) => {
90
+ const data = event.data;
91
+ handleMessage(typeof data === 'string' ? data : String(data));
92
+ });
93
+ ws.addEventListener('close', (event) => {
94
+ clearTimeout(connectTimer);
95
+ connected = false;
96
+ const code = event.code ?? 1006;
97
+ const reasonStr = event.reason ?? '';
98
+ // Reject all pending requests
99
+ for (const [id, pending] of pendingRequests) {
100
+ if (pending.timer)
101
+ clearTimeout(pending.timer);
102
+ pending.reject(new Error(`Connection closed: ${code} ${reasonStr}`));
103
+ pendingRequests.delete(id);
104
+ }
105
+ // Complete all pending streams with error
106
+ for (const [id, stream] of pendingStreams) {
107
+ stream.error = new Error(`Connection closed: ${code} ${reasonStr}`);
108
+ stream.done = true;
109
+ stream.notify?.();
110
+ stream.notify = null;
111
+ pendingStreams.delete(id);
112
+ }
113
+ onDisconnect?.(code, reasonStr);
114
+ // Auto-reconnect
115
+ if (!intentionalClose && autoReconnect) {
116
+ reconnectController.schedule();
117
+ }
118
+ });
119
+ ws.addEventListener('error', (event) => {
120
+ clearTimeout(connectTimer);
121
+ const error = event.error ?? event.message ?? 'WebSocket error';
122
+ onError?.(error instanceof Error ? error : new Error(String(error)));
123
+ });
124
+ }
125
+ /**
126
+ * Extract the base stream ID from compound IDs.
127
+ * Router generates IDs like `{baseId}:stream:data:{timestamp}`, `{baseId}:stream:end`, etc.
128
+ */
129
+ function extractBaseStreamId(id) {
130
+ const streamIdx = id.indexOf(':stream:');
131
+ if (streamIdx === -1)
132
+ return undefined;
133
+ return id.slice(0, streamIdx);
134
+ }
135
+ function handleMessage(raw) {
136
+ let parsed;
137
+ try {
138
+ parsed = JSON.parse(raw);
139
+ }
140
+ catch {
141
+ return; // Ignore unparseable messages
142
+ }
143
+ const type = parsed.type;
144
+ // --- Channel Messages ---
145
+ if (type === 'event' && parsed.channel) {
146
+ const channelName = parsed.channel;
147
+ const eventName = parsed.event;
148
+ const sub = channelSubs.get(channelName);
149
+ if (sub) {
150
+ // Update members for presence events
151
+ if (eventName === 'member_added' && parsed.data) {
152
+ const memberData = parsed.data;
153
+ sub.members.push(memberData);
154
+ }
155
+ else if (eventName === 'member_removed' && parsed.data) {
156
+ const memberData = parsed.data;
157
+ sub.members = sub.members.filter((m) => m.id !== memberData.id);
158
+ }
159
+ const handlers = sub.listeners.get(eventName);
160
+ if (handlers) {
161
+ for (const handler of handlers) {
162
+ handler(parsed.data);
163
+ }
164
+ }
165
+ // Also fire wildcard '*' listeners
166
+ const wildcardHandlers = sub.listeners.get('*');
167
+ if (wildcardHandlers) {
168
+ for (const handler of wildcardHandlers) {
169
+ handler({ event: eventName, data: parsed.data });
170
+ }
171
+ }
172
+ }
173
+ return;
174
+ }
175
+ // Handle subscribed response
176
+ if (type === 'subscribed') {
177
+ const channelName = parsed.channel;
178
+ const sub = channelSubs.get(channelName);
179
+ if (sub && parsed.members) {
180
+ sub.members = parsed.members;
181
+ }
182
+ const id = parsed.id;
183
+ if (id) {
184
+ const pending = pendingRequests.get(id);
185
+ if (pending) {
186
+ if (pending.timer)
187
+ clearTimeout(pending.timer);
188
+ pending.resolve(parsed);
189
+ pendingRequests.delete(id);
190
+ }
191
+ }
192
+ return;
193
+ }
194
+ // Handle subscribed:batch response
195
+ if (type === 'subscribed:batch') {
196
+ const id = parsed.id;
197
+ if (id) {
198
+ const pending = pendingRequests.get(id);
199
+ if (pending) {
200
+ if (pending.timer)
201
+ clearTimeout(pending.timer);
202
+ pending.resolve(parsed);
203
+ pendingRequests.delete(id);
204
+ }
205
+ }
206
+ return;
207
+ }
208
+ // Handle unsubscribed response
209
+ if (type === 'unsubscribed') {
210
+ const id = parsed.id;
211
+ if (id) {
212
+ const pending = pendingRequests.get(id);
213
+ if (pending) {
214
+ if (pending.timer)
215
+ clearTimeout(pending.timer);
216
+ pending.resolve(parsed);
217
+ pendingRequests.delete(id);
218
+ }
219
+ }
220
+ return;
221
+ }
222
+ // Handle auth:refreshed response
223
+ if (type === 'auth:refreshed') {
224
+ const id = parsed.id;
225
+ if (id) {
226
+ const pending = pendingRequests.get(id);
227
+ if (pending) {
228
+ if (pending.timer)
229
+ clearTimeout(pending.timer);
230
+ pending.resolve(parsed);
231
+ pendingRequests.delete(id);
232
+ }
233
+ }
234
+ return;
235
+ }
236
+ // --- Envelope Messages ---
237
+ const id = parsed.id;
238
+ if (!id)
239
+ return;
240
+ // Handle channel-level errors (with id)
241
+ if (type === 'error' && (parsed.code || parsed.status)) {
242
+ const pending = pendingRequests.get(id);
243
+ if (pending) {
244
+ if (pending.timer)
245
+ clearTimeout(pending.timer);
246
+ const error = new Error(parsed.message ?? 'Unknown error');
247
+ error.code = parsed.code;
248
+ error.status = parsed.status;
249
+ pending.reject(error);
250
+ pendingRequests.delete(id);
251
+ return;
252
+ }
253
+ }
254
+ // Check if this is a stream chunk
255
+ if (type === 'stream:data' || type === 'stream:chunk') {
256
+ const baseId = extractBaseStreamId(id);
257
+ const stream = pendingStreams.get(id) ?? (baseId ? pendingStreams.get(baseId) : undefined);
258
+ if (stream) {
259
+ stream.queue.push(parsed.payload);
260
+ stream.notify?.();
261
+ stream.notify = null;
262
+ }
263
+ return;
264
+ }
265
+ // Stream start (ack from server)
266
+ if (type === 'stream:start') {
267
+ return;
268
+ }
269
+ // Stream end
270
+ if (type === 'stream:end') {
271
+ const baseId = extractBaseStreamId(id);
272
+ const stream = pendingStreams.get(id) ?? (baseId ? pendingStreams.get(baseId) : undefined);
273
+ if (stream) {
274
+ stream.done = true;
275
+ stream.notify?.();
276
+ stream.notify = null;
277
+ pendingStreams.delete(id);
278
+ if (baseId)
279
+ pendingStreams.delete(baseId);
280
+ }
281
+ return;
282
+ }
283
+ // Stream error
284
+ if (type === 'stream:error') {
285
+ const baseId = extractBaseStreamId(id);
286
+ const stream = pendingStreams.get(id) ?? (baseId ? pendingStreams.get(baseId) : undefined);
287
+ if (stream) {
288
+ const payload = parsed.payload;
289
+ stream.error = new Error(payload?.message ?? 'Stream error');
290
+ stream.done = true;
291
+ stream.notify?.();
292
+ stream.notify = null;
293
+ pendingStreams.delete(id);
294
+ if (baseId)
295
+ pendingStreams.delete(baseId);
296
+ }
297
+ return;
298
+ }
299
+ // Error response
300
+ if (type === 'error') {
301
+ const baseId = id.endsWith(':error') ? id.slice(0, -6) : id;
302
+ const pending = pendingRequests.get(baseId) ?? pendingRequests.get(id);
303
+ if (pending) {
304
+ if (pending.timer)
305
+ clearTimeout(pending.timer);
306
+ const payload = parsed.payload;
307
+ const error = new Error(payload?.message ?? 'Unknown error');
308
+ error.code = payload?.code;
309
+ pending.reject(error);
310
+ pendingRequests.delete(baseId);
311
+ pendingRequests.delete(id);
312
+ }
313
+ return;
314
+ }
315
+ // Response (procedure result)
316
+ if (type === 'response') {
317
+ const baseId = id.endsWith(':response') ? id.slice(0, -9) : id;
318
+ const pending = pendingRequests.get(baseId) ?? pendingRequests.get(id);
319
+ if (pending) {
320
+ if (pending.timer)
321
+ clearTimeout(pending.timer);
322
+ pending.resolve(parsed.payload);
323
+ pendingRequests.delete(baseId);
324
+ pendingRequests.delete(id);
325
+ }
326
+ return;
327
+ }
328
+ }
329
+ function send(message) {
330
+ if (!ws || ws.readyState !== WS.OPEN) {
331
+ throw new Error('Not connected');
332
+ }
333
+ ws.send(JSON.stringify(message));
334
+ }
335
+ // Connect immediately
336
+ connect();
337
+ const client = {
338
+ call(procedure, payload, callOptions) {
339
+ return new Promise((resolve, reject) => {
340
+ if (!ws || ws.readyState !== WS.OPEN) {
341
+ reject(new Error('Not connected'));
342
+ return;
343
+ }
344
+ const id = nextId();
345
+ const timeout = callOptions?.timeout ?? requestTimeout;
346
+ const timer = timeout > 0
347
+ ? setTimeout(() => {
348
+ pendingRequests.delete(id);
349
+ reject(new Error(`Request timeout: ${procedure}`));
350
+ }, timeout)
351
+ : undefined;
352
+ pendingRequests.set(id, {
353
+ resolve: resolve,
354
+ reject,
355
+ timer,
356
+ });
357
+ try {
358
+ send({
359
+ id,
360
+ procedure,
361
+ type: 'request',
362
+ payload: payload ?? {},
363
+ metadata: callOptions?.metadata ?? {},
364
+ });
365
+ }
366
+ catch (err) {
367
+ if (timer)
368
+ clearTimeout(timer);
369
+ pendingRequests.delete(id);
370
+ reject(err);
371
+ }
372
+ });
373
+ },
374
+ stream(procedure, payload, callOptions) {
375
+ const id = nextId();
376
+ const streamState = {
377
+ queue: [],
378
+ notify: null,
379
+ done: false,
380
+ };
381
+ pendingStreams.set(id, streamState);
382
+ // Send the stream request
383
+ try {
384
+ send({
385
+ id,
386
+ procedure,
387
+ type: 'stream:start',
388
+ payload: payload ?? {},
389
+ metadata: callOptions?.metadata ?? {},
390
+ });
391
+ }
392
+ catch (err) {
393
+ streamState.error = err;
394
+ streamState.done = true;
395
+ pendingStreams.delete(id);
396
+ }
397
+ const iterator = {
398
+ async next() {
399
+ while (true) {
400
+ if (streamState.queue.length > 0) {
401
+ return { value: streamState.queue.shift(), done: false };
402
+ }
403
+ if (streamState.done) {
404
+ if (streamState.error)
405
+ throw streamState.error;
406
+ return { value: undefined, done: true };
407
+ }
408
+ await new Promise((r) => { streamState.notify = r; });
409
+ }
410
+ },
411
+ };
412
+ return {
413
+ [Symbol.asyncIterator]() {
414
+ return iterator;
415
+ },
416
+ cancel() {
417
+ streamState.done = true;
418
+ streamState.notify?.();
419
+ streamState.notify = null;
420
+ pendingStreams.delete(id);
421
+ try {
422
+ send({ id, type: 'cancel' });
423
+ }
424
+ catch {
425
+ // Ignore send errors on cancel
426
+ }
427
+ },
428
+ };
429
+ },
430
+ subscribe(channel, since) {
431
+ const sub = {
432
+ listeners: new Map(),
433
+ members: [],
434
+ };
435
+ channelSubs.set(channel, sub);
436
+ const msgId = nextId();
437
+ try {
438
+ send({
439
+ id: msgId,
440
+ type: 'subscribe',
441
+ channel,
442
+ ...(since ? { since } : {}),
443
+ });
444
+ }
445
+ catch {
446
+ // Will be handled by reconnection
447
+ }
448
+ const clientChannel = {
449
+ on(event, handler) {
450
+ let handlers = sub.listeners.get(event);
451
+ if (!handlers) {
452
+ handlers = new Set();
453
+ sub.listeners.set(event, handlers);
454
+ }
455
+ handlers.add(handler);
456
+ },
457
+ off(event, handler) {
458
+ const handlers = sub.listeners.get(event);
459
+ if (handlers) {
460
+ handlers.delete(handler);
461
+ if (handlers.size === 0)
462
+ sub.listeners.delete(event);
463
+ }
464
+ },
465
+ unsubscribe() {
466
+ client.unsubscribe(channel);
467
+ },
468
+ get name() {
469
+ return channel;
470
+ },
471
+ get members() {
472
+ return sub.members;
473
+ },
474
+ };
475
+ return clientChannel;
476
+ },
477
+ unsubscribe(channel) {
478
+ channelSubs.delete(channel);
479
+ const msgId = nextId();
480
+ try {
481
+ send({
482
+ id: msgId,
483
+ type: 'unsubscribe',
484
+ channel,
485
+ });
486
+ }
487
+ catch {
488
+ // Ignore send errors on unsubscribe
489
+ }
490
+ },
491
+ publish(channel, event, data) {
492
+ const msgId = nextId();
493
+ send({
494
+ id: msgId,
495
+ type: 'publish',
496
+ channel,
497
+ event,
498
+ data,
499
+ });
500
+ },
501
+ async refreshAuth(newToken) {
502
+ return new Promise((resolve, reject) => {
503
+ if (!ws || ws.readyState !== WS.OPEN) {
504
+ reject(new Error('Not connected'));
505
+ return;
506
+ }
507
+ const id = nextId();
508
+ const timer = setTimeout(() => {
509
+ pendingRequests.delete(id);
510
+ reject(new Error('Auth refresh timeout'));
511
+ }, requestTimeout);
512
+ pendingRequests.set(id, {
513
+ resolve: () => resolve(),
514
+ reject,
515
+ timer,
516
+ });
517
+ try {
518
+ send({ id, type: 'auth:refresh', token: newToken });
519
+ }
520
+ catch (err) {
521
+ clearTimeout(timer);
522
+ pendingRequests.delete(id);
523
+ reject(err);
524
+ }
525
+ });
526
+ },
527
+ close() {
528
+ intentionalClose = true;
529
+ reconnectController.stop();
530
+ if (ws) {
531
+ ws.close(1000, 'Client closing');
532
+ ws = null;
533
+ }
534
+ connected = false;
535
+ channelSubs.clear();
536
+ },
537
+ get connected() {
538
+ return connected;
539
+ },
540
+ };
541
+ return client;
542
+ }
543
+ //# sourceMappingURL=create.js.map