@fishenv/http 0.0.3 → 0.0.4
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/ws/channels/handler.d.ts +11 -0
- package/dist/ws/channels/handler.js +42 -11
- package/dist/ws/hub.d.ts +18 -0
- package/dist/ws/hub.js +92 -19
- package/package.json +1 -1
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
* leave order holds); different sockets run concurrently
|
|
8
8
|
* - nothing reaches a handler before the channel is joined and the payload
|
|
9
9
|
* validated against the contract
|
|
10
|
+
* - a joining socket is a room member from the moment its join handler
|
|
11
|
+
* starts: channel events sent meanwhile (broadcasts, `push`) are queued and
|
|
12
|
+
* delivered right after the join reply, so a snapshot returned by the
|
|
13
|
+
* handler plus the events that follow never leave a gap. Events sent while
|
|
14
|
+
* the handler runs may also be in its snapshot; dedupe on the client (a
|
|
15
|
+
* sequence number in the payload) if that matters
|
|
10
16
|
* - repeated protocol violations close the socket with `1008`
|
|
11
17
|
*/
|
|
12
18
|
import { WsHub } from "../hub.ts";
|
|
@@ -19,6 +25,11 @@ export interface ChannelsOptions {
|
|
|
19
25
|
maxViolations?: number;
|
|
20
26
|
/** Frames queued behind a slow handler before closing with `1013`. Default 64. */
|
|
21
27
|
maxPendingFrames?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Channel events queued for a socket while its join handler runs. Default
|
|
30
|
+
* 1024. Exceeding it fails the join with `503 join_overflow`.
|
|
31
|
+
*/
|
|
32
|
+
maxJoinBufferedFrames?: number;
|
|
22
33
|
/**
|
|
23
34
|
* Hub that backs channel membership — pass an instance or a DI token to
|
|
24
35
|
* reach channels from server code via `hub.channel(name)`. A private hub is
|
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
* leave order holds); different sockets run concurrently
|
|
8
8
|
* - nothing reaches a handler before the channel is joined and the payload
|
|
9
9
|
* validated against the contract
|
|
10
|
+
* - a joining socket is a room member from the moment its join handler
|
|
11
|
+
* starts: channel events sent meanwhile (broadcasts, `push`) are queued and
|
|
12
|
+
* delivered right after the join reply, so a snapshot returned by the
|
|
13
|
+
* handler plus the events that follow never leave a gap. Events sent while
|
|
14
|
+
* the handler runs may also be in its snapshot; dedupe on the client (a
|
|
15
|
+
* sequence number in the payload) if that matters
|
|
10
16
|
* - repeated protocol violations close the socket with `1008`
|
|
11
17
|
*/
|
|
12
18
|
import * as v from "valibot";
|
|
@@ -21,6 +27,7 @@ const DEFAULTS = {
|
|
|
21
27
|
maxChannelsPerSocket: 32,
|
|
22
28
|
maxViolations: 10,
|
|
23
29
|
maxPendingFrames: 64,
|
|
30
|
+
maxJoinBufferedFrames: 1024,
|
|
24
31
|
};
|
|
25
32
|
/** Errors for rejected frames, mapped onto the HTTP error vocabulary. */
|
|
26
33
|
class ChannelError extends HttpError {
|
|
@@ -202,23 +209,43 @@ class ChannelSession {
|
|
|
202
209
|
const data = parsePayload(contract.join.payload, frame.d);
|
|
203
210
|
const context = this.#context(def, frame.ch);
|
|
204
211
|
const args = { ...this.#base(path), data, channel: context };
|
|
205
|
-
const
|
|
206
|
-
checkOutbound(contract.join.reply, reply, this.#env.development, `join reply of "${frame.ch}"`);
|
|
207
|
-
// Resolve presence before committing, so a failing tracker aborts the join.
|
|
212
|
+
const room = channelRoom(frame.ch);
|
|
208
213
|
const presence = contract.presence;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
+
// Member (held) before the handler runs, so nothing sent meanwhile is lost.
|
|
215
|
+
const held = this.#hub._hold(this.#socket, room, this.#opts.maxJoinBufferedFrames);
|
|
216
|
+
let reply;
|
|
217
|
+
let tracked = null;
|
|
218
|
+
try {
|
|
219
|
+
reply = def.join ? await def.join(args) : undefined;
|
|
220
|
+
checkOutbound(contract.join.reply, reply, this.#env.development, `join reply of "${frame.ch}"`);
|
|
221
|
+
// Resolve presence before committing, so a failing tracker aborts the join.
|
|
222
|
+
if (presence != null && def.presence != null) {
|
|
223
|
+
tracked = (await def.presence(args));
|
|
224
|
+
}
|
|
225
|
+
if (tracked != null) {
|
|
226
|
+
checkOutbound(presence?.meta, tracked.meta, this.#env.development, `presence meta of "${frame.ch}"`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
if (held)
|
|
231
|
+
this.#hub.leave(this.#socket, room);
|
|
232
|
+
throw err;
|
|
214
233
|
}
|
|
215
234
|
if (this.#socket.closed)
|
|
216
235
|
return;
|
|
217
|
-
|
|
218
|
-
this.#hub.
|
|
236
|
+
// Not held: the app already put the socket in the room through the hub.
|
|
237
|
+
const queued = held ? this.#hub._release(this.#socket, room) : [];
|
|
238
|
+
if (queued === null) {
|
|
239
|
+
throw new ChannelError(409, "join_aborted", "Removed from the channel while joining", false);
|
|
240
|
+
}
|
|
241
|
+
if (queued === "overflow") {
|
|
242
|
+
throw new ChannelError(503, "join_overflow", "Too many channel events while joining", false);
|
|
243
|
+
}
|
|
219
244
|
this.#hub._onLeave(this.#socket, room, (reason) => this.#schedule(() => this.#serverLeave(frame.ch, "kicked", reason)));
|
|
220
245
|
this.#joined.set(frame.ch, { impl: def, name: frame.ch, path, context });
|
|
221
246
|
this.#reply(frame.ref, reply);
|
|
247
|
+
for (const queuedFrame of queued)
|
|
248
|
+
this.#socket.send(queuedFrame);
|
|
222
249
|
if (presence != null) {
|
|
223
250
|
const registry = presenceFor(this.#hub);
|
|
224
251
|
if (tracked != null) {
|
|
@@ -392,7 +419,11 @@ class ChannelSession {
|
|
|
392
419
|
},
|
|
393
420
|
push: (event, payload) => {
|
|
394
421
|
check(event, payload);
|
|
395
|
-
|
|
422
|
+
const frame = eventFrame(name, event, payload);
|
|
423
|
+
// From a join handler: queued behind the join reply.
|
|
424
|
+
if (this.#hub._holdFrame(this.#socket, room, frame))
|
|
425
|
+
return "sent";
|
|
426
|
+
return this.#socket.send(frame);
|
|
396
427
|
},
|
|
397
428
|
leave: (reason = "") => {
|
|
398
429
|
this.#schedule(() => this.#serverLeave(name, "server", reason));
|
package/dist/ws/hub.d.ts
CHANGED
|
@@ -116,6 +116,24 @@ export declare class WsHub {
|
|
|
116
116
|
join(socket: WsConnection, room: string): void;
|
|
117
117
|
/** Remove a socket from a room. Does *not* run leave hooks. */
|
|
118
118
|
leave(socket: WsConnection, room: string): void;
|
|
119
|
+
/**
|
|
120
|
+
* @internal Make a socket a member of `room` whose frames for the room are
|
|
121
|
+
* queued instead of sent, until {@link _release}. Channels use this while a
|
|
122
|
+
* join handler runs, so nothing broadcast meanwhile is missed and nothing
|
|
123
|
+
* arrives before the join reply. Past `limit` queued frames the hold is
|
|
124
|
+
* marked overflowed (further frames are dropped). `false` when nothing is
|
|
125
|
+
* held: the socket is closed or already a (normal) member.
|
|
126
|
+
*/
|
|
127
|
+
_hold(socket: WsConnection, room: string, limit: number): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* @internal End a hold: subscribe the socket normally and return the queued
|
|
130
|
+
* frames, oldest first, for the caller to send. `null` when the socket is no
|
|
131
|
+
* longer held (it left, was kicked or closed); `"overflow"` when the queue
|
|
132
|
+
* overflowed, in which case the socket also leaves the room.
|
|
133
|
+
*/
|
|
134
|
+
_release(socket: WsConnection, room: string): Frame[] | "overflow" | null;
|
|
135
|
+
/** @internal Queue `frame` if `socket` is held in `room`; `false` otherwise. */
|
|
136
|
+
_holdFrame(socket: WsConnection, room: string, frame: Frame): boolean;
|
|
119
137
|
/** Rooms a socket is in. */
|
|
120
138
|
rooms(socket: WsConnection): string[];
|
|
121
139
|
/** Every room with at least one local member. */
|
package/dist/ws/hub.js
CHANGED
|
@@ -114,6 +114,8 @@ export class WsHub {
|
|
|
114
114
|
#roomsOf = new Map();
|
|
115
115
|
#tracked = new Set();
|
|
116
116
|
#leaveHooks = new Map();
|
|
117
|
+
/** Members whose room frames are queued until `_release` (pending joins). */
|
|
118
|
+
#held = new Map();
|
|
117
119
|
#extensions = new Map();
|
|
118
120
|
#bridge;
|
|
119
121
|
#replicated;
|
|
@@ -161,29 +163,14 @@ export class WsHub {
|
|
|
161
163
|
// ── Rooms ───────────────────────────────────────────────────────────
|
|
162
164
|
/** Put a socket in a room (idempotent). Server-side only. */
|
|
163
165
|
join(socket, room) {
|
|
164
|
-
if (socket
|
|
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)
|
|
166
|
+
if (this.#addMember(socket, room) && serverOf(socket) != null) {
|
|
181
167
|
socket.raw.subscribe(this.#topic(room));
|
|
182
|
-
|
|
168
|
+
}
|
|
183
169
|
}
|
|
184
170
|
/** Remove a socket from a room. Does *not* run leave hooks. */
|
|
185
171
|
leave(socket, room) {
|
|
186
172
|
this.#leaveHooks.get(socket)?.delete(room);
|
|
173
|
+
this.#unhold(socket, room);
|
|
187
174
|
const members = this.#rooms.get(room);
|
|
188
175
|
if (members == null || !members.delete(socket))
|
|
189
176
|
return;
|
|
@@ -197,6 +184,86 @@ export class WsHub {
|
|
|
197
184
|
socket.raw.unsubscribe(this.#topic(room));
|
|
198
185
|
}
|
|
199
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* @internal Make a socket a member of `room` whose frames for the room are
|
|
189
|
+
* queued instead of sent, until {@link _release}. Channels use this while a
|
|
190
|
+
* join handler runs, so nothing broadcast meanwhile is missed and nothing
|
|
191
|
+
* arrives before the join reply. Past `limit` queued frames the hold is
|
|
192
|
+
* marked overflowed (further frames are dropped). `false` when nothing is
|
|
193
|
+
* held: the socket is closed or already a (normal) member.
|
|
194
|
+
*/
|
|
195
|
+
_hold(socket, room, limit) {
|
|
196
|
+
if (!this.#addMember(socket, room))
|
|
197
|
+
return false;
|
|
198
|
+
let held = this.#held.get(socket);
|
|
199
|
+
if (held == null) {
|
|
200
|
+
held = new Map();
|
|
201
|
+
this.#held.set(socket, held);
|
|
202
|
+
}
|
|
203
|
+
held.set(room, { frames: [], limit, overflowed: false });
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* @internal End a hold: subscribe the socket normally and return the queued
|
|
208
|
+
* frames, oldest first, for the caller to send. `null` when the socket is no
|
|
209
|
+
* longer held (it left, was kicked or closed); `"overflow"` when the queue
|
|
210
|
+
* overflowed, in which case the socket also leaves the room.
|
|
211
|
+
*/
|
|
212
|
+
_release(socket, room) {
|
|
213
|
+
const held = this.#held.get(socket)?.get(room);
|
|
214
|
+
if (held == null)
|
|
215
|
+
return null;
|
|
216
|
+
this.#unhold(socket, room);
|
|
217
|
+
if (held.overflowed) {
|
|
218
|
+
this.leave(socket, room);
|
|
219
|
+
return "overflow";
|
|
220
|
+
}
|
|
221
|
+
if (serverOf(socket) != null)
|
|
222
|
+
socket.raw.subscribe(this.#topic(room));
|
|
223
|
+
return held.frames;
|
|
224
|
+
}
|
|
225
|
+
/** @internal Queue `frame` if `socket` is held in `room`; `false` otherwise. */
|
|
226
|
+
_holdFrame(socket, room, frame) {
|
|
227
|
+
const held = this.#held.get(socket)?.get(room);
|
|
228
|
+
if (held == null)
|
|
229
|
+
return false;
|
|
230
|
+
if (held.overflowed)
|
|
231
|
+
return true;
|
|
232
|
+
if (held.frames.length >= held.limit) {
|
|
233
|
+
held.overflowed = true;
|
|
234
|
+
held.frames = [];
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
held.frames.push(frame);
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
/** `true` when the socket was added (not closed, not already a member). */
|
|
242
|
+
#addMember(socket, room) {
|
|
243
|
+
if (socket.closed)
|
|
244
|
+
return false;
|
|
245
|
+
let members = this.#rooms.get(room);
|
|
246
|
+
if (members == null) {
|
|
247
|
+
members = new Set();
|
|
248
|
+
this.#rooms.set(room, members);
|
|
249
|
+
}
|
|
250
|
+
if (members.has(socket))
|
|
251
|
+
return false;
|
|
252
|
+
members.add(socket);
|
|
253
|
+
let rooms = this.#roomsOf.get(socket);
|
|
254
|
+
if (rooms == null) {
|
|
255
|
+
rooms = new Set();
|
|
256
|
+
this.#roomsOf.set(socket, rooms);
|
|
257
|
+
}
|
|
258
|
+
rooms.add(room);
|
|
259
|
+
this.#track(socket);
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
#unhold(socket, room) {
|
|
263
|
+
const held = this.#held.get(socket);
|
|
264
|
+
if (held?.delete(room) && held.size === 0)
|
|
265
|
+
this.#held.delete(socket);
|
|
266
|
+
}
|
|
200
267
|
/** Rooms a socket is in. */
|
|
201
268
|
rooms(socket) {
|
|
202
269
|
return [...(this.#roomsOf.get(socket) ?? [])];
|
|
@@ -330,6 +397,10 @@ export class WsHub {
|
|
|
330
397
|
for (const socket of members) {
|
|
331
398
|
if (socket.closed)
|
|
332
399
|
continue;
|
|
400
|
+
if (this._holdFrame(socket, room, frame)) {
|
|
401
|
+
delivered++;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
333
404
|
const server = serverOf(socket);
|
|
334
405
|
if (server == null) {
|
|
335
406
|
if (isDelivered(socket.send(frame)))
|
|
@@ -348,7 +419,9 @@ export class WsHub {
|
|
|
348
419
|
for (const socket of [...members]) {
|
|
349
420
|
if (!this.#matches(socket, filter))
|
|
350
421
|
continue;
|
|
351
|
-
if (
|
|
422
|
+
if (this._holdFrame(socket, room, frame))
|
|
423
|
+
delivered++;
|
|
424
|
+
else if (isDelivered(socket.send(frame)))
|
|
352
425
|
delivered++;
|
|
353
426
|
}
|
|
354
427
|
return delivered;
|