@mauroandre/velojs 0.0.56 → 0.0.57

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.
@@ -3,11 +3,18 @@
3
3
  *
4
4
  * After hydration, every rendered module that declares `channels` opens one SSE
5
5
  * connection per (module, channel) at `/_channel/{moduleId}/{channel}`. The
6
- * snapshot the connection receives — the connect snapshot and each emit's —
7
- * replaces the value of the module's entry in the loader store, so the page
8
- * stays faithful to server state without a line of code from the developer:
9
- * `useLoader()`/`Loader()` handles keep the same signal instance and the
10
- * reading component re-renders on its own.
6
+ * connection declares the route params the module's path declares, extracted
7
+ * from the current URL and serialized in `?_route=` — the client declares the
8
+ * address it is already seeing, the server validates it against the declared
9
+ * path and decides the partition. The snapshot the connection receives — the
10
+ * connect snapshot and each emit's — replaces the value of the module's entry
11
+ * in the loader store, so the page stays faithful to server state without a
12
+ * line of code from the developer: `useLoader()`/`Loader()` handles keep the
13
+ * same signal instance and the reading component re-renders on its own.
14
+ *
15
+ * The connection is keyed by those params, not by the pathname: navigating
16
+ * between children of the same params keeps the connection; a move to another
17
+ * resource closes it and opens a new one.
11
18
  *
12
19
  * A `slice` event is the other delivery mode: the producer already holds the
13
20
  * value, so the payload carries only the changed keys. A slice is merged
@@ -68,19 +75,45 @@ export declare function loaderFreshness(): Signal<Freshness>;
68
75
  * closer — the channel lives exactly as long as the module that declares it is
69
76
  * mounted.
70
77
  *
78
+ * `routeParams` are the params the module's route declares, extracted from the
79
+ * current URL: the client **declares** the address it is already seeing (the
80
+ * same information `?_data=1` carries on every SPA navigation), serialized in
81
+ * the `?_route=` query param of the connection. It is not free input — the
82
+ * server validates the keys against the module's path and rejects what the path
83
+ * does not declare.
84
+ *
71
85
  * Inert (no connection, no freshness change) in a static build: there is no
72
86
  * server to speak SSE.
73
87
  */
74
- export declare function connectChannel(moduleId: string, channel: string): () => void;
88
+ export declare function connectChannel(moduleId: string, channel: string, routeParams?: Record<string, string> | undefined): () => void;
89
+ /**
90
+ * The params the module's route declares, extracted from a pathname. The
91
+ * module's `fullPath` matches as a **prefix** of the pathname: a layout
92
+ * declared at `/projeto/:id` covers `/projeto/7/sala` and extracts `id=7` —
93
+ * the child's suffix takes no part in the extraction. A path with a catch-all
94
+ * (`/docs/*`) declares no `:` key and yields the empty object, so the channel
95
+ * travels by principal.
96
+ */
97
+ export declare function channelRouteParams(fullPath: string | undefined, pathname: string): Record<string, string>;
75
98
  /**
76
99
  * Mounts the live channels of one route module. It renders its children
77
100
  * unchanged (no DOM node, so SSR output and hydration are untouched) and only
78
101
  * exists to tie the connection lifecycle to the module's own mount/unmount —
79
102
  * SPA navigation and page close close the connections.
103
+ *
104
+ * The connection is **keyed by the params the module's route declares**, not
105
+ * by the pathname: navigating between children of the same params
106
+ * (`/projeto/7` → `/projeto/7/sala`) does not reconnect (the child's suffix is
107
+ * not part of the connection's identity), while a move to another resource
108
+ * (`/projeto/7` → `/projeto/9`) closes the old connection and opens a new one —
109
+ * with a fresh snapshot on connect, mirroring the loader store, which refetches
110
+ * when its own route's params move.
80
111
  */
81
- export declare function ChannelBoundary({ moduleId, channels, children, }: {
112
+ export declare function ChannelBoundary({ moduleId, channels, fullPath, children, }: {
82
113
  moduleId?: string | undefined;
83
114
  channels?: readonly string[] | undefined;
115
+ /** The module's declared route path (`metadata.fullPath`) — its `:params`. */
116
+ fullPath?: string | undefined;
84
117
  children?: ComponentChildren;
85
118
  }): ComponentChildren;
86
119
  /** Test-only. Drops every connection and resets every freshness signal. */
@@ -1,33 +1,34 @@
1
- import { signal as h } from "@preact/signals";
2
- import { useEffect as S } from "preact/hooks";
3
- import { loaderEntry as v } from "./loader-store.js";
4
- const E = 6e4, _ = "snapshot", g = "slice", a = /* @__PURE__ */ new Set(), u = h("live"), d = /* @__PURE__ */ new Map();
5
- function b(e) {
6
- let n = d.get(e);
7
- return n || (n = h({}), d.set(e, n)), n;
1
+ import { signal as v } from "@preact/signals";
2
+ import { useEffect as m } from "preact/hooks";
3
+ import { useLocation as g } from "wouter-preact";
4
+ import { extractParams as _, loaderEntry as S } from "./loader-store.js";
5
+ const L = 6e4, C = "snapshot", w = "slice", f = /* @__PURE__ */ new Set(), p = v("live"), y = /* @__PURE__ */ new Map();
6
+ function R(e) {
7
+ let n = y.get(e);
8
+ return n || (n = v({}), y.set(e, n)), n;
8
9
  }
9
- function O() {
10
- return u;
10
+ function V() {
11
+ return p;
11
12
  }
12
- const f = /* @__PURE__ */ new Map();
13
- let p = !1;
14
- function m(e, n) {
15
- let t = f.get(e);
16
- t || (t = [], f.set(e, t)), t.push(n), !p && (p = !0, queueMicrotask(y));
13
+ const u = /* @__PURE__ */ new Map();
14
+ let h = !1;
15
+ function N(e, n) {
16
+ let o = u.get(e);
17
+ o || (o = [], u.set(e, o)), o.push(n), !h && (h = !0, queueMicrotask(E));
17
18
  }
18
- function y() {
19
- if (p = !1, f.size === 0) return;
20
- const e = [...f];
21
- f.clear();
22
- for (const [n, t] of e) {
23
- const r = v(n);
24
- let s = r.value;
25
- for (const o of t)
26
- s = { ...s !== null && typeof s == "object" && !Array.isArray(s) ? s : {}, ...o };
27
- r.value = s;
19
+ function E() {
20
+ if (h = !1, u.size === 0) return;
21
+ const e = [...u];
22
+ u.clear();
23
+ for (const [n, o] of e) {
24
+ const s = S(n);
25
+ let t = s.value;
26
+ for (const l of o)
27
+ t = { ...t !== null && typeof t == "object" && !Array.isArray(t) ? t : {}, ...l };
28
+ s.value = t;
28
29
  }
29
30
  }
30
- function L(e) {
31
+ function T(e) {
31
32
  if (typeof e != "string" || e.length === 0) return null;
32
33
  try {
33
34
  const n = JSON.parse(e);
@@ -36,96 +37,105 @@ function L(e) {
36
37
  return null;
37
38
  }
38
39
  }
39
- function C(e) {
40
+ function O(e) {
40
41
  return e.some((n) => n.failed) ? "error" : e.some((n) => n.state === "open" && !n.silent) ? "live" : "stale";
41
42
  }
42
- function c() {
43
+ function a() {
43
44
  let e = !1, n = !1;
44
- for (const t of a)
45
- t.failed && (e = !0), t.state === "open" && !t.silent && (n = !0);
46
- e ? u.value = "error" : a.size > 0 && !n ? u.value = "stale" : u.value = "live";
47
- for (const [t, r] of d) {
48
- const s = /* @__PURE__ */ new Map();
49
- for (const l of a) {
50
- if (l.moduleId !== t) continue;
51
- const i = s.get(l.channel);
52
- i ? i.push(l) : s.set(l.channel, [l]);
45
+ for (const o of f)
46
+ o.failed && (e = !0), o.state === "open" && !o.silent && (n = !0);
47
+ e ? p.value = "error" : f.size > 0 && !n ? p.value = "stale" : p.value = "live";
48
+ for (const [o, s] of y) {
49
+ const t = /* @__PURE__ */ new Map();
50
+ for (const i of f) {
51
+ if (i.moduleId !== o) continue;
52
+ const r = t.get(i.channel);
53
+ r ? r.push(i) : t.set(i.channel, [i]);
53
54
  }
54
- const o = {};
55
- for (const [l, i] of s)
56
- o[l] = C(i);
57
- r.value = o;
55
+ const l = {};
56
+ for (const [i, r] of t)
57
+ l[i] = O(r);
58
+ s.value = l;
58
59
  }
59
60
  }
60
- function T(e, n) {
61
+ function A(e, n, o) {
61
62
  if (typeof __VELO_STATIC__ < "u" && __VELO_STATIC__) return () => {
62
63
  };
63
64
  if (typeof window > "u" || typeof EventSource > "u")
64
65
  return () => {
65
66
  };
66
- const t = {
67
+ const s = {
67
68
  state: "open",
68
69
  failed: !1,
69
70
  silent: !1,
70
71
  moduleId: e,
71
72
  channel: n
72
73
  };
73
- a.add(t), c();
74
- let r = null;
75
- const s = () => {
76
- r !== null && clearTimeout(r), r = setTimeout(() => {
77
- r = null, t.silent = !0, c();
78
- }, E);
74
+ f.add(s), a();
75
+ let t = null;
76
+ const l = () => {
77
+ t !== null && clearTimeout(t), t = setTimeout(() => {
78
+ t = null, s.silent = !0, a();
79
+ }, L);
79
80
  };
80
- s();
81
- const o = new EventSource(
82
- `/_channel/${encodeURI(e)}/${encodeURIComponent(n)}`
81
+ l();
82
+ const i = o && Object.keys(o).length > 0 ? `?_route=${encodeURIComponent(JSON.stringify(o))}` : "", r = new EventSource(
83
+ `/_channel/${encodeURI(e)}/${encodeURIComponent(n)}${i}`
83
84
  );
84
- return o.addEventListener(_, (l) => {
85
+ return r.addEventListener(C, (c) => {
85
86
  try {
86
- y(), v(e).value = JSON.parse(l.data), t.state = "open", t.failed = !1, t.silent = !1, s(), c();
87
+ E(), S(e).value = JSON.parse(c.data), s.state = "open", s.failed = !1, s.silent = !1, l(), a();
87
88
  } catch {
88
89
  }
89
- }), o.addEventListener(g, (l) => {
90
- const i = L(l.data);
91
- i && (t.state = "open", t.failed = !1, t.silent = !1, s(), c(), m(e, i));
92
- }), o.addEventListener("error", (l) => {
93
- const i = l.data;
94
- typeof i == "string" && i.length > 0 ? t.failed = !0 : t.state = o.readyState === EventSource.CLOSED ? "closed" : "reconnecting", c();
95
- }), o.addEventListener("close", () => {
96
- t.state = "closed", c();
90
+ }), r.addEventListener(w, (c) => {
91
+ const d = T(c.data);
92
+ d && (s.state = "open", s.failed = !1, s.silent = !1, l(), a(), N(e, d));
93
+ }), r.addEventListener("error", (c) => {
94
+ const d = c.data;
95
+ typeof d == "string" && d.length > 0 ? s.failed = !0 : s.state = r.readyState === EventSource.CLOSED ? "closed" : "reconnecting", a();
96
+ }), r.addEventListener("close", () => {
97
+ s.state = "closed", a();
97
98
  }), () => {
98
- a.delete(t), r !== null && clearTimeout(r);
99
+ f.delete(s), t !== null && clearTimeout(t);
99
100
  try {
100
- o.close();
101
+ r.close();
101
102
  } catch {
102
103
  }
103
- c();
104
+ a();
104
105
  };
105
106
  }
106
- function M({
107
+ function b(e, n) {
108
+ return e ? _(e, n) ?? {} : {};
109
+ }
110
+ function $({
107
111
  moduleId: e,
108
112
  channels: n,
109
- children: t
113
+ fullPath: o,
114
+ children: s
110
115
  }) {
111
- const r = n && n.length > 0 ? n.join(",") : "";
112
- return S(() => {
113
- if (!e || r === "") return;
114
- const s = r.split(",").map((o) => T(e, o));
116
+ const t = n && n.length > 0 ? n.join(",") : "";
117
+ g();
118
+ const l = t === "" || typeof window > "u" ? "" : JSON.stringify(b(o, window.location.pathname));
119
+ return m(() => {
120
+ if (!e || t === "") return;
121
+ const i = l === "" ? void 0 : JSON.parse(l), r = t.split(",").map(
122
+ (c) => A(e, c, i)
123
+ );
115
124
  return () => {
116
- for (const o of s) o();
125
+ for (const c of r) c();
117
126
  };
118
- }, [e, r]), t;
127
+ }, [e, t, l]), s;
119
128
  }
120
- function F() {
121
- a.clear(), f.clear(), p = !1, u.value = "live";
122
- for (const e of d.values()) e.value = {};
129
+ function k() {
130
+ f.clear(), u.clear(), h = !1, p.value = "live";
131
+ for (const e of y.values()) e.value = {};
123
132
  }
124
133
  export {
125
- E as CHANNEL_SILENCE_MS,
126
- M as ChannelBoundary,
127
- F as __resetLiveLoader,
128
- T as connectChannel,
129
- O as loaderFreshness,
130
- b as moduleChannelFreshness
134
+ L as CHANNEL_SILENCE_MS,
135
+ $ as ChannelBoundary,
136
+ k as __resetLiveLoader,
137
+ b as channelRouteParams,
138
+ A as connectChannel,
139
+ V as loaderFreshness,
140
+ R as moduleChannelFreshness
131
141
  };
package/dist/server.d.ts CHANGED
@@ -2,7 +2,7 @@ import { Hono } from "hono";
2
2
  import type { AppRoutes } from "./types.js";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
4
  export { emit, inspectChannels, registerChannels } from "./channels.js";
5
- export type { ChannelDefinition, ChannelMap, ChannelScopeResult, ChannelRegistryOptions, ChannelEmitModule, ChannelEmitKind, ChannelSlice, ChannelInspectGroup, ChannelInspectChannel, ChannelInspectWindow, ChannelInspectReport, } from "./channels.js";
5
+ export type { ChannelDefinition, ChannelMap, ChannelScopeContext, ChannelGroupOpenContext, ChannelGroupCloseContext, ChannelScopeResult, ChannelRegistryOptions, ChannelEmitModule, ChannelEmitKind, ChannelSlice, ChannelInspectGroup, ChannelInspectChannel, ChannelInspectWindow, ChannelInspectReport, } from "./channels.js";
6
6
  export declare const serverDataStorage: AsyncLocalStorage<Record<string, unknown>>;
7
7
  type ServerCallback = (server: import("http").Server) => void;
8
8
  export declare function onServer(fn: ServerCallback): void;
@@ -21,4 +21,4 @@
21
21
  * snapshot — no sleep, no retry.
22
22
  */
23
23
  export { createTestApp } from "./createTestApp.js";
24
- export type { TestApp, TestResponse, TestSubscription, TestChannelSubscription, ChannelEvent, ChannelModuleRef, CreateTestAppOptions, RequestOptions, BodyRequestOptions, LoaderRequestOptions, SubscribeOptions, NextOptions, MockContextOptions, Cookies, Headers, Query, Params, } from "./types.js";
24
+ export type { TestApp, TestResponse, TestSubscription, TestChannelSubscription, ChannelEvent, ChannelModuleRef, CreateTestAppOptions, RequestOptions, BodyRequestOptions, LoaderRequestOptions, SubscribeOptions, ChannelSubscribeOptions, NextOptions, MockContextOptions, Cookies, Headers, Query, Params, } from "./types.js";