@mauroandre/velojs 0.0.26 → 0.0.27

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
@@ -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/17-endpoints.md](./site/docs/17-endpoints.md).
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/16-testing.md) for the full guide with patterns, isolation, and FAQ.
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>): void;
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 A } from "./app-context.js";
2
- const M = 2e4, R = 300 * 1e3, m = "";
3
- function x(t) {
4
- const n = A().pendingStreamRoutes.splice(0);
5
- for (const l of n) l(t);
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 I = /* @__PURE__ */ new Set();
8
- function y() {
9
- return I;
7
+ const q = /* @__PURE__ */ new Set();
8
+ function O() {
9
+ return q;
10
10
  }
11
- function L(t = {}) {
12
- if (t.broadcast && t.channel && console.warn(
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
- ), t.source && t.perChannelSource)
14
+ ), (r.source ? 1 : 0) + (r.perChannelSource ? 1 : 0) + (r.pipe ? 1 : 0) > 1)
15
15
  throw new Error(
16
- "[velojs] createEventStream: `source` and `perChannelSource` are mutually exclusive. Use `source` for stream-wide producers or `perChannelSource` for per-channel resources, not both."
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 a = /* @__PURE__ */ new Map(), n = /* @__PURE__ */ new Map(), l = /* @__PURE__ */ new Set(), h = /* @__PURE__ */ new Map();
19
- let c = null, w = 0;
20
- const i = /* @__PURE__ */ new Map(), b = () => {
21
- t.source && (c || (c = new AbortController(), Promise.resolve(
22
- t.source(_, { abortSignal: c.signal })
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
- }, g = () => {
27
- w > 0 || c && (c.abort(), c = null);
28
- }, d = (e) => {
29
- if (!t.perChannelSource || i.has(e)) return;
30
- const r = new AbortController();
31
- i.set(e, r);
32
- const o = (s, f) => {
33
- p.emit(e, s, f);
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
- t.perChannelSource(e, o, { abortSignal: r.signal })
37
- ).catch((s) => {
36
+ r.perChannelSource(e, s, { abortSignal: t.signal })
37
+ ).catch((n) => {
38
38
  console.error(
39
39
  `[velojs] perChannelSource threw for channel "${e}":`,
40
- s
40
+ n
41
41
  );
42
42
  });
43
- }, S = (e) => {
44
- const r = i.get(e);
45
- r && (r.abort(), i.delete(e));
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 _(e, r, o) {
48
- let s, f, u;
49
- if (!!t.broadcast ? (s = m, f = e, u = r) : (s = e, f = r, u = o), l.has(s)) {
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 "${s}" — ignored. Reopen the channel by emitting to a fresh ID, or remove the close() call.`
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 (u?.snapshot) {
56
- const v = n.get(s) ?? [];
57
- v.push(f);
58
- const T = t.bufferSize ?? 1 / 0;
59
- v.length > T && v.splice(0, v.length - T), n.set(s, v);
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 E = a.get(s);
62
- if (E)
63
- for (const v of E) v(f);
94
+ const g = o.get(n);
95
+ if (g)
96
+ for (const _ of g) _(u);
64
97
  }
65
- function C(e = m) {
66
- if (l.has(e)) return;
67
- l.add(e);
68
- const r = a.get(e);
69
- if (r)
70
- for (const f of r) f.close();
71
- const o = t.retainMs ?? R, s = h.get(e);
72
- s && clearTimeout(s), h.set(
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
- n.delete(e), h.delete(e), l.delete(e);
76
- }, o)
108
+ a.delete(e), l.delete(e), p.delete(e);
109
+ }, s)
77
110
  );
78
111
  }
79
- const p = {
80
- emit: _,
81
- close: C,
112
+ const c = {
113
+ emit: v,
114
+ close: T,
82
115
  __isVeloEventStream: !0,
83
- __config: t,
84
- __listeners: a,
85
- __buffers: n,
86
- __path: t.path,
87
- __onConnect(e, r) {
88
- let o = a.get(e);
89
- const s = !o || o.size === 0;
90
- o || (o = /* @__PURE__ */ new Set(), a.set(e, o)), o.add(r), w++, b(), s && d(e);
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, r) {
93
- const o = a.get(e);
94
- o && (o.delete(r), o.size === 0 && (a.delete(e), S(e)), w--, g());
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 a.values())
98
- for (const r of e)
130
+ for (const e of o.values())
131
+ for (const t of e)
99
132
  try {
100
- r.close();
133
+ t.close();
101
134
  } catch {
102
135
  }
103
- a.clear(), n.clear(), l.clear();
104
- for (const e of h.values()) clearTimeout(e);
105
- h.clear(), c && (c.abort(), c = null);
106
- for (const e of i.values()) e.abort();
107
- i.clear(), w = 0;
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 t.path && A().pendingStreamRoutes.push((e) => {
111
- P(e, t.path, p, t.middlewares ?? []);
112
- }), I.add(p), p;
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 B(t) {
115
- return async (a, { abortSignal: n }) => {
116
- for (; !n.aborted; ) {
149
+ function z(r) {
150
+ return async (b, { abortSignal: o }) => {
151
+ for (; !o.aborted; ) {
117
152
  try {
118
- await t.tick(a);
119
- } catch (l) {
120
- console.error("[velojs] poll tick threw:", l);
153
+ await r.tick(b);
154
+ } catch (a) {
155
+ console.error("[velojs] poll tick threw:", a);
121
156
  }
122
- await new Promise((l) => {
123
- const h = setTimeout(l, t.intervalMs);
124
- n.addEventListener(
157
+ await new Promise((a) => {
158
+ const p = setTimeout(a, r.intervalMs);
159
+ o.addEventListener(
125
160
  "abort",
126
161
  () => {
127
- clearTimeout(h), l();
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 j(t) {
136
- return t.req.query("channel") ?? m;
170
+ function L(r) {
171
+ return r.req.query("channel") ?? M;
137
172
  }
138
- function P(t, a, n, l = []) {
139
- const h = async (c) => {
140
- const { streamSSE: w } = await import("hono/streaming");
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 (n.__config.broadcast)
143
- i = m;
177
+ if (o.__config.broadcast)
178
+ i = M;
144
179
  else {
145
- const d = n.__config.channel ?? j;
146
- let S;
180
+ const d = o.__config.channel ?? L;
181
+ let m;
147
182
  try {
148
- S = await d(c);
149
- } catch (_) {
150
- return console.error("[velojs] channel resolver threw:", _), c.json({ error: "internal" }, 500);
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 (S == null)
153
- return c.json({ error: "forbidden" }, 403);
154
- i = S;
187
+ if (m == null)
188
+ return l.json({ error: "forbidden" }, 403);
189
+ i = m;
155
190
  }
156
- const b = n.__config.heartbeatMs, g = b === !1 ? 0 : b ?? M;
157
- return w(c, async (d) => {
158
- const S = n.__buffers.get(i);
159
- if (S && S.length > 0 && await d.writeSSE({
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(S)
162
- }), n.__config.snapshot) {
163
- const u = await n.__config.snapshot(
164
- i === m ? void 0 : 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
- u !== void 0 && await d.writeSSE({
201
+ t !== void 0 && await d.writeSSE({
167
202
  event: "snapshot",
168
- data: JSON.stringify(u)
203
+ data: JSON.stringify(t)
169
204
  });
170
205
  }
171
- let _;
172
- const C = new Promise((u) => {
173
- _ = u;
206
+ let C;
207
+ const P = new Promise((t) => {
208
+ C = t;
174
209
  });
175
- let p = !1, e = null, r = Promise.resolve();
176
- const o = (u) => (r = r.then(() => d.writeSSE(u)).catch(() => {
177
- }), r), s = () => {
178
- p || (p = !0, e && clearInterval(e), n.__onDisconnect(i, f), _());
179
- }, f = ((u) => {
180
- p || (o({ data: JSON.stringify(u) }), n.__config.closeOn?.(u) && r.then(() => {
181
- d.close(), s();
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
- f.close = () => {
185
- p || (o({ event: "close", data: "" }), r.then(() => {
186
- d.close(), s();
219
+ e.close = () => {
220
+ w || (T({ event: "close", data: "" }), v.then(() => {
221
+ d.close(), c();
187
222
  }));
188
- }, n.__onConnect(i, f), e = g > 0 ? setInterval(() => {
189
- p || o({ event: "heartbeat", data: "" });
190
- }, g) : null, d.onAbort(s), await C;
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
- l.length > 0 ? t.on(["GET"], [a], ...l, h) : t.on(["GET"], [a], h);
232
+ a.length > 0 ? r.on(["GET"], [b], ...a, p) : r.on(["GET"], [b], p);
194
233
  }
195
234
  export {
196
- L as createEventStream,
197
- x as flushPendingStreamRoutes,
198
- y as getRegisteredStreams,
199
- B as poll,
200
- P as registerStreamHandler
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 { 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: SocketStub | {
205
+ __path: string;
206
+ }, options?: UseSocketOptions): UseSocketApi;
207
+ export {};