@mauroandre/velojs 0.0.54 → 0.0.56

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
@@ -124,6 +139,35 @@ export interface TestSubscription<TEvent = any, TSnapshot = any> {
124
139
  /** Force disconnect. Triggers per-channel source abortSignal server-side. */
125
140
  close(): Promise<void>;
126
141
  }
142
+ /**
143
+ * A live-loader channel subscription. Every update — the snapshot on connect,
144
+ * each emit's snapshot and each slice — arrives through `next()` as the `data`
145
+ * exactly as it went on the wire (a whole snapshot or a raw slice), so `await
146
+ * next({ timeoutMs })` is the clock: no sleep, no retry, no timing assertion.
147
+ *
148
+ * `nextEvent()` is the same arrival stream discriminated as
149
+ * `{ type: "snapshot" | "slice", data }` — use it when the test cares which
150
+ * mode delivered the update. `next()` and `nextEvent()` each advance their own
151
+ * cursor over the same arrivals, so use one or the other within a test.
152
+ *
153
+ * The connection only exists after the connect snapshot: await the first
154
+ * `next()` before emitting, and the server is guaranteed to have registered it.
155
+ */
156
+ export interface TestChannelSubscription<T = any> extends TestSubscription<T, T> {
157
+ /** Next arrival, discriminated by the mode that delivered it. */
158
+ nextEvent(opts: NextOptions): Promise<ChannelEvent<T>>;
159
+ }
160
+ /** One channel arrival: a whole snapshot or a slice to be merged. */
161
+ export interface ChannelEvent<T = any> {
162
+ type: "snapshot" | "slice";
163
+ data: T;
164
+ }
165
+ /** The module (or moduleId) a channel belongs to, as `app.channel()` accepts it. */
166
+ export type ChannelModuleRef = string | {
167
+ metadata?: {
168
+ moduleId?: string;
169
+ } | undefined;
170
+ };
127
171
  export interface MockContextOptions {
128
172
  user?: any;
129
173
  params?: Params;
@@ -168,6 +212,30 @@ export interface TestApp {
168
212
  */
169
213
  loader<T = any>(fn: Function | string, opts?: LoaderRequestOptions): Promise<T | TestResponse>;
170
214
  subscribe<TEvent = any, TSnapshot = any>(stream: EventStream<TEvent, TSnapshot> | string, opts?: SubscribeOptions): Promise<TestSubscription<TEvent, TSnapshot>>;
215
+ /**
216
+ * Open a live-loader channel connection for a module's declared channel.
217
+ *
218
+ * The module may be the imported route module (its `metadata.moduleId` is
219
+ * read) or a moduleId string. Cookies (and therefore the principal) come
220
+ * from the options or from `app.as(user)`; the route node's middlewares run
221
+ * for real, so an unauthenticated subscription is denied exactly as in
222
+ * production.
223
+ *
224
+ * ```ts
225
+ * const sub = await app.as({ id: 7 }).channel(Expenses, "teamExpenses");
226
+ * await sub.next({ timeoutMs: 1000 }); // snapshot on connect
227
+ * await emit("teamExpenses", { teamId: 7 });
228
+ * expect(await sub.next({ timeoutMs: 1000 })).toEqual({ teamTotal: 2 });
229
+ *
230
+ * // The same arrivals discriminated: nextEvent() tells a snapshot from a slice.
231
+ * await emit(Expenses, "teamExpenses", { teamId: 7 }, { teamTotal: 880 });
232
+ * expect(await sub.nextEvent({ timeoutMs: 1000 })).toEqual({
233
+ * type: "slice",
234
+ * data: { teamTotal: 880 },
235
+ * });
236
+ * ```
237
+ */
238
+ channel<T = any>(module: ChannelModuleRef, name: string, opts?: SubscribeOptions): Promise<TestChannelSubscription<T>>;
171
239
  /**
172
240
  * Open an in-memory session against a `socket_*` handler.
173
241
  *
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
@@ -1,4 +1,4 @@
1
- import type { PluginOption } from "vite";
1
+ import { type PluginOption } from "vite";
2
2
  import type { VeloConfig } from "./config.js";
3
3
  import * as t from "@babel/types";
4
4
  export declare function injectMetadata(code: string, moduleId: string, fullPath?: string, path?: string): string;
@@ -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