@wooksjs/ws-client 0.7.0
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/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/index.cjs +409 -0
- package/dist/index.d.ts +144 -0
- package/dist/index.mjs +406 -0
- package/package.json +59 -0
- package/scripts/setup-skills.js +78 -0
- package/skills/wooksjs-ws-client/SKILL.md +35 -0
- package/skills/wooksjs-ws-client/core.md +203 -0
- package/skills/wooksjs-ws-client/push.md +111 -0
- package/skills/wooksjs-ws-client/reconnect.md +145 -0
- package/skills/wooksjs-ws-client/rpc.md +134 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
//#region packages/ws-client/src/ws-client-error.ts
|
|
2
|
+
/** Error from the WebSocket client (server error reply, timeout, or disconnect). */
|
|
3
|
+
var WsClientError = class extends Error {
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message ?? `WsClientError ${code}`);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = "WsClientError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region packages/ws-client/src/rpc-tracker.ts
|
|
13
|
+
/** Tracks pending RPC calls by correlation ID. */
|
|
14
|
+
var RpcTracker = class {
|
|
15
|
+
nextId = 1;
|
|
16
|
+
pending = /* @__PURE__ */ new Map();
|
|
17
|
+
/** Generate a new unique message ID. */
|
|
18
|
+
generateId() {
|
|
19
|
+
return this.nextId++;
|
|
20
|
+
}
|
|
21
|
+
/** Track a new pending RPC. Returns a promise that resolves/rejects based on the reply. */
|
|
22
|
+
track(id, timeout) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const timer = setTimeout(() => {
|
|
25
|
+
this.pending.delete(id);
|
|
26
|
+
reject(new WsClientError(408, "RPC timeout"));
|
|
27
|
+
}, timeout);
|
|
28
|
+
this.pending.set(id, {
|
|
29
|
+
resolve,
|
|
30
|
+
reject,
|
|
31
|
+
timer
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** Resolve a pending RPC with a server reply. Returns true if an RPC was found. */
|
|
36
|
+
resolve(reply) {
|
|
37
|
+
const id = typeof reply.id === "number" ? reply.id : Number(reply.id);
|
|
38
|
+
const entry = this.pending.get(id);
|
|
39
|
+
if (!entry) return false;
|
|
40
|
+
clearTimeout(entry.timer);
|
|
41
|
+
this.pending.delete(id);
|
|
42
|
+
if (reply.error) entry.reject(new WsClientError(reply.error.code, reply.error.message));
|
|
43
|
+
else entry.resolve(reply.data);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
/** Reject all pending RPCs (on disconnect or close). */
|
|
47
|
+
rejectAll(code, message) {
|
|
48
|
+
for (const [, entry] of this.pending) {
|
|
49
|
+
clearTimeout(entry.timer);
|
|
50
|
+
entry.reject(new WsClientError(code, message));
|
|
51
|
+
}
|
|
52
|
+
this.pending.clear();
|
|
53
|
+
}
|
|
54
|
+
/** Number of pending RPCs. */
|
|
55
|
+
get size() {
|
|
56
|
+
return this.pending.size;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region packages/ws-client/src/push-dispatcher.ts
|
|
62
|
+
/** Dispatches server push messages to registered client-side listeners. */
|
|
63
|
+
var PushDispatcher = class {
|
|
64
|
+
/** Exact match: key = "event:path", value = set of handlers. */
|
|
65
|
+
exact = /* @__PURE__ */ new Map();
|
|
66
|
+
/** Wildcard listeners (path ends with "*"). */
|
|
67
|
+
wildcards = [];
|
|
68
|
+
/**
|
|
69
|
+
* Register a push listener. Returns an unregister function.
|
|
70
|
+
*
|
|
71
|
+
* - Exact path: `"/chat/rooms/lobby"` — O(1) Map lookup.
|
|
72
|
+
* - Wildcard: `"/chat/rooms/*"` — `startsWith` prefix check.
|
|
73
|
+
*/
|
|
74
|
+
on(event, pathPattern, handler) {
|
|
75
|
+
const h = handler;
|
|
76
|
+
if (pathPattern.endsWith("*")) {
|
|
77
|
+
const prefix = pathPattern.slice(0, -1);
|
|
78
|
+
const entry = {
|
|
79
|
+
event,
|
|
80
|
+
prefix,
|
|
81
|
+
handler: h
|
|
82
|
+
};
|
|
83
|
+
this.wildcards.push(entry);
|
|
84
|
+
return () => {
|
|
85
|
+
const idx = this.wildcards.indexOf(entry);
|
|
86
|
+
if (idx !== -1) this.wildcards.splice(idx, 1);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const key = `${event}:${pathPattern}`;
|
|
90
|
+
let set = this.exact.get(key);
|
|
91
|
+
if (!set) {
|
|
92
|
+
set = /* @__PURE__ */ new Set();
|
|
93
|
+
this.exact.set(key, set);
|
|
94
|
+
}
|
|
95
|
+
set.add(h);
|
|
96
|
+
return () => {
|
|
97
|
+
set.delete(h);
|
|
98
|
+
if (set.size === 0) this.exact.delete(key);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Dispatch a server push message to matching listeners. */
|
|
102
|
+
dispatch(msg) {
|
|
103
|
+
const pushEvent = {
|
|
104
|
+
event: msg.event,
|
|
105
|
+
path: msg.path,
|
|
106
|
+
params: msg.params ?? {},
|
|
107
|
+
data: msg.data
|
|
108
|
+
};
|
|
109
|
+
const key = `${msg.event}:${msg.path}`;
|
|
110
|
+
const exactSet = this.exact.get(key);
|
|
111
|
+
if (exactSet) for (const handler of exactSet) handler(pushEvent);
|
|
112
|
+
for (const { event, prefix, handler } of this.wildcards) if (msg.event === event && msg.path.startsWith(prefix)) handler(pushEvent);
|
|
113
|
+
}
|
|
114
|
+
/** Remove all listeners. */
|
|
115
|
+
clear() {
|
|
116
|
+
this.exact.clear();
|
|
117
|
+
this.wildcards.length = 0;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region packages/ws-client/src/message-queue.ts
|
|
123
|
+
/** Queue for outbound messages that accumulate while disconnected. */
|
|
124
|
+
var MessageQueue = class {
|
|
125
|
+
queue = [];
|
|
126
|
+
/** Enqueue a serialized message. */
|
|
127
|
+
enqueue(serialized) {
|
|
128
|
+
this.queue.push(serialized);
|
|
129
|
+
}
|
|
130
|
+
/** Flush all queued messages via the provided send function. Returns number flushed. */
|
|
131
|
+
flush(send) {
|
|
132
|
+
const count = this.queue.length;
|
|
133
|
+
for (const msg of this.queue) send(msg);
|
|
134
|
+
this.queue.length = 0;
|
|
135
|
+
return count;
|
|
136
|
+
}
|
|
137
|
+
/** Discard all queued messages. */
|
|
138
|
+
clear() {
|
|
139
|
+
this.queue.length = 0;
|
|
140
|
+
}
|
|
141
|
+
/** Number of queued messages. */
|
|
142
|
+
get size() {
|
|
143
|
+
return this.queue.length;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region packages/ws-client/src/reconnect.ts
|
|
149
|
+
/** Normalize the user-facing reconnect option into a full config. */
|
|
150
|
+
function normalizeReconnectConfig(opt) {
|
|
151
|
+
if (opt === true) return {
|
|
152
|
+
enabled: true,
|
|
153
|
+
maxRetries: Infinity,
|
|
154
|
+
baseDelay: 1e3,
|
|
155
|
+
maxDelay: 3e4,
|
|
156
|
+
backoff: "exponential"
|
|
157
|
+
};
|
|
158
|
+
if (opt === false || opt === void 0) return {
|
|
159
|
+
enabled: false,
|
|
160
|
+
maxRetries: 0,
|
|
161
|
+
baseDelay: 1e3,
|
|
162
|
+
maxDelay: 3e4,
|
|
163
|
+
backoff: "exponential"
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
enabled: opt.enabled,
|
|
167
|
+
maxRetries: opt.maxRetries ?? Infinity,
|
|
168
|
+
baseDelay: opt.baseDelay ?? 1e3,
|
|
169
|
+
maxDelay: opt.maxDelay ?? 3e4,
|
|
170
|
+
backoff: opt.backoff ?? "exponential"
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/** Manages reconnection state and backoff calculation. */
|
|
174
|
+
var ReconnectController = class {
|
|
175
|
+
attempt = 0;
|
|
176
|
+
timer;
|
|
177
|
+
stopped = false;
|
|
178
|
+
constructor(config) {
|
|
179
|
+
this.config = config;
|
|
180
|
+
}
|
|
181
|
+
/** Whether reconnect is enabled and not manually stopped. */
|
|
182
|
+
get enabled() {
|
|
183
|
+
return this.config.enabled && !this.stopped;
|
|
184
|
+
}
|
|
185
|
+
/** Current attempt number. */
|
|
186
|
+
get currentAttempt() {
|
|
187
|
+
return this.attempt;
|
|
188
|
+
}
|
|
189
|
+
/** Schedule a reconnection attempt. Returns false if max retries exceeded or stopped. */
|
|
190
|
+
schedule(onReconnect) {
|
|
191
|
+
if (this.stopped) return false;
|
|
192
|
+
if (this.attempt >= this.config.maxRetries) return false;
|
|
193
|
+
const delay = this.getDelay();
|
|
194
|
+
this.attempt++;
|
|
195
|
+
this.timer = setTimeout(onReconnect, delay);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
/** Reset attempt counter (called on successful connection). */
|
|
199
|
+
reset() {
|
|
200
|
+
this.attempt = 0;
|
|
201
|
+
this.cancelPending();
|
|
202
|
+
}
|
|
203
|
+
/** Permanently stop reconnection (called on explicit close()). */
|
|
204
|
+
stop() {
|
|
205
|
+
this.stopped = true;
|
|
206
|
+
this.cancelPending();
|
|
207
|
+
}
|
|
208
|
+
getDelay() {
|
|
209
|
+
const { baseDelay, maxDelay, backoff } = this.config;
|
|
210
|
+
const delay = backoff === "exponential" ? baseDelay * 2 ** this.attempt : baseDelay * (this.attempt + 1);
|
|
211
|
+
return Math.min(delay, maxDelay);
|
|
212
|
+
}
|
|
213
|
+
cancelPending() {
|
|
214
|
+
if (this.timer !== void 0) {
|
|
215
|
+
clearTimeout(this.timer);
|
|
216
|
+
this.timer = void 0;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region packages/ws-client/src/ws-client.ts
|
|
223
|
+
const DEFAULT_RPC_TIMEOUT = 1e4;
|
|
224
|
+
function getWebSocketImpl(override) {
|
|
225
|
+
if (override) return override;
|
|
226
|
+
if (typeof WebSocket !== "undefined") return WebSocket;
|
|
227
|
+
throw new TypeError("@wooksjs/ws-client: No WebSocket implementation found. Install the \"ws\" package for Node.js < 22 or use a polyfill.");
|
|
228
|
+
}
|
|
229
|
+
/** WebSocket client with RPC, subscriptions, reconnection, and push listeners. */
|
|
230
|
+
var WsClient = class {
|
|
231
|
+
ws = null;
|
|
232
|
+
url;
|
|
233
|
+
protocols;
|
|
234
|
+
rpcTimeout;
|
|
235
|
+
serializer;
|
|
236
|
+
parser;
|
|
237
|
+
WsImpl;
|
|
238
|
+
rpc;
|
|
239
|
+
dispatcher;
|
|
240
|
+
queue;
|
|
241
|
+
reconnector;
|
|
242
|
+
/** Active subscriptions for auto-resubscribe: path → data. */
|
|
243
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
244
|
+
openHandlers = [];
|
|
245
|
+
closeHandlers = [];
|
|
246
|
+
errorHandlers = [];
|
|
247
|
+
reconnectHandlers = [];
|
|
248
|
+
closed = false;
|
|
249
|
+
constructor(url, options) {
|
|
250
|
+
this.url = url;
|
|
251
|
+
this.protocols = options?.protocols;
|
|
252
|
+
this.rpcTimeout = options?.rpcTimeout ?? DEFAULT_RPC_TIMEOUT;
|
|
253
|
+
this.serializer = options?.messageSerializer ?? JSON.stringify;
|
|
254
|
+
this.parser = options?.messageParser ?? JSON.parse;
|
|
255
|
+
this.WsImpl = getWebSocketImpl(options?._WebSocket);
|
|
256
|
+
this.rpc = new RpcTracker();
|
|
257
|
+
this.dispatcher = new PushDispatcher();
|
|
258
|
+
this.queue = new MessageQueue();
|
|
259
|
+
this.reconnector = new ReconnectController(normalizeReconnectConfig(options?.reconnect));
|
|
260
|
+
this.connect();
|
|
261
|
+
}
|
|
262
|
+
/** Fire-and-forget. Queued when disconnected with reconnect enabled. */
|
|
263
|
+
send(event, path, data) {
|
|
264
|
+
const msg = {
|
|
265
|
+
event,
|
|
266
|
+
path
|
|
267
|
+
};
|
|
268
|
+
if (data !== void 0) msg.data = data;
|
|
269
|
+
const serialized = this.serializer(msg);
|
|
270
|
+
if (this.isOpen()) this.ws.send(serialized);
|
|
271
|
+
else if (this.reconnector.enabled) this.queue.enqueue(serialized);
|
|
272
|
+
}
|
|
273
|
+
/** RPC with auto-generated correlation ID. Rejects when not connected. */
|
|
274
|
+
call(event, path, data) {
|
|
275
|
+
if (!this.isOpen()) return Promise.reject(new WsClientError(503, "Not connected"));
|
|
276
|
+
const id = this.rpc.generateId();
|
|
277
|
+
const msg = {
|
|
278
|
+
event,
|
|
279
|
+
path,
|
|
280
|
+
id
|
|
281
|
+
};
|
|
282
|
+
if (data !== void 0) msg.data = data;
|
|
283
|
+
this.ws.send(this.serializer(msg));
|
|
284
|
+
return this.rpc.track(id, this.rpcTimeout);
|
|
285
|
+
}
|
|
286
|
+
/** Subscribe to a path. Returns an unsubscribe function. Auto-resubscribes on reconnect. */
|
|
287
|
+
async subscribe(path, data) {
|
|
288
|
+
await this.call("subscribe", path, data);
|
|
289
|
+
this.subscriptions.set(path, data);
|
|
290
|
+
return () => {
|
|
291
|
+
this.subscriptions.delete(path);
|
|
292
|
+
if (this.isOpen()) this.send("unsubscribe", path);
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/** Register a client-side push listener. Returns an unregister function. */
|
|
296
|
+
on(event, pathPattern, handler) {
|
|
297
|
+
return this.dispatcher.on(event, pathPattern, handler);
|
|
298
|
+
}
|
|
299
|
+
/** Close the connection. Disables reconnect. Rejects pending RPCs. */
|
|
300
|
+
close() {
|
|
301
|
+
this.closed = true;
|
|
302
|
+
this.reconnector.stop();
|
|
303
|
+
this.rpc.rejectAll(503, "Connection closed");
|
|
304
|
+
this.queue.clear();
|
|
305
|
+
if (this.ws) {
|
|
306
|
+
this.ws.close(1e3, "Client closed");
|
|
307
|
+
this.ws = null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/** Register a handler called when the WebSocket connection opens. Returns an unregister function. */
|
|
311
|
+
onOpen(handler) {
|
|
312
|
+
this.openHandlers.push(handler);
|
|
313
|
+
return () => {
|
|
314
|
+
const idx = this.openHandlers.indexOf(handler);
|
|
315
|
+
if (idx !== -1) this.openHandlers.splice(idx, 1);
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/** Register a handler called when the WebSocket connection closes. Returns an unregister function. */
|
|
319
|
+
onClose(handler) {
|
|
320
|
+
this.closeHandlers.push(handler);
|
|
321
|
+
return () => {
|
|
322
|
+
const idx = this.closeHandlers.indexOf(handler);
|
|
323
|
+
if (idx !== -1) this.closeHandlers.splice(idx, 1);
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
/** Register a handler called on WebSocket errors. Returns an unregister function. */
|
|
327
|
+
onError(handler) {
|
|
328
|
+
this.errorHandlers.push(handler);
|
|
329
|
+
return () => {
|
|
330
|
+
const idx = this.errorHandlers.indexOf(handler);
|
|
331
|
+
if (idx !== -1) this.errorHandlers.splice(idx, 1);
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
/** Register a handler called before each reconnection attempt. Returns an unregister function. */
|
|
335
|
+
onReconnect(handler) {
|
|
336
|
+
this.reconnectHandlers.push(handler);
|
|
337
|
+
return () => {
|
|
338
|
+
const idx = this.reconnectHandlers.indexOf(handler);
|
|
339
|
+
if (idx !== -1) this.reconnectHandlers.splice(idx, 1);
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
isOpen() {
|
|
343
|
+
return this.ws !== null && this.ws.readyState === 1;
|
|
344
|
+
}
|
|
345
|
+
connect() {
|
|
346
|
+
if (this.closed) return;
|
|
347
|
+
const ws = new this.WsImpl(this.url, this.protocols);
|
|
348
|
+
this.ws = ws;
|
|
349
|
+
ws.addEventListener("open", () => {
|
|
350
|
+
this.reconnector.reset();
|
|
351
|
+
this.queue.flush((data) => ws.send(data));
|
|
352
|
+
this.resubscribe();
|
|
353
|
+
for (const h of this.openHandlers) h();
|
|
354
|
+
});
|
|
355
|
+
ws.addEventListener("close", (ev) => {
|
|
356
|
+
this.rpc.rejectAll(503, "Connection lost");
|
|
357
|
+
for (const h of this.closeHandlers) h(ev.code, ev.reason ?? "");
|
|
358
|
+
if (!this.closed && this.reconnector.enabled) this.reconnector.schedule(() => {
|
|
359
|
+
for (const h of this.reconnectHandlers) h(this.reconnector.currentAttempt);
|
|
360
|
+
this.connect();
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
ws.addEventListener("error", (ev) => {
|
|
364
|
+
for (const h of this.errorHandlers) h(ev);
|
|
365
|
+
});
|
|
366
|
+
ws.addEventListener("message", (ev) => {
|
|
367
|
+
this.handleMessage(ev.data);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
handleMessage(raw) {
|
|
371
|
+
let msg;
|
|
372
|
+
try {
|
|
373
|
+
msg = this.parser(raw);
|
|
374
|
+
} catch {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if ("id" in msg && msg.id !== void 0) {
|
|
378
|
+
this.rpc.resolve(msg);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if ("event" in msg && "path" in msg) this.dispatcher.dispatch(msg);
|
|
382
|
+
}
|
|
383
|
+
resubscribe() {
|
|
384
|
+
for (const [path, data] of this.subscriptions) this.call("subscribe", path, data).catch(() => {});
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
/**
|
|
388
|
+
* Creates a new WebSocket client.
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* ```ts
|
|
392
|
+
* const client = createWsClient('wss://api.example.com', { reconnect: true })
|
|
393
|
+
*
|
|
394
|
+
* client.on('message', '/chat/*', ({ data }) => console.log(data))
|
|
395
|
+
* await client.subscribe('/chat/rooms/lobby')
|
|
396
|
+
* client.send('message', '/chat/rooms/lobby', { text: 'hello' })
|
|
397
|
+
*
|
|
398
|
+
* const me = await client.call('rpc', '/users/me')
|
|
399
|
+
* ```
|
|
400
|
+
*/
|
|
401
|
+
function createWsClient(url, options) {
|
|
402
|
+
return new WsClient(url, options);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
//#endregion
|
|
406
|
+
export { WsClient, WsClientError, createWsClient };
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wooksjs/ws-client",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "WebSocket client for Wooks with RPC, subscriptions, reconnection, and push listeners",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"client",
|
|
7
|
+
"realtime",
|
|
8
|
+
"rpc",
|
|
9
|
+
"websocket",
|
|
10
|
+
"wooks",
|
|
11
|
+
"ws"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/wooksjs/wooksjs/tree/main/packages/ws-client#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/wooksjs/wooksjs/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Artem Maltsev",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/wooksjs/wooksjs.git",
|
|
22
|
+
"directory": "packages/ws-client"
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"wooksjs-ws-client-skill": "./scripts/setup-skills.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"skills",
|
|
30
|
+
"scripts/setup-skills.js"
|
|
31
|
+
],
|
|
32
|
+
"main": "dist/index.cjs",
|
|
33
|
+
"module": "dist/index.mjs",
|
|
34
|
+
"types": "dist/index.d.ts",
|
|
35
|
+
"exports": {
|
|
36
|
+
"./package.json": "./package.json",
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"require": "./dist/index.cjs",
|
|
40
|
+
"import": "./dist/index.mjs"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"typescript": "^5.9.3",
|
|
45
|
+
"vitest": "^3.2.4"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"ws": "^8.0.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"ws": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "rolldown -c ../../rolldown.config.mjs",
|
|
57
|
+
"setup-skills": "node ./scripts/setup-skills.js"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* prettier-ignore */
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import os from 'os'
|
|
6
|
+
import { fileURLToPath } from 'url'
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
|
|
10
|
+
const SKILL_NAME = 'wooksjs-ws-client'
|
|
11
|
+
const SKILL_SRC = path.join(__dirname, '..', 'skills', SKILL_NAME)
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(SKILL_SRC)) {
|
|
14
|
+
console.error(`No skills found at ${SKILL_SRC}`)
|
|
15
|
+
console.error('Add your SKILL.md files to the skills/' + SKILL_NAME + '/ directory first.')
|
|
16
|
+
process.exit(1)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const AGENTS = {
|
|
20
|
+
'Claude Code': { dir: '.claude/skills', global: path.join(os.homedir(), '.claude', 'skills') },
|
|
21
|
+
'Cursor': { dir: '.cursor/skills', global: path.join(os.homedir(), '.cursor', 'skills') },
|
|
22
|
+
'Windsurf': { dir: '.windsurf/skills', global: path.join(os.homedir(), '.windsurf', 'skills') },
|
|
23
|
+
'Codex': { dir: '.codex/skills', global: path.join(os.homedir(), '.codex', 'skills') },
|
|
24
|
+
'OpenCode': { dir: '.opencode/skills', global: path.join(os.homedir(), '.opencode', 'skills') },
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const args = process.argv.slice(2)
|
|
28
|
+
const isGlobal = args.includes('--global') || args.includes('-g')
|
|
29
|
+
const isPostinstall = args.includes('--postinstall')
|
|
30
|
+
let installed = 0, skipped = 0
|
|
31
|
+
const installedDirs = []
|
|
32
|
+
|
|
33
|
+
for (const [agentName, cfg] of Object.entries(AGENTS)) {
|
|
34
|
+
const targetBase = isGlobal ? cfg.global : path.join(process.cwd(), cfg.dir)
|
|
35
|
+
const agentRootDir = path.dirname(cfg.global) // Check if the agent has ever been installed globally
|
|
36
|
+
|
|
37
|
+
// In postinstall mode: silently skip agents that aren't set up globally
|
|
38
|
+
if (isPostinstall || isGlobal) {
|
|
39
|
+
if (!fs.existsSync(agentRootDir)) { skipped++; continue }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const dest = path.join(targetBase, SKILL_NAME)
|
|
43
|
+
try {
|
|
44
|
+
fs.mkdirSync(dest, { recursive: true })
|
|
45
|
+
fs.cpSync(SKILL_SRC, dest, { recursive: true })
|
|
46
|
+
console.log(`✅ ${agentName}: installed to ${dest}`)
|
|
47
|
+
installed++
|
|
48
|
+
if (!isGlobal) installedDirs.push(cfg.dir + '/' + SKILL_NAME)
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.warn(`⚠️ ${agentName}: failed — ${err.message}`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Add locally-installed skill dirs to .gitignore
|
|
55
|
+
if (!isGlobal && installedDirs.length > 0) {
|
|
56
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore')
|
|
57
|
+
let gitignoreContent = ''
|
|
58
|
+
try { gitignoreContent = fs.readFileSync(gitignorePath, 'utf8') } catch {}
|
|
59
|
+
const linesToAdd = installedDirs.filter(d => !gitignoreContent.includes(d))
|
|
60
|
+
if (linesToAdd.length > 0) {
|
|
61
|
+
const hasHeader = gitignoreContent.includes('# AI agent skills')
|
|
62
|
+
const block = (gitignoreContent && !gitignoreContent.endsWith('\n') ? '\n' : '')
|
|
63
|
+
+ (hasHeader ? '' : '\n# AI agent skills (auto-generated by setup-skills)\n')
|
|
64
|
+
+ linesToAdd.join('\n') + '\n'
|
|
65
|
+
fs.appendFileSync(gitignorePath, block)
|
|
66
|
+
console.log(`📝 Added ${linesToAdd.length} entries to .gitignore`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (installed === 0 && isPostinstall) {
|
|
71
|
+
// Silence is fine — no agents present, nothing to do
|
|
72
|
+
} else if (installed === 0 && skipped === Object.keys(AGENTS).length) {
|
|
73
|
+
console.log('No agent directories detected. Try --global or run without it for project-local install.')
|
|
74
|
+
} else if (installed === 0) {
|
|
75
|
+
console.log('Nothing installed. Run without --global to install project-locally.')
|
|
76
|
+
} else {
|
|
77
|
+
console.log(`\n✨ Done! Restart your AI agent to pick up the "${SKILL_NAME}" skill.`)
|
|
78
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wooksjs-ws-client
|
|
3
|
+
description: Use this skill when working with @wooksjs/ws-client — to create a WebSocket client with createWsClient() or WsClient, send fire-and-forget messages with send(), make RPC calls with call(), subscribe to server paths with subscribe(), listen for push messages with on(), handle reconnection with WsClientReconnectOptions, manage lifecycle events with onOpen()/onClose()/onError()/onReconnect(), use WsClientError for error handling, or configure custom serializers and protocols. Covers the wire protocol (WsClientMessage, WsReplyMessage, WsPushMessage), message queuing during disconnect, and auto-resubscription.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @wooksjs/ws-client
|
|
7
|
+
|
|
8
|
+
WebSocket client for Wooks with RPC, subscriptions, reconnection, push listeners, and message queuing — works in browsers and Node.js.
|
|
9
|
+
|
|
10
|
+
## How to use this skill
|
|
11
|
+
|
|
12
|
+
Read the domain file that matches the task. Do not load all files — only what you need.
|
|
13
|
+
|
|
14
|
+
| Domain | File | Load when... |
|
|
15
|
+
| --------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
16
|
+
| Core concepts & setup | [core.md](core.md) | Creating a client, understanding the wire protocol, configuring options, sending messages |
|
|
17
|
+
| RPC & subscriptions | [rpc.md](rpc.md) | Making RPC calls with `call()`, subscribing to paths with `subscribe()`, handling timeouts and errors |
|
|
18
|
+
| Push listeners | [push.md](push.md) | Listening for server push messages with `on()`, exact and wildcard matching, dispatching |
|
|
19
|
+
| Reconnection | [reconnect.md](reconnect.md) | Configuring auto-reconnect, backoff strategies, message queuing, auto-resubscription |
|
|
20
|
+
|
|
21
|
+
## Quick reference
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createWsClient, WsClient, WsClientError } from '@wooksjs/ws-client'
|
|
25
|
+
|
|
26
|
+
import type {
|
|
27
|
+
WsClientOptions,
|
|
28
|
+
WsClientReconnectOptions,
|
|
29
|
+
WsClientMessage,
|
|
30
|
+
WsReplyMessage,
|
|
31
|
+
WsPushMessage,
|
|
32
|
+
WsClientPushEvent,
|
|
33
|
+
WsPushHandler,
|
|
34
|
+
} from '@wooksjs/ws-client'
|
|
35
|
+
```
|