@ttt-productions/query-core 0.14.22 → 0.14.23

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 (125) hide show
  1. package/README.md +6 -6
  2. package/dist/defaults.d.ts +0 -15
  3. package/dist/defaults.d.ts.map +1 -1
  4. package/dist/defaults.js +0 -15
  5. package/dist/defaults.js.map +1 -1
  6. package/dist/firestore/types.d.ts +1 -49
  7. package/dist/firestore/types.d.ts.map +1 -1
  8. package/dist/firestore/types.js.map +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +0 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/keys.d.ts +0 -22
  13. package/dist/keys.d.ts.map +1 -1
  14. package/dist/keys.js +0 -4
  15. package/dist/keys.js.map +1 -1
  16. package/dist/react/cache-helpers.d.ts +0 -32
  17. package/dist/react/cache-helpers.d.ts.map +1 -1
  18. package/dist/react/cache-helpers.js +0 -28
  19. package/dist/react/cache-helpers.js.map +1 -1
  20. package/dist/react/defaults.d.ts +0 -4
  21. package/dist/react/defaults.d.ts.map +1 -1
  22. package/dist/react/defaults.js +2 -6
  23. package/dist/react/defaults.js.map +1 -1
  24. package/dist/react/domain-events/cache-invalidation.d.ts +0 -37
  25. package/dist/react/domain-events/cache-invalidation.d.ts.map +1 -1
  26. package/dist/react/domain-events/cache-invalidation.js +4 -53
  27. package/dist/react/domain-events/cache-invalidation.js.map +1 -1
  28. package/dist/react/domain-events/create-domain-event-invalidator.d.ts +0 -18
  29. package/dist/react/domain-events/create-domain-event-invalidator.d.ts.map +1 -1
  30. package/dist/react/domain-events/create-domain-event-invalidator.js +0 -5
  31. package/dist/react/domain-events/create-domain-event-invalidator.js.map +1 -1
  32. package/dist/react/firestore/absence-scheduler.d.ts +0 -28
  33. package/dist/react/firestore/absence-scheduler.d.ts.map +1 -1
  34. package/dist/react/firestore/absence-scheduler.js +1 -39
  35. package/dist/react/firestore/absence-scheduler.js.map +1 -1
  36. package/dist/react/firestore/batch-doc-loader.d.ts +0 -30
  37. package/dist/react/firestore/batch-doc-loader.d.ts.map +1 -1
  38. package/dist/react/firestore/batch-doc-loader.js +0 -34
  39. package/dist/react/firestore/batch-doc-loader.js.map +1 -1
  40. package/dist/react/firestore/context.d.ts +0 -19
  41. package/dist/react/firestore/context.d.ts.map +1 -1
  42. package/dist/react/firestore/context.js +0 -19
  43. package/dist/react/firestore/context.js.map +1 -1
  44. package/dist/react/firestore/doc-subscription-registry.d.ts +0 -23
  45. package/dist/react/firestore/doc-subscription-registry.d.ts.map +1 -1
  46. package/dist/react/firestore/doc-subscription-registry.js +0 -25
  47. package/dist/react/firestore/doc-subscription-registry.js.map +1 -1
  48. package/dist/react/firestore/infinite-helpers.d.ts +0 -12
  49. package/dist/react/firestore/infinite-helpers.d.ts.map +1 -1
  50. package/dist/react/firestore/infinite-helpers.js +0 -12
  51. package/dist/react/firestore/infinite-helpers.js.map +1 -1
  52. package/dist/react/firestore/resubscribe.d.ts +0 -20
  53. package/dist/react/firestore/resubscribe.d.ts.map +1 -1
  54. package/dist/react/firestore/resubscribe.js +0 -20
  55. package/dist/react/firestore/resubscribe.js.map +1 -1
  56. package/dist/react/firestore/subscription-status.d.ts +0 -29
  57. package/dist/react/firestore/subscription-status.d.ts.map +1 -1
  58. package/dist/react/firestore/subscription-status.js +1 -34
  59. package/dist/react/firestore/subscription-status.js.map +1 -1
  60. package/dist/react/firestore/types.d.ts +0 -64
  61. package/dist/react/firestore/types.d.ts.map +1 -1
  62. package/dist/react/firestore/types.js +0 -3
  63. package/dist/react/firestore/types.js.map +1 -1
  64. package/dist/react/firestore/use-firestore-collection.d.ts +0 -20
  65. package/dist/react/firestore/use-firestore-collection.d.ts.map +1 -1
  66. package/dist/react/firestore/use-firestore-collection.js +1 -35
  67. package/dist/react/firestore/use-firestore-collection.js.map +1 -1
  68. package/dist/react/firestore/use-firestore-count.d.ts +0 -15
  69. package/dist/react/firestore/use-firestore-count.d.ts.map +1 -1
  70. package/dist/react/firestore/use-firestore-count.js +0 -15
  71. package/dist/react/firestore/use-firestore-count.js.map +1 -1
  72. package/dist/react/firestore/use-firestore-doc.d.ts +0 -23
  73. package/dist/react/firestore/use-firestore-doc.d.ts.map +1 -1
  74. package/dist/react/firestore/use-firestore-doc.js +3 -40
  75. package/dist/react/firestore/use-firestore-doc.js.map +1 -1
  76. package/dist/react/firestore/use-firestore-infinite.d.ts +0 -29
  77. package/dist/react/firestore/use-firestore-infinite.d.ts.map +1 -1
  78. package/dist/react/firestore/use-firestore-infinite.js +0 -32
  79. package/dist/react/firestore/use-firestore-infinite.js.map +1 -1
  80. package/dist/react/firestore/use-firestore-live-infinite.d.ts +0 -34
  81. package/dist/react/firestore/use-firestore-live-infinite.d.ts.map +1 -1
  82. package/dist/react/firestore/use-firestore-live-infinite.js +1 -35
  83. package/dist/react/firestore/use-firestore-live-infinite.js.map +1 -1
  84. package/dist/react/firestore/use-firestore-paginated.d.ts +0 -30
  85. package/dist/react/firestore/use-firestore-paginated.d.ts.map +1 -1
  86. package/dist/react/firestore/use-firestore-paginated.js +2 -33
  87. package/dist/react/firestore/use-firestore-paginated.js.map +1 -1
  88. package/dist/react/firestore/useBatchFirestoreDocs.d.ts +1 -123
  89. package/dist/react/firestore/useBatchFirestoreDocs.d.ts.map +1 -1
  90. package/dist/react/firestore/useBatchFirestoreDocs.js +5 -58
  91. package/dist/react/firestore/useBatchFirestoreDocs.js.map +1 -1
  92. package/dist/react/index.d.ts.map +1 -1
  93. package/dist/react/index.js +0 -13
  94. package/dist/react/index.js.map +1 -1
  95. package/dist/react/pending-mutations.d.ts +1 -21
  96. package/dist/react/pending-mutations.d.ts.map +1 -1
  97. package/dist/react/pending-mutations.js +1 -32
  98. package/dist/react/pending-mutations.js.map +1 -1
  99. package/dist/react/provider.d.ts +0 -5
  100. package/dist/react/provider.d.ts.map +1 -1
  101. package/dist/react/provider.js +0 -4
  102. package/dist/react/provider.js.map +1 -1
  103. package/dist/react/query-client.d.ts +0 -4
  104. package/dist/react/query-client.d.ts.map +1 -1
  105. package/dist/react/query-client.js +0 -4
  106. package/dist/react/query-client.js.map +1 -1
  107. package/dist/react/search/types.d.ts +0 -12
  108. package/dist/react/search/types.d.ts.map +1 -1
  109. package/dist/react/search/types.js.map +1 -1
  110. package/dist/react/search/use-firestore-search.d.ts +0 -29
  111. package/dist/react/search/use-firestore-search.d.ts.map +1 -1
  112. package/dist/react/search/use-firestore-search.js +3 -41
  113. package/dist/react/search/use-firestore-search.js.map +1 -1
  114. package/dist/react/use-read-retry-state.d.ts +1 -21
  115. package/dist/react/use-read-retry-state.d.ts.map +1 -1
  116. package/dist/react/use-read-retry-state.js +2 -21
  117. package/dist/react/use-read-retry-state.js.map +1 -1
  118. package/dist/search/rule.d.ts +0 -6
  119. package/dist/search/rule.d.ts.map +1 -1
  120. package/dist/search/rule.js +0 -6
  121. package/dist/search/rule.js.map +1 -1
  122. package/dist/search/types.d.ts +1 -13
  123. package/dist/search/types.d.ts.map +1 -1
  124. package/dist/search/types.js.map +1 -1
  125. package/package.json +2 -2
@@ -4,25 +4,6 @@ export interface FirestoreProviderProps {
4
4
  db: Firestore;
5
5
  children: ReactNode;
6
6
  }
7
- /**
8
- * Provides Firestore instance to all useFirestore* hooks.
9
- * Wrap your app with this provider alongside QueryProvider.
10
- *
11
- * @example
12
- * ```tsx
13
- * import { db } from '@/lib/firebase';
14
- *
15
- * <QueryProvider>
16
- * <FirestoreProvider db={db}>
17
- * {children}
18
- * </FirestoreProvider>
19
- * </QueryProvider>
20
- * ```
21
- */
22
7
  export declare function FirestoreProvider({ db, children }: FirestoreProviderProps): import("react").JSX.Element;
23
- /**
24
- * Access the Firestore instance from context.
25
- * Throws if used outside of FirestoreProvider.
26
- */
27
8
  export declare function useFirestoreDb(): Firestore;
28
9
  //# sourceMappingURL=context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/context.tsx"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAClE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAQpD,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,SAAS,CAAC;IACd,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,sBAAsB,+BAEzE;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,SAAS,CAS1C"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/context.tsx"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAClE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAQpD,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,SAAS,CAAC;IACd,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,sBAAsB,+BAEzE;AAED,wBAAgB,cAAc,IAAI,SAAS,CAS1C"}
@@ -2,28 +2,9 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { createContext, useContext } from 'react';
4
4
  const FirestoreContext = createContext(null);
5
- /**
6
- * Provides Firestore instance to all useFirestore* hooks.
7
- * Wrap your app with this provider alongside QueryProvider.
8
- *
9
- * @example
10
- * ```tsx
11
- * import { db } from '@/lib/firebase';
12
- *
13
- * <QueryProvider>
14
- * <FirestoreProvider db={db}>
15
- * {children}
16
- * </FirestoreProvider>
17
- * </QueryProvider>
18
- * ```
19
- */
20
5
  export function FirestoreProvider({ db, children }) {
21
6
  return _jsx(FirestoreContext.Provider, { value: { db }, children: children });
22
7
  }
23
- /**
24
- * Access the Firestore instance from context.
25
- * Throws if used outside of FirestoreProvider.
26
- */
27
8
  export function useFirestoreDb() {
28
9
  const context = useContext(FirestoreContext);
29
10
  if (!context) {
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../../../src/react/firestore/context.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,aAAa,EAAE,UAAU,EAAkB,MAAM,OAAO,CAAC;AAOlE,MAAM,gBAAgB,GAAG,aAAa,CAA+B,IAAI,CAAC,CAAC;AAO3E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAAE,EAAE,EAAE,QAAQ,EAA0B;IACxE,OAAO,KAAC,gBAAgB,CAAC,QAAQ,IAAC,KAAK,EAAE,EAAE,EAAE,EAAE,YAAG,QAAQ,GAA6B,CAAC;AAC1F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc;IAC5B,MAAM,OAAO,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAC7C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,0DAA0D;YACxD,iDAAiD,CACpD,CAAC;IACJ,CAAC;IACD,OAAO,OAAO,CAAC,EAAE,CAAC;AACpB,CAAC","sourcesContent":["'use client';\n\nimport { createContext, useContext, type ReactNode } from 'react';\nimport type { Firestore } from 'firebase/firestore';\n\ninterface FirestoreContextValue {\n db: Firestore;\n}\n\nconst FirestoreContext = createContext<FirestoreContextValue | null>(null);\n\nexport interface FirestoreProviderProps {\n db: Firestore;\n children: ReactNode;\n}\n\n/**\n * Provides Firestore instance to all useFirestore* hooks.\n * Wrap your app with this provider alongside QueryProvider.\n *\n * @example\n * ```tsx\n * import { db } from '@/lib/firebase';\n *\n * <QueryProvider>\n * <FirestoreProvider db={db}>\n * {children}\n * </FirestoreProvider>\n * </QueryProvider>\n * ```\n */\nexport function FirestoreProvider({ db, children }: FirestoreProviderProps) {\n return <FirestoreContext.Provider value={{ db }}>{children}</FirestoreContext.Provider>;\n}\n\n/**\n * Access the Firestore instance from context.\n * Throws if used outside of FirestoreProvider.\n */\nexport function useFirestoreDb(): Firestore {\n const context = useContext(FirestoreContext);\n if (!context) {\n throw new Error(\n 'useFirestoreDb must be used within a FirestoreProvider. ' +\n 'Wrap your app with <FirestoreProvider db={db}>.',\n );\n }\n return context.db;\n}\n"]}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../../src/react/firestore/context.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,aAAa,EAAE,UAAU,EAAkB,MAAM,OAAO,CAAC;AAOlE,MAAM,gBAAgB,GAAG,aAAa,CAA+B,IAAI,CAAC,CAAC;AAO3E,MAAM,UAAU,iBAAiB,CAAC,EAAE,EAAE,EAAE,QAAQ,EAA0B;IACxE,OAAO,KAAC,gBAAgB,CAAC,QAAQ,IAAC,KAAK,EAAE,EAAE,EAAE,EAAE,YAAG,QAAQ,GAA6B,CAAC;AAC1F,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,MAAM,OAAO,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAC7C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,0DAA0D;YACxD,iDAAiD,CACpD,CAAC;IACJ,CAAC;IACD,OAAO,OAAO,CAAC,EAAE,CAAC;AACpB,CAAC","sourcesContent":["'use client';\n\nimport { createContext, useContext, type ReactNode } from 'react';\nimport type { Firestore } from 'firebase/firestore';\n\ninterface FirestoreContextValue {\n db: Firestore;\n}\n\nconst FirestoreContext = createContext<FirestoreContextValue | null>(null);\n\nexport interface FirestoreProviderProps {\n db: Firestore;\n children: ReactNode;\n}\n\nexport function FirestoreProvider({ db, children }: FirestoreProviderProps) {\n return <FirestoreContext.Provider value={{ db }}>{children}</FirestoreContext.Provider>;\n}\n\nexport function useFirestoreDb(): Firestore {\n const context = useContext(FirestoreContext);\n if (!context) {\n throw new Error(\n 'useFirestoreDb must be used within a FirestoreProvider. ' +\n 'Wrap your app with <FirestoreProvider db={db}>.',\n );\n }\n return context.db;\n}\n"]}
@@ -1,39 +1,16 @@
1
1
  import { type Firestore } from 'firebase/firestore';
2
2
  import type { QueryClient } from '@tanstack/react-query';
3
- /**
4
- * True while a shared listener owns `[queryKeyPrefix, id]` for this client.
5
- *
6
- * A one-shot read that was already in flight when a consumer entered subscribe mode would
7
- * otherwise resolve AFTER the listener's first snapshot and overwrite it — and `onSnapshot`
8
- * does not re-emit until the document actually changes, so the stale value would stick.
9
- * The one-shot `queryFn` consults this after its read and yields to the listener's value.
10
- */
11
3
  export declare function isListenerOwned(queryClient: QueryClient, queryKeyPrefix: string, id: string): boolean;
12
- /**
13
- * The error of the shared listener that owns `[queryKeyPrefix, id]`, or `null` when no listener
14
- * owns the key or it has not errored. The listener writes `null` to the key when it errors, so a
15
- * one-shot reader of the same key consults this before taking that `null` as a missing document.
16
- */
17
4
  export declare function listenerErrorOf(queryClient: QueryClient, queryKeyPrefix: string, id: string): Error | null;
18
5
  export interface SubscribeDocParams {
19
6
  queryClient: QueryClient;
20
7
  db: Firestore;
21
- /** Top-level/full collection path the doc lives in (e.g. 'publicUsers'). */
22
8
  collectionPath: string;
23
- /** Cache key prefix; the doc is cached at `[queryKeyPrefix, id]`. */
24
9
  queryKeyPrefix: string;
25
10
  id: string;
26
- /** Called whenever a snapshot (data or missing) lands, so the caller can re-render. */
27
11
  onUpdate: () => void;
28
- /** Called on listener error — and at once on joining a listener that has already errored. */
29
12
  onError?: (error: Error) => void;
30
13
  }
31
- /**
32
- * Subscribe to a single document via a shared, reference-counted listener.
33
- * Returns an idempotent unsubscribe function; the underlying Firestore listener is
34
- * detached only when the last subscriber for that id unsubscribes.
35
- */
36
14
  export declare function subscribeDoc(params: SubscribeDocParams): () => void;
37
- /** Test-only: number of active shared listeners for a QueryClient. */
38
15
  export declare function __activeListenerCount(queryClient: QueryClient): number;
39
16
  //# sourceMappingURL=doc-subscription-registry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"doc-subscription-registry.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/doc-subscription-registry.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AA8CzD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,WAAW,EACxB,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,GACT,OAAO,CAET;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,WAAW,EACxB,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,GACT,KAAK,GAAG,IAAI,CAEd;AAWD,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,WAAW,CAAC;IACzB,EAAE,EAAE,SAAS,CAAC;IACd,4EAA4E;IAC5E,cAAc,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,cAAc,EAAE,MAAM,CAAC;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,uFAAuF;IACvF,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,6FAA6F;IAC7F,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,IAAI,CA4DnE;AAED,sEAAsE;AACtE,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,MAAM,CAEtE"}
1
+ {"version":3,"file":"doc-subscription-registry.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/doc-subscription-registry.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAmBzD,wBAAgB,eAAe,CAC7B,WAAW,EAAE,WAAW,EACxB,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,GACT,OAAO,CAET;AAED,wBAAgB,eAAe,CAC7B,WAAW,EAAE,WAAW,EACxB,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,GACT,KAAK,GAAG,IAAI,CAEd;AAWD,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,WAAW,CAAC;IACzB,EAAE,EAAE,SAAS,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,IAAI,CAuDnE;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,MAAM,CAEtE"}
@@ -1,26 +1,12 @@
1
1
  'use client';
2
2
  import { doc, onSnapshot } from 'firebase/firestore';
3
- // Per-QueryClient registry. WeakMap so a discarded QueryClient (and its entries) is GC'd.
4
3
  const registries = new WeakMap();
5
4
  function registryKey(queryKeyPrefix, id) {
6
5
  return `${queryKeyPrefix} ${id}`;
7
6
  }
8
- /**
9
- * True while a shared listener owns `[queryKeyPrefix, id]` for this client.
10
- *
11
- * A one-shot read that was already in flight when a consumer entered subscribe mode would
12
- * otherwise resolve AFTER the listener's first snapshot and overwrite it — and `onSnapshot`
13
- * does not re-emit until the document actually changes, so the stale value would stick.
14
- * The one-shot `queryFn` consults this after its read and yields to the listener's value.
15
- */
16
7
  export function isListenerOwned(queryClient, queryKeyPrefix, id) {
17
8
  return registries.get(queryClient)?.has(registryKey(queryKeyPrefix, id)) ?? false;
18
9
  }
19
- /**
20
- * The error of the shared listener that owns `[queryKeyPrefix, id]`, or `null` when no listener
21
- * owns the key or it has not errored. The listener writes `null` to the key when it errors, so a
22
- * one-shot reader of the same key consults this before taking that `null` as a missing document.
23
- */
24
10
  export function listenerErrorOf(queryClient, queryKeyPrefix, id) {
25
11
  return registries.get(queryClient)?.get(registryKey(queryKeyPrefix, id))?.error ?? null;
26
12
  }
@@ -32,11 +18,6 @@ function getRegistry(queryClient) {
32
18
  }
33
19
  return registry;
34
20
  }
35
- /**
36
- * Subscribe to a single document via a shared, reference-counted listener.
37
- * Returns an idempotent unsubscribe function; the underlying Firestore listener is
38
- * detached only when the last subscriber for that id unsubscribes.
39
- */
40
21
  export function subscribeDoc(params) {
41
22
  const { queryClient, db, collectionPath, queryKeyPrefix, id, onUpdate, onError } = params;
42
23
  const registry = getRegistry(queryClient);
@@ -57,9 +38,6 @@ export function subscribeDoc(params) {
57
38
  for (const sub of [...subscribers])
58
39
  sub.onUpdate();
59
40
  }, (error) => {
60
- // Write `null` so the id resolves — as a failure, through the kept error — instead
61
- // of loading forever: subscribe-mode loading ("no cache entry yet") would otherwise
62
- // stay true for ids whose listener can never land a snapshot.
63
41
  created.error = error;
64
42
  queryClient.setQueryData([queryKeyPrefix, id], null, { updatedAt: Date.now() });
65
43
  for (const sub of [...subscribers])
@@ -84,15 +62,12 @@ export function subscribeDoc(params) {
84
62
  if (current.subscribers.size === 0) {
85
63
  current.unsubscribe();
86
64
  registry.delete(key);
87
- // A failed listener's `null` is a placeholder, not a read: once nothing owns the key,
88
- // mark it stale so a remaining one-shot reader reads the document for itself.
89
65
  if (current.error) {
90
66
  void queryClient.invalidateQueries({ queryKey: [queryKeyPrefix, id], exact: true });
91
67
  }
92
68
  }
93
69
  };
94
70
  }
95
- /** Test-only: number of active shared listeners for a QueryClient. */
96
71
  export function __activeListenerCount(queryClient) {
97
72
  return registries.get(queryClient)?.size ?? 0;
98
73
  }
@@ -1 +1 @@
1
- {"version":3,"file":"doc-subscription-registry.js","sourceRoot":"","sources":["../../../src/react/firestore/doc-subscription-registry.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { doc, onSnapshot, type Firestore } from 'firebase/firestore';\nimport type { QueryClient } from '@tanstack/react-query';\n\n/**\n * Reference-counted, real-time document subscription registry.\n *\n * Backs the `subscribe` mode of {@link useBatchFirestoreDocs}. The problem it solves:\n * many components on a page resolve the SAME document id (e.g. the same author rendered\n * in a feed card, a comment, and a mention). Without sharing, each would open its own\n * `onSnapshot` listener. This registry keeps exactly ONE Firestore listener per\n * `(queryClient, queryKeyPrefix, id)` and fans its updates out to every subscriber, so N\n * components showing the same id cost one listener.\n *\n * The single listener writes the document straight into the React Query cache at\n * `[queryKeyPrefix, id]` — `{ id, ...data }` when the doc exists, or `null` when it does\n * not (negative caching, so a missing/lagging doc resolves the instant it appears instead\n * of staying blank until staleTime). A listener ERROR also negative-caches the id: a\n * rules-denied doc (hidden, or non-existent when the rule references `resource.data`) is\n * resolved, not forever-loading — consumers see `data: null` plus the error instead\n * of an eternal spinner. Firestore ends a listener that errors, so the entry keeps that error\n * for as long as the entry lives (until its last subscriber leaves; a later snapshot, were one\n * to arrive, clears it): a subscriber that joins an errored listener receives the error too,\n * and a one-shot reader of the same key reads it through `listenerErrorOf`, so neither takes\n * the cached `null` as a missing document. The registry is keyed per `QueryClient` (a WeakMap),\n * which in practice means per browser tab, so each tab listens independently and stays\n * consistent without cross-tab plumbing.\n */\n\ntype Subscriber = {\n onUpdate: () => void;\n onError?: (error: Error) => void;\n};\n\ntype Entry = {\n unsubscribe: () => void;\n subscribers: Set<Subscriber>;\n /** The listener's last error, cleared by the next snapshot. */\n error: Error | null;\n};\n\n// Per-QueryClient registry. WeakMap so a discarded QueryClient (and its entries) is GC'd.\nconst registries = new WeakMap<QueryClient, Map<string, Entry>>();\n\nfunction registryKey(queryKeyPrefix: string, id: string): string {\n return `${queryKeyPrefix} ${id}`;\n}\n\n/**\n * True while a shared listener owns `[queryKeyPrefix, id]` for this client.\n *\n * A one-shot read that was already in flight when a consumer entered subscribe mode would\n * otherwise resolve AFTER the listener's first snapshot and overwrite it — and `onSnapshot`\n * does not re-emit until the document actually changes, so the stale value would stick.\n * The one-shot `queryFn` consults this after its read and yields to the listener's value.\n */\nexport function isListenerOwned(\n queryClient: QueryClient,\n queryKeyPrefix: string,\n id: string,\n): boolean {\n return registries.get(queryClient)?.has(registryKey(queryKeyPrefix, id)) ?? false;\n}\n\n/**\n * The error of the shared listener that owns `[queryKeyPrefix, id]`, or `null` when no listener\n * owns the key or it has not errored. The listener writes `null` to the key when it errors, so a\n * one-shot reader of the same key consults this before taking that `null` as a missing document.\n */\nexport function listenerErrorOf(\n queryClient: QueryClient,\n queryKeyPrefix: string,\n id: string,\n): Error | null {\n return registries.get(queryClient)?.get(registryKey(queryKeyPrefix, id))?.error ?? null;\n}\n\nfunction getRegistry(queryClient: QueryClient): Map<string, Entry> {\n let registry = registries.get(queryClient);\n if (!registry) {\n registry = new Map();\n registries.set(queryClient, registry);\n }\n return registry;\n}\n\nexport interface SubscribeDocParams {\n queryClient: QueryClient;\n db: Firestore;\n /** Top-level/full collection path the doc lives in (e.g. 'publicUsers'). */\n collectionPath: string;\n /** Cache key prefix; the doc is cached at `[queryKeyPrefix, id]`. */\n queryKeyPrefix: string;\n id: string;\n /** Called whenever a snapshot (data or missing) lands, so the caller can re-render. */\n onUpdate: () => void;\n /** Called on listener error — and at once on joining a listener that has already errored. */\n onError?: (error: Error) => void;\n}\n\n/**\n * Subscribe to a single document via a shared, reference-counted listener.\n * Returns an idempotent unsubscribe function; the underlying Firestore listener is\n * detached only when the last subscriber for that id unsubscribes.\n */\nexport function subscribeDoc(params: SubscribeDocParams): () => void {\n const { queryClient, db, collectionPath, queryKeyPrefix, id, onUpdate, onError } = params;\n const registry = getRegistry(queryClient);\n const key = registryKey(queryKeyPrefix, id);\n\n let entry = registry.get(key);\n if (!entry) {\n const created: Entry = {\n unsubscribe: () => {},\n subscribers: new Set<Subscriber>(),\n error: null,\n };\n const subscribers = created.subscribers;\n const ref = doc(db, collectionPath, id);\n created.unsubscribe = onSnapshot(\n ref,\n (snapshot) => {\n created.error = null;\n queryClient.setQueryData(\n [queryKeyPrefix, id],\n snapshot.exists() ? { id: snapshot.id, ...snapshot.data() } : null,\n { updatedAt: Date.now() },\n );\n // Copy before iterating: an onUpdate could (re-entrantly) unsubscribe.\n for (const sub of [...subscribers]) sub.onUpdate();\n },\n (error) => {\n // Write `null` so the id resolves — as a failure, through the kept error — instead\n // of loading forever: subscribe-mode loading (\"no cache entry yet\") would otherwise\n // stay true for ids whose listener can never land a snapshot.\n created.error = error;\n queryClient.setQueryData([queryKeyPrefix, id], null, { updatedAt: Date.now() });\n for (const sub of [...subscribers]) sub.onError?.(error);\n },\n );\n entry = created;\n registry.set(key, entry);\n }\n\n const subscriber: Subscriber = { onUpdate, onError };\n entry.subscribers.add(subscriber);\n if (entry.error) onError?.(entry.error);\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const current = registry.get(key);\n if (!current) return;\n current.subscribers.delete(subscriber);\n if (current.subscribers.size === 0) {\n current.unsubscribe();\n registry.delete(key);\n // A failed listener's `null` is a placeholder, not a read: once nothing owns the key,\n // mark it stale so a remaining one-shot reader reads the document for itself.\n if (current.error) {\n void queryClient.invalidateQueries({ queryKey: [queryKeyPrefix, id], exact: true });\n }\n }\n };\n}\n\n/** Test-only: number of active shared listeners for a QueryClient. */\nexport function __activeListenerCount(queryClient: QueryClient): number {\n return registries.get(queryClient)?.size ?? 0;\n}\n"]}
1
+ {"version":3,"file":"doc-subscription-registry.js","sourceRoot":"","sources":["../../../src/react/firestore/doc-subscription-registry.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { doc, onSnapshot, type Firestore } from 'firebase/firestore';\nimport type { QueryClient } from '@tanstack/react-query';\n\ntype Subscriber = {\n onUpdate: () => void;\n onError?: (error: Error) => void;\n};\n\ntype Entry = {\n unsubscribe: () => void;\n subscribers: Set<Subscriber>;\n error: Error | null;\n};\n\nconst registries = new WeakMap<QueryClient, Map<string, Entry>>();\n\nfunction registryKey(queryKeyPrefix: string, id: string): string {\n return `${queryKeyPrefix} ${id}`;\n}\n\nexport function isListenerOwned(\n queryClient: QueryClient,\n queryKeyPrefix: string,\n id: string,\n): boolean {\n return registries.get(queryClient)?.has(registryKey(queryKeyPrefix, id)) ?? false;\n}\n\nexport function listenerErrorOf(\n queryClient: QueryClient,\n queryKeyPrefix: string,\n id: string,\n): Error | null {\n return registries.get(queryClient)?.get(registryKey(queryKeyPrefix, id))?.error ?? null;\n}\n\nfunction getRegistry(queryClient: QueryClient): Map<string, Entry> {\n let registry = registries.get(queryClient);\n if (!registry) {\n registry = new Map();\n registries.set(queryClient, registry);\n }\n return registry;\n}\n\nexport interface SubscribeDocParams {\n queryClient: QueryClient;\n db: Firestore;\n collectionPath: string;\n queryKeyPrefix: string;\n id: string;\n onUpdate: () => void;\n onError?: (error: Error) => void;\n}\n\nexport function subscribeDoc(params: SubscribeDocParams): () => void {\n const { queryClient, db, collectionPath, queryKeyPrefix, id, onUpdate, onError } = params;\n const registry = getRegistry(queryClient);\n const key = registryKey(queryKeyPrefix, id);\n\n let entry = registry.get(key);\n if (!entry) {\n const created: Entry = {\n unsubscribe: () => {},\n subscribers: new Set<Subscriber>(),\n error: null,\n };\n const subscribers = created.subscribers;\n const ref = doc(db, collectionPath, id);\n created.unsubscribe = onSnapshot(\n ref,\n (snapshot) => {\n created.error = null;\n queryClient.setQueryData(\n [queryKeyPrefix, id],\n snapshot.exists() ? { id: snapshot.id, ...snapshot.data() } : null,\n { updatedAt: Date.now() },\n );\n // Copy before iterating: an onUpdate could (re-entrantly) unsubscribe.\n for (const sub of [...subscribers]) sub.onUpdate();\n },\n (error) => {\n created.error = error;\n queryClient.setQueryData([queryKeyPrefix, id], null, { updatedAt: Date.now() });\n for (const sub of [...subscribers]) sub.onError?.(error);\n },\n );\n entry = created;\n registry.set(key, entry);\n }\n\n const subscriber: Subscriber = { onUpdate, onError };\n entry.subscribers.add(subscriber);\n if (entry.error) onError?.(entry.error);\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const current = registry.get(key);\n if (!current) return;\n current.subscribers.delete(subscriber);\n if (current.subscribers.size === 0) {\n current.unsubscribe();\n registry.delete(key);\n if (current.error) {\n void queryClient.invalidateQueries({ queryKey: [queryKeyPrefix, id], exact: true });\n }\n }\n };\n}\n\nexport function __activeListenerCount(queryClient: QueryClient): number {\n return registries.get(queryClient)?.size ?? 0;\n}\n"]}
@@ -1,20 +1,8 @@
1
1
  import type { WithId } from '../../firestore/types.js';
2
2
  import type { InfinitePage } from './types.js';
3
- /**
4
- * Flatten infinite query pages into a single array of items.
5
- *
6
- * @example
7
- * ```tsx
8
- * const { data } = useFirestoreInfinite<Post>({ ... });
9
- * const posts = flattenInfiniteData(data);
10
- * ```
11
- */
12
3
  export declare function flattenInfiniteData<T>(data: {
13
4
  pages: InfinitePage<T>[];
14
5
  } | undefined): WithId<T>[];
15
- /**
16
- * Get total count of items across all loaded pages.
17
- */
18
6
  export declare function getInfiniteDataCount<T>(data: {
19
7
  pages: InfinitePage<T>[];
20
8
  } | undefined): number;
@@ -1 +1 @@
1
- {"version":3,"file":"infinite-helpers.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/infinite-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EACnC,IAAI,EAAE;IAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,CAAA;CAAE,GAAG,SAAS,GAC7C,MAAM,CAAC,CAAC,CAAC,EAAE,CAGb;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,IAAI,EAAE;IAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,CAAA;CAAE,GAAG,SAAS,GAAG,MAAM,CAG9F"}
1
+ {"version":3,"file":"infinite-helpers.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/infinite-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,wBAAgB,mBAAmB,CAAC,CAAC,EACnC,IAAI,EAAE;IAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,CAAA;CAAE,GAAG,SAAS,GAC7C,MAAM,CAAC,CAAC,CAAC,EAAE,CAGb;AAED,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,IAAI,EAAE;IAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,CAAA;CAAE,GAAG,SAAS,GAAG,MAAM,CAG9F"}
@@ -1,20 +1,8 @@
1
- /**
2
- * Flatten infinite query pages into a single array of items.
3
- *
4
- * @example
5
- * ```tsx
6
- * const { data } = useFirestoreInfinite<Post>({ ... });
7
- * const posts = flattenInfiniteData(data);
8
- * ```
9
- */
10
1
  export function flattenInfiniteData(data) {
11
2
  if (!data)
12
3
  return [];
13
4
  return data.pages.flatMap((page) => page.items);
14
5
  }
15
- /**
16
- * Get total count of items across all loaded pages.
17
- */
18
6
  export function getInfiniteDataCount(data) {
19
7
  if (!data)
20
8
  return 0;
@@ -1 +1 @@
1
- {"version":3,"file":"infinite-helpers.js","sourceRoot":"","sources":["../../../src/react/firestore/infinite-helpers.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAA8C;IAE9C,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAClD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAI,IAA8C;IACpF,IAAI,CAAC,IAAI;QAAE,OAAO,CAAC,CAAC;IACpB,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;AACtE,CAAC","sourcesContent":["import type { WithId } from '../../firestore/types.js';\nimport type { InfinitePage } from './types.js';\n\n/**\n * Flatten infinite query pages into a single array of items.\n *\n * @example\n * ```tsx\n * const { data } = useFirestoreInfinite<Post>({ ... });\n * const posts = flattenInfiniteData(data);\n * ```\n */\nexport function flattenInfiniteData<T>(\n data: { pages: InfinitePage<T>[] } | undefined,\n): WithId<T>[] {\n if (!data) return [];\n return data.pages.flatMap((page) => page.items);\n}\n\n/**\n * Get total count of items across all loaded pages.\n */\nexport function getInfiniteDataCount<T>(data: { pages: InfinitePage<T>[] } | undefined): number {\n if (!data) return 0;\n return data.pages.reduce((sum, page) => sum + page.items.length, 0);\n}\n"]}
1
+ {"version":3,"file":"infinite-helpers.js","sourceRoot":"","sources":["../../../src/react/firestore/infinite-helpers.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,mBAAmB,CACjC,IAA8C;IAE9C,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAI,IAA8C;IACpF,IAAI,CAAC,IAAI;QAAE,OAAO,CAAC,CAAC;IACpB,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;AACtE,CAAC","sourcesContent":["import type { WithId } from '../../firestore/types.js';\nimport type { InfinitePage } from './types.js';\n\nexport function flattenInfiniteData<T>(\n data: { pages: InfinitePage<T>[] } | undefined,\n): WithId<T>[] {\n if (!data) return [];\n return data.pages.flatMap((page) => page.items);\n}\n\nexport function getInfiniteDataCount<T>(data: { pages: InfinitePage<T>[] } | undefined): number {\n if (!data) return 0;\n return data.pages.reduce((sum, page) => sum + page.items.length, 0);\n}\n"]}
@@ -1,23 +1,3 @@
1
- /**
2
- * Bounded resubscribe ladder for realtime Firestore listeners.
3
- *
4
- * A subscription that dies on a `permission-denied` listener error never retries on
5
- * its own. In practice this fires transiently mid-session: the App Check token
6
- * expires ~1h in and the SDK's silent refresh mint can fail (e.g. the refresh hits
7
- * the reCAPTCHA bot-score throttle), so an active `onSnapshot` errors with
8
- * `permission-denied` even though the user is still authorized. Without a retry the
9
- * widget stays permanently dead until a full reload.
10
- *
11
- * These delays (ms) are the backoff between resubscribe attempts. After the last
12
- * one the error surfaces exactly as it did before. While the ladder runs the
13
- * consumer keeps its last-known data and reads `sourceState: 'connecting'`; a
14
- * healthy snapshot resets the ladder so a later blip gets a fresh window.
15
- */
16
1
  export declare const RESUBSCRIBE_DELAYS_MS: readonly [5000, 15000, 45000];
17
- /**
18
- * True for a Firestore `permission-denied` listener error — the only code the
19
- * subscription hooks bounded-retry (a failed token refresh heals within seconds).
20
- * Keys off the `FirestoreError.code`, never the message.
21
- */
22
2
  export declare function isPermissionDeniedError(error: unknown): boolean;
23
3
  //# sourceMappingURL=resubscribe.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resubscribe.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/resubscribe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB,+BAAmC,CAAC;AAEtE;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/D"}
1
+ {"version":3,"file":"resubscribe.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/resubscribe.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,+BAAmC,CAAC;AAEtE,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/D"}
@@ -1,24 +1,4 @@
1
- /**
2
- * Bounded resubscribe ladder for realtime Firestore listeners.
3
- *
4
- * A subscription that dies on a `permission-denied` listener error never retries on
5
- * its own. In practice this fires transiently mid-session: the App Check token
6
- * expires ~1h in and the SDK's silent refresh mint can fail (e.g. the refresh hits
7
- * the reCAPTCHA bot-score throttle), so an active `onSnapshot` errors with
8
- * `permission-denied` even though the user is still authorized. Without a retry the
9
- * widget stays permanently dead until a full reload.
10
- *
11
- * These delays (ms) are the backoff between resubscribe attempts. After the last
12
- * one the error surfaces exactly as it did before. While the ladder runs the
13
- * consumer keeps its last-known data and reads `sourceState: 'connecting'`; a
14
- * healthy snapshot resets the ladder so a later blip gets a fresh window.
15
- */
16
1
  export const RESUBSCRIBE_DELAYS_MS = [5_000, 15_000, 45_000];
17
- /**
18
- * True for a Firestore `permission-denied` listener error — the only code the
19
- * subscription hooks bounded-retry (a failed token refresh heals within seconds).
20
- * Keys off the `FirestoreError.code`, never the message.
21
- */
22
2
  export function isPermissionDeniedError(error) {
23
3
  return error?.code === 'permission-denied';
24
4
  }
@@ -1 +1 @@
1
- {"version":3,"file":"resubscribe.js","sourceRoot":"","sources":["../../../src/react/firestore/resubscribe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAU,CAAC;AAEtE;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAc;IACpD,OAAQ,KAA8C,EAAE,IAAI,KAAK,mBAAmB,CAAC;AACvF,CAAC","sourcesContent":["/**\n * Bounded resubscribe ladder for realtime Firestore listeners.\n *\n * A subscription that dies on a `permission-denied` listener error never retries on\n * its own. In practice this fires transiently mid-session: the App Check token\n * expires ~1h in and the SDK's silent refresh mint can fail (e.g. the refresh hits\n * the reCAPTCHA bot-score throttle), so an active `onSnapshot` errors with\n * `permission-denied` even though the user is still authorized. Without a retry the\n * widget stays permanently dead until a full reload.\n *\n * These delays (ms) are the backoff between resubscribe attempts. After the last\n * one the error surfaces exactly as it did before. While the ladder runs the\n * consumer keeps its last-known data and reads `sourceState: 'connecting'`; a\n * healthy snapshot resets the ladder so a later blip gets a fresh window.\n */\nexport const RESUBSCRIBE_DELAYS_MS = [5_000, 15_000, 45_000] as const;\n\n/**\n * True for a Firestore `permission-denied` listener error — the only code the\n * subscription hooks bounded-retry (a failed token refresh heals within seconds).\n * Keys off the `FirestoreError.code`, never the message.\n */\nexport function isPermissionDeniedError(error: unknown): boolean {\n return (error as { code?: string } | null | undefined)?.code === 'permission-denied';\n}\n"]}
1
+ {"version":3,"file":"resubscribe.js","sourceRoot":"","sources":["../../../src/react/firestore/resubscribe.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAU,CAAC;AAEtE,MAAM,UAAU,uBAAuB,CAAC,KAAc;IACpD,OAAQ,KAA8C,EAAE,IAAI,KAAK,mBAAmB,CAAC;AACvF,CAAC","sourcesContent":["export const RESUBSCRIBE_DELAYS_MS = [5_000, 15_000, 45_000] as const;\n\nexport function isPermissionDeniedError(error: unknown): boolean {\n return (error as { code?: string } | null | undefined)?.code === 'permission-denied';\n}\n"]}
@@ -1,10 +1,7 @@
1
1
  import type { UseQueryResult } from '@tanstack/react-query';
2
2
  import type { FirestoreSourceState, WithSourceState } from '../../firestore/types.js';
3
- /** A restart a subscribed result's `refetch()` asked for, open until the listener's first snapshot or its next error. */
4
3
  interface PendingRestart {
5
- /** The listener had failed when the restart was asked for. */
6
4
  fromError: boolean;
7
- /** That failure, kept beside the held data while the restart is open, as a one-shot refetch keeps it. */
8
5
  failure: Error | null;
9
6
  }
10
7
  interface SubscriptionStatus {
@@ -12,19 +9,9 @@ interface SubscriptionStatus {
12
9
  error: Error | null;
13
10
  sourceState: FirestoreSourceState;
14
11
  restart: PendingRestart | null;
15
- /** Bumped by every restart; the listener effect depends on it, so a bump re-opens the listener. */
16
12
  restarts: number;
17
13
  }
18
14
  type StatusPatch = Partial<Omit<SubscriptionStatus, 'identity' | 'restarts'>>;
19
- /**
20
- * A realtime listener's source state and error, tagged to the subscription identity that
21
- * produced them. A new identity reads `connecting` with no error from its first render;
22
- * writes made for a retired identity are dropped once the new identity's listener has
23
- * started (it calls `reset`).
24
- *
25
- * `restart()` clears the error and bumps `restarts`; the listener effect re-opens on the bump,
26
- * and its first snapshot or next surfaced error ends the restart (`settled` in the patch).
27
- */
28
15
  export declare function useSubscriptionStatus(identity: string): {
29
16
  error: Error | null;
30
17
  sourceState: FirestoreSourceState;
@@ -35,22 +22,6 @@ export declare function useSubscriptionStatus(identity: string): {
35
22
  restart: () => boolean;
36
23
  };
37
24
  type SubscriptionStatusApi = ReturnType<typeof useSubscriptionStatus>;
38
- /**
39
- * The subscribed hooks' result: the query's own result, with the listener's error and source
40
- * state merged in. In subscribe mode `refetch()` re-opens the listener (clearing its error, with a
41
- * fresh resubscribe ladder) and resolves once the re-opened listener delivers its first snapshot or
42
- * surfaces its next error; until then the result reports fetching.
43
- *
44
- * A listener that fails after it answered keeps its last snapshot as `data` beside the failure, the
45
- * way a one-shot read that fails a refetch keeps its data: `status: 'error'` and `isRefetchError`
46
- * say the rows are not current, and a consumer can keep what it showed instead of tearing it down.
47
- * A restart from that failure keeps both — the held data and the failure — until the re-opened
48
- * listener answers. A listener that failed before it ever answered has no data to hold.
49
- *
50
- * A `permission-denied` failure holds nothing: the reader lost access, and a snapshot it may no
51
- * longer read is never presented as readable. It reads like a failure before any answer, and so
52
- * does its restart.
53
- */
54
25
  export declare function useSubscribedResult<TData>(query: UseQueryResult<TData, Error>, subscribe: boolean, enabled: boolean, status: SubscriptionStatusApi): WithSourceState<UseQueryResult<TData, Error>>;
55
26
  export {};
56
27
  //# sourceMappingURL=subscription-status.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"subscription-status.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/subscription-status.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAuC,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACjG,OAAO,KAAK,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAGtF,yHAAyH;AACzH,UAAU,cAAc;IACtB,8DAA8D;IAC9D,SAAS,EAAE,OAAO,CAAC;IACnB,yGAAyG;IACzG,OAAO,EAAE,KAAK,GAAG,IAAI,CAAC;CACvB;AAED,UAAU,kBAAkB;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,WAAW,EAAE,oBAAoB,CAAC;IAClC,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;IAC/B,mGAAmG;IACnG,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,KAAK,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,UAAU,GAAG,UAAU,CAAC,CAAC,CAAC;AAM9E;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM;;;;;;oBAc1C,WAAW,GAAG,CAAC,CAAC,IAAI,EAAE,kBAAkB,KAAK,WAAW,CAAC;mBAkBnC,OAAO;EA2BxC;AAED,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAQtE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EACvC,KAAK,EAAE,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,EACnC,SAAS,EAAE,OAAO,EAClB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,qBAAqB,GAC5B,eAAe,CAAC,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CA6H/C"}
1
+ {"version":3,"file":"subscription-status.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/subscription-status.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAuC,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACjG,OAAO,KAAK,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAGtF,UAAU,cAAc;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,KAAK,GAAG,IAAI,CAAC;CACvB;AAED,UAAU,kBAAkB;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,WAAW,EAAE,oBAAoB,CAAC;IAClC,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,KAAK,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,UAAU,GAAG,UAAU,CAAC,CAAC,CAAC;AAM9E,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM;;;;;;oBAc1C,WAAW,GAAG,CAAC,CAAC,IAAI,EAAE,kBAAkB,KAAK,WAAW,CAAC;mBAenC,OAAO;EA2BxC;AAED,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAQtE,wBAAgB,mBAAmB,CAAC,KAAK,EACvC,KAAK,EAAE,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,EACnC,SAAS,EAAE,OAAO,EAClB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,qBAAqB,GAC5B,eAAe,CAAC,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAwH/C"}
@@ -4,15 +4,6 @@ import { isPermissionDeniedError } from './resubscribe.js';
4
4
  function freshStatus(identity) {
5
5
  return { identity, error: null, sourceState: 'connecting', restart: null, restarts: 0 };
6
6
  }
7
- /**
8
- * A realtime listener's source state and error, tagged to the subscription identity that
9
- * produced them. A new identity reads `connecting` with no error from its first render;
10
- * writes made for a retired identity are dropped once the new identity's listener has
11
- * started (it calls `reset`).
12
- *
13
- * `restart()` clears the error and bumps `restarts`; the listener effect re-opens on the bump,
14
- * and its first snapshot or next surfaced error ends the restart (`settled` in the patch).
15
- */
16
7
  export function useSubscriptionStatus(identity) {
17
8
  const [status, setStatus] = useState(() => freshStatus(identity));
18
9
  const current = status.identity === identity ? status : freshStatus(identity);
@@ -29,13 +20,10 @@ export function useSubscriptionStatus(identity) {
29
20
  return { ...prev, ...next };
30
21
  });
31
22
  }, [identity]);
32
- // The identity the hook renders now. A `restart` captured under an earlier identity must not
33
- // touch the current one's status, or every write the current listener makes would be dropped.
34
23
  const renderedIdentity = useRef(identity);
35
24
  useEffect(() => {
36
25
  renderedIdentity.current = identity;
37
26
  }, [identity]);
38
- /** Re-opens the listener; false (and nothing changes) when this identity is no longer rendered. */
39
27
  const restart = useCallback(() => {
40
28
  if (renderedIdentity.current !== identity)
41
29
  return false;
@@ -64,22 +52,6 @@ export function useSubscriptionStatus(identity) {
64
52
  restart,
65
53
  };
66
54
  }
67
- /**
68
- * The subscribed hooks' result: the query's own result, with the listener's error and source
69
- * state merged in. In subscribe mode `refetch()` re-opens the listener (clearing its error, with a
70
- * fresh resubscribe ladder) and resolves once the re-opened listener delivers its first snapshot or
71
- * surfaces its next error; until then the result reports fetching.
72
- *
73
- * A listener that fails after it answered keeps its last snapshot as `data` beside the failure, the
74
- * way a one-shot read that fails a refetch keeps its data: `status: 'error'` and `isRefetchError`
75
- * say the rows are not current, and a consumer can keep what it showed instead of tearing it down.
76
- * A restart from that failure keeps both — the held data and the failure — until the re-opened
77
- * listener answers. A listener that failed before it ever answered has no data to hold.
78
- *
79
- * A `permission-denied` failure holds nothing: the reader lost access, and a snapshot it may no
80
- * longer read is never presented as readable. It reads like a failure before any answer, and so
81
- * does its restart.
82
- */
83
55
  export function useSubscribedResult(query, subscribe, enabled, status) {
84
56
  const { error, sourceState, pendingRestart, restart } = status;
85
57
  const waiters = useRef([]);
@@ -87,8 +59,6 @@ export function useSubscribedResult(query, subscribe, enabled, status) {
87
59
  const mounted = useRef(true);
88
60
  const active = subscribe && enabled;
89
61
  const refetch = useCallback((options) => {
90
- // An unmounted hook, or a refetch captured under an identity the hook has left, re-opens
91
- // nothing: it settles at once rather than waiting on a listener that will never report.
92
62
  if (!active || !mounted.current)
93
63
  return Promise.resolve(latest.current);
94
64
  return new Promise((resolve, reject) => {
@@ -167,9 +137,7 @@ export function useSubscribedResult(query, subscribe, enabled, status) {
167
137
  };
168
138
  }
169
139
  else if (active && query.isPending) {
170
- // The listener has not delivered its first snapshot. The query underneath is disabled in
171
- // subscribe mode, so it reports pending but not loading; the read IS loading, exactly like a
172
- // one-shot read's first fetch — never an answered absence.
140
+ // The query underneath is disabled in subscribe mode, so it reports pending but not loading.
173
141
  result = {
174
142
  ...query,
175
143
  isLoading: true,
@@ -195,7 +163,6 @@ export function useSubscribedResult(query, subscribe, enabled, status) {
195
163
  waiter.resolve(result);
196
164
  }
197
165
  });
198
- // A refetch still open when the hook unmounts resolves with the last result instead of hanging.
199
166
  useEffect(() => {
200
167
  mounted.current = true;
201
168
  return () => {
@@ -1 +1 @@
1
- {"version":3,"file":"subscription-status.js","sourceRoot":"","sources":["../../../src/react/firestore/subscription-status.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport type { QueryObserverResult, RefetchOptions, UseQueryResult } from '@tanstack/react-query';\nimport type { FirestoreSourceState, WithSourceState } from '../../firestore/types.js';\nimport { isPermissionDeniedError } from './resubscribe.js';\n\n/** A restart a subscribed result's `refetch()` asked for, open until the listener's first snapshot or its next error. */\ninterface PendingRestart {\n /** The listener had failed when the restart was asked for. */\n fromError: boolean;\n /** That failure, kept beside the held data while the restart is open, as a one-shot refetch keeps it. */\n failure: Error | null;\n}\n\ninterface SubscriptionStatus {\n identity: string;\n error: Error | null;\n sourceState: FirestoreSourceState;\n restart: PendingRestart | null;\n /** Bumped by every restart; the listener effect depends on it, so a bump re-opens the listener. */\n restarts: number;\n}\n\ntype StatusPatch = Partial<Omit<SubscriptionStatus, 'identity' | 'restarts'>>;\n\nfunction freshStatus(identity: string): SubscriptionStatus {\n return { identity, error: null, sourceState: 'connecting', restart: null, restarts: 0 };\n}\n\n/**\n * A realtime listener's source state and error, tagged to the subscription identity that\n * produced them. A new identity reads `connecting` with no error from its first render;\n * writes made for a retired identity are dropped once the new identity's listener has\n * started (it calls `reset`).\n *\n * `restart()` clears the error and bumps `restarts`; the listener effect re-opens on the bump,\n * and its first snapshot or next surfaced error ends the restart (`settled` in the patch).\n */\nexport function useSubscriptionStatus(identity: string) {\n const [status, setStatus] = useState<SubscriptionStatus>(() => freshStatus(identity));\n\n const current: SubscriptionStatus = status.identity === identity ? status : freshStatus(identity);\n\n const reset = useCallback(() => {\n setStatus((prev) =>\n prev.identity === identity\n ? { ...prev, error: null, sourceState: 'connecting' }\n : freshStatus(identity),\n );\n }, [identity]);\n\n const update = useCallback(\n (patch: StatusPatch | ((prev: SubscriptionStatus) => StatusPatch)) => {\n setStatus((prev) => {\n if (prev.identity !== identity) return prev;\n const next = typeof patch === 'function' ? patch(prev) : patch;\n return { ...prev, ...next };\n });\n },\n [identity],\n );\n\n // The identity the hook renders now. A `restart` captured under an earlier identity must not\n // touch the current one's status, or every write the current listener makes would be dropped.\n const renderedIdentity = useRef(identity);\n useEffect(() => {\n renderedIdentity.current = identity;\n }, [identity]);\n\n /** Re-opens the listener; false (and nothing changes) when this identity is no longer rendered. */\n const restart = useCallback((): boolean => {\n if (renderedIdentity.current !== identity) return false;\n setStatus((prev) => {\n const base = prev.identity === identity ? prev : freshStatus(identity);\n return {\n ...base,\n error: null,\n sourceState: 'connecting',\n restart: {\n fromError: base.error !== null || base.restart?.fromError === true,\n failure: base.error ?? base.restart?.failure ?? null,\n },\n restarts: base.restarts + 1,\n };\n });\n return true;\n }, [identity]);\n\n return {\n error: current.error,\n sourceState: current.sourceState,\n pendingRestart: current.restart,\n restarts: current.restarts,\n reset,\n update,\n restart,\n };\n}\n\ntype SubscriptionStatusApi = ReturnType<typeof useSubscriptionStatus>;\n\ninterface RefetchWaiter<TData> {\n resolve: (result: QueryObserverResult<TData, Error>) => void;\n reject: (error: Error) => void;\n throwOnError: boolean;\n}\n\n/**\n * The subscribed hooks' result: the query's own result, with the listener's error and source\n * state merged in. In subscribe mode `refetch()` re-opens the listener (clearing its error, with a\n * fresh resubscribe ladder) and resolves once the re-opened listener delivers its first snapshot or\n * surfaces its next error; until then the result reports fetching.\n *\n * A listener that fails after it answered keeps its last snapshot as `data` beside the failure, the\n * way a one-shot read that fails a refetch keeps its data: `status: 'error'` and `isRefetchError`\n * say the rows are not current, and a consumer can keep what it showed instead of tearing it down.\n * A restart from that failure keeps both — the held data and the failure — until the re-opened\n * listener answers. A listener that failed before it ever answered has no data to hold.\n *\n * A `permission-denied` failure holds nothing: the reader lost access, and a snapshot it may no\n * longer read is never presented as readable. It reads like a failure before any answer, and so\n * does its restart.\n */\nexport function useSubscribedResult<TData>(\n query: UseQueryResult<TData, Error>,\n subscribe: boolean,\n enabled: boolean,\n status: SubscriptionStatusApi,\n): WithSourceState<UseQueryResult<TData, Error>> {\n const { error, sourceState, pendingRestart, restart } = status;\n const waiters = useRef<RefetchWaiter<TData>[]>([]);\n const latest = useRef(query as QueryObserverResult<TData, Error>);\n const mounted = useRef(true);\n const active = subscribe && enabled;\n\n const refetch = useCallback(\n (options?: RefetchOptions): Promise<QueryObserverResult<TData, Error>> => {\n // An unmounted hook, or a refetch captured under an identity the hook has left, re-opens\n // nothing: it settles at once rather than waiting on a listener that will never report.\n if (!active || !mounted.current) return Promise.resolve(latest.current);\n return new Promise((resolve, reject) => {\n if (!restart()) {\n resolve(latest.current);\n return;\n }\n waiters.current.push({ resolve, reject, throwOnError: options?.throwOnError === true });\n });\n },\n [active, restart],\n );\n\n const denied = isPermissionDeniedError(error ?? pendingRestart?.failure);\n const holdable = query.data !== undefined && !denied;\n\n let result: UseQueryResult<TData, Error>;\n if (!subscribe) {\n result = query;\n } else if (error) {\n const held = holdable;\n result = {\n ...query,\n data: held ? query.data : undefined,\n error,\n isError: true,\n isLoadingError: !held,\n isRefetchError: held,\n isSuccess: false,\n isPending: false,\n isLoading: false,\n status: 'error',\n refetch,\n } as UseQueryResult<TData, Error>;\n } else if (pendingRestart?.fromError && holdable) {\n result = {\n ...query,\n error: pendingRestart.failure,\n isError: pendingRestart.failure !== null,\n isLoadingError: false,\n isRefetchError: pendingRestart.failure !== null,\n isSuccess: false,\n isPending: false,\n isLoading: false,\n isFetching: true,\n isRefetching: true,\n status: pendingRestart.failure !== null ? 'error' : query.status,\n fetchStatus: 'fetching',\n refetch,\n } as UseQueryResult<TData, Error>;\n } else if (pendingRestart?.fromError) {\n result = {\n ...query,\n data: undefined,\n error: null,\n isError: false,\n isLoadingError: false,\n isRefetchError: false,\n isSuccess: false,\n isPending: true,\n isLoading: true,\n isFetching: true,\n isRefetching: false,\n status: 'pending',\n fetchStatus: 'fetching',\n refetch,\n } as UseQueryResult<TData, Error>;\n } else if (pendingRestart) {\n result = {\n ...query,\n isFetching: true,\n isLoading: query.isPending,\n isRefetching: !query.isPending,\n fetchStatus: 'fetching',\n refetch,\n } as UseQueryResult<TData, Error>;\n } else if (active && query.isPending) {\n // The listener has not delivered its first snapshot. The query underneath is disabled in\n // subscribe mode, so it reports pending but not loading; the read IS loading, exactly like a\n // one-shot read's first fetch — never an answered absence.\n result = {\n ...query,\n isLoading: true,\n isFetching: true,\n fetchStatus: 'fetching',\n refetch,\n } as UseQueryResult<TData, Error>;\n } else {\n result = { ...query, refetch } as UseQueryResult<TData, Error>;\n }\n\n const restartOpen = pendingRestart !== null;\n useEffect(() => {\n latest.current = result as QueryObserverResult<TData, Error>;\n if (restartOpen || waiters.current.length === 0) return;\n const settled = waiters.current;\n waiters.current = [];\n for (const waiter of settled) {\n if (waiter.throwOnError && result.error) waiter.reject(result.error);\n else waiter.resolve(result as QueryObserverResult<TData, Error>);\n }\n });\n\n // A refetch still open when the hook unmounts resolves with the last result instead of hanging.\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n const settled = waiters.current;\n waiters.current = [];\n for (const waiter of settled) waiter.resolve(latest.current);\n };\n }, []);\n\n return { ...result, sourceState } as WithSourceState<UseQueryResult<TData, Error>>;\n}\n"]}
1
+ {"version":3,"file":"subscription-status.js","sourceRoot":"","sources":["../../../src/react/firestore/subscription-status.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport type { QueryObserverResult, RefetchOptions, UseQueryResult } from '@tanstack/react-query';\nimport type { FirestoreSourceState, WithSourceState } from '../../firestore/types.js';\nimport { isPermissionDeniedError } from './resubscribe.js';\n\ninterface PendingRestart {\n fromError: boolean;\n failure: Error | null;\n}\n\ninterface SubscriptionStatus {\n identity: string;\n error: Error | null;\n sourceState: FirestoreSourceState;\n restart: PendingRestart | null;\n restarts: number;\n}\n\ntype StatusPatch = Partial<Omit<SubscriptionStatus, 'identity' | 'restarts'>>;\n\nfunction freshStatus(identity: string): SubscriptionStatus {\n return { identity, error: null, sourceState: 'connecting', restart: null, restarts: 0 };\n}\n\nexport function useSubscriptionStatus(identity: string) {\n const [status, setStatus] = useState<SubscriptionStatus>(() => freshStatus(identity));\n\n const current: SubscriptionStatus = status.identity === identity ? status : freshStatus(identity);\n\n const reset = useCallback(() => {\n setStatus((prev) =>\n prev.identity === identity\n ? { ...prev, error: null, sourceState: 'connecting' }\n : freshStatus(identity),\n );\n }, [identity]);\n\n const update = useCallback(\n (patch: StatusPatch | ((prev: SubscriptionStatus) => StatusPatch)) => {\n setStatus((prev) => {\n if (prev.identity !== identity) return prev;\n const next = typeof patch === 'function' ? patch(prev) : patch;\n return { ...prev, ...next };\n });\n },\n [identity],\n );\n\n const renderedIdentity = useRef(identity);\n useEffect(() => {\n renderedIdentity.current = identity;\n }, [identity]);\n\n const restart = useCallback((): boolean => {\n if (renderedIdentity.current !== identity) return false;\n setStatus((prev) => {\n const base = prev.identity === identity ? prev : freshStatus(identity);\n return {\n ...base,\n error: null,\n sourceState: 'connecting',\n restart: {\n fromError: base.error !== null || base.restart?.fromError === true,\n failure: base.error ?? base.restart?.failure ?? null,\n },\n restarts: base.restarts + 1,\n };\n });\n return true;\n }, [identity]);\n\n return {\n error: current.error,\n sourceState: current.sourceState,\n pendingRestart: current.restart,\n restarts: current.restarts,\n reset,\n update,\n restart,\n };\n}\n\ntype SubscriptionStatusApi = ReturnType<typeof useSubscriptionStatus>;\n\ninterface RefetchWaiter<TData> {\n resolve: (result: QueryObserverResult<TData, Error>) => void;\n reject: (error: Error) => void;\n throwOnError: boolean;\n}\n\nexport function useSubscribedResult<TData>(\n query: UseQueryResult<TData, Error>,\n subscribe: boolean,\n enabled: boolean,\n status: SubscriptionStatusApi,\n): WithSourceState<UseQueryResult<TData, Error>> {\n const { error, sourceState, pendingRestart, restart } = status;\n const waiters = useRef<RefetchWaiter<TData>[]>([]);\n const latest = useRef(query as QueryObserverResult<TData, Error>);\n const mounted = useRef(true);\n const active = subscribe && enabled;\n\n const refetch = useCallback(\n (options?: RefetchOptions): Promise<QueryObserverResult<TData, Error>> => {\n if (!active || !mounted.current) return Promise.resolve(latest.current);\n return new Promise((resolve, reject) => {\n if (!restart()) {\n resolve(latest.current);\n return;\n }\n waiters.current.push({ resolve, reject, throwOnError: options?.throwOnError === true });\n });\n },\n [active, restart],\n );\n\n const denied = isPermissionDeniedError(error ?? pendingRestart?.failure);\n const holdable = query.data !== undefined && !denied;\n\n let result: UseQueryResult<TData, Error>;\n if (!subscribe) {\n result = query;\n } else if (error) {\n const held = holdable;\n result = {\n ...query,\n data: held ? query.data : undefined,\n error,\n isError: true,\n isLoadingError: !held,\n isRefetchError: held,\n isSuccess: false,\n isPending: false,\n isLoading: false,\n status: 'error',\n refetch,\n } as UseQueryResult<TData, Error>;\n } else if (pendingRestart?.fromError && holdable) {\n result = {\n ...query,\n error: pendingRestart.failure,\n isError: pendingRestart.failure !== null,\n isLoadingError: false,\n isRefetchError: pendingRestart.failure !== null,\n isSuccess: false,\n isPending: false,\n isLoading: false,\n isFetching: true,\n isRefetching: true,\n status: pendingRestart.failure !== null ? 'error' : query.status,\n fetchStatus: 'fetching',\n refetch,\n } as UseQueryResult<TData, Error>;\n } else if (pendingRestart?.fromError) {\n result = {\n ...query,\n data: undefined,\n error: null,\n isError: false,\n isLoadingError: false,\n isRefetchError: false,\n isSuccess: false,\n isPending: true,\n isLoading: true,\n isFetching: true,\n isRefetching: false,\n status: 'pending',\n fetchStatus: 'fetching',\n refetch,\n } as UseQueryResult<TData, Error>;\n } else if (pendingRestart) {\n result = {\n ...query,\n isFetching: true,\n isLoading: query.isPending,\n isRefetching: !query.isPending,\n fetchStatus: 'fetching',\n refetch,\n } as UseQueryResult<TData, Error>;\n } else if (active && query.isPending) {\n // The query underneath is disabled in subscribe mode, so it reports pending but not loading.\n result = {\n ...query,\n isLoading: true,\n isFetching: true,\n fetchStatus: 'fetching',\n refetch,\n } as UseQueryResult<TData, Error>;\n } else {\n result = { ...query, refetch } as UseQueryResult<TData, Error>;\n }\n\n const restartOpen = pendingRestart !== null;\n useEffect(() => {\n latest.current = result as QueryObserverResult<TData, Error>;\n if (restartOpen || waiters.current.length === 0) return;\n const settled = waiters.current;\n waiters.current = [];\n for (const waiter of settled) {\n if (waiter.throwOnError && result.error) waiter.reject(result.error);\n else waiter.resolve(result as QueryObserverResult<TData, Error>);\n }\n });\n\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n const settled = waiters.current;\n waiters.current = [];\n for (const waiter of settled) waiter.resolve(latest.current);\n };\n }, []);\n\n return { ...result, sourceState } as WithSourceState<UseQueryResult<TData, Error>>;\n}\n"]}