@fishenv/http 0.0.2 → 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/contract/builder.d.ts +9 -4
- package/dist/contract/builder.js +2 -0
- package/dist/contract/index.d.ts +3 -1
- package/dist/contract/index.js +1 -0
- package/dist/contract/types.d.ts +9 -1
- package/dist/contract/ws.d.ts +178 -0
- package/dist/contract/ws.js +164 -0
- package/dist/core/compose.js +5 -0
- package/dist/core/error.d.ts +21 -0
- package/dist/core/error.js +50 -7
- package/dist/core/index.d.ts +7 -3
- package/dist/core/index.js +5 -1
- package/dist/core/matcher.js +2 -1
- package/dist/core/route-builder.d.ts +4 -4
- package/dist/core/router.d.ts +16 -2
- package/dist/core/router.js +45 -6
- package/dist/core/transport.d.ts +39 -0
- package/dist/core/transport.js +68 -0
- package/dist/core/types.d.ts +22 -3
- package/dist/core/upgrade.d.ts +85 -0
- package/dist/core/upgrade.js +110 -0
- package/dist/core/utils/url.d.ts +4 -1
- package/dist/core/utils/url.js +14 -3
- package/dist/sse/hub.js +15 -33
- package/dist/sse/types.d.ts +3 -5
- package/dist/static/index.d.ts +27 -0
- package/dist/static/index.js +25 -0
- package/dist/static/negotiate.d.ts +23 -0
- package/dist/static/negotiate.js +85 -0
- package/dist/static/options.d.ts +44 -0
- package/dist/static/options.js +190 -0
- package/dist/static/resolve.d.ts +56 -0
- package/dist/static/resolve.js +113 -0
- package/dist/static/response.d.ts +53 -0
- package/dist/static/response.js +146 -0
- package/dist/static/router.d.ts +59 -0
- package/dist/static/router.js +217 -0
- package/dist/static/rules.d.ts +21 -0
- package/dist/static/rules.js +49 -0
- package/dist/static/types.d.ts +76 -0
- package/dist/static/types.js +0 -0
- package/dist/ws/channels/channel.d.ts +175 -0
- package/dist/ws/channels/channel.js +125 -0
- package/dist/ws/channels/handler.d.ts +44 -0
- package/dist/ws/channels/handler.js +517 -0
- package/dist/ws/channels/protocol.d.ts +96 -0
- package/dist/ws/channels/protocol.js +94 -0
- package/dist/ws/channels/target.d.ts +36 -0
- package/dist/ws/channels/target.js +56 -0
- package/dist/ws/connection.d.ts +56 -0
- package/dist/ws/connection.js +172 -0
- package/dist/ws/helpers.d.ts +17 -0
- package/dist/ws/helpers.js +48 -0
- package/dist/ws/hub.d.ts +190 -0
- package/dist/ws/hub.js +562 -0
- package/dist/ws/index.d.ts +65 -0
- package/dist/ws/index.js +66 -0
- package/dist/ws/lifecycle.d.ts +21 -0
- package/dist/ws/lifecycle.js +97 -0
- package/dist/ws/limits.d.ts +32 -0
- package/dist/ws/limits.js +67 -0
- package/dist/ws/message.d.ts +19 -0
- package/dist/ws/message.js +58 -0
- package/dist/ws/origin.d.ts +40 -0
- package/dist/ws/origin.js +152 -0
- package/dist/ws/presence.d.ts +96 -0
- package/dist/ws/presence.js +420 -0
- package/dist/ws/route-builder.d.ts +136 -0
- package/dist/ws/route-builder.js +265 -0
- package/dist/ws/router.d.ts +21 -0
- package/dist/ws/router.js +36 -0
- package/dist/ws/tickets.d.ts +82 -0
- package/dist/ws/tickets.js +161 -0
- package/dist/ws/typed.d.ts +54 -0
- package/dist/ws/typed.js +104 -0
- package/dist/ws/types.d.ts +69 -0
- package/dist/ws/types.js +70 -0
- package/package.json +14 -2
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `channel()` — the server-side implementation of a channel, either from a
|
|
3
|
+
* shared `c.channel()` contract or defined inline.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* const roomImpl = channel(contract.room)
|
|
7
|
+
* .join(async ({ ctx, path, data }) => ({ history: await load(path.roomId) }))
|
|
8
|
+
* .on("message", async ({ data, channel }) => {
|
|
9
|
+
* channel.broadcast("message", saved);
|
|
10
|
+
* return { id: saved.id };
|
|
11
|
+
* });
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Builders are immutable. The type parameters track which client events have
|
|
15
|
+
* handlers and whether an authorizer exists, so `.channels([...])` rejects
|
|
16
|
+
* incomplete implementations at compile time (and again at `build()`).
|
|
17
|
+
*/
|
|
18
|
+
import type * as v from "valibot";
|
|
19
|
+
import { type AnyChannelContract, ChannelContract, type InferChannel } from "../../contract/index.ts";
|
|
20
|
+
import { type AnySchema, type DIContainer } from "../../core/index.ts";
|
|
21
|
+
import type { WsConnection } from "../connection.ts";
|
|
22
|
+
import type { WsSendResult } from "../types.ts";
|
|
23
|
+
type Shape<C> = InferChannel<C>;
|
|
24
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
25
|
+
export type ClientEventName<C> = keyof Shape<C>["clientEvents"] & string;
|
|
26
|
+
type ServerEvents<C> = Shape<C>["serverEvents"];
|
|
27
|
+
/** Declared server events, or any name when the contract declares none (inline). */
|
|
28
|
+
export type ServerEventName<C> = [keyof ServerEvents<C>] extends [never] ? string : keyof ServerEvents<C> & string;
|
|
29
|
+
export type ServerEventPayload<C, E> = E extends keyof ServerEvents<C> ? ServerEvents<C>[E]["payloadIn"] : unknown;
|
|
30
|
+
/** Why a channel was left; passed to `.leave()` handlers. */
|
|
31
|
+
export type LeaveReason = "client" | "disconnect" | "kicked" | "server";
|
|
32
|
+
type PresenceMetaIn<C> = [Shape<C>["presenceMeta"]] extends [never] ? never : Shape<C>["presenceMeta"] extends {
|
|
33
|
+
in: infer M;
|
|
34
|
+
} ? M : never;
|
|
35
|
+
/** Presence of the current channel, from a handler's point of view. */
|
|
36
|
+
export interface ChannelPresence<M> {
|
|
37
|
+
/** Change the current socket's meta (it must be tracked). */
|
|
38
|
+
update(meta: M): boolean;
|
|
39
|
+
/** Cluster-wide `{ [key]: { ref, meta }[] }`. */
|
|
40
|
+
list(): Record<string, {
|
|
41
|
+
ref: string;
|
|
42
|
+
meta: M;
|
|
43
|
+
}[]>;
|
|
44
|
+
has(key: string): boolean;
|
|
45
|
+
keys(): string[];
|
|
46
|
+
}
|
|
47
|
+
/** The `channel` argument of every handler: actions scoped to this channel. */
|
|
48
|
+
export interface ChannelContext<C> {
|
|
49
|
+
/** Only on channels whose contract declares `.presence()`. */
|
|
50
|
+
readonly presence: [PresenceMetaIn<C>] extends [never] ? undefined : ChannelPresence<PresenceMetaIn<C>>;
|
|
51
|
+
/** Concrete channel name, e.g. `rooms/42`. */
|
|
52
|
+
readonly name: string;
|
|
53
|
+
/** Everyone in the channel on every node, including the sender. */
|
|
54
|
+
broadcast<E extends ServerEventName<C>>(event: E, payload: ServerEventPayload<C, E>): number;
|
|
55
|
+
/** Everyone except the current socket. */
|
|
56
|
+
broadcastFrom<E extends ServerEventName<C>>(event: E, payload: ServerEventPayload<C, E>): number;
|
|
57
|
+
/** Only the current socket. */
|
|
58
|
+
push<E extends ServerEventName<C>>(event: E, payload: ServerEventPayload<C, E>): WsSendResult;
|
|
59
|
+
/** Server-initiated leave for the current socket. */
|
|
60
|
+
leave(reason?: string): void;
|
|
61
|
+
}
|
|
62
|
+
export interface ChannelHandlerArgs<Ctx, C> {
|
|
63
|
+
ctx: Ctx;
|
|
64
|
+
/** Channel-name params, validated by the contract's `.param()` schemas. */
|
|
65
|
+
path: Shape<C>["params"];
|
|
66
|
+
socket: WsConnection;
|
|
67
|
+
/** The upgrade request. */
|
|
68
|
+
req: Request;
|
|
69
|
+
container: DIContainer;
|
|
70
|
+
channel: ChannelContext<C>;
|
|
71
|
+
}
|
|
72
|
+
type ReplyOf<R> = [R] extends [undefined] ? unknown : R;
|
|
73
|
+
export type JoinFn<Ctx, C> = (args: ChannelHandlerArgs<Ctx, C> & {
|
|
74
|
+
data: Shape<C>["join"]["payload"];
|
|
75
|
+
}) => MaybePromise<ReplyOf<Shape<C>["join"]["replyIn"]>>;
|
|
76
|
+
export type EventFn<Ctx, C, E extends ClientEventName<C>> = (args: ChannelHandlerArgs<Ctx, C> & {
|
|
77
|
+
data: Shape<C>["clientEvents"][E]["payload"];
|
|
78
|
+
}) => MaybePromise<ReplyOf<Shape<C>["clientEvents"][E]["replyIn"]>>;
|
|
79
|
+
export type AnyEventFn<Ctx, C> = (args: ChannelHandlerArgs<Ctx, C> & {
|
|
80
|
+
event: string;
|
|
81
|
+
data: unknown;
|
|
82
|
+
}) => unknown;
|
|
83
|
+
/**
|
|
84
|
+
* Decide how the joining socket appears in presence: return its key (e.g.
|
|
85
|
+
* the user id) and meta, or `null` to join without being tracked.
|
|
86
|
+
*/
|
|
87
|
+
export type PresenceFn<Ctx, C> = (args: ChannelHandlerArgs<Ctx, C> & {
|
|
88
|
+
data: Shape<C>["join"]["payload"];
|
|
89
|
+
}) => MaybePromise<{
|
|
90
|
+
key: string;
|
|
91
|
+
meta: PresenceMetaIn<C>;
|
|
92
|
+
} | null | undefined>;
|
|
93
|
+
export type LeaveFn<Ctx, C> = (args: Omit<ChannelHandlerArgs<Ctx, C>, "channel"> & {
|
|
94
|
+
reason: LeaveReason;
|
|
95
|
+
}) => unknown;
|
|
96
|
+
/** Runtime data of a channel implementation. */
|
|
97
|
+
export interface ChannelImplDefinition {
|
|
98
|
+
contract: AnyChannelContract;
|
|
99
|
+
pattern: RegExp;
|
|
100
|
+
paramNames: string[];
|
|
101
|
+
inline: boolean;
|
|
102
|
+
isPublic: boolean;
|
|
103
|
+
join?: (args: any) => unknown;
|
|
104
|
+
events: Map<string, (args: any) => unknown>;
|
|
105
|
+
any?: (args: any) => unknown;
|
|
106
|
+
leave?: (args: any) => unknown;
|
|
107
|
+
presence?: (args: any) => unknown;
|
|
108
|
+
}
|
|
109
|
+
type Status<Missing, Joined> = [Missing] extends [never] ? Joined extends true ? "complete" : "missing .join() or .public()" : `missing .on("${Missing & string}")`;
|
|
110
|
+
export declare class ChannelImpl<Ctx, C extends AnyChannelContract, Handled extends string = never, Joined extends boolean = false> {
|
|
111
|
+
#private;
|
|
112
|
+
/** @internal Phantom: makes `Ctx` contravariant for `.channels()` checks. */
|
|
113
|
+
readonly _ctx?: (ctx: Ctx) => void;
|
|
114
|
+
/** @internal Phantom: `"complete"` once every event is handled and joins are authorized. */
|
|
115
|
+
readonly _status?: Status<Exclude<ClientEventName<C>, Handled>, Joined>;
|
|
116
|
+
readonly contract: C;
|
|
117
|
+
/** @internal Use {@link channel}. */
|
|
118
|
+
constructor(contract: C, def?: Partial<ChannelImplDefinition>);
|
|
119
|
+
get _definition(): ChannelImplDefinition;
|
|
120
|
+
/** Re-type the handler context, e.g. `channel(room).context<AppCtx>()`. */
|
|
121
|
+
context<NewCtx>(): ChannelImpl<NewCtx, C, Handled, Joined>;
|
|
122
|
+
/** Inline channels only: narrow/coerce a channel-name param. */
|
|
123
|
+
param<Name extends string & keyof Shape<C>["params"], S extends AnySchema>(name: Name, schema: S): ChannelImpl<Ctx, ParamResult<C, Name, S>, Handled, Joined>;
|
|
124
|
+
/**
|
|
125
|
+
* The join authorizer — throw (e.g. `ForbiddenError`) to refuse. The return
|
|
126
|
+
* value is the join reply. Inline channels may pass a payload schema first.
|
|
127
|
+
*/
|
|
128
|
+
join(fn: JoinFn<Ctx, C>): ChannelImpl<Ctx, C, Handled, true>;
|
|
129
|
+
join<S extends AnySchema>(payload: S, fn: JoinFn<Ctx, JoinResult<C, S>>): ChannelImpl<Ctx, JoinResult<C, S>, Handled, true>;
|
|
130
|
+
/** Explicitly allow joining without authorization. */
|
|
131
|
+
public(): ChannelImpl<Ctx, C, Handled, true>;
|
|
132
|
+
/**
|
|
133
|
+
* Handle a client event. The return value is the reply (sent when the
|
|
134
|
+
* client passed a `ref`). Inline channels pass the payload schema second.
|
|
135
|
+
*/
|
|
136
|
+
on<E extends ClientEventName<C>>(event: E, fn: EventFn<Ctx, C, E>): ChannelImpl<Ctx, C, Handled | E, Joined>;
|
|
137
|
+
on<E extends string, S extends AnySchema>(event: E, payload: S, fn: EventFn<Ctx, EventResult<C, E, S>, E & ClientEventName<EventResult<C, E, S>>>): ChannelImpl<Ctx, EventResult<C, E, S>, Handled | E, Joined>;
|
|
138
|
+
/**
|
|
139
|
+
* Catch-all for events without a handler — unvalidated, for proxies and
|
|
140
|
+
* bridges. Undeclared events are otherwise rejected with `unknown_event`.
|
|
141
|
+
*/
|
|
142
|
+
onAny(fn: AnyEventFn<Ctx, C>): ChannelImpl<Ctx, C, Handled, Joined>;
|
|
143
|
+
/**
|
|
144
|
+
* Track joining sockets in presence (contract must declare `.presence()`).
|
|
145
|
+
* Untracked automatically on leave, kick and disconnect.
|
|
146
|
+
*/
|
|
147
|
+
presence(fn: PresenceFn<Ctx, C>): ChannelImpl<Ctx, C, Handled, Joined>;
|
|
148
|
+
/** Runs when the socket leaves for any reason (client, disconnect, kick). */
|
|
149
|
+
leave(fn: LeaveFn<Ctx, C>): ChannelImpl<Ctx, C, Handled, Joined>;
|
|
150
|
+
}
|
|
151
|
+
type ParamResult<C, Name extends string, S extends AnySchema> = C extends ChannelContract<infer N, infer P, infer J, infer CE, infer SE, infer PM> ? ChannelContract<N, Omit<P, Name> & Record<Name, v.InferOutput<S>>, J, CE, SE, PM> : never;
|
|
152
|
+
type JoinResult<C, S extends AnySchema> = C extends ChannelContract<infer N, infer P, infer _J, infer CE, infer SE, infer PM> ? ChannelContract<N, P, {
|
|
153
|
+
payload: S;
|
|
154
|
+
reply: undefined;
|
|
155
|
+
}, CE, SE, PM> : never;
|
|
156
|
+
type EventResult<C, E extends string, S extends AnySchema> = C extends ChannelContract<infer N, infer P, infer J, infer CE, infer SE, infer PM> ? ChannelContract<N, P, J, CE & Record<E, {
|
|
157
|
+
payload: S;
|
|
158
|
+
reply: undefined;
|
|
159
|
+
}>, SE, PM> : never;
|
|
160
|
+
export type AnyChannelImpl = ChannelImpl<any, AnyChannelContract, any, any>;
|
|
161
|
+
export interface ChannelFactory<Ctx> {
|
|
162
|
+
/** Implement a shared `c.channel()` contract. */
|
|
163
|
+
<C extends AnyChannelContract>(contract: C): ChannelImpl<Ctx, C>;
|
|
164
|
+
/** Define a channel inline (router path syntax, e.g. `rooms/:roomId`). */
|
|
165
|
+
<N extends string>(name: N): ChannelImpl<Ctx, ChannelContract<N>>;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Create a channel implementation. Handler `ctx` is untyped by default; use
|
|
169
|
+
* `channel.withContext<AppCtx>()` (or `.context<AppCtx>()`) to type it, or
|
|
170
|
+
* the factory passed to `.channels((channel) => [...])`.
|
|
171
|
+
*/
|
|
172
|
+
export declare const channel: ChannelFactory<Record<string, unknown>> & {
|
|
173
|
+
withContext<Ctx>(): ChannelFactory<Ctx>;
|
|
174
|
+
};
|
|
175
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `channel()` — the server-side implementation of a channel, either from a
|
|
3
|
+
* shared `c.channel()` contract or defined inline.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* const roomImpl = channel(contract.room)
|
|
7
|
+
* .join(async ({ ctx, path, data }) => ({ history: await load(path.roomId) }))
|
|
8
|
+
* .on("message", async ({ data, channel }) => {
|
|
9
|
+
* channel.broadcast("message", saved);
|
|
10
|
+
* return { id: saved.id };
|
|
11
|
+
* });
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Builders are immutable. The type parameters track which client events have
|
|
15
|
+
* handlers and whether an authorizer exists, so `.channels([...])` rejects
|
|
16
|
+
* incomplete implementations at compile time (and again at `build()`).
|
|
17
|
+
*/
|
|
18
|
+
import { ChannelContract, } from "../../contract/index.js";
|
|
19
|
+
import { buildRouteRegex, } from "../../core/index.js";
|
|
20
|
+
export class ChannelImpl {
|
|
21
|
+
contract;
|
|
22
|
+
#def;
|
|
23
|
+
/** @internal Use {@link channel}. */
|
|
24
|
+
constructor(contract, def) {
|
|
25
|
+
this.contract = contract;
|
|
26
|
+
const { regex, paramNames } = buildRouteRegex(`/${contract.name}`);
|
|
27
|
+
this.#def = {
|
|
28
|
+
contract,
|
|
29
|
+
pattern: regex,
|
|
30
|
+
paramNames,
|
|
31
|
+
inline: false,
|
|
32
|
+
isPublic: false,
|
|
33
|
+
events: new Map(),
|
|
34
|
+
...def,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
get _definition() {
|
|
38
|
+
return this.#def;
|
|
39
|
+
}
|
|
40
|
+
#next(patch, contract) {
|
|
41
|
+
return new ChannelImpl(contract ?? this.contract, {
|
|
42
|
+
...this.#def,
|
|
43
|
+
events: new Map(this.#def.events),
|
|
44
|
+
...patch,
|
|
45
|
+
...(contract ? { contract } : {}),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Re-type the handler context, e.g. `channel(room).context<AppCtx>()`. */
|
|
49
|
+
context() {
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
/** Inline channels only: narrow/coerce a channel-name param. */
|
|
53
|
+
param(name, schema) {
|
|
54
|
+
this.#assertInline("param");
|
|
55
|
+
return this.#next({}, this.contract.param(name, schema));
|
|
56
|
+
}
|
|
57
|
+
// biome-ignore lint/suspicious/noExplicitAny: overload implementation
|
|
58
|
+
join(a, b) {
|
|
59
|
+
if (b === undefined)
|
|
60
|
+
return this.#next({ join: a });
|
|
61
|
+
this.#assertInline("join(schema, fn)");
|
|
62
|
+
return this.#next({ join: b }, this.contract.join(a));
|
|
63
|
+
}
|
|
64
|
+
/** Explicitly allow joining without authorization. */
|
|
65
|
+
public() {
|
|
66
|
+
return this.#next({ isPublic: true });
|
|
67
|
+
}
|
|
68
|
+
// biome-ignore lint/suspicious/noExplicitAny: overload implementation
|
|
69
|
+
on(event, a, b) {
|
|
70
|
+
if (this.#def.events.has(event)) {
|
|
71
|
+
throw new Error(`${this.contract.name}: .on("${event}") is already defined`);
|
|
72
|
+
}
|
|
73
|
+
if (b === undefined) {
|
|
74
|
+
if (!this.contract._definition.clientEvents.has(event)) {
|
|
75
|
+
throw new Error(`${this.contract.name}: "${event}" is not a client event of the contract`);
|
|
76
|
+
}
|
|
77
|
+
const events = new Map(this.#def.events);
|
|
78
|
+
events.set(event, a);
|
|
79
|
+
return this.#next({ events });
|
|
80
|
+
}
|
|
81
|
+
this.#assertInline(`on("${event}", schema, fn)`);
|
|
82
|
+
const events = new Map(this.#def.events);
|
|
83
|
+
events.set(event, b);
|
|
84
|
+
return this.#next({ events }, this.contract.clientEvent(event, a));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Catch-all for events without a handler — unvalidated, for proxies and
|
|
88
|
+
* bridges. Undeclared events are otherwise rejected with `unknown_event`.
|
|
89
|
+
*/
|
|
90
|
+
onAny(fn) {
|
|
91
|
+
return this.#next({ any: fn });
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Track joining sockets in presence (contract must declare `.presence()`).
|
|
95
|
+
* Untracked automatically on leave, kick and disconnect.
|
|
96
|
+
*/
|
|
97
|
+
presence(fn) {
|
|
98
|
+
if (this.contract._definition.presence == null) {
|
|
99
|
+
throw new Error(`${this.contract.name}: .presence() requires presence in the channel contract`);
|
|
100
|
+
}
|
|
101
|
+
return this.#next({ presence: fn });
|
|
102
|
+
}
|
|
103
|
+
/** Runs when the socket leaves for any reason (client, disconnect, kick). */
|
|
104
|
+
leave(fn) {
|
|
105
|
+
return this.#next({ leave: fn });
|
|
106
|
+
}
|
|
107
|
+
#assertInline(what) {
|
|
108
|
+
if (!this.#def.inline) {
|
|
109
|
+
throw new Error(`${this.contract.name}: ${what} is only available on inline channels — the contract defines it`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function createFactory() {
|
|
114
|
+
return ((source) => typeof source === "string"
|
|
115
|
+
? new ChannelImpl(new ChannelContract(source), { inline: true })
|
|
116
|
+
: new ChannelImpl(source));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Create a channel implementation. Handler `ctx` is untyped by default; use
|
|
120
|
+
* `channel.withContext<AppCtx>()` (or `.context<AppCtx>()`) to type it, or
|
|
121
|
+
* the factory passed to `.channels((channel) => [...])`.
|
|
122
|
+
*/
|
|
123
|
+
export const channel = Object.assign(createFactory(), {
|
|
124
|
+
withContext: () => createFactory(),
|
|
125
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `channels()` — the opt-in protocol handler. A per-socket state machine
|
|
3
|
+
* that multiplexes authorized channels over one socket.
|
|
4
|
+
*
|
|
5
|
+
* Guarantees:
|
|
6
|
+
* - frames of one socket are processed **sequentially** (join → event →
|
|
7
|
+
* leave order holds); different sockets run concurrently
|
|
8
|
+
* - nothing reaches a handler before the channel is joined and the payload
|
|
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
|
|
16
|
+
* - repeated protocol violations close the socket with `1008`
|
|
17
|
+
*/
|
|
18
|
+
import { WsHub } from "../hub.ts";
|
|
19
|
+
import type { WsProtocolHandler } from "../route-builder.ts";
|
|
20
|
+
import type { AnyChannelImpl } from "./channel.ts";
|
|
21
|
+
export interface ChannelsOptions {
|
|
22
|
+
/** Default 32. Exceeding it fails the join with `429 too_many_channels`. */
|
|
23
|
+
maxChannelsPerSocket?: number;
|
|
24
|
+
/** Protocol violations tolerated before closing with `1008`. Default 10. */
|
|
25
|
+
maxViolations?: number;
|
|
26
|
+
/** Frames queued behind a slow handler before closing with `1013`. Default 64. */
|
|
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;
|
|
33
|
+
/**
|
|
34
|
+
* Hub that backs channel membership — pass an instance or a DI token to
|
|
35
|
+
* reach channels from server code via `hub.channel(name)`. A private hub is
|
|
36
|
+
* used otherwise.
|
|
37
|
+
*/
|
|
38
|
+
hub?: WsHub | (abstract new (...args: any[]) => WsHub);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build the protocol handler. Prefer `.channels([...])` on a ws route,
|
|
42
|
+
* which also type-checks completeness; this form plugs into `.handleWith()`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function channels(impls: readonly AnyChannelImpl[], options?: ChannelsOptions): WsProtocolHandler<Record<string, unknown>, Record<string, unknown>>;
|