@ttt-productions/query-core 0.14.21 → 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.
- package/README.md +6 -6
- package/dist/defaults.d.ts +0 -22
- package/dist/defaults.d.ts.map +1 -1
- package/dist/defaults.js +0 -35
- package/dist/defaults.js.map +1 -1
- package/dist/firestore/types.d.ts +1 -191
- package/dist/firestore/types.d.ts.map +1 -1
- package/dist/firestore/types.js +1 -6
- package/dist/firestore/types.js.map +1 -1
- package/dist/index.d.ts +2 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -8
- package/dist/index.js.map +1 -1
- package/dist/keys.d.ts +0 -22
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +0 -4
- package/dist/keys.js.map +1 -1
- package/dist/react/cache-helpers.d.ts +16 -0
- package/dist/react/cache-helpers.d.ts.map +1 -0
- package/dist/{cache-helpers.js → react/cache-helpers.js} +0 -28
- package/dist/react/cache-helpers.js.map +1 -0
- package/dist/react/defaults.d.ts +4 -0
- package/dist/react/defaults.d.ts.map +1 -0
- package/dist/react/defaults.js +17 -0
- package/dist/react/defaults.js.map +1 -0
- package/dist/react/domain-events/cache-invalidation.d.ts +33 -0
- package/dist/react/domain-events/cache-invalidation.d.ts.map +1 -0
- package/dist/react/domain-events/cache-invalidation.js +74 -0
- package/dist/react/domain-events/cache-invalidation.js.map +1 -0
- package/dist/react/domain-events/create-domain-event-invalidator.d.ts +28 -0
- package/dist/react/domain-events/create-domain-event-invalidator.d.ts.map +1 -0
- package/dist/{domain-events → react/domain-events}/create-domain-event-invalidator.js +1 -6
- package/dist/react/domain-events/create-domain-event-invalidator.js.map +1 -0
- package/dist/react/firestore/absence-scheduler.d.ts +0 -28
- package/dist/react/firestore/absence-scheduler.d.ts.map +1 -1
- package/dist/react/firestore/absence-scheduler.js +1 -39
- package/dist/react/firestore/absence-scheduler.js.map +1 -1
- package/dist/react/firestore/batch-doc-loader.d.ts +0 -30
- package/dist/react/firestore/batch-doc-loader.d.ts.map +1 -1
- package/dist/react/firestore/batch-doc-loader.js +0 -34
- package/dist/react/firestore/batch-doc-loader.js.map +1 -1
- package/dist/react/firestore/context.d.ts +0 -19
- package/dist/react/firestore/context.d.ts.map +1 -1
- package/dist/react/firestore/context.js +1 -20
- package/dist/react/firestore/context.js.map +1 -1
- package/dist/react/firestore/doc-subscription-registry.d.ts +0 -23
- package/dist/react/firestore/doc-subscription-registry.d.ts.map +1 -1
- package/dist/react/firestore/doc-subscription-registry.js +5 -26
- package/dist/react/firestore/doc-subscription-registry.js.map +1 -1
- package/dist/react/firestore/infinite-helpers.d.ts +9 -0
- package/dist/react/firestore/infinite-helpers.d.ts.map +1 -0
- package/dist/react/firestore/infinite-helpers.js +11 -0
- package/dist/react/firestore/infinite-helpers.js.map +1 -0
- package/dist/react/firestore/resubscribe.d.ts +0 -20
- package/dist/react/firestore/resubscribe.d.ts.map +1 -1
- package/dist/react/firestore/resubscribe.js +0 -20
- package/dist/react/firestore/resubscribe.js.map +1 -1
- package/dist/react/firestore/subscription-status.d.ts +0 -29
- package/dist/react/firestore/subscription-status.d.ts.map +1 -1
- package/dist/react/firestore/subscription-status.js +1 -34
- package/dist/react/firestore/subscription-status.js.map +1 -1
- package/dist/react/firestore/types.d.ts +80 -0
- package/dist/react/firestore/types.d.ts.map +1 -0
- package/dist/react/firestore/types.js +4 -0
- package/dist/react/firestore/types.js.map +1 -0
- package/dist/react/firestore/use-firestore-collection.d.ts +2 -21
- package/dist/react/firestore/use-firestore-collection.d.ts.map +1 -1
- package/dist/react/firestore/use-firestore-collection.js +23 -44
- package/dist/react/firestore/use-firestore-collection.js.map +1 -1
- package/dist/react/firestore/use-firestore-count.d.ts +1 -16
- package/dist/react/firestore/use-firestore-count.d.ts.map +1 -1
- package/dist/react/firestore/use-firestore-count.js +0 -15
- package/dist/react/firestore/use-firestore-count.js.map +1 -1
- package/dist/react/firestore/use-firestore-doc.d.ts +2 -24
- package/dist/react/firestore/use-firestore-doc.d.ts.map +1 -1
- package/dist/react/firestore/use-firestore-doc.js +21 -42
- package/dist/react/firestore/use-firestore-doc.js.map +1 -1
- package/dist/react/firestore/use-firestore-infinite.d.ts +1 -30
- package/dist/react/firestore/use-firestore-infinite.d.ts.map +1 -1
- package/dist/react/firestore/use-firestore-infinite.js +0 -32
- package/dist/react/firestore/use-firestore-infinite.js.map +1 -1
- package/dist/react/firestore/use-firestore-live-infinite.d.ts +2 -35
- package/dist/react/firestore/use-firestore-live-infinite.d.ts.map +1 -1
- package/dist/react/firestore/use-firestore-live-infinite.js +10 -36
- package/dist/react/firestore/use-firestore-live-infinite.js.map +1 -1
- package/dist/react/firestore/use-firestore-mutations.d.ts +2 -1
- package/dist/react/firestore/use-firestore-mutations.d.ts.map +1 -1
- package/dist/react/firestore/use-firestore-mutations.js +3 -3
- package/dist/react/firestore/use-firestore-mutations.js.map +1 -1
- package/dist/react/firestore/use-firestore-paginated.d.ts +2 -31
- package/dist/react/firestore/use-firestore-paginated.d.ts.map +1 -1
- package/dist/react/firestore/use-firestore-paginated.js +8 -36
- package/dist/react/firestore/use-firestore-paginated.js.map +1 -1
- package/dist/react/firestore/useBatchFirestoreDocs.d.ts +1 -123
- package/dist/react/firestore/useBatchFirestoreDocs.d.ts.map +1 -1
- package/dist/react/firestore/useBatchFirestoreDocs.js +12 -61
- package/dist/react/firestore/useBatchFirestoreDocs.js.map +1 -1
- package/dist/react/index.d.ts +13 -4
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +8 -13
- package/dist/react/index.js.map +1 -1
- package/dist/react/pending-mutations.d.ts +2 -22
- package/dist/react/pending-mutations.d.ts.map +1 -1
- package/dist/react/pending-mutations.js +7 -38
- package/dist/react/pending-mutations.js.map +1 -1
- package/dist/react/provider.d.ts +1 -6
- package/dist/react/provider.d.ts.map +1 -1
- package/dist/react/provider.js +1 -5
- package/dist/react/provider.js.map +1 -1
- package/dist/{query-client.d.ts → react/query-client.d.ts} +0 -4
- package/dist/react/query-client.d.ts.map +1 -0
- package/dist/{query-client.js → react/query-client.js} +0 -4
- package/dist/react/query-client.js.map +1 -0
- package/dist/react/search/types.d.ts +16 -0
- package/dist/react/search/types.d.ts.map +1 -0
- package/dist/react/search/types.js +2 -0
- package/dist/react/search/types.js.map +1 -0
- package/dist/react/search/use-firestore-search.d.ts +1 -30
- package/dist/react/search/use-firestore-search.d.ts.map +1 -1
- package/dist/react/search/use-firestore-search.js +3 -41
- package/dist/react/search/use-firestore-search.js.map +1 -1
- package/dist/react/use-read-retry-state.d.ts +1 -21
- package/dist/react/use-read-retry-state.d.ts.map +1 -1
- package/dist/react/use-read-retry-state.js +3 -22
- package/dist/react/use-read-retry-state.js.map +1 -1
- package/dist/search/rule.d.ts +0 -6
- package/dist/search/rule.d.ts.map +1 -1
- package/dist/search/rule.js +0 -6
- package/dist/search/rule.js.map +1 -1
- package/dist/search/types.d.ts +1 -39
- package/dist/search/types.d.ts.map +1 -1
- package/dist/search/types.js.map +1 -1
- package/package.json +2 -2
- package/dist/cache-helpers.d.ts +0 -48
- package/dist/cache-helpers.d.ts.map +0 -1
- package/dist/cache-helpers.js.map +0 -1
- package/dist/domain-events/cache-invalidation.d.ts +0 -70
- package/dist/domain-events/cache-invalidation.d.ts.map +0 -1
- package/dist/domain-events/cache-invalidation.js +0 -123
- package/dist/domain-events/cache-invalidation.js.map +0 -1
- package/dist/domain-events/create-domain-event-invalidator.d.ts +0 -46
- package/dist/domain-events/create-domain-event-invalidator.d.ts.map +0 -1
- package/dist/domain-events/create-domain-event-invalidator.js.map +0 -1
- package/dist/domain-events/index.d.ts +0 -5
- package/dist/domain-events/index.d.ts.map +0 -1
- package/dist/domain-events/index.js +0 -3
- package/dist/domain-events/index.js.map +0 -1
- package/dist/firestore/infinite-helpers.d.ts +0 -20
- package/dist/firestore/infinite-helpers.d.ts.map +0 -1
- package/dist/firestore/infinite-helpers.js +0 -23
- package/dist/firestore/infinite-helpers.js.map +0 -1
- package/dist/query-client.d.ts.map +0 -1
- package/dist/query-client.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/react/defaults.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE/E,eAAO,MAAM,mBAAmB,EAAE,cAcjC,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,iBAEtC,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const defaultQueryOptions = {
|
|
2
|
+
queries: {
|
|
3
|
+
refetchOnWindowFocus: false,
|
|
4
|
+
refetchOnReconnect: true,
|
|
5
|
+
refetchOnMount: false,
|
|
6
|
+
staleTime: 30_000,
|
|
7
|
+
gcTime: 5 * 60_000,
|
|
8
|
+
retry: (failureCount) => failureCount < 2,
|
|
9
|
+
},
|
|
10
|
+
mutations: {
|
|
11
|
+
retry: 0,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
export const defaultQueryClientConfig = {
|
|
15
|
+
defaultOptions: defaultQueryOptions,
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=defaults.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../src/react/defaults.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,mBAAmB,GAAmB;IACjD,OAAO,EAAE;QACP,oBAAoB,EAAE,KAAK;QAC3B,kBAAkB,EAAE,IAAI;QACxB,cAAc,EAAE,KAAK;QAErB,SAAS,EAAE,MAAM;QACjB,MAAM,EAAE,CAAC,GAAG,MAAM;QAElB,KAAK,EAAE,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,GAAG,CAAC;KAC1C;IACD,SAAS,EAAE;QACT,KAAK,EAAE,CAAC;KACT;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,wBAAwB,GAAsB;IACzD,cAAc,EAAE,mBAAmB;CACpC,CAAC","sourcesContent":["import type { DefaultOptions, QueryClientConfig } from '@tanstack/react-query';\n\nexport const defaultQueryOptions: DefaultOptions = {\n queries: {\n refetchOnWindowFocus: false,\n refetchOnReconnect: true,\n refetchOnMount: false,\n\n staleTime: 30_000,\n gcTime: 5 * 60_000,\n\n retry: (failureCount) => failureCount < 2,\n },\n mutations: {\n retry: 0,\n },\n};\n\nexport const defaultQueryClientConfig: QueryClientConfig = {\n defaultOptions: defaultQueryOptions,\n};\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Query, QueryClient, QueryKey } from '@tanstack/react-query';
|
|
2
|
+
export type RefetchType = 'active' | 'all' | 'none';
|
|
3
|
+
export type CacheAction = 'refresh' | 'remove' | 'reset';
|
|
4
|
+
export type CacheInvalidation = {
|
|
5
|
+
kind: 'exact';
|
|
6
|
+
queryKey: QueryKey;
|
|
7
|
+
refetchType?: RefetchType;
|
|
8
|
+
action?: CacheAction;
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'prefix';
|
|
11
|
+
queryKey: QueryKey;
|
|
12
|
+
refetchType?: RefetchType;
|
|
13
|
+
action?: CacheAction;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'predicate';
|
|
16
|
+
description: string;
|
|
17
|
+
match: (query: Query) => boolean;
|
|
18
|
+
refetchType?: RefetchType;
|
|
19
|
+
action?: CacheAction;
|
|
20
|
+
};
|
|
21
|
+
type EntryOptions = {
|
|
22
|
+
refetchType?: RefetchType;
|
|
23
|
+
action?: 'refresh';
|
|
24
|
+
} | {
|
|
25
|
+
action: 'remove' | 'reset';
|
|
26
|
+
};
|
|
27
|
+
export declare function exact(queryKey: QueryKey, opts?: EntryOptions): CacheInvalidation;
|
|
28
|
+
export declare function prefix(queryKey: QueryKey, opts?: EntryOptions): CacheInvalidation;
|
|
29
|
+
export declare function predicate(description: string, match: (query: Query) => boolean, opts?: EntryOptions): CacheInvalidation;
|
|
30
|
+
export declare function serializeInvalidation(inv: CacheInvalidation): string;
|
|
31
|
+
export declare function applyInvalidations(queryClient: QueryClient, invalidations: ReadonlyArray<CacheInvalidation>): Promise<void>;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=cache-invalidation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-invalidation.d.ts","sourceRoot":"","sources":["../../../src/react/domain-events/cache-invalidation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAgB,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAExF,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;AAEpD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEzD,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,WAAW,CAAC,EAAE,WAAW,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GACtF;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,WAAW,CAAC,EAAE,WAAW,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GACvF;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IACjC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEN,KAAK,YAAY,GACf;IAAE,WAAW,CAAC,EAAE,WAAW,CAAC;IAAC,MAAM,CAAC,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAA;CAAE,CAAC;AAOrF,wBAAgB,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,iBAAiB,CAEhF;AAED,wBAAgB,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,iBAAiB,CAEjF;AAED,wBAAgB,SAAS,CACvB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,EAChC,IAAI,CAAC,EAAE,YAAY,GAClB,iBAAiB,CAEnB;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAIpE;AA0BD,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE,aAAa,CAAC,iBAAiB,CAAC,GAC9C,OAAO,CAAC,IAAI,CAAC,CA+Bf"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const entryFields = (opts) => ({
|
|
2
|
+
refetchType: opts && 'refetchType' in opts ? opts.refetchType : undefined,
|
|
3
|
+
action: opts?.action,
|
|
4
|
+
});
|
|
5
|
+
export function exact(queryKey, opts) {
|
|
6
|
+
return { kind: 'exact', queryKey, ...entryFields(opts) };
|
|
7
|
+
}
|
|
8
|
+
export function prefix(queryKey, opts) {
|
|
9
|
+
return { kind: 'prefix', queryKey, ...entryFields(opts) };
|
|
10
|
+
}
|
|
11
|
+
export function predicate(description, match, opts) {
|
|
12
|
+
return { kind: 'predicate', description, match, ...entryFields(opts) };
|
|
13
|
+
}
|
|
14
|
+
export function serializeInvalidation(inv) {
|
|
15
|
+
const action = inv.action && inv.action !== 'refresh' ? `${inv.action}:` : '';
|
|
16
|
+
if (inv.kind === 'predicate')
|
|
17
|
+
return `${action}predicate:${inv.description}`;
|
|
18
|
+
return `${action}${inv.kind}:${JSON.stringify(inv.queryKey)}`;
|
|
19
|
+
}
|
|
20
|
+
function invalidationFilters(inv) {
|
|
21
|
+
if (inv.kind === 'exact')
|
|
22
|
+
return { queryKey: inv.queryKey, exact: true };
|
|
23
|
+
if (inv.kind === 'prefix')
|
|
24
|
+
return { queryKey: inv.queryKey, exact: false };
|
|
25
|
+
return { predicate: inv.match };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* TanStack's `Query.fetch` reuses an in-flight initial fetch instead of cancelling it, so without
|
|
29
|
+
* this cancel the invalidation would resolve with the value read before the mutation committed.
|
|
30
|
+
*/
|
|
31
|
+
function cancelPendingInitialReads(queryClient, filters) {
|
|
32
|
+
for (const query of queryClient.getQueryCache().findAll(filters)) {
|
|
33
|
+
if (query.state.fetchStatus !== 'fetching')
|
|
34
|
+
continue;
|
|
35
|
+
if (query.state.data !== undefined)
|
|
36
|
+
continue;
|
|
37
|
+
void queryClient.cancelQueries({ queryKey: query.queryKey, exact: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** A cancel or reset changes the state a predicate reads, so the second step must not re-apply the filter. */
|
|
41
|
+
function matchedNow(queryClient, filters) {
|
|
42
|
+
const matched = new Set(queryClient.getQueryCache().findAll(filters));
|
|
43
|
+
return { predicate: (query) => matched.has(query) };
|
|
44
|
+
}
|
|
45
|
+
export function applyInvalidations(queryClient, invalidations) {
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
const refreshes = [];
|
|
48
|
+
for (const inv of invalidations) {
|
|
49
|
+
const sig = serializeInvalidation(inv);
|
|
50
|
+
if (seen.has(sig))
|
|
51
|
+
continue;
|
|
52
|
+
seen.add(sig);
|
|
53
|
+
const filters = invalidationFilters(inv);
|
|
54
|
+
const action = inv.action ?? 'refresh';
|
|
55
|
+
if (action === 'remove') {
|
|
56
|
+
const matched = matchedNow(queryClient, filters);
|
|
57
|
+
// Removing a query abandons its in-flight read silently, leaving a watching screen on fetching; cancelling first ends it visibly.
|
|
58
|
+
void queryClient.cancelQueries(matched);
|
|
59
|
+
queryClient.removeQueries(matched);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (action === 'reset') {
|
|
63
|
+
refreshes.push(queryClient.resetQueries(matchedNow(queryClient, filters)));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const refetchType = inv.refetchType ?? 'active';
|
|
67
|
+
if (refetchType !== 'none') {
|
|
68
|
+
cancelPendingInitialReads(queryClient, filters);
|
|
69
|
+
}
|
|
70
|
+
refreshes.push(queryClient.invalidateQueries({ ...filters, refetchType }));
|
|
71
|
+
}
|
|
72
|
+
return Promise.all(refreshes).then(() => undefined);
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=cache-invalidation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-invalidation.js","sourceRoot":"","sources":["../../../src/react/domain-events/cache-invalidation.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Query, QueryClient, QueryFilters, QueryKey } from '@tanstack/react-query';\n\nexport type RefetchType = 'active' | 'all' | 'none';\n\nexport type CacheAction = 'refresh' | 'remove' | 'reset';\n\nexport type CacheInvalidation =\n | { kind: 'exact'; queryKey: QueryKey; refetchType?: RefetchType; action?: CacheAction }\n | { kind: 'prefix'; queryKey: QueryKey; refetchType?: RefetchType; action?: CacheAction }\n | {\n kind: 'predicate';\n description: string;\n match: (query: Query) => boolean;\n refetchType?: RefetchType;\n action?: CacheAction;\n };\n\ntype EntryOptions =\n { refetchType?: RefetchType; action?: 'refresh' } | { action: 'remove' | 'reset' };\n\nconst entryFields = (opts?: EntryOptions) => ({\n refetchType: opts && 'refetchType' in opts ? opts.refetchType : undefined,\n action: opts?.action,\n});\n\nexport function exact(queryKey: QueryKey, opts?: EntryOptions): CacheInvalidation {\n return { kind: 'exact', queryKey, ...entryFields(opts) };\n}\n\nexport function prefix(queryKey: QueryKey, opts?: EntryOptions): CacheInvalidation {\n return { kind: 'prefix', queryKey, ...entryFields(opts) };\n}\n\nexport function predicate(\n description: string,\n match: (query: Query) => boolean,\n opts?: EntryOptions,\n): CacheInvalidation {\n return { kind: 'predicate', description, match, ...entryFields(opts) };\n}\n\nexport function serializeInvalidation(inv: CacheInvalidation): string {\n const action = inv.action && inv.action !== 'refresh' ? `${inv.action}:` : '';\n if (inv.kind === 'predicate') return `${action}predicate:${inv.description}`;\n return `${action}${inv.kind}:${JSON.stringify(inv.queryKey)}`;\n}\n\nfunction invalidationFilters(inv: CacheInvalidation): QueryFilters {\n if (inv.kind === 'exact') return { queryKey: inv.queryKey, exact: true };\n if (inv.kind === 'prefix') return { queryKey: inv.queryKey, exact: false };\n return { predicate: inv.match };\n}\n\n/**\n * TanStack's `Query.fetch` reuses an in-flight initial fetch instead of cancelling it, so without\n * this cancel the invalidation would resolve with the value read before the mutation committed.\n */\nfunction cancelPendingInitialReads(queryClient: QueryClient, filters: QueryFilters): void {\n for (const query of queryClient.getQueryCache().findAll(filters)) {\n if (query.state.fetchStatus !== 'fetching') continue;\n if (query.state.data !== undefined) continue;\n void queryClient.cancelQueries({ queryKey: query.queryKey, exact: true });\n }\n}\n\n/** A cancel or reset changes the state a predicate reads, so the second step must not re-apply the filter. */\nfunction matchedNow(queryClient: QueryClient, filters: QueryFilters): QueryFilters {\n const matched = new Set(queryClient.getQueryCache().findAll(filters));\n return { predicate: (query) => matched.has(query) };\n}\n\nexport function applyInvalidations(\n queryClient: QueryClient,\n invalidations: ReadonlyArray<CacheInvalidation>,\n): Promise<void> {\n const seen = new Set<string>();\n const refreshes: Promise<void>[] = [];\n for (const inv of invalidations) {\n const sig = serializeInvalidation(inv);\n if (seen.has(sig)) continue;\n seen.add(sig);\n\n const filters = invalidationFilters(inv);\n const action: CacheAction = inv.action ?? 'refresh';\n\n if (action === 'remove') {\n const matched = matchedNow(queryClient, filters);\n // Removing a query abandons its in-flight read silently, leaving a watching screen on fetching; cancelling first ends it visibly.\n void queryClient.cancelQueries(matched);\n queryClient.removeQueries(matched);\n continue;\n }\n if (action === 'reset') {\n refreshes.push(queryClient.resetQueries(matchedNow(queryClient, filters)));\n continue;\n }\n\n const refetchType: RefetchType = inv.refetchType ?? 'active';\n if (refetchType !== 'none') {\n cancelPendingInitialReads(queryClient, filters);\n }\n\n refreshes.push(queryClient.invalidateQueries({ ...filters, refetchType }));\n }\n return Promise.all(refreshes).then(() => undefined);\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { QueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { exact, prefix, predicate, type CacheInvalidation } from './cache-invalidation.js';
|
|
3
|
+
export type DomainEventInvalidationRegistry<TEvent extends {
|
|
4
|
+
type: string;
|
|
5
|
+
ids: unknown;
|
|
6
|
+
}> = {
|
|
7
|
+
[K in TEvent['type']]: (ids: Extract<TEvent, {
|
|
8
|
+
type: K;
|
|
9
|
+
}>['ids']) => ReadonlyArray<CacheInvalidation>;
|
|
10
|
+
};
|
|
11
|
+
export interface DomainEventInvalidator<TEvent extends {
|
|
12
|
+
type: string;
|
|
13
|
+
ids: unknown;
|
|
14
|
+
}> {
|
|
15
|
+
notify(queryClient: QueryClient, event: TEvent): Promise<void>;
|
|
16
|
+
notifyAll(queryClient: QueryClient, events: ReadonlyArray<TEvent>): Promise<void>;
|
|
17
|
+
readonly helpers: {
|
|
18
|
+
exact: typeof exact;
|
|
19
|
+
prefix: typeof prefix;
|
|
20
|
+
predicate: typeof predicate;
|
|
21
|
+
};
|
|
22
|
+
readonly registry: DomainEventInvalidationRegistry<TEvent>;
|
|
23
|
+
}
|
|
24
|
+
export declare function createDomainEventInvalidator<TEvent extends {
|
|
25
|
+
type: string;
|
|
26
|
+
ids: unknown;
|
|
27
|
+
}>(registry: DomainEventInvalidationRegistry<TEvent>): DomainEventInvalidator<TEvent>;
|
|
28
|
+
//# sourceMappingURL=create-domain-event-invalidator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-domain-event-invalidator.d.ts","sourceRoot":"","sources":["../../../src/react/domain-events/create-domain-event-invalidator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAEL,KAAK,EACL,MAAM,EACN,SAAS,EACT,KAAK,iBAAiB,EACvB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,MAAM,+BAA+B,CAAC,MAAM,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,OAAO,CAAA;CAAE,IAAI;KAC1F,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,CACrB,GAAG,EAAE,OAAO,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,CAAC,CAAA;KAAE,CAAC,CAAC,KAAK,CAAC,KACrC,aAAa,CAAC,iBAAiB,CAAC;CACtC,CAAC;AAEF,MAAM,WAAW,sBAAsB,CAAC,MAAM,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,OAAO,CAAA;CAAE;IACnF,MAAM,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,SAAS,CAAC,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClF,QAAQ,CAAC,OAAO,EAAE;QAChB,KAAK,EAAE,OAAO,KAAK,CAAC;QACpB,MAAM,EAAE,OAAO,MAAM,CAAC;QACtB,SAAS,EAAE,OAAO,SAAS,CAAC;KAC7B,CAAC;IACF,QAAQ,CAAC,QAAQ,EAAE,+BAA+B,CAAC,MAAM,CAAC,CAAC;CAC5D;AAED,wBAAgB,4BAA4B,CAAC,MAAM,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,OAAO,CAAA;CAAE,EACxF,QAAQ,EAAE,+BAA+B,CAAC,MAAM,CAAC,GAChD,sBAAsB,CAAC,MAAM,CAAC,CA4BhC"}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import { applyInvalidations, exact, prefix, predicate, } from
|
|
2
|
-
/**
|
|
3
|
-
* Build a domain-event → cache-invalidation notifier from a typed registry.
|
|
4
|
-
* The registry's keys are statically tied to `TEvent["type"]`, so missing or
|
|
5
|
-
* extra entries are compile errors.
|
|
6
|
-
*/
|
|
1
|
+
import { applyInvalidations, exact, prefix, predicate, } from './cache-invalidation.js';
|
|
7
2
|
export function createDomainEventInvalidator(registry) {
|
|
8
3
|
function lookup(type) {
|
|
9
4
|
return registry[type];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-domain-event-invalidator.js","sourceRoot":"","sources":["../../../src/react/domain-events/create-domain-event-invalidator.ts"],"names":[],"mappings":"AACA,OAAO,EACL,kBAAkB,EAClB,KAAK,EACL,MAAM,EACN,SAAS,GAEV,MAAM,yBAAyB,CAAC;AAmBjC,MAAM,UAAU,4BAA4B,CAC1C,QAAiD;IAEjD,SAAS,MAAM,CAA+B,IAAW;QACvD,OAAO,QAAQ,CAAC,IAAI,CAEiB,CAAC;IACxC,CAAC;IAED,SAAS,MAAM,CAAC,WAAwB,EAAE,KAAa;QACrD,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,IAAsB,CAAC,CAAC;QAChD,MAAM,aAAa,GAAG,EAAE,CAAC,KAAK,CAAC,GAAuD,CAAC,CAAC;QACxF,OAAO,kBAAkB,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IACxD,CAAC;IAED,SAAS,SAAS,CAAC,WAAwB,EAAE,MAA6B;QACxE,MAAM,GAAG,GAAwB,EAAE,CAAC;QACpC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,IAAsB,CAAC,CAAC;YAChD,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAuD,CAAC,CAAC,CAAC;QACjF,CAAC;QACD,OAAO,kBAAkB,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;IAC9C,CAAC;IAED,OAAO;QACL,MAAM;QACN,SAAS;QACT,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE;QACrC,QAAQ;KACT,CAAC;AACJ,CAAC","sourcesContent":["import type { QueryClient } from '@tanstack/react-query';\nimport {\n applyInvalidations,\n exact,\n prefix,\n predicate,\n type CacheInvalidation,\n} from './cache-invalidation.js';\n\nexport type DomainEventInvalidationRegistry<TEvent extends { type: string; ids: unknown }> = {\n [K in TEvent['type']]: (\n ids: Extract<TEvent, { type: K }>['ids'],\n ) => ReadonlyArray<CacheInvalidation>;\n};\n\nexport interface DomainEventInvalidator<TEvent extends { type: string; ids: unknown }> {\n notify(queryClient: QueryClient, event: TEvent): Promise<void>;\n notifyAll(queryClient: QueryClient, events: ReadonlyArray<TEvent>): Promise<void>;\n readonly helpers: {\n exact: typeof exact;\n prefix: typeof prefix;\n predicate: typeof predicate;\n };\n readonly registry: DomainEventInvalidationRegistry<TEvent>;\n}\n\nexport function createDomainEventInvalidator<TEvent extends { type: string; ids: unknown }>(\n registry: DomainEventInvalidationRegistry<TEvent>,\n): DomainEventInvalidator<TEvent> {\n function lookup<TType extends TEvent['type']>(type: TType) {\n return registry[type] as (\n ids: Extract<TEvent, { type: TType }>['ids'],\n ) => ReadonlyArray<CacheInvalidation>;\n }\n\n function notify(queryClient: QueryClient, event: TEvent): Promise<void> {\n const fn = lookup(event.type as TEvent['type']);\n const invalidations = fn(event.ids as Extract<TEvent, { type: TEvent['type'] }>['ids']);\n return applyInvalidations(queryClient, invalidations);\n }\n\n function notifyAll(queryClient: QueryClient, events: ReadonlyArray<TEvent>): Promise<void> {\n const all: CacheInvalidation[] = [];\n for (const event of events) {\n const fn = lookup(event.type as TEvent['type']);\n all.push(...fn(event.ids as Extract<TEvent, { type: TEvent['type'] }>['ids']));\n }\n return applyInvalidations(queryClient, all);\n }\n\n return {\n notify,\n notifyAll,\n helpers: { exact, prefix, predicate },\n registry,\n };\n}\n"]}
|
|
@@ -1,39 +1,11 @@
|
|
|
1
1
|
import type { QueryClient, QueryKey } from '@tanstack/react-query';
|
|
2
|
-
/**
|
|
3
|
-
* Bounded re-read ladder for a document that resolved ABSENT.
|
|
4
|
-
*
|
|
5
|
-
* A one-shot lookup that lands `null` has no listener to tell it when the doc appears —
|
|
6
|
-
* the row would stay blank until something invalidates it. That is the real failure the
|
|
7
|
-
* realtime path was introduced to fix (a mirror doc that is missing or lagging at fetch
|
|
8
|
-
* time: just after a create-and-redirect, or in a second tab racing the server mirror).
|
|
9
|
-
* This scheduler restores the recovery without a listener: after an absent result it
|
|
10
|
-
* re-reads a few times on a widening ladder, then gives up.
|
|
11
|
-
*
|
|
12
|
-
* Delays (ms) between re-reads within one absence episode. After the last rung the episode
|
|
13
|
-
* is exhausted and never restarts on its own; only a later PRESENT→absent transition opens
|
|
14
|
-
* a new one.
|
|
15
|
-
*/
|
|
16
2
|
export declare const ABSENT_RETRY_DELAYS_MS: readonly [5000, 15000, 45000, 120000];
|
|
17
3
|
export interface TrackAbsentDocParams {
|
|
18
4
|
queryClient: QueryClient;
|
|
19
|
-
/** Exact key of the per-id lookup query. */
|
|
20
5
|
queryKey: QueryKey;
|
|
21
|
-
/**
|
|
22
|
-
* Ladder this registrant wants; an empty array means it wants no re-reads. Registrants
|
|
23
|
-
* on the same key combine by union — the longest ladder among them governs — so one
|
|
24
|
-
* consumer passing `[]` never disables polling another consumer asked for.
|
|
25
|
-
*/
|
|
26
6
|
delays?: readonly number[];
|
|
27
7
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Register interest in re-reading `queryKey` while it resolves absent. Returns an
|
|
30
|
-
* idempotent release function; releasing the last interested consumer pauses the ladder
|
|
31
|
-
* without spending the episode's remaining budget, and releasing the last registrant that
|
|
32
|
-
* wanted polling shrinks the effective ladder for the episode still in progress.
|
|
33
|
-
*/
|
|
34
8
|
export declare function trackAbsentDoc({ queryClient, queryKey, delays, }: TrackAbsentDocParams): () => void;
|
|
35
|
-
/** Test-only: number of absence episodes currently tracked for a QueryClient. */
|
|
36
9
|
export declare function __absenceEpisodeCount(queryClient: QueryClient): number;
|
|
37
|
-
/** Test-only: rungs already spent on a key's current episode, or `null` if there is none. */
|
|
38
10
|
export declare function __absenceAttempt(queryClient: QueryClient, queryKey: QueryKey): number | null;
|
|
39
11
|
//# sourceMappingURL=absence-scheduler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"absence-scheduler.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/absence-scheduler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEnE
|
|
1
|
+
{"version":3,"file":"absence-scheduler.d.ts","sourceRoot":"","sources":["../../../src/react/firestore/absence-scheduler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEnE,eAAO,MAAM,sBAAsB,uCAA4C,CAAC;AAwIhF,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AAED,wBAAgB,cAAc,CAAC,EAC7B,WAAW,EACX,QAAQ,EACR,MAA+B,GAChC,EAAE,oBAAoB,GAAG,MAAM,IAAI,CAiCnC;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,MAAM,CAEtE;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,GAAG,MAAM,GAAG,IAAI,CAE5F"}
|
|
@@ -1,31 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
/**
|
|
3
|
-
* Bounded re-read ladder for a document that resolved ABSENT.
|
|
4
|
-
*
|
|
5
|
-
* A one-shot lookup that lands `null` has no listener to tell it when the doc appears —
|
|
6
|
-
* the row would stay blank until something invalidates it. That is the real failure the
|
|
7
|
-
* realtime path was introduced to fix (a mirror doc that is missing or lagging at fetch
|
|
8
|
-
* time: just after a create-and-redirect, or in a second tab racing the server mirror).
|
|
9
|
-
* This scheduler restores the recovery without a listener: after an absent result it
|
|
10
|
-
* re-reads a few times on a widening ladder, then gives up.
|
|
11
|
-
*
|
|
12
|
-
* Delays (ms) between re-reads within one absence episode. After the last rung the episode
|
|
13
|
-
* is exhausted and never restarts on its own; only a later PRESENT→absent transition opens
|
|
14
|
-
* a new one.
|
|
15
|
-
*/
|
|
16
2
|
export const ABSENT_RETRY_DELAYS_MS = [5_000, 15_000, 45_000, 120_000];
|
|
17
3
|
const NO_DELAYS = [];
|
|
18
|
-
// Per-QueryClient, like the doc-subscription registry: a discarded client is GC'd with its
|
|
19
|
-
// bookkeeping, and each browser tab schedules independently.
|
|
20
4
|
const schedulers = new WeakMap();
|
|
21
|
-
/**
|
|
22
|
-
* Hash a key the way THIS client hashes it, so a consumer with a custom
|
|
23
|
-
* `queryKeyHashFn` still lines up with `QueryCache.get()`.
|
|
24
|
-
*/
|
|
5
|
+
/** Hashes with this client's `queryKeyHashFn` so a custom hash still lines up with `QueryCache.get()`. */
|
|
25
6
|
function hashOf(queryClient, queryKey) {
|
|
26
7
|
return queryClient.defaultQueryOptions({ queryKey }).queryHash;
|
|
27
8
|
}
|
|
28
|
-
/** Union of the registrants' ladders: the longest one wins; none means no polling. */
|
|
29
9
|
function effectiveDelays(tracked) {
|
|
30
10
|
if (!tracked)
|
|
31
11
|
return NO_DELAYS;
|
|
@@ -61,8 +41,6 @@ function schedule(queryClient, state, hash) {
|
|
|
61
41
|
if (!current || !isAbsentAndActive(queryClient, hash))
|
|
62
42
|
return;
|
|
63
43
|
void queryClient.refetchQueries({ queryKey: current.queryKey, exact: true, type: 'active' });
|
|
64
|
-
// Lay the next rung; the ladder stops the moment the doc appears, the budget runs
|
|
65
|
-
// out, or the last enabled observer goes away.
|
|
66
44
|
evaluate(queryClient, state, hash);
|
|
67
45
|
}, delays[episode.attempt]);
|
|
68
46
|
}
|
|
@@ -80,13 +58,10 @@ function evaluate(queryClient, state, hash) {
|
|
|
80
58
|
}
|
|
81
59
|
const { status, data } = query.state;
|
|
82
60
|
if (status === 'success' && data !== null) {
|
|
83
|
-
// The doc is present — the episode is over.
|
|
84
61
|
discardEpisode(state, hash);
|
|
85
62
|
return;
|
|
86
63
|
}
|
|
87
64
|
if (status !== 'success') {
|
|
88
|
-
// Pending or errored: never polled, but the budget survives so an error mid-episode
|
|
89
|
-
// cannot hand the id a fresh ladder when it returns to absent.
|
|
90
65
|
clearTimer(state.episodes.get(hash));
|
|
91
66
|
return;
|
|
92
67
|
}
|
|
@@ -97,7 +72,6 @@ function evaluate(queryClient, state, hash) {
|
|
|
97
72
|
}
|
|
98
73
|
let episode = state.episodes.get(hash);
|
|
99
74
|
if (!episode) {
|
|
100
|
-
// No registrant wants polling — do not open an episode that could never spend a rung.
|
|
101
75
|
if (effectiveDelays(tracked).length === 0)
|
|
102
76
|
return;
|
|
103
77
|
episode = { attempt: 0, timer: null };
|
|
@@ -116,8 +90,6 @@ function getState(queryClient) {
|
|
|
116
90
|
state = { episodes: new Map(), tracked: new Map() };
|
|
117
91
|
schedulers.set(queryClient, state);
|
|
118
92
|
const scheduler = state;
|
|
119
|
-
// Exactly ONE cache listener per QueryClient, created with this state and never detached:
|
|
120
|
-
// it is WeakMap-scoped, so it is collected together with the client it belongs to.
|
|
121
93
|
queryClient.getQueryCache().subscribe((event) => {
|
|
122
94
|
const hash = event.query.queryHash;
|
|
123
95
|
if (!scheduler.tracked.has(hash) && !scheduler.episodes.has(hash))
|
|
@@ -130,12 +102,6 @@ function getState(queryClient) {
|
|
|
130
102
|
});
|
|
131
103
|
return state;
|
|
132
104
|
}
|
|
133
|
-
/**
|
|
134
|
-
* Register interest in re-reading `queryKey` while it resolves absent. Returns an
|
|
135
|
-
* idempotent release function; releasing the last interested consumer pauses the ladder
|
|
136
|
-
* without spending the episode's remaining budget, and releasing the last registrant that
|
|
137
|
-
* wanted polling shrinks the effective ladder for the episode still in progress.
|
|
138
|
-
*/
|
|
139
105
|
export function trackAbsentDoc({ queryClient, queryKey, delays = ABSENT_RETRY_DELAYS_MS, }) {
|
|
140
106
|
const state = getState(queryClient);
|
|
141
107
|
const hash = hashOf(queryClient, queryKey);
|
|
@@ -164,18 +130,14 @@ export function trackAbsentDoc({ queryClient, queryKey, delays = ABSENT_RETRY_DE
|
|
|
164
130
|
return;
|
|
165
131
|
}
|
|
166
132
|
if (effectiveDelays(current).length !== before) {
|
|
167
|
-
// The effective ladder changed: re-derive under the new one rather than letting a
|
|
168
|
-
// timer from the departed registrant's ladder fire.
|
|
169
133
|
clearTimer(state.episodes.get(hash));
|
|
170
134
|
evaluate(queryClient, state, hash);
|
|
171
135
|
}
|
|
172
136
|
};
|
|
173
137
|
}
|
|
174
|
-
/** Test-only: number of absence episodes currently tracked for a QueryClient. */
|
|
175
138
|
export function __absenceEpisodeCount(queryClient) {
|
|
176
139
|
return schedulers.get(queryClient)?.episodes.size ?? 0;
|
|
177
140
|
}
|
|
178
|
-
/** Test-only: rungs already spent on a key's current episode, or `null` if there is none. */
|
|
179
141
|
export function __absenceAttempt(queryClient, queryKey) {
|
|
180
142
|
return schedulers.get(queryClient)?.episodes.get(hashOf(queryClient, queryKey))?.attempt ?? null;
|
|
181
143
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"absence-scheduler.js","sourceRoot":"","sources":["../../../src/react/firestore/absence-scheduler.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAIb;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAU,CAAC;AA+ChF,MAAM,SAAS,GAAsB,EAAE,CAAC;AAExC,2FAA2F;AAC3F,6DAA6D;AAC7D,MAAM,UAAU,GAAG,IAAI,OAAO,EAA+B,CAAC;AAE9D;;;GAGG;AACH,SAAS,MAAM,CAAC,WAAwB,EAAE,QAAkB;IAC1D,OAAO,WAAW,CAAC,mBAAmB,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,SAAS,CAAC;AACjE,CAAC;AAED,sFAAsF;AACtF,SAAS,eAAe,CAAC,OAA4B;IACnD,IAAI,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IAC/B,IAAI,OAAO,GAAsB,SAAS,CAAC;IAC3C,KAAK,MAAM,YAAY,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;QACjD,IAAI,YAAY,CAAC,MAAM,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM;YAAE,OAAO,GAAG,YAAY,CAAC,MAAM,CAAC;IACjF,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,UAAU,CAAC,OAA4B;IAC9C,IAAI,OAAO,EAAE,KAAK,IAAI,IAAI,EAAE,CAAC;QAC3B,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAC5B,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC;IACvB,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,KAAqB,EAAE,IAAY;IACzD,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;IACrC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED,SAAS,QAAQ,CAAC,WAAwB,EAAE,KAAqB,EAAE,IAAY;IAC7E,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzC,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,KAAK,KAAK,IAAI;QAAE,OAAO;IAE3D,MAAM,MAAM,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IACxC,IAAI,OAAO,CAAC,OAAO,IAAI,MAAM,CAAC,MAAM;QAAE,OAAO;IAE7C,OAAO,CAAC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;QAC9B,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC;QACrB,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC;QACrB,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,CAAC,OAAO,IAAI,CAAC,iBAAiB,CAAC,WAAW,EAAE,IAAI,CAAC;YAAE,OAAO;QAC9D,KAAK,WAAW,CAAC,cAAc,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC7F,kFAAkF;QAClF,+CAA+C;QAC/C,QAAQ,CAAC,WAAW,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IACrC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;AAC9B,CAAC;AAED,SAAS,iBAAiB,CAAC,WAAwB,EAAE,IAAY;IAC/D,MAAM,KAAK,GAAG,WAAW,CAAC,aAAa,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpD,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;AAC3F,CAAC;AAED,SAAS,QAAQ,CAAC,WAAwB,EAAE,KAAqB,EAAE,IAAY;IAC7E,MAAM,KAAK,GAAG,WAAW,CAAC,aAAa,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACpD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAC5B,OAAO;IACT,CAAC;IAED,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC;IAErC,IAAI,MAAM,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAC1C,4CAA4C;QAC5C,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAC5B,OAAO;IACT,CAAC;IAED,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,oFAAoF;QACpF,+DAA+D;QAC/D,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QACrC,OAAO;IACT,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QACrC,OAAO;IACT,CAAC;IAED,IAAI,OAAO,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACvC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,sFAAsF;QACtF,IAAI,eAAe,CAAC,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAClD,OAAO,GAAG,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QACtC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACpC,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,CAAC;QACtB,UAAU,CAAC,OAAO,CAAC,CAAC;QACpB,OAAO;IACT,CAAC;IAED,QAAQ,CAAC,WAAW,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,QAAQ,CAAC,WAAwB;IACxC,IAAI,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACxC,IAAI,KAAK;QAAE,OAAO,KAAK,CAAC;IAExB,KAAK,GAAG,EAAE,QAAQ,EAAE,IAAI,GAAG,EAAE,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC;IACpD,UAAU,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAEnC,MAAM,SAAS,GAAG,KAAK,CAAC;IACxB,0FAA0F;IAC1F,mFAAmF;IACnF,WAAW,CAAC,aAAa,EAAE,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;QAC9C,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC;QACnC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,OAAO;QAC1E,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC7B,cAAc,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAChC,OAAO;QACT,CAAC;QACD,QAAQ,CAAC,WAAW,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC;AACf,CAAC;AAcD;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,EAC7B,WAAW,EACX,QAAQ,EACR,MAAM,GAAG,sBAAsB,GACV;IACrB,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAC3C,MAAM,YAAY,GAAiB,EAAE,MAAM,EAAE,CAAC;IAE9C,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzC,IAAI,QAAQ;QAAE,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;;QACnD,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IAE1E,QAAQ,CAAC,WAAW,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IAEnC,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,OAAO,GAAG,EAAE;QACV,IAAI,QAAQ;YAAE,OAAO;QACrB,QAAQ,GAAG,IAAI,CAAC;QAChB,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,CAAC,OAAO;YAAE,OAAO;QAErB,MAAM,MAAM,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;QAC/C,MAAM,KAAK,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;QAC1D,IAAI,KAAK,IAAI,CAAC;YAAE,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAEvD,IAAI,OAAO,CAAC,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC3B,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;YACrC,OAAO;QACT,CAAC;QAED,IAAI,eAAe,CAAC,OAAO,CAAC,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC/C,kFAAkF;YAClF,oDAAoD;YACpD,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;YACrC,QAAQ,CAAC,WAAW,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QACrC,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,qBAAqB,CAAC,WAAwB;IAC5D,OAAO,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAC;AACzD,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,gBAAgB,CAAC,WAAwB,EAAE,QAAkB;IAC3E,OAAO,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,EAAE,OAAO,IAAI,IAAI,CAAC;AACnG,CAAC","sourcesContent":["'use client';\n\nimport type { QueryClient, QueryKey } from '@tanstack/react-query';\n\n/**\n * Bounded re-read ladder for a document that resolved ABSENT.\n *\n * A one-shot lookup that lands `null` has no listener to tell it when the doc appears —\n * the row would stay blank until something invalidates it. That is the real failure the\n * realtime path was introduced to fix (a mirror doc that is missing or lagging at fetch\n * time: just after a create-and-redirect, or in a second tab racing the server mirror).\n * This scheduler restores the recovery without a listener: after an absent result it\n * re-reads a few times on a widening ladder, then gives up.\n *\n * Delays (ms) between re-reads within one absence episode. After the last rung the episode\n * is exhausted and never restarts on its own; only a later PRESENT→absent transition opens\n * a new one.\n */\nexport const ABSENT_RETRY_DELAYS_MS = [5_000, 15_000, 45_000, 120_000] as const;\n\n/**\n * The rules the scheduler holds to:\n *\n * - It polls ONLY a query in `status: 'success'` whose data is `null`. An error state is\n * never polled (a denial is not an absence), and an error mid-episode pauses the ladder\n * with its remaining budget intact.\n * - One timer per query key and one budget per absence episode: the first successful\n * `undefined → null` opens the episode, and neither a repeated `null` nor an additional\n * consumer resets it. A `present → null` transition opens a NEW episode.\n * - Several consumers may register the same key with DIFFERENT ladders. The effective\n * ladder is the union: poll if any registrant wants polling, using the LONGEST ladder\n * among them, recomputed on every register and release. Mount order never decides.\n * - Eligibility is TanStack's `query.isActive()` — at least one observer that is not\n * disabled. Subscribe-mode and disabled observers therefore never keep an episode\n * polling, while disabling ONE consumer cannot stop polling another still needs.\n * - Losing the last enabled observer PAUSES the ladder and retains the episode's remaining\n * rungs; a later enabled observer resumes from there. A new observer on an EXHAUSTED\n * episode does not restart it.\n * - Cache removal (gc) discards the episode outright, so a later re-fetch that lands absent\n * starts fresh.\n *\n * Because the timer is per key rather than per observer, N consumers of one absent id cost\n * one re-read per rung, not N.\n */\n\ntype Episode = {\n /** Rungs consumed so far; `>= effectiveDelays().length` means exhausted. */\n attempt: number;\n timer: ReturnType<typeof setTimeout> | null;\n};\n\ntype Registration = {\n delays: readonly number[];\n};\n\ntype Tracked = {\n queryKey: QueryKey;\n registrations: Registration[];\n};\n\ntype SchedulerState = {\n episodes: Map<string, Episode>;\n tracked: Map<string, Tracked>;\n};\n\nconst NO_DELAYS: readonly number[] = [];\n\n// Per-QueryClient, like the doc-subscription registry: a discarded client is GC'd with its\n// bookkeeping, and each browser tab schedules independently.\nconst schedulers = new WeakMap<QueryClient, SchedulerState>();\n\n/**\n * Hash a key the way THIS client hashes it, so a consumer with a custom\n * `queryKeyHashFn` still lines up with `QueryCache.get()`.\n */\nfunction hashOf(queryClient: QueryClient, queryKey: QueryKey): string {\n return queryClient.defaultQueryOptions({ queryKey }).queryHash;\n}\n\n/** Union of the registrants' ladders: the longest one wins; none means no polling. */\nfunction effectiveDelays(tracked: Tracked | undefined): readonly number[] {\n if (!tracked) return NO_DELAYS;\n let longest: readonly number[] = NO_DELAYS;\n for (const registration of tracked.registrations) {\n if (registration.delays.length > longest.length) longest = registration.delays;\n }\n return longest;\n}\n\nfunction clearTimer(episode: Episode | undefined): void {\n if (episode?.timer != null) {\n clearTimeout(episode.timer);\n episode.timer = null;\n }\n}\n\nfunction discardEpisode(state: SchedulerState, hash: string): void {\n clearTimer(state.episodes.get(hash));\n state.episodes.delete(hash);\n}\n\nfunction schedule(queryClient: QueryClient, state: SchedulerState, hash: string): void {\n const tracked = state.tracked.get(hash);\n const episode = state.episodes.get(hash);\n if (!tracked || !episode || episode.timer !== null) return;\n\n const delays = effectiveDelays(tracked);\n if (episode.attempt >= delays.length) return;\n\n episode.timer = setTimeout(() => {\n episode.timer = null;\n episode.attempt += 1;\n const current = state.tracked.get(hash);\n if (!current || !isAbsentAndActive(queryClient, hash)) return;\n void queryClient.refetchQueries({ queryKey: current.queryKey, exact: true, type: 'active' });\n // Lay the next rung; the ladder stops the moment the doc appears, the budget runs\n // out, or the last enabled observer goes away.\n evaluate(queryClient, state, hash);\n }, delays[episode.attempt]);\n}\n\nfunction isAbsentAndActive(queryClient: QueryClient, hash: string): boolean {\n const query = queryClient.getQueryCache().get(hash);\n if (!query) return false;\n return query.state.status === 'success' && query.state.data === null && query.isActive();\n}\n\nfunction evaluate(queryClient: QueryClient, state: SchedulerState, hash: string): void {\n const query = queryClient.getQueryCache().get(hash);\n if (!query) {\n discardEpisode(state, hash);\n return;\n }\n\n const { status, data } = query.state;\n\n if (status === 'success' && data !== null) {\n // The doc is present — the episode is over.\n discardEpisode(state, hash);\n return;\n }\n\n if (status !== 'success') {\n // Pending or errored: never polled, but the budget survives so an error mid-episode\n // cannot hand the id a fresh ladder when it returns to absent.\n clearTimer(state.episodes.get(hash));\n return;\n }\n\n const tracked = state.tracked.get(hash);\n if (!tracked) {\n clearTimer(state.episodes.get(hash));\n return;\n }\n\n let episode = state.episodes.get(hash);\n if (!episode) {\n // No registrant wants polling — do not open an episode that could never spend a rung.\n if (effectiveDelays(tracked).length === 0) return;\n episode = { attempt: 0, timer: null };\n state.episodes.set(hash, episode);\n }\n\n if (!query.isActive()) {\n clearTimer(episode);\n return;\n }\n\n schedule(queryClient, state, hash);\n}\n\nfunction getState(queryClient: QueryClient): SchedulerState {\n let state = schedulers.get(queryClient);\n if (state) return state;\n\n state = { episodes: new Map(), tracked: new Map() };\n schedulers.set(queryClient, state);\n\n const scheduler = state;\n // Exactly ONE cache listener per QueryClient, created with this state and never detached:\n // it is WeakMap-scoped, so it is collected together with the client it belongs to.\n queryClient.getQueryCache().subscribe((event) => {\n const hash = event.query.queryHash;\n if (!scheduler.tracked.has(hash) && !scheduler.episodes.has(hash)) return;\n if (event.type === 'removed') {\n discardEpisode(scheduler, hash);\n return;\n }\n evaluate(queryClient, scheduler, hash);\n });\n\n return state;\n}\n\nexport interface TrackAbsentDocParams {\n queryClient: QueryClient;\n /** Exact key of the per-id lookup query. */\n queryKey: QueryKey;\n /**\n * Ladder this registrant wants; an empty array means it wants no re-reads. Registrants\n * on the same key combine by union — the longest ladder among them governs — so one\n * consumer passing `[]` never disables polling another consumer asked for.\n */\n delays?: readonly number[];\n}\n\n/**\n * Register interest in re-reading `queryKey` while it resolves absent. Returns an\n * idempotent release function; releasing the last interested consumer pauses the ladder\n * without spending the episode's remaining budget, and releasing the last registrant that\n * wanted polling shrinks the effective ladder for the episode still in progress.\n */\nexport function trackAbsentDoc({\n queryClient,\n queryKey,\n delays = ABSENT_RETRY_DELAYS_MS,\n}: TrackAbsentDocParams): () => void {\n const state = getState(queryClient);\n const hash = hashOf(queryClient, queryKey);\n const registration: Registration = { delays };\n\n const existing = state.tracked.get(hash);\n if (existing) existing.registrations.push(registration);\n else state.tracked.set(hash, { queryKey, registrations: [registration] });\n\n evaluate(queryClient, state, hash);\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const current = state.tracked.get(hash);\n if (!current) return;\n\n const before = effectiveDelays(current).length;\n const index = current.registrations.indexOf(registration);\n if (index >= 0) current.registrations.splice(index, 1);\n\n if (current.registrations.length === 0) {\n state.tracked.delete(hash);\n clearTimer(state.episodes.get(hash));\n return;\n }\n\n if (effectiveDelays(current).length !== before) {\n // The effective ladder changed: re-derive under the new one rather than letting a\n // timer from the departed registrant's ladder fire.\n clearTimer(state.episodes.get(hash));\n evaluate(queryClient, state, hash);\n }\n };\n}\n\n/** Test-only: number of absence episodes currently tracked for a QueryClient. */\nexport function __absenceEpisodeCount(queryClient: QueryClient): number {\n return schedulers.get(queryClient)?.episodes.size ?? 0;\n}\n\n/** Test-only: rungs already spent on a key's current episode, or `null` if there is none. */\nexport function __absenceAttempt(queryClient: QueryClient, queryKey: QueryKey): number | null {\n return schedulers.get(queryClient)?.episodes.get(hashOf(queryClient, queryKey))?.attempt ?? null;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"absence-scheduler.js","sourceRoot":"","sources":["../../../src/react/firestore/absence-scheduler.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport type { QueryClient, QueryKey } from '@tanstack/react-query';\n\nexport const ABSENT_RETRY_DELAYS_MS = [5_000, 15_000, 45_000, 120_000] as const;\n\ntype Episode = {\n attempt: number;\n timer: ReturnType<typeof setTimeout> | null;\n};\n\ntype Registration = {\n delays: readonly number[];\n};\n\ntype Tracked = {\n queryKey: QueryKey;\n registrations: Registration[];\n};\n\ntype SchedulerState = {\n episodes: Map<string, Episode>;\n tracked: Map<string, Tracked>;\n};\n\nconst NO_DELAYS: readonly number[] = [];\n\nconst schedulers = new WeakMap<QueryClient, SchedulerState>();\n\n/** Hashes with this client's `queryKeyHashFn` so a custom hash still lines up with `QueryCache.get()`. */\nfunction hashOf(queryClient: QueryClient, queryKey: QueryKey): string {\n return queryClient.defaultQueryOptions({ queryKey }).queryHash;\n}\n\nfunction effectiveDelays(tracked: Tracked | undefined): readonly number[] {\n if (!tracked) return NO_DELAYS;\n let longest: readonly number[] = NO_DELAYS;\n for (const registration of tracked.registrations) {\n if (registration.delays.length > longest.length) longest = registration.delays;\n }\n return longest;\n}\n\nfunction clearTimer(episode: Episode | undefined): void {\n if (episode?.timer != null) {\n clearTimeout(episode.timer);\n episode.timer = null;\n }\n}\n\nfunction discardEpisode(state: SchedulerState, hash: string): void {\n clearTimer(state.episodes.get(hash));\n state.episodes.delete(hash);\n}\n\nfunction schedule(queryClient: QueryClient, state: SchedulerState, hash: string): void {\n const tracked = state.tracked.get(hash);\n const episode = state.episodes.get(hash);\n if (!tracked || !episode || episode.timer !== null) return;\n\n const delays = effectiveDelays(tracked);\n if (episode.attempt >= delays.length) return;\n\n episode.timer = setTimeout(() => {\n episode.timer = null;\n episode.attempt += 1;\n const current = state.tracked.get(hash);\n if (!current || !isAbsentAndActive(queryClient, hash)) return;\n void queryClient.refetchQueries({ queryKey: current.queryKey, exact: true, type: 'active' });\n evaluate(queryClient, state, hash);\n }, delays[episode.attempt]);\n}\n\nfunction isAbsentAndActive(queryClient: QueryClient, hash: string): boolean {\n const query = queryClient.getQueryCache().get(hash);\n if (!query) return false;\n return query.state.status === 'success' && query.state.data === null && query.isActive();\n}\n\nfunction evaluate(queryClient: QueryClient, state: SchedulerState, hash: string): void {\n const query = queryClient.getQueryCache().get(hash);\n if (!query) {\n discardEpisode(state, hash);\n return;\n }\n\n const { status, data } = query.state;\n\n if (status === 'success' && data !== null) {\n discardEpisode(state, hash);\n return;\n }\n\n if (status !== 'success') {\n clearTimer(state.episodes.get(hash));\n return;\n }\n\n const tracked = state.tracked.get(hash);\n if (!tracked) {\n clearTimer(state.episodes.get(hash));\n return;\n }\n\n let episode = state.episodes.get(hash);\n if (!episode) {\n if (effectiveDelays(tracked).length === 0) return;\n episode = { attempt: 0, timer: null };\n state.episodes.set(hash, episode);\n }\n\n if (!query.isActive()) {\n clearTimer(episode);\n return;\n }\n\n schedule(queryClient, state, hash);\n}\n\nfunction getState(queryClient: QueryClient): SchedulerState {\n let state = schedulers.get(queryClient);\n if (state) return state;\n\n state = { episodes: new Map(), tracked: new Map() };\n schedulers.set(queryClient, state);\n\n const scheduler = state;\n queryClient.getQueryCache().subscribe((event) => {\n const hash = event.query.queryHash;\n if (!scheduler.tracked.has(hash) && !scheduler.episodes.has(hash)) return;\n if (event.type === 'removed') {\n discardEpisode(scheduler, hash);\n return;\n }\n evaluate(queryClient, scheduler, hash);\n });\n\n return state;\n}\n\nexport interface TrackAbsentDocParams {\n queryClient: QueryClient;\n queryKey: QueryKey;\n delays?: readonly number[];\n}\n\nexport function trackAbsentDoc({\n queryClient,\n queryKey,\n delays = ABSENT_RETRY_DELAYS_MS,\n}: TrackAbsentDocParams): () => void {\n const state = getState(queryClient);\n const hash = hashOf(queryClient, queryKey);\n const registration: Registration = { delays };\n\n const existing = state.tracked.get(hash);\n if (existing) existing.registrations.push(registration);\n else state.tracked.set(hash, { queryKey, registrations: [registration] });\n\n evaluate(queryClient, state, hash);\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const current = state.tracked.get(hash);\n if (!current) return;\n\n const before = effectiveDelays(current).length;\n const index = current.registrations.indexOf(registration);\n if (index >= 0) current.registrations.splice(index, 1);\n\n if (current.registrations.length === 0) {\n state.tracked.delete(hash);\n clearTimer(state.episodes.get(hash));\n return;\n }\n\n if (effectiveDelays(current).length !== before) {\n clearTimer(state.episodes.get(hash));\n evaluate(queryClient, state, hash);\n }\n };\n}\n\nexport function __absenceEpisodeCount(queryClient: QueryClient): number {\n return schedulers.get(queryClient)?.episodes.size ?? 0;\n}\n\nexport function __absenceAttempt(queryClient: QueryClient, queryKey: QueryKey): number | null {\n return schedulers.get(queryClient)?.episodes.get(hashOf(queryClient, queryKey))?.attempt ?? null;\n}\n"]}
|
|
@@ -1,42 +1,12 @@
|
|
|
1
1
|
import { type Firestore } from 'firebase/firestore';
|
|
2
2
|
import type { QueryClient } from '@tanstack/react-query';
|
|
3
|
-
/**
|
|
4
|
-
* Coalescing document loader behind {@link useBatchFirestoreDocs}'s one-shot mode.
|
|
5
|
-
*
|
|
6
|
-
* Every id is its own observed React Query query, so without coalescing N ids would issue
|
|
7
|
-
* N Firestore round-trips. The loader collects the ids enqueued in a single microtask and
|
|
8
|
-
* dispatches them as `where(documentId(), 'in', chunk)` queries of at most `FIRESTORE_IN_FILTER_LIMIT` ids each
|
|
9
|
-
* (`'batch'` transport), or as concurrent per-document gets (`'get'` transport — required
|
|
10
|
-
* for collections whose rules gate reads on `resource.data`, where an unconstrained list
|
|
11
|
-
* query is denied wholesale but per-document gets are evaluated per doc).
|
|
12
|
-
*
|
|
13
|
-
* Two properties are load-bearing:
|
|
14
|
-
*
|
|
15
|
-
* - **The loader resolves promises only — it NEVER writes the query cache.** TanStack's
|
|
16
|
-
* per-query fetch identity is therefore the sole guard against a late batch response
|
|
17
|
-
* overwriting a newer per-id result.
|
|
18
|
-
* - **A dispatched batch is closed.** A later enqueue of the same id (a refetch after a
|
|
19
|
-
* cancellation, say) always joins a NEW batch, never the in-flight one — so a cancelled
|
|
20
|
-
* read can never be served by the request it was cancelled out of.
|
|
21
|
-
*
|
|
22
|
-
* Cancelling one per-id query detaches only that waiter; the shared request still completes
|
|
23
|
-
* for its batch-mates.
|
|
24
|
-
*
|
|
25
|
-
* Loaders are scoped per `(QueryClient, Firestore instance, collectionPath)`. The Firestore
|
|
26
|
-
* instance is part of the scope on purpose: two `db` instances sharing a collection path
|
|
27
|
-
* must never share a request.
|
|
28
|
-
*/
|
|
29
|
-
/** Maximum `in`-query requests in flight at once, per loader scope. */
|
|
30
3
|
export declare const MAX_CONCURRENT_BATCH_REQUESTS = 4;
|
|
31
|
-
/** Maximum per-document gets in flight at once, per loader scope. */
|
|
32
4
|
export declare const MAX_CONCURRENT_DOC_GETS = 12;
|
|
33
5
|
export type LoadedDoc = Record<string, unknown> & {
|
|
34
6
|
id: string;
|
|
35
7
|
};
|
|
36
8
|
export type DocLoader = {
|
|
37
|
-
/** Resolve one id through the coalescing `in`-query batcher. */
|
|
38
9
|
loadBatched(id: string, signal?: AbortSignal): Promise<LoadedDoc | null>;
|
|
39
|
-
/** Resolve one id through its own `getDoc`, bounded by the get concurrency limit. */
|
|
40
10
|
loadOne(id: string, signal?: AbortSignal): Promise<LoadedDoc | null>;
|
|
41
11
|
};
|
|
42
12
|
export declare function getDocLoader(queryClient: QueryClient, db: Firestore, collectionPath: string): DocLoader;
|
|
@@ -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
|
|
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"]}
|