@mauroandre/velojs 0.0.18 → 0.0.20

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 CHANGED
@@ -501,6 +501,200 @@ export const action_login = async ({ body }: { body: LoginBody }) => {
501
501
 
502
502
  **Error handling**: Actions do NOT throw on server errors. They resolve with `{ error: "message" }`. Always check `result.error` explicitly.
503
503
 
504
+ ### Shared actions in Layouts
505
+
506
+ Actions are tied to the module where they're declared — the route is always `/_action/{moduleId}/{actionName}`. Declare an action in a Layout and import it from any child module to share it across multiple pages.
507
+
508
+ ```typescript
509
+ // app/admin/Layout.tsx — action declared once in the layout
510
+ import type { ActionArgs } from "@mauroandre/velojs";
511
+
512
+ export const action_logout = async ({ c }: ActionArgs) => {
513
+ const { deleteCookie } = await import("@mauroandre/velojs/cookie");
514
+ deleteCookie(c!, "session");
515
+ return { ok: true };
516
+ };
517
+
518
+ export const Component = ({ children }) => (/* layout with children */);
519
+ ```
520
+
521
+ ```typescript
522
+ // app/admin/Dashboard.tsx — imports and uses the action from Layout
523
+ import { action_logout } from "./Layout.js";
524
+
525
+ export const Component = () => (
526
+ <button onClick={async () => {
527
+ await action_logout({});
528
+ window.location.href = "/login";
529
+ }}>Logout</button>
530
+ );
531
+ ```
532
+
533
+ The client-side transform rewrites the import to fetch `/_action/admin/Layout/logout`, so the action always points to the correct URL regardless of where it's imported. Middlewares on the Layout apply automatically.
534
+
535
+ ---
536
+
537
+ ## Event Streams
538
+
539
+ Push real-time updates from server to client via Server-Sent Events (SSE). Live progress, notifications, metrics, log streaming, AI tokens — anything server → client.
540
+
541
+ Three verbs: `emit`, `close`, `useEventStream`. The framework handles routing, types, listener management, snapshots, lifecycle, reconnection, and cleanup.
542
+
543
+ ### Shortest example
544
+
545
+ ```typescript
546
+ // app/admin/Provision.tsx
547
+ import { createEventStream } from "@mauroandre/velojs";
548
+ import { useEventStream } from "@mauroandre/velojs/hooks";
549
+
550
+ export const stream_logs = createEventStream<string>();
551
+
552
+ export const Component = () => {
553
+ const { snapshot, data, closed } = useEventStream(stream_logs, { channel: sessionId });
554
+ const lines = [...(snapshot.value ?? []), ...(data.value ? [data.value] : [])];
555
+ return <pre>{lines.join("\n")}{closed.value && "\n[done]"}</pre>;
556
+ };
557
+ ```
558
+
559
+ ```typescript
560
+ // app/admin/provision.service.ts
561
+ import { stream_logs } from "./Provision.js";
562
+
563
+ export async function provision(sessionId: string) {
564
+ try {
565
+ stream_logs.emit(sessionId, "Connecting...", { snapshot: true });
566
+ // ... real work
567
+ stream_logs.emit(sessionId, "Worker ready.", { snapshot: true });
568
+ } finally {
569
+ stream_logs.close(sessionId);
570
+ }
571
+ }
572
+ ```
573
+
574
+ One line to declare, three verbs to use. Route at `/_event/admin/Provision/logs` is registered automatically. Middlewares inherited from parent route nodes.
575
+
576
+ ### Two ways to declare
577
+
578
+ **Convention `stream_*`** (recommended) — for streams logically tied to a page/layout. Path derived from module ID, middlewares inherited.
579
+
580
+ ```typescript
581
+ export const stream_progress = createEventStream<DeployState>();
582
+ ```
583
+
584
+ **Standalone** — for cross-cutting streams (global metrics, notifications). Pass `path` and (if needed) `middlewares` explicitly.
585
+
586
+ ```typescript
587
+ export const containerMetrics = createEventStream<Metric[]>({
588
+ path: "/api/metrics/containers",
589
+ middlewares: [authMiddleware],
590
+ });
591
+ ```
592
+
593
+ ### Three ways to emit
594
+
595
+ **Reactive** — call `emit()` from anywhere when something happens. Most common:
596
+
597
+ ```typescript
598
+ stream_logs.emit(sessionId, "Line", { snapshot: true });
599
+ ```
600
+
601
+ **Source-driven** — pass a `source` function. Framework runs it only while subscribed (zero CPU when nobody is watching):
602
+
603
+ ```typescript
604
+ import { poll } from "@mauroandre/velojs";
605
+
606
+ export const stream_metrics = createEventStream<Metric[]>({
607
+ source: poll({
608
+ intervalMs: 3000,
609
+ tick: async (emit) => emit(await collectMetrics()),
610
+ }),
611
+ });
612
+ ```
613
+
614
+ **Stateful snapshot** — for state-machine patterns where each emit is the complete current state:
615
+
616
+ ```typescript
617
+ const deploys = new Map<string, DeployState>();
618
+
619
+ export const stream_deploy = createEventStream<DeployState>({
620
+ snapshot: (id) => deploys.get(id ?? ""),
621
+ });
622
+ ```
623
+
624
+ ### Configuration
625
+
626
+ | Option | Type | Description |
627
+ |--------|------|-------------|
628
+ | `path` | `string` | (Standalone) Explicit URL path |
629
+ | `channel` | `(c) => string` | Extract channel ID. Default: `?channel=...` query param |
630
+ | `snapshot` | `(channel) => TSnapshot` | Returns current state on connect (state-machine pattern) |
631
+ | `closeOn` | `(event) => boolean` | Closes SSE when matching event is sent (declarative) |
632
+ | `source` | `(emit, { abortSignal }) => Promise<void>` | Self-running producer, only runs while subscribed |
633
+ | `retainMs` | `number` | Buffer retention after `close()`. Default `300000` (5 min) |
634
+ | `heartbeatMs` | `number \| false` | Heartbeat interval. Default `20000` (20s). `false` to disable |
635
+ | `middlewares` | `MiddlewareHandler[]` | (Standalone) Hono middlewares for the SSE route |
636
+
637
+ ### Snapshot mechanisms
638
+
639
+ Two snapshot patterns for two cases:
640
+
641
+ **Per-emit `{ snapshot: true }` — append pattern.** Framework keeps a buffer per channel. Late subscribers receive the array.
642
+
643
+ ```typescript
644
+ stream_logs.emit(id, "line 1", { snapshot: true });
645
+ stream_logs.emit(id, "line 2", { snapshot: true });
646
+ // Late subscriber → snapshot.value = ["line 1", "line 2"]
647
+ ```
648
+
649
+ **`snapshot: callback` config — replace pattern.** You return the latest state from your own data structure.
650
+
651
+ ```typescript
652
+ createEventStream({ snapshot: (id) => deploys.get(id ?? "") });
653
+ // Late subscriber → snapshot.value = the latest DeployState
654
+ ```
655
+
656
+ Both survive after `close()` for `retainMs` (default 5 min) so refresh-after-finish still shows final state.
657
+
658
+ ### Closing
659
+
660
+ Two ways, choose either or both:
661
+
662
+ ```typescript
663
+ // Imperative — call from anywhere
664
+ stream.close(channelId);
665
+
666
+ // Declarative — predicate on event
667
+ createEventStream({ closeOn: (s) => s.status === "success" });
668
+ ```
669
+
670
+ After close, subsequent `emit()` to that channel is ignored with a warning.
671
+
672
+ ### `useEventStream` hook
673
+
674
+ ```typescript
675
+ const { data, snapshot, closed, error } = useEventStream(stream, {
676
+ channel: "deploy-123", // optional
677
+ enabled: true, // optional
678
+ });
679
+ ```
680
+
681
+ | Signal | Description |
682
+ |--------|-------------|
683
+ | `data` | Latest event received |
684
+ | `snapshot` | Initial state on connect (buffer or callback) |
685
+ | `closed` | `true` when server closed the stream |
686
+ | `error` | Parse or connection error, if any |
687
+
688
+ Lifecycle is automatic: opens on mount, closes on unmount, re-opens fresh when `channel` changes.
689
+
690
+ ### `poll` helper
691
+
692
+ For interval-based polling sources. Wraps your tick function in a loop that respects the `AbortSignal`. Errors in tick are logged but don't stop the loop.
693
+
694
+ ```typescript
695
+ poll({ intervalMs: 3000, tick: async (emit) => emit(await collect()) })
696
+ ```
697
+
504
698
  ---
505
699
 
506
700
  ## Hooks
@@ -511,6 +705,7 @@ All hooks work in both SSR and client (via AsyncLocalStorage on server, wouter/D
511
705
  |------|-------------|
512
706
  | `useLoader<T>(deps?)` | Loader data with SSR + SPA support. Returns `{ data, loading, refetch }` |
513
707
  | `Loader<T>()` | Module-level SSR-only loader. Returns `{ data, loading }` |
708
+ | `useEventStream<T, S>(stream, opts?)` | Subscribe to a server-sent event stream. Returns `{ data, snapshot, closed, error }` |
514
709
  | `useParams<T>()` | Route parameters (e.g., `:id`) |
515
710
  | `useQuery<T>()` | Query string parameters |
516
711
  | `useNavigate()` | Programmatic navigation. Returns `navigate(path)` function |
@@ -701,7 +896,25 @@ export default [
701
896
 
702
897
  ### Inheritance
703
898
 
704
- Middlewares accumulate from parent to child. In the example above, `/master/workers` runs `authMiddleware` first, then `masterMiddleware`. This applies to both page loads (loaders) and action calls.
899
+ Middlewares accumulate from parent to child and apply to **every nested route**, including:
900
+
901
+ - **Page routes** (GET) — when loading a page
902
+ - **Action routes** (POST `/_action/...`) — when calling a server action
903
+ - **Data fetches** (GET with `?_data=1`) — during SPA navigation
904
+
905
+ In the example above, `/master/workers` runs `authMiddleware` first, then `masterMiddleware`. The same applies when calling `action_*` functions from any page under `MasterLayout` — both middlewares run before the action executes.
906
+
907
+ ```typescript
908
+ // This action, defined in app/master/Workers.tsx, is registered as
909
+ // POST /_action/master/Workers/delete
910
+ // with authMiddleware + masterMiddleware applied.
911
+ export const action_delete = async ({ body, c }: ActionArgs<{ id: string }>) => {
912
+ const user = c!.get("user"); // set by authMiddleware
913
+ // ...
914
+ };
915
+ ```
916
+
917
+ A middleware on a Layout guards everything underneath it — pages, loaders, and actions — with no extra wiring.
705
918
 
706
919
  ### Accessing middleware data in loaders and actions
707
920
 
@@ -927,10 +1140,11 @@ Hooks (`useParams`, `useQuery`, `usePathname`, `Loader`, `useLoader`) access thi
927
1140
 
928
1141
  | Import | Contents |
929
1142
  |--------|----------|
930
- | `@mauroandre/velojs` | Types (`AppRoutes`, `ActionArgs`, `LoaderArgs`, `Metadata`), `Scripts`, `Link`, `defineConfig` |
1143
+ | `@mauroandre/velojs` | Types (`AppRoutes`, `ActionArgs`, `LoaderArgs`, `Metadata`, `EventStream`, `EventStreamConfig`, `EmitFn`, `EmitOptions`, `SourceFn`), `Scripts`, `Link`, `createEventStream`, `poll`, `defineConfig` |
931
1144
  | `@mauroandre/velojs/server` | `startServer`, `createApp`, `addRoutes`, `onServer`, `serverDataStorage` |
932
1145
  | `@mauroandre/velojs/client` | `startClient` |
933
- | `@mauroandre/velojs/hooks` | `Loader`, `useLoader`, `useParams`, `useQuery`, `useNavigate`, `usePathname`, `touch` |
1146
+ | `@mauroandre/velojs/hooks` | `Loader`, `useLoader`, `useEventStream`, `useParams`, `useQuery`, `useNavigate`, `usePathname`, `touch` |
1147
+ | `@mauroandre/velojs/events` | `createEventStream`, `poll`, `EventStream`, `EventStreamConfig`, `EmitFn`, `EmitOptions`, `SourceFn` (also re-exported from root) |
934
1148
  | `@mauroandre/velojs/cookie` | `getCookie`, `setCookie`, `deleteCookie`, `getSignedCookie`, `setSignedCookie` |
935
1149
  | `@mauroandre/velojs/factory` | `createMiddleware`, `createFactory` |
936
1150
  | `@mauroandre/velojs/vite` | `veloPlugin` |
@@ -0,0 +1,184 @@
1
+ /**
2
+ * VeloJS Event Streams
3
+ * Server-Sent Events (SSE) with snapshot, channels, source-driven emission, and auto-close.
4
+ *
5
+ * Two ways to declare:
6
+ *
7
+ * 1. Convention `stream_*` (per-page or per-layout, route registered automatically):
8
+ * ```typescript
9
+ * // app/admin/Deploy.tsx
10
+ * export const stream_progress = createEventStream<DeployState>();
11
+ * ```
12
+ *
13
+ * 2. Standalone (cross-cutting streams like global metrics):
14
+ * ```typescript
15
+ * export const metricsStream = createEventStream<Metric[]>({
16
+ * path: "/api/metrics",
17
+ * middlewares: [authMiddleware],
18
+ * });
19
+ * ```
20
+ *
21
+ * Three ways to emit (server-side):
22
+ *
23
+ * - Reactive (call from anywhere when something happens):
24
+ * `stream.emit(channelId, value, { snapshot: true })`
25
+ * - Source-driven (framework runs your producer only when subscribed):
26
+ * `createEventStream({ source: poll({ intervalMs, tick }) })`
27
+ * - Stateful snapshot via callback (for state-machine patterns):
28
+ * `createEventStream({ snapshot: (channelId) => activeStates.get(channelId) })`
29
+ */
30
+ import type { Context, Hono, MiddlewareHandler } from "hono";
31
+ /** Function passed to `source` for emitting values. */
32
+ export type EmitFn<TEvent> = {
33
+ /** Broadcast emit (no channel) */
34
+ (event: TEvent, options?: EmitOptions): void;
35
+ /** Channel-targeted emit */
36
+ (channel: string, event: TEvent, options?: EmitOptions): void;
37
+ };
38
+ /** Options accepted by `emit()` per call. */
39
+ export interface EmitOptions {
40
+ /**
41
+ * If true, the value is appended to the stream's internal buffer for that channel.
42
+ * Late subscribers receive the full buffer as a snapshot on connect.
43
+ * Default: false (event is broadcast-only, not retained).
44
+ */
45
+ snapshot?: boolean;
46
+ }
47
+ /**
48
+ * A producer function that runs **only when there are active subscribers**.
49
+ * The framework passes an `AbortSignal` that fires when the last subscriber disconnects.
50
+ *
51
+ * Example (event-driven):
52
+ * ```typescript
53
+ * source: async (emit, { abortSignal }) => {
54
+ * const handler = (e) => emit(e);
55
+ * bus.on("event", handler);
56
+ * abortSignal.addEventListener("abort", () => bus.off("event", handler));
57
+ * await new Promise(r => abortSignal.addEventListener("abort", r));
58
+ * }
59
+ * ```
60
+ *
61
+ * For polling, use the `poll` helper instead of writing the loop yourself.
62
+ *
63
+ * **Anti-pattern**: ignoring `abortSignal` keeps the source running forever (and leaks
64
+ * resources). Only do this if the producer is meant to run independently of UI.
65
+ */
66
+ export type SourceFn<TEvent> = (emit: EmitFn<TEvent>, ctx: {
67
+ abortSignal: AbortSignal;
68
+ }) => void | Promise<void>;
69
+ export interface EventStreamConfig<TEvent, TSnapshot = TEvent> {
70
+ /**
71
+ * Explicit URL path for the SSE endpoint (standalone usage).
72
+ * When omitted, the path is derived from the module ID at build time
73
+ * (convention `stream_*`).
74
+ */
75
+ path?: string;
76
+ /**
77
+ * Channel selector — extracts a channel ID from the request.
78
+ * Subscribers only receive events emitted to their channel.
79
+ * When omitted, defaults to `c.req.query("channel") ?? ""` so client-side
80
+ * `useEventStream(stream, { channel })` works out of the box.
81
+ */
82
+ channel?: (c: Context) => string;
83
+ /**
84
+ * Returns the current state of a channel for snapshot-on-connect.
85
+ * Sent before any new events to bring the client up to date.
86
+ *
87
+ * Use this for **state-machine patterns** where each emit is a complete state
88
+ * (deploy progress, migration status). The callback returns the latest state.
89
+ *
90
+ * For **append-only patterns** (logs, tokens), prefer `emit(..., { snapshot: true })`
91
+ * which uses an internal buffer managed by the framework.
92
+ */
93
+ snapshot?: (channel: string | undefined) => TSnapshot | undefined | Promise<TSnapshot | undefined>;
94
+ /**
95
+ * Returns true when an event marks the end of the stream for that channel.
96
+ * The server closes the SSE connection after sending such an event.
97
+ *
98
+ * For imperative close from anywhere, use `stream.close(channel)` instead.
99
+ */
100
+ closeOn?: (event: TEvent) => boolean;
101
+ /**
102
+ * Heartbeat interval in milliseconds. Sends `:heartbeat\n\n` to keep
103
+ * proxies (Cloudflare, Nginx, ALB) from closing idle connections.
104
+ * Default: 20000 (20s). Set to `false` to disable.
105
+ */
106
+ heartbeatMs?: number | false;
107
+ /**
108
+ * Middlewares to apply to the SSE route (standalone streams only).
109
+ * For `stream_*` convention streams, middlewares are inherited from
110
+ * parent route nodes automatically.
111
+ */
112
+ middlewares?: MiddlewareHandler[];
113
+ /**
114
+ * Self-running producer that emits values into the stream.
115
+ * The framework only invokes `source` while there are active subscribers.
116
+ * When the last subscriber disconnects, the AbortSignal fires.
117
+ *
118
+ * Use the `poll` helper for the common interval-based pattern.
119
+ */
120
+ source?: SourceFn<TEvent>;
121
+ /**
122
+ * Time (ms) the per-channel snapshot buffer is retained after `close()`.
123
+ * Reconnecting clients within this window still see the final state.
124
+ * Default: 5 * 60 * 1000 (5 minutes).
125
+ */
126
+ retainMs?: number;
127
+ }
128
+ export interface EventStream<TEvent, TSnapshot = TEvent> {
129
+ /** Broadcast emit (no channel) */
130
+ emit(event: TEvent, options?: EmitOptions): void;
131
+ /** Channel-targeted emit */
132
+ emit(channel: string, event: TEvent, options?: EmitOptions): void;
133
+ /**
134
+ * Closes the stream (or a specific channel).
135
+ * - Notifies current subscribers so their `useEventStream().closed` becomes true.
136
+ * - Schedules cleanup of the per-channel buffer after `retainMs`.
137
+ * - Idempotent: calling close on an already-closed channel is a no-op.
138
+ */
139
+ close(channel?: string): void;
140
+ /** Internal: marker for runtime detection */
141
+ readonly __isVeloEventStream: true;
142
+ /** Internal: configuration */
143
+ readonly __config: EventStreamConfig<TEvent, TSnapshot>;
144
+ /** Internal: registered listeners by channel ("" for broadcast) */
145
+ readonly __listeners: Map<string, Set<StreamListener<TEvent>>>;
146
+ /** Internal: per-channel buffer for { snapshot: true } emits */
147
+ readonly __buffers: Map<string, TEvent[]>;
148
+ /** Internal: assigned URL path (set by standalone or by stream_* discovery) */
149
+ __path: string | undefined;
150
+ /** Internal: source lifecycle hook */
151
+ __onConnect(channelKey: string, listener: StreamListener<TEvent>): void;
152
+ /** Internal: source lifecycle hook */
153
+ __onDisconnect(channelKey: string, listener: StreamListener<TEvent>): void;
154
+ }
155
+ /** Internal listener — receives events and a close signal. */
156
+ interface StreamListener<TEvent> {
157
+ (event: TEvent): void;
158
+ close(): void;
159
+ }
160
+ export declare function flushPendingStreamRoutes(app: Hono): void;
161
+ export declare function createEventStream<TEvent, TSnapshot = TEvent>(config?: EventStreamConfig<TEvent, TSnapshot>): EventStream<TEvent, TSnapshot>;
162
+ /**
163
+ * Creates a `source` function that runs `tick` every `intervalMs` while subscribed.
164
+ * The tick function is wrapped in try/catch — errors are logged but don't stop the loop.
165
+ *
166
+ * ```typescript
167
+ * export const stream_metrics = createEventStream<Metric[]>({
168
+ * source: poll({
169
+ * intervalMs: 3000,
170
+ * tick: async (emit) => emit(await collectMetrics()),
171
+ * }),
172
+ * });
173
+ * ```
174
+ */
175
+ export declare function poll<TEvent>(opts: {
176
+ intervalMs: number;
177
+ tick: (emit: EmitFn<TEvent>) => void | Promise<void>;
178
+ }): SourceFn<TEvent>;
179
+ /**
180
+ * Registers a GET SSE route for the given stream at the given path.
181
+ * Used both by standalone streams and by stream_* discovery.
182
+ */
183
+ export declare function registerStreamHandler<TEvent, TSnapshot>(app: Hono, path: string, stream: EventStream<TEvent, TSnapshot>, middlewares?: Array<(c: Context, next: () => Promise<void>) => any>): void;
184
+ export {};
package/dist/events.js ADDED
@@ -0,0 +1,137 @@
1
+ const p = [];
2
+ function m(t) {
3
+ for (const r of p) r(t);
4
+ p.length = 0;
5
+ }
6
+ function C(t = {}) {
7
+ const r = /* @__PURE__ */ new Map(), n = /* @__PURE__ */ new Map(), a = /* @__PURE__ */ new Set(), _ = /* @__PURE__ */ new Map();
8
+ let u = null, S = 0;
9
+ const b = () => {
10
+ t.source && (u || (u = new AbortController(), Promise.resolve(
11
+ t.source(A, { abortSignal: u.signal })
12
+ ).catch((e) => {
13
+ console.error("[velojs] event stream source threw:", e);
14
+ })));
15
+ }, h = () => {
16
+ S > 0 || u && (u.abort(), u = null);
17
+ };
18
+ function A(e, i, s) {
19
+ let o, l, d;
20
+ if (!!t.channel ? (o = e, l = i, d = s) : (o = "", l = e, d = i), a.has(o)) {
21
+ console.warn(
22
+ `[velojs] emit() called on closed channel "${o}" — ignored. Reopen the channel by emitting to a fresh ID, or remove the close() call.`
23
+ );
24
+ return;
25
+ }
26
+ if (d?.snapshot) {
27
+ const T = n.get(o) ?? [];
28
+ T.push(l), n.set(o, T);
29
+ }
30
+ const f = r.get(o);
31
+ if (f)
32
+ for (const T of f) T(l);
33
+ }
34
+ function E(e = "") {
35
+ if (a.has(e)) return;
36
+ a.add(e);
37
+ const i = r.get(e);
38
+ if (i)
39
+ for (const l of i) l.close();
40
+ const s = t.retainMs ?? 3e5, o = _.get(e);
41
+ o && clearTimeout(o), _.set(
42
+ e,
43
+ setTimeout(() => {
44
+ n.delete(e), _.delete(e), a.delete(e);
45
+ }, s)
46
+ );
47
+ }
48
+ const c = {
49
+ emit: A,
50
+ close: E,
51
+ __isVeloEventStream: !0,
52
+ __config: t,
53
+ __listeners: r,
54
+ __buffers: n,
55
+ __path: t.path,
56
+ __onConnect(e, i) {
57
+ let s = r.get(e);
58
+ s || (s = /* @__PURE__ */ new Set(), r.set(e, s)), s.add(i), S++, b();
59
+ },
60
+ __onDisconnect(e, i) {
61
+ const s = r.get(e);
62
+ s && (s.delete(i), s.size === 0 && r.delete(e), S--, h());
63
+ }
64
+ };
65
+ return t.path && p.push((e) => {
66
+ v(e, t.path, c, t.middlewares ?? []);
67
+ }), c;
68
+ }
69
+ function N(t) {
70
+ return async (r, { abortSignal: n }) => {
71
+ for (; !n.aborted; ) {
72
+ try {
73
+ await t.tick(r);
74
+ } catch (a) {
75
+ console.error("[velojs] poll tick threw:", a);
76
+ }
77
+ await new Promise((a) => {
78
+ const _ = setTimeout(a, t.intervalMs);
79
+ n.addEventListener(
80
+ "abort",
81
+ () => {
82
+ clearTimeout(_), a();
83
+ },
84
+ { once: !0 }
85
+ );
86
+ });
87
+ }
88
+ };
89
+ }
90
+ function g(t) {
91
+ return t.req.query("channel") ?? "";
92
+ }
93
+ function v(t, r, n, a = []) {
94
+ const _ = async (u) => {
95
+ const { streamSSE: S } = await import("hono/streaming"), h = (n.__config.channel ?? g)(u), A = n.__config.heartbeatMs, E = A === !1 ? 0 : A ?? 2e4;
96
+ return S(u, async (c) => {
97
+ const e = n.__buffers.get(h);
98
+ if (e && e.length > 0 && await c.writeSSE({
99
+ event: "snapshot",
100
+ data: JSON.stringify(e)
101
+ }), n.__config.snapshot) {
102
+ const f = await n.__config.snapshot(
103
+ h === "" ? void 0 : h
104
+ );
105
+ f !== void 0 && await c.writeSSE({
106
+ event: "snapshot",
107
+ data: JSON.stringify(f)
108
+ });
109
+ }
110
+ let i;
111
+ const s = new Promise((f) => {
112
+ i = f;
113
+ });
114
+ let o = !1, l = null;
115
+ const d = () => {
116
+ o || (o = !0, l && clearInterval(l), n.__onDisconnect(h, w), i());
117
+ }, w = ((f) => {
118
+ o || (c.writeSSE({ data: JSON.stringify(f) }).catch(() => {
119
+ }), n.__config.closeOn?.(f) && (c.close(), d()));
120
+ });
121
+ w.close = () => {
122
+ o || (c.writeSSE({ event: "close", data: "" }).catch(() => {
123
+ }), c.close(), d());
124
+ }, n.__onConnect(h, w), l = E > 0 ? setInterval(() => {
125
+ o || c.writeSSE({ event: "heartbeat", data: "" }).catch(() => {
126
+ });
127
+ }, E) : null, c.onAbort(d), await s;
128
+ });
129
+ };
130
+ a.length > 0 ? t.on(["GET"], [r], ...a, _) : t.on(["GET"], [r], _);
131
+ }
132
+ export {
133
+ C as createEventStream,
134
+ m as flushPendingStreamRoutes,
135
+ N as poll,
136
+ v as registerStreamHandler
137
+ };
package/dist/hooks.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type Signal } from "@preact/signals";
2
+ import type { EventStream } from "./events.js";
2
3
  export declare const __veloUpdatePending: Signal<boolean>;
3
4
  /**
4
5
  * Força o signal a notificar mudanças após mutação de propriedades aninhadas
@@ -118,3 +119,33 @@ export declare function useQuery<T extends Record<string, string> = Record<strin
118
119
  * ```
119
120
  */
120
121
  export declare function usePathname(): string;
122
+ /**
123
+ * Subscribes to a server event stream (SSE) created with createEventStream.
124
+ *
125
+ * Returns:
126
+ * - `data`: Signal with the latest event received (null until first event)
127
+ * - `snapshot`: Signal with the initial snapshot if the server provided one
128
+ * - `closed`: Signal that becomes true when the stream is closed by the server
129
+ * - `error`: Signal with the connection error, if any
130
+ *
131
+ * Usage:
132
+ * ```tsx
133
+ * // Per-page stream (stream_* convention)
134
+ * const { data, snapshot } = useEventStream(stream_progress, { channel: deployId });
135
+ *
136
+ * // Standalone stream (no channel)
137
+ * const { data } = useEventStream(metricsStream);
138
+ * ```
139
+ *
140
+ * The connection closes automatically when the component unmounts or when
141
+ * the channel changes (re-opens with new channel).
142
+ */
143
+ export declare function useEventStream<TEvent, TSnapshot = TEvent>(stream: EventStream<TEvent, TSnapshot>, options?: {
144
+ channel?: string;
145
+ enabled?: boolean;
146
+ }): {
147
+ data: Signal<TEvent | null>;
148
+ snapshot: Signal<TSnapshot | null>;
149
+ closed: Signal<boolean>;
150
+ error: Signal<Error | null>;
151
+ };