@mauroandre/velojs 0.0.55 → 0.0.57
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/README.md +423 -3
- package/agent/AGENTS.md +216 -0
- package/agent/skills/velojs-live-loader/SKILL.md +410 -0
- package/dist/channels.d.ts +370 -0
- package/dist/channels.js +376 -0
- package/dist/client.js +56 -45
- package/dist/components.js +7 -6
- package/dist/events.d.ts +6 -0
- package/dist/events.js +1 -0
- package/dist/hooks.d.ts +26 -21
- package/dist/hooks.js +117 -98
- package/dist/live-loader.d.ts +120 -0
- package/dist/live-loader.js +141 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +144 -118
- package/dist/testing/index.d.ts +5 -1
- package/dist/testing/index.js +495 -412
- package/dist/testing/subscription.d.ts +18 -2
- package/dist/testing/types.d.ts +85 -0
- package/dist/types.d.ts +9 -0
- package/dist/vite.d.ts +15 -0
- package/dist/vite.js +431 -340
- package/package.json +1 -1
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live loader — server-side runtime (slice 1).
|
|
3
|
+
*
|
|
4
|
+
* A route module opts into state synchronization with a sibling `channels`
|
|
5
|
+
* export (an array of channel names). The framework derives one internal SSE
|
|
6
|
+
* route per declared channel — `GET /_channel/{moduleId}/{channel}`, the same
|
|
7
|
+
* family as `/_action` and `/_event` — which inherits the middlewares of the
|
|
8
|
+
* module's route node and serves the module's loader as a stream of snapshots.
|
|
9
|
+
*
|
|
10
|
+
* The app owns the partition policy in `app/channels.ts`, a name → resolver map:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* export const channels = {
|
|
14
|
+
* teamExpenses: { scope: ({ user }) => `team:${user?.teamId}` },
|
|
15
|
+
* };
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* On subscribe the resolver receives `{ user, params }`: `user` is the
|
|
19
|
+
* materialized principal (`c.get("user")`, the house key — `undefined` when the
|
|
20
|
+
* app does not materialize one, so a public channel ignores it and an
|
|
21
|
+
* authenticated one returns `null`, which denies the subscription with 403) and
|
|
22
|
+
* `params` are the route params the client declared and the runtime validated
|
|
23
|
+
* against the module's path (empty for a route without `:params`). The
|
|
24
|
+
* partition NEVER derives from the query string or from any other client input;
|
|
25
|
+
* `params` are not input — they are the address the client is already seeing,
|
|
26
|
+
* validated by shape and then by the scope (ownership/permission).
|
|
27
|
+
*
|
|
28
|
+
* `emit(channel, ctx)` resolves the same partition key on the producer side —
|
|
29
|
+
* the producer builds the same `{ user, params }` shape with the fields its
|
|
30
|
+
* scope consumes — finds the group of connections in that partition and
|
|
31
|
+
* re-executes the loader for each connection, with that connection's own
|
|
32
|
+
* principal and route params, pushing a fresh snapshot down the wire. Emitting
|
|
33
|
+
* to a channel with no subscribers is a no-op; emitting to a channel with no
|
|
34
|
+
* entry in the app map throws immediately.
|
|
35
|
+
*
|
|
36
|
+
* `emit(module, channel, ctx, slice)` is the second mode, addressed to the
|
|
37
|
+
* (module, channel) pair: the producer already holds the value, so it travels
|
|
38
|
+
* as a slice — the connections of that pair in the emitting partition merge it
|
|
39
|
+
* shallowly by key, and no loader runs. The slice is typed as a `Partial` of
|
|
40
|
+
* the module's loader return, which is why the module is part of the address:
|
|
41
|
+
* one channel can be declared by modules with different `Data` (a layout and a
|
|
42
|
+
* page), and a slice is the shape of exactly one of them.
|
|
43
|
+
*
|
|
44
|
+
* A channel's group (channel + partition) has a lifecycle: the first connection
|
|
45
|
+
* arms it (`onGroupOpen({ partition, params })`) and the last one disarms it
|
|
46
|
+
* (`onGroupClose({ partition })`), both awaited — that is where a watcher with a
|
|
47
|
+
* cost of life is armed and disarmed, instead of running forever for an empty
|
|
48
|
+
* group.
|
|
49
|
+
*
|
|
50
|
+
* Every emit logs one line (channel, partition, kind, connections at the
|
|
51
|
+
* moment of the gesture, timestamp) unless `logEmits: false` is registered,
|
|
52
|
+
* and invalidations are folded by a coalescing window per (channel, partition)
|
|
53
|
+
* (`coalesceMs`, default 50, `0` disables). Before delivering anything the
|
|
54
|
+
* runtime re-derives each connection's partition with the ctx captured on
|
|
55
|
+
* subscribe: a connection that moved (or whose scope now returns null) is
|
|
56
|
+
* dropped and closed instead of receiving data of a group it left.
|
|
57
|
+
*/
|
|
58
|
+
import type { Context, Hono, MiddlewareHandler } from "hono";
|
|
59
|
+
import type { LoaderArgs, RouteModule } from "./types.js";
|
|
60
|
+
/** Result of a partition resolver. `null`/`undefined` denies the subscription. */
|
|
61
|
+
export type ChannelScopeResult = string | null | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* The ctx of a partition resolver — the same shape on both sides.
|
|
64
|
+
*
|
|
65
|
+
* On **subscribe** the runtime builds it: `user` is the materialized principal
|
|
66
|
+
* (`c.get("user")`, the house key — `undefined` when the app does not
|
|
67
|
+
* materialize one) and `params` are the route params the client declared and
|
|
68
|
+
* the runtime validated against the module's path (URL strings; empty for a
|
|
69
|
+
* route without `:params`).
|
|
70
|
+
*
|
|
71
|
+
* On **emit** the producer builds the same object with the fields its scope
|
|
72
|
+
* consumes — `{ params: { id: 7 } }` for a resource channel, `{ user: … }`
|
|
73
|
+
* for a principal one — and the same `scope` runs against it.
|
|
74
|
+
*/
|
|
75
|
+
export interface ChannelScopeContext {
|
|
76
|
+
user?: any;
|
|
77
|
+
params?: Record<string, any>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* What `onGroupOpen` receives: the partition key of the group that just got its
|
|
81
|
+
* first connection, and the validated route params of **that** connection.
|
|
82
|
+
*
|
|
83
|
+
* The params are identical across the group only when the partition derives
|
|
84
|
+
* from them (a watcher per resource — the case the hook exists for). A channel
|
|
85
|
+
* partitioned by principal, declared in a module with `:params`, mixes
|
|
86
|
+
* resources in the same group and the hook's params are the first connection's
|
|
87
|
+
* — arbitrary for the rest; such a channel must not consume them here.
|
|
88
|
+
*/
|
|
89
|
+
export interface ChannelGroupOpenContext {
|
|
90
|
+
partition: string;
|
|
91
|
+
params: Record<string, string>;
|
|
92
|
+
}
|
|
93
|
+
/** What `onGroupClose` receives: the partition key of the group being torn down. */
|
|
94
|
+
export interface ChannelGroupCloseContext {
|
|
95
|
+
partition: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* One entry of the app's channel map (`app/channels.ts`).
|
|
99
|
+
*
|
|
100
|
+
* `scope` receives `{ user, params }` on subscribe and the emitter's object on
|
|
101
|
+
* emit; returning `null` denies with 403. Omitted → the channel is public
|
|
102
|
+
* (`() => "all"`).
|
|
103
|
+
*
|
|
104
|
+
* `onGroupOpen`/`onGroupClose` are the group lifecycle — armed by the first
|
|
105
|
+
* connection of a (channel, partition) group, disarmed by the last one — and
|
|
106
|
+
* may be async: the runtime awaits them (the group is only marked open after
|
|
107
|
+
* `onGroupOpen` resolves; only discarded after `onGroupClose` resolves). A hook
|
|
108
|
+
* that throws or rejects is logged loudly and the group continues.
|
|
109
|
+
*/
|
|
110
|
+
export interface ChannelDefinition {
|
|
111
|
+
scope?: (ctx: ChannelScopeContext) => ChannelScopeResult | Promise<ChannelScopeResult>;
|
|
112
|
+
onGroupOpen?: (ctx: ChannelGroupOpenContext) => void | Promise<void>;
|
|
113
|
+
onGroupClose?: (ctx: ChannelGroupCloseContext) => void | Promise<void>;
|
|
114
|
+
}
|
|
115
|
+
/** The `app/channels.ts` map: channel name → partition resolver. */
|
|
116
|
+
export type ChannelMap = Record<string, ChannelDefinition>;
|
|
117
|
+
/** SSE event carrying the loader's current value — it replaces the module's data. */
|
|
118
|
+
export declare const SNAPSHOT_EVENT = "snapshot";
|
|
119
|
+
/** SSE event carrying a slice — the client merges it into the module's current value. */
|
|
120
|
+
export declare const SLICE_EVENT = "slice";
|
|
121
|
+
/** SSE event signaling that the loader re-execution failed on the server. */
|
|
122
|
+
export declare const CHANNEL_ERROR_EVENT = "error";
|
|
123
|
+
/** SSE event signaling a deliberate server-side close. */
|
|
124
|
+
export declare const CHANNEL_CLOSE_EVENT = "close";
|
|
125
|
+
/** Default coalescing window, in milliseconds. */
|
|
126
|
+
export declare const DEFAULT_COALESCE_MS = 50;
|
|
127
|
+
/**
|
|
128
|
+
* Default idle timeout of a channel connection, in milliseconds: a connection
|
|
129
|
+
* with no deliveries (snapshot or slice) for this long is closed by the
|
|
130
|
+
* server. The heartbeat does NOT reset it — idle is about data delivery,
|
|
131
|
+
* heartbeat is transport. The close makes a live page cycle
|
|
132
|
+
* (close → EventSource reconnect → snapshot on connect), which repairs the
|
|
133
|
+
* state, and frees an abandoned page's resources for good.
|
|
134
|
+
*/
|
|
135
|
+
export declare const DEFAULT_IDLE_MS = 300000;
|
|
136
|
+
/** The two modes of `emit`, as they appear in the emit log. */
|
|
137
|
+
export type ChannelEmitKind = "invalidate" | "slice";
|
|
138
|
+
/**
|
|
139
|
+
* `registerChannels` options.
|
|
140
|
+
*
|
|
141
|
+
* `coalesceMs` is the coalescing window of the invalidation mode, per
|
|
142
|
+
* (channel, partition): every emit inside an open window becomes one
|
|
143
|
+
* re-execution round, fired when the window closes. `0` disables it (immediate
|
|
144
|
+
* — the slice-1 behavior). The default is `DEFAULT_COALESCE_MS` in any
|
|
145
|
+
* registration without options; `createTestApp` registers with `0` for
|
|
146
|
+
* determinism.
|
|
147
|
+
*
|
|
148
|
+
* `logEmits` writes one line per emit gesture (never per consolidated
|
|
149
|
+
* delivery). Default: on.
|
|
150
|
+
*
|
|
151
|
+
* `idleMs` closes a connection that had no deliveries (snapshot or slice) for
|
|
152
|
+
* that long — a silent connection is closed even though its heartbeat keeps
|
|
153
|
+
* flowing; heartbeats never reset the idle clock. Default: `DEFAULT_IDLE_MS`
|
|
154
|
+
* (5 min) in any registration without options; `0` disables it.
|
|
155
|
+
* `createTestApp` registers with `0` for determinism.
|
|
156
|
+
*
|
|
157
|
+
* `heartbeatMs` writes an SSE comment (`: ping`) through the write chain every
|
|
158
|
+
* interval, keeping proxies and the browser from buffering or closing an idle
|
|
159
|
+
* stream. A comment generates no client event and never counts as a delivery
|
|
160
|
+
* (no effect on freshness or idle). Default: `DEFAULT_HEARTBEAT_MS` (20s) in
|
|
161
|
+
* any registration without options; `0` disables it.
|
|
162
|
+
*/
|
|
163
|
+
export interface ChannelRegistryOptions {
|
|
164
|
+
coalesceMs?: number;
|
|
165
|
+
logEmits?: boolean;
|
|
166
|
+
idleMs?: number;
|
|
167
|
+
heartbeatMs?: number;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* What `emit(module, channel, ctx, slice)` accepts: the imported route module
|
|
171
|
+
* (the same object the route tree uses — the Vite plugin has injected its
|
|
172
|
+
* `metadata`, and its `loader` types the slice).
|
|
173
|
+
*/
|
|
174
|
+
export interface ChannelEmitModule {
|
|
175
|
+
metadata?: {
|
|
176
|
+
moduleId?: string;
|
|
177
|
+
} | undefined;
|
|
178
|
+
channels?: readonly string[] | undefined;
|
|
179
|
+
loader?: ((args: LoaderArgs) => unknown) | undefined;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The slice type of a module: a `Partial` of whatever its loader returns.
|
|
183
|
+
* `Partial` (plus `exactOptionalPropertyTypes` on the app's tsconfig) is what
|
|
184
|
+
* makes a wrong key or a wrong value type a compile error instead of a silent
|
|
185
|
+
* no-op at runtime.
|
|
186
|
+
*/
|
|
187
|
+
export type ChannelSlice<M> = M extends {
|
|
188
|
+
loader?: (...args: any[]) => infer R;
|
|
189
|
+
} ? Partial<Awaited<R>> : never;
|
|
190
|
+
/**
|
|
191
|
+
* Registers (replaces) the app's channel map and its options. Idempotent.
|
|
192
|
+
*
|
|
193
|
+
* Called without options, the coalescing window is `DEFAULT_COALESCE_MS` — the
|
|
194
|
+
* window matters wherever emits burst (a job writing N rows), not only in
|
|
195
|
+
* production.
|
|
196
|
+
*/
|
|
197
|
+
export declare function registerChannels(map: ChannelMap | undefined, options?: ChannelRegistryOptions): void;
|
|
198
|
+
/** The currently registered channel map (read-only use). */
|
|
199
|
+
export declare function getChannelMap(): ChannelMap;
|
|
200
|
+
/** Channel names declared in the app map, in declaration order. */
|
|
201
|
+
export declare function channelNames(): string[];
|
|
202
|
+
/** One live channel connection (an SSE subscription). */
|
|
203
|
+
export interface ChannelConnection {
|
|
204
|
+
channel: string;
|
|
205
|
+
/** Partition key resolved at subscribe time — never from client input. */
|
|
206
|
+
partition: string;
|
|
207
|
+
moduleId: string;
|
|
208
|
+
/**
|
|
209
|
+
* The principal materialized at subscribe (`c.get("user")`), captured for
|
|
210
|
+
* the partition revalidation of every emit — the scope re-runs against it.
|
|
211
|
+
*/
|
|
212
|
+
principal: unknown;
|
|
213
|
+
loader: (args: LoaderArgs) => Promise<unknown>;
|
|
214
|
+
/** The subscribing request's context — the loader re-runs against it. */
|
|
215
|
+
c: Context;
|
|
216
|
+
params: Record<string, string>;
|
|
217
|
+
query: Record<string, string>;
|
|
218
|
+
/** Writes one SSE event on this connection. Resolves when it hits the wire. */
|
|
219
|
+
send: (event: string, data: string) => Promise<void>;
|
|
220
|
+
/**
|
|
221
|
+
* Ends this connection: a `close` event on the wire, then the group entry
|
|
222
|
+
* and the stream. Used when the connection's partition no longer holds —
|
|
223
|
+
* or when its idle timeout elapsed.
|
|
224
|
+
*/
|
|
225
|
+
terminate: (reason?: string) => Promise<void>;
|
|
226
|
+
/**
|
|
227
|
+
* Epoch ms of this connection's last delivery (snapshot or slice) — the
|
|
228
|
+
* clock the idle timeout runs on and what the inspector reports. Heartbeat
|
|
229
|
+
* comments never touch it.
|
|
230
|
+
*/
|
|
231
|
+
lastDeliveryAt?: number | undefined;
|
|
232
|
+
}
|
|
233
|
+
/** One live group of a channel: the connections of one module in one partition. */
|
|
234
|
+
export interface ChannelInspectGroup {
|
|
235
|
+
/** The route module holding these connections (`metadata.moduleId`). */
|
|
236
|
+
moduleId: string;
|
|
237
|
+
/** The partition key resolved at subscribe time. */
|
|
238
|
+
partition: string;
|
|
239
|
+
/** Live connections in this (module, partition) pair. */
|
|
240
|
+
connections: number;
|
|
241
|
+
/** ISO timestamp of the last delivery; `null` when none was delivered yet. */
|
|
242
|
+
lastDeliveryAt: string | null;
|
|
243
|
+
}
|
|
244
|
+
/** One live channel: its groups and its total. */
|
|
245
|
+
export interface ChannelInspectChannel {
|
|
246
|
+
channel: string;
|
|
247
|
+
/** Groups identified by (moduleId, partition) — which page holds what. */
|
|
248
|
+
groups: ChannelInspectGroup[];
|
|
249
|
+
connections: number;
|
|
250
|
+
}
|
|
251
|
+
/** One open coalescing window: the invalidations not yet delivered. */
|
|
252
|
+
export interface ChannelInspectWindow {
|
|
253
|
+
channel: string;
|
|
254
|
+
partition: string;
|
|
255
|
+
}
|
|
256
|
+
/** The full live state of the channel registry, as `inspectChannels()` sees it. */
|
|
257
|
+
export interface ChannelInspectReport {
|
|
258
|
+
channels: ChannelInspectChannel[];
|
|
259
|
+
/** Open coalescing windows, per (channel, partition). */
|
|
260
|
+
openCoalesceWindows: ChannelInspectWindow[];
|
|
261
|
+
totalConnections: number;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Server-side snapshot of the live channels: per channel, the groups
|
|
265
|
+
* identified by (moduleId, partition key), each with its connection count and
|
|
266
|
+
* the timestamp of its last delivery; the coalescing windows still open; and
|
|
267
|
+
* the grand totals. Always available — the app decides how (and with what
|
|
268
|
+
* guard) to expose it; the dev server exposes it as `GET /_channel-inspect`.
|
|
269
|
+
*/
|
|
270
|
+
export declare function inspectChannels(): ChannelInspectReport;
|
|
271
|
+
/** Test-only. Drops every live connection, group and open window. */
|
|
272
|
+
export declare function __resetChannels(): void;
|
|
273
|
+
/**
|
|
274
|
+
* Executes the connection's loader with the connection's own principal and
|
|
275
|
+
* pushes the result as a snapshot. Per-connection execution (rather than one
|
|
276
|
+
* execution per partition) is deliberate: every subscriber always receives the
|
|
277
|
+
* snapshot computed with its own scope, which makes the homogeneous-partition
|
|
278
|
+
* rule unnecessary as a security invariant. What a burst of invalidations
|
|
279
|
+
* amortizes is the number of *rounds*, not the executions inside one — a single
|
|
280
|
+
* computation serving the whole partition is a later slice.
|
|
281
|
+
*
|
|
282
|
+
* A loader that throws reports the failure on the connection (`error` event)
|
|
283
|
+
* instead of dying silently; a loader that returns a `Response` (a redirect
|
|
284
|
+
* short-circuit) has no state to send.
|
|
285
|
+
*/
|
|
286
|
+
export declare function pushSnapshot(conn: ChannelConnection): Promise<void>;
|
|
287
|
+
/**
|
|
288
|
+
* Signals that a channel's partition changed: every connection in that
|
|
289
|
+
* partition receives a fresh snapshot (its loader re-executed with the
|
|
290
|
+
* connection's own principal and route params). Connections in other
|
|
291
|
+
* partitions receive nothing; a channel with no subscribers is a no-op; a
|
|
292
|
+
* channel with no entry in the app map throws immediately, naming the channel.
|
|
293
|
+
*
|
|
294
|
+
* The ctx is the object the same `scope` receives: the producer builds it with
|
|
295
|
+
* the fields its scope consumes (`{ params: { id: 7 } }` for a resource
|
|
296
|
+
* channel, `{ user: … }` for a principal one).
|
|
297
|
+
*
|
|
298
|
+
* ```ts
|
|
299
|
+
* import { emit } from "@mauroandre/velojs/server";
|
|
300
|
+
* await emit("teamExpenses", { user: { teamId: 7 } });
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
export declare function emit(channel: string, ctx?: ChannelScopeContext): Promise<void>;
|
|
304
|
+
/**
|
|
305
|
+
* Slice mode: pushes the value the producer already holds to the connections
|
|
306
|
+
* of the (module, channel) pair in the emitting partition — **without**
|
|
307
|
+
* re-executing the loader. The slice is typed as a `Partial` of what the
|
|
308
|
+
* module's loader returns, and the merge on arrival is shallow by key.
|
|
309
|
+
*
|
|
310
|
+
* ```ts
|
|
311
|
+
* import * as Expenses from "../app/expenses/Expenses.js";
|
|
312
|
+
* await emit(Expenses, "teamExpenses", { user: { teamId: 7 } }, { teamTotal: 880 });
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
export declare function emit<M extends ChannelEmitModule>(module: M, channel: string, ctx: ChannelScopeContext, slice: ChannelSlice<M>): Promise<void>;
|
|
316
|
+
/**
|
|
317
|
+
* Serializes the SSE writes of one connection and never lets a failed write
|
|
318
|
+
* break the chain.
|
|
319
|
+
*
|
|
320
|
+
* A write can fail because the client went away mid-event (tab closed, proxy
|
|
321
|
+
* cut). The connection is cleaned up by the abort handler either way, but the
|
|
322
|
+
* failure is logged instead of swallowed: a snapshot lost in a dead write must
|
|
323
|
+
* not be indistinguishable from an emit that never happened.
|
|
324
|
+
*/
|
|
325
|
+
export declare function createChannelWriteChain(channel: string, write: (payload: ChannelWritePayload) => Promise<unknown>): (payload: ChannelWritePayload) => Promise<void>;
|
|
326
|
+
/**
|
|
327
|
+
* What the write chain carries: an SSE event (`{ event, data }`) or a raw
|
|
328
|
+
* chunk — the heartbeat is a comment (`": ping\n\n"`), which is no event at
|
|
329
|
+
* all: no client event fires for it and no delivery clock moves.
|
|
330
|
+
*/
|
|
331
|
+
export type ChannelWritePayload = {
|
|
332
|
+
event: string;
|
|
333
|
+
data: string;
|
|
334
|
+
} | string;
|
|
335
|
+
/** Query key carrying the route params the connection declares. */
|
|
336
|
+
export declare const ROUTE_PARAMS_QUERY = "_route";
|
|
337
|
+
/**
|
|
338
|
+
* The `:param` names a route path declares, in declaration order. A path with
|
|
339
|
+
* a catch-all (`/docs/*`) declares none: the pattern has no key to extract, so
|
|
340
|
+
* the connection travels by principal only.
|
|
341
|
+
*/
|
|
342
|
+
export declare function declaredRouteParams(fullPath: string | null | undefined): string[];
|
|
343
|
+
/**
|
|
344
|
+
* Parses and validates the `?_route=` payload of a channel connection against
|
|
345
|
+
* the names the module's route path declares.
|
|
346
|
+
*
|
|
347
|
+
* The client declares the params of the URL it is already seeing; it is not
|
|
348
|
+
* free input: a key the module's path does not declare rejects the
|
|
349
|
+
* subscription with an explicit error, and every value is a URL string. An
|
|
350
|
+
* absent payload is the empty object — a route without `:params` sends none.
|
|
351
|
+
*/
|
|
352
|
+
export declare function parseRouteParamsQuery(raw: string | undefined | null, declared: readonly string[]): {
|
|
353
|
+
params: Record<string, string>;
|
|
354
|
+
error: string | null;
|
|
355
|
+
};
|
|
356
|
+
/**
|
|
357
|
+
* Registers the internal SSE route of one declared channel:
|
|
358
|
+
* `GET /_channel/{moduleId}/{channel}`, inheriting the route node's
|
|
359
|
+
* middlewares (auth policy is declared once per node).
|
|
360
|
+
*
|
|
361
|
+
* Guards are loud: a module that declares `channels` without a `loader`, or a
|
|
362
|
+
* channel with no entry in `app/channels.ts`, is an explicit error — never a
|
|
363
|
+
* silent inert route.
|
|
364
|
+
*
|
|
365
|
+
* The connection carries the client-declared route params (validated here
|
|
366
|
+
* against the module's path), the principal captured for the partition
|
|
367
|
+
* revalidation of every emit, and `terminate` lets an emit close a connection
|
|
368
|
+
* whose partition no longer holds.
|
|
369
|
+
*/
|
|
370
|
+
export declare function registerChannelRoute(app: Hono, path: string, channel: string, module: RouteModule, moduleId: string, middlewares: MiddlewareHandler[]): void;
|