@mauroandre/velojs 0.0.19 → 0.0.21
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 +115 -48
- package/dist/events.d.ts +133 -26
- package/dist/events.js +120 -42
- package/dist/hooks.js +27 -25
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -536,57 +536,91 @@ 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).
|
|
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
|
-
|
|
541
|
+
Three verbs: `emit`, `close`, `useEventStream`. The framework handles routing, types, listener management, snapshots, lifecycle, reconnection, and cleanup.
|
|
542
542
|
|
|
543
|
-
|
|
543
|
+
### Shortest example
|
|
544
544
|
|
|
545
545
|
```typescript
|
|
546
|
-
// app/admin/
|
|
546
|
+
// app/admin/Provision.tsx
|
|
547
547
|
import { createEventStream } from "@mauroandre/velojs";
|
|
548
|
-
import { useEventStream
|
|
548
|
+
import { useEventStream } from "@mauroandre/velojs/hooks";
|
|
549
549
|
|
|
550
|
-
|
|
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
|
|
562
|
-
const
|
|
563
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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. Use `broadcast: true` for streams without channels.
|
|
575
585
|
|
|
576
|
-
|
|
586
|
+
```typescript
|
|
587
|
+
export const containerMetrics = createEventStream<Metric[]>({
|
|
588
|
+
path: "/api/metrics/containers",
|
|
589
|
+
broadcast: true,
|
|
590
|
+
middlewares: [authMiddleware],
|
|
591
|
+
});
|
|
577
592
|
```
|
|
578
593
|
|
|
579
|
-
###
|
|
594
|
+
### Three ways to emit
|
|
580
595
|
|
|
581
|
-
|
|
596
|
+
**Reactive** — call `emit()` from anywhere when something happens. Most common:
|
|
582
597
|
|
|
583
598
|
```typescript
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
599
|
+
stream_logs.emit(sessionId, "Line", { snapshot: true });
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
**Source-driven** — pass a `source` function. Framework runs it only while subscribed (zero CPU when nobody is watching):
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
import { poll } from "@mauroandre/velojs";
|
|
606
|
+
|
|
607
|
+
export const stream_metrics = createEventStream<Metric[]>({
|
|
608
|
+
broadcast: true,
|
|
609
|
+
source: poll({
|
|
610
|
+
intervalMs: 3000,
|
|
611
|
+
tick: async (emit) => emit(await collectMetrics()),
|
|
612
|
+
}),
|
|
587
613
|
});
|
|
614
|
+
```
|
|
588
615
|
|
|
589
|
-
|
|
616
|
+
**Stateful snapshot** — for state-machine patterns where each emit is the complete current state:
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
const deploys = new Map<string, DeployState>();
|
|
620
|
+
|
|
621
|
+
export const stream_deploy = createEventStream<DeployState>({
|
|
622
|
+
snapshot: (id) => deploys.get(id ?? ""),
|
|
623
|
+
});
|
|
590
624
|
```
|
|
591
625
|
|
|
592
626
|
### Configuration
|
|
@@ -594,25 +628,50 @@ setInterval(() => metricsStream.emit(collectMetrics()), 2000);
|
|
|
594
628
|
| Option | Type | Description |
|
|
595
629
|
|--------|------|-------------|
|
|
596
630
|
| `path` | `string` | (Standalone) Explicit URL path |
|
|
597
|
-
| `
|
|
598
|
-
| `
|
|
599
|
-
| `
|
|
600
|
-
| `
|
|
601
|
-
| `
|
|
631
|
+
| `broadcast` | `boolean` | If true, every emit goes to all subscribers (no channels). Default: false |
|
|
632
|
+
| `channel` | `(c) => string` | Extract channel ID. Default: `?channel=...` query param. Ignored when `broadcast: true` |
|
|
633
|
+
| `snapshot` | `(channel) => TSnapshot` | Returns current state on connect (state-machine pattern) |
|
|
634
|
+
| `closeOn` | `(event) => boolean` | Closes SSE when matching event is sent (declarative) |
|
|
635
|
+
| `source` | `(emit, { abortSignal }) => Promise<void>` | Self-running producer, only runs while subscribed |
|
|
636
|
+
| `retainMs` | `number` | Buffer retention after `close()`. Default `300000` (5 min) |
|
|
637
|
+
| `heartbeatMs` | `number \| false` | Heartbeat interval. Default `20000` (20s). `false` to disable |
|
|
638
|
+
| `middlewares` | `MiddlewareHandler[]` | (Standalone) Hono middlewares for the SSE route |
|
|
602
639
|
|
|
603
|
-
###
|
|
640
|
+
### Snapshot mechanisms
|
|
604
641
|
|
|
605
|
-
|
|
642
|
+
Two snapshot patterns for two cases:
|
|
606
643
|
|
|
607
|
-
|
|
644
|
+
**Per-emit `{ snapshot: true }` — append pattern.** Framework keeps a buffer per channel. Late subscribers receive the array.
|
|
608
645
|
|
|
609
646
|
```typescript
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
});
|
|
647
|
+
stream_logs.emit(id, "line 1", { snapshot: true });
|
|
648
|
+
stream_logs.emit(id, "line 2", { snapshot: true });
|
|
649
|
+
// Late subscriber → snapshot.value = ["line 1", "line 2"]
|
|
614
650
|
```
|
|
615
651
|
|
|
652
|
+
**`snapshot: callback` config — replace pattern.** You return the latest state from your own data structure.
|
|
653
|
+
|
|
654
|
+
```typescript
|
|
655
|
+
createEventStream({ snapshot: (id) => deploys.get(id ?? "") });
|
|
656
|
+
// Late subscriber → snapshot.value = the latest DeployState
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Both survive after `close()` for `retainMs` (default 5 min) so refresh-after-finish still shows final state.
|
|
660
|
+
|
|
661
|
+
### Closing
|
|
662
|
+
|
|
663
|
+
Two ways, choose either or both:
|
|
664
|
+
|
|
665
|
+
```typescript
|
|
666
|
+
// Imperative — call from anywhere
|
|
667
|
+
stream.close(channelId);
|
|
668
|
+
|
|
669
|
+
// Declarative — predicate on event
|
|
670
|
+
createEventStream({ closeOn: (s) => s.status === "success" });
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
After close, subsequent `emit()` to that channel is ignored with a warning.
|
|
674
|
+
|
|
616
675
|
### `useEventStream` hook
|
|
617
676
|
|
|
618
677
|
```typescript
|
|
@@ -624,12 +683,20 @@ const { data, snapshot, closed, error } = useEventStream(stream, {
|
|
|
624
683
|
|
|
625
684
|
| Signal | Description |
|
|
626
685
|
|--------|-------------|
|
|
627
|
-
| `data` | Latest event received
|
|
628
|
-
| `snapshot` | Initial state on connect (
|
|
629
|
-
| `closed` | `true` when server closed the stream
|
|
686
|
+
| `data` | Latest event received |
|
|
687
|
+
| `snapshot` | Initial state on connect (buffer or callback) |
|
|
688
|
+
| `closed` | `true` when server closed the stream |
|
|
630
689
|
| `error` | Parse or connection error, if any |
|
|
631
690
|
|
|
632
|
-
|
|
691
|
+
Lifecycle is automatic: opens on mount, closes on unmount, re-opens fresh when `channel` changes.
|
|
692
|
+
|
|
693
|
+
### `poll` helper
|
|
694
|
+
|
|
695
|
+
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.
|
|
696
|
+
|
|
697
|
+
```typescript
|
|
698
|
+
poll({ intervalMs: 3000, tick: async (emit) => emit(await collect()) })
|
|
699
|
+
```
|
|
633
700
|
|
|
634
701
|
---
|
|
635
702
|
|
|
@@ -1076,11 +1143,11 @@ Hooks (`useParams`, `useQuery`, `usePathname`, `Loader`, `useLoader`) access thi
|
|
|
1076
1143
|
|
|
1077
1144
|
| Import | Contents |
|
|
1078
1145
|
|--------|----------|
|
|
1079
|
-
| `@mauroandre/velojs` | Types (`AppRoutes`, `ActionArgs`, `LoaderArgs`, `Metadata`, `EventStream`, `EventStreamConfig`), `Scripts`, `Link`, `createEventStream`, `defineConfig` |
|
|
1146
|
+
| `@mauroandre/velojs` | Types (`AppRoutes`, `ActionArgs`, `LoaderArgs`, `Metadata`, `EventStream`, `EventStreamConfig`, `EmitFn`, `EmitOptions`, `SourceFn`), `Scripts`, `Link`, `createEventStream`, `poll`, `defineConfig` |
|
|
1080
1147
|
| `@mauroandre/velojs/server` | `startServer`, `createApp`, `addRoutes`, `onServer`, `serverDataStorage` |
|
|
1081
1148
|
| `@mauroandre/velojs/client` | `startClient` |
|
|
1082
1149
|
| `@mauroandre/velojs/hooks` | `Loader`, `useLoader`, `useEventStream`, `useParams`, `useQuery`, `useNavigate`, `usePathname`, `touch` |
|
|
1083
|
-
| `@mauroandre/velojs/events` | `createEventStream`, `EventStream`, `EventStreamConfig` (also re-exported from root) |
|
|
1150
|
+
| `@mauroandre/velojs/events` | `createEventStream`, `poll`, `EventStream`, `EventStreamConfig`, `EmitFn`, `EmitOptions`, `SourceFn` (also re-exported from root) |
|
|
1084
1151
|
| `@mauroandre/velojs/cookie` | `getCookie`, `setCookie`, `deleteCookie`, `getSignedCookie`, `setSignedCookie` |
|
|
1085
1152
|
| `@mauroandre/velojs/factory` | `createMiddleware`, `createFactory` |
|
|
1086
1153
|
| `@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
|
|
5
|
+
* Two ways to declare:
|
|
6
6
|
*
|
|
7
|
-
* 1.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
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,37 @@ 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,
|
|
79
|
+
* When omitted, defaults to `c.req.query("channel") ?? ""` so client-side
|
|
80
|
+
* `useEventStream(stream, { channel })` works out of the box.
|
|
81
|
+
*
|
|
82
|
+
* Ignored when `broadcast: true` is set.
|
|
37
83
|
*/
|
|
38
84
|
channel?: (c: Context) => string;
|
|
85
|
+
/**
|
|
86
|
+
* If true, this stream has no channels — every emit goes to all subscribers.
|
|
87
|
+
* - `emit(value, options?)` (no channel arg)
|
|
88
|
+
* - `useEventStream(stream)` (no channel option)
|
|
89
|
+
* - Server uses a single broadcast bucket regardless of `?channel=` query.
|
|
90
|
+
*
|
|
91
|
+
* Default: false (channel-aware — `emit(channel, value, options?)`).
|
|
92
|
+
*/
|
|
93
|
+
broadcast?: boolean;
|
|
39
94
|
/**
|
|
40
95
|
* Returns the current state of a channel for snapshot-on-connect.
|
|
41
96
|
* Sent before any new events to bring the client up to date.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
97
|
+
*
|
|
98
|
+
* Use this for **state-machine patterns** where each emit is a complete state
|
|
99
|
+
* (deploy progress, migration status). The callback returns the latest state.
|
|
100
|
+
*
|
|
101
|
+
* For **append-only patterns** (logs, tokens), prefer `emit(..., { snapshot: true })`
|
|
102
|
+
* which uses an internal buffer managed by the framework.
|
|
44
103
|
*/
|
|
45
104
|
snapshot?: (channel: string | undefined) => TSnapshot | undefined | Promise<TSnapshot | undefined>;
|
|
46
105
|
/**
|
|
47
106
|
* Returns true when an event marks the end of the stream for that channel.
|
|
48
107
|
* The server closes the SSE connection after sending such an event.
|
|
108
|
+
*
|
|
109
|
+
* For imperative close from anywhere, use `stream.close(channel)` instead.
|
|
49
110
|
*/
|
|
50
111
|
closeOn?: (event: TEvent) => boolean;
|
|
51
112
|
/**
|
|
@@ -60,32 +121,78 @@ export interface EventStreamConfig<TEvent, TSnapshot = TEvent> {
|
|
|
60
121
|
* parent route nodes automatically.
|
|
61
122
|
*/
|
|
62
123
|
middlewares?: MiddlewareHandler[];
|
|
124
|
+
/**
|
|
125
|
+
* Self-running producer that emits values into the stream.
|
|
126
|
+
* The framework only invokes `source` while there are active subscribers.
|
|
127
|
+
* When the last subscriber disconnects, the AbortSignal fires.
|
|
128
|
+
*
|
|
129
|
+
* Use the `poll` helper for the common interval-based pattern.
|
|
130
|
+
*/
|
|
131
|
+
source?: SourceFn<TEvent>;
|
|
132
|
+
/**
|
|
133
|
+
* Time (ms) the per-channel snapshot buffer is retained after `close()`.
|
|
134
|
+
* Reconnecting clients within this window still see the final state.
|
|
135
|
+
* Default: 5 * 60 * 1000 (5 minutes).
|
|
136
|
+
*/
|
|
137
|
+
retainMs?: number;
|
|
63
138
|
}
|
|
64
139
|
export interface EventStream<TEvent, TSnapshot = TEvent> {
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Emit an event.
|
|
142
|
+
* - For channel-aware streams (default): `emit(channel, value, options?)`
|
|
143
|
+
* - For broadcast streams (`config.broadcast: true`): `emit(value, options?)`
|
|
144
|
+
*/
|
|
145
|
+
emit(event: TEvent, options?: EmitOptions): void;
|
|
146
|
+
emit(channel: string, event: TEvent, options?: EmitOptions): void;
|
|
147
|
+
/**
|
|
148
|
+
* Closes the stream (or a specific channel).
|
|
149
|
+
* - Notifies current subscribers so their `useEventStream().closed` becomes true.
|
|
150
|
+
* - Schedules cleanup of the per-channel buffer after `retainMs`.
|
|
151
|
+
* - Idempotent: calling close on an already-closed channel is a no-op.
|
|
152
|
+
*/
|
|
153
|
+
close(channel?: string): void;
|
|
69
154
|
/** Internal: marker for runtime detection */
|
|
70
155
|
readonly __isVeloEventStream: true;
|
|
71
156
|
/** Internal: configuration */
|
|
72
157
|
readonly __config: EventStreamConfig<TEvent, TSnapshot>;
|
|
73
158
|
/** Internal: registered listeners by channel ("" for broadcast) */
|
|
74
|
-
readonly __listeners: Map<string, Set<
|
|
159
|
+
readonly __listeners: Map<string, Set<StreamListener<TEvent>>>;
|
|
160
|
+
/** Internal: per-channel buffer for { snapshot: true } emits */
|
|
161
|
+
readonly __buffers: Map<string, TEvent[]>;
|
|
75
162
|
/** Internal: assigned URL path (set by standalone or by stream_* discovery) */
|
|
76
163
|
__path: string | undefined;
|
|
164
|
+
/** Internal: source lifecycle hook */
|
|
165
|
+
__onConnect(channelKey: string, listener: StreamListener<TEvent>): void;
|
|
166
|
+
/** Internal: source lifecycle hook */
|
|
167
|
+
__onDisconnect(channelKey: string, listener: StreamListener<TEvent>): void;
|
|
168
|
+
}
|
|
169
|
+
/** Internal listener — receives events and a close signal. */
|
|
170
|
+
interface StreamListener<TEvent> {
|
|
171
|
+
(event: TEvent): void;
|
|
172
|
+
close(): void;
|
|
77
173
|
}
|
|
78
|
-
/**
|
|
79
|
-
* Returns and clears the queue of pending standalone streams.
|
|
80
|
-
* Called by createApp to register their routes.
|
|
81
|
-
*/
|
|
82
174
|
export declare function flushPendingStreamRoutes(app: Hono): void;
|
|
175
|
+
export declare function createEventStream<TEvent, TSnapshot = TEvent>(config?: EventStreamConfig<TEvent, TSnapshot>): EventStream<TEvent, TSnapshot>;
|
|
83
176
|
/**
|
|
84
|
-
* Creates a
|
|
177
|
+
* Creates a `source` function that runs `tick` every `intervalMs` while subscribed.
|
|
178
|
+
* The tick function is wrapped in try/catch — errors are logged but don't stop the loop.
|
|
179
|
+
*
|
|
180
|
+
* ```typescript
|
|
181
|
+
* export const stream_metrics = createEventStream<Metric[]>({
|
|
182
|
+
* source: poll({
|
|
183
|
+
* intervalMs: 3000,
|
|
184
|
+
* tick: async (emit) => emit(await collectMetrics()),
|
|
185
|
+
* }),
|
|
186
|
+
* });
|
|
187
|
+
* ```
|
|
85
188
|
*/
|
|
86
|
-
export declare function
|
|
189
|
+
export declare function poll<TEvent>(opts: {
|
|
190
|
+
intervalMs: number;
|
|
191
|
+
tick: (emit: EmitFn<TEvent>) => void | Promise<void>;
|
|
192
|
+
}): SourceFn<TEvent>;
|
|
87
193
|
/**
|
|
88
194
|
* Registers a GET SSE route for the given stream at the given path.
|
|
89
195
|
* Used both by standalone streams and by stream_* discovery.
|
|
90
196
|
*/
|
|
91
197
|
export declare function registerStreamHandler<TEvent, TSnapshot>(app: Hono, path: string, stream: EventStream<TEvent, TSnapshot>, middlewares?: Array<(c: Context, next: () => Promise<void>) => any>): void;
|
|
198
|
+
export {};
|
package/dist/events.js
CHANGED
|
@@ -1,62 +1,140 @@
|
|
|
1
|
-
const
|
|
2
|
-
function
|
|
3
|
-
for (const
|
|
4
|
-
|
|
1
|
+
const T = [];
|
|
2
|
+
function m(t) {
|
|
3
|
+
for (const r of T) r(t);
|
|
4
|
+
T.length = 0;
|
|
5
5
|
}
|
|
6
6
|
function N(t = {}) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
t.broadcast && t.channel && console.warn(
|
|
8
|
+
"[velojs] createEventStream: `broadcast: true` ignores `channel`. Remove one of them — broadcast streams have no channels."
|
|
9
|
+
);
|
|
10
|
+
const r = /* @__PURE__ */ new Map(), o = /* @__PURE__ */ new Map(), a = /* @__PURE__ */ new Set(), u = /* @__PURE__ */ new Map();
|
|
11
|
+
let f = null, A = 0;
|
|
12
|
+
const d = () => {
|
|
13
|
+
t.source && (f || (f = new AbortController(), Promise.resolve(
|
|
14
|
+
t.source(E, { abortSignal: f.signal })
|
|
15
|
+
).catch((e) => {
|
|
16
|
+
console.error("[velojs] event stream source threw:", e);
|
|
17
|
+
})));
|
|
18
|
+
}, w = () => {
|
|
19
|
+
A > 0 || f && (f.abort(), f = null);
|
|
20
|
+
};
|
|
21
|
+
function E(e, c, n) {
|
|
22
|
+
let s, l, h;
|
|
23
|
+
if (!!t.broadcast ? (s = "", l = e, h = c) : (s = e, l = c, h = n), a.has(s)) {
|
|
24
|
+
console.warn(
|
|
25
|
+
`[velojs] emit() called on closed channel "${s}" — ignored. Reopen the channel by emitting to a fresh ID, or remove the close() call.`
|
|
26
|
+
);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (h?.snapshot) {
|
|
30
|
+
const b = o.get(s) ?? [];
|
|
31
|
+
b.push(l), o.set(s, b);
|
|
32
|
+
}
|
|
33
|
+
const v = r.get(s);
|
|
34
|
+
if (v)
|
|
35
|
+
for (const b of v) b(l);
|
|
36
|
+
}
|
|
37
|
+
function i(e = "") {
|
|
38
|
+
if (a.has(e)) return;
|
|
39
|
+
a.add(e);
|
|
40
|
+
const c = r.get(e);
|
|
12
41
|
if (c)
|
|
13
|
-
for (const
|
|
42
|
+
for (const l of c) l.close();
|
|
43
|
+
const n = t.retainMs ?? 3e5, s = u.get(e);
|
|
44
|
+
s && clearTimeout(s), u.set(
|
|
45
|
+
e,
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
o.delete(e), u.delete(e), a.delete(e);
|
|
48
|
+
}, n)
|
|
49
|
+
);
|
|
14
50
|
}
|
|
15
|
-
const
|
|
16
|
-
emit:
|
|
51
|
+
const S = {
|
|
52
|
+
emit: E,
|
|
53
|
+
close: i,
|
|
17
54
|
__isVeloEventStream: !0,
|
|
18
55
|
__config: t,
|
|
19
|
-
__listeners:
|
|
20
|
-
|
|
56
|
+
__listeners: r,
|
|
57
|
+
__buffers: o,
|
|
58
|
+
__path: t.path,
|
|
59
|
+
__onConnect(e, c) {
|
|
60
|
+
let n = r.get(e);
|
|
61
|
+
n || (n = /* @__PURE__ */ new Set(), r.set(e, n)), n.add(c), A++, d();
|
|
62
|
+
},
|
|
63
|
+
__onDisconnect(e, c) {
|
|
64
|
+
const n = r.get(e);
|
|
65
|
+
n && (n.delete(c), n.size === 0 && r.delete(e), A--, w());
|
|
66
|
+
}
|
|
21
67
|
};
|
|
22
|
-
return t.path &&
|
|
23
|
-
|
|
24
|
-
}),
|
|
68
|
+
return t.path && T.push((e) => {
|
|
69
|
+
g(e, t.path, S, t.middlewares ?? []);
|
|
70
|
+
}), S;
|
|
25
71
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
72
|
+
function C(t) {
|
|
73
|
+
return async (r, { abortSignal: o }) => {
|
|
74
|
+
for (; !o.aborted; ) {
|
|
75
|
+
try {
|
|
76
|
+
await t.tick(r);
|
|
77
|
+
} catch (a) {
|
|
78
|
+
console.error("[velojs] poll tick threw:", a);
|
|
79
|
+
}
|
|
80
|
+
await new Promise((a) => {
|
|
81
|
+
const u = setTimeout(a, t.intervalMs);
|
|
82
|
+
o.addEventListener(
|
|
83
|
+
"abort",
|
|
84
|
+
() => {
|
|
85
|
+
clearTimeout(u), a();
|
|
86
|
+
},
|
|
87
|
+
{ once: !0 }
|
|
33
88
|
);
|
|
34
|
-
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function p(t) {
|
|
94
|
+
return t.req.query("channel") ?? "";
|
|
95
|
+
}
|
|
96
|
+
function g(t, r, o, a = []) {
|
|
97
|
+
const u = async (f) => {
|
|
98
|
+
const { streamSSE: A } = await import("hono/streaming"), d = o.__config.broadcast ? "" : (o.__config.channel ?? p)(f), w = o.__config.heartbeatMs, E = w === !1 ? 0 : w ?? 2e4;
|
|
99
|
+
return A(f, async (i) => {
|
|
100
|
+
const S = o.__buffers.get(d);
|
|
101
|
+
if (S && S.length > 0 && await i.writeSSE({
|
|
102
|
+
event: "snapshot",
|
|
103
|
+
data: JSON.stringify(S)
|
|
104
|
+
}), o.__config.snapshot) {
|
|
105
|
+
const _ = await o.__config.snapshot(
|
|
106
|
+
d === "" ? void 0 : d
|
|
107
|
+
);
|
|
108
|
+
_ !== void 0 && await i.writeSSE({
|
|
35
109
|
event: "snapshot",
|
|
36
|
-
data: JSON.stringify(
|
|
110
|
+
data: JSON.stringify(_)
|
|
37
111
|
});
|
|
38
112
|
}
|
|
39
|
-
let
|
|
40
|
-
const
|
|
41
|
-
|
|
113
|
+
let e;
|
|
114
|
+
const c = new Promise((_) => {
|
|
115
|
+
e = _;
|
|
116
|
+
});
|
|
117
|
+
let n = !1, s = null;
|
|
118
|
+
const l = () => {
|
|
119
|
+
n || (n = !0, s && clearInterval(s), o.__onDisconnect(d, h), e());
|
|
120
|
+
}, h = ((_) => {
|
|
121
|
+
n || (i.writeSSE({ data: JSON.stringify(_) }).catch(() => {
|
|
122
|
+
}), o.__config.closeOn?.(_) && (i.close(), l()));
|
|
42
123
|
});
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
},
|
|
47
|
-
|
|
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(() => {
|
|
124
|
+
h.close = () => {
|
|
125
|
+
n || (i.writeSSE({ event: "close", data: "" }).catch(() => {
|
|
126
|
+
}), i.close(), l());
|
|
127
|
+
}, o.__onConnect(d, h), s = E > 0 ? setInterval(() => {
|
|
128
|
+
n || i.writeSSE({ event: "heartbeat", data: "" }).catch(() => {
|
|
52
129
|
});
|
|
53
|
-
},
|
|
130
|
+
}, E) : null, i.onAbort(l), await c;
|
|
54
131
|
});
|
|
55
132
|
};
|
|
56
|
-
|
|
133
|
+
a.length > 0 ? t.on(["GET"], [r], ...a, u) : t.on(["GET"], [r], u);
|
|
57
134
|
}
|
|
58
135
|
export {
|
|
59
136
|
N as createEventStream,
|
|
60
|
-
|
|
61
|
-
|
|
137
|
+
m as flushPendingStreamRoutes,
|
|
138
|
+
C as poll,
|
|
139
|
+
g as registerStreamHandler
|
|
62
140
|
};
|
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__,
|
|
17
|
-
return { data: v(
|
|
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,
|
|
21
|
-
Array.isArray(e) ?
|
|
22
|
-
let
|
|
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
|
|
25
|
-
|
|
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] && (
|
|
29
|
+
a?.[t] && (n = a[t], delete a[t]);
|
|
30
30
|
}
|
|
31
|
-
const u = c(
|
|
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
|
|
37
|
-
return
|
|
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((
|
|
40
|
-
typeof __VELO_BUILD_HASH__ < "u" &&
|
|
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(
|
|
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
|
-
},
|
|
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((
|
|
68
|
-
t[
|
|
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),
|
|
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,
|
|
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", (
|
|
84
|
+
return a.addEventListener("snapshot", (i) => {
|
|
85
85
|
try {
|
|
86
|
-
|
|
86
|
+
r.value = JSON.parse(i.data);
|
|
87
87
|
} catch (f) {
|
|
88
88
|
u.value = f;
|
|
89
89
|
}
|
|
90
|
-
}), a.addEventListener("message", (
|
|
90
|
+
}), a.addEventListener("message", (i) => {
|
|
91
91
|
try {
|
|
92
|
-
t.value = JSON.parse(
|
|
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 && (
|
|
100
|
+
a.readyState === EventSource.CLOSED && (n.value = !0);
|
|
99
101
|
}), () => {
|
|
100
102
|
a.close();
|
|
101
103
|
};
|
|
102
|
-
}, [e, s, _]), { data: t, snapshot:
|
|
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
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
o as defineConfig
|
|
6
|
+
p as Scripts,
|
|
7
|
+
m as createEventStream,
|
|
8
|
+
o as defineConfig,
|
|
9
|
+
n as poll
|
|
9
10
|
};
|