@ttt-productions/query-core 0.14.18 → 0.14.20

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.
@@ -1,28 +1,40 @@
1
1
  import type { Query, QueryClient, QueryKey } from "@tanstack/react-query";
2
2
  export type RefetchType = "active" | "all" | "none";
3
+ /**
4
+ * What an entry does to the queries it matches:
5
+ * - `"refresh"` (the default) marks them stale and refetches per `refetchType`;
6
+ * - `"remove"` cancels them and drops them from the cache — for data that no longer exists, or a
7
+ * held answer that must not be served again;
8
+ * - `"reset"` returns them to their initial state and refetches the active ones from scratch.
9
+ */
10
+ export type CacheAction = "refresh" | "remove" | "reset";
3
11
  export type CacheInvalidation = {
4
12
  kind: "exact";
5
13
  queryKey: QueryKey;
6
14
  refetchType?: RefetchType;
15
+ action?: CacheAction;
7
16
  } | {
8
17
  kind: "prefix";
9
18
  queryKey: QueryKey;
10
19
  refetchType?: RefetchType;
20
+ action?: CacheAction;
11
21
  } | {
12
22
  kind: "predicate";
13
23
  description: string;
14
24
  match: (query: Query) => boolean;
15
25
  refetchType?: RefetchType;
26
+ action?: CacheAction;
16
27
  };
17
- export declare function exact(queryKey: QueryKey, opts?: {
18
- refetchType?: RefetchType;
19
- }): CacheInvalidation;
20
- export declare function prefix(queryKey: QueryKey, opts?: {
28
+ /** `refetchType` belongs to a refresh; a remove or reset takes none. */
29
+ type EntryOptions = {
21
30
  refetchType?: RefetchType;
22
- }): CacheInvalidation;
23
- export declare function predicate(description: string, match: (query: Query) => boolean, opts?: {
24
- refetchType?: RefetchType;
25
- }): CacheInvalidation;
31
+ action?: "refresh";
32
+ } | {
33
+ action: "remove" | "reset";
34
+ };
35
+ export declare function exact(queryKey: QueryKey, opts?: EntryOptions): CacheInvalidation;
36
+ export declare function prefix(queryKey: QueryKey, opts?: EntryOptions): CacheInvalidation;
37
+ export declare function predicate(description: string, match: (query: Query) => boolean, opts?: EntryOptions): CacheInvalidation;
26
38
  export declare function serializeInvalidation(inv: CacheInvalidation): string;
27
39
  /**
28
40
  * Dispatch a deduplicated list of cache invalidations.
@@ -31,7 +43,7 @@ export declare function serializeInvalidation(inv: CacheInvalidation): string;
31
43
  * has settled — i.e. once the refetches those invalidations triggered have
32
44
  * landed. The dispatch itself stays synchronous (every cancellation and
33
45
  * invalidation is issued before this function returns), so a fire-and-forget
34
- * caller keeps the prior behavior exactly, while a mutation `onSuccess` that
46
+ * caller can ignore the promise, while a mutation `onSuccess` that
35
47
  * RETURNS or awaits the promise stays pending until the refreshed data is in
36
48
  * the cache. The promise never rejects: `invalidateQueries` refetches without
37
49
  * `throwOnError`, so a failed refetch surfaces on its own query, never as a
@@ -54,4 +66,5 @@ export declare function serializeInvalidation(inv: CacheInvalidation): string;
54
66
  * consumer has and leave it with no data and no pending request.
55
67
  */
56
68
  export declare function applyInvalidations(queryClient: QueryClient, invalidations: ReadonlyArray<CacheInvalidation>): Promise<void>;
69
+ export {};
57
70
  //# sourceMappingURL=cache-invalidation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cache-invalidation.d.ts","sourceRoot":"","sources":["../../src/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,iBAAiB,GACzB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,WAAW,CAAC,EAAE,WAAW,CAAA;CAAE,GAChE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,WAAW,CAAC,EAAE,WAAW,CAAA;CAAE,GACjE;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;CAC3B,CAAC;AAEN,wBAAgB,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,WAAW,CAAA;CAAE,GAAG,iBAAiB,CAEjG;AAED,wBAAgB,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,WAAW,CAAA;CAAE,GAAG,iBAAiB,CAElG;AAED,wBAAgB,SAAS,CACvB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,EAChC,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,WAAW,CAAA;CAAE,GACnC,iBAAiB,CAEnB;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAGpE;AAiCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE,aAAa,CAAC,iBAAiB,CAAC,GAC9C,OAAO,CAAC,IAAI,CAAC,CAkBf"}
1
+ {"version":3,"file":"cache-invalidation.d.ts","sourceRoot":"","sources":["../../src/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;;;;;;GAMG;AACH,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,wEAAwE;AACxE,KAAK,YAAY,GAAG;IAAE,WAAW,CAAC,EAAE,WAAW,CAAC;IAAC,MAAM,CAAC,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAA;CAAE,CAAC;AAOvG,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;AAiCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE,aAAa,CAAC,iBAAiB,CAAC,GAC9C,OAAO,CAAC,IAAI,CAAC,CAgCf"}
@@ -1,16 +1,21 @@
1
+ const entryFields = (opts) => ({
2
+ refetchType: opts && "refetchType" in opts ? opts.refetchType : undefined,
3
+ action: opts?.action,
4
+ });
1
5
  export function exact(queryKey, opts) {
2
- return { kind: "exact", queryKey, refetchType: opts?.refetchType };
6
+ return { kind: "exact", queryKey, ...entryFields(opts) };
3
7
  }
4
8
  export function prefix(queryKey, opts) {
5
- return { kind: "prefix", queryKey, refetchType: opts?.refetchType };
9
+ return { kind: "prefix", queryKey, ...entryFields(opts) };
6
10
  }
7
11
  export function predicate(description, match, opts) {
8
- return { kind: "predicate", description, match, refetchType: opts?.refetchType };
12
+ return { kind: "predicate", description, match, ...entryFields(opts) };
9
13
  }
10
14
  export function serializeInvalidation(inv) {
15
+ const action = inv.action && inv.action !== "refresh" ? `${inv.action}:` : "";
11
16
  if (inv.kind === "predicate")
12
- return `predicate:${inv.description}`;
13
- return `${inv.kind}:${JSON.stringify(inv.queryKey)}`;
17
+ return `${action}predicate:${inv.description}`;
18
+ return `${action}${inv.kind}:${JSON.stringify(inv.queryKey)}`;
14
19
  }
15
20
  /** The cache filter that selects exactly the queries one invalidation targets. */
16
21
  function invalidationFilters(inv) {
@@ -52,7 +57,7 @@ function cancelPendingInitialReads(queryClient, filters) {
52
57
  * has settled — i.e. once the refetches those invalidations triggered have
53
58
  * landed. The dispatch itself stays synchronous (every cancellation and
54
59
  * invalidation is issued before this function returns), so a fire-and-forget
55
- * caller keeps the prior behavior exactly, while a mutation `onSuccess` that
60
+ * caller can ignore the promise, while a mutation `onSuccess` that
56
61
  * RETURNS or awaits the promise stays pending until the refreshed data is in
57
62
  * the cache. The promise never rejects: `invalidateQueries` refetches without
58
63
  * `throwOnError`, so a failed refetch surfaces on its own query, never as a
@@ -82,8 +87,21 @@ export function applyInvalidations(queryClient, invalidations) {
82
87
  if (seen.has(sig))
83
88
  continue;
84
89
  seen.add(sig);
85
- const refetchType = inv.refetchType ?? "active";
86
90
  const filters = invalidationFilters(inv);
91
+ const action = inv.action ?? "refresh";
92
+ if (action === "remove") {
93
+ // Removing a query abandons its read in flight silently, so that read never puts the dropped
94
+ // answer back, but a screen still watching it would go on showing it as fetching. Cancelled
95
+ // first, the read ends visibly and the watcher reads idle.
96
+ void queryClient.cancelQueries(filters);
97
+ queryClient.removeQueries(filters);
98
+ continue;
99
+ }
100
+ if (action === "reset") {
101
+ refreshes.push(queryClient.resetQueries(filters));
102
+ continue;
103
+ }
104
+ const refetchType = inv.refetchType ?? "active";
87
105
  if (refetchType !== "none") {
88
106
  cancelPendingInitialReads(queryClient, filters);
89
107
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cache-invalidation.js","sourceRoot":"","sources":["../../src/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 CacheInvalidation =\n | { kind: \"exact\"; queryKey: QueryKey; refetchType?: RefetchType }\n | { kind: \"prefix\"; queryKey: QueryKey; refetchType?: RefetchType }\n | {\n kind: \"predicate\";\n description: string;\n match: (query: Query) => boolean;\n refetchType?: RefetchType;\n };\n\nexport function exact(queryKey: QueryKey, opts?: { refetchType?: RefetchType }): CacheInvalidation {\n return { kind: \"exact\", queryKey, refetchType: opts?.refetchType };\n}\n\nexport function prefix(queryKey: QueryKey, opts?: { refetchType?: RefetchType }): CacheInvalidation {\n return { kind: \"prefix\", queryKey, refetchType: opts?.refetchType };\n}\n\nexport function predicate(\n description: string,\n match: (query: Query) => boolean,\n opts?: { refetchType?: RefetchType },\n): CacheInvalidation {\n return { kind: \"predicate\", description, match, refetchType: opts?.refetchType };\n}\n\nexport function serializeInvalidation(inv: CacheInvalidation): string {\n if (inv.kind === \"predicate\") return `predicate:${inv.description}`;\n return `${inv.kind}:${JSON.stringify(inv.queryKey)}`;\n}\n\n/** The cache filter that selects exactly the queries one invalidation targets. */\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 * Cancel every query matched by `filters` that is in a PENDING INITIAL READ —\n * currently fetching with no data yet.\n *\n * TanStack's `Query.fetch` only cancels a running request on refetch when the\n * query already holds data; an initial fetch in flight is REUSED instead, so\n * the invalidation's refetch resolves with the value the server returned BEFORE\n * the mutation committed and then marks the query fresh. Cancelling first makes\n * the invalidation's refetch a genuinely new request: the retryer is rejected\n * synchronously (its `onCancel` reverts state to idle in the same tick), so the\n * `invalidateQueries` call that follows starts a fresh fetch, and the abandoned\n * promise's late, stale resolution is discarded.\n *\n * Cancellation is always scoped to one concrete query key — never a bare\n * `cancelQueries()`.\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/**\n * Dispatch a deduplicated list of cache invalidations.\n *\n * Returns a promise that resolves once every `invalidateQueries` call it issued\n * has settled — i.e. once the refetches those invalidations triggered have\n * landed. The dispatch itself stays synchronous (every cancellation and\n * invalidation is issued before this function returns), so a fire-and-forget\n * caller keeps the prior behavior exactly, while a mutation `onSuccess` that\n * RETURNS or awaits the promise stays pending until the refreshed data is in\n * the cache. The promise never rejects: `invalidateQueries` refetches without\n * `throwOnError`, so a failed refetch surfaces on its own query, never as a\n * failure of the committed action that triggered it. The scoped\n * `cancelQueries` calls stay fire-and-forget — cancellation takes effect\n * synchronously and is not part of the refresh being awaited.\n *\n * Before each invalidation, any query it matches that is mid-PENDING-INITIAL-READ\n * is cancelled (see `cancelPendingInitialReads`) so a read that started before\n * the write committed can never land as the query's first — and freshly-marked —\n * value.\n *\n * `refetchType` governs whether that cancellation happens:\n * - `'active'` (the default) and `'all'` cancel-then-invalidate every matched\n * pending initial read — including one TanStack's refetch pass will not\n * restart, since a stale first value would otherwise land and mark the query\n * fresh, while a cancelled one simply refetches for its next observer.\n * - `'none'` invalidates WITHOUT cancelling: the caller has asked for no\n * refetch at all, so cancelling would abandon the only in-flight read the\n * consumer has and leave it with no data and no pending request.\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 refetchType: RefetchType = inv.refetchType ?? \"active\";\n const filters = invalidationFilters(inv);\n\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"]}
1
+ {"version":3,"file":"cache-invalidation.js","sourceRoot":"","sources":["../../src/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\n/**\n * What an entry does to the queries it matches:\n * - `\"refresh\"` (the default) marks them stale and refetches per `refetchType`;\n * - `\"remove\"` cancels them and drops them from the cache — for data that no longer exists, or a\n * held answer that must not be served again;\n * - `\"reset\"` returns them to their initial state and refetches the active ones from scratch.\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\n/** `refetchType` belongs to a refresh; a remove or reset takes none. */\ntype EntryOptions = { 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\n/** The cache filter that selects exactly the queries one invalidation targets. */\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 * Cancel every query matched by `filters` that is in a PENDING INITIAL READ —\n * currently fetching with no data yet.\n *\n * TanStack's `Query.fetch` only cancels a running request on refetch when the\n * query already holds data; an initial fetch in flight is REUSED instead, so\n * the invalidation's refetch resolves with the value the server returned BEFORE\n * the mutation committed and then marks the query fresh. Cancelling first makes\n * the invalidation's refetch a genuinely new request: the retryer is rejected\n * synchronously (its `onCancel` reverts state to idle in the same tick), so the\n * `invalidateQueries` call that follows starts a fresh fetch, and the abandoned\n * promise's late, stale resolution is discarded.\n *\n * Cancellation is always scoped to one concrete query key — never a bare\n * `cancelQueries()`.\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/**\n * Dispatch a deduplicated list of cache invalidations.\n *\n * Returns a promise that resolves once every `invalidateQueries` call it issued\n * has settled — i.e. once the refetches those invalidations triggered have\n * landed. The dispatch itself stays synchronous (every cancellation and\n * invalidation is issued before this function returns), so a fire-and-forget\n * caller can ignore the promise, while a mutation `onSuccess` that\n * RETURNS or awaits the promise stays pending until the refreshed data is in\n * the cache. The promise never rejects: `invalidateQueries` refetches without\n * `throwOnError`, so a failed refetch surfaces on its own query, never as a\n * failure of the committed action that triggered it. The scoped\n * `cancelQueries` calls stay fire-and-forget — cancellation takes effect\n * synchronously and is not part of the refresh being awaited.\n *\n * Before each invalidation, any query it matches that is mid-PENDING-INITIAL-READ\n * is cancelled (see `cancelPendingInitialReads`) so a read that started before\n * the write committed can never land as the query's first — and freshly-marked —\n * value.\n *\n * `refetchType` governs whether that cancellation happens:\n * - `'active'` (the default) and `'all'` cancel-then-invalidate every matched\n * pending initial read — including one TanStack's refetch pass will not\n * restart, since a stale first value would otherwise land and mark the query\n * fresh, while a cancelled one simply refetches for its next observer.\n * - `'none'` invalidates WITHOUT cancelling: the caller has asked for no\n * refetch at all, so cancelling would abandon the only in-flight read the\n * consumer has and leave it with no data and no pending request.\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 // Removing a query abandons its read in flight silently, so that read never puts the dropped\n // answer back, but a screen still watching it would go on showing it as fetching. Cancelled\n // first, the read ends visibly and the watcher reads idle.\n void queryClient.cancelQueries(filters);\n queryClient.removeQueries(filters);\n continue;\n }\n if (action === \"reset\") {\n refreshes.push(queryClient.resetQueries(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"]}
@@ -1,5 +1,5 @@
1
1
  export { exact, prefix, predicate, serializeInvalidation, applyInvalidations, } from "./cache-invalidation.js";
2
- export type { CacheInvalidation, RefetchType } from "./cache-invalidation.js";
2
+ export type { CacheAction, CacheInvalidation, RefetchType } from "./cache-invalidation.js";
3
3
  export { createDomainEventInvalidator } from "./create-domain-event-invalidator.js";
4
4
  export type { DomainEventInvalidator, DomainEventInvalidationRegistry, } from "./create-domain-event-invalidator.js";
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/domain-events/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EACL,MAAM,EACN,SAAS,EACT,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,4BAA4B,EAAE,MAAM,sCAAsC,CAAC;AACpF,YAAY,EACV,sBAAsB,EACtB,+BAA+B,GAChC,MAAM,sCAAsC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/domain-events/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EACL,MAAM,EACN,SAAS,EACT,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3F,OAAO,EAAE,4BAA4B,EAAE,MAAM,sCAAsC,CAAC;AACpF,YAAY,EACV,sBAAsB,EACtB,+BAA+B,GAChC,MAAM,sCAAsC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/domain-events/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EACL,MAAM,EACN,SAAS,EACT,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,4BAA4B,EAAE,MAAM,sCAAsC,CAAC","sourcesContent":["export {\n exact,\n prefix,\n predicate,\n serializeInvalidation,\n applyInvalidations,\n} from \"./cache-invalidation.js\";\nexport type { CacheInvalidation, RefetchType } from \"./cache-invalidation.js\";\n\nexport { createDomainEventInvalidator } from \"./create-domain-event-invalidator.js\";\nexport type {\n DomainEventInvalidator,\n DomainEventInvalidationRegistry,\n} from \"./create-domain-event-invalidator.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/domain-events/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EACL,MAAM,EACN,SAAS,EACT,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,4BAA4B,EAAE,MAAM,sCAAsC,CAAC","sourcesContent":["export {\n exact,\n prefix,\n predicate,\n serializeInvalidation,\n applyInvalidations,\n} from \"./cache-invalidation.js\";\nexport type { CacheAction, CacheInvalidation, RefetchType } from \"./cache-invalidation.js\";\n\nexport { createDomainEventInvalidator } from \"./create-domain-event-invalidator.js\";\nexport type {\n DomainEventInvalidator,\n DomainEventInvalidationRegistry,\n} from \"./create-domain-event-invalidator.js\";\n"]}
package/dist/index.d.ts CHANGED
@@ -7,5 +7,5 @@ export { flattenInfiniteData, getInfiniteDataCount, } from './firestore/infinite
7
7
  export type { FirestoreSearchConfig, FirestoreSearchOptions, SearchEqualityFilter, } from './search/types.js';
8
8
  export { FIRESTORE_SEARCH_MIN_LENGTH, isSearchableText, normalizeSearchText } from './search/rule.js';
9
9
  export { createDomainEventInvalidator, exact, prefix, predicate, serializeInvalidation, applyInvalidations, } from "./domain-events/index.js";
10
- export type { DomainEventInvalidator, DomainEventInvalidationRegistry, CacheInvalidation, RefetchType, } from "./domain-events/index.js";
10
+ export type { DomainEventInvalidator, DomainEventInvalidationRegistry, CacheAction, CacheInvalidation, RefetchType, } from "./domain-events/index.js";
11
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG5C,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,EAC3B,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAGpD,YAAY,EACV,MAAM,EACN,oBAAoB,EACpB,eAAe,EACf,wBAAwB,EACxB,oBAAoB,EACpB,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,yBAAyB,EACzB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EACL,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,iCAAiC,CAAC;AAGzC,YAAY,EACV,qBAAqB,EACrB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,2BAA2B,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAGtG,OAAO,EACL,4BAA4B,EAC5B,KAAK,EACL,MAAM,EACN,SAAS,EACT,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,sBAAsB,EACtB,+BAA+B,EAC/B,iBAAiB,EACjB,WAAW,GACZ,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG5C,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,EAC3B,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAGpD,YAAY,EACV,MAAM,EACN,oBAAoB,EACpB,eAAe,EACf,wBAAwB,EACxB,oBAAoB,EACpB,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,yBAAyB,EACzB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EACL,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,iCAAiC,CAAC;AAGzC,YAAY,EACV,qBAAqB,EACrB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,2BAA2B,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAGtG,OAAO,EACL,4BAA4B,EAC5B,KAAK,EACL,MAAM,EACN,SAAS,EACT,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,sBAAsB,EACtB,+BAA+B,EAC/B,WAAW,EACX,iBAAiB,EACjB,WAAW,GACZ,MAAM,0BAA0B,CAAC"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,gBAAgB;AAChB,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,EAC3B,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAoB5B,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EACL,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,iCAAiC,CAAC;AAQzC,OAAO,EAAE,2BAA2B,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEtG,iFAAiF;AACjF,OAAO,EACL,4BAA4B,EAC5B,KAAK,EACL,MAAM,EACN,SAAS,EACT,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC","sourcesContent":["// Query defaults & staleTime presets (server-safe — plain constants, no React runtime)\nexport { STALE_TIMES } from './defaults.js';\n\n// Cache Helpers\nexport {\n invalidateByPrefix,\n removeByPrefix,\n updateQueryData,\n paginatedPageKey,\n mapPaginatedItems,\n prependToFirstPaginatedPage,\n placeRowInPages,\n} from './cache-helpers.js';\nexport type { PagedRows } from './cache-helpers.js';\n\n// Firestore Types & Pure Helpers\nexport type {\n WithId,\n FirestoreSourceState,\n WithSourceState,\n BatchFirestoreDocOutcome,\n FirestoreBaseOptions,\n FirestoreDocOptions,\n FirestoreCollectionOptions,\n FirestoreInfiniteOptions,\n FirestorePaginatedOptions,\n InfinitePage,\n PaginatedPage,\n MutationOperation,\n FirestoreMutationOptions,\n FirestoreBatchOptions,\n} from './firestore/types.js';\nexport { docWithId } from './firestore/types.js';\nexport {\n flattenInfiniteData,\n getInfiniteDataCount,\n} from './firestore/infinite-helpers.js';\n\n// Search Types\nexport type {\n FirestoreSearchConfig,\n FirestoreSearchOptions,\n SearchEqualityFilter,\n} from './search/types.js';\nexport { FIRESTORE_SEARCH_MIN_LENGTH, isSearchableText, normalizeSearchText } from './search/rule.js';\n\n// Domain-event invalidator (mechanism — consumers register their event registry)\nexport {\n createDomainEventInvalidator,\n exact,\n prefix,\n predicate,\n serializeInvalidation,\n applyInvalidations,\n} from \"./domain-events/index.js\";\nexport type {\n DomainEventInvalidator,\n DomainEventInvalidationRegistry,\n CacheInvalidation,\n RefetchType,\n} from \"./domain-events/index.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,gBAAgB;AAChB,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,2BAA2B,EAC3B,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAoB5B,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EACL,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,iCAAiC,CAAC;AAQzC,OAAO,EAAE,2BAA2B,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEtG,iFAAiF;AACjF,OAAO,EACL,4BAA4B,EAC5B,KAAK,EACL,MAAM,EACN,SAAS,EACT,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC","sourcesContent":["// Query defaults & staleTime presets (server-safe — plain constants, no React runtime)\nexport { STALE_TIMES } from './defaults.js';\n\n// Cache Helpers\nexport {\n invalidateByPrefix,\n removeByPrefix,\n updateQueryData,\n paginatedPageKey,\n mapPaginatedItems,\n prependToFirstPaginatedPage,\n placeRowInPages,\n} from './cache-helpers.js';\nexport type { PagedRows } from './cache-helpers.js';\n\n// Firestore Types & Pure Helpers\nexport type {\n WithId,\n FirestoreSourceState,\n WithSourceState,\n BatchFirestoreDocOutcome,\n FirestoreBaseOptions,\n FirestoreDocOptions,\n FirestoreCollectionOptions,\n FirestoreInfiniteOptions,\n FirestorePaginatedOptions,\n InfinitePage,\n PaginatedPage,\n MutationOperation,\n FirestoreMutationOptions,\n FirestoreBatchOptions,\n} from './firestore/types.js';\nexport { docWithId } from './firestore/types.js';\nexport {\n flattenInfiniteData,\n getInfiniteDataCount,\n} from './firestore/infinite-helpers.js';\n\n// Search Types\nexport type {\n FirestoreSearchConfig,\n FirestoreSearchOptions,\n SearchEqualityFilter,\n} from './search/types.js';\nexport { FIRESTORE_SEARCH_MIN_LENGTH, isSearchableText, normalizeSearchText } from './search/rule.js';\n\n// Domain-event invalidator (mechanism — consumers register their event registry)\nexport {\n createDomainEventInvalidator,\n exact,\n prefix,\n predicate,\n serializeInvalidation,\n applyInvalidations,\n} from \"./domain-events/index.js\";\nexport type {\n DomainEventInvalidator,\n DomainEventInvalidationRegistry,\n CacheAction,\n CacheInvalidation,\n RefetchType,\n} from \"./domain-events/index.js\";\n"]}
@@ -20,4 +20,5 @@ export { ABSENT_RETRY_DELAYS_MS } from './firestore/absence-scheduler.js';
20
20
  export { useFirestoreSearch } from './search/use-firestore-search.js';
21
21
  export { useReadRetryState, useReadRetryStates } from './use-read-retry-state.js';
22
22
  export type { ReadRetryEntry, ReadRetrySource, ReadRetryState } from './use-read-retry-state.js';
23
+ export { usePendingMutationVariables, pendingMutationVariables, usePendingMutationTargets, hasPendingMutation, duplicateSubmissionError, useDuplicateGuardedMutation, } from './pending-mutations.js';
23
24
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,YAAY,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAGnE,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGxD,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC3E,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAGrE,OAAO,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,yCAAyC,CAAC;AACjF,YAAY,EAAE,oBAAoB,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAC7G,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAC7E,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AAC/E,YAAY,EAAE,2BAA2B,EAAE,MAAM,wCAAwC,CAAC;AAC1F,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,4CAA4C,CAAC;AACtF,YAAY,EAAE,2BAA2B,EAAE,MAAM,4CAA4C,CAAC;AAG9F,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,wCAAwC,CAAC;AAGhD,OAAO,EACL,qBAAqB,GACtB,MAAM,sCAAsC,CAAC;AAC9C,YAAY,EACV,yBAAyB,EACzB,wBAAwB,EACxB,2BAA2B,GAC5B,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAG1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAGtE,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAClF,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,YAAY,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAGnE,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGxD,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC3E,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAGrE,OAAO,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,yCAAyC,CAAC;AACjF,YAAY,EAAE,oBAAoB,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAC7G,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAC7E,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AAC/E,YAAY,EAAE,2BAA2B,EAAE,MAAM,wCAAwC,CAAC;AAC1F,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,4CAA4C,CAAC;AACtF,YAAY,EAAE,2BAA2B,EAAE,MAAM,4CAA4C,CAAC;AAG9F,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,wCAAwC,CAAC;AAGhD,OAAO,EACL,qBAAqB,GACtB,MAAM,sCAAsC,CAAC;AAC9C,YAAY,EACV,yBAAyB,EACzB,wBAAwB,EACxB,2BAA2B,GAC5B,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAG1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAGtE,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAClF,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAIjG,OAAO,EACL,2BAA2B,EAC3B,wBAAwB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,wBAAwB,EACxB,2BAA2B,GAC5B,MAAM,wBAAwB,CAAC"}
@@ -21,4 +21,7 @@ export { ABSENT_RETRY_DELAYS_MS } from './firestore/absence-scheduler.js';
21
21
  export { useFirestoreSearch } from './search/use-firestore-search.js';
22
22
  // Read retry state
23
23
  export { useReadRetryState, useReadRetryStates } from './use-read-retry-state.js';
24
+ // Authoritative pending for interactive mutations: in-flight state and duplicate guards read from
25
+ // the shared MutationCache under the app's stable mutation keys.
26
+ export { usePendingMutationVariables, pendingMutationVariables, usePendingMutationTargets, hasPendingMutation, duplicateSubmissionError, useDuplicateGuardedMutation, } from './pending-mutations.js';
24
27
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,qEAAqE;AACrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAGvD,WAAW;AACX,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG9C,qBAAqB;AACrB,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAG3E,wBAAwB;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,yCAAyC,CAAC;AAEjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAC7E,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AAE/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,4CAA4C,CAAC;AAGtF,2BAA2B;AAC3B,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,wCAAwC,CAAC;AAEhD,uBAAuB;AACvB,OAAO,EACL,qBAAqB,GACtB,MAAM,sCAAsC,CAAC;AAM9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAE1E,cAAc;AACd,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAEtE,mBAAmB;AACnB,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC","sourcesContent":["\"use client\";\n\n// Query Client factory (client-only; requires @tanstack/react-query)\nexport { createQueryClient } from '../query-client.js';\nexport type { CreateQueryClientOptions } from '../query-client.js';\n\n// Provider\nexport { QueryProvider } from './provider.js';\nexport type { QueryProviderProps } from './provider.js';\n\n// Firestore Provider\nexport { FirestoreProvider, useFirestoreDb } from './firestore/context.js';\nexport type { FirestoreProviderProps } from './firestore/context.js';\n\n// Firestore Query Hooks\nexport { useFirestoreDoc } from './firestore/use-firestore-doc.js';\nexport { useFirestoreCollection } from './firestore/use-firestore-collection.js';\nexport type { FirestoreSourceState, WithSourceState, BatchFirestoreDocOutcome } from '../firestore/types.js';\nexport { useFirestoreInfinite } from './firestore/use-firestore-infinite.js';\nexport { useFirestorePaginated } from './firestore/use-firestore-paginated.js';\nexport type { UseFirestorePaginatedResult } from './firestore/use-firestore-paginated.js';\nexport { useFirestoreCount } from './firestore/use-firestore-count.js';\nexport { useFirestoreLiveInfinite } from './firestore/use-firestore-live-infinite.js';\nexport type { FirestoreLiveInfiniteResult } from './firestore/use-firestore-live-infinite.js';\n\n// Firestore Mutation Hooks\nexport {\n useFirestoreSet,\n useFirestoreUpdate,\n useFirestoreDelete,\n useFirestoreBatch,\n} from './firestore/use-firestore-mutations.js';\n\n// Batch Firestore Docs\nexport {\n useBatchFirestoreDocs,\n} from './firestore/useBatchFirestoreDocs.js';\nexport type {\n BatchFirestoreDocsOptions,\n BatchFirestoreDocsResult,\n BatchFirestoreDocsTransport,\n} from './firestore/useBatchFirestoreDocs.js';\nexport { ABSENT_RETRY_DELAYS_MS } from './firestore/absence-scheduler.js';\n\n// Search Hook\nexport { useFirestoreSearch } from './search/use-firestore-search.js';\n\n// Read retry state\nexport { useReadRetryState, useReadRetryStates } from './use-read-retry-state.js';\nexport type { ReadRetryEntry, ReadRetrySource, ReadRetryState } from './use-read-retry-state.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,qEAAqE;AACrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAGvD,WAAW;AACX,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG9C,qBAAqB;AACrB,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAG3E,wBAAwB;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,yCAAyC,CAAC;AAEjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAC7E,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AAE/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,4CAA4C,CAAC;AAGtF,2BAA2B;AAC3B,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,wCAAwC,CAAC;AAEhD,uBAAuB;AACvB,OAAO,EACL,qBAAqB,GACtB,MAAM,sCAAsC,CAAC;AAM9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAE1E,cAAc;AACd,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAEtE,mBAAmB;AACnB,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAGlF,kGAAkG;AAClG,iEAAiE;AACjE,OAAO,EACL,2BAA2B,EAC3B,wBAAwB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,wBAAwB,EACxB,2BAA2B,GAC5B,MAAM,wBAAwB,CAAC","sourcesContent":["\"use client\";\n\n// Query Client factory (client-only; requires @tanstack/react-query)\nexport { createQueryClient } from '../query-client.js';\nexport type { CreateQueryClientOptions } from '../query-client.js';\n\n// Provider\nexport { QueryProvider } from './provider.js';\nexport type { QueryProviderProps } from './provider.js';\n\n// Firestore Provider\nexport { FirestoreProvider, useFirestoreDb } from './firestore/context.js';\nexport type { FirestoreProviderProps } from './firestore/context.js';\n\n// Firestore Query Hooks\nexport { useFirestoreDoc } from './firestore/use-firestore-doc.js';\nexport { useFirestoreCollection } from './firestore/use-firestore-collection.js';\nexport type { FirestoreSourceState, WithSourceState, BatchFirestoreDocOutcome } from '../firestore/types.js';\nexport { useFirestoreInfinite } from './firestore/use-firestore-infinite.js';\nexport { useFirestorePaginated } from './firestore/use-firestore-paginated.js';\nexport type { UseFirestorePaginatedResult } from './firestore/use-firestore-paginated.js';\nexport { useFirestoreCount } from './firestore/use-firestore-count.js';\nexport { useFirestoreLiveInfinite } from './firestore/use-firestore-live-infinite.js';\nexport type { FirestoreLiveInfiniteResult } from './firestore/use-firestore-live-infinite.js';\n\n// Firestore Mutation Hooks\nexport {\n useFirestoreSet,\n useFirestoreUpdate,\n useFirestoreDelete,\n useFirestoreBatch,\n} from './firestore/use-firestore-mutations.js';\n\n// Batch Firestore Docs\nexport {\n useBatchFirestoreDocs,\n} from './firestore/useBatchFirestoreDocs.js';\nexport type {\n BatchFirestoreDocsOptions,\n BatchFirestoreDocsResult,\n BatchFirestoreDocsTransport,\n} from './firestore/useBatchFirestoreDocs.js';\nexport { ABSENT_RETRY_DELAYS_MS } from './firestore/absence-scheduler.js';\n\n// Search Hook\nexport { useFirestoreSearch } from './search/use-firestore-search.js';\n\n// Read retry state\nexport { useReadRetryState, useReadRetryStates } from './use-read-retry-state.js';\nexport type { ReadRetryEntry, ReadRetrySource, ReadRetryState } from './use-read-retry-state.js';\n\n// Authoritative pending for interactive mutations: in-flight state and duplicate guards read from\n// the shared MutationCache under the app's stable mutation keys.\nexport {\n usePendingMutationVariables,\n pendingMutationVariables,\n usePendingMutationTargets,\n hasPendingMutation,\n duplicateSubmissionError,\n useDuplicateGuardedMutation,\n} from './pending-mutations.js';\n"]}
@@ -0,0 +1,29 @@
1
+ import { type MutationKey, type QueryClient, type UseMutationResult } from "@tanstack/react-query";
2
+ /** The variables of every pending mutation under `mutationKey` (prefix-matched), live. */
3
+ export declare function usePendingMutationVariables<V>(mutationKey: MutationKey): ReadonlyArray<V | undefined>;
4
+ /** The variables of every pending mutation under `mutationKey`, read once — for a duplicate guard. */
5
+ export declare function pendingMutationVariables<V>(queryClient: QueryClient, mutationKey: MutationKey): Array<V | undefined>;
6
+ /**
7
+ * The targets of every pending mutation under `mutationKey`, live — for a list whose rows each
8
+ * ask "is mine pending?" through one hook call. `targetOf` names a variables' target, or returns
9
+ * nothing for variables without one.
10
+ */
11
+ export declare function usePendingMutationTargets<V>(mutationKey: MutationKey, targetOf: (variables: V) => string | null | undefined): ReadonlySet<string>;
12
+ /**
13
+ * True when a mutation under `mutationKey` is pending — any one, or with `matches`, one whose
14
+ * variables are for the same target.
15
+ */
16
+ export declare function hasPendingMutation<V>(queryClient: QueryClient, mutationKey: MutationKey, matches?: (variables: V) => boolean): boolean;
17
+ /**
18
+ * The rejection a guarded `mutateAsync` throws for a duplicate submission: an `AbortError`, the
19
+ * shape a caller already treats as "nothing happened" rather than a failure to report.
20
+ */
21
+ export declare function duplicateSubmissionError(what: string): Error;
22
+ /**
23
+ * Wrap a keyed mutation's `mutate` / `mutateAsync` in the cache-consulting duplicate guard:
24
+ * `mutate` drops a duplicate silently, and `mutateAsync` rejects it with
25
+ * {@link duplicateSubmissionError}. `isDuplicate` must have a stable identity (declare it at module
26
+ * level) so the guarded callbacks keep theirs.
27
+ */
28
+ export declare function useDuplicateGuardedMutation<TData, TError, TVariables, TContext>(mutation: UseMutationResult<TData, TError, TVariables, TContext>, isDuplicate: (queryClient: QueryClient, variables: TVariables) => boolean, what: string): UseMutationResult<TData, TError, TVariables, TContext>;
29
+ //# sourceMappingURL=pending-mutations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-mutations.d.ts","sourceRoot":"","sources":["../../src/react/pending-mutations.ts"],"names":[],"mappings":"AAeA,OAAO,EAIL,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAC;AAE/B,0FAA0F;AAC1F,wBAAgB,2BAA2B,CAAC,CAAC,EAAE,WAAW,EAAE,WAAW,GAAG,aAAa,CAAC,CAAC,GAAG,SAAS,CAAC,CAKrG;AAED,sGAAsG;AACtG,wBAAgB,wBAAwB,CAAC,CAAC,EACxC,WAAW,EAAE,WAAW,EACxB,WAAW,EAAE,WAAW,GACvB,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC,CAKtB;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,EACzC,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,CAAC,SAAS,EAAE,CAAC,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,WAAW,CAAC,MAAM,CAAC,CAQrB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,WAAW,EAAE,WAAW,EACxB,WAAW,EAAE,WAAW,EACxB,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,KAAK,OAAO,GAClC,OAAO,CAIT;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAI5D;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAC7E,QAAQ,EAAE,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,EAChE,WAAW,EAAE,CAAC,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,KAAK,OAAO,EACzE,IAAI,EAAE,MAAM,GACX,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,CAkBxD"}
@@ -0,0 +1,85 @@
1
+ "use client";
2
+ // Authoritative pending for interactive mutations. A mutation registers in React Query's shared
3
+ // MutationCache under a stable `mutationKey` the app declares (one key per action), and its target
4
+ // travels in the mutation's variables. Pending is read from that cache, never from one mounted
5
+ // control's `isPending`, which gives three guarantees:
6
+ //
7
+ // 1. an in-flight request survives its originating control unmounting;
8
+ // 2. a remounted control still renders pending for that target;
9
+ // 3. a second submission for the same target is refused while the first is pending.
10
+ //
11
+ // The app owns its keys, its variable shapes and what counts as the same target; this module owns
12
+ // only the mechanics.
13
+ import { useCallback } from "react";
14
+ import { useMutationState, useQueryClient, } from "@tanstack/react-query";
15
+ /** The variables of every pending mutation under `mutationKey` (prefix-matched), live. */
16
+ export function usePendingMutationVariables(mutationKey) {
17
+ return useMutationState({
18
+ filters: { mutationKey, status: "pending" },
19
+ select: (mutation) => mutation.state.variables,
20
+ });
21
+ }
22
+ /** The variables of every pending mutation under `mutationKey`, read once — for a duplicate guard. */
23
+ export function pendingMutationVariables(queryClient, mutationKey) {
24
+ return queryClient
25
+ .getMutationCache()
26
+ .findAll({ mutationKey, status: "pending" })
27
+ .map((mutation) => mutation.state.variables);
28
+ }
29
+ /**
30
+ * The targets of every pending mutation under `mutationKey`, live — for a list whose rows each
31
+ * ask "is mine pending?" through one hook call. `targetOf` names a variables' target, or returns
32
+ * nothing for variables without one.
33
+ */
34
+ export function usePendingMutationTargets(mutationKey, targetOf) {
35
+ const targets = new Set();
36
+ for (const variables of usePendingMutationVariables(mutationKey)) {
37
+ if (variables === undefined)
38
+ continue;
39
+ const target = targetOf(variables);
40
+ if (target)
41
+ targets.add(target);
42
+ }
43
+ return targets;
44
+ }
45
+ /**
46
+ * True when a mutation under `mutationKey` is pending — any one, or with `matches`, one whose
47
+ * variables are for the same target.
48
+ */
49
+ export function hasPendingMutation(queryClient, mutationKey, matches) {
50
+ const pending = pendingMutationVariables(queryClient, mutationKey);
51
+ if (!matches)
52
+ return pending.length > 0;
53
+ return pending.some((variables) => variables !== undefined && matches(variables));
54
+ }
55
+ /**
56
+ * The rejection a guarded `mutateAsync` throws for a duplicate submission: an `AbortError`, the
57
+ * shape a caller already treats as "nothing happened" rather than a failure to report.
58
+ */
59
+ export function duplicateSubmissionError(what) {
60
+ const duplicate = new Error(`${what} is already in flight.`);
61
+ duplicate.name = "AbortError";
62
+ return duplicate;
63
+ }
64
+ /**
65
+ * Wrap a keyed mutation's `mutate` / `mutateAsync` in the cache-consulting duplicate guard:
66
+ * `mutate` drops a duplicate silently, and `mutateAsync` rejects it with
67
+ * {@link duplicateSubmissionError}. `isDuplicate` must have a stable identity (declare it at module
68
+ * level) so the guarded callbacks keep theirs.
69
+ */
70
+ export function useDuplicateGuardedMutation(mutation, isDuplicate, what) {
71
+ const queryClient = useQueryClient();
72
+ const { mutate: rawMutate, mutateAsync: rawMutateAsync } = mutation;
73
+ const mutate = useCallback((variables, options) => {
74
+ if (isDuplicate(queryClient, variables))
75
+ return;
76
+ rawMutate(variables, options);
77
+ }, [queryClient, rawMutate, isDuplicate]);
78
+ const mutateAsync = useCallback(async (variables, options) => {
79
+ if (isDuplicate(queryClient, variables))
80
+ throw duplicateSubmissionError(what);
81
+ return rawMutateAsync(variables, options);
82
+ }, [queryClient, rawMutateAsync, isDuplicate, what]);
83
+ return { ...mutation, mutate, mutateAsync };
84
+ }
85
+ //# sourceMappingURL=pending-mutations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-mutations.js","sourceRoot":"","sources":["../../src/react/pending-mutations.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n// Authoritative pending for interactive mutations. A mutation registers in React Query's shared\n// MutationCache under a stable `mutationKey` the app declares (one key per action), and its target\n// travels in the mutation's variables. Pending is read from that cache, never from one mounted\n// control's `isPending`, which gives three guarantees:\n//\n// 1. an in-flight request survives its originating control unmounting;\n// 2. a remounted control still renders pending for that target;\n// 3. a second submission for the same target is refused while the first is pending.\n//\n// The app owns its keys, its variable shapes and what counts as the same target; this module owns\n// only the mechanics.\n\nimport { useCallback } from \"react\";\nimport {\n useMutationState,\n useQueryClient,\n type MutateOptions,\n type MutationKey,\n type QueryClient,\n type UseMutationResult,\n} from \"@tanstack/react-query\";\n\n/** The variables of every pending mutation under `mutationKey` (prefix-matched), live. */\nexport function usePendingMutationVariables<V>(mutationKey: MutationKey): ReadonlyArray<V | undefined> {\n return useMutationState({\n filters: { mutationKey, status: \"pending\" },\n select: (mutation) => mutation.state.variables as V | undefined,\n });\n}\n\n/** The variables of every pending mutation under `mutationKey`, read once — for a duplicate guard. */\nexport function pendingMutationVariables<V>(\n queryClient: QueryClient,\n mutationKey: MutationKey,\n): Array<V | undefined> {\n return queryClient\n .getMutationCache()\n .findAll({ mutationKey, status: \"pending\" })\n .map((mutation) => mutation.state.variables as V | undefined);\n}\n\n/**\n * The targets of every pending mutation under `mutationKey`, live — for a list whose rows each\n * ask \"is mine pending?\" through one hook call. `targetOf` names a variables' target, or returns\n * nothing for variables without one.\n */\nexport function usePendingMutationTargets<V>(\n mutationKey: MutationKey,\n targetOf: (variables: V) => string | null | undefined,\n): ReadonlySet<string> {\n const targets = new Set<string>();\n for (const variables of usePendingMutationVariables<V>(mutationKey)) {\n if (variables === undefined) continue;\n const target = targetOf(variables);\n if (target) targets.add(target);\n }\n return targets;\n}\n\n/**\n * True when a mutation under `mutationKey` is pending — any one, or with `matches`, one whose\n * variables are for the same target.\n */\nexport function hasPendingMutation<V>(\n queryClient: QueryClient,\n mutationKey: MutationKey,\n matches?: (variables: V) => boolean,\n): boolean {\n const pending = pendingMutationVariables<V>(queryClient, mutationKey);\n if (!matches) return pending.length > 0;\n return pending.some((variables) => variables !== undefined && matches(variables));\n}\n\n/**\n * The rejection a guarded `mutateAsync` throws for a duplicate submission: an `AbortError`, the\n * shape a caller already treats as \"nothing happened\" rather than a failure to report.\n */\nexport function duplicateSubmissionError(what: string): Error {\n const duplicate = new Error(`${what} is already in flight.`);\n duplicate.name = \"AbortError\";\n return duplicate;\n}\n\n/**\n * Wrap a keyed mutation's `mutate` / `mutateAsync` in the cache-consulting duplicate guard:\n * `mutate` drops a duplicate silently, and `mutateAsync` rejects it with\n * {@link duplicateSubmissionError}. `isDuplicate` must have a stable identity (declare it at module\n * level) so the guarded callbacks keep theirs.\n */\nexport function useDuplicateGuardedMutation<TData, TError, TVariables, TContext>(\n mutation: UseMutationResult<TData, TError, TVariables, TContext>,\n isDuplicate: (queryClient: QueryClient, variables: TVariables) => boolean,\n what: string,\n): UseMutationResult<TData, TError, TVariables, TContext> {\n const queryClient = useQueryClient();\n const { mutate: rawMutate, mutateAsync: rawMutateAsync } = mutation;\n const mutate = useCallback(\n (variables: TVariables, options?: MutateOptions<TData, TError, TVariables, TContext>) => {\n if (isDuplicate(queryClient, variables)) return;\n rawMutate(variables, options);\n },\n [queryClient, rawMutate, isDuplicate],\n );\n const mutateAsync = useCallback(\n async (variables: TVariables, options?: MutateOptions<TData, TError, TVariables, TContext>) => {\n if (isDuplicate(queryClient, variables)) throw duplicateSubmissionError(what);\n return rawMutateAsync(variables, options);\n },\n [queryClient, rawMutateAsync, isDuplicate, what],\n );\n return { ...mutation, mutate, mutateAsync } as UseMutationResult<TData, TError, TVariables, TContext>;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/query-core",
3
- "version": "0.14.18",
3
+ "version": "0.14.20",
4
4
  "description": "Shared TanStack Query (React Query) client defaults, query key helpers, and Firestore integration hooks for TTT Productions apps",
5
5
  "repository": {
6
6
  "type": "git",
@@ -60,7 +60,7 @@
60
60
  }
61
61
  },
62
62
  "dependencies": {
63
- "@ttt-productions/firebase-helpers": "^0.12.11"
63
+ "@ttt-productions/firebase-helpers": "^0.12.12"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@tanstack/react-query": "^5.0.0",