@ttt-productions/query-core 0.14.22 → 0.14.24

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 (137) 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 +5 -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/callables/create-callables.d.ts +40 -0
  21. package/dist/react/callables/create-callables.d.ts.map +1 -0
  22. package/dist/react/callables/create-callables.js +101 -0
  23. package/dist/react/callables/create-callables.js.map +1 -0
  24. package/dist/react/defaults.d.ts +0 -4
  25. package/dist/react/defaults.d.ts.map +1 -1
  26. package/dist/react/defaults.js +2 -6
  27. package/dist/react/defaults.js.map +1 -1
  28. package/dist/react/domain-events/cache-invalidation.d.ts +0 -37
  29. package/dist/react/domain-events/cache-invalidation.d.ts.map +1 -1
  30. package/dist/react/domain-events/cache-invalidation.js +4 -53
  31. package/dist/react/domain-events/cache-invalidation.js.map +1 -1
  32. package/dist/react/domain-events/create-domain-event-invalidator.d.ts +0 -18
  33. package/dist/react/domain-events/create-domain-event-invalidator.d.ts.map +1 -1
  34. package/dist/react/domain-events/create-domain-event-invalidator.js +0 -5
  35. package/dist/react/domain-events/create-domain-event-invalidator.js.map +1 -1
  36. package/dist/react/firestore/absence-scheduler.d.ts +0 -28
  37. package/dist/react/firestore/absence-scheduler.d.ts.map +1 -1
  38. package/dist/react/firestore/absence-scheduler.js +1 -39
  39. package/dist/react/firestore/absence-scheduler.js.map +1 -1
  40. package/dist/react/firestore/batch-doc-loader.d.ts +0 -30
  41. package/dist/react/firestore/batch-doc-loader.d.ts.map +1 -1
  42. package/dist/react/firestore/batch-doc-loader.js +0 -34
  43. package/dist/react/firestore/batch-doc-loader.js.map +1 -1
  44. package/dist/react/firestore/context.d.ts +0 -19
  45. package/dist/react/firestore/context.d.ts.map +1 -1
  46. package/dist/react/firestore/context.js +0 -19
  47. package/dist/react/firestore/context.js.map +1 -1
  48. package/dist/react/firestore/doc-subscription-registry.d.ts +0 -23
  49. package/dist/react/firestore/doc-subscription-registry.d.ts.map +1 -1
  50. package/dist/react/firestore/doc-subscription-registry.js +5 -26
  51. package/dist/react/firestore/doc-subscription-registry.js.map +1 -1
  52. package/dist/react/firestore/infinite-helpers.d.ts +0 -12
  53. package/dist/react/firestore/infinite-helpers.d.ts.map +1 -1
  54. package/dist/react/firestore/infinite-helpers.js +0 -12
  55. package/dist/react/firestore/infinite-helpers.js.map +1 -1
  56. package/dist/react/firestore/resubscribe.d.ts +0 -20
  57. package/dist/react/firestore/resubscribe.d.ts.map +1 -1
  58. package/dist/react/firestore/resubscribe.js +0 -20
  59. package/dist/react/firestore/resubscribe.js.map +1 -1
  60. package/dist/react/firestore/subscription-status.d.ts +0 -29
  61. package/dist/react/firestore/subscription-status.d.ts.map +1 -1
  62. package/dist/react/firestore/subscription-status.js +1 -34
  63. package/dist/react/firestore/subscription-status.js.map +1 -1
  64. package/dist/react/firestore/types.d.ts +0 -64
  65. package/dist/react/firestore/types.d.ts.map +1 -1
  66. package/dist/react/firestore/types.js +0 -3
  67. package/dist/react/firestore/types.js.map +1 -1
  68. package/dist/react/firestore/use-firestore-collection.d.ts +0 -20
  69. package/dist/react/firestore/use-firestore-collection.d.ts.map +1 -1
  70. package/dist/react/firestore/use-firestore-collection.js +1 -35
  71. package/dist/react/firestore/use-firestore-collection.js.map +1 -1
  72. package/dist/react/firestore/use-firestore-count.d.ts +0 -15
  73. package/dist/react/firestore/use-firestore-count.d.ts.map +1 -1
  74. package/dist/react/firestore/use-firestore-count.js +0 -15
  75. package/dist/react/firestore/use-firestore-count.js.map +1 -1
  76. package/dist/react/firestore/use-firestore-doc.d.ts +0 -23
  77. package/dist/react/firestore/use-firestore-doc.d.ts.map +1 -1
  78. package/dist/react/firestore/use-firestore-doc.js +3 -40
  79. package/dist/react/firestore/use-firestore-doc.js.map +1 -1
  80. package/dist/react/firestore/use-firestore-infinite.d.ts +0 -29
  81. package/dist/react/firestore/use-firestore-infinite.d.ts.map +1 -1
  82. package/dist/react/firestore/use-firestore-infinite.js +0 -32
  83. package/dist/react/firestore/use-firestore-infinite.js.map +1 -1
  84. package/dist/react/firestore/use-firestore-live-infinite.d.ts +0 -34
  85. package/dist/react/firestore/use-firestore-live-infinite.d.ts.map +1 -1
  86. package/dist/react/firestore/use-firestore-live-infinite.js +1 -35
  87. package/dist/react/firestore/use-firestore-live-infinite.js.map +1 -1
  88. package/dist/react/firestore/use-firestore-mutations.d.ts.map +1 -1
  89. package/dist/react/firestore/use-firestore-mutations.js +6 -5
  90. package/dist/react/firestore/use-firestore-mutations.js.map +1 -1
  91. package/dist/react/firestore/use-firestore-paginated.d.ts +0 -30
  92. package/dist/react/firestore/use-firestore-paginated.d.ts.map +1 -1
  93. package/dist/react/firestore/use-firestore-paginated.js +2 -33
  94. package/dist/react/firestore/use-firestore-paginated.js.map +1 -1
  95. package/dist/react/firestore/useBatchFirestoreDocs.d.ts +1 -123
  96. package/dist/react/firestore/useBatchFirestoreDocs.d.ts.map +1 -1
  97. package/dist/react/firestore/useBatchFirestoreDocs.js +23 -64
  98. package/dist/react/firestore/useBatchFirestoreDocs.js.map +1 -1
  99. package/dist/react/index.d.ts +4 -0
  100. package/dist/react/index.d.ts.map +1 -1
  101. package/dist/react/index.js +2 -13
  102. package/dist/react/index.js.map +1 -1
  103. package/dist/react/pending-mutations.d.ts +1 -21
  104. package/dist/react/pending-mutations.d.ts.map +1 -1
  105. package/dist/react/pending-mutations.js +1 -32
  106. package/dist/react/pending-mutations.js.map +1 -1
  107. package/dist/react/provider.d.ts +0 -5
  108. package/dist/react/provider.d.ts.map +1 -1
  109. package/dist/react/provider.js +0 -4
  110. package/dist/react/provider.js.map +1 -1
  111. package/dist/react/query-client.d.ts +0 -4
  112. package/dist/react/query-client.d.ts.map +1 -1
  113. package/dist/react/query-client.js +0 -4
  114. package/dist/react/query-client.js.map +1 -1
  115. package/dist/react/search/types.d.ts +0 -12
  116. package/dist/react/search/types.d.ts.map +1 -1
  117. package/dist/react/search/types.js.map +1 -1
  118. package/dist/react/search/use-firestore-search.d.ts +0 -29
  119. package/dist/react/search/use-firestore-search.d.ts.map +1 -1
  120. package/dist/react/search/use-firestore-search.js +3 -41
  121. package/dist/react/search/use-firestore-search.js.map +1 -1
  122. package/dist/react/session-mutation.d.ts +27 -0
  123. package/dist/react/session-mutation.d.ts.map +1 -0
  124. package/dist/react/session-mutation.js +150 -0
  125. package/dist/react/session-mutation.js.map +1 -0
  126. package/dist/react/use-read-retry-state.d.ts +1 -21
  127. package/dist/react/use-read-retry-state.d.ts.map +1 -1
  128. package/dist/react/use-read-retry-state.js +2 -21
  129. package/dist/react/use-read-retry-state.js.map +1 -1
  130. package/dist/search/rule.d.ts +0 -6
  131. package/dist/search/rule.d.ts.map +1 -1
  132. package/dist/search/rule.js +0 -6
  133. package/dist/search/rule.js.map +1 -1
  134. package/dist/search/types.d.ts +1 -13
  135. package/dist/search/types.d.ts.map +1 -1
  136. package/dist/search/types.js.map +1 -1
  137. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"batch-doc-loader.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/batch-doc-loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGzD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,uEAAuE;AACvE,eAAO,MAAM,6BAA6B,IAAI,CAAC;AAE/C,qEAAqE;AACrE,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAOjE,MAAM,MAAM,SAAS,GAAG;IACtB,gEAAgE;IAChE,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IACzE,qFAAqF;IACrF,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;CACtE,CAAC;AAmJF,wBAAgB,YAAY,CAC1B,WAAW,EAAE,WAAW,EACxB,EAAE,EAAE,SAAS,EACb,cAAc,EAAE,MAAM,GACrB,SAAS,CAiBX"}
1
+ {"version":3,"file":"batch-doc-loader.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/batch-doc-loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGzD,eAAO,MAAM,6BAA6B,IAAI,CAAC;AAE/C,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAOjE,MAAM,MAAM,SAAS,GAAG;IACtB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IACzE,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;CACtE,CAAC;AA6IF,wBAAgB,YAAY,CAC1B,WAAW,EAAE,WAAW,EACxB,EAAE,EAAE,SAAS,EACb,cAAc,EAAE,MAAM,GACrB,SAAS,CAiBX"}
@@ -1,37 +1,8 @@
1
1
  'use client';
2
2
  import { collection, doc, documentId, getDoc, getDocs, query, where, } from 'firebase/firestore';
3
3
  import { chunk, FIRESTORE_IN_FILTER_LIMIT } from '@ttt-productions/firebase-helpers';
4
- /**
5
- * Coalescing document loader behind {@link useBatchFirestoreDocs}'s one-shot mode.
6
- *
7
- * Every id is its own observed React Query query, so without coalescing N ids would issue
8
- * N Firestore round-trips. The loader collects the ids enqueued in a single microtask and
9
- * dispatches them as `where(documentId(), 'in', chunk)` queries of at most `FIRESTORE_IN_FILTER_LIMIT` ids each
10
- * (`'batch'` transport), or as concurrent per-document gets (`'get'` transport — required
11
- * for collections whose rules gate reads on `resource.data`, where an unconstrained list
12
- * query is denied wholesale but per-document gets are evaluated per doc).
13
- *
14
- * Two properties are load-bearing:
15
- *
16
- * - **The loader resolves promises only — it NEVER writes the query cache.** TanStack's
17
- * per-query fetch identity is therefore the sole guard against a late batch response
18
- * overwriting a newer per-id result.
19
- * - **A dispatched batch is closed.** A later enqueue of the same id (a refetch after a
20
- * cancellation, say) always joins a NEW batch, never the in-flight one — so a cancelled
21
- * read can never be served by the request it was cancelled out of.
22
- *
23
- * Cancelling one per-id query detaches only that waiter; the shared request still completes
24
- * for its batch-mates.
25
- *
26
- * Loaders are scoped per `(QueryClient, Firestore instance, collectionPath)`. The Firestore
27
- * instance is part of the scope on purpose: two `db` instances sharing a collection path
28
- * must never share a request.
29
- */
30
- /** Maximum `in`-query requests in flight at once, per loader scope. */
31
4
  export const MAX_CONCURRENT_BATCH_REQUESTS = 4;
32
- /** Maximum per-document gets in flight at once, per loader scope. */
33
5
  export const MAX_CONCURRENT_DOC_GETS = 12;
34
- /** Runs at most `max` tasks concurrently; a slot is released when its task settles. */
35
6
  function createConcurrencyLimiter(max) {
36
7
  let active = 0;
37
8
  const waiting = [];
@@ -65,8 +36,6 @@ function abortReason(signal) {
65
36
  function createDocLoader(db, collectionPath) {
66
37
  const runBatchRequest = createConcurrencyLimiter(MAX_CONCURRENT_BATCH_REQUESTS);
67
38
  const runDocGet = createConcurrencyLimiter(MAX_CONCURRENT_DOC_GETS);
68
- // Ids enqueued in the CURRENT microtask. Nulled out at dispatch so the dispatched batch
69
- // is closed and a later enqueue opens a fresh one.
70
39
  let openBatch = null;
71
40
  const runChunk = (chunk, batch) => runBatchRequest(async () => {
72
41
  let found = null;
@@ -128,7 +97,6 @@ function createDocLoader(db, collectionPath) {
128
97
  const index = waiters.indexOf(waiter);
129
98
  if (index >= 0)
130
99
  waiters.splice(index, 1);
131
- // An id nobody is waiting on any more must not ride into a billed `in` query.
132
100
  if (waiters.length === 0 && batch.get(id) === waiters)
133
101
  batch.delete(id);
134
102
  if (onAbort)
@@ -153,8 +121,6 @@ function createDocLoader(db, collectionPath) {
153
121
  });
154
122
  return { loadBatched, loadOne };
155
123
  }
156
- // Per-QueryClient → per-Firestore-instance → per-collectionPath. WeakMaps so a discarded
157
- // client or db instance takes its loaders with it.
158
124
  const loaderScopes = new WeakMap();
159
125
  export function getDocLoader(queryClient, db, collectionPath) {
160
126
  let byDb = loaderScopes.get(queryClient);
@@ -1 +1 @@
1
- {"version":3,"file":"batch-doc-loader.js","sourceRoot":"","sources":["../../../src/react/firestore/batch-doc-loader.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport {\n collection,\n doc,\n documentId,\n getDoc,\n getDocs,\n query,\n where,\n type Firestore,\n} from 'firebase/firestore';\nimport type { QueryClient } from '@tanstack/react-query';\nimport { chunk, FIRESTORE_IN_FILTER_LIMIT } from '@ttt-productions/firebase-helpers';\n\n/**\n * Coalescing document loader behind {@link useBatchFirestoreDocs}'s one-shot mode.\n *\n * Every id is its own observed React Query query, so without coalescing N ids would issue\n * N Firestore round-trips. The loader collects the ids enqueued in a single microtask and\n * dispatches them as `where(documentId(), 'in', chunk)` queries of at most `FIRESTORE_IN_FILTER_LIMIT` ids each\n * (`'batch'` transport), or as concurrent per-document gets (`'get'` transport — required\n * for collections whose rules gate reads on `resource.data`, where an unconstrained list\n * query is denied wholesale but per-document gets are evaluated per doc).\n *\n * Two properties are load-bearing:\n *\n * - **The loader resolves promises only — it NEVER writes the query cache.** TanStack's\n * per-query fetch identity is therefore the sole guard against a late batch response\n * overwriting a newer per-id result.\n * - **A dispatched batch is closed.** A later enqueue of the same id (a refetch after a\n * cancellation, say) always joins a NEW batch, never the in-flight one — so a cancelled\n * read can never be served by the request it was cancelled out of.\n *\n * Cancelling one per-id query detaches only that waiter; the shared request still completes\n * for its batch-mates.\n *\n * Loaders are scoped per `(QueryClient, Firestore instance, collectionPath)`. The Firestore\n * instance is part of the scope on purpose: two `db` instances sharing a collection path\n * must never share a request.\n */\n\n/** Maximum `in`-query requests in flight at once, per loader scope. */\nexport const MAX_CONCURRENT_BATCH_REQUESTS = 4;\n\n/** Maximum per-document gets in flight at once, per loader scope. */\nexport const MAX_CONCURRENT_DOC_GETS = 12;\n\nexport type LoadedDoc = Record<string, unknown> & { id: string };\n\ntype Waiter = {\n resolve: (value: LoadedDoc | null) => void;\n reject: (error: unknown) => void;\n};\n\nexport type DocLoader = {\n /** Resolve one id through the coalescing `in`-query batcher. */\n loadBatched(id: string, signal?: AbortSignal): Promise<LoadedDoc | null>;\n /** Resolve one id through its own `getDoc`, bounded by the get concurrency limit. */\n loadOne(id: string, signal?: AbortSignal): Promise<LoadedDoc | null>;\n};\n\n/** Runs at most `max` tasks concurrently; a slot is released when its task settles. */\nfunction createConcurrencyLimiter(max: number) {\n let active = 0;\n const waiting: (() => void)[] = [];\n\n const pump = () => {\n while (active < max && waiting.length > 0) {\n const start = waiting.shift();\n active += 1;\n start?.();\n }\n };\n\n const release = () => {\n active -= 1;\n pump();\n };\n\n return <R>(task: () => Promise<R>): Promise<R> =>\n new Promise<R>((resolve, reject) => {\n waiting.push(() => {\n task().then(\n (value) => {\n release();\n resolve(value);\n },\n (error) => {\n release();\n reject(error);\n },\n );\n });\n pump();\n });\n}\n\nfunction abortReason(signal: AbortSignal): unknown {\n return signal.reason ?? new Error('Document read was cancelled');\n}\n\nfunction createDocLoader(db: Firestore, collectionPath: string): DocLoader {\n const runBatchRequest = createConcurrencyLimiter(MAX_CONCURRENT_BATCH_REQUESTS);\n const runDocGet = createConcurrencyLimiter(MAX_CONCURRENT_DOC_GETS);\n\n // Ids enqueued in the CURRENT microtask. Nulled out at dispatch so the dispatched batch\n // is closed and a later enqueue opens a fresh one.\n let openBatch: Map<string, Waiter[]> | null = null;\n\n const runChunk = (chunk: string[], batch: Map<string, Waiter[]>) =>\n runBatchRequest(async () => {\n let found: Map<string, LoadedDoc> | null = null;\n let failure: unknown;\n try {\n const snapshot = await getDocs(\n query(collection(db, collectionPath), where(documentId(), 'in', chunk)),\n );\n found = new Map<string, LoadedDoc>();\n snapshot.forEach((snap) => {\n found!.set(snap.id, { id: snap.id, ...snap.data() } as LoadedDoc);\n });\n } catch (error) {\n failure = error;\n }\n\n for (const id of chunk) {\n const waiters = batch.get(id);\n batch.delete(id);\n if (!waiters) continue;\n for (const waiter of [...waiters]) {\n if (found) waiter.resolve(found.get(id) ?? null);\n else waiter.reject(failure);\n }\n }\n });\n\n const dispatch = () => {\n const batch = openBatch;\n openBatch = null;\n if (!batch || batch.size === 0) return;\n for (const ids of chunk([...batch.keys()], FIRESTORE_IN_FILTER_LIMIT)) {\n void runChunk(ids, batch);\n }\n };\n\n const loadBatched = (id: string, signal?: AbortSignal): Promise<LoadedDoc | null> =>\n new Promise<LoadedDoc | null>((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortReason(signal));\n return;\n }\n\n if (!openBatch) {\n openBatch = new Map();\n queueMicrotask(dispatch);\n }\n\n const batch = openBatch;\n let slot = batch.get(id);\n if (!slot) {\n slot = [];\n batch.set(id, slot);\n }\n const waiters = slot;\n\n let settled = false;\n let onAbort: (() => void) | undefined;\n\n const settle = (run: () => void) => {\n if (settled) return;\n settled = true;\n const index = waiters.indexOf(waiter);\n if (index >= 0) waiters.splice(index, 1);\n // An id nobody is waiting on any more must not ride into a billed `in` query.\n if (waiters.length === 0 && batch.get(id) === waiters) batch.delete(id);\n if (onAbort) signal?.removeEventListener('abort', onAbort);\n run();\n };\n\n const waiter: Waiter = {\n resolve: (value) => settle(() => resolve(value)),\n reject: (error) => settle(() => reject(error)),\n };\n\n waiters.push(waiter);\n\n if (signal) {\n onAbort = () => waiter.reject(abortReason(signal));\n signal.addEventListener('abort', onAbort, { once: true });\n }\n });\n\n const loadOne = (id: string, signal?: AbortSignal): Promise<LoadedDoc | null> =>\n runDocGet(async () => {\n if (signal?.aborted) throw abortReason(signal);\n const snapshot = await getDoc(doc(db, collectionPath, id));\n return snapshot.exists() ? ({ id: snapshot.id, ...snapshot.data() } as LoadedDoc) : null;\n });\n\n return { loadBatched, loadOne };\n}\n\n// Per-QueryClient → per-Firestore-instance → per-collectionPath. WeakMaps so a discarded\n// client or db instance takes its loaders with it.\nconst loaderScopes = new WeakMap<QueryClient, WeakMap<Firestore, Map<string, DocLoader>>>();\n\nexport function getDocLoader(\n queryClient: QueryClient,\n db: Firestore,\n collectionPath: string,\n): DocLoader {\n let byDb = loaderScopes.get(queryClient);\n if (!byDb) {\n byDb = new WeakMap();\n loaderScopes.set(queryClient, byDb);\n }\n let byPath = byDb.get(db);\n if (!byPath) {\n byPath = new Map();\n byDb.set(db, byPath);\n }\n let loader = byPath.get(collectionPath);\n if (!loader) {\n loader = createDocLoader(db, collectionPath);\n byPath.set(collectionPath, loader);\n }\n return loader;\n}\n"]}
1
+ {"version":3,"file":"batch-doc-loader.js","sourceRoot":"","sources":["../../../src/react/firestore/batch-doc-loader.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport {\n collection,\n doc,\n documentId,\n getDoc,\n getDocs,\n query,\n where,\n type Firestore,\n} from 'firebase/firestore';\nimport type { QueryClient } from '@tanstack/react-query';\nimport { chunk, FIRESTORE_IN_FILTER_LIMIT } from '@ttt-productions/firebase-helpers';\n\nexport const MAX_CONCURRENT_BATCH_REQUESTS = 4;\n\nexport const MAX_CONCURRENT_DOC_GETS = 12;\n\nexport type LoadedDoc = Record<string, unknown> & { id: string };\n\ntype Waiter = {\n resolve: (value: LoadedDoc | null) => void;\n reject: (error: unknown) => void;\n};\n\nexport type DocLoader = {\n loadBatched(id: string, signal?: AbortSignal): Promise<LoadedDoc | null>;\n loadOne(id: string, signal?: AbortSignal): Promise<LoadedDoc | null>;\n};\n\nfunction createConcurrencyLimiter(max: number) {\n let active = 0;\n const waiting: (() => void)[] = [];\n\n const pump = () => {\n while (active < max && waiting.length > 0) {\n const start = waiting.shift();\n active += 1;\n start?.();\n }\n };\n\n const release = () => {\n active -= 1;\n pump();\n };\n\n return <R>(task: () => Promise<R>): Promise<R> =>\n new Promise<R>((resolve, reject) => {\n waiting.push(() => {\n task().then(\n (value) => {\n release();\n resolve(value);\n },\n (error) => {\n release();\n reject(error);\n },\n );\n });\n pump();\n });\n}\n\nfunction abortReason(signal: AbortSignal): unknown {\n return signal.reason ?? new Error('Document read was cancelled');\n}\n\nfunction createDocLoader(db: Firestore, collectionPath: string): DocLoader {\n const runBatchRequest = createConcurrencyLimiter(MAX_CONCURRENT_BATCH_REQUESTS);\n const runDocGet = createConcurrencyLimiter(MAX_CONCURRENT_DOC_GETS);\n\n let openBatch: Map<string, Waiter[]> | null = null;\n\n const runChunk = (chunk: string[], batch: Map<string, Waiter[]>) =>\n runBatchRequest(async () => {\n let found: Map<string, LoadedDoc> | null = null;\n let failure: unknown;\n try {\n const snapshot = await getDocs(\n query(collection(db, collectionPath), where(documentId(), 'in', chunk)),\n );\n found = new Map<string, LoadedDoc>();\n snapshot.forEach((snap) => {\n found!.set(snap.id, { id: snap.id, ...snap.data() } as LoadedDoc);\n });\n } catch (error) {\n failure = error;\n }\n\n for (const id of chunk) {\n const waiters = batch.get(id);\n batch.delete(id);\n if (!waiters) continue;\n for (const waiter of [...waiters]) {\n if (found) waiter.resolve(found.get(id) ?? null);\n else waiter.reject(failure);\n }\n }\n });\n\n const dispatch = () => {\n const batch = openBatch;\n openBatch = null;\n if (!batch || batch.size === 0) return;\n for (const ids of chunk([...batch.keys()], FIRESTORE_IN_FILTER_LIMIT)) {\n void runChunk(ids, batch);\n }\n };\n\n const loadBatched = (id: string, signal?: AbortSignal): Promise<LoadedDoc | null> =>\n new Promise<LoadedDoc | null>((resolve, reject) => {\n if (signal?.aborted) {\n reject(abortReason(signal));\n return;\n }\n\n if (!openBatch) {\n openBatch = new Map();\n queueMicrotask(dispatch);\n }\n\n const batch = openBatch;\n let slot = batch.get(id);\n if (!slot) {\n slot = [];\n batch.set(id, slot);\n }\n const waiters = slot;\n\n let settled = false;\n let onAbort: (() => void) | undefined;\n\n const settle = (run: () => void) => {\n if (settled) return;\n settled = true;\n const index = waiters.indexOf(waiter);\n if (index >= 0) waiters.splice(index, 1);\n if (waiters.length === 0 && batch.get(id) === waiters) batch.delete(id);\n if (onAbort) signal?.removeEventListener('abort', onAbort);\n run();\n };\n\n const waiter: Waiter = {\n resolve: (value) => settle(() => resolve(value)),\n reject: (error) => settle(() => reject(error)),\n };\n\n waiters.push(waiter);\n\n if (signal) {\n onAbort = () => waiter.reject(abortReason(signal));\n signal.addEventListener('abort', onAbort, { once: true });\n }\n });\n\n const loadOne = (id: string, signal?: AbortSignal): Promise<LoadedDoc | null> =>\n runDocGet(async () => {\n if (signal?.aborted) throw abortReason(signal);\n const snapshot = await getDoc(doc(db, collectionPath, id));\n return snapshot.exists() ? ({ id: snapshot.id, ...snapshot.data() } as LoadedDoc) : null;\n });\n\n return { loadBatched, loadOne };\n}\n\nconst loaderScopes = new WeakMap<QueryClient, WeakMap<Firestore, Map<string, DocLoader>>>();\n\nexport function getDocLoader(\n queryClient: QueryClient,\n db: Firestore,\n collectionPath: string,\n): DocLoader {\n let byDb = loaderScopes.get(queryClient);\n if (!byDb) {\n byDb = new WeakMap();\n loaderScopes.set(queryClient, byDb);\n }\n let byPath = byDb.get(db);\n if (!byPath) {\n byPath = new Map();\n byDb.set(db, byPath);\n }\n let loader = byPath.get(collectionPath);\n if (!loader) {\n loader = createDocLoader(db, collectionPath);\n byPath.set(collectionPath, loader);\n }\n return loader;\n}\n"]}
@@ -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;AAoBzD,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,CA0DnE;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,MAAM,CAEtE"}
@@ -1,26 +1,13 @@
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.
3
+ import { isPermissionDeniedError } from './resubscribe.js';
4
4
  const registries = new WeakMap();
5
5
  function registryKey(queryKeyPrefix, id) {
6
6
  return `${queryKeyPrefix} ${id}`;
7
7
  }
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
8
  export function isListenerOwned(queryClient, queryKeyPrefix, id) {
17
9
  return registries.get(queryClient)?.has(registryKey(queryKeyPrefix, id)) ?? false;
18
10
  }
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
11
  export function listenerErrorOf(queryClient, queryKeyPrefix, id) {
25
12
  return registries.get(queryClient)?.get(registryKey(queryKeyPrefix, id))?.error ?? null;
26
13
  }
@@ -32,11 +19,6 @@ function getRegistry(queryClient) {
32
19
  }
33
20
  return registry;
34
21
  }
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
22
  export function subscribeDoc(params) {
41
23
  const { queryClient, db, collectionPath, queryKeyPrefix, id, onUpdate, onError } = params;
42
24
  const registry = getRegistry(queryClient);
@@ -57,11 +39,11 @@ export function subscribeDoc(params) {
57
39
  for (const sub of [...subscribers])
58
40
  sub.onUpdate();
59
41
  }, (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
42
  created.error = error;
64
- queryClient.setQueryData([queryKeyPrefix, id], null, { updatedAt: Date.now() });
43
+ const held = queryClient.getQueryData([queryKeyPrefix, id]);
44
+ if (held === undefined || held === null || isPermissionDeniedError(error)) {
45
+ queryClient.setQueryData([queryKeyPrefix, id], null, { updatedAt: Date.now() });
46
+ }
65
47
  for (const sub of [...subscribers])
66
48
  sub.onError?.(error);
67
49
  });
@@ -84,15 +66,12 @@ export function subscribeDoc(params) {
84
66
  if (current.subscribers.size === 0) {
85
67
  current.unsubscribe();
86
68
  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
69
  if (current.error) {
90
70
  void queryClient.invalidateQueries({ queryKey: [queryKeyPrefix, id], exact: true });
91
71
  }
92
72
  }
93
73
  };
94
74
  }
95
- /** Test-only: number of active shared listeners for a QueryClient. */
96
75
  export function __activeListenerCount(queryClient) {
97
76
  return registries.get(queryClient)?.size ?? 0;
98
77
  }
@@ -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';\nimport { isPermissionDeniedError } from './resubscribe.js';\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 const held = queryClient.getQueryData([queryKeyPrefix, id]);\n if (held === undefined || held === null || isPermissionDeniedError(error)) {\n queryClient.setQueryData([queryKeyPrefix, id], null, { updatedAt: Date.now() });\n }\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"}