@mauroandre/velojs 0.0.19 → 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
@@ -536,57 +536,89 @@ The client-side transform rewrites the import to fetch `/_action/admin/Layout/lo
536
536
 
537
537
  ## Event Streams
538
538
 
539
- Push real-time updates from server to client via Server-Sent Events (SSE). Useful for live progress, notifications, metrics, log streaming, AI tokens, and any server → client push scenario.
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
540
 
541
- ### `stream_*` convention
541
+ Three verbs: `emit`, `close`, `useEventStream`. The framework handles routing, types, listener management, snapshots, lifecycle, reconnection, and cleanup.
542
542
 
543
- Declare a stream alongside `loader` / `action_*` in any page or layout module. The framework registers an SSE route at `/_event/{moduleId}/{name}` automatically:
543
+ ### Shortest example
544
544
 
545
545
  ```typescript
546
- // app/admin/Deploy.tsx
546
+ // app/admin/Provision.tsx
547
547
  import { createEventStream } from "@mauroandre/velojs";
548
- import { useEventStream, useParams } from "@mauroandre/velojs/hooks";
548
+ import { useEventStream } from "@mauroandre/velojs/hooks";
549
549
 
550
- type DeployState = { step: string; status: "running" | "success" | "error" };
551
-
552
- const activeDeploys = new Map<string, DeployState>();
553
-
554
- export const stream_progress = createEventStream<DeployState>({
555
- channel: (c) => c.req.query("channel") ?? "",
556
- snapshot: (channel) => activeDeploys.get(channel ?? ""),
557
- closeOn: (s) => s.status === "success" || s.status === "error",
558
- });
550
+ export const stream_logs = createEventStream<string>();
559
551
 
560
552
  export const Component = () => {
561
- const params = useParams<{ id: string }>();
562
- const { data, snapshot, closed } = useEventStream(stream_progress, {
563
- channel: params.id,
564
- });
565
-
566
- const state = data.value ?? snapshot.value;
567
- return <div>{state?.step ?? "waiting..."}</div>;
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>;
568
556
  };
569
557
  ```
570
558
 
571
- Emit from any service that imports the stream:
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.
572
579
 
573
580
  ```typescript
574
- import { stream_progress } from "./Deploy.js";
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.
575
585
 
576
- stream_progress.emit("app-123", { step: "building", status: "running" });
586
+ ```typescript
587
+ export const containerMetrics = createEventStream<Metric[]>({
588
+ path: "/api/metrics/containers",
589
+ middlewares: [authMiddleware],
590
+ });
577
591
  ```
578
592
 
579
- ### Standalone streams
593
+ ### Three ways to emit
580
594
 
581
- For cross-cutting streams (global metrics, notifications) consumed by many pages, use an explicit `path`:
595
+ **Reactive** — call `emit()` from anywhere when something happens. Most common:
582
596
 
583
597
  ```typescript
584
- // app/streams/metrics.ts
585
- export const metricsStream = createEventStream<Metric[]>({
586
- path: "/api/metrics/containers",
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
+ }),
587
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>();
588
618
 
589
- setInterval(() => metricsStream.emit(collectMetrics()), 2000);
619
+ export const stream_deploy = createEventStream<DeployState>({
620
+ snapshot: (id) => deploys.get(id ?? ""),
621
+ });
590
622
  ```
591
623
 
592
624
  ### Configuration
@@ -594,25 +626,49 @@ setInterval(() => metricsStream.emit(collectMetrics()), 2000);
594
626
  | Option | Type | Description |
595
627
  |--------|------|-------------|
596
628
  | `path` | `string` | (Standalone) Explicit URL path |
597
- | `channel` | `(c: Context) => string` | Extract channel ID from request — subscribers only get matching events |
598
- | `snapshot` | `(channel) => TSnapshot \| undefined` | Returns current state on connect (and reconnect) |
599
- | `closeOn` | `(event: TEvent) => boolean` | Closes the SSE connection when matching event is sent |
600
- | `heartbeatMs` | `number \| false` | Keep-alive heartbeat interval, default `20000` (20s). `false` to disable |
601
- | `middlewares` | `MiddlewareHandler[]` | (Standalone) Hono middlewares applied to the SSE route |
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 |
602
636
 
603
- ### Middleware inheritance
637
+ ### Snapshot mechanisms
604
638
 
605
- Streams declared with `stream_*` automatically inherit middlewares from parent route nodes — same as pages and actions. A stream under an `AdminLayout` with `authMiddleware` is protected without extra wiring.
639
+ Two snapshot patterns for two cases:
606
640
 
607
- For standalone streams, pass middlewares explicitly:
641
+ **Per-emit `{ snapshot: true }` — append pattern.** Framework keeps a buffer per channel. Late subscribers receive the array.
608
642
 
609
643
  ```typescript
610
- export const metricsStream = createEventStream<Metric[]>({
611
- path: "/api/metrics",
612
- middlewares: [authMiddleware],
613
- });
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" });
614
668
  ```
615
669
 
670
+ After close, subsequent `emit()` to that channel is ignored with a warning.
671
+
616
672
  ### `useEventStream` hook
617
673
 
618
674
  ```typescript
@@ -624,12 +680,20 @@ const { data, snapshot, closed, error } = useEventStream(stream, {
624
680
 
625
681
  | Signal | Description |
626
682
  |--------|-------------|
627
- | `data` | Latest event received from the server |
628
- | `snapshot` | Initial state on connect (if `snapshot` is configured) |
629
- | `closed` | `true` when server closed the stream (`closeOn` matched) |
683
+ | `data` | Latest event received |
684
+ | `snapshot` | Initial state on connect (buffer or callback) |
685
+ | `closed` | `true` when server closed the stream |
630
686
  | `error` | Parse or connection error, if any |
631
687
 
632
- The hook handles `EventSource` lifecycle automatically: opens on mount, closes on unmount, re-opens when `channel` changes.
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
+ ```
633
697
 
634
698
  ---
635
699
 
@@ -1076,11 +1140,11 @@ Hooks (`useParams`, `useQuery`, `usePathname`, `Loader`, `useLoader`) access thi
1076
1140
 
1077
1141
  | Import | Contents |
1078
1142
  |--------|----------|
1079
- | `@mauroandre/velojs` | Types (`AppRoutes`, `ActionArgs`, `LoaderArgs`, `Metadata`, `EventStream`, `EventStreamConfig`), `Scripts`, `Link`, `createEventStream`, `defineConfig` |
1143
+ | `@mauroandre/velojs` | Types (`AppRoutes`, `ActionArgs`, `LoaderArgs`, `Metadata`, `EventStream`, `EventStreamConfig`, `EmitFn`, `EmitOptions`, `SourceFn`), `Scripts`, `Link`, `createEventStream`, `poll`, `defineConfig` |
1080
1144
  | `@mauroandre/velojs/server` | `startServer`, `createApp`, `addRoutes`, `onServer`, `serverDataStorage` |
1081
1145
  | `@mauroandre/velojs/client` | `startClient` |
1082
1146
  | `@mauroandre/velojs/hooks` | `Loader`, `useLoader`, `useEventStream`, `useParams`, `useQuery`, `useNavigate`, `usePathname`, `touch` |
1083
- | `@mauroandre/velojs/events` | `createEventStream`, `EventStream`, `EventStreamConfig` (also re-exported from root) |
1147
+ | `@mauroandre/velojs/events` | `createEventStream`, `poll`, `EventStream`, `EventStreamConfig`, `EmitFn`, `EmitOptions`, `SourceFn` (also re-exported from root) |
1084
1148
  | `@mauroandre/velojs/cookie` | `getCookie`, `setCookie`, `deleteCookie`, `getSignedCookie`, `setSignedCookie` |
1085
1149
  | `@mauroandre/velojs/factory` | `createMiddleware`, `createFactory` |
1086
1150
  | `@mauroandre/velojs/vite` | `veloPlugin` |
package/dist/events.d.ts CHANGED
@@ -1,28 +1,71 @@
1
1
  /**
2
2
  * VeloJS Event Streams
3
- * Server-Sent Events (SSE) with snapshot, channels, and auto-close.
3
+ * Server-Sent Events (SSE) with snapshot, channels, source-driven emission, and auto-close.
4
4
  *
5
- * Two ways to use:
5
+ * Two ways to declare:
6
6
  *
7
- * 1. Standalone (cross-cutting streams like global metrics):
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):
8
14
  * ```typescript
9
15
  * export const metricsStream = createEventStream<Metric[]>({
10
16
  * path: "/api/metrics",
17
+ * middlewares: [authMiddleware],
11
18
  * });
12
- * metricsStream.emit(currentMetrics);
13
19
  * ```
14
20
  *
15
- * 2. Convention `stream_*` (per-page streams, route registered automatically):
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):
16
52
  * ```typescript
17
- * // app/admin/Deploy.tsx
18
- * export const stream_progress = createEventStream<DeployState>({
19
- * channel: (c) => c.req.param("id"),
20
- * snapshot: (id) => activeDeploys.get(id),
21
- * closeOn: (s) => s.status === "success" || s.status === "error",
22
- * });
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
+ * }
23
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.
24
65
  */
25
- import type { Context, Hono, MiddlewareHandler } from "hono";
66
+ export type SourceFn<TEvent> = (emit: EmitFn<TEvent>, ctx: {
67
+ abortSignal: AbortSignal;
68
+ }) => void | Promise<void>;
26
69
  export interface EventStreamConfig<TEvent, TSnapshot = TEvent> {
27
70
  /**
28
71
  * Explicit URL path for the SSE endpoint (standalone usage).
@@ -33,19 +76,26 @@ export interface EventStreamConfig<TEvent, TSnapshot = TEvent> {
33
76
  /**
34
77
  * Channel selector — extracts a channel ID from the request.
35
78
  * Subscribers only receive events emitted to their channel.
36
- * When omitted, the stream is a broadcast (all subscribers receive all events).
79
+ * When omitted, defaults to `c.req.query("channel") ?? ""` so client-side
80
+ * `useEventStream(stream, { channel })` works out of the box.
37
81
  */
38
82
  channel?: (c: Context) => string;
39
83
  /**
40
84
  * Returns the current state of a channel for snapshot-on-connect.
41
85
  * Sent before any new events to bring the client up to date.
42
- * Must remain available even after the stream "closes" — clients reconnecting
43
- * after a terminal event still need to see the final state.
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.
44
92
  */
45
93
  snapshot?: (channel: string | undefined) => TSnapshot | undefined | Promise<TSnapshot | undefined>;
46
94
  /**
47
95
  * Returns true when an event marks the end of the stream for that channel.
48
96
  * The server closes the SSE connection after sending such an event.
97
+ *
98
+ * For imperative close from anywhere, use `stream.close(channel)` instead.
49
99
  */
50
100
  closeOn?: (event: TEvent) => boolean;
51
101
  /**
@@ -60,32 +110,75 @@ export interface EventStreamConfig<TEvent, TSnapshot = TEvent> {
60
110
  * parent route nodes automatically.
61
111
  */
62
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;
63
127
  }
64
128
  export interface EventStream<TEvent, TSnapshot = TEvent> {
65
129
  /** Broadcast emit (no channel) */
66
- emit(event: TEvent): void;
130
+ emit(event: TEvent, options?: EmitOptions): void;
67
131
  /** Channel-targeted emit */
68
- emit(channel: string, event: TEvent): void;
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;
69
140
  /** Internal: marker for runtime detection */
70
141
  readonly __isVeloEventStream: true;
71
142
  /** Internal: configuration */
72
143
  readonly __config: EventStreamConfig<TEvent, TSnapshot>;
73
144
  /** Internal: registered listeners by channel ("" for broadcast) */
74
- readonly __listeners: Map<string, Set<(event: TEvent) => void>>;
145
+ readonly __listeners: Map<string, Set<StreamListener<TEvent>>>;
146
+ /** Internal: per-channel buffer for { snapshot: true } emits */
147
+ readonly __buffers: Map<string, TEvent[]>;
75
148
  /** Internal: assigned URL path (set by standalone or by stream_* discovery) */
76
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;
77
159
  }
78
- /**
79
- * Returns and clears the queue of pending standalone streams.
80
- * Called by createApp to register their routes.
81
- */
82
160
  export declare function flushPendingStreamRoutes(app: Hono): void;
161
+ export declare function createEventStream<TEvent, TSnapshot = TEvent>(config?: EventStreamConfig<TEvent, TSnapshot>): EventStream<TEvent, TSnapshot>;
83
162
  /**
84
- * Creates a typed event stream with optional channel, snapshot, and close-on-event.
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
+ * ```
85
174
  */
86
- export declare function createEventStream<TEvent, TSnapshot = TEvent>(config?: EventStreamConfig<TEvent, TSnapshot>): EventStream<TEvent, TSnapshot>;
175
+ export declare function poll<TEvent>(opts: {
176
+ intervalMs: number;
177
+ tick: (emit: EmitFn<TEvent>) => void | Promise<void>;
178
+ }): SourceFn<TEvent>;
87
179
  /**
88
180
  * Registers a GET SSE route for the given stream at the given path.
89
181
  * Used both by standalone streams and by stream_* discovery.
90
182
  */
91
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 CHANGED
@@ -1,62 +1,137 @@
1
- const A = [];
2
- function w(t) {
3
- for (const s of A) s(t);
4
- A.length = 0;
1
+ const p = [];
2
+ function m(t) {
3
+ for (const r of p) r(t);
4
+ p.length = 0;
5
5
  }
6
- function N(t = {}) {
7
- const s = /* @__PURE__ */ new Map();
8
- function e(a, f) {
9
- let l, n;
10
- arguments.length === 1 ? (l = "", n = a) : (l = a, n = f);
11
- const c = s.get(l);
12
- if (c)
13
- for (const h of c) h(n);
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
+ );
14
47
  }
15
- const r = {
16
- emit: e,
48
+ const c = {
49
+ emit: A,
50
+ close: E,
17
51
  __isVeloEventStream: !0,
18
52
  __config: t,
19
- __listeners: s,
20
- __path: t.path
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
+ }
21
64
  };
22
- return t.path && A.push((a) => {
23
- p(a, t.path, r, t.middlewares ?? []);
24
- }), r;
65
+ return t.path && p.push((e) => {
66
+ v(e, t.path, c, t.middlewares ?? []);
67
+ }), c;
25
68
  }
26
- function p(t, s, e, r = []) {
27
- const a = async (f) => {
28
- const { streamSSE: l } = await import("hono/streaming"), n = e.__config.channel ? e.__config.channel(f) : "", c = e.__config.heartbeatMs, h = c === !1 ? 0 : c ?? 2e4;
29
- return l(f, async (_) => {
30
- if (e.__config.snapshot) {
31
- const o = await e.__config.snapshot(
32
- n === "" ? void 0 : n
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 }
33
85
  );
34
- o !== void 0 && await _.writeSSE({
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({
35
106
  event: "snapshot",
36
- data: JSON.stringify(o)
107
+ data: JSON.stringify(f)
37
108
  });
38
109
  }
39
- let E;
40
- const T = new Promise((o) => {
41
- E = o;
110
+ let i;
111
+ const s = new Promise((f) => {
112
+ i = f;
42
113
  });
43
- let S = !1, u = null, i;
114
+ let o = !1, l = null;
44
115
  const d = () => {
45
- S || (S = !0, u && clearInterval(u), i?.delete(g), i?.size === 0 && e.__listeners.delete(n), E());
46
- }, g = (o) => {
47
- S || (_.writeSSE({ data: JSON.stringify(o) }).catch(() => {
48
- }), e.__config.closeOn?.(o) && (_.close(), d()));
49
- };
50
- i = e.__listeners.get(n), i || (i = /* @__PURE__ */ new Set(), e.__listeners.set(n, i)), i.add(g), u = h > 0 ? setInterval(() => {
51
- S || _.writeSSE({ event: "heartbeat", data: "" }).catch(() => {
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(() => {
52
126
  });
53
- }, h) : null, _.onAbort(d), await T;
127
+ }, E) : null, c.onAbort(d), await s;
54
128
  });
55
129
  };
56
- r.length > 0 ? t.on(["GET"], [s], ...r, a) : t.on(["GET"], [s], a);
130
+ a.length > 0 ? t.on(["GET"], [r], ...a, _) : t.on(["GET"], [r], _);
57
131
  }
58
132
  export {
59
- N as createEventStream,
60
- w as flushPendingStreamRoutes,
61
- p as registerStreamHandler
133
+ C as createEventStream,
134
+ m as flushPendingStreamRoutes,
135
+ N as poll,
136
+ v as registerStreamHandler
62
137
  };
package/dist/hooks.js CHANGED
@@ -13,42 +13,42 @@ function m(e) {
13
13
  return e ? globalThis.__veloServerData?.getStore?.()?.[e] ?? null : null;
14
14
  }
15
15
  }, loading: o };
16
- const t = window.__PAGE_DATA__, n = e && t?.[e] ? t[e] : null;
17
- return { data: v(n), loading: o };
16
+ const t = window.__PAGE_DATA__, r = e && t?.[e] ? t[e] : null;
17
+ return { data: v(r), loading: o };
18
18
  }
19
19
  function A(e, o) {
20
- let t, n;
21
- Array.isArray(e) ? n = e : (t = e, n = o);
22
- let r = null;
20
+ let t, r;
21
+ Array.isArray(e) ? r = e : (t = e, r = o);
22
+ let n = null;
23
23
  if (typeof window > "u" && t) {
24
- const l = globalThis.__veloServerData?.getStore?.();
25
- l?.[t] && (r = l[t]);
24
+ const i = globalThis.__veloServerData?.getStore?.();
25
+ i?.[t] && (n = i[t]);
26
26
  }
27
27
  if (typeof window < "u" && t) {
28
28
  const a = window.__PAGE_DATA__;
29
- a?.[t] && (r = a[t], delete a[t]);
29
+ a?.[t] && (n = a[t], delete a[t]);
30
30
  }
31
- const u = c(r), s = c(!1), _ = () => {
31
+ const u = c(n), s = c(!1), _ = () => {
32
32
  if (typeof window > "u" || !t) return;
33
33
  const a = window.location.pathname;
34
34
  s.value = !0;
35
35
  const f = typeof __VELO_STATIC__ < "u" && __VELO_STATIC__ ? `${a === "/" ? "" : a}/index.json` : (() => {
36
- const i = new URLSearchParams(window.location.search);
37
- return i.set("_data", "1"), `${a}?${i.toString()}`;
36
+ const l = new URLSearchParams(window.location.search);
37
+ return l.set("_data", "1"), `${a}?${l.toString()}`;
38
38
  })();
39
- fetch(f, { cache: "no-cache" }).then((i) => i.json()).then((i) => {
40
- typeof __VELO_BUILD_HASH__ < "u" && i.__buildHash && i.__buildHash !== __VELO_BUILD_HASH__ && (S.value = !0), u.value = i[t], s.value = !1;
39
+ fetch(f, { cache: "no-cache" }).then((l) => l.json()).then((l) => {
40
+ typeof __VELO_BUILD_HASH__ < "u" && l.__buildHash && l.__buildHash !== __VELO_BUILD_HASH__ && (S.value = !0), u.value = l[t], s.value = !1;
41
41
  }).catch(() => {
42
42
  s.value = !1;
43
43
  });
44
- }, d = p(r !== null);
44
+ }, d = p(n !== null);
45
45
  return h(() => {
46
46
  if (d.current) {
47
47
  d.current = !1;
48
48
  return;
49
49
  }
50
50
  _();
51
- }, n ?? []), { data: u, loading: s, refetch: _ };
51
+ }, r ?? []), { data: u, loading: s, refetch: _ };
52
52
  }
53
53
  function P() {
54
54
  const [, e] = w();
@@ -64,42 +64,44 @@ function b() {
64
64
  if (e?.__query)
65
65
  return e.__query;
66
66
  const o = new URLSearchParams(window.location.search), t = {};
67
- return o.forEach((n, r) => {
68
- t[r] = n;
67
+ return o.forEach((r, n) => {
68
+ t[n] = r;
69
69
  }), t;
70
70
  }
71
71
  function H() {
72
72
  return typeof window > "u" ? globalThis.__veloServerData?.getStore?.()?.__pathname ?? "/" : (w(), window.location.pathname);
73
73
  }
74
74
  function U(e, o = {}) {
75
- const t = c(null), n = c(null), r = c(!1), u = c(null), { channel: s, enabled: _ = !0 } = o;
75
+ const t = c(null), r = c(null), n = c(!1), u = c(null), { channel: s, enabled: _ = !0 } = o;
76
76
  return h(() => {
77
77
  if (typeof window > "u" || !_) return;
78
78
  if (!e.__path) {
79
79
  console.warn("[velojs] useEventStream: stream has no __path");
80
80
  return;
81
81
  }
82
- t.value = null, n.value = null, r.value = !1, u.value = null;
82
+ t.value = null, r.value = null, n.value = !1, u.value = null;
83
83
  const d = s ? `${e.__path}?channel=${encodeURIComponent(s)}` : e.__path, a = new EventSource(d);
84
- return a.addEventListener("snapshot", (l) => {
84
+ return a.addEventListener("snapshot", (i) => {
85
85
  try {
86
- n.value = JSON.parse(l.data);
86
+ r.value = JSON.parse(i.data);
87
87
  } catch (f) {
88
88
  u.value = f;
89
89
  }
90
- }), a.addEventListener("message", (l) => {
90
+ }), a.addEventListener("message", (i) => {
91
91
  try {
92
- t.value = JSON.parse(l.data);
92
+ t.value = JSON.parse(i.data);
93
93
  } catch (f) {
94
94
  u.value = f;
95
95
  }
96
96
  }), a.addEventListener("heartbeat", () => {
97
+ }), a.addEventListener("close", () => {
98
+ n.value = !0, a.close();
97
99
  }), a.addEventListener("error", () => {
98
- a.readyState === EventSource.CLOSED && (r.value = !0);
100
+ a.readyState === EventSource.CLOSED && (n.value = !0);
99
101
  }), () => {
100
102
  a.close();
101
103
  };
102
- }, [e, s, _]), { data: t, snapshot: n, closed: r, error: u };
104
+ }, [e, s, _]), { data: t, snapshot: r, closed: n, error: u };
103
105
  }
104
106
  export {
105
107
  m as Loader,
package/dist/index.d.ts CHANGED
@@ -2,6 +2,6 @@ export type { AppRoutes, RouteNode, RouteModule, LoaderArgs, ActionArgs, Metadat
2
2
  export type { VeloConfig } from "./config.js";
3
3
  export { defineConfig } from "./config.js";
4
4
  export { Scripts, Link } from "./components.js";
5
- export { createEventStream } from "./events.js";
6
- export type { EventStream, EventStreamConfig } from "./events.js";
5
+ export { createEventStream, poll } from "./events.js";
6
+ export type { EventStream, EventStreamConfig, EmitFn, EmitOptions, SourceFn, } from "./events.js";
7
7
  export type { Context, MiddlewareHandler, Next } from "hono";
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { defineConfig as o } from "./config.js";
2
- import { Link as f, Scripts as i } from "./components.js";
3
- import { createEventStream as n } from "./events.js";
2
+ import { Link as f, Scripts as p } from "./components.js";
3
+ import { createEventStream as m, poll as n } from "./events.js";
4
4
  export {
5
5
  f as Link,
6
- i as Scripts,
7
- n as createEventStream,
8
- o as defineConfig
6
+ p as Scripts,
7
+ m as createEventStream,
8
+ o as defineConfig,
9
+ n as poll
9
10
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mauroandre/velojs",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
4
4
  "type": "module",
5
5
  "description": "Fullstack web framework built on Hono + Preact with Vite-powered AST transforms",
6
6
  "keywords": [