gemi 0.129.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.
Files changed (60) hide show
  1. package/dist/{chunk-wmkrb1qv.js → chunk-13agap7z.js} +1 -1
  2. package/dist/{chunk-zw5z1k84.js → chunk-7ynnce4b.js} +1 -1
  3. package/dist/{chunk-sbdm4enk.js → chunk-xtqhagxz.js} +2 -2
  4. package/dist/{chunk-sbdm4enk.js.map → chunk-xtqhagxz.js.map} +1 -1
  5. package/dist/chunks/MutationError-DuKqBW8o.js +199 -0
  6. package/dist/chunks/MutationError-DuKqBW8o.js.map +1 -0
  7. package/dist/chunks/{RealtimeClient-CqGve6h8.js → RealtimeClient-DsWbFRC7.js} +11 -3
  8. package/dist/chunks/RealtimeClient-DsWbFRC7.js.map +1 -0
  9. package/dist/chunks/{ThemeProvider-DaX0eyqs.js → ThemeProvider-gWqXjGh-.js} +70 -91
  10. package/dist/chunks/ThemeProvider-gWqXjGh-.js.map +1 -0
  11. package/dist/chunks/channelKey-CIhHpX6x.js +10 -0
  12. package/dist/chunks/channelKey-CIhHpX6x.js.map +1 -0
  13. package/dist/chunks/fetchFileRequest-BSrgYnAy.js +57 -0
  14. package/dist/chunks/fetchFileRequest-BSrgYnAy.js.map +1 -0
  15. package/dist/chunks/{live-DIUDdLrA.js → live-CUdMhXHL.js} +22 -3
  16. package/dist/chunks/live-CUdMhXHL.js.map +1 -0
  17. package/dist/chunks/mutationExtras-CtY-X9m7.js +115 -0
  18. package/dist/chunks/mutationExtras-CtY-X9m7.js.map +1 -0
  19. package/dist/client/MutationError.d.ts +7 -9
  20. package/dist/client/MutationError.d.ts.map +1 -1
  21. package/dist/client/QueryResource.d.ts +3 -11
  22. package/dist/client/QueryResource.d.ts.map +1 -1
  23. package/dist/client/fetchFile.d.ts +4 -7
  24. package/dist/client/fetchFile.d.ts.map +1 -1
  25. package/dist/client/fetchFileRequest.d.ts +14 -0
  26. package/dist/client/fetchFileRequest.d.ts.map +1 -0
  27. package/dist/client/index.js +88 -339
  28. package/dist/client/index.js.map +1 -1
  29. package/dist/client/lazyModule.d.ts +32 -0
  30. package/dist/client/lazyModule.d.ts.map +1 -0
  31. package/dist/client/mutationErrorHelpers.d.ts +11 -0
  32. package/dist/client/mutationErrorHelpers.d.ts.map +1 -0
  33. package/dist/client/mutationExtras.d.ts +3 -0
  34. package/dist/client/mutationExtras.d.ts.map +1 -0
  35. package/dist/client/mutationModules.d.ts +17 -0
  36. package/dist/client/mutationModules.d.ts.map +1 -0
  37. package/dist/client/queryUpdates.d.ts +22 -0
  38. package/dist/client/queryUpdates.d.ts.map +1 -0
  39. package/dist/client/realtime/RealtimeClient.d.ts +5 -0
  40. package/dist/client/realtime/RealtimeClient.d.ts.map +1 -1
  41. package/dist/client/realtime/channelKey.d.ts +4 -0
  42. package/dist/client/realtime/channelKey.d.ts.map +1 -0
  43. package/dist/client/realtime/live.d.ts +6 -0
  44. package/dist/client/realtime/live.d.ts.map +1 -1
  45. package/dist/client/realtime/loader.d.ts +20 -23
  46. package/dist/client/realtime/loader.d.ts.map +1 -1
  47. package/dist/client/realtime/useChannelSubscription.d.ts.map +1 -1
  48. package/dist/client/useMutation.d.ts +1 -1
  49. package/dist/client/useMutation.d.ts.map +1 -1
  50. package/dist/client/useSetQueryData.d.ts +4 -17
  51. package/dist/client/useSetQueryData.d.ts.map +1 -1
  52. package/dist/server/index.js +1 -1
  53. package/dist/testing/index.js +4 -3
  54. package/dist/testing/index.js.map +1 -1
  55. package/package.json +1 -1
  56. package/dist/chunks/RealtimeClient-CqGve6h8.js.map +0 -1
  57. package/dist/chunks/ThemeProvider-DaX0eyqs.js.map +0 -1
  58. package/dist/chunks/live-DIUDdLrA.js.map +0 -1
  59. /package/dist/{chunk-wmkrb1qv.js.map → chunk-13agap7z.js.map} +0 -0
  60. /package/dist/{chunk-zw5z1k84.js.map → chunk-7ynnce4b.js.map} +0 -0
@@ -1,5 +1,6 @@
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";
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";
@@ -535,221 +536,6 @@ function useInfiniteQuery(url, options, config) {
535
536
  };
536
537
  }
537
538
  //#endregion
538
- //#region http/refusal.ts
539
- /**
540
- * The kind a status stands for, for a refusal that does not name one: an app's
541
- * own `RequestBreakerError`, or an older server's `{ message }`. Any other 4xx
542
- * is a `form_error`, a message about this request for `<FormError>` to show.
543
- */
544
- function refusalKindForStatus(status) {
545
- if (status === 401) return "authentication";
546
- if (status === 403) return "permission";
547
- if (status === 404) return "not_found";
548
- if (status === 416) return "range_not_satisfiable";
549
- if (status === 429) return "rate_limit";
550
- if (status >= 500) return "server_error";
551
- return "form_error";
552
- }
553
- //#endregion
554
- //#region client/MutationError.ts
555
- var LEGACY_MESSAGES = {
556
- "Authentication error": "authentication",
557
- "Not authorized": "authorization",
558
- "Insufficient permissions": "permission",
559
- "Invalid CSRF token": "csrf",
560
- "Internal Server Error": "server_error",
561
- Forbidden: "permission",
562
- "Not found": "not_found",
563
- "Rate limit exceeded": "rate_limit"
564
- };
565
- function isObject(error) {
566
- return typeof error === "object" && error !== null;
567
- }
568
- function legacyKind(message, status) {
569
- return LEGACY_MESSAGES[message] ?? (status === 401 ? "authorization" : refusalKindForStatus(status));
570
- }
571
- /**
572
- * The `MutationError` a non-2xx response's parsed body stands for.
573
- *
574
- * gemi's own error bodies are `{ error }`, and the value under `error` is what
575
- * is handed over. An app's body without one —
576
- * `HttpResponse.json({ message: "Taken" }, { status: 409 })` — is handed over
577
- * whole. A body with no content (`null`) becomes a refusal naming the status.
578
- *
579
- * What is handed over carries `status`, and a `kind` when it has a `message`:
580
- * a bare string, which servers before 0.88 sent for most refusals, becomes
581
- * `{ kind, message, status }`, and so does a `{ message }` without a kind.
582
- *
583
- * @internal
584
- */
585
- function mutationErrorFromBody(body, status) {
586
- const error = isObject(body) && "error" in body && body.error != null ? body.error : body;
587
- if (error == null) return {
588
- kind: refusalKindForStatus(status),
589
- message: `Request failed with status ${status}`,
590
- status
591
- };
592
- if (typeof error === "string") return {
593
- kind: legacyKind(error, status),
594
- message: error,
595
- status
596
- };
597
- if (!isObject(error) || error instanceof Error) return error;
598
- const withStatus = {
599
- ...error,
600
- status: typeof error.status === "number" ? error.status : status
601
- };
602
- if (typeof error.kind !== "string" && typeof error.message === "string") return {
603
- ...withStatus,
604
- kind: refusalKindForStatus(withStatus.status)
605
- };
606
- return withStatus;
607
- }
608
- function kindOf(error) {
609
- if (typeof error === "string") return LEGACY_MESSAGES[error];
610
- if (!isObject(error) || error instanceof Error) return void 0;
611
- if (typeof error.kind === "string") return error.kind;
612
- if (typeof error.status === "number") return refusalKindForStatus(error.status);
613
- return typeof error.message === "string" ? LEGACY_MESSAGES[error.message] : void 0;
614
- }
615
- /** Field messages, as `<ValidationErrors>` renders them. */
616
- function isValidationError(error) {
617
- return isObject(error) && error.kind === "validation_error";
618
- }
619
- /** A form-level message, as `<FormError>` renders it. */
620
- function isFormError(error) {
621
- return isObject(error) && error.kind === "form_error";
622
- }
623
- /** No signed-in user: `AuthenticationError` (401). */
624
- function isAuthenticationError(error) {
625
- return kindOf(error) === "authentication";
626
- }
627
- /**
628
- * A signed-in user who may not do this: `InsufficientPermissionsError` (403),
629
- * `AuthorizationError` (401) or a policy denial (403), whatever message it was
630
- * thrown with.
631
- */
632
- function isPermissionError(error) {
633
- const kind = kindOf(error);
634
- return kind === "permission" || kind === "authorization";
635
- }
636
- /** The CSRF cookie is missing or stale (403). A reload fixes it. */
637
- function isCsrfError(error) {
638
- return kindOf(error) === "csrf";
639
- }
640
- /** A missing record, file or route (404). */
641
- function isNotFoundError(error) {
642
- return kindOf(error) === "not_found";
643
- }
644
- /**
645
- * `RateLimitMiddleware` or `RateLimiter.enforce` refused the request (429).
646
- * `retryAfter` (ms) says when to try again, when the server sent it.
647
- */
648
- function isRateLimitError(error) {
649
- return kindOf(error) === "rate_limit";
650
- }
651
- /**
652
- * The server failed (a 500, under `gemi dev` too), or answered something that
653
- * is not JSON.
654
- */
655
- function isServerError(error) {
656
- return kindOf(error) === "server_error" || error instanceof SyntaxError;
657
- }
658
- /** The request got no answer: offline, DNS, CORS, a dropped connection. */
659
- function isNetworkError(error) {
660
- return error instanceof TypeError;
661
- }
662
- /**
663
- * Which of the guards above `error` passes, for a `switch` over all of them.
664
- * `"unknown"` is everything else: a 416, an error body of the app's own shape.
665
- */
666
- function mutationErrorKind(error) {
667
- if (isValidationError(error)) return "validation";
668
- if (isFormError(error)) return "form";
669
- if (isAuthenticationError(error)) return "authentication";
670
- if (isPermissionError(error)) return "permission";
671
- if (isCsrfError(error)) return "csrf";
672
- if (isNotFoundError(error)) return "not_found";
673
- if (isRateLimitError(error)) return "rate_limit";
674
- if (isServerError(error)) return "server";
675
- if (isNetworkError(error)) return "network";
676
- return "unknown";
677
- }
678
- /**
679
- * An error answered with `status`: a route's typed error from
680
- * `HttpResponse.error(status, body)` / `httpError`, narrowed to the members
681
- * of `error`'s type with that status.
682
- *
683
- * ```ts
684
- * onError: (error) => {
685
- * if (isHttpError(error, 410)) toast(error.message);
686
- * }
687
- * ```
688
- *
689
- * At run time it checks the status alone, so gemi's own refusals with the
690
- * same status (a 404 `not_found`, a 429 `rate_limit`) pass it too; pick a
691
- * status gemi does not use for its own errors, or check `kind` after it.
692
- */
693
- function isHttpError(error, status) {
694
- return isObject(error) && !(error instanceof Error) && error.status === status;
695
- }
696
- var FALLBACK_MESSAGE = "Something went wrong. Try again.";
697
- function unwrapQueryError(error) {
698
- if (error instanceof Error && error.name === "QueryError" && "body" in error) {
699
- const { body, status } = error;
700
- return body == null ? error : mutationErrorFromBody(body, status);
701
- }
702
- return error;
703
- }
704
- /**
705
- * `error.list(fallback)` for any value: an error kept from a mutation, a
706
- * `useQuery` `QueryError` (its body is read), or anything else.
707
- */
708
- function mutationErrorList(error, fallback = FALLBACK_MESSAGE) {
709
- const value = unwrapQueryError(error);
710
- if (isValidationError(value)) {
711
- const messages = Object.values(value.messages ?? {}).flat().filter((message) => typeof message === "string" && message !== "");
712
- return messages.length > 0 ? messages : [fallback];
713
- }
714
- if (isObject(value) && !(value instanceof Error) && value.kind !== "server_error" && typeof value.message === "string" && value.message !== "") return [value.message];
715
- return [fallback];
716
- }
717
- /**
718
- * `error.field(name)` for any value: a validation error's messages for
719
- * `name` (a `QueryError`'s body is read), `[]` for anything else.
720
- */
721
- function mutationErrorField(error, name) {
722
- const value = unwrapQueryError(error);
723
- if (!isValidationError(value)) return [];
724
- const messages = value.messages?.[name];
725
- return Array.isArray(messages) ? [...messages] : [];
726
- }
727
- /**
728
- * Puts `list` and `field` on an error the hooks hand over, as non-enumerable
729
- * properties, so `toEqual`, a spread and `JSON.stringify` see the same object
730
- * as before. A key the error already has is left alone; a primitive or a
731
- * frozen value is returned as it is.
732
- *
733
- * @internal
734
- */
735
- function withMutationErrorHelpers(error) {
736
- if (!isObject(error) || !Object.isExtensible(error)) return error;
737
- const helpers = {
738
- list: (fallback) => mutationErrorList(error, fallback),
739
- field: (name) => mutationErrorField(error, name)
740
- };
741
- for (const key of ["list", "field"]) {
742
- if (key in error) continue;
743
- Object.defineProperty(error, key, {
744
- value: helpers[key],
745
- configurable: true,
746
- writable: true,
747
- enumerable: false
748
- });
749
- }
750
- return error;
751
- }
752
- //#endregion
753
539
  //#region services/broadcast/socketHeader.ts
754
540
  /**
755
541
  * The request header a client sends its socket id in, for `toOthers`.
@@ -760,67 +546,26 @@ function withMutationErrorHelpers(error) {
760
546
  */
761
547
  var SOCKET_ID_HEADER = "x-gemi-socket";
762
548
  //#endregion
763
- //#region client/useSetQueryData.ts
764
- /**
765
- * Write into every variant `target` names. `next` is the value to write, or
766
- * a function of the cached value; a function skips a variant with nothing
767
- * cached. A value seeds a single variant that is not cached yet; a predicate
768
- * only ever reaches variants that are.
769
- */
770
- function writeQueryData(getResource, routeParams, target, next) {
771
- const { path, params = {}, search = {} } = target;
772
- const resource = getResource(applyParams$1(String(path), {
773
- ...routeParams,
774
- ...params
775
- }));
776
- const write = (variantKey) => {
777
- if ("value" in next) {
778
- resource.set(variantKey, next.value);
779
- return;
780
- }
781
- const state = resource.peek(variantKey);
782
- if (!state?.hasData) return;
783
- resource.set(variantKey, next.update(state.data));
784
- };
785
- if (typeof search === "function") {
786
- for (const variantKey of resource.variantKeys()) {
787
- if (!resource.peek(variantKey)?.hasData) continue;
788
- if (search(new URLSearchParams(variantKey))) write(variantKey);
789
- }
790
- return;
791
- }
792
- write(toVariantKey(search));
793
- }
794
- /**
795
- * Write a mutation's `updates` with its answer.
796
- */
797
- function applyQueryUpdates(getResource, routeParams, updates, answer) {
798
- for (const update of updates ?? []) {
799
- const { data } = update;
800
- writeQueryData(getResource, routeParams, update, data ? { update: (current) => data(answer, current) } : { value: answer });
801
- }
802
- }
549
+ //#region client/mutationModules.ts
803
550
  /**
804
- * Returns a function that writes into any query's cache by path, with no
805
- * refetch: for data the server already answered with. `useMutate` is the
806
- * optimistic counterpart, which refetches to reconcile.
807
- *
808
- * ```ts
809
- * const setQueryData = useSetQueryData();
810
- * setQueryData({ path: "/pages/:pageId/assets", params: { pageId } }, assets);
811
- * setQueryData({ path: "/todos", search: () => true }, (todos) => todos.slice(1));
812
- * ```
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.
813
555
  *
814
- * A value replaces the variant's data, and seeds it when nothing is cached
815
- * yet. A function updates what is cached and skips a variant with nothing
816
- * 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`).
817
561
  */
818
- function useSetQueryData() {
819
- const { getResource } = useContext(QueryManagerContext);
820
- const routeParams = useParams();
821
- return useCallback(function setQueryData(target, next) {
822
- writeQueryData(getResource, routeParams, target, typeof next === "function" ? { update: next } : { value: next });
823
- }, [getResource, routeParams]);
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(() => {});
824
569
  }
825
570
  //#endregion
826
571
  //#region client/useMutation.ts
@@ -855,6 +600,7 @@ function useMutation(method, url, ...args) {
855
600
  ...config
856
601
  };
857
602
  const parallel = options.concurrency === "parallel";
603
+ useEffect(preloadMutationExtras, []);
858
604
  async function trigger(input, call = {}) {
859
605
  const controller = new AbortController();
860
606
  abortController.current = controller;
@@ -897,24 +643,26 @@ function useMutation(method, url, ...args) {
897
643
  if (input instanceof FormData) body = input;
898
644
  else if (typeof input === "undefined") body = formData.current;
899
645
  else if (input) body = JSON.stringify(input);
646
+ const socketId = realtime.client?.socketId;
900
647
  try {
901
648
  const response = await fetch(`/api${finalUrl}`, {
902
649
  method,
903
650
  headers: {
904
651
  ...contentType,
905
- ...socketIdHeader()
652
+ ...socketId ? { [SOCKET_ID_HEADER]: socketId } : {}
906
653
  },
907
654
  ...body ? { body } : {},
908
655
  signal: controller.signal
909
656
  });
910
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;
911
660
  if (isSuperseded()) {
912
661
  settle((prev) => prev, true);
913
662
  return;
914
663
  }
915
664
  resetFormData();
916
665
  if (!response.ok) {
917
- const error = withMutationErrorHelpers(mutationErrorFromBody(data, response.status));
918
666
  settle((prev) => ({
919
667
  data: prev.data,
920
668
  error
@@ -924,7 +672,7 @@ function useMutation(method, url, ...args) {
924
672
  return;
925
673
  }
926
674
  clearPrefetchCache?.();
927
- applyQueryUpdates(getResource, _params, options.updates, data);
675
+ extras?.applyQueryUpdates(getResource, _params, options.updates, data);
928
676
  options.onSuccess(data);
929
677
  call.onSuccess?.(data);
930
678
  settle(() => ({
@@ -932,7 +680,8 @@ function useMutation(method, url, ...args) {
932
680
  error: null
933
681
  }));
934
682
  return data;
935
- } catch (error) {
683
+ } catch (thrown) {
684
+ const error = await withErrorHelpers(thrown);
936
685
  if (isSuperseded()) {
937
686
  settle((prev) => prev, true);
938
687
  return;
@@ -942,12 +691,11 @@ function useMutation(method, url, ...args) {
942
691
  settle((prev) => prev);
943
692
  return;
944
693
  }
945
- const failure = withMutationErrorHelpers(error);
946
- options.onError(failure);
947
- call.onError?.(failure);
694
+ options.onError(error);
695
+ call.onError?.(error);
948
696
  settle((prev) => ({
949
697
  data: parallel ? prev.data : null,
950
- error: failure
698
+ error
951
699
  }));
952
700
  }
953
701
  }
@@ -995,6 +743,7 @@ function useUpload(url, ...args) {
995
743
  const { getResource } = useContext(QueryManagerContext);
996
744
  const abortRef = useRef(null);
997
745
  const [inputs = {}, options = defaultOptions] = args ?? [];
746
+ useEffect(preloadMutationExtras, []);
998
747
  const cancel = () => {
999
748
  const abort = abortRef.current;
1000
749
  if (!abort) return;
@@ -1064,20 +813,22 @@ function useUpload(url, ...args) {
1064
813
  try {
1065
814
  error = mutationErrorFromBody(JSON.parse(outcome.body), outcome.status);
1066
815
  } catch {}
816
+ const failure = await withErrorHelpers(error);
1067
817
  setState("error");
1068
- options?.onError?.(withMutationErrorHelpers(error));
818
+ options?.onError?.(failure);
1069
819
  return;
1070
820
  }
1071
821
  const json = JSON.parse(outcome.body);
1072
822
  clearPrefetchCache?.();
1073
- applyQueryUpdates(getResource, _params, options?.updates, json);
823
+ if (options.updates) (await loadOrNull())?.applyQueryUpdates(getResource, _params, options.updates, json);
1074
824
  options?.onSuccess?.(json);
1075
825
  setState("done");
1076
826
  return json;
1077
827
  } catch (error) {
1078
828
  release();
829
+ const failure = await withErrorHelpers(error);
1079
830
  setState("error");
1080
- options?.onError?.(withMutationErrorHelpers(error));
831
+ options?.onError?.(failure);
1081
832
  return;
1082
833
  }
1083
834
  };
@@ -1088,13 +839,17 @@ function useUpload(url, ...args) {
1088
839
  cancel
1089
840
  };
1090
841
  }
1091
- /**
1092
- * `X-Gemi-Socket` with this tab's broadcast socket id, while one is open, so
1093
- * a controller's `Broadcast.toOthers(req)` skips the tab that made the change.
1094
- */
1095
- function socketIdHeader() {
1096
- const id = currentSocketId();
1097
- return id ? { [SOCKET_ID_HEADER]: id } : {};
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;
1098
853
  }
1099
854
  //#endregion
1100
855
  //#region client/useMutate.ts
@@ -1142,6 +897,10 @@ function useMutate() {
1142
897
  }
1143
898
  //#endregion
1144
899
  //#region client/fetchFile.ts
900
+ var request = {
901
+ current: null,
902
+ load: () => import("../chunks/fetchFileRequest-BSrgYnAy.js")
903
+ };
1145
904
  /**
1146
905
  * Downloads a file route's body as a `Blob`.
1147
906
  *
@@ -1156,62 +915,52 @@ function useMutate() {
1156
915
  * `retryAfter`), the error `useQuery` throws, so `mutationErrorList(error)`
1157
916
  * reads its message. A network failure rejects with the browser's `TypeError`,
1158
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.
1159
922
  */
1160
923
  async function fetchBlob(path, ...args) {
1161
- const response = await request(path, args[0]);
1162
- return {
1163
- blob: await response.blob(),
1164
- filename: filenameOf(response),
1165
- response
1166
- };
924
+ return (await loadLazy(request)).fetchFile(path, args[0], "blob");
1167
925
  }
1168
926
  /** `fetchBlob`, with the body read as text. */
1169
927
  async function fetchText(path, ...args) {
1170
- const response = await request(path, args[0]);
1171
- return {
1172
- text: await response.text(),
1173
- filename: filenameOf(response),
1174
- response
1175
- };
1176
- }
1177
- async function request(path, options = {}) {
1178
- const { params = {}, search, signal, init } = options;
1179
- const url = applyParams$1(path, params);
1180
- const query = searchString(search);
1181
- const response = await fetch(`/api${url}${query ? `?${query}` : ""}`, {
1182
- ...init,
1183
- method: "GET",
1184
- signal
1185
- });
1186
- if (!response.ok) {
1187
- const body = await response.json().catch(() => null);
1188
- throw new QueryError(url, query, response.status, body, parseRetryAfter(response.headers.get("retry-after")));
1189
- }
1190
- return response;
1191
- }
1192
- function searchString(search) {
1193
- if (!search) return "";
1194
- if (search instanceof URLSearchParams) return search.toString();
1195
- const params = new URLSearchParams();
1196
- for (const [key, value] of Object.entries(search)) if (value !== null && value !== void 0) params.append(key, String(value));
1197
- return params.toString();
928
+ return (await loadLazy(request)).fetchFile(path, args[0], "text");
1198
929
  }
930
+ //#endregion
931
+ //#region client/useSetQueryData.ts
1199
932
  /**
1200
- * The file name in a `Content-Disposition` header: the RFC 5987
1201
- * `filename*=UTF-8''...` form when present and decodable, else `filename`.
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.
1202
936
  *
1203
- * @internal Exported for tests.
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.
946
+ *
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.
1204
950
  */
1205
- function filenameOf(response) {
1206
- const header = response.headers.get("content-disposition");
1207
- if (!header) return null;
1208
- const extended = /filename\*\s*=\s*([^']*)'[^']*'([^;]+)/i.exec(header);
1209
- if (extended) try {
1210
- return decodeURIComponent(extended[2].trim());
1211
- } catch {}
1212
- const plain = /filename\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^;]+))/i.exec(header);
1213
- if (!plain) return null;
1214
- return (plain[1] !== void 0 ? plain[1].replace(/\\(.)/g, "$1") : plain[2].trim()) || null;
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]);
1215
964
  }
1216
965
  //#endregion
1217
966
  //#region client/useSubscription.ts