@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.
@@ -2,10 +2,26 @@
2
2
  * SSE subscription helper — parses the SSE protocol from a streaming Response
3
3
  * and exposes assertion-friendly methods for tests.
4
4
  */
5
- import type { TestSubscription } from "./types.js";
5
+ import type { TestSubscription, NextOptions, ChannelEvent } from "./types.js";
6
6
  interface BuildSubscriptionOptions {
7
7
  response: Response;
8
8
  parseJson?: boolean;
9
+ /**
10
+ * Deliver `snapshot` events through `events`/`next()` too, not only via the
11
+ * `snapshot` getter. The live loader has no `message` events at all — every
12
+ * update is a snapshot — so its subscription reads them as plain events.
13
+ */
14
+ snapshotEvents?: boolean;
15
+ /**
16
+ * Live-loader mode: `slice` events travel through `events`/`next()` like the
17
+ * snapshots (the payload exactly as it went on the wire), and every arrival
18
+ * is recorded discriminated in `nextEvent()` — `{ type: "snapshot" |
19
+ * "slice", data }`. `next()` keeps the slice-1 contract: the raw `data`.
20
+ */
21
+ channelEvents?: boolean;
9
22
  }
10
- export declare function buildSubscription<TEvent, TSnapshot>(opts: BuildSubscriptionOptions): Promise<TestSubscription<TEvent, TSnapshot>>;
23
+ export declare function buildSubscription<TEvent, TSnapshot>(opts: BuildSubscriptionOptions): Promise<TestSubscription<TEvent, TSnapshot> & {
24
+ /** Next arrival discriminated by the mode that delivered it. */
25
+ nextEvent(opts: NextOptions): Promise<ChannelEvent<TEvent>>;
26
+ }>;
11
27
  export {};
@@ -5,6 +5,7 @@ import type { Hono, Context } from "hono";
5
5
  import type { AppRoutes } from "../types.js";
6
6
  import type { EventStream } from "../events.js";
7
7
  import type { SocketHandler, SocketStub } from "../sockets.js";
8
+ import type { ChannelMap, ChannelRegistryOptions } from "../channels.js";
8
9
  export type Cookies = Record<string, string>;
9
10
  export type Headers = Record<string, string>;
10
11
  export type Query = Record<string, string | string[]>;
@@ -28,6 +29,20 @@ export interface CreateTestAppOptions {
28
29
  getSessionCookie?: (input: {
29
30
  user: any;
30
31
  }) => Promise<Cookies> | Cookies;
32
+ /**
33
+ * The app's live-loader channel map — the same object `app/channels.ts`
34
+ * exports. Registered before the app is created, so the channel routes and
35
+ * `emit()` can resolve partitions. May also be registered from `bootstrap`
36
+ * via `registerChannels()`.
37
+ */
38
+ channels?: ChannelMap;
39
+ /**
40
+ * Options of the channel registration: the coalescing window of the
41
+ * invalidation mode and the emit log. `createTestApp` registers with
42
+ * `coalesceMs: 0` (immediate, deterministic) unless this option says
43
+ * otherwise; `logEmits` defaults to on, as in production.
44
+ */
45
+ channelOptions?: ChannelRegistryOptions;
31
46
  /**
32
47
  * Serve the app over real TCP on this port, in addition to the in-memory
33
48
  * API. Needed when an actor outside this process must reach the app over
@@ -80,6 +95,18 @@ export interface SubscribeOptions extends RequestOptions {
80
95
  /** Channel ID — sent as `?channel=...`. */
81
96
  channel?: string;
82
97
  }
98
+ /**
99
+ * Options of a live-loader channel connection.
100
+ *
101
+ * `params` are the route params the connection declares — the same ones the
102
+ * browser extracts from the URL and sends in `?_route=`. The server-side
103
+ * validation of the declared path runs for real: a key the module's path does
104
+ * not declare rejects the subscription (400), never silently ignores the input.
105
+ */
106
+ export interface ChannelSubscribeOptions extends SubscribeOptions {
107
+ /** Route params the connection declares (serialized into `?_route=`). */
108
+ params?: Params;
109
+ }
83
110
  export interface NextOptions {
84
111
  /** Reject if no event arrives within this many ms. */
85
112
  timeoutMs: number;
@@ -124,6 +151,35 @@ export interface TestSubscription<TEvent = any, TSnapshot = any> {
124
151
  /** Force disconnect. Triggers per-channel source abortSignal server-side. */
125
152
  close(): Promise<void>;
126
153
  }
154
+ /**
155
+ * A live-loader channel subscription. Every update — the snapshot on connect,
156
+ * each emit's snapshot and each slice — arrives through `next()` as the `data`
157
+ * exactly as it went on the wire (a whole snapshot or a raw slice), so `await
158
+ * next({ timeoutMs })` is the clock: no sleep, no retry, no timing assertion.
159
+ *
160
+ * `nextEvent()` is the same arrival stream discriminated as
161
+ * `{ type: "snapshot" | "slice", data }` — use it when the test cares which
162
+ * mode delivered the update. `next()` and `nextEvent()` each advance their own
163
+ * cursor over the same arrivals, so use one or the other within a test.
164
+ *
165
+ * The connection only exists after the connect snapshot: await the first
166
+ * `next()` before emitting, and the server is guaranteed to have registered it.
167
+ */
168
+ export interface TestChannelSubscription<T = any> extends TestSubscription<T, T> {
169
+ /** Next arrival, discriminated by the mode that delivered it. */
170
+ nextEvent(opts: NextOptions): Promise<ChannelEvent<T>>;
171
+ }
172
+ /** One channel arrival: a whole snapshot or a slice to be merged. */
173
+ export interface ChannelEvent<T = any> {
174
+ type: "snapshot" | "slice";
175
+ data: T;
176
+ }
177
+ /** The module (or moduleId) a channel belongs to, as `app.channel()` accepts it. */
178
+ export type ChannelModuleRef = string | {
179
+ metadata?: {
180
+ moduleId?: string;
181
+ } | undefined;
182
+ };
127
183
  export interface MockContextOptions {
128
184
  user?: any;
129
185
  params?: Params;
@@ -168,6 +224,35 @@ export interface TestApp {
168
224
  */
169
225
  loader<T = any>(fn: Function | string, opts?: LoaderRequestOptions): Promise<T | TestResponse>;
170
226
  subscribe<TEvent = any, TSnapshot = any>(stream: EventStream<TEvent, TSnapshot> | string, opts?: SubscribeOptions): Promise<TestSubscription<TEvent, TSnapshot>>;
227
+ /**
228
+ * Open a live-loader channel connection for a module's declared channel.
229
+ *
230
+ * The module may be the imported route module (its `metadata.moduleId` is
231
+ * read) or a moduleId string. Cookies (and therefore the principal) come
232
+ * from the options or from `app.as(user)`; the route node's middlewares run
233
+ * for real, so an unauthenticated subscription is denied exactly as in
234
+ * production. `params` are the route params the connection declares — the
235
+ * server validates their keys against the module's path.
236
+ *
237
+ * ```ts
238
+ * const sub = await app.as({ id: 7 }).channel(Expenses, "teamExpenses");
239
+ * await sub.next({ timeoutMs: 1000 }); // snapshot on connect
240
+ * await emit("teamExpenses", { user: { teamId: 7 } });
241
+ * expect(await sub.next({ timeoutMs: 1000 })).toEqual({ teamTotal: 2 });
242
+ *
243
+ * // A route with `:params`: the connection declares them and the loader
244
+ * // re-executes with them.
245
+ * const project = await app.channel(Project, "projectFiles", { params: { id: "7" } });
246
+ *
247
+ * // The same arrivals discriminated: nextEvent() tells a snapshot from a slice.
248
+ * await emit(Expenses, "teamExpenses", { user: { teamId: 7 } }, { teamTotal: 880 });
249
+ * expect(await sub.nextEvent({ timeoutMs: 1000 })).toEqual({
250
+ * type: "slice",
251
+ * data: { teamTotal: 880 },
252
+ * });
253
+ * ```
254
+ */
255
+ channel<T = any>(module: ChannelModuleRef, name: string, opts?: ChannelSubscribeOptions): Promise<TestChannelSubscription<T>>;
171
256
  /**
172
257
  * Open an in-memory session against a `socket_*` handler.
173
258
  *
package/dist/types.d.ts CHANGED
@@ -20,6 +20,15 @@ export interface Metadata {
20
20
  export interface RouteModule {
21
21
  Component: ComponentType<any>;
22
22
  loader?: (args: LoaderArgs) => Promise<any>;
23
+ /**
24
+ * Channel names whose state must keep this module's loader data faithful.
25
+ * The opt-in for the live loader: with `channels`, the framework derives an
26
+ * internal SSE route per channel (`/_channel/{moduleId}/{channel}`) and the
27
+ * client opens one connection per (module, channel) after hydration. A
28
+ * module that declares `channels` without a `loader` is an explicit
29
+ * dev/build error. Requires an entry per name in `app/channels.ts`.
30
+ */
31
+ channels?: readonly string[];
23
32
  staticPaths?: () => Promise<Record<string, string>[]>;
24
33
  metadata?: Metadata;
25
34
  [key: `action_${string}`]: (args: ActionArgs<any>) => Promise<any>;
package/dist/vite.d.ts CHANGED
@@ -66,6 +66,21 @@ export declare function buildFullPathMap(code: string): Map<string, PathInfo>;
66
66
  * Percorre recursivamente a árvore de rotas e coleta moduleName → { fullPath, path }
67
67
  */
68
68
  export declare function collectFullPaths(elements: (t.Expression | t.SpreadElement | null)[], parentPath: string, result: Map<string, PathInfo>): void;
69
+ /**
70
+ * Names declared by a module's `channels` export — `null` when the module does
71
+ * not declare the convention (no `channels` array export at all).
72
+ *
73
+ * Only the array form is a channel declaration: `app/channels.ts` also exports
74
+ * a `channels` binding, but an object (the name → resolver map), and must never
75
+ * be read as a module declaring channels.
76
+ */
77
+ export declare function declaredChannelNames(code: string): string[] | null;
78
+ /**
79
+ * Channel names declared in the app's channel map file (`app/channels.ts`) —
80
+ * the canonical list the framework validates module declarations against.
81
+ * A missing file yields an empty set, which makes every declaration unknown.
82
+ */
83
+ export declare function readChannelMapNames(file: string): Set<string>;
69
84
  /**
70
85
  * @hono/vite-dev-server treats `exclude` as all-or-nothing: passing one
71
86
  * replaces the defaults entirely. We compose instead — the upstream defaults