@fishenv/http 0.0.2 → 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 (78) hide show
  1. package/dist/contract/builder.d.ts +9 -4
  2. package/dist/contract/builder.js +2 -0
  3. package/dist/contract/index.d.ts +3 -1
  4. package/dist/contract/index.js +1 -0
  5. package/dist/contract/types.d.ts +9 -1
  6. package/dist/contract/ws.d.ts +178 -0
  7. package/dist/contract/ws.js +164 -0
  8. package/dist/core/compose.js +5 -0
  9. package/dist/core/error.d.ts +21 -0
  10. package/dist/core/error.js +50 -7
  11. package/dist/core/index.d.ts +7 -3
  12. package/dist/core/index.js +5 -1
  13. package/dist/core/matcher.js +2 -1
  14. package/dist/core/route-builder.d.ts +4 -4
  15. package/dist/core/router.d.ts +16 -2
  16. package/dist/core/router.js +45 -6
  17. package/dist/core/transport.d.ts +39 -0
  18. package/dist/core/transport.js +68 -0
  19. package/dist/core/types.d.ts +22 -3
  20. package/dist/core/upgrade.d.ts +85 -0
  21. package/dist/core/upgrade.js +110 -0
  22. package/dist/core/utils/url.d.ts +4 -1
  23. package/dist/core/utils/url.js +14 -3
  24. package/dist/sse/hub.js +15 -33
  25. package/dist/sse/types.d.ts +3 -5
  26. package/dist/static/index.d.ts +27 -0
  27. package/dist/static/index.js +25 -0
  28. package/dist/static/negotiate.d.ts +23 -0
  29. package/dist/static/negotiate.js +85 -0
  30. package/dist/static/options.d.ts +44 -0
  31. package/dist/static/options.js +190 -0
  32. package/dist/static/resolve.d.ts +56 -0
  33. package/dist/static/resolve.js +113 -0
  34. package/dist/static/response.d.ts +53 -0
  35. package/dist/static/response.js +146 -0
  36. package/dist/static/router.d.ts +59 -0
  37. package/dist/static/router.js +217 -0
  38. package/dist/static/rules.d.ts +21 -0
  39. package/dist/static/rules.js +49 -0
  40. package/dist/static/types.d.ts +76 -0
  41. package/dist/static/types.js +0 -0
  42. package/dist/ws/channels/channel.d.ts +175 -0
  43. package/dist/ws/channels/channel.js +125 -0
  44. package/dist/ws/channels/handler.d.ts +33 -0
  45. package/dist/ws/channels/handler.js +486 -0
  46. package/dist/ws/channels/protocol.d.ts +96 -0
  47. package/dist/ws/channels/protocol.js +94 -0
  48. package/dist/ws/channels/target.d.ts +36 -0
  49. package/dist/ws/channels/target.js +56 -0
  50. package/dist/ws/connection.d.ts +56 -0
  51. package/dist/ws/connection.js +172 -0
  52. package/dist/ws/helpers.d.ts +17 -0
  53. package/dist/ws/helpers.js +48 -0
  54. package/dist/ws/hub.d.ts +172 -0
  55. package/dist/ws/hub.js +489 -0
  56. package/dist/ws/index.d.ts +65 -0
  57. package/dist/ws/index.js +66 -0
  58. package/dist/ws/lifecycle.d.ts +21 -0
  59. package/dist/ws/lifecycle.js +97 -0
  60. package/dist/ws/limits.d.ts +32 -0
  61. package/dist/ws/limits.js +67 -0
  62. package/dist/ws/message.d.ts +19 -0
  63. package/dist/ws/message.js +58 -0
  64. package/dist/ws/origin.d.ts +40 -0
  65. package/dist/ws/origin.js +152 -0
  66. package/dist/ws/presence.d.ts +96 -0
  67. package/dist/ws/presence.js +420 -0
  68. package/dist/ws/route-builder.d.ts +136 -0
  69. package/dist/ws/route-builder.js +265 -0
  70. package/dist/ws/router.d.ts +21 -0
  71. package/dist/ws/router.js +36 -0
  72. package/dist/ws/tickets.d.ts +82 -0
  73. package/dist/ws/tickets.js +161 -0
  74. package/dist/ws/typed.d.ts +54 -0
  75. package/dist/ws/typed.js +104 -0
  76. package/dist/ws/types.d.ts +69 -0
  77. package/dist/ws/types.js +70 -0
  78. 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,33 @@
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
+ * - repeated protocol violations close the socket with `1008`
11
+ */
12
+ import { WsHub } from "../hub.ts";
13
+ import type { WsProtocolHandler } from "../route-builder.ts";
14
+ import type { AnyChannelImpl } from "./channel.ts";
15
+ export interface ChannelsOptions {
16
+ /** Default 32. Exceeding it fails the join with `429 too_many_channels`. */
17
+ maxChannelsPerSocket?: number;
18
+ /** Protocol violations tolerated before closing with `1008`. Default 10. */
19
+ maxViolations?: number;
20
+ /** Frames queued behind a slow handler before closing with `1013`. Default 64. */
21
+ maxPendingFrames?: number;
22
+ /**
23
+ * Hub that backs channel membership — pass an instance or a DI token to
24
+ * reach channels from server code via `hub.channel(name)`. A private hub is
25
+ * used otherwise.
26
+ */
27
+ hub?: WsHub | (abstract new (...args: any[]) => WsHub);
28
+ }
29
+ /**
30
+ * Build the protocol handler. Prefer `.channels([...])` on a ws route,
31
+ * which also type-checks completeness; this form plugs into `.handleWith()`.
32
+ */
33
+ export declare function channels(impls: readonly AnyChannelImpl[], options?: ChannelsOptions): WsProtocolHandler<Record<string, unknown>, Record<string, unknown>>;