gemi 0.128.0 → 0.130.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.
- package/dist/{chunk-0mp9tck6.js → chunk-0ryswty9.js} +20 -20
- package/dist/{chunk-0mp9tck6.js.map → chunk-0ryswty9.js.map} +5 -4
- package/dist/{chunk-df0mekds.js → chunk-13agap7z.js} +1 -1
- package/dist/{chunk-qz0da20r.js → chunk-7ynnce4b.js} +1 -1
- package/dist/{chunk-wzfvwvz0.js → chunk-jj1ppnnk.js} +2 -2
- package/dist/{chunk-wzfvwvz0.js.map → chunk-jj1ppnnk.js.map} +1 -1
- package/dist/{chunk-tchte4dv.js → chunk-xtqhagxz.js} +2 -2
- package/dist/{chunk-tchte4dv.js.map → chunk-xtqhagxz.js.map} +1 -1
- package/dist/chunks/MutationError-DuKqBW8o.js +199 -0
- package/dist/chunks/MutationError-DuKqBW8o.js.map +1 -0
- package/dist/chunks/RealtimeClient-DsWbFRC7.js +553 -0
- package/dist/chunks/RealtimeClient-DsWbFRC7.js.map +1 -0
- package/dist/chunks/{ThemeProvider-BIrCSrFY.js → ThemeProvider-gWqXjGh-.js} +173 -578
- package/dist/chunks/ThemeProvider-gWqXjGh-.js.map +1 -0
- package/dist/chunks/channelKey-CIhHpX6x.js +10 -0
- package/dist/chunks/channelKey-CIhHpX6x.js.map +1 -0
- package/dist/chunks/fetchFileRequest-BSrgYnAy.js +57 -0
- package/dist/chunks/fetchFileRequest-BSrgYnAy.js.map +1 -0
- package/dist/chunks/live-CUdMhXHL.js +88 -0
- package/dist/chunks/live-CUdMhXHL.js.map +1 -0
- package/dist/chunks/mutationExtras-CtY-X9m7.js +115 -0
- package/dist/chunks/mutationExtras-CtY-X9m7.js.map +1 -0
- package/dist/chunks/subscriptionStream-CULcRbx0.js +166 -0
- package/dist/chunks/subscriptionStream-CULcRbx0.js.map +1 -0
- package/dist/client/MutationError.d.ts +7 -9
- package/dist/client/MutationError.d.ts.map +1 -1
- package/dist/client/QueryResource.d.ts +3 -11
- package/dist/client/QueryResource.d.ts.map +1 -1
- package/dist/client/fetchFile.d.ts +4 -7
- package/dist/client/fetchFile.d.ts.map +1 -1
- package/dist/client/fetchFileRequest.d.ts +14 -0
- package/dist/client/fetchFileRequest.d.ts.map +1 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +201 -664
- package/dist/client/index.js.map +1 -1
- package/dist/client/init.d.ts +1 -1
- package/dist/client/init.d.ts.map +1 -1
- package/dist/client/lazyModule.d.ts +32 -0
- package/dist/client/lazyModule.d.ts.map +1 -0
- package/dist/client/mutationErrorHelpers.d.ts +11 -0
- package/dist/client/mutationErrorHelpers.d.ts.map +1 -0
- package/dist/client/mutationExtras.d.ts +3 -0
- package/dist/client/mutationExtras.d.ts.map +1 -0
- package/dist/client/mutationModules.d.ts +17 -0
- package/dist/client/mutationModules.d.ts.map +1 -0
- package/dist/client/queryUpdates.d.ts +22 -0
- package/dist/client/queryUpdates.d.ts.map +1 -0
- package/dist/client/realtime/FakeSocket.d.ts +1 -1
- package/dist/client/realtime/FakeSocket.d.ts.map +1 -1
- package/dist/client/realtime/RealtimeClient.d.ts +7 -18
- package/dist/client/realtime/RealtimeClient.d.ts.map +1 -1
- package/dist/client/realtime/channelKey.d.ts +4 -0
- package/dist/client/realtime/channelKey.d.ts.map +1 -0
- package/dist/client/realtime/live.d.ts +31 -0
- package/dist/client/realtime/live.d.ts.map +1 -0
- package/dist/client/realtime/loader.d.ts +56 -0
- package/dist/client/realtime/loader.d.ts.map +1 -0
- package/dist/client/realtime/useChannelSubscription.d.ts +1 -1
- package/dist/client/realtime/useChannelSubscription.d.ts.map +1 -1
- package/dist/client/subscriptionStream.d.ts +36 -0
- package/dist/client/subscriptionStream.d.ts.map +1 -0
- package/dist/client/useChannel.d.ts +1 -13
- package/dist/client/useChannel.d.ts.map +1 -1
- package/dist/client/useMutation.d.ts +1 -1
- package/dist/client/useMutation.d.ts.map +1 -1
- package/dist/client/useQuery.d.ts +1 -1
- package/dist/client/useQuery.d.ts.map +1 -1
- package/dist/client/useSetQueryData.d.ts +4 -17
- package/dist/client/useSetQueryData.d.ts.map +1 -1
- package/dist/client/useSubscription.d.ts +5 -10
- package/dist/client/useSubscription.d.ts.map +1 -1
- package/dist/facades/index.js +1 -1
- package/dist/http/index.js +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/kernel/index.js +1 -1
- package/dist/server/index.js +1 -1
- package/dist/services/broadcast/protocol.d.ts +1 -2
- package/dist/services/broadcast/protocol.d.ts.map +1 -1
- package/dist/services/broadcast/socketHeader.d.ts +9 -0
- package/dist/services/broadcast/socketHeader.d.ts.map +1 -0
- package/dist/services/index.js +2 -2
- package/dist/services/index.js.map +1 -1
- package/dist/testing/Page.d.ts.map +1 -1
- package/dist/testing/index.js +4 -1
- package/dist/testing/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunks/ThemeProvider-BIrCSrFY.js.map +0 -1
- /package/dist/{chunk-df0mekds.js.map → chunk-13agap7z.js.map} +0 -0
- /package/dist/{chunk-qz0da20r.js.map → chunk-7ynnce4b.js.map} +0 -0
package/dist/client/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as DEFAULT_STALE_TIME, A as ServerDataContext, B as followLive, C as readRoutePayload, D as useNavigate, E as useSearchParams, G as loadedModule, H as realtime, 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 reconnectRealtime, W as loadLazy, 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, u as ComponentsContext, v as recoverFromChunkLoadError, w as routeDataUrl, x as registerDictionary, y as loadDictionary, z as configureRealtime } from "../chunks/ThemeProvider-gWqXjGh-.js";
|
|
2
2
|
import { n as RouteStateContext, r as RouteStateProvider, t as useParams } from "../chunks/useParams-BN3XXfmG.js";
|
|
3
|
+
import { a as isNetworkError, c as isPermissionError, d as isValidationError, f as mutationErrorField, h as mutationErrorList, i as isHttpError, l as isRateLimitError, m as mutationErrorKind, n as isCsrfError, o as isNotFoundError, p as mutationErrorFromBody, r as isFormError, s as isObject, t as isAuthenticationError, u as isServerError } from "../chunks/MutationError-DuKqBW8o.js";
|
|
3
4
|
import { Fragment, StrictMode, Suspense, createContext, createElement, isValidElement, lazy, memo, use, useCallback, useContext, useDeferredValue, useEffect, useMemo, useRef, useState, useSyncExternalStore, useTransition } from "react";
|
|
4
5
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
import { createRoot as createRoot$1, hydrateRoot } from "react-dom/client";
|
|
@@ -27,180 +28,6 @@ function isPlainObject(value) {
|
|
|
27
28
|
return value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
28
29
|
}
|
|
29
30
|
//#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
31
|
//#region client/useQuery.ts
|
|
205
32
|
/**
|
|
206
33
|
* A DOM event or React's synthetic one. Never a plain object, so a cached
|
|
@@ -275,18 +102,15 @@ function useFrameworkQuery(url, ...args) {
|
|
|
275
102
|
const lazy = config.lazy;
|
|
276
103
|
const configRef = useRef(config);
|
|
277
104
|
configRef.current = config;
|
|
278
|
-
const
|
|
105
|
+
const realtime = useContext(RealtimeContext);
|
|
106
|
+
const [liveOpen, setLiveOpen] = useState(false);
|
|
279
107
|
const liveResourceRef = useRef(resource);
|
|
280
108
|
liveResourceRef.current = resource;
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const liveOpen = useChannelSubscription(liveChannel?.pattern ?? null, liveChannel?.params ?? {}, {
|
|
287
|
-
onEvent: refreshLiveOnEvent,
|
|
288
|
-
onResync: refreshLive
|
|
289
|
-
}, liveChannel !== null).status === "open";
|
|
109
|
+
const liveKey = config.live ? JSON.stringify(config.live) : null;
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (liveKey === null) return;
|
|
112
|
+
return followLive(realtime, JSON.parse(liveKey), () => liveResourceRef.current, setLiveOpen);
|
|
113
|
+
}, [realtime, liveKey]);
|
|
290
114
|
const refreshIntervalRef = useRef(null);
|
|
291
115
|
const fetchedRef = useRef(!lazy);
|
|
292
116
|
const pollRef = useRef({
|
|
@@ -712,282 +536,36 @@ function useInfiniteQuery(url, options, config) {
|
|
|
712
536
|
};
|
|
713
537
|
}
|
|
714
538
|
//#endregion
|
|
715
|
-
//#region
|
|
716
|
-
/**
|
|
717
|
-
* The kind a status stands for, for a refusal that does not name one: an app's
|
|
718
|
-
* own `RequestBreakerError`, or an older server's `{ message }`. Any other 4xx
|
|
719
|
-
* is a `form_error`, a message about this request for `<FormError>` to show.
|
|
720
|
-
*/
|
|
721
|
-
function refusalKindForStatus(status) {
|
|
722
|
-
if (status === 401) return "authentication";
|
|
723
|
-
if (status === 403) return "permission";
|
|
724
|
-
if (status === 404) return "not_found";
|
|
725
|
-
if (status === 416) return "range_not_satisfiable";
|
|
726
|
-
if (status === 429) return "rate_limit";
|
|
727
|
-
if (status >= 500) return "server_error";
|
|
728
|
-
return "form_error";
|
|
729
|
-
}
|
|
730
|
-
//#endregion
|
|
731
|
-
//#region client/MutationError.ts
|
|
732
|
-
var LEGACY_MESSAGES = {
|
|
733
|
-
"Authentication error": "authentication",
|
|
734
|
-
"Not authorized": "authorization",
|
|
735
|
-
"Insufficient permissions": "permission",
|
|
736
|
-
"Invalid CSRF token": "csrf",
|
|
737
|
-
"Internal Server Error": "server_error",
|
|
738
|
-
Forbidden: "permission",
|
|
739
|
-
"Not found": "not_found",
|
|
740
|
-
"Rate limit exceeded": "rate_limit"
|
|
741
|
-
};
|
|
742
|
-
function isObject(error) {
|
|
743
|
-
return typeof error === "object" && error !== null;
|
|
744
|
-
}
|
|
745
|
-
function legacyKind(message, status) {
|
|
746
|
-
return LEGACY_MESSAGES[message] ?? (status === 401 ? "authorization" : refusalKindForStatus(status));
|
|
747
|
-
}
|
|
539
|
+
//#region services/broadcast/socketHeader.ts
|
|
748
540
|
/**
|
|
749
|
-
* The
|
|
750
|
-
*
|
|
751
|
-
* gemi's own error bodies are `{ error }`, and the value under `error` is what
|
|
752
|
-
* is handed over. An app's body without one —
|
|
753
|
-
* `HttpResponse.json({ message: "Taken" }, { status: 409 })` — is handed over
|
|
754
|
-
* whole. A body with no content (`null`) becomes a refusal naming the status.
|
|
755
|
-
*
|
|
756
|
-
* What is handed over carries `status`, and a `kind` when it has a `message`:
|
|
757
|
-
* a bare string, which servers before 0.88 sent for most refusals, becomes
|
|
758
|
-
* `{ kind, message, status }`, and so does a `{ message }` without a kind.
|
|
541
|
+
* The request header a client sends its socket id in, for `toOthers`.
|
|
759
542
|
*
|
|
760
|
-
*
|
|
543
|
+
* A module of its own, re-exported by `./protocol`: every mutation in the
|
|
544
|
+
* browser reads it, and anything else in `./protocol` it pulled along would
|
|
545
|
+
* land in every route's entry chunk instead of the lazily loaded socket's.
|
|
761
546
|
*/
|
|
762
|
-
|
|
763
|
-
const error = isObject(body) && "error" in body && body.error != null ? body.error : body;
|
|
764
|
-
if (error == null) return {
|
|
765
|
-
kind: refusalKindForStatus(status),
|
|
766
|
-
message: `Request failed with status ${status}`,
|
|
767
|
-
status
|
|
768
|
-
};
|
|
769
|
-
if (typeof error === "string") return {
|
|
770
|
-
kind: legacyKind(error, status),
|
|
771
|
-
message: error,
|
|
772
|
-
status
|
|
773
|
-
};
|
|
774
|
-
if (!isObject(error) || error instanceof Error) return error;
|
|
775
|
-
const withStatus = {
|
|
776
|
-
...error,
|
|
777
|
-
status: typeof error.status === "number" ? error.status : status
|
|
778
|
-
};
|
|
779
|
-
if (typeof error.kind !== "string" && typeof error.message === "string") return {
|
|
780
|
-
...withStatus,
|
|
781
|
-
kind: refusalKindForStatus(withStatus.status)
|
|
782
|
-
};
|
|
783
|
-
return withStatus;
|
|
784
|
-
}
|
|
785
|
-
function kindOf(error) {
|
|
786
|
-
if (typeof error === "string") return LEGACY_MESSAGES[error];
|
|
787
|
-
if (!isObject(error) || error instanceof Error) return void 0;
|
|
788
|
-
if (typeof error.kind === "string") return error.kind;
|
|
789
|
-
if (typeof error.status === "number") return refusalKindForStatus(error.status);
|
|
790
|
-
return typeof error.message === "string" ? LEGACY_MESSAGES[error.message] : void 0;
|
|
791
|
-
}
|
|
792
|
-
/** Field messages, as `<ValidationErrors>` renders them. */
|
|
793
|
-
function isValidationError(error) {
|
|
794
|
-
return isObject(error) && error.kind === "validation_error";
|
|
795
|
-
}
|
|
796
|
-
/** A form-level message, as `<FormError>` renders it. */
|
|
797
|
-
function isFormError(error) {
|
|
798
|
-
return isObject(error) && error.kind === "form_error";
|
|
799
|
-
}
|
|
800
|
-
/** No signed-in user: `AuthenticationError` (401). */
|
|
801
|
-
function isAuthenticationError(error) {
|
|
802
|
-
return kindOf(error) === "authentication";
|
|
803
|
-
}
|
|
804
|
-
/**
|
|
805
|
-
* A signed-in user who may not do this: `InsufficientPermissionsError` (403),
|
|
806
|
-
* `AuthorizationError` (401) or a policy denial (403), whatever message it was
|
|
807
|
-
* thrown with.
|
|
808
|
-
*/
|
|
809
|
-
function isPermissionError(error) {
|
|
810
|
-
const kind = kindOf(error);
|
|
811
|
-
return kind === "permission" || kind === "authorization";
|
|
812
|
-
}
|
|
813
|
-
/** The CSRF cookie is missing or stale (403). A reload fixes it. */
|
|
814
|
-
function isCsrfError(error) {
|
|
815
|
-
return kindOf(error) === "csrf";
|
|
816
|
-
}
|
|
817
|
-
/** A missing record, file or route (404). */
|
|
818
|
-
function isNotFoundError(error) {
|
|
819
|
-
return kindOf(error) === "not_found";
|
|
820
|
-
}
|
|
821
|
-
/**
|
|
822
|
-
* `RateLimitMiddleware` or `RateLimiter.enforce` refused the request (429).
|
|
823
|
-
* `retryAfter` (ms) says when to try again, when the server sent it.
|
|
824
|
-
*/
|
|
825
|
-
function isRateLimitError(error) {
|
|
826
|
-
return kindOf(error) === "rate_limit";
|
|
827
|
-
}
|
|
828
|
-
/**
|
|
829
|
-
* The server failed (a 500, under `gemi dev` too), or answered something that
|
|
830
|
-
* is not JSON.
|
|
831
|
-
*/
|
|
832
|
-
function isServerError(error) {
|
|
833
|
-
return kindOf(error) === "server_error" || error instanceof SyntaxError;
|
|
834
|
-
}
|
|
835
|
-
/** The request got no answer: offline, DNS, CORS, a dropped connection. */
|
|
836
|
-
function isNetworkError(error) {
|
|
837
|
-
return error instanceof TypeError;
|
|
838
|
-
}
|
|
839
|
-
/**
|
|
840
|
-
* Which of the guards above `error` passes, for a `switch` over all of them.
|
|
841
|
-
* `"unknown"` is everything else: a 416, an error body of the app's own shape.
|
|
842
|
-
*/
|
|
843
|
-
function mutationErrorKind(error) {
|
|
844
|
-
if (isValidationError(error)) return "validation";
|
|
845
|
-
if (isFormError(error)) return "form";
|
|
846
|
-
if (isAuthenticationError(error)) return "authentication";
|
|
847
|
-
if (isPermissionError(error)) return "permission";
|
|
848
|
-
if (isCsrfError(error)) return "csrf";
|
|
849
|
-
if (isNotFoundError(error)) return "not_found";
|
|
850
|
-
if (isRateLimitError(error)) return "rate_limit";
|
|
851
|
-
if (isServerError(error)) return "server";
|
|
852
|
-
if (isNetworkError(error)) return "network";
|
|
853
|
-
return "unknown";
|
|
854
|
-
}
|
|
855
|
-
/**
|
|
856
|
-
* An error answered with `status`: a route's typed error from
|
|
857
|
-
* `HttpResponse.error(status, body)` / `httpError`, narrowed to the members
|
|
858
|
-
* of `error`'s type with that status.
|
|
859
|
-
*
|
|
860
|
-
* ```ts
|
|
861
|
-
* onError: (error) => {
|
|
862
|
-
* if (isHttpError(error, 410)) toast(error.message);
|
|
863
|
-
* }
|
|
864
|
-
* ```
|
|
865
|
-
*
|
|
866
|
-
* At run time it checks the status alone, so gemi's own refusals with the
|
|
867
|
-
* same status (a 404 `not_found`, a 429 `rate_limit`) pass it too; pick a
|
|
868
|
-
* status gemi does not use for its own errors, or check `kind` after it.
|
|
869
|
-
*/
|
|
870
|
-
function isHttpError(error, status) {
|
|
871
|
-
return isObject(error) && !(error instanceof Error) && error.status === status;
|
|
872
|
-
}
|
|
873
|
-
var FALLBACK_MESSAGE = "Something went wrong. Try again.";
|
|
874
|
-
function unwrapQueryError(error) {
|
|
875
|
-
if (error instanceof Error && error.name === "QueryError" && "body" in error) {
|
|
876
|
-
const { body, status } = error;
|
|
877
|
-
return body == null ? error : mutationErrorFromBody(body, status);
|
|
878
|
-
}
|
|
879
|
-
return error;
|
|
880
|
-
}
|
|
881
|
-
/**
|
|
882
|
-
* `error.list(fallback)` for any value: an error kept from a mutation, a
|
|
883
|
-
* `useQuery` `QueryError` (its body is read), or anything else.
|
|
884
|
-
*/
|
|
885
|
-
function mutationErrorList(error, fallback = FALLBACK_MESSAGE) {
|
|
886
|
-
const value = unwrapQueryError(error);
|
|
887
|
-
if (isValidationError(value)) {
|
|
888
|
-
const messages = Object.values(value.messages ?? {}).flat().filter((message) => typeof message === "string" && message !== "");
|
|
889
|
-
return messages.length > 0 ? messages : [fallback];
|
|
890
|
-
}
|
|
891
|
-
if (isObject(value) && !(value instanceof Error) && value.kind !== "server_error" && typeof value.message === "string" && value.message !== "") return [value.message];
|
|
892
|
-
return [fallback];
|
|
893
|
-
}
|
|
894
|
-
/**
|
|
895
|
-
* `error.field(name)` for any value: a validation error's messages for
|
|
896
|
-
* `name` (a `QueryError`'s body is read), `[]` for anything else.
|
|
897
|
-
*/
|
|
898
|
-
function mutationErrorField(error, name) {
|
|
899
|
-
const value = unwrapQueryError(error);
|
|
900
|
-
if (!isValidationError(value)) return [];
|
|
901
|
-
const messages = value.messages?.[name];
|
|
902
|
-
return Array.isArray(messages) ? [...messages] : [];
|
|
903
|
-
}
|
|
904
|
-
/**
|
|
905
|
-
* Puts `list` and `field` on an error the hooks hand over, as non-enumerable
|
|
906
|
-
* properties, so `toEqual`, a spread and `JSON.stringify` see the same object
|
|
907
|
-
* as before. A key the error already has is left alone; a primitive or a
|
|
908
|
-
* frozen value is returned as it is.
|
|
909
|
-
*
|
|
910
|
-
* @internal
|
|
911
|
-
*/
|
|
912
|
-
function withMutationErrorHelpers(error) {
|
|
913
|
-
if (!isObject(error) || !Object.isExtensible(error)) return error;
|
|
914
|
-
const helpers = {
|
|
915
|
-
list: (fallback) => mutationErrorList(error, fallback),
|
|
916
|
-
field: (name) => mutationErrorField(error, name)
|
|
917
|
-
};
|
|
918
|
-
for (const key of ["list", "field"]) {
|
|
919
|
-
if (key in error) continue;
|
|
920
|
-
Object.defineProperty(error, key, {
|
|
921
|
-
value: helpers[key],
|
|
922
|
-
configurable: true,
|
|
923
|
-
writable: true,
|
|
924
|
-
enumerable: false
|
|
925
|
-
});
|
|
926
|
-
}
|
|
927
|
-
return error;
|
|
928
|
-
}
|
|
547
|
+
var SOCKET_ID_HEADER = "x-gemi-socket";
|
|
929
548
|
//#endregion
|
|
930
|
-
//#region client/
|
|
549
|
+
//#region client/mutationModules.ts
|
|
931
550
|
/**
|
|
932
|
-
*
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
*
|
|
936
|
-
*/
|
|
937
|
-
function writeQueryData(getResource, routeParams, target, next) {
|
|
938
|
-
const { path, params = {}, search = {} } = target;
|
|
939
|
-
const resource = getResource(applyParams$1(String(path), {
|
|
940
|
-
...routeParams,
|
|
941
|
-
...params
|
|
942
|
-
}));
|
|
943
|
-
const write = (variantKey) => {
|
|
944
|
-
if ("value" in next) {
|
|
945
|
-
resource.set(variantKey, next.value);
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
const state = resource.peek(variantKey);
|
|
949
|
-
if (!state?.hasData) return;
|
|
950
|
-
resource.set(variantKey, next.update(state.data));
|
|
951
|
-
};
|
|
952
|
-
if (typeof search === "function") {
|
|
953
|
-
for (const variantKey of resource.variantKeys()) {
|
|
954
|
-
if (!resource.peek(variantKey)?.hasData) continue;
|
|
955
|
-
if (search(new URLSearchParams(variantKey))) write(variantKey);
|
|
956
|
-
}
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
write(toVariantKey(search));
|
|
960
|
-
}
|
|
961
|
-
/**
|
|
962
|
-
* Write a mutation's `updates` with its answer.
|
|
963
|
-
*/
|
|
964
|
-
function applyQueryUpdates(getResource, routeParams, updates, answer) {
|
|
965
|
-
for (const update of updates ?? []) {
|
|
966
|
-
const { data } = update;
|
|
967
|
-
writeQueryData(getResource, routeParams, update, data ? { update: (current) => data(answer, current) } : { value: answer });
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
/**
|
|
971
|
-
* Returns a function that writes into any query's cache by path, with no
|
|
972
|
-
* refetch: for data the server already answered with. `useMutate` is the
|
|
973
|
-
* optimistic counterpart, which refetches to reconcile.
|
|
551
|
+
* `./mutationExtras`, which a page only needs once it mutates: loaded when the
|
|
552
|
+
* first mutation hook, or `useSetQueryData()`, mounts (#915), so no route's
|
|
553
|
+
* entry chunk carries it. By the time a request settles it is normally there;
|
|
554
|
+
* one that settles first waits for it.
|
|
974
555
|
*
|
|
975
|
-
*
|
|
976
|
-
*
|
|
977
|
-
*
|
|
978
|
-
*
|
|
979
|
-
*
|
|
980
|
-
*
|
|
981
|
-
* A value replaces the variant's data, and seeds it when nothing is cached
|
|
982
|
-
* yet. A function updates what is cached and skips a variant with nothing
|
|
983
|
-
* cached. A predicate `search` reaches every cached variant it accepts.
|
|
556
|
+
* Keep this module out of `gemi/testing`'s imports. Shared with that entry, it
|
|
557
|
+
* moves into the chunk both entries import, and there an app's bundler can
|
|
558
|
+
* emit `./mutationExtras` even for an app that never mutates, which splits the
|
|
559
|
+
* app's entry chunk ("needs no chunk besides the entry to start" in
|
|
560
|
+
* `packaging.test.ts`).
|
|
984
561
|
*/
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
562
|
+
var mutationExtras = {
|
|
563
|
+
current: null,
|
|
564
|
+
load: () => import("../chunks/mutationExtras-CtY-X9m7.js")
|
|
565
|
+
};
|
|
566
|
+
/** Starts loading `./mutationExtras`: the hooks' mount effect. */
|
|
567
|
+
function preloadMutationExtras() {
|
|
568
|
+
loadLazy(mutationExtras).catch(() => {});
|
|
991
569
|
}
|
|
992
570
|
//#endregion
|
|
993
571
|
//#region client/useMutation.ts
|
|
@@ -1022,6 +600,7 @@ function useMutation(method, url, ...args) {
|
|
|
1022
600
|
...config
|
|
1023
601
|
};
|
|
1024
602
|
const parallel = options.concurrency === "parallel";
|
|
603
|
+
useEffect(preloadMutationExtras, []);
|
|
1025
604
|
async function trigger(input, call = {}) {
|
|
1026
605
|
const controller = new AbortController();
|
|
1027
606
|
abortController.current = controller;
|
|
@@ -1064,24 +643,26 @@ function useMutation(method, url, ...args) {
|
|
|
1064
643
|
if (input instanceof FormData) body = input;
|
|
1065
644
|
else if (typeof input === "undefined") body = formData.current;
|
|
1066
645
|
else if (input) body = JSON.stringify(input);
|
|
646
|
+
const socketId = realtime.client?.socketId;
|
|
1067
647
|
try {
|
|
1068
648
|
const response = await fetch(`/api${finalUrl}`, {
|
|
1069
649
|
method,
|
|
1070
650
|
headers: {
|
|
1071
651
|
...contentType,
|
|
1072
|
-
...
|
|
652
|
+
...socketId ? { [SOCKET_ID_HEADER]: socketId } : {}
|
|
1073
653
|
},
|
|
1074
654
|
...body ? { body } : {},
|
|
1075
655
|
signal: controller.signal
|
|
1076
656
|
});
|
|
1077
657
|
const data = await response.json();
|
|
658
|
+
const error = response.ok ? null : await withErrorHelpers(mutationErrorFromBody(data, response.status));
|
|
659
|
+
const extras = response.ok && options.updates ? await loadOrNull() : null;
|
|
1078
660
|
if (isSuperseded()) {
|
|
1079
661
|
settle((prev) => prev, true);
|
|
1080
662
|
return;
|
|
1081
663
|
}
|
|
1082
664
|
resetFormData();
|
|
1083
665
|
if (!response.ok) {
|
|
1084
|
-
const error = withMutationErrorHelpers(mutationErrorFromBody(data, response.status));
|
|
1085
666
|
settle((prev) => ({
|
|
1086
667
|
data: prev.data,
|
|
1087
668
|
error
|
|
@@ -1091,7 +672,7 @@ function useMutation(method, url, ...args) {
|
|
|
1091
672
|
return;
|
|
1092
673
|
}
|
|
1093
674
|
clearPrefetchCache?.();
|
|
1094
|
-
applyQueryUpdates(getResource, _params, options.updates, data);
|
|
675
|
+
extras?.applyQueryUpdates(getResource, _params, options.updates, data);
|
|
1095
676
|
options.onSuccess(data);
|
|
1096
677
|
call.onSuccess?.(data);
|
|
1097
678
|
settle(() => ({
|
|
@@ -1099,7 +680,8 @@ function useMutation(method, url, ...args) {
|
|
|
1099
680
|
error: null
|
|
1100
681
|
}));
|
|
1101
682
|
return data;
|
|
1102
|
-
} catch (
|
|
683
|
+
} catch (thrown) {
|
|
684
|
+
const error = await withErrorHelpers(thrown);
|
|
1103
685
|
if (isSuperseded()) {
|
|
1104
686
|
settle((prev) => prev, true);
|
|
1105
687
|
return;
|
|
@@ -1109,12 +691,11 @@ function useMutation(method, url, ...args) {
|
|
|
1109
691
|
settle((prev) => prev);
|
|
1110
692
|
return;
|
|
1111
693
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
call.onError?.(failure);
|
|
694
|
+
options.onError(error);
|
|
695
|
+
call.onError?.(error);
|
|
1115
696
|
settle((prev) => ({
|
|
1116
697
|
data: parallel ? prev.data : null,
|
|
1117
|
-
error
|
|
698
|
+
error
|
|
1118
699
|
}));
|
|
1119
700
|
}
|
|
1120
701
|
}
|
|
@@ -1162,6 +743,7 @@ function useUpload(url, ...args) {
|
|
|
1162
743
|
const { getResource } = useContext(QueryManagerContext);
|
|
1163
744
|
const abortRef = useRef(null);
|
|
1164
745
|
const [inputs = {}, options = defaultOptions] = args ?? [];
|
|
746
|
+
useEffect(preloadMutationExtras, []);
|
|
1165
747
|
const cancel = () => {
|
|
1166
748
|
const abort = abortRef.current;
|
|
1167
749
|
if (!abort) return;
|
|
@@ -1231,20 +813,22 @@ function useUpload(url, ...args) {
|
|
|
1231
813
|
try {
|
|
1232
814
|
error = mutationErrorFromBody(JSON.parse(outcome.body), outcome.status);
|
|
1233
815
|
} catch {}
|
|
816
|
+
const failure = await withErrorHelpers(error);
|
|
1234
817
|
setState("error");
|
|
1235
|
-
options?.onError?.(
|
|
818
|
+
options?.onError?.(failure);
|
|
1236
819
|
return;
|
|
1237
820
|
}
|
|
1238
821
|
const json = JSON.parse(outcome.body);
|
|
1239
822
|
clearPrefetchCache?.();
|
|
1240
|
-
applyQueryUpdates(getResource, _params, options
|
|
823
|
+
if (options.updates) (await loadOrNull())?.applyQueryUpdates(getResource, _params, options.updates, json);
|
|
1241
824
|
options?.onSuccess?.(json);
|
|
1242
825
|
setState("done");
|
|
1243
826
|
return json;
|
|
1244
827
|
} catch (error) {
|
|
1245
828
|
release();
|
|
829
|
+
const failure = await withErrorHelpers(error);
|
|
1246
830
|
setState("error");
|
|
1247
|
-
options?.onError?.(
|
|
831
|
+
options?.onError?.(failure);
|
|
1248
832
|
return;
|
|
1249
833
|
}
|
|
1250
834
|
};
|
|
@@ -1255,13 +839,17 @@ function useUpload(url, ...args) {
|
|
|
1255
839
|
cancel
|
|
1256
840
|
};
|
|
1257
841
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
842
|
+
function loadOrNull() {
|
|
843
|
+
return loadLazy(mutationExtras).catch(() => null);
|
|
844
|
+
}
|
|
845
|
+
async function withErrorHelpers(error) {
|
|
846
|
+
const extras = await loadOrNull();
|
|
847
|
+
if (extras) return extras.withMutationErrorHelpers(error);
|
|
848
|
+
if (isObject(error) && Object.isExtensible(error) && !("list" in error)) Object.defineProperties(error, {
|
|
849
|
+
list: { value: (fallback = "Something went wrong. Try again.") => [fallback] },
|
|
850
|
+
field: { value: () => [] }
|
|
851
|
+
});
|
|
852
|
+
return error;
|
|
1265
853
|
}
|
|
1266
854
|
//#endregion
|
|
1267
855
|
//#region client/useMutate.ts
|
|
@@ -1309,6 +897,10 @@ function useMutate() {
|
|
|
1309
897
|
}
|
|
1310
898
|
//#endregion
|
|
1311
899
|
//#region client/fetchFile.ts
|
|
900
|
+
var request = {
|
|
901
|
+
current: null,
|
|
902
|
+
load: () => import("../chunks/fetchFileRequest-BSrgYnAy.js")
|
|
903
|
+
};
|
|
1312
904
|
/**
|
|
1313
905
|
* Downloads a file route's body as a `Blob`.
|
|
1314
906
|
*
|
|
@@ -1323,75 +915,57 @@ function useMutate() {
|
|
|
1323
915
|
* `retryAfter`), the error `useQuery` throws, so `mutationErrorList(error)`
|
|
1324
916
|
* reads its message. A network failure rejects with the browser's `TypeError`,
|
|
1325
917
|
* an abort with its `AbortError`.
|
|
918
|
+
*
|
|
919
|
+
* The request code loads on the first call (#915), so a route that never
|
|
920
|
+
* downloads a file carries none of it; a failed load rejects as a network
|
|
921
|
+
* failure does.
|
|
1326
922
|
*/
|
|
1327
923
|
async function fetchBlob(path, ...args) {
|
|
1328
|
-
|
|
1329
|
-
return {
|
|
1330
|
-
blob: await response.blob(),
|
|
1331
|
-
filename: filenameOf(response),
|
|
1332
|
-
response
|
|
1333
|
-
};
|
|
924
|
+
return (await loadLazy(request)).fetchFile(path, args[0], "blob");
|
|
1334
925
|
}
|
|
1335
926
|
/** `fetchBlob`, with the body read as text. */
|
|
1336
927
|
async function fetchText(path, ...args) {
|
|
1337
|
-
|
|
1338
|
-
return {
|
|
1339
|
-
text: await response.text(),
|
|
1340
|
-
filename: filenameOf(response),
|
|
1341
|
-
response
|
|
1342
|
-
};
|
|
1343
|
-
}
|
|
1344
|
-
async function request(path, options = {}) {
|
|
1345
|
-
const { params = {}, search, signal, init } = options;
|
|
1346
|
-
const url = applyParams$1(path, params);
|
|
1347
|
-
const query = searchString(search);
|
|
1348
|
-
const response = await fetch(`/api${url}${query ? `?${query}` : ""}`, {
|
|
1349
|
-
...init,
|
|
1350
|
-
method: "GET",
|
|
1351
|
-
signal
|
|
1352
|
-
});
|
|
1353
|
-
if (!response.ok) {
|
|
1354
|
-
const body = await response.json().catch(() => null);
|
|
1355
|
-
throw new QueryError(url, query, response.status, body, parseRetryAfter(response.headers.get("retry-after")));
|
|
1356
|
-
}
|
|
1357
|
-
return response;
|
|
1358
|
-
}
|
|
1359
|
-
function searchString(search) {
|
|
1360
|
-
if (!search) return "";
|
|
1361
|
-
if (search instanceof URLSearchParams) return search.toString();
|
|
1362
|
-
const params = new URLSearchParams();
|
|
1363
|
-
for (const [key, value] of Object.entries(search)) if (value !== null && value !== void 0) params.append(key, String(value));
|
|
1364
|
-
return params.toString();
|
|
928
|
+
return (await loadLazy(request)).fetchFile(path, args[0], "text");
|
|
1365
929
|
}
|
|
930
|
+
//#endregion
|
|
931
|
+
//#region client/useSetQueryData.ts
|
|
1366
932
|
/**
|
|
1367
|
-
*
|
|
1368
|
-
*
|
|
933
|
+
* Returns a function that writes into any query's cache by path, with no
|
|
934
|
+
* refetch: for data the server already answered with. `useMutate` is the
|
|
935
|
+
* optimistic counterpart, which refetches to reconcile.
|
|
936
|
+
*
|
|
937
|
+
* ```ts
|
|
938
|
+
* const setQueryData = useSetQueryData();
|
|
939
|
+
* setQueryData({ path: "/pages/:pageId/assets", params: { pageId } }, assets);
|
|
940
|
+
* setQueryData({ path: "/todos", search: () => true }, (todos) => todos.slice(1));
|
|
941
|
+
* ```
|
|
942
|
+
*
|
|
943
|
+
* A value replaces the variant's data, and seeds it when nothing is cached
|
|
944
|
+
* yet. A function updates what is cached and skips a variant with nothing
|
|
945
|
+
* cached. A predicate `search` reaches every cached variant it accepts.
|
|
1369
946
|
*
|
|
1370
|
-
*
|
|
947
|
+
* The code behind the writes is loaded when the hook mounts. A call made
|
|
948
|
+
* before it has arrived (during the first render, say) is written once it
|
|
949
|
+
* has, in order.
|
|
1371
950
|
*/
|
|
1372
|
-
function
|
|
1373
|
-
const
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
951
|
+
function useSetQueryData() {
|
|
952
|
+
const { getResource } = useContext(QueryManagerContext);
|
|
953
|
+
const routeParams = useParams();
|
|
954
|
+
useEffect(preloadMutationExtras, []);
|
|
955
|
+
return useCallback(function setQueryData(target, next) {
|
|
956
|
+
const write = typeof next === "function" ? { update: next } : { value: next };
|
|
957
|
+
const writes = mutationExtras.current;
|
|
958
|
+
if (writes) {
|
|
959
|
+
writes.writeQueryData(getResource, routeParams, target, write);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
loadLazy(mutationExtras).then((loaded) => loaded.writeQueryData(getResource, routeParams, target, write)).catch(() => {});
|
|
963
|
+
}, [getResource, routeParams]);
|
|
1382
964
|
}
|
|
1383
965
|
//#endregion
|
|
1384
966
|
//#region client/useSubscription.ts
|
|
1385
|
-
|
|
1386
|
-
var
|
|
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
|
-
};
|
|
967
|
+
/** `./subscriptionStream`, loaded on first use. Replaceable in tests. */
|
|
968
|
+
var streamModule = { load: () => import("../chunks/subscriptionStream-CULcRbx0.js") };
|
|
1395
969
|
/**
|
|
1396
970
|
* Follows a route that answers with `ChangeFeed.stream`, as long as the
|
|
1397
971
|
* component is mounted: each change reaches `onChange`, in order, exactly
|
|
@@ -1442,105 +1016,27 @@ function useSubscription(url, options = {}, config = {}) {
|
|
|
1442
1016
|
return;
|
|
1443
1017
|
}
|
|
1444
1018
|
let stopped = false;
|
|
1445
|
-
let
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1019
|
+
let stop = null;
|
|
1020
|
+
streamModule.load().then((module) => {
|
|
1021
|
+
const { follow } = loadedModule(module);
|
|
1022
|
+
if (stopped) return;
|
|
1023
|
+
stop = follow({
|
|
1024
|
+
endpoint,
|
|
1025
|
+
pauseWhenHidden,
|
|
1026
|
+
cursor,
|
|
1027
|
+
callbacks,
|
|
1028
|
+
setState
|
|
1029
|
+
});
|
|
1030
|
+
}).catch((error) => {
|
|
1031
|
+
if (!stopped) setState((s) => ({
|
|
1032
|
+
...s,
|
|
1033
|
+
status: "closed",
|
|
1034
|
+
error
|
|
1035
|
+
}));
|
|
1462
1036
|
});
|
|
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
1037
|
return () => {
|
|
1540
1038
|
stopped = true;
|
|
1541
|
-
|
|
1542
|
-
resume?.();
|
|
1543
|
-
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
1039
|
+
stop?.();
|
|
1544
1040
|
};
|
|
1545
1041
|
}, [
|
|
1546
1042
|
endpoint,
|
|
@@ -1550,48 +1046,89 @@ function useSubscription(url, options = {}, config = {}) {
|
|
|
1550
1046
|
]);
|
|
1551
1047
|
return state;
|
|
1552
1048
|
}
|
|
1049
|
+
//#endregion
|
|
1050
|
+
//#region client/useChannel.ts
|
|
1051
|
+
var warnedWithoutResync = /* @__PURE__ */ new Set();
|
|
1553
1052
|
/**
|
|
1554
|
-
*
|
|
1555
|
-
*
|
|
1556
|
-
*
|
|
1053
|
+
* Subscribes to a broadcast channel while the component is mounted.
|
|
1054
|
+
*
|
|
1055
|
+
* ```tsx
|
|
1056
|
+
* const { status } = useChannel("site.:siteId", { params: { siteId } }, {
|
|
1057
|
+
* on: { changed: (d) => mutate({ path: "/pages/:pageId", params: { pageId } }) },
|
|
1058
|
+
* onResync: () => mutate({ path: "/pages/:pageId", params: { pageId } }),
|
|
1059
|
+
* });
|
|
1060
|
+
* ```
|
|
1061
|
+
*
|
|
1062
|
+
* Events are hints, delivered at most once: pair `on` with `onResync`, which
|
|
1063
|
+
* runs whenever some may have been missed. `useChannelInvalidate` does both
|
|
1064
|
+
* for the common case. On the server, and until the effect runs, `status` is
|
|
1065
|
+
* `closed`.
|
|
1557
1066
|
*/
|
|
1558
|
-
|
|
1559
|
-
const
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1067
|
+
function useChannel(pattern, options = {}, config = {}) {
|
|
1068
|
+
const configRef = useRef(config);
|
|
1069
|
+
configRef.current = config;
|
|
1070
|
+
const hasOn = config.on !== void 0 && Object.keys(config.on).length > 0;
|
|
1071
|
+
const hasResync = config.onResync !== void 0;
|
|
1072
|
+
useEffect(() => {
|
|
1073
|
+
if (process.env.NODE_ENV === "production") return;
|
|
1074
|
+
if (!hasOn || hasResync || warnedWithoutResync.has(pattern)) return;
|
|
1075
|
+
warnedWithoutResync.add(pattern);
|
|
1076
|
+
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).`);
|
|
1077
|
+
}, [
|
|
1078
|
+
pattern,
|
|
1079
|
+
hasOn,
|
|
1080
|
+
hasResync
|
|
1081
|
+
]);
|
|
1082
|
+
return useChannelSubscription(pattern, options.params ?? {}, {
|
|
1083
|
+
onEvent: (event, data) => {
|
|
1084
|
+
const handler = configRef.current.on?.[event];
|
|
1085
|
+
handler?.(data);
|
|
1086
|
+
},
|
|
1087
|
+
onResync: () => configRef.current.onResync?.()
|
|
1088
|
+
}, config.enabled !== false);
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Refetches `paths` on any event on the channel and on every resync. Only
|
|
1092
|
+
* the variants being rendered refetch now; the rest are marked stale. Events
|
|
1093
|
+
* are coalesced: a burst within 150 ms refetches once.
|
|
1094
|
+
*
|
|
1095
|
+
* ```tsx
|
|
1096
|
+
* useChannelInvalidate("page.:pageId", { pageId }, ["/pages/:pageId", "/pages/:pageId/pictures"]);
|
|
1097
|
+
* ```
|
|
1098
|
+
*/
|
|
1099
|
+
function useChannelInvalidate(pattern, params, paths, config = {}) {
|
|
1100
|
+
const { getResource } = useContext(QueryManagerContext);
|
|
1101
|
+
const routeParams = useParams();
|
|
1102
|
+
const channelParams = params ?? {};
|
|
1103
|
+
const latest = useRef({
|
|
1104
|
+
paths,
|
|
1105
|
+
routeParams,
|
|
1106
|
+
channelParams,
|
|
1107
|
+
getResource
|
|
1108
|
+
});
|
|
1109
|
+
latest.current = {
|
|
1110
|
+
paths,
|
|
1111
|
+
routeParams,
|
|
1112
|
+
channelParams,
|
|
1113
|
+
getResource
|
|
1114
|
+
};
|
|
1115
|
+
const invalidate = () => {
|
|
1116
|
+
const { paths, routeParams, channelParams, getResource } = latest.current;
|
|
1117
|
+
for (const entry of paths) {
|
|
1118
|
+
const target = typeof entry === "string" ? { path: entry } : entry;
|
|
1119
|
+
const resource = getResource(applyParams$1(target.path, {
|
|
1120
|
+
...routeParams,
|
|
1121
|
+
...channelParams,
|
|
1122
|
+
...target.params
|
|
1123
|
+
}));
|
|
1124
|
+
const keys = target.search ? [toVariantKey(target.search)] : resource.variantKeys();
|
|
1125
|
+
for (const key of keys) resource.invalidate(key);
|
|
1591
1126
|
}
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1127
|
+
};
|
|
1128
|
+
return useChannelSubscription(pattern, channelParams, {
|
|
1129
|
+
onEvent: useCoalesced(invalidate),
|
|
1130
|
+
onResync: invalidate
|
|
1131
|
+
}, config.enabled !== false);
|
|
1595
1132
|
}
|
|
1596
1133
|
//#endregion
|
|
1597
1134
|
//#region client/Mutation.tsx
|