@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 +112 -48
- package/dist/events.d.ts +117 -24
- package/dist/events.js +119 -44
- 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,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).
|
|
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.
|
|
575
585
|
|
|
576
|
-
|
|
586
|
+
```typescript
|
|
587
|
+
export const containerMetrics = createEventStream<Metric[]>({
|
|
588
|
+
path: "/api/metrics/containers",
|
|
589
|
+
middlewares: [authMiddleware],
|
|
590
|
+
});
|
|
577
591
|
```
|
|
578
592
|
|
|
579
|
-
###
|
|
593
|
+
### Three ways to emit
|
|
580
594
|
|
|
581
|
-
|
|
595
|
+
**Reactive** — call `emit()` from anywhere when something happens. Most common:
|
|
582
596
|
|
|
583
597
|
```typescript
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
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
|
|
598
|
-
| `snapshot` | `(channel) => TSnapshot
|
|
599
|
-
| `closeOn` | `(event
|
|
600
|
-
| `
|
|
601
|
-
| `
|
|
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
|
-
###
|
|
637
|
+
### Snapshot mechanisms
|
|
604
638
|
|
|
605
|
-
|
|
639
|
+
Two snapshot patterns for two cases:
|
|
606
640
|
|
|
607
|
-
|
|
641
|
+
**Per-emit `{ snapshot: true }` — append pattern.** Framework keeps a buffer per channel. Late subscribers receive the array.
|
|
608
642
|
|
|
609
643
|
```typescript
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
|
628
|
-
| `snapshot` | Initial state on connect (
|
|
629
|
-
| `closed` | `true` when server closed the stream
|
|
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
|
-
|
|
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
|
|
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,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,
|
|
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
|
-
*
|
|
43
|
-
*
|
|
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<
|
|
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
|
|
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
|
|
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
|
|
2
|
-
function
|
|
3
|
-
for (const
|
|
4
|
-
|
|
1
|
+
const p = [];
|
|
2
|
+
function m(t) {
|
|
3
|
+
for (const r of p) r(t);
|
|
4
|
+
p.length = 0;
|
|
5
5
|
}
|
|
6
|
-
function
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
16
|
-
emit:
|
|
48
|
+
const c = {
|
|
49
|
+
emit: A,
|
|
50
|
+
close: E,
|
|
17
51
|
__isVeloEventStream: !0,
|
|
18
52
|
__config: t,
|
|
19
|
-
__listeners:
|
|
20
|
-
|
|
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 &&
|
|
23
|
-
|
|
24
|
-
}),
|
|
65
|
+
return t.path && p.push((e) => {
|
|
66
|
+
v(e, t.path, c, t.middlewares ?? []);
|
|
67
|
+
}), c;
|
|
25
68
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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(
|
|
107
|
+
data: JSON.stringify(f)
|
|
37
108
|
});
|
|
38
109
|
}
|
|
39
|
-
let
|
|
40
|
-
const
|
|
41
|
-
|
|
110
|
+
let i;
|
|
111
|
+
const s = new Promise((f) => {
|
|
112
|
+
i = f;
|
|
42
113
|
});
|
|
43
|
-
let
|
|
114
|
+
let o = !1, l = null;
|
|
44
115
|
const d = () => {
|
|
45
|
-
|
|
46
|
-
},
|
|
47
|
-
|
|
48
|
-
}),
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
},
|
|
127
|
+
}, E) : null, c.onAbort(d), await s;
|
|
54
128
|
});
|
|
55
129
|
};
|
|
56
|
-
|
|
130
|
+
a.length > 0 ? t.on(["GET"], [r], ...a, _) : t.on(["GET"], [r], _);
|
|
57
131
|
}
|
|
58
132
|
export {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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__,
|
|
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
|
};
|