@fishenv/http 0.0.0-stage → 0.0.3

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.
Files changed (106) hide show
  1. package/dist/contract/builder.d.ts +87 -0
  2. package/dist/contract/builder.js +89 -0
  3. package/dist/contract/index.d.ts +4 -0
  4. package/dist/contract/index.js +2 -0
  5. package/dist/contract/types.d.ts +47 -0
  6. package/dist/contract/types.js +0 -0
  7. package/dist/contract/ws.d.ts +178 -0
  8. package/dist/contract/ws.js +164 -0
  9. package/dist/core/compose.d.ts +22 -0
  10. package/dist/core/compose.js +87 -0
  11. package/dist/core/cors.d.ts +38 -0
  12. package/dist/core/cors.js +96 -0
  13. package/dist/core/error.d.ts +73 -0
  14. package/dist/core/error.js +142 -0
  15. package/dist/core/index.d.ts +20 -0
  16. package/dist/core/index.js +23 -0
  17. package/dist/core/matcher.d.ts +17 -0
  18. package/dist/core/matcher.js +110 -0
  19. package/dist/core/request.d.ts +28 -0
  20. package/dist/core/request.js +106 -0
  21. package/dist/core/route-builder.d.ts +58 -0
  22. package/dist/core/route-builder.js +122 -0
  23. package/dist/core/router.d.ts +162 -0
  24. package/dist/core/router.js +327 -0
  25. package/dist/core/transport.d.ts +39 -0
  26. package/dist/core/transport.js +68 -0
  27. package/dist/core/types.d.ts +86 -0
  28. package/dist/core/types.js +0 -0
  29. package/dist/core/upgrade.d.ts +85 -0
  30. package/dist/core/upgrade.js +110 -0
  31. package/dist/core/utils/headers.d.ts +1 -0
  32. package/dist/core/utils/headers.js +2 -0
  33. package/dist/core/utils/url.d.ts +27 -0
  34. package/dist/core/utils/url.js +130 -0
  35. package/dist/core/validation.d.ts +13 -0
  36. package/dist/core/validation.js +24 -0
  37. package/dist/sse/connection.d.ts +30 -0
  38. package/dist/sse/connection.js +104 -0
  39. package/dist/sse/encode.d.ts +10 -0
  40. package/dist/sse/encode.js +27 -0
  41. package/dist/sse/hub.d.ts +33 -0
  42. package/dist/sse/hub.js +115 -0
  43. package/dist/sse/index.d.ts +38 -0
  44. package/dist/sse/index.js +40 -0
  45. package/dist/sse/response.d.ts +10 -0
  46. package/dist/sse/response.js +50 -0
  47. package/dist/sse/route-builder.d.ts +44 -0
  48. package/dist/sse/route-builder.js +65 -0
  49. package/dist/sse/router.d.ts +11 -0
  50. package/dist/sse/router.js +16 -0
  51. package/dist/sse/types.d.ts +43 -0
  52. package/dist/sse/types.js +4 -0
  53. package/dist/static/index.d.ts +27 -0
  54. package/dist/static/index.js +25 -0
  55. package/dist/static/negotiate.d.ts +23 -0
  56. package/dist/static/negotiate.js +85 -0
  57. package/dist/static/options.d.ts +44 -0
  58. package/dist/static/options.js +190 -0
  59. package/dist/static/resolve.d.ts +56 -0
  60. package/dist/static/resolve.js +113 -0
  61. package/dist/static/response.d.ts +53 -0
  62. package/dist/static/response.js +146 -0
  63. package/dist/static/router.d.ts +59 -0
  64. package/dist/static/router.js +217 -0
  65. package/dist/static/rules.d.ts +21 -0
  66. package/dist/static/rules.js +49 -0
  67. package/dist/static/types.d.ts +76 -0
  68. package/dist/static/types.js +0 -0
  69. package/dist/ws/channels/channel.d.ts +175 -0
  70. package/dist/ws/channels/channel.js +125 -0
  71. package/dist/ws/channels/handler.d.ts +33 -0
  72. package/dist/ws/channels/handler.js +486 -0
  73. package/dist/ws/channels/protocol.d.ts +96 -0
  74. package/dist/ws/channels/protocol.js +94 -0
  75. package/dist/ws/channels/target.d.ts +36 -0
  76. package/dist/ws/channels/target.js +56 -0
  77. package/dist/ws/connection.d.ts +56 -0
  78. package/dist/ws/connection.js +172 -0
  79. package/dist/ws/helpers.d.ts +17 -0
  80. package/dist/ws/helpers.js +48 -0
  81. package/dist/ws/hub.d.ts +172 -0
  82. package/dist/ws/hub.js +489 -0
  83. package/dist/ws/index.d.ts +65 -0
  84. package/dist/ws/index.js +66 -0
  85. package/dist/ws/lifecycle.d.ts +21 -0
  86. package/dist/ws/lifecycle.js +97 -0
  87. package/dist/ws/limits.d.ts +32 -0
  88. package/dist/ws/limits.js +67 -0
  89. package/dist/ws/message.d.ts +19 -0
  90. package/dist/ws/message.js +58 -0
  91. package/dist/ws/origin.d.ts +40 -0
  92. package/dist/ws/origin.js +152 -0
  93. package/dist/ws/presence.d.ts +96 -0
  94. package/dist/ws/presence.js +420 -0
  95. package/dist/ws/route-builder.d.ts +136 -0
  96. package/dist/ws/route-builder.js +265 -0
  97. package/dist/ws/router.d.ts +21 -0
  98. package/dist/ws/router.js +36 -0
  99. package/dist/ws/tickets.d.ts +82 -0
  100. package/dist/ws/tickets.js +161 -0
  101. package/dist/ws/typed.d.ts +54 -0
  102. package/dist/ws/typed.js +104 -0
  103. package/dist/ws/types.d.ts +69 -0
  104. package/dist/ws/types.js +70 -0
  105. package/package.json +54 -4
  106. package/README.md +0 -3
package/dist/ws/hub.js ADDED
@@ -0,0 +1,489 @@
1
+ /**
2
+ * Keyed registry of live sockets plus server-side rooms — the seam for
3
+ * targeted (non-broadcast) sends from anywhere in the application.
4
+ */
5
+ import { createTransportBridge, } from "../core/index.js";
6
+ import { WsChannelTarget } from "./channels/target.js";
7
+ import { PresenceRegistry, PresenceTopic, } from "./presence.js";
8
+ import { WsCloseCode } from "./types.js";
9
+ const NO_FILTER = {
10
+ exceptIds: new Set(),
11
+ exceptKeys: new Set(),
12
+ onlyKeys: null,
13
+ };
14
+ function isUnfiltered(filter) {
15
+ return (filter.exceptIds.size === 0 &&
16
+ filter.exceptKeys.size === 0 &&
17
+ filter.onlyKeys == null);
18
+ }
19
+ function toWireFilter(filter) {
20
+ if (isUnfiltered(filter))
21
+ return undefined;
22
+ return {
23
+ exceptIds: filter.exceptIds.size ? [...filter.exceptIds] : undefined,
24
+ exceptKeys: filter.exceptKeys.size ? [...filter.exceptKeys] : undefined,
25
+ onlyKeys: filter.onlyKeys ? [...filter.onlyKeys] : undefined,
26
+ };
27
+ }
28
+ function fromWireFilter(wire) {
29
+ if (wire == null)
30
+ return NO_FILTER;
31
+ return {
32
+ exceptIds: new Set(wire.exceptIds ?? []),
33
+ exceptKeys: new Set(wire.exceptKeys ?? []),
34
+ onlyKeys: wire.onlyKeys ? new Set(wire.onlyKeys) : null,
35
+ };
36
+ }
37
+ /** A fan-out target: a room, optionally narrowed to/away from sockets or keys. */
38
+ export class WsRoomTarget {
39
+ #hub;
40
+ #room;
41
+ #filter;
42
+ /** @internal Use `hub.to(room)`. */
43
+ constructor(hub, room, filter = NO_FILTER) {
44
+ this.#hub = hub;
45
+ this.#room = room;
46
+ this.#filter = filter;
47
+ }
48
+ /** Exclude a socket (e.g. the sender) or every socket of a key. */
49
+ except(...targets) {
50
+ const exceptIds = new Set(this.#filter.exceptIds);
51
+ const exceptKeys = new Set(this.#filter.exceptKeys);
52
+ for (const t of targets) {
53
+ if (typeof t === "string")
54
+ exceptKeys.add(t);
55
+ else
56
+ exceptIds.add(t.id);
57
+ }
58
+ return new WsRoomTarget(this.#hub, this.#room, {
59
+ ...this.#filter,
60
+ exceptIds,
61
+ exceptKeys,
62
+ });
63
+ }
64
+ /** Only members registered under one of `keys`. */
65
+ only(...keys) {
66
+ const onlyKeys = new Set(keys);
67
+ if (this.#filter.onlyKeys != null) {
68
+ for (const k of [...onlyKeys]) {
69
+ if (!this.#filter.onlyKeys.has(k))
70
+ onlyKeys.delete(k);
71
+ }
72
+ }
73
+ return new WsRoomTarget(this.#hub, this.#room, {
74
+ ...this.#filter,
75
+ onlyKeys,
76
+ });
77
+ }
78
+ /** Deliver to matching members on every node. Returns the local delivery count. */
79
+ send(data) {
80
+ return this.#hub._sendRoom(this.#room, encodeFrame(data), this.#filter, true);
81
+ }
82
+ /**
83
+ * Remove matching members from the room on every node, running any leave
84
+ * hook registered for them (channels use this to notify the client).
85
+ */
86
+ kick(reason = "") {
87
+ return this.#hub._kick(this.#room, this.#filter, reason, true);
88
+ }
89
+ /** Local matching member count. */
90
+ count() {
91
+ return this.sockets().length;
92
+ }
93
+ /** Local matching members. */
94
+ sockets() {
95
+ return this.#hub._members(this.#room, this.#filter);
96
+ }
97
+ }
98
+ /**
99
+ * Sockets keyed by an application identity (user id, tenant id, device…),
100
+ * plus server-side **rooms** — groupings the server assigns; clients can't
101
+ * ask to join one. One key maps to many sockets (tabs); one socket has at
102
+ * most one key and any number of rooms.
103
+ *
104
+ * Subclass it to get a DI token:
105
+ * ```ts
106
+ * class ChatSockets extends WsHub {}
107
+ * ```
108
+ */
109
+ export class WsHub {
110
+ name;
111
+ #byKey = new Map();
112
+ #keyOf = new Map();
113
+ #rooms = new Map();
114
+ #roomsOf = new Map();
115
+ #tracked = new Set();
116
+ #leaveHooks = new Map();
117
+ #extensions = new Map();
118
+ #bridge;
119
+ #replicated;
120
+ #presenceOptions;
121
+ #presence;
122
+ constructor(options = {}) {
123
+ this.name = options.name ?? this.constructor.name;
124
+ this.#replicated = options.transport != null;
125
+ this.#presenceOptions = options.presence;
126
+ this.#bridge = createTransportBridge(options.transport, (message) => {
127
+ if (message.hub === this.name)
128
+ this.#onRemote(message);
129
+ }, { label: "[fishenv.ws]" });
130
+ }
131
+ /** Random id of this hub instance on the transport. */
132
+ get nodeId() {
133
+ return this.#bridge.nodeId;
134
+ }
135
+ // ── Registration ────────────────────────────────────────────────────
136
+ /**
137
+ * Register a socket under `key`. Returns an unregister fn; also
138
+ * auto-removes (keys and rooms) on close. Re-adding moves the socket.
139
+ */
140
+ add(key, socket) {
141
+ if (socket.closed)
142
+ return () => { };
143
+ this.#removeKey(socket);
144
+ let set = this.#byKey.get(key);
145
+ if (set == null) {
146
+ set = new Set();
147
+ this.#byKey.set(key, set);
148
+ }
149
+ set.add(socket);
150
+ this.#keyOf.set(socket, key);
151
+ this.#track(socket);
152
+ return () => {
153
+ if (this.#keyOf.get(socket) === key)
154
+ this.#removeKey(socket);
155
+ };
156
+ }
157
+ /** The key a socket was registered under, if any. */
158
+ keyOf(socket) {
159
+ return this.#keyOf.get(socket);
160
+ }
161
+ // ── Rooms ───────────────────────────────────────────────────────────
162
+ /** Put a socket in a room (idempotent). Server-side only. */
163
+ join(socket, room) {
164
+ if (socket.closed)
165
+ return;
166
+ let members = this.#rooms.get(room);
167
+ if (members == null) {
168
+ members = new Set();
169
+ this.#rooms.set(room, members);
170
+ }
171
+ if (members.has(socket))
172
+ return;
173
+ members.add(socket);
174
+ let rooms = this.#roomsOf.get(socket);
175
+ if (rooms == null) {
176
+ rooms = new Set();
177
+ this.#roomsOf.set(socket, rooms);
178
+ }
179
+ rooms.add(room);
180
+ if (serverOf(socket) != null)
181
+ socket.raw.subscribe(this.#topic(room));
182
+ this.#track(socket);
183
+ }
184
+ /** Remove a socket from a room. Does *not* run leave hooks. */
185
+ leave(socket, room) {
186
+ this.#leaveHooks.get(socket)?.delete(room);
187
+ const members = this.#rooms.get(room);
188
+ if (members == null || !members.delete(socket))
189
+ return;
190
+ if (members.size === 0)
191
+ this.#rooms.delete(room);
192
+ const rooms = this.#roomsOf.get(socket);
193
+ rooms?.delete(room);
194
+ if (rooms?.size === 0)
195
+ this.#roomsOf.delete(socket);
196
+ if (!socket.closed && serverOf(socket) != null) {
197
+ socket.raw.unsubscribe(this.#topic(room));
198
+ }
199
+ }
200
+ /** Rooms a socket is in. */
201
+ rooms(socket) {
202
+ return [...(this.#roomsOf.get(socket) ?? [])];
203
+ }
204
+ /** Every room with at least one local member. */
205
+ roomNames() {
206
+ return [...this.#rooms.keys()];
207
+ }
208
+ to(room) {
209
+ return new WsRoomTarget(this, room);
210
+ }
211
+ channel(a, b) {
212
+ return new WsChannelTarget(this, typeof a === "string" ? a : (b ?? ""));
213
+ }
214
+ // ── Sending ─────────────────────────────────────────────────────────
215
+ /** Send to every socket of `key` on every node. Returns local delivery count. */
216
+ send(key, data) {
217
+ const frame = encodeFrame(data);
218
+ this.#bridge.publish({
219
+ hub: this.name,
220
+ kind: "send",
221
+ key,
222
+ payload: toWire(frame),
223
+ });
224
+ return this.#deliverKey(key, frame);
225
+ }
226
+ sendMany(keys, data) {
227
+ let delivered = 0;
228
+ for (const key of keys)
229
+ delivered += this.send(key, data);
230
+ return delivered;
231
+ }
232
+ /** Send to every keyed socket in this hub, on every node. */
233
+ broadcast(data) {
234
+ const frame = encodeFrame(data);
235
+ this.#bridge.publish({
236
+ hub: this.name,
237
+ kind: "send",
238
+ key: null,
239
+ payload: toWire(frame),
240
+ });
241
+ return this.#deliverAll(frame);
242
+ }
243
+ /**
244
+ * Close every socket of `key` on every node — e.g. on logout or when
245
+ * permissions are revoked. Returns the local count.
246
+ */
247
+ close(key, code = WsCloseCode.Normal, reason = "") {
248
+ this.#bridge.publish({ hub: this.name, kind: "close", key, code, reason });
249
+ return this.#closeKey(key, code, reason);
250
+ }
251
+ /** Close every local socket and detach from the transport. */
252
+ closeAll(code = WsCloseCode.GoingAway, reason = "") {
253
+ for (const socket of [...this.#tracked])
254
+ socket.close(code, reason);
255
+ this.#presence?.close();
256
+ this.#bridge.close();
257
+ }
258
+ // ── Presence ────────────────────────────────────────────────────────
259
+ /**
260
+ * Presence for a topic — who is "in" it, by key, across every node.
261
+ * Channels with contract presence use `hub.channel(name).presence`.
262
+ */
263
+ presence(topic) {
264
+ return new PresenceTopic(this._presenceRegistry(), topic);
265
+ }
266
+ /** @internal The hub's presence registry (created on first use). */
267
+ _presenceRegistry() {
268
+ this.#presence ??= new PresenceRegistry(this, this.#replicated, this.#presenceOptions);
269
+ return this.#presence;
270
+ }
271
+ // ── Introspection ───────────────────────────────────────────────────
272
+ has(key) {
273
+ return (this.#byKey.get(key)?.size ?? 0) > 0;
274
+ }
275
+ /** Socket count for `key`, or every keyed socket when omitted. */
276
+ count(key) {
277
+ if (key != null)
278
+ return this.#byKey.get(key)?.size ?? 0;
279
+ return this.#keyOf.size;
280
+ }
281
+ keys() {
282
+ return [...this.#byKey.keys()];
283
+ }
284
+ connections(key) {
285
+ return [...(this.#byKey.get(key) ?? [])];
286
+ }
287
+ // ── Extension points (channels, presence) ───────────────────────────
288
+ /**
289
+ * @internal Run `hook` when `socket` is kicked from `room` (locally or by
290
+ * another node). The hook is responsible for calling `leave()`.
291
+ */
292
+ _onLeave(socket, room, hook) {
293
+ let hooks = this.#leaveHooks.get(socket);
294
+ if (hooks == null) {
295
+ hooks = new Map();
296
+ this.#leaveHooks.set(socket, hooks);
297
+ }
298
+ hooks.set(room, hook);
299
+ }
300
+ /** @internal Receive transport messages of an extension (e.g. presence). */
301
+ _registerExtension(kind, onRemote) {
302
+ this.#extensions.set(kind, onRemote);
303
+ }
304
+ /** @internal Publish an extension message to other nodes. */
305
+ _publishExtension(kind, body) {
306
+ this.#bridge.publish({ hub: this.name, kind, body });
307
+ }
308
+ /** @internal */
309
+ _members(room, filter = NO_FILTER) {
310
+ return [...(this.#rooms.get(room) ?? [])].filter((s) => this.#matches(s, filter));
311
+ }
312
+ /** @internal */
313
+ _sendRoom(room, frame, filter, publish) {
314
+ if (publish) {
315
+ this.#bridge.publish({
316
+ hub: this.name,
317
+ kind: "room",
318
+ room,
319
+ payload: toWire(frame),
320
+ filter: toWireFilter(filter),
321
+ });
322
+ }
323
+ const members = this.#rooms.get(room);
324
+ if (members == null)
325
+ return 0;
326
+ // Fast path: no filter → one native publish per server.
327
+ if (isUnfiltered(filter)) {
328
+ const servers = new Set();
329
+ let delivered = 0;
330
+ for (const socket of members) {
331
+ if (socket.closed)
332
+ continue;
333
+ const server = serverOf(socket);
334
+ if (server == null) {
335
+ if (isDelivered(socket.send(frame)))
336
+ delivered++;
337
+ }
338
+ else {
339
+ servers.add(server);
340
+ delivered++;
341
+ }
342
+ }
343
+ for (const server of servers)
344
+ server.publish(this.#topic(room), frame);
345
+ return delivered;
346
+ }
347
+ let delivered = 0;
348
+ for (const socket of [...members]) {
349
+ if (!this.#matches(socket, filter))
350
+ continue;
351
+ if (isDelivered(socket.send(frame)))
352
+ delivered++;
353
+ }
354
+ return delivered;
355
+ }
356
+ /** @internal */
357
+ _kick(room, filter, reason, publish) {
358
+ if (publish) {
359
+ this.#bridge.publish({
360
+ hub: this.name,
361
+ kind: "kick",
362
+ room,
363
+ filter: toWireFilter(filter),
364
+ reason,
365
+ });
366
+ }
367
+ const targets = this._members(room, filter);
368
+ for (const socket of targets) {
369
+ const hook = this.#leaveHooks.get(socket)?.get(room);
370
+ if (hook != null)
371
+ hook(reason);
372
+ else
373
+ this.leave(socket, room);
374
+ }
375
+ return targets.length;
376
+ }
377
+ // ── Internals ───────────────────────────────────────────────────────
378
+ #matches(socket, filter) {
379
+ if (filter.exceptIds.has(socket.id))
380
+ return false;
381
+ const key = this.#keyOf.get(socket);
382
+ if (key != null && filter.exceptKeys.has(key))
383
+ return false;
384
+ if (filter.onlyKeys != null && (key == null || !filter.onlyKeys.has(key))) {
385
+ return false;
386
+ }
387
+ return true;
388
+ }
389
+ #topic(room) {
390
+ return `fx:${this.name}:r:${room}`;
391
+ }
392
+ #deliverKey(key, frame) {
393
+ let delivered = 0;
394
+ for (const socket of [...(this.#byKey.get(key) ?? [])]) {
395
+ if (isDelivered(socket.send(frame)))
396
+ delivered++;
397
+ }
398
+ return delivered;
399
+ }
400
+ #deliverAll(frame) {
401
+ let delivered = 0;
402
+ for (const socket of [...this.#keyOf.keys()]) {
403
+ if (isDelivered(socket.send(frame)))
404
+ delivered++;
405
+ }
406
+ return delivered;
407
+ }
408
+ #closeKey(key, code, reason) {
409
+ const sockets = [...(this.#byKey.get(key) ?? [])];
410
+ for (const socket of sockets)
411
+ socket.close(code, reason);
412
+ return sockets.length;
413
+ }
414
+ #onRemote(message) {
415
+ switch (message.kind) {
416
+ case "send": {
417
+ const frame = fromWire(message.payload);
418
+ if (message.key == null)
419
+ this.#deliverAll(frame);
420
+ else
421
+ this.#deliverKey(message.key, frame);
422
+ return;
423
+ }
424
+ case "room":
425
+ this._sendRoom(message.room, fromWire(message.payload), fromWireFilter(message.filter), false);
426
+ return;
427
+ case "close":
428
+ this.#closeKey(message.key, message.code, message.reason);
429
+ return;
430
+ case "kick":
431
+ this._kick(message.room, fromWireFilter(message.filter), message.reason, false);
432
+ return;
433
+ case "presence":
434
+ this.#extensions.get("presence")?.(message.body);
435
+ return;
436
+ }
437
+ }
438
+ #track(socket) {
439
+ if (this.#tracked.has(socket))
440
+ return;
441
+ this.#tracked.add(socket);
442
+ socket.onClose(() => {
443
+ this.#tracked.delete(socket);
444
+ this.#removeKey(socket);
445
+ for (const room of this.rooms(socket))
446
+ this.leave(socket, room);
447
+ this.#leaveHooks.delete(socket);
448
+ });
449
+ }
450
+ #removeKey(socket) {
451
+ const key = this.#keyOf.get(socket);
452
+ if (key == null)
453
+ return;
454
+ this.#keyOf.delete(socket);
455
+ const set = this.#byKey.get(key);
456
+ set?.delete(socket);
457
+ if (set?.size === 0)
458
+ this.#byKey.delete(key);
459
+ }
460
+ }
461
+ function serverOf(socket) {
462
+ return socket.raw.data?.server;
463
+ }
464
+ function isDelivered(result) {
465
+ return result === "sent" || result === "backpressure";
466
+ }
467
+ /** Text and binary frames go out as-is; everything else as JSON. */
468
+ export function encodeFrame(data) {
469
+ if (typeof data === "string")
470
+ return data;
471
+ if (data instanceof Uint8Array)
472
+ return data;
473
+ if (data instanceof ArrayBuffer)
474
+ return new Uint8Array(data);
475
+ if (ArrayBuffer.isView(data)) {
476
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
477
+ }
478
+ return JSON.stringify(data);
479
+ }
480
+ function toWire(frame) {
481
+ return typeof frame === "string"
482
+ ? { text: frame }
483
+ : { base64: Buffer.from(frame).toString("base64") };
484
+ }
485
+ function fromWire(payload) {
486
+ if (payload.text != null)
487
+ return payload.text;
488
+ return new Uint8Array(Buffer.from(payload.base64 ?? "", "base64"));
489
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * WebSockets for `@fishenv/http`.
3
+ *
4
+ * A ws route is an ordinary `GET` route: origin checks, middleware (auth),
5
+ * path-param validation and connection limits all run *before* the upgrade,
6
+ * so a rejected handshake is a normal HTTP error and no socket is opened.
7
+ *
8
+ * ```ts
9
+ * const api = withWs(r({ prefix: "/api" }).use(auth));
10
+ *
11
+ * api.ws("/echo/:room")
12
+ * .maxMessageSize(16 * 1024)
13
+ * .rateLimit({ messages: 20, perMs: 1000 })
14
+ * .handle(({ socket, path }) => {
15
+ * socket.onMessage((msg) => socket.send(`[${path.room}] ${msg.text()}`));
16
+ * });
17
+ * ```
18
+ *
19
+ * Module layout:
20
+ * - `types.ts` — shared types, close codes, error → close mapping
21
+ * - `message.ts` — `WsMessage`, a lazily decoded inbound message
22
+ * - `connection.ts` — `WsConnection`, a single live socket
23
+ * - `lifecycle.ts` — per-socket buffering, limits and error handling
24
+ * - `origin.ts` — CSWSH origin policy
25
+ * - `limits.ts` — rate limiting and connection counting
26
+ * - `helpers.ts` — handshake helpers
27
+ * - `hub.ts` — `WsHub`, keyed registry + server-side rooms
28
+ * - `presence.ts` — replicated presence (`hub.presence(topic)`)
29
+ * - `tickets.ts` — `WsTickets`, single-use handshake tickets
30
+ * - `typed.ts` — `TypedWsConnection` for `c.ws().messages()` contracts
31
+ * - `route-builder.ts` — `WsRouteBuilder`
32
+ * - `channels/` — the opt-in channel protocol: `channel()`, `channels()`,
33
+ * wire format, `hub.channel(name)` targets
34
+ * - `router.ts` — `withWs()` mixin
35
+ */
36
+ export type { AnyChannelImpl, AnyEventFn, ChannelContext, ChannelFactory, ChannelHandlerArgs, ChannelImplDefinition, ChannelPresence, ClientEventName, EventFn, JoinFn, LeaveFn, LeaveReason, PresenceFn, ServerEventName, ServerEventPayload, } from "./channels/channel.ts";
37
+ export { ChannelImpl, channel } from "./channels/channel.ts";
38
+ export type { ChannelsOptions } from "./channels/handler.ts";
39
+ export { channels } from "./channels/handler.ts";
40
+ export type { ClientFrame, PresenceEntries, ServerFrame, WireError, } from "./channels/protocol.ts";
41
+ export { CHANNELS_PROTOCOL, FrameError, parseClientFrame, } from "./channels/protocol.ts";
42
+ export { channelRoom, WsChannelTarget } from "./channels/target.ts";
43
+ export type { WsCloseListener, WsMessageListener } from "./connection.ts";
44
+ export { WsConnection } from "./connection.ts";
45
+ export { isWebSocketUpgrade, negotiateProtocol, requestedProtocols, UpgradeRequiredError, } from "./helpers.ts";
46
+ export type { RoomFilter, WireFilter, WirePayload, WsHubOptions, WsHubPayload, WsTransportMessage, } from "./hub.ts";
47
+ export { encodeFrame, WsHub, WsRoomTarget } from "./hub.ts";
48
+ export type { LifecycleOptions, WsSetupFn } from "./lifecycle.ts";
49
+ export { createLifecycle } from "./lifecycle.ts";
50
+ export type { WsConnectionLimit } from "./limits.ts";
51
+ export { ConnectionCounter, TokenBucket } from "./limits.ts";
52
+ export { WsMessage } from "./message.ts";
53
+ export type { OriginPolicy, OriginPredicate, OriginSource } from "./origin.ts";
54
+ export { createOriginPolicy, originSourcesFromCors } from "./origin.ts";
55
+ export type { PresenceDiff, PresenceEntry, PresenceOptions, PresenceState, } from "./presence.ts";
56
+ export { PresenceLimitError, PresenceRegistry, PresenceTopic, } from "./presence.ts";
57
+ export type { ChannelCoverage, CompleteChannelImpl, WsHandlerArgs, WsHandlerFn, WsProtocolHandler, WsRouteInfo, WsRouteOptions, WsRouterOptions, } from "./route-builder.ts";
58
+ export { WsRouteBuilder } from "./route-builder.ts";
59
+ export type { SocketOf, WsRouter } from "./router.ts";
60
+ export { withWs } from "./router.ts";
61
+ export type { TicketStore, WsTicketsOptions } from "./tickets.ts";
62
+ export { bearerFromProtocol, MemoryTicketStore, WsTickets } from "./tickets.ts";
63
+ export { ContractViolationError, checkOutbound, parseInbound, TypedWsConnection, } from "./typed.ts";
64
+ export type { WsCleanupFn, WsConnectionOptions, WsRateLimit, WsSendable, WsSendResult, } from "./types.ts";
65
+ export { closeFrameFor, MAX_CLOSE_REASON_BYTES, truncateReason, WsCloseCode, WsCloseError, WsProtocolError, } from "./types.ts";
@@ -0,0 +1,66 @@
1
+ /**
2
+ * WebSockets for `@fishenv/http`.
3
+ *
4
+ * A ws route is an ordinary `GET` route: origin checks, middleware (auth),
5
+ * path-param validation and connection limits all run *before* the upgrade,
6
+ * so a rejected handshake is a normal HTTP error and no socket is opened.
7
+ *
8
+ * ```ts
9
+ * const api = withWs(r({ prefix: "/api" }).use(auth));
10
+ *
11
+ * api.ws("/echo/:room")
12
+ * .maxMessageSize(16 * 1024)
13
+ * .rateLimit({ messages: 20, perMs: 1000 })
14
+ * .handle(({ socket, path }) => {
15
+ * socket.onMessage((msg) => socket.send(`[${path.room}] ${msg.text()}`));
16
+ * });
17
+ * ```
18
+ *
19
+ * Module layout:
20
+ * - `types.ts` — shared types, close codes, error → close mapping
21
+ * - `message.ts` — `WsMessage`, a lazily decoded inbound message
22
+ * - `connection.ts` — `WsConnection`, a single live socket
23
+ * - `lifecycle.ts` — per-socket buffering, limits and error handling
24
+ * - `origin.ts` — CSWSH origin policy
25
+ * - `limits.ts` — rate limiting and connection counting
26
+ * - `helpers.ts` — handshake helpers
27
+ * - `hub.ts` — `WsHub`, keyed registry + server-side rooms
28
+ * - `presence.ts` — replicated presence (`hub.presence(topic)`)
29
+ * - `tickets.ts` — `WsTickets`, single-use handshake tickets
30
+ * - `typed.ts` — `TypedWsConnection` for `c.ws().messages()` contracts
31
+ * - `route-builder.ts` — `WsRouteBuilder`
32
+ * - `channels/` — the opt-in channel protocol: `channel()`, `channels()`,
33
+ * wire format, `hub.channel(name)` targets
34
+ * - `router.ts` — `withWs()` mixin
35
+ */
36
+ // Channels (Layer 3)
37
+ export { ChannelImpl, channel } from "./channels/channel.js";
38
+ export { channels } from "./channels/handler.js";
39
+ export { CHANNELS_PROTOCOL, FrameError, parseClientFrame, } from "./channels/protocol.js";
40
+ export { channelRoom, WsChannelTarget } from "./channels/target.js";
41
+ // Connection
42
+ export { WsConnection } from "./connection.js";
43
+ // Handshake helpers
44
+ export { isWebSocketUpgrade, negotiateProtocol, requestedProtocols, UpgradeRequiredError, } from "./helpers.js";
45
+ // Hub
46
+ export { encodeFrame, WsHub, WsRoomTarget } from "./hub.js";
47
+ // Lifecycle (for custom routes built on core's requestUpgrade)
48
+ export { createLifecycle } from "./lifecycle.js";
49
+ // Limits
50
+ export { ConnectionCounter, TokenBucket } from "./limits.js";
51
+ // Messages
52
+ export { WsMessage } from "./message.js";
53
+ // Origin policy
54
+ export { createOriginPolicy, originSourcesFromCors } from "./origin.js";
55
+ // Presence
56
+ export { PresenceLimitError, PresenceRegistry, PresenceTopic, } from "./presence.js";
57
+ // Route builder
58
+ export { WsRouteBuilder } from "./route-builder.js";
59
+ // Router mixin
60
+ export { withWs } from "./router.js";
61
+ // Tickets
62
+ export { bearerFromProtocol, MemoryTicketStore, WsTickets } from "./tickets.js";
63
+ // Contract-typed sockets
64
+ export { ContractViolationError, checkOutbound, parseInbound, TypedWsConnection, } from "./typed.js";
65
+ // Types & close codes
66
+ export { closeFrameFor, MAX_CLOSE_REASON_BYTES, truncateReason, WsCloseCode, WsCloseError, WsProtocolError, } from "./types.js";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Turns a route's `handle(fn)` into the per-socket `SocketHandlers` that core
3
+ * dispatches to. One lifecycle object is created per accepted handshake.
4
+ */
5
+ import { type Logger, type SocketHandlers } from "../core/index.ts";
6
+ import { WsConnection } from "./connection.ts";
7
+ import { type WsCleanupFn, type WsConnectionOptions } from "./types.ts";
8
+ export type WsSetupFn = (socket: WsConnection) => void | WsCleanupFn | Promise<void | WsCleanupFn>;
9
+ export interface LifecycleOptions extends WsConnectionOptions {
10
+ logger?: Pick<Logger, "error">;
11
+ /** Called after the socket opened and before setup runs; throw to reject. */
12
+ onOpen?: (socket: WsConnection) => void;
13
+ }
14
+ /**
15
+ * - Messages arriving while an async setup is still running are buffered
16
+ * (bounded) and replayed in order once it settles — Bun does not hold
17
+ * `message` back while `open` awaits.
18
+ * - Size and rate limits are enforced before anything reaches user code.
19
+ * - Any throw/rejection closes the socket via {@link closeFrameFor}.
20
+ */
21
+ export declare function createLifecycle(req: Request, protocol: string | null, setup: WsSetupFn, options?: LifecycleOptions): SocketHandlers;