@mauroandre/velojs 0.0.26 → 0.0.28
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 +81 -2
- package/dist/events.d.ts +58 -3
- package/dist/events.js +174 -135
- package/dist/hooks.d.ts +56 -0
- package/dist/hooks.js +128 -68
- package/dist/index.d.ts +2 -1
- package/dist/server.js +51 -50
- package/dist/sockets.d.ts +109 -0
- package/dist/sockets.js +196 -0
- package/dist/testing/index.js +454 -291
- package/dist/testing/socketSession.d.ts +13 -0
- package/dist/testing/types.d.ts +36 -0
- package/dist/vite.d.ts +8 -0
- package/dist/vite.js +267 -236
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -571,7 +571,7 @@ Because endpoints live in `routes.tsx`, the testing toolkit sees them automatica
|
|
|
571
571
|
const res = await app.post("/api/github/webhook", { body: {...}, headers: {...} });
|
|
572
572
|
```
|
|
573
573
|
|
|
574
|
-
Full documentation: [site/docs/
|
|
574
|
+
Full documentation: [site/docs/06-endpoints.md](./site/docs/06-endpoints.md).
|
|
575
575
|
|
|
576
576
|
---
|
|
577
577
|
|
|
@@ -790,6 +790,85 @@ Only affects emits with `{ snapshot: true }`.
|
|
|
790
790
|
|
|
791
791
|
---
|
|
792
792
|
|
|
793
|
+
## Sockets
|
|
794
|
+
|
|
795
|
+
Declarative WebSocket handlers, co-located with the page that needs bidirectional communication. Use for interactive terminals, collaborative editing, live cursors — anything where the client also sends messages.
|
|
796
|
+
|
|
797
|
+
Export `socket_<name>` from a page or layout, and VeloJS registers a WebSocket route at `/_socket/{moduleId}/{name}` with middleware inheritance.
|
|
798
|
+
|
|
799
|
+
### Shortest example
|
|
800
|
+
|
|
801
|
+
```tsx
|
|
802
|
+
// app/workers/WorkerTerminal.tsx
|
|
803
|
+
import type { SocketHandler } from "@mauroandre/velojs";
|
|
804
|
+
import { parseJson } from "@mauroandre/velojs/sockets";
|
|
805
|
+
import { useSocket } from "@mauroandre/velojs/hooks";
|
|
806
|
+
|
|
807
|
+
export const socket_terminal: SocketHandler = async ({
|
|
808
|
+
incoming, send, keepOpen, abortSignal, c, params,
|
|
809
|
+
}) => {
|
|
810
|
+
const session = await createTerminal(params.workerId);
|
|
811
|
+
session.on("data", (chunk) => send({ type: "data", data: chunk }));
|
|
812
|
+
abortSignal.addEventListener("abort", () => session.destroy());
|
|
813
|
+
keepOpen();
|
|
814
|
+
|
|
815
|
+
for await (const msg of parseJson<{ type: string; data?: string; cols?: number; rows?: number }>(incoming)) {
|
|
816
|
+
if (msg.type === "data" && msg.data) session.write(msg.data);
|
|
817
|
+
if (msg.type === "resize") session.resize(msg.cols!, msg.rows!);
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
export const Component = () => {
|
|
822
|
+
const { send, status } = useSocket(socket_terminal, { channel: workerId });
|
|
823
|
+
// ...
|
|
824
|
+
};
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
### Handler args
|
|
828
|
+
|
|
829
|
+
| Arg | Description |
|
|
830
|
+
|---|---|
|
|
831
|
+
| `incoming` | `AsyncIterable<string \| Uint8Array>` — raw frames. Wrap with `parseJson<T>()` for JSON auto-parse. |
|
|
832
|
+
| `send(msg)` | Send a frame. `string` / `Uint8Array` pass through; `object` → `JSON.stringify`. |
|
|
833
|
+
| `close(code?, reason?)` | Server-initiated close. |
|
|
834
|
+
| `keepOpen()` | Required for long-lived handlers. If you return without it, the socket closes. `for await (const m of incoming)` implicitly holds the handler. |
|
|
835
|
+
| `abortSignal` | Fires on disconnect / `app.close()`. Register all cleanup here. |
|
|
836
|
+
| `c`, `params`, `query` | Hono context, URL params, query. Auth via `c.get("user")` from middleware. |
|
|
837
|
+
|
|
838
|
+
### Middleware inheritance
|
|
839
|
+
|
|
840
|
+
Middleware runs **before** the WebSocket upgrade — an `authMiddleware` that rejects returns an HTTP error and the client never sees an open socket. Same `middlewares` array on parent route nodes as pages/actions/streams.
|
|
841
|
+
|
|
842
|
+
### Client hook
|
|
843
|
+
|
|
844
|
+
```ts
|
|
845
|
+
const { send, status, lastMessage, close } = useSocket(socket_terminal, {
|
|
846
|
+
channel: workerId,
|
|
847
|
+
onMessage: (msg) => { /* ... */ },
|
|
848
|
+
});
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
- `status: Signal<"connecting" | "open" | "closed">` — reactive.
|
|
852
|
+
- **No auto-reconnect** — sockets are usually stateful (pty sessions, collaborative state); blind reconnect loses state silently. Re-mount or toggle `enabled` to reconnect.
|
|
853
|
+
|
|
854
|
+
### Testing
|
|
855
|
+
|
|
856
|
+
```ts
|
|
857
|
+
const ws = await app.socket(socket_terminal, {
|
|
858
|
+
user: { id: "alice" },
|
|
859
|
+
params: { workerId: "w42" },
|
|
860
|
+
});
|
|
861
|
+
ws.send({ type: "data", data: "ls\n" });
|
|
862
|
+
const reply = await ws.next({ timeoutMs: 500 });
|
|
863
|
+
await ws.close();
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
`app.socket()` invokes the handler in-memory and aborts on `app.close()`. **Middleware does NOT run** — pass `user` via options to shortcut `c.get("user")`; test middleware via regular endpoint tests that use the same middleware.
|
|
867
|
+
|
|
868
|
+
Full reference: [site/docs/12-sockets.md](./site/docs/12-sockets.md).
|
|
869
|
+
|
|
870
|
+
---
|
|
871
|
+
|
|
793
872
|
## Hooks
|
|
794
873
|
|
|
795
874
|
All hooks work in both SSR and client (via AsyncLocalStorage on server, wouter/DOM on client).
|
|
@@ -1279,7 +1358,7 @@ await app.close();
|
|
|
1279
1358
|
| `app.reset()` | Clear stream buffers/listeners between tests |
|
|
1280
1359
|
| `app.close()` | Tear down everything (zero open handles guaranteed) |
|
|
1281
1360
|
|
|
1282
|
-
See [Testing docs](https://github.com/mauro-andre/velojs/blob/dev/site/docs/
|
|
1361
|
+
See [Testing docs](https://github.com/mauro-andre/velojs/blob/dev/site/docs/17-testing.md) for the full guide with patterns, isolation, and FAQ.
|
|
1283
1362
|
|
|
1284
1363
|
---
|
|
1285
1364
|
|
package/dist/events.d.ts
CHANGED
|
@@ -89,6 +89,45 @@ export type SourceFn<TEvent> = (emit: EmitFn<TEvent>, ctx: {
|
|
|
89
89
|
export type PerChannelSourceFn<TEvent> = (channelKey: string, emit: (event: TEvent, options?: EmitOptions) => void, ctx: {
|
|
90
90
|
abortSignal: AbortSignal;
|
|
91
91
|
}) => void | Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Arguments passed to a stream `pipe` handler.
|
|
94
|
+
*
|
|
95
|
+
* A `pipe` is a **per-channel producer** (same scoping as `perChannelSource`)
|
|
96
|
+
* with the unified vocabulary used across `stream_*` and `socket_*`:
|
|
97
|
+
* `send / keepOpen / close / abortSignal`. Prefer `pipe` for new code.
|
|
98
|
+
*
|
|
99
|
+
* `c`, `params`, `query` are captured from the **first** subscriber whose
|
|
100
|
+
* connect spawned the pipe on this channel. Subsequent subscribers share the
|
|
101
|
+
* same pipe instance.
|
|
102
|
+
*/
|
|
103
|
+
export interface StreamPipeArgs<TEvent> {
|
|
104
|
+
/** Emit an event to all current subscribers of this channel. */
|
|
105
|
+
send: (event: TEvent, options?: EmitOptions) => void;
|
|
106
|
+
/**
|
|
107
|
+
* Mark the pipe as long-running. If `pipe` returns **without** calling
|
|
108
|
+
* `keepOpen()`, the framework closes this channel (cleanup + notify subs).
|
|
109
|
+
* If `keepOpen()` is called, the pipe stays alive until the abort signal
|
|
110
|
+
* fires (last subscriber disconnected, or external `stream.close`).
|
|
111
|
+
*/
|
|
112
|
+
keepOpen: () => void;
|
|
113
|
+
/** Close this channel imperatively (fires notify + schedules cleanup). */
|
|
114
|
+
close: () => void;
|
|
115
|
+
/** Fires when the last subscriber of this channel disconnects. */
|
|
116
|
+
abortSignal: AbortSignal;
|
|
117
|
+
/** Hono Context from the first subscriber's request. */
|
|
118
|
+
c: Context;
|
|
119
|
+
/** URL params from the first subscriber's request. */
|
|
120
|
+
params: Record<string, string>;
|
|
121
|
+
/** Query params from the first subscriber's request. */
|
|
122
|
+
query: Record<string, string>;
|
|
123
|
+
/** The channel key this pipe is running for. Empty string for broadcast streams. */
|
|
124
|
+
channel: string;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A `pipe` handler — per-channel producer with unified vocabulary.
|
|
128
|
+
* Mutually exclusive with `source` and `perChannelSource`.
|
|
129
|
+
*/
|
|
130
|
+
export type StreamPipeFn<TEvent> = (args: StreamPipeArgs<TEvent>) => void | Promise<void>;
|
|
92
131
|
/**
|
|
93
132
|
* Async-capable channel resolver. Returns a channel ID, or `null`/`undefined`
|
|
94
133
|
* to reject the connection (framework responds 403).
|
|
@@ -172,9 +211,21 @@ export interface EventStreamConfig<TEvent, TSnapshot = TEvent> {
|
|
|
172
211
|
* external pub/sub subscriptions). The `emit` provided is bound to the current
|
|
173
212
|
* channel — call it with just the value.
|
|
174
213
|
*
|
|
175
|
-
* Mutually exclusive with `source`.
|
|
214
|
+
* Mutually exclusive with `source` and `pipe`.
|
|
176
215
|
*/
|
|
177
216
|
perChannelSource?: PerChannelSourceFn<TEvent>;
|
|
217
|
+
/**
|
|
218
|
+
* Per-channel producer with the unified `send / keepOpen / abortSignal`
|
|
219
|
+
* vocabulary shared with `socket_*`. Invoked when the first subscriber of a
|
|
220
|
+
* channel connects. Aborted when the last subscriber of that channel
|
|
221
|
+
* disconnects.
|
|
222
|
+
*
|
|
223
|
+
* Prefer `pipe` for new code. `source` and `perChannelSource` remain for
|
|
224
|
+
* backwards compatibility.
|
|
225
|
+
*
|
|
226
|
+
* Mutually exclusive with `source` and `perChannelSource`.
|
|
227
|
+
*/
|
|
228
|
+
pipe?: StreamPipeFn<TEvent>;
|
|
178
229
|
/**
|
|
179
230
|
* Time (ms) the per-channel snapshot buffer is retained after `close()`.
|
|
180
231
|
* Reconnecting clients within this window still see the final state.
|
|
@@ -217,8 +268,12 @@ export interface EventStream<TEvent, TSnapshot = TEvent> {
|
|
|
217
268
|
readonly __buffers: Map<string, TEvent[]>;
|
|
218
269
|
/** Internal: assigned URL path (set by standalone or by stream_* discovery) */
|
|
219
270
|
__path: string | undefined;
|
|
220
|
-
/** Internal: source lifecycle hook */
|
|
221
|
-
__onConnect(channelKey: string, listener: StreamListener<TEvent
|
|
271
|
+
/** Internal: source lifecycle hook. `ctx` is used to seed `pipe` when it spawns. */
|
|
272
|
+
__onConnect(channelKey: string, listener: StreamListener<TEvent>, ctx?: {
|
|
273
|
+
c: Context;
|
|
274
|
+
params: Record<string, string>;
|
|
275
|
+
query: Record<string, string>;
|
|
276
|
+
}): void;
|
|
222
277
|
/** Internal: source lifecycle hook */
|
|
223
278
|
__onDisconnect(channelKey: string, listener: StreamListener<TEvent>): void;
|
|
224
279
|
/** @internal Test-only: reset all transient state (buffers, listeners, controllers, timers). */
|
package/dist/events.js
CHANGED
|
@@ -1,130 +1,165 @@
|
|
|
1
|
-
import { getAppContext as
|
|
2
|
-
const
|
|
3
|
-
function
|
|
4
|
-
const
|
|
5
|
-
for (const
|
|
1
|
+
import { getAppContext as R } from "./app-context.js";
|
|
2
|
+
const D = 2e4, N = 300 * 1e3, M = "";
|
|
3
|
+
function F(r) {
|
|
4
|
+
const o = R().pendingStreamRoutes.splice(0);
|
|
5
|
+
for (const a of o) a(r);
|
|
6
6
|
}
|
|
7
|
-
const
|
|
8
|
-
function
|
|
9
|
-
return
|
|
7
|
+
const q = /* @__PURE__ */ new Set();
|
|
8
|
+
function O() {
|
|
9
|
+
return q;
|
|
10
10
|
}
|
|
11
|
-
function
|
|
12
|
-
if (
|
|
11
|
+
function x(r = {}) {
|
|
12
|
+
if (r.broadcast && r.channel && console.warn(
|
|
13
13
|
"[velojs] createEventStream: `broadcast: true` ignores `channel`. Remove one of them — broadcast streams have no channels."
|
|
14
|
-
),
|
|
14
|
+
), (r.source ? 1 : 0) + (r.perChannelSource ? 1 : 0) + (r.pipe ? 1 : 0) > 1)
|
|
15
15
|
throw new Error(
|
|
16
|
-
"[velojs] createEventStream: `source` and `
|
|
16
|
+
"[velojs] createEventStream: `source`, `perChannelSource`, and `pipe` are mutually exclusive — pick one. `source` is stream-wide, `perChannelSource` and `pipe` are per-channel (prefer `pipe` for new code)."
|
|
17
17
|
);
|
|
18
|
-
const
|
|
19
|
-
let
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
const o = /* @__PURE__ */ new Map(), a = /* @__PURE__ */ new Map(), p = /* @__PURE__ */ new Set(), l = /* @__PURE__ */ new Map();
|
|
19
|
+
let f = null, i = 0;
|
|
20
|
+
const h = /* @__PURE__ */ new Map(), S = /* @__PURE__ */ new Map(), d = () => {
|
|
21
|
+
r.source && (f || (f = new AbortController(), Promise.resolve(
|
|
22
|
+
r.source(v, { abortSignal: f.signal })
|
|
23
23
|
).catch((e) => {
|
|
24
24
|
console.error("[velojs] event stream source threw:", e);
|
|
25
25
|
})));
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
},
|
|
29
|
-
if (!
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
26
|
+
}, m = () => {
|
|
27
|
+
i > 0 || f && (f.abort(), f = null);
|
|
28
|
+
}, C = (e) => {
|
|
29
|
+
if (!r.perChannelSource || h.has(e)) return;
|
|
30
|
+
const t = new AbortController();
|
|
31
|
+
h.set(e, t);
|
|
32
|
+
const s = (n, u) => {
|
|
33
|
+
c.emit(e, n, u);
|
|
34
34
|
};
|
|
35
35
|
Promise.resolve(
|
|
36
|
-
|
|
37
|
-
).catch((
|
|
36
|
+
r.perChannelSource(e, s, { abortSignal: t.signal })
|
|
37
|
+
).catch((n) => {
|
|
38
38
|
console.error(
|
|
39
39
|
`[velojs] perChannelSource threw for channel "${e}":`,
|
|
40
|
-
|
|
40
|
+
n
|
|
41
41
|
);
|
|
42
42
|
});
|
|
43
|
-
},
|
|
44
|
-
const
|
|
45
|
-
|
|
43
|
+
}, P = (e) => {
|
|
44
|
+
const t = h.get(e);
|
|
45
|
+
t && (t.abort(), h.delete(e));
|
|
46
|
+
}, w = (e, t) => {
|
|
47
|
+
if (!r.pipe || S.has(e) || !t)
|
|
48
|
+
return;
|
|
49
|
+
const s = new AbortController();
|
|
50
|
+
S.set(e, s);
|
|
51
|
+
let n = !1;
|
|
52
|
+
const u = (g, _) => {
|
|
53
|
+
c.emit(e, g, _);
|
|
54
|
+
}, A = () => c.close(e), j = () => {
|
|
55
|
+
n = !0;
|
|
56
|
+
};
|
|
57
|
+
Promise.resolve(
|
|
58
|
+
r.pipe({
|
|
59
|
+
send: u,
|
|
60
|
+
keepOpen: j,
|
|
61
|
+
close: A,
|
|
62
|
+
abortSignal: s.signal,
|
|
63
|
+
c: t.c,
|
|
64
|
+
params: t.params,
|
|
65
|
+
query: t.query,
|
|
66
|
+
channel: e
|
|
67
|
+
})
|
|
68
|
+
).then(() => {
|
|
69
|
+
s.signal.aborted || n || c.close(e);
|
|
70
|
+
}).catch((g) => {
|
|
71
|
+
console.error(
|
|
72
|
+
`[velojs] stream pipe threw for channel "${e}":`,
|
|
73
|
+
g
|
|
74
|
+
), s.signal.aborted || c.close(e);
|
|
75
|
+
});
|
|
76
|
+
}, E = (e) => {
|
|
77
|
+
const t = S.get(e);
|
|
78
|
+
t && (t.abort(), S.delete(e));
|
|
46
79
|
};
|
|
47
|
-
function
|
|
48
|
-
let
|
|
49
|
-
if (!!
|
|
80
|
+
function v(e, t, s) {
|
|
81
|
+
let n, u, A;
|
|
82
|
+
if (!!r.broadcast ? (n = M, u = e, A = t) : (n = e, u = t, A = s), p.has(n)) {
|
|
50
83
|
console.warn(
|
|
51
|
-
`[velojs] emit() called on closed channel "${
|
|
84
|
+
`[velojs] emit() called on closed channel "${n}" — ignored. Reopen the channel by emitting to a fresh ID, or remove the close() call.`
|
|
52
85
|
);
|
|
53
86
|
return;
|
|
54
87
|
}
|
|
55
|
-
if (
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
88
|
+
if (A?.snapshot) {
|
|
89
|
+
const _ = a.get(n) ?? [];
|
|
90
|
+
_.push(u);
|
|
91
|
+
const I = r.bufferSize ?? 1 / 0;
|
|
92
|
+
_.length > I && _.splice(0, _.length - I), a.set(n, _);
|
|
60
93
|
}
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
for (const
|
|
94
|
+
const g = o.get(n);
|
|
95
|
+
if (g)
|
|
96
|
+
for (const _ of g) _(u);
|
|
64
97
|
}
|
|
65
|
-
function
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
for (const
|
|
71
|
-
const
|
|
72
|
-
|
|
98
|
+
function T(e = M) {
|
|
99
|
+
if (p.has(e)) return;
|
|
100
|
+
p.add(e);
|
|
101
|
+
const t = o.get(e);
|
|
102
|
+
if (t)
|
|
103
|
+
for (const u of t) u.close();
|
|
104
|
+
const s = r.retainMs ?? N, n = l.get(e);
|
|
105
|
+
n && clearTimeout(n), l.set(
|
|
73
106
|
e,
|
|
74
107
|
setTimeout(() => {
|
|
75
|
-
|
|
76
|
-
},
|
|
108
|
+
a.delete(e), l.delete(e), p.delete(e);
|
|
109
|
+
}, s)
|
|
77
110
|
);
|
|
78
111
|
}
|
|
79
|
-
const
|
|
80
|
-
emit:
|
|
81
|
-
close:
|
|
112
|
+
const c = {
|
|
113
|
+
emit: v,
|
|
114
|
+
close: T,
|
|
82
115
|
__isVeloEventStream: !0,
|
|
83
|
-
__config:
|
|
84
|
-
__listeners:
|
|
85
|
-
__buffers:
|
|
86
|
-
__path:
|
|
87
|
-
__onConnect(e,
|
|
88
|
-
let
|
|
89
|
-
const
|
|
90
|
-
|
|
116
|
+
__config: r,
|
|
117
|
+
__listeners: o,
|
|
118
|
+
__buffers: a,
|
|
119
|
+
__path: r.path,
|
|
120
|
+
__onConnect(e, t, s) {
|
|
121
|
+
let n = o.get(e);
|
|
122
|
+
const u = !n || n.size === 0;
|
|
123
|
+
n || (n = /* @__PURE__ */ new Set(), o.set(e, n)), n.add(t), i++, d(), u && (C(e), w(e, s));
|
|
91
124
|
},
|
|
92
|
-
__onDisconnect(e,
|
|
93
|
-
const
|
|
94
|
-
|
|
125
|
+
__onDisconnect(e, t) {
|
|
126
|
+
const s = o.get(e);
|
|
127
|
+
s && (s.delete(t), s.size === 0 && (o.delete(e), P(e), E(e)), i--, m());
|
|
95
128
|
},
|
|
96
129
|
__reset() {
|
|
97
|
-
for (const e of
|
|
98
|
-
for (const
|
|
130
|
+
for (const e of o.values())
|
|
131
|
+
for (const t of e)
|
|
99
132
|
try {
|
|
100
|
-
|
|
133
|
+
t.close();
|
|
101
134
|
} catch {
|
|
102
135
|
}
|
|
103
|
-
|
|
104
|
-
for (const e of
|
|
105
|
-
|
|
106
|
-
for (const e of
|
|
107
|
-
|
|
136
|
+
o.clear(), a.clear(), p.clear();
|
|
137
|
+
for (const e of l.values()) clearTimeout(e);
|
|
138
|
+
l.clear(), f && (f.abort(), f = null);
|
|
139
|
+
for (const e of h.values()) e.abort();
|
|
140
|
+
h.clear();
|
|
141
|
+
for (const e of S.values()) e.abort();
|
|
142
|
+
S.clear(), i = 0;
|
|
108
143
|
}
|
|
109
144
|
};
|
|
110
|
-
return
|
|
111
|
-
|
|
112
|
-
}),
|
|
145
|
+
return r.path && R().pendingStreamRoutes.push((e) => {
|
|
146
|
+
k(e, r.path, c, r.middlewares ?? []);
|
|
147
|
+
}), q.add(c), c;
|
|
113
148
|
}
|
|
114
|
-
function
|
|
115
|
-
return async (
|
|
116
|
-
for (; !
|
|
149
|
+
function z(r) {
|
|
150
|
+
return async (b, { abortSignal: o }) => {
|
|
151
|
+
for (; !o.aborted; ) {
|
|
117
152
|
try {
|
|
118
|
-
await
|
|
119
|
-
} catch (
|
|
120
|
-
console.error("[velojs] poll tick threw:",
|
|
153
|
+
await r.tick(b);
|
|
154
|
+
} catch (a) {
|
|
155
|
+
console.error("[velojs] poll tick threw:", a);
|
|
121
156
|
}
|
|
122
|
-
await new Promise((
|
|
123
|
-
const
|
|
124
|
-
|
|
157
|
+
await new Promise((a) => {
|
|
158
|
+
const p = setTimeout(a, r.intervalMs);
|
|
159
|
+
o.addEventListener(
|
|
125
160
|
"abort",
|
|
126
161
|
() => {
|
|
127
|
-
clearTimeout(
|
|
162
|
+
clearTimeout(p), a();
|
|
128
163
|
},
|
|
129
164
|
{ once: !0 }
|
|
130
165
|
);
|
|
@@ -132,70 +167,74 @@ function B(t) {
|
|
|
132
167
|
}
|
|
133
168
|
};
|
|
134
169
|
}
|
|
135
|
-
function
|
|
136
|
-
return
|
|
170
|
+
function L(r) {
|
|
171
|
+
return r.req.query("channel") ?? M;
|
|
137
172
|
}
|
|
138
|
-
function
|
|
139
|
-
const
|
|
140
|
-
const { streamSSE:
|
|
173
|
+
function k(r, b, o, a = []) {
|
|
174
|
+
const p = async (l) => {
|
|
175
|
+
const { streamSSE: f } = await import("hono/streaming");
|
|
141
176
|
let i;
|
|
142
|
-
if (
|
|
143
|
-
i =
|
|
177
|
+
if (o.__config.broadcast)
|
|
178
|
+
i = M;
|
|
144
179
|
else {
|
|
145
|
-
const d =
|
|
146
|
-
let
|
|
180
|
+
const d = o.__config.channel ?? L;
|
|
181
|
+
let m;
|
|
147
182
|
try {
|
|
148
|
-
|
|
149
|
-
} catch (
|
|
150
|
-
return console.error("[velojs] channel resolver threw:",
|
|
183
|
+
m = await d(l);
|
|
184
|
+
} catch (C) {
|
|
185
|
+
return console.error("[velojs] channel resolver threw:", C), l.json({ error: "internal" }, 500);
|
|
151
186
|
}
|
|
152
|
-
if (
|
|
153
|
-
return
|
|
154
|
-
i =
|
|
187
|
+
if (m == null)
|
|
188
|
+
return l.json({ error: "forbidden" }, 403);
|
|
189
|
+
i = m;
|
|
155
190
|
}
|
|
156
|
-
const
|
|
157
|
-
return
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
191
|
+
const h = o.__config.heartbeatMs, S = h === !1 ? 0 : h ?? D;
|
|
192
|
+
return f(l, async (d) => {
|
|
193
|
+
const m = o.__buffers.get(i);
|
|
194
|
+
if (m && m.length > 0 && await d.writeSSE({
|
|
160
195
|
event: "snapshot",
|
|
161
|
-
data: JSON.stringify(
|
|
162
|
-
}),
|
|
163
|
-
const
|
|
164
|
-
i ===
|
|
196
|
+
data: JSON.stringify(m)
|
|
197
|
+
}), o.__config.snapshot) {
|
|
198
|
+
const t = await o.__config.snapshot(
|
|
199
|
+
i === M ? void 0 : i
|
|
165
200
|
);
|
|
166
|
-
|
|
201
|
+
t !== void 0 && await d.writeSSE({
|
|
167
202
|
event: "snapshot",
|
|
168
|
-
data: JSON.stringify(
|
|
203
|
+
data: JSON.stringify(t)
|
|
169
204
|
});
|
|
170
205
|
}
|
|
171
|
-
let
|
|
172
|
-
const
|
|
173
|
-
|
|
206
|
+
let C;
|
|
207
|
+
const P = new Promise((t) => {
|
|
208
|
+
C = t;
|
|
174
209
|
});
|
|
175
|
-
let
|
|
176
|
-
const
|
|
177
|
-
}),
|
|
178
|
-
|
|
179
|
-
},
|
|
180
|
-
|
|
181
|
-
d.close(),
|
|
210
|
+
let w = !1, E = null, v = Promise.resolve();
|
|
211
|
+
const T = (t) => (v = v.then(() => d.writeSSE(t)).catch(() => {
|
|
212
|
+
}), v), c = () => {
|
|
213
|
+
w || (w = !0, E && clearInterval(E), o.__onDisconnect(i, e), C());
|
|
214
|
+
}, e = ((t) => {
|
|
215
|
+
w || (T({ data: JSON.stringify(t) }), o.__config.closeOn?.(t) && v.then(() => {
|
|
216
|
+
d.close(), c();
|
|
182
217
|
}));
|
|
183
218
|
});
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
d.close(),
|
|
219
|
+
e.close = () => {
|
|
220
|
+
w || (T({ event: "close", data: "" }), v.then(() => {
|
|
221
|
+
d.close(), c();
|
|
187
222
|
}));
|
|
188
|
-
},
|
|
189
|
-
|
|
190
|
-
|
|
223
|
+
}, o.__onConnect(i, e, {
|
|
224
|
+
c: l,
|
|
225
|
+
params: l.req.param(),
|
|
226
|
+
query: l.req.query()
|
|
227
|
+
}), E = S > 0 ? setInterval(() => {
|
|
228
|
+
w || T({ event: "heartbeat", data: "" });
|
|
229
|
+
}, S) : null, d.onAbort(c), await P;
|
|
191
230
|
});
|
|
192
231
|
};
|
|
193
|
-
|
|
232
|
+
a.length > 0 ? r.on(["GET"], [b], ...a, p) : r.on(["GET"], [b], p);
|
|
194
233
|
}
|
|
195
234
|
export {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
235
|
+
x as createEventStream,
|
|
236
|
+
F as flushPendingStreamRoutes,
|
|
237
|
+
O as getRegisteredStreams,
|
|
238
|
+
z as poll,
|
|
239
|
+
k as registerStreamHandler
|
|
201
240
|
};
|
package/dist/hooks.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Signal } from "@preact/signals";
|
|
2
2
|
import type { EventStream } from "./events.js";
|
|
3
|
+
import type { SocketHandler, SocketStub } from "./sockets.js";
|
|
3
4
|
export declare const __veloUpdatePending: Signal<boolean>;
|
|
4
5
|
/**
|
|
5
6
|
* Força o signal a notificar mudanças após mutação de propriedades aninhadas
|
|
@@ -149,3 +150,58 @@ export declare function useEventStream<TEvent, TSnapshot = TEvent>(stream: Event
|
|
|
149
150
|
closed: Signal<boolean>;
|
|
150
151
|
error: Signal<Error | null>;
|
|
151
152
|
};
|
|
153
|
+
type SocketStatus = "connecting" | "open" | "closed";
|
|
154
|
+
export interface UseSocketOptions {
|
|
155
|
+
/** Optional channel; appended as `?channel=...` to the socket URL. */
|
|
156
|
+
channel?: string;
|
|
157
|
+
/** Called for every incoming message (text frame = string, binary frame = `Uint8Array`). */
|
|
158
|
+
onMessage?: (msg: string | Uint8Array) => void;
|
|
159
|
+
/** Called on successful connection. */
|
|
160
|
+
onOpen?: () => void;
|
|
161
|
+
/** Called on close (client or server). */
|
|
162
|
+
onClose?: (evt: CloseEvent) => void;
|
|
163
|
+
/** Called on socket error. */
|
|
164
|
+
onError?: (err: Event) => void;
|
|
165
|
+
/** When false, the connection is not opened. Flip to true to connect later. Default: true. */
|
|
166
|
+
enabled?: boolean;
|
|
167
|
+
}
|
|
168
|
+
export interface UseSocketApi {
|
|
169
|
+
/**
|
|
170
|
+
* Send a message. Strings and `Uint8Array` are sent as-is;
|
|
171
|
+
* objects are auto-serialized with `JSON.stringify`.
|
|
172
|
+
*
|
|
173
|
+
* Buffered if called before the socket is open — dropped once closed.
|
|
174
|
+
*/
|
|
175
|
+
send: (msg: string | Uint8Array | object) => void;
|
|
176
|
+
/** Close the socket (client-initiated). */
|
|
177
|
+
close: (code?: number, reason?: string) => void;
|
|
178
|
+
/** `"connecting" | "open" | "closed"`. Reactive. */
|
|
179
|
+
status: Signal<SocketStatus>;
|
|
180
|
+
/** Last received message. Reactive. */
|
|
181
|
+
lastMessage: Signal<string | Uint8Array | null>;
|
|
182
|
+
/** Last socket error, if any. Reactive. */
|
|
183
|
+
error: Signal<Event | null>;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Subscribes to a WebSocket declared via the `socket_*` convention.
|
|
187
|
+
*
|
|
188
|
+
* The hook opens the connection on mount (when `enabled !== false`) and closes
|
|
189
|
+
* it on unmount or when `channel` / `stub` / `enabled` change.
|
|
190
|
+
*
|
|
191
|
+
* **No auto-reconnect.** If the server closes the socket, `status.value`
|
|
192
|
+
* becomes `"closed"` and stays that way — user flow decides whether to
|
|
193
|
+
* reconnect (typically by re-mounting or toggling `enabled`).
|
|
194
|
+
*
|
|
195
|
+
* ```tsx
|
|
196
|
+
* const { send, status, lastMessage } = useSocket(socket_terminal, {
|
|
197
|
+
* channel: workerId,
|
|
198
|
+
* onMessage: (msg) => { ... },
|
|
199
|
+
* });
|
|
200
|
+
*
|
|
201
|
+
* send({ type: "resize", cols: 80, rows: 24 });
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
export declare function useSocket(stub: SocketHandler | SocketStub | {
|
|
205
|
+
__path: string;
|
|
206
|
+
}, options?: UseSocketOptions): UseSocketApi;
|
|
207
|
+
export {};
|