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.
- package/dist/client/browser.d.ts +30 -0
- package/dist/client/browser.d.ts.map +1 -0
- package/dist/client/browser.js +27 -0
- package/dist/client/browser.js.map +1 -0
- package/dist/client/create.d.ts +49 -0
- package/dist/client/create.d.ts.map +1 -0
- package/dist/client/create.js +543 -0
- package/dist/client/create.js.map +1 -0
- package/dist/client/index.d.ts +5 -16
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +8 -511
- package/dist/client/index.js.map +1 -1
- package/dist/client/types.d.ts +4 -0
- package/dist/client/types.d.ts.map +1 -1
- package/package.json +6 -1
|
@@ -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
|