gemi 0.128.0 → 0.129.0

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.
Files changed (58) hide show
  1. package/dist/{chunk-0mp9tck6.js → chunk-0ryswty9.js} +20 -20
  2. package/dist/{chunk-0mp9tck6.js.map → chunk-0ryswty9.js.map} +5 -4
  3. package/dist/{chunk-wzfvwvz0.js → chunk-jj1ppnnk.js} +2 -2
  4. package/dist/{chunk-wzfvwvz0.js.map → chunk-jj1ppnnk.js.map} +1 -1
  5. package/dist/{chunk-tchte4dv.js → chunk-sbdm4enk.js} +2 -2
  6. package/dist/{chunk-tchte4dv.js.map → chunk-sbdm4enk.js.map} +1 -1
  7. package/dist/{chunk-df0mekds.js → chunk-wmkrb1qv.js} +1 -1
  8. package/dist/{chunk-qz0da20r.js → chunk-zw5z1k84.js} +1 -1
  9. package/dist/chunks/RealtimeClient-CqGve6h8.js +545 -0
  10. package/dist/chunks/RealtimeClient-CqGve6h8.js.map +1 -0
  11. package/dist/chunks/{ThemeProvider-BIrCSrFY.js → ThemeProvider-DaX0eyqs.js} +155 -539
  12. package/dist/chunks/ThemeProvider-DaX0eyqs.js.map +1 -0
  13. package/dist/chunks/live-DIUDdLrA.js +69 -0
  14. package/dist/chunks/live-DIUDdLrA.js.map +1 -0
  15. package/dist/chunks/subscriptionStream-CULcRbx0.js +166 -0
  16. package/dist/chunks/subscriptionStream-CULcRbx0.js.map +1 -0
  17. package/dist/client/index.d.ts +1 -1
  18. package/dist/client/index.d.ts.map +1 -1
  19. package/dist/client/index.js +118 -330
  20. package/dist/client/index.js.map +1 -1
  21. package/dist/client/init.d.ts +1 -1
  22. package/dist/client/init.d.ts.map +1 -1
  23. package/dist/client/realtime/FakeSocket.d.ts +1 -1
  24. package/dist/client/realtime/FakeSocket.d.ts.map +1 -1
  25. package/dist/client/realtime/RealtimeClient.d.ts +4 -20
  26. package/dist/client/realtime/RealtimeClient.d.ts.map +1 -1
  27. package/dist/client/realtime/live.d.ts +25 -0
  28. package/dist/client/realtime/live.d.ts.map +1 -0
  29. package/dist/client/realtime/loader.d.ts +59 -0
  30. package/dist/client/realtime/loader.d.ts.map +1 -0
  31. package/dist/client/realtime/useChannelSubscription.d.ts +1 -1
  32. package/dist/client/realtime/useChannelSubscription.d.ts.map +1 -1
  33. package/dist/client/subscriptionStream.d.ts +36 -0
  34. package/dist/client/subscriptionStream.d.ts.map +1 -0
  35. package/dist/client/useChannel.d.ts +1 -13
  36. package/dist/client/useChannel.d.ts.map +1 -1
  37. package/dist/client/useQuery.d.ts +1 -1
  38. package/dist/client/useQuery.d.ts.map +1 -1
  39. package/dist/client/useSubscription.d.ts +5 -10
  40. package/dist/client/useSubscription.d.ts.map +1 -1
  41. package/dist/facades/index.js +1 -1
  42. package/dist/http/index.js +1 -1
  43. package/dist/i18n/index.js +1 -1
  44. package/dist/kernel/index.js +1 -1
  45. package/dist/server/index.js +1 -1
  46. package/dist/services/broadcast/protocol.d.ts +1 -2
  47. package/dist/services/broadcast/protocol.d.ts.map +1 -1
  48. package/dist/services/broadcast/socketHeader.d.ts +9 -0
  49. package/dist/services/broadcast/socketHeader.d.ts.map +1 -0
  50. package/dist/services/index.js +2 -2
  51. package/dist/services/index.js.map +1 -1
  52. package/dist/testing/Page.d.ts.map +1 -1
  53. package/dist/testing/index.js +3 -1
  54. package/dist/testing/index.js.map +1 -1
  55. package/package.json +1 -1
  56. package/dist/chunks/ThemeProvider-BIrCSrFY.js.map +0 -1
  57. /package/dist/{chunk-df0mekds.js.map → chunk-wmkrb1qv.js.map} +0 -0
  58. /package/dist/{chunk-qz0da20r.js.map → chunk-zw5z1k84.js.map} +0 -0
@@ -1,4 +1,4 @@
1
- import { $ as retryDelayFor, A as useLocation, B as getRealtimeClient, C as useRoute, D as I18nProvider, E as I18nContext, G as QueryConfigContext, H as SOCKET_ID_HEADER, I as RealtimeContext, J as USER_QUERY_CONFIG_KEYS, K as QueryManagerContext, L as channelKey, M as isAbsoluteUrl, O as ServerDataContext, P as Action, Q as parseRetryAfter, R as configureRealtime, S as routeDataUrl, T as useNavigate, U as toVariantKey, V as reconnectRealtime, W as applyParams$1, X as DEFAULT_STALE_TIME, Y as pickDefined, Z as isRetryableQueryError, _ as loadDictionary, a as useRouteTransition, b as setActiveLocale, c as ComponentsContext, d as loadViewModule, et as shouldRetryQuery, f as subscribeViewModules, g as recoverFromChunkLoadError, h as isChunkLoadError, i as RouteTransitionProvider, j as domainUrl, k as ServerDataProvider, l as ComponentsProvider, m as configureChunkLoadRecovery, n as useTheme, nt as Subject, o as ClientRouterContext, p as CHUNK_RELOAD_MARKER, q as QueryManagerProvider, r as m, s as ClientRouterProvider, t as ThemeProvider, tt as QueryError, u as initialViewModulesReady, v as loadDictionaryForRender, w as useSearchParams, x as readRoutePayload, y as registerDictionary, z as currentSocketId } from "../chunks/ThemeProvider-BIrCSrFY.js";
1
+ import { $ as DEFAULT_STALE_TIME, A as ServerDataContext, B as configureRealtime, C as readRoutePayload, D as useNavigate, E as useSearchParams, G as reconnectRealtime, H as followLive, I as Action, J as QueryConfigContext, K as toVariantKey, M as useLocation, N as domainUrl, O as I18nContext, P as isAbsoluteUrl, Q as pickDefined, R as RealtimeContext, S as setActiveLocale, T as useRoute, U as loadedModule, V as currentSocketId, X as QueryManagerProvider, Y as QueryManagerContext, Z as USER_QUERY_CONFIG_KEYS, _ as isChunkLoadError, a as useRouteTransition, at as Subject, b as loadDictionaryForRender, c as ClientRouterContext, d as ComponentsProvider, et as isRetryableQueryError, f as initialViewModulesReady, g as configureChunkLoadRecovery, h as CHUNK_RELOAD_MARKER, i as RouteTransitionProvider, it as QueryError, j as ServerDataProvider, k as I18nProvider, l as ClientRouterProvider, m as subscribeViewModules, n as useTheme, nt as retryDelayFor, o as useChannelSubscription, p as loadViewModule, q as applyParams$1, r as m, rt as shouldRetryQuery, s as useCoalesced, t as ThemeProvider, tt as parseRetryAfter, u as ComponentsContext, v as recoverFromChunkLoadError, w as routeDataUrl, x as registerDictionary, y as loadDictionary } from "../chunks/ThemeProvider-DaX0eyqs.js";
2
2
  import { n as RouteStateContext, r as RouteStateProvider, t as useParams } from "../chunks/useParams-BN3XXfmG.js";
3
3
  import { Fragment, StrictMode, Suspense, createContext, createElement, isValidElement, lazy, memo, use, useCallback, useContext, useDeferredValue, useEffect, useMemo, useRef, useState, useSyncExternalStore, useTransition } from "react";
4
4
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -27,180 +27,6 @@ function isPlainObject(value) {
27
27
  return value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
28
28
  }
29
29
  //#endregion
30
- //#region client/realtime/useChannelSubscription.ts
31
- var CLOSED = {
32
- status: "closed",
33
- code: null
34
- };
35
- var IDLE = {
36
- status: "idle",
37
- code: null
38
- };
39
- /**
40
- * The one subscription primitive the channel hooks share. Subscribes only in
41
- * an effect, so a server render, a static page or an island that never
42
- * mounts opens no socket; on the server it reports `closed`. Handlers may
43
- * change between renders without resubscribing.
44
- */
45
- function useChannelSubscription(pattern, params, handlers, enabled) {
46
- const active = enabled && pattern !== null;
47
- const [state, setState] = useState(active ? CLOSED : IDLE);
48
- const provided = useContext(RealtimeContext);
49
- const handlersRef = useRef(handlers);
50
- handlersRef.current = handlers;
51
- const paramsRef = useRef(params);
52
- paramsRef.current = params;
53
- useEffect(() => {
54
- if (!active || pattern === null) {
55
- setState(IDLE);
56
- return;
57
- }
58
- const socket = provided ?? getRealtimeClient();
59
- if (!socket) return;
60
- const release = socket.subscribe(pattern, paramsRef.current, {
61
- onEvent: (event, data) => handlersRef.current.onEvent?.(event, data),
62
- onResync: () => handlersRef.current.onResync?.(),
63
- onStatus: (status, code) => setState((previous) => previous.status === status && previous.code === code ? previous : {
64
- status,
65
- code
66
- })
67
- });
68
- return () => {
69
- release();
70
- setState(CLOSED);
71
- };
72
- }, [
73
- provided,
74
- pattern === null ? null : channelKey(pattern, params),
75
- active
76
- ]);
77
- if (typeof window === "undefined") return active ? CLOSED : IDLE;
78
- return state;
79
- }
80
- /**
81
- * `fn`, run at most once per `ms`: the first call starts the window, and the
82
- * calls inside it fold into the one run at its end. Stable across renders;
83
- * a pending run is dropped on unmount.
84
- */
85
- function useCoalesced(fn, ms = 150) {
86
- const fnRef = useRef(fn);
87
- fnRef.current = fn;
88
- const timer = useRef(null);
89
- useEffect(() => () => {
90
- if (timer.current) clearTimeout(timer.current);
91
- timer.current = null;
92
- }, []);
93
- const coalesced = useRef(null);
94
- coalesced.current ??= () => {
95
- if (timer.current) return;
96
- timer.current = setTimeout(() => {
97
- timer.current = null;
98
- fnRef.current();
99
- }, ms);
100
- };
101
- return coalesced.current;
102
- }
103
- //#endregion
104
- //#region client/useChannel.ts
105
- var warnedWithoutResync = /* @__PURE__ */ new Set();
106
- /**
107
- * Subscribes to a broadcast channel while the component is mounted.
108
- *
109
- * ```tsx
110
- * const { status } = useChannel("site.:siteId", { params: { siteId } }, {
111
- * on: { changed: (d) => mutate({ path: "/pages/:pageId", params: { pageId } }) },
112
- * onResync: () => mutate({ path: "/pages/:pageId", params: { pageId } }),
113
- * });
114
- * ```
115
- *
116
- * Events are hints, delivered at most once: pair `on` with `onResync`, which
117
- * runs whenever some may have been missed. `useChannelInvalidate` does both
118
- * for the common case. On the server, and until the effect runs, `status` is
119
- * `closed`.
120
- */
121
- function useChannel(pattern, options = {}, config = {}) {
122
- const configRef = useRef(config);
123
- configRef.current = config;
124
- const hasOn = config.on !== void 0 && Object.keys(config.on).length > 0;
125
- const hasResync = config.onResync !== void 0;
126
- useEffect(() => {
127
- if (process.env.NODE_ENV === "production") return;
128
- if (!hasOn || hasResync || warnedWithoutResync.has(pattern)) return;
129
- warnedWithoutResync.add(pattern);
130
- console.warn(`[gemi] useChannel("${pattern}") has \`on\` handlers but no \`onResync\`. Broadcast events are hints delivered at most once: after a reconnect or a gap the events in between are gone, so refetch in \`onResync\` (or use useChannelInvalidate).`);
131
- }, [
132
- pattern,
133
- hasOn,
134
- hasResync
135
- ]);
136
- return useChannelSubscription(pattern, options.params ?? {}, {
137
- onEvent: (event, data) => {
138
- const handler = configRef.current.on?.[event];
139
- handler?.(data);
140
- },
141
- onResync: () => configRef.current.onResync?.()
142
- }, config.enabled !== false);
143
- }
144
- /**
145
- * Refetches `paths` on any event on the channel and on every resync. Only
146
- * the variants being rendered refetch now; the rest are marked stale. Events
147
- * are coalesced: a burst within 150 ms refetches once.
148
- *
149
- * ```tsx
150
- * useChannelInvalidate("page.:pageId", { pageId }, ["/pages/:pageId", "/pages/:pageId/pictures"]);
151
- * ```
152
- */
153
- function useChannelInvalidate(pattern, params, paths, config = {}) {
154
- const { getResource } = useContext(QueryManagerContext);
155
- const routeParams = useParams();
156
- const channelParams = params ?? {};
157
- const latest = useRef({
158
- paths,
159
- routeParams,
160
- channelParams,
161
- getResource
162
- });
163
- latest.current = {
164
- paths,
165
- routeParams,
166
- channelParams,
167
- getResource
168
- };
169
- const invalidate = () => {
170
- const { paths, routeParams, channelParams, getResource } = latest.current;
171
- for (const entry of paths) {
172
- const target = typeof entry === "string" ? { path: entry } : entry;
173
- const resource = getResource(applyParams$1(target.path, {
174
- ...routeParams,
175
- ...channelParams,
176
- ...target.params
177
- }));
178
- const keys = target.search ? [toVariantKey(target.search)] : resource.variantKeys();
179
- for (const key of keys) resource.invalidate(key);
180
- }
181
- };
182
- return useChannelSubscription(pattern, channelParams, {
183
- onEvent: useCoalesced(invalidate),
184
- onResync: invalidate
185
- }, config.enabled !== false);
186
- }
187
- function resolveLiveChannel(live) {
188
- if (!live) return null;
189
- if (typeof live === "string") return {
190
- pattern: live,
191
- params: {}
192
- };
193
- if (Array.isArray(live)) return {
194
- pattern: live[0],
195
- params: live[1] ?? {}
196
- };
197
- const object = live;
198
- return {
199
- pattern: object.channel,
200
- params: object.params ?? {}
201
- };
202
- }
203
- //#endregion
204
30
  //#region client/useQuery.ts
205
31
  /**
206
32
  * A DOM event or React's synthetic one. Never a plain object, so a cached
@@ -275,18 +101,15 @@ function useFrameworkQuery(url, ...args) {
275
101
  const lazy = config.lazy;
276
102
  const configRef = useRef(config);
277
103
  configRef.current = config;
278
- const liveChannel = resolveLiveChannel(config.live);
104
+ const realtime = useContext(RealtimeContext);
105
+ const [liveOpen, setLiveOpen] = useState(false);
279
106
  const liveResourceRef = useRef(resource);
280
107
  liveResourceRef.current = resource;
281
- const refreshLive = useCallback(() => {
282
- const target = liveResourceRef.current;
283
- for (const key of target.variantKeys()) target.invalidate(key);
284
- }, []);
285
- const refreshLiveOnEvent = useCoalesced(refreshLive);
286
- const liveOpen = useChannelSubscription(liveChannel?.pattern ?? null, liveChannel?.params ?? {}, {
287
- onEvent: refreshLiveOnEvent,
288
- onResync: refreshLive
289
- }, liveChannel !== null).status === "open";
108
+ const liveKey = config.live ? JSON.stringify(config.live) : null;
109
+ useEffect(() => {
110
+ if (liveKey === null) return;
111
+ return followLive(realtime, JSON.parse(liveKey), () => liveResourceRef.current, setLiveOpen);
112
+ }, [realtime, liveKey]);
290
113
  const refreshIntervalRef = useRef(null);
291
114
  const fetchedRef = useRef(!lazy);
292
115
  const pollRef = useRef({
@@ -927,6 +750,16 @@ function withMutationErrorHelpers(error) {
927
750
  return error;
928
751
  }
929
752
  //#endregion
753
+ //#region services/broadcast/socketHeader.ts
754
+ /**
755
+ * The request header a client sends its socket id in, for `toOthers`.
756
+ *
757
+ * A module of its own, re-exported by `./protocol`: every mutation in the
758
+ * browser reads it, and anything else in `./protocol` it pulled along would
759
+ * land in every route's entry chunk instead of the lazily loaded socket's.
760
+ */
761
+ var SOCKET_ID_HEADER = "x-gemi-socket";
762
+ //#endregion
930
763
  //#region client/useSetQueryData.ts
931
764
  /**
932
765
  * Write into every variant `target` names. `next` is the value to write, or
@@ -1382,16 +1215,8 @@ function filenameOf(response) {
1382
1215
  }
1383
1216
  //#endregion
1384
1217
  //#region client/useSubscription.ts
1385
- var MIN_RETRY_MS = 1e3;
1386
- var MAX_RETRY_MS = 3e4;
1387
- /** The server answered in a way retrying will not change (401, 403, 404). */
1388
- var FatalStatus = class extends Error {
1389
- status;
1390
- constructor(status) {
1391
- super(`The subscription was refused with ${status}.`);
1392
- this.status = status;
1393
- }
1394
- };
1218
+ /** `./subscriptionStream`, loaded on first use. Replaceable in tests. */
1219
+ var streamModule = { load: () => import("../chunks/subscriptionStream-CULcRbx0.js") };
1395
1220
  /**
1396
1221
  * Follows a route that answers with `ChangeFeed.stream`, as long as the
1397
1222
  * component is mounted: each change reaches `onChange`, in order, exactly
@@ -1442,105 +1267,27 @@ function useSubscription(url, options = {}, config = {}) {
1442
1267
  return;
1443
1268
  }
1444
1269
  let stopped = false;
1445
- let request = null;
1446
- let resume = null;
1447
- let retryMs = MIN_RETRY_MS;
1448
- const hidden = () => pauseWhenHidden && typeof document !== "undefined" && document.visibilityState === "hidden";
1449
- const onVisibilityChange = () => {
1450
- if (hidden()) request?.abort();
1451
- else resume?.();
1452
- };
1453
- if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisibilityChange);
1454
- const wait = (ms) => new Promise((resolve) => {
1455
- const timer = setTimeout(done, ms);
1456
- function done() {
1457
- clearTimeout(timer);
1458
- resume = null;
1459
- resolve();
1460
- }
1461
- resume = done;
1270
+ let stop = null;
1271
+ streamModule.load().then((module) => {
1272
+ const { follow } = loadedModule(module);
1273
+ if (stopped) return;
1274
+ stop = follow({
1275
+ endpoint,
1276
+ pauseWhenHidden,
1277
+ cursor,
1278
+ callbacks,
1279
+ setState
1280
+ });
1281
+ }).catch((error) => {
1282
+ if (!stopped) setState((s) => ({
1283
+ ...s,
1284
+ status: "closed",
1285
+ error
1286
+ }));
1462
1287
  });
1463
- (async () => {
1464
- while (!stopped) {
1465
- if (hidden()) {
1466
- setState((s) => ({
1467
- ...s,
1468
- status: "paused"
1469
- }));
1470
- await new Promise((resolve) => resume = resolve);
1471
- resume = null;
1472
- continue;
1473
- }
1474
- setState((s) => ({
1475
- ...s,
1476
- status: "connecting"
1477
- }));
1478
- const controller = request = new AbortController();
1479
- let delay = retryMs;
1480
- try {
1481
- const headers = { Accept: "text/event-stream" };
1482
- if (cursor.current) headers["Last-Event-ID"] = cursor.current;
1483
- const response = await fetch(endpoint, {
1484
- headers,
1485
- signal: controller.signal
1486
- });
1487
- if (!response.ok || !response.body) {
1488
- const status = response.status;
1489
- if (status >= 400 && status < 500 && status !== 408 && status !== 429) throw new FatalStatus(status);
1490
- const retryAfter = Number(response.headers.get("Retry-After"));
1491
- if (Number.isFinite(retryAfter) && retryAfter > 0) delay = retryAfter * 1e3;
1492
- throw new Error(`The subscription answered ${status}.`);
1493
- }
1494
- for await (const { id, message } of readEvents(response.body)) {
1495
- if (stopped) return;
1496
- if (id !== void 0) cursor.current = id;
1497
- if (message.type === "ready") {
1498
- retryMs = MIN_RETRY_MS;
1499
- delay = MIN_RETRY_MS;
1500
- setState({
1501
- status: "open",
1502
- cursor: cursor.current,
1503
- error: null
1504
- });
1505
- } else {
1506
- setState((s) => ({
1507
- ...s,
1508
- cursor: cursor.current
1509
- }));
1510
- if (message.type === "change") callbacks.current.onChange?.(message);
1511
- else if (message.type === "reset") callbacks.current.onReset?.(message);
1512
- }
1513
- }
1514
- } catch (error) {
1515
- if (stopped) return;
1516
- if (error instanceof FatalStatus) {
1517
- setState((s) => ({
1518
- ...s,
1519
- status: "closed",
1520
- error
1521
- }));
1522
- return;
1523
- }
1524
- if (controller.signal.aborted) continue;
1525
- setState((s) => ({
1526
- ...s,
1527
- error
1528
- }));
1529
- }
1530
- if (stopped || controller.signal.aborted) continue;
1531
- setState((s) => ({
1532
- ...s,
1533
- status: "connecting"
1534
- }));
1535
- await wait(delay);
1536
- retryMs = Math.min(retryMs * 2, MAX_RETRY_MS);
1537
- }
1538
- })();
1539
1288
  return () => {
1540
1289
  stopped = true;
1541
- request?.abort();
1542
- resume?.();
1543
- if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisibilityChange);
1290
+ stop?.();
1544
1291
  };
1545
1292
  }, [
1546
1293
  endpoint,
@@ -1550,48 +1297,89 @@ function useSubscription(url, options = {}, config = {}) {
1550
1297
  ]);
1551
1298
  return state;
1552
1299
  }
1300
+ //#endregion
1301
+ //#region client/useChannel.ts
1302
+ var warnedWithoutResync = /* @__PURE__ */ new Set();
1553
1303
  /**
1554
- * The events of an SSE body: `id` and the JSON `data`. Comments (keepalives)
1555
- * and events without data are skipped; an event whose data is not JSON is
1556
- * dropped rather than ending the stream.
1304
+ * Subscribes to a broadcast channel while the component is mounted.
1305
+ *
1306
+ * ```tsx
1307
+ * const { status } = useChannel("site.:siteId", { params: { siteId } }, {
1308
+ * on: { changed: (d) => mutate({ path: "/pages/:pageId", params: { pageId } }) },
1309
+ * onResync: () => mutate({ path: "/pages/:pageId", params: { pageId } }),
1310
+ * });
1311
+ * ```
1312
+ *
1313
+ * Events are hints, delivered at most once: pair `on` with `onResync`, which
1314
+ * runs whenever some may have been missed. `useChannelInvalidate` does both
1315
+ * for the common case. On the server, and until the effect runs, `status` is
1316
+ * `closed`.
1557
1317
  */
1558
- async function* readEvents(body) {
1559
- const reader = body.getReader();
1560
- const decoder = new TextDecoder();
1561
- let buffer = "";
1562
- try {
1563
- for (;;) {
1564
- const { done, value } = await reader.read();
1565
- if (done) return;
1566
- buffer = (buffer + decoder.decode(value, { stream: true })).replace(/\r\n?/g, "\n");
1567
- let boundary = buffer.indexOf("\n\n");
1568
- while (boundary !== -1) {
1569
- const block = buffer.slice(0, boundary);
1570
- buffer = buffer.slice(boundary + 2);
1571
- boundary = buffer.indexOf("\n\n");
1572
- let id;
1573
- const data = [];
1574
- for (const line of block.split("\n")) {
1575
- if (line === "" || line.startsWith(":")) continue;
1576
- const colon = line.indexOf(":");
1577
- const field = colon === -1 ? line : line.slice(0, colon);
1578
- let text = colon === -1 ? "" : line.slice(colon + 1);
1579
- if (text.startsWith(" ")) text = text.slice(1);
1580
- if (field === "id") id = text;
1581
- else if (field === "data") data.push(text);
1582
- }
1583
- if (data.length === 0) continue;
1584
- try {
1585
- yield {
1586
- id,
1587
- message: JSON.parse(data.join("\n"))
1588
- };
1589
- } catch {}
1590
- }
1318
+ function useChannel(pattern, options = {}, config = {}) {
1319
+ const configRef = useRef(config);
1320
+ configRef.current = config;
1321
+ const hasOn = config.on !== void 0 && Object.keys(config.on).length > 0;
1322
+ const hasResync = config.onResync !== void 0;
1323
+ useEffect(() => {
1324
+ if (process.env.NODE_ENV === "production") return;
1325
+ if (!hasOn || hasResync || warnedWithoutResync.has(pattern)) return;
1326
+ warnedWithoutResync.add(pattern);
1327
+ console.warn(`[gemi] useChannel("${pattern}") has \`on\` handlers but no \`onResync\`. Broadcast events are hints delivered at most once: after a reconnect or a gap the events in between are gone, so refetch in \`onResync\` (or use useChannelInvalidate).`);
1328
+ }, [
1329
+ pattern,
1330
+ hasOn,
1331
+ hasResync
1332
+ ]);
1333
+ return useChannelSubscription(pattern, options.params ?? {}, {
1334
+ onEvent: (event, data) => {
1335
+ const handler = configRef.current.on?.[event];
1336
+ handler?.(data);
1337
+ },
1338
+ onResync: () => configRef.current.onResync?.()
1339
+ }, config.enabled !== false);
1340
+ }
1341
+ /**
1342
+ * Refetches `paths` on any event on the channel and on every resync. Only
1343
+ * the variants being rendered refetch now; the rest are marked stale. Events
1344
+ * are coalesced: a burst within 150 ms refetches once.
1345
+ *
1346
+ * ```tsx
1347
+ * useChannelInvalidate("page.:pageId", { pageId }, ["/pages/:pageId", "/pages/:pageId/pictures"]);
1348
+ * ```
1349
+ */
1350
+ function useChannelInvalidate(pattern, params, paths, config = {}) {
1351
+ const { getResource } = useContext(QueryManagerContext);
1352
+ const routeParams = useParams();
1353
+ const channelParams = params ?? {};
1354
+ const latest = useRef({
1355
+ paths,
1356
+ routeParams,
1357
+ channelParams,
1358
+ getResource
1359
+ });
1360
+ latest.current = {
1361
+ paths,
1362
+ routeParams,
1363
+ channelParams,
1364
+ getResource
1365
+ };
1366
+ const invalidate = () => {
1367
+ const { paths, routeParams, channelParams, getResource } = latest.current;
1368
+ for (const entry of paths) {
1369
+ const target = typeof entry === "string" ? { path: entry } : entry;
1370
+ const resource = getResource(applyParams$1(target.path, {
1371
+ ...routeParams,
1372
+ ...channelParams,
1373
+ ...target.params
1374
+ }));
1375
+ const keys = target.search ? [toVariantKey(target.search)] : resource.variantKeys();
1376
+ for (const key of keys) resource.invalidate(key);
1591
1377
  }
1592
- } finally {
1593
- await reader.cancel().catch(() => {});
1594
- }
1378
+ };
1379
+ return useChannelSubscription(pattern, channelParams, {
1380
+ onEvent: useCoalesced(invalidate),
1381
+ onResync: invalidate
1382
+ }, config.enabled !== false);
1595
1383
  }
1596
1384
  //#endregion
1597
1385
  //#region client/Mutation.tsx