@mmstack/resource 19.6.1 → 19.6.3

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/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './lib/infinite-query';
1
2
  export * from './lib/manual-query';
2
3
  export * from './lib/mutation-resource';
3
4
  export * from './lib/options';
@@ -0,0 +1,81 @@
1
+ import { type HttpResourceRequest } from '@angular/common/http';
2
+ import { type Signal } from '@angular/core';
3
+ import { type PAUSED, type QueryResourceOptions, type QueryResourceRef, type RequestContext } from './query-resource';
4
+ /**
5
+ * Context passed to an infinite query's request fn: the {@link RequestContext}
6
+ * (so the fn can return `ctx.paused` to pause the resource, exactly like
7
+ * `queryResource`) plus the `pageParam` addressing the page to load.
8
+ */
9
+ export type InfiniteRequestContext<TPageParam> = RequestContext & {
10
+ pageParam: TPageParam;
11
+ };
12
+ /**
13
+ * Options for {@link infiniteQueryResource}. Extends {@link QueryResourceOptions}
14
+ * (minus `defaultValue` — the aggregate value is always the `pages` array) with the
15
+ * pagination contract.
16
+ */
17
+ export type InfiniteQueryResourceOptions<TPage, TRaw = TPage, TPageParam = unknown> = Omit<QueryResourceOptions<TPage, TRaw>, 'defaultValue'> & {
18
+ /** The page param the FIRST page is requested with (e.g. `0`, `1`, or a cursor seed). */
19
+ initialPageParam: TPageParam;
20
+ /**
21
+ * Derives the NEXT page's param from the freshly loaded page (and all pages so far).
22
+ * Return `null`/`undefined` to signal "no more pages" — `hasNextPage` flips false
23
+ * and `fetchNextPage()` becomes a no-op.
24
+ *
25
+ * @example
26
+ * // cursor-based
27
+ * getNextPageParam: (last) => last.nextCursor;
28
+ * // offset-based
29
+ * getNextPageParam: (last, all) => (last.items.length < PAGE_SIZE ? null : all.length);
30
+ */
31
+ getNextPageParam: (lastPage: NoInfer<TPage>, allPages: NoInfer<TPage>[]) => TPageParam | null | undefined;
32
+ };
33
+ /**
34
+ * A paginated query resource. `pages` accumulates every loaded page in order;
35
+ * `fetchNextPage()` loads the next one (no-op while one is in flight or when
36
+ * exhausted). Inherits the underlying query's `status`/`error`/`isLoading` and
37
+ * its features (cache, retry, circuit breaker, refresh).
38
+ */
39
+ export type InfiniteQueryResourceRef<TPage> = {
40
+ /** Every page loaded so far, in load order. */
41
+ pages: Signal<TPage[]>;
42
+ /** `true` once the first page is in and `getNextPageParam` keeps producing params. */
43
+ hasNextPage: Signal<boolean>;
44
+ /** `true` while a page request beyond the first is in flight. */
45
+ isFetchingNextPage: Signal<boolean>;
46
+ /** The underlying query's loading state (first page + subsequent pages). */
47
+ isLoading: Signal<boolean>;
48
+ status: QueryResourceRef<TPage | undefined>['status'];
49
+ error: QueryResourceRef<TPage | undefined>['error'];
50
+ /** Loads the next page. No-op while loading or when `hasNextPage()` is false. */
51
+ fetchNextPage: () => void;
52
+ /** Reloads the CURRENT page param — the freshly loaded page replaces its slot. */
53
+ reload: () => boolean;
54
+ /** Drops all pages and refetches from `initialPageParam`. */
55
+ reset: () => void;
56
+ destroy: () => void;
57
+ };
58
+ /**
59
+ * Creates a paginated HTTP resource over {@link queryResource}: one page request at a
60
+ * time, accumulated into a `pages` signal — cursor- and offset-based pagination both
61
+ * fit through `getNextPageParam`. Each page request inherits the full queryResource
62
+ * feature set (caching per page, retries, circuit breaker, refresh triggers).
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * const posts = infiniteQueryResource<PostPage, PostPage, number>(
67
+ * ({ pageParam }) => ({ url: '/api/posts', params: { page: pageParam } }),
68
+ * {
69
+ * initialPageParam: 0,
70
+ * getNextPageParam: (last, all) => (last.items.length < 20 ? null : all.length),
71
+ * cache: true,
72
+ * },
73
+ * );
74
+ *
75
+ * // template:
76
+ * // @for (page of posts.pages(); track $index) { ... }
77
+ * // <button (click)="posts.fetchNextPage()" [disabled]="!posts.hasNextPage()">More</button>
78
+ * const flat = computed(() => posts.pages().flatMap((p) => p.items));
79
+ * ```
80
+ */
81
+ export declare function infiniteQueryResource<TPage, TRaw = TPage, TPageParam = unknown>(request: (ctx: InfiniteRequestContext<TPageParam>) => HttpResourceRequest | string | undefined | typeof PAUSED, options: InfiniteQueryResourceOptions<TPage, TRaw, TPageParam>): InfiniteQueryResourceRef<TPage>;
@@ -36,7 +36,7 @@ type NextRequest<TMethod extends HttpResourceRequest['method'], TMutation> = TMe
36
36
  * };
37
37
  * ```
38
38
  */
39
- export type MutationResourceOptions<TResult, TRaw = TResult, TMutation = TResult, TCTX = void, TICTX = TCTX, TError = unknown> = Omit<QueryResourceOptions<TResult, TRaw>, 'equal' | 'onError' | 'keepPrevious' | 'refresh' | 'cache'> & {
39
+ export type MutationResourceOptions<TResult, TRaw = TResult, TMutation = TResult, TCTX = void, TICTX = TCTX, TError = unknown> = Omit<QueryResourceOptions<TResult, TRaw>, 'equal' | 'onError' | 'keepPrevious' | 'refresh' | 'cache' | 'pause'> & {
40
40
  /**
41
41
  * A callback function that is called before the mutation request is made.
42
42
  * @param value The value being mutated (the `body` of the request).
@@ -66,6 +66,25 @@ export type MutationResourceOptions<TResult, TRaw = TResult, TMutation = TResult
66
66
  * @default false
67
67
  */
68
68
  queue?: boolean;
69
+ /**
70
+ * Cache entries to invalidate after a SUCCESSFUL mutation — the declarative
71
+ * alternative to calling `injectQueryCache().invalidatePrefix(...)` in `onSuccess`.
72
+ *
73
+ * Each string is a URL prefix matched against auto-generated `GET` cache keys
74
+ * (`GET:${url}:...`): `'/api/posts'` invalidates `/api/posts` with any query params,
75
+ * plus subpaths like `/api/posts/123` — and all `varyHeaders` variants of each.
76
+ * Note that plain prefix matching also catches sibling paths sharing the prefix
77
+ * (`/api/posts-archive`); pass `'/api/posts/'` or the exact URL to narrow.
78
+ *
79
+ * Entries keyed by a custom `hash` function follow that function's shape, not the
80
+ * auto-key shape — invalidate those manually via `injectQueryCache().invalidateWhere`.
81
+ *
82
+ * The function form receives the mutation result and the mutated value:
83
+ * ```ts
84
+ * invalidates: (saved) => [`/api/posts`, `/api/users/${saved.authorId}`]
85
+ * ```
86
+ */
87
+ invalidates?: string[] | ((value: NoInfer<TResult>, mutation: NoInfer<TMutation>) => string[]);
69
88
  equal?: ValueEqualityFn<TMutation>;
70
89
  };
71
90
  /**
package/lib/options.d.ts CHANGED
@@ -1,5 +1,59 @@
1
1
  import { InjectionToken, type Injector, type Provider, type ResourceRef } from '@angular/core';
2
2
  import { type CircuitBreakerOptions, type RetryOptions } from './util';
3
+ import { type HttpResourceRequest } from '@angular/common/http';
4
+ /**
5
+ * Options for enabling and configuring caching for a resource.
6
+ *
7
+ * - `true`: Enables caching with default settings.
8
+ * - `{ ttl?: number; staleTime?: number; hash?: (req: HttpResourceRequest) => string; }`: Configures caching with custom settings.
9
+ */
10
+ export type ResourceCacheOptions = true | {
11
+ /**
12
+ * The time-to-live for the cached value in milliseconds.
13
+ * After this time, the value is removed from the cache entirely.
14
+ * Defaults to 5 minutes (`300_000`).
15
+ */
16
+ ttl?: number;
17
+ /**
18
+ * The time in milliseconds during which the cached value is considered "fresh".
19
+ * If a request is made within this time, the cached value is returned immediately without a background fetch.
20
+ * Defaults to 0 (always stale, triggering a background fetch).
21
+ */
22
+ staleTime?: number;
23
+ /**
24
+ * A custom function to generate the cache key from the HTTP request.
25
+ * By default, it hashes the URL, method, headers (specified by `varyHeaders`), and body.
26
+ */
27
+ hash?: (req: HttpResourceRequest) => string;
28
+ /**
29
+ * A list of header names to include in the default cache key generation.
30
+ * Ignored if a custom `hash` function is provided.
31
+ *
32
+ * Note: still call `cache.clear()` on logout — the previous user's entries are
33
+ * unreachable under the new key but linger until their TTL.
34
+ */
35
+ varyHeaders?: string[];
36
+ /**
37
+ * Whether to bust the browser cache by appending a unique query parameter to the request URL.
38
+ * This is useful for ensuring that the latest data is fetched from the server, bypassing any
39
+ * cached responses in the browser. The unique parameter is removed before calling the cache function, so it does not affect the cache key.
40
+ * @default false - By default, the resource will not bust the browser cache.
41
+ */
42
+ bustBrowserCache?: boolean;
43
+ /**
44
+ * Whether to ignore the `Cache-Control` headers from the server when caching responses.
45
+ * If set to `true`, the resource will not respect any cache directives from the server,
46
+ * allowing you to control caching behavior entirely through the resource options.
47
+ * @default false - By default the resource will respect `Cache-Control` headers.
48
+ */
49
+ ignoreCacheControl?: boolean;
50
+ /**
51
+ * If true, it saves the cached responses to an indexedDb table, making it available across
52
+ * tabs, sessions and reloads..only valid JSON responses can be persisted (so no Blobs, formData, ArrayBuffers etc.)
53
+ * @default false
54
+ */
55
+ persist?: boolean;
56
+ };
3
57
  /**
4
58
  * Auto-registration into the nearest transition scope, as a resource OPTION:
5
59
  * - `'suspend'` — register as *suspending*: the boundary holds its placeholder until this
@@ -1,48 +1,9 @@
1
1
  import { type HttpHeaders, type HttpResourceOptions, type HttpResourceRef, type HttpResourceRequest } from '@angular/common/http';
2
2
  import { type Provider, type Signal, type WritableSignal } from '@angular/core';
3
- import { type CommonResourceOptions } from './options';
4
- /**
5
- * Options for configuring caching behavior of a `queryResource`.
6
- * - `true`: Enables caching with default settings.
7
- * - `{ ttl?: number; staleTime?: number; hash?: (req: HttpResourceRequest) => string; }`: Configures caching with custom settings.
8
- */
9
- type ResourceCacheOptions = true | {
10
- /**
11
- * The Time To Live (TTL) for the cached data, in milliseconds. After this time, the cached data is
12
- * considered expired and will be refetched.
13
- */
14
- ttl?: number;
15
- /**
16
- * The duration, in milliseconds, during which stale data can be served while a revalidation request
17
- * is made in the background.
18
- */
19
- staleTime?: number;
20
- /**
21
- * A custom function to generate the cache key. Defaults to using the request URL with parameters.
22
- * Provide a custom hash function if you need more control over how cache keys are generated,
23
- * for instance, to ignore certain query parameters or to use request body for the cache key.
24
- */
25
- hash?: (req: HttpResourceRequest) => string;
26
- /**
27
- * Whether to bust the browser cache by appending a unique query parameter to the request URL.
28
- * This is useful for ensuring that the latest data is fetched from the server, bypassing any
29
- * cached responses in the browser. The unique parameter is removed before calling the cache function, so it does not affect the cache key.
30
- * @default false - By default, the resource will not bust the browser cache.
31
- */
32
- bustBrowserCache?: boolean;
33
- /**
34
- * Whether to ignore the `Cache-Control` headers from the server when caching responses.
35
- * If set to `true`, the resource will not respect any cache directives from the server,
36
- * allowing you to control caching behavior entirely through the resource options.
37
- * @default false - By default the resource will respect `Cache-Control` headers.
38
- */
39
- ignoreCacheControl?: boolean;
40
- /**
41
- * Whether to persist the cache entry in the local DB instance.
42
- * @default false - By default, the cache entry is not persisted.
43
- */
44
- persist?: boolean;
45
- };
3
+ import { type PauseOption } from '@mmstack/primitives';
4
+ import { type CommonResourceOptions, type ResourceCacheOptions } from './options';
5
+ import { type RefreshOptions } from './util';
6
+ export { type RefreshOptions } from './util';
46
7
  /**
47
8
  * Options for configuring a `queryResource`. Extends Angular's
48
9
  * `HttpResourceOptions` with caching, retries, refresh intervals, circuit
@@ -68,10 +29,19 @@ export type QueryResourceOptions<TResult, TRaw = TResult> = HttpResourceOptions<
68
29
  */
69
30
  keepPrevious?: boolean;
70
31
  /**
71
- * The refresh interval, in milliseconds. If provided, the resource will automatically
72
- * refresh its data at the specified interval.
32
+ * Automatic refresh behavior. A number polls every n milliseconds; the object form
33
+ * composes polling with event-driven triggers:
34
+ *
35
+ * ```ts
36
+ * refresh: 30_000 // poll every 30s
37
+ * refresh: { onFocus: true, onReconnect: true } // refetch on tab refocus / back-online
38
+ * refresh: { interval: 60_000, onFocus: true } // both
39
+ * ```
40
+ *
41
+ * Triggers respect the resource's disabled/paused state (no refetch while
42
+ * offline, circuit-open, or paused).
73
43
  */
74
- refresh?: number;
44
+ refresh?: RefreshOptions;
75
45
  /**
76
46
  * Called on every failed attempt, including each retry.
77
47
  *
@@ -87,6 +57,20 @@ export type QueryResourceOptions<TResult, TRaw = TResult> = HttpResourceOptions<
87
57
  * Options for enabling and configuring caching for the resource.
88
58
  */
89
59
  cache?: ResourceCacheOptions;
60
+ /**
61
+ * Opt-in automatic pausing (off by default — existing behavior unchanged):
62
+ * - `true` — pause whenever the surrounding Activity boundary (`MmActivity` /
63
+ * `providePaused` from `@mmstack/primitives`) is paused. Outside a boundary this
64
+ * is a no-op, so it's safe to set app-wide via `provideQueryResourceOptions`.
65
+ * - a `() => boolean` predicate (a `Signal<boolean>` qualifies) — pause while it
66
+ * returns `true`.
67
+ *
68
+ * Pausing has the same semantics as returning `ctx.paused` from the request fn:
69
+ * the resource HOLDS its current value and last request (no refetch on resume if
70
+ * the request is unchanged) and stops background work (polling, focus/reconnect
71
+ * triggers). The two compose — either source can pause the resource.
72
+ */
73
+ pause?: PauseOption;
90
74
  /**
91
75
  * Comparison of request object
92
76
  */
@@ -113,6 +97,9 @@ export declare function provideQueryResourceOptions(valueOrFn: Partial<QueryReso
113
97
  * }
114
98
  * });
115
99
  * ```
100
+ *
101
+ * Note: a PAUSED resource also reports `'no-request'` — it holds its previous value
102
+ * and request, but no request is currently active.
116
103
  */
117
104
  export type DisabledReason = 'offline' | 'circuit-open' | 'no-request';
118
105
  /**
@@ -171,7 +158,10 @@ export type QueryResourceRef<TResult> = Omit<HttpResourceRef<TResult>, 'headers'
171
158
  disabledReason: Signal<DisabledReason | null>;
172
159
  /**
173
160
  * Prefetches data for the resource, populating the cache if caching is enabled. This can be
174
- * used to proactively load data before it's needed. If a slow connection is detected, prefetching is skipped.
161
+ * used to proactively load data before it's needed.
162
+ *
163
+ * Resolves immediately without fetching when caching is disabled or a slow
164
+ * connection is detected (prefetching would compete with user-initiated requests).
175
165
  *
176
166
  * @param req - Optional partial request parameters to use for the prefetch. This allows you
177
167
  * to prefetch data with different parameters than the main resource request.
@@ -235,4 +225,3 @@ export declare function queryResource<TResult, TRaw = TResult>(request: Resource
235
225
  * ```
236
226
  */
237
227
  export declare function queryResource<TResult, TRaw = TResult>(request: ResourceRequestFn, options?: QueryResourceOptions<TResult, TRaw>): QueryResourceRef<TResult | undefined>;
238
- export {};
@@ -21,6 +21,10 @@ export declare function setCacheContext(ctx: HttpContext | undefined, opt: Omit<
21
21
  * is made to the server, and the response is cached according to the configured TTL and staleness.
22
22
  * The interceptor also respects `Cache-Control` headers from the server.
23
23
  *
24
+ * Cache-enabled requests are single-flighted per cache key: N concurrent consumers of
25
+ * the same missing/stale entry share ONE network request. Non-cached requests are not
26
+ * touched — pair with `createDedupeRequestsInterceptor` to coalesce those as well.
27
+ *
24
28
  * @param allowedMethods - An array of HTTP methods for which caching should be enabled.
25
29
  * Defaults to `['GET', 'HEAD', 'OPTIONS']`.
26
30
  *
@@ -43,8 +43,11 @@ export type CacheEntry<T> = {
43
43
  updated: number;
44
44
  stale: number;
45
45
  useCount: number;
46
+ /** Timestamp of the last read/write — drives LRU eviction. */
47
+ lastAccessed: number;
46
48
  expiresAt: number;
47
- timeout: ReturnType<typeof setTimeout>;
49
+ /** Absent for non-finite/over-int32 TTLs — those rely on lazy expiry instead. */
50
+ timeout?: ReturnType<typeof setTimeout>;
48
51
  key: string;
49
52
  };
50
53
  /**
@@ -65,9 +68,27 @@ export declare class Cache<T> {
65
68
  private readonly internal;
66
69
  private readonly cleanupOpt;
67
70
  private readonly id;
71
+ /** True once async hydration from the persistence layer has completed (or was empty). */
72
+ private hydrated;
73
+ /** Keys invalidated while hydration was still in flight — must not be resurrected by it. */
74
+ private readonly hydrationTombstones;
75
+ private readonly hitCount;
76
+ private readonly missCount;
68
77
  /**
69
- * Destroys the cache instance, cleaning up any resources used by the cache.
70
- * This method is called automatically when the cache instance is garbage collected.
78
+ * Read-only cache statistics for debugging/observability entry count plus
79
+ * request-level hit/miss counters (counted on direct lookups, e.g. the cache
80
+ * interceptor's, not on every reactive signal read). Render it in a debug
81
+ * panel; it intentionally exposes no way to mutate the cache.
82
+ */
83
+ readonly stats: Signal<{
84
+ size: number;
85
+ hits: number;
86
+ misses: number;
87
+ }>;
88
+ /**
89
+ * Destroys the cache instance, clearing the cleanup interval and closing the
90
+ * cross-tab channel. Called automatically when the providing injector is destroyed
91
+ * (wired up by `provideQueryCache`); call it manually for caches you construct yourself.
71
92
  */
72
93
  readonly destroy: () => void;
73
94
  private readonly broadcast;
@@ -89,9 +110,11 @@ export declare class Cache<T> {
89
110
  }, db?: Promise<CacheDB<T>>);
90
111
  /** @internal */
91
112
  private getInternal;
113
+ /** @internal Imperative access bookkeeping for LRU eviction. */
114
+ private touch;
92
115
  /**
93
- * Retrieves a cache entry without affecting its usage count (for LRU). This is primarily
94
- * for internal use or debugging.
116
+ * Retrieves a cache entry directly (non-reactively), updating its access bookkeeping
117
+ * for LRU eviction.
95
118
  * @internal
96
119
  * @param key - The key of the entry to retrieve.
97
120
  * @returns The cache entry, or `null` if not found or expired.
@@ -122,13 +145,27 @@ export declare class Cache<T> {
122
145
  /**
123
146
  * Stores a value in the cache.
124
147
  *
148
+ * NOTE: cached values are shared by reference across all consumers (current and
149
+ * future cache hits, persistence, cross-tab sync) — do not mutate a value after
150
+ * storing it or after reading it from the cache.
151
+ *
125
152
  * @param key - The key under which to store the value.
126
153
  * @param value - The value to store.
127
154
  * @param staleTime - (Optional) The stale time for this entry, in milliseconds. Overrides the default `staleTime`.
128
155
  * @param ttl - (Optional) The TTL for this entry, in milliseconds. Overrides the default `ttl`.
156
+ * @param persist - (Optional) Whether to also write the entry to the persistence layer (IndexedDB). Defaults to `false`.
129
157
  */
130
158
  store(key: string, value: T, staleTime?: number, ttl?: number, persist?: boolean): void;
131
159
  private storeInternal;
160
+ /**
161
+ * @internal
162
+ * Inserts an entry that already carries ABSOLUTE timestamps — hydration from the
163
+ * persistence layer and cross-tab sync messages. Never re-anchors freshness to
164
+ * `Date.now()`, never persists, never broadcasts.
165
+ */
166
+ private restoreInternal;
167
+ /** @internal Shared writer: arms the expiry timer only within the safe delay range. */
168
+ private setEntry;
132
169
  /**
133
170
  * Invalidates (removes) a cache entry.
134
171
  *
@@ -154,7 +191,12 @@ export declare class Cache<T> {
154
191
  */
155
192
  invalidateWhere(predicate: (key: string) => boolean): number;
156
193
  private invalidateInternal;
157
- /** @internal */
194
+ /**
195
+ * Removes EVERY entry — memory, persisted rows, and (via broadcast) other tabs.
196
+ * Call on logout/auth changes so no prior user's responses survive.
197
+ */
198
+ clear(): void;
199
+ /** @internal Drops expired entries, then enforces `maxSize` by the configured strategy. */
158
200
  private cleanup;
159
201
  }
160
202
  /**
@@ -238,4 +280,16 @@ export declare function provideQueryCache(opt?: CacheOptions): Provider;
238
280
  * }
239
281
  */
240
282
  export declare function injectQueryCache<TRaw = unknown>(injector?: Injector): Cache<HttpResponse<TRaw>>;
283
+ /**
284
+ * Injects the cache statistics, including the current size of the cache and the number of hits and misses.
285
+ *
286
+ * @param injector - (Optional) The injector to use. If not provided, the current
287
+ * injection context is used.
288
+ * @returns A signal containing the cache statistics.
289
+ */
290
+ export declare function injectCacheStats(injector?: Injector): Signal<{
291
+ size: number;
292
+ hits: number;
293
+ misses: number;
294
+ }>;
241
295
  export {};
@@ -1,2 +1,2 @@
1
- export { Cache, injectQueryCache, provideQueryCache } from './cache';
1
+ export { Cache, injectCacheStats, injectQueryCache, provideQueryCache, } from './cache';
2
2
  export * from './cache-interceptor';
@@ -1,2 +1,2 @@
1
- export { Cache, injectQueryCache, provideQueryCache } from './cache';
1
+ export { Cache, injectQueryCache, provideQueryCache, type CacheEntry, type CleanupType, } from './cache';
2
2
  export { createCacheInterceptor } from './cache-interceptor';
@@ -22,6 +22,12 @@ export declare function noDedupe(ctx?: HttpContext): HttpContext;
22
22
  * only the first request will be sent to the server. Subsequent requests will
23
23
  * receive the response from the first request.
24
24
  *
25
+ * Relationship to `createCacheInterceptor`: the cache interceptor has built-in
26
+ * single-flight for CACHE-ENABLED requests (keyed by the cache key). This interceptor
27
+ * covers everything the cache doesn't see — non-cached resources, plain HttpClient
28
+ * calls, DELETEs — keyed by the request hash. Installing both is the recommended
29
+ * setup; where they overlap, this one degrades to a no-op passthrough.
30
+ *
25
31
  * @param allowed - An array of HTTP methods for which deduplication should be enabled.
26
32
  * Defaults to `['GET', 'DELETE', 'HEAD', 'OPTIONS']`.
27
33
  * @param keyFn - Optional function to compute the dedupe key from a request.
@@ -5,17 +5,24 @@ type HashableRequest = {
5
5
  responseType?: string;
6
6
  params?: HttpResourceRequest['params'] | HttpRequest<unknown>['params'];
7
7
  body?: unknown;
8
+ headers?: HttpResourceRequest['headers'] | HttpRequest<unknown>['headers'];
8
9
  };
9
10
  /**
10
11
  * Builds a stable cache/dedupe key from an HTTP request shape (accepts both
11
12
  * `HttpRequest` and `HttpResourceRequest`).
12
13
  *
13
- * Key composition: `${method}:${url}:${responseType}[:${params}][:${body}]`
14
+ * Key composition: `${method}:${url}:${responseType}[:${params}][:${body}][:${vary}]`
14
15
  * - `method` defaults to `'GET'`, `responseType` to `'json'` (Angular defaults).
15
16
  * - Query params are sorted alphabetically and URL-encoded for stability.
16
17
  * - Body hashing handles `File`/`Blob`/`FormData`/`URLSearchParams`/`ArrayBuffer`
17
18
  * and typed arrays explicitly; everything else flows through key-sorted
18
19
  * `JSON.stringify` via `hash()`.
20
+ * - `varyHeaders` (opt-in) mixes the named request headers into the key so responses
21
+ * that differ per header (e.g. `Authorization` → per-user, `Accept-Language`) get
22
+ * separate entries. Known-safe content-negotiation headers (`Accept`,
23
+ * `Accept-Language`, `Content-Language`, `Content-Type`) embed their value raw for
24
+ * readable keys; all other header VALUES are one-way digested, never embedded raw —
25
+ * keys are persisted to IndexedDB and broadcast across tabs.
19
26
  */
20
- export declare function hashRequest(req: HashableRequest): string;
27
+ export declare function hashRequest(req: HashableRequest, varyHeaders?: readonly string[]): string;
21
28
  export {};
@@ -5,6 +5,7 @@ export * from './dedupe-interceptor';
5
5
  export * from './equality';
6
6
  export * from './has-slow-connection';
7
7
  export * from './hash-request';
8
+ export * from './merge-options';
8
9
  export * from './persist';
9
10
  export * from './refresh';
10
11
  export * from './retry-on-error';
@@ -0,0 +1,24 @@
1
+ import type { CircuitBreakerOptions } from './circuit-breaker';
2
+ import type { RefreshOptions } from './refresh';
3
+ import type { RetryOptions } from './retry-on-error';
4
+ import type { ResourceCacheOptions } from '../options';
5
+ /**
6
+ * Deep merges multiple circuit breaker options.
7
+ * The latter options override the former.
8
+ */
9
+ export declare function mergeCircuitBreakerOptions(global?: CircuitBreakerOptions | true, query?: CircuitBreakerOptions | true, local?: CircuitBreakerOptions | true): CircuitBreakerOptions | true | undefined;
10
+ /**
11
+ * Deep merges multiple retry options.
12
+ * The latter options override the former.
13
+ */
14
+ export declare function mergeRetryOptions(global?: RetryOptions | number, query?: RetryOptions | number, local?: RetryOptions | number): RetryOptions | number | undefined;
15
+ /**
16
+ * Deep merges multiple cache options.
17
+ * The latter options override the former.
18
+ */
19
+ export declare function mergeCacheOptions(query?: ResourceCacheOptions, local?: ResourceCacheOptions): ResourceCacheOptions | undefined;
20
+ /**
21
+ * Deep merges multiple refresh options.
22
+ * The latter options override the former.
23
+ */
24
+ export declare function mergeRefreshOptions(query?: RefreshOptions | number, local?: RefreshOptions | number): RefreshOptions | number | undefined;
@@ -1,3 +1,4 @@
1
1
  export * from './cache/public_api';
2
2
  export { createCircuitBreaker, provideCircuitBreakerDefaultOptions, } from './circuit-breaker';
3
3
  export { createDedupeRequestsInterceptor, noDedupe, } from './dedupe-interceptor';
4
+ export { hashRequest } from './hash-request';
@@ -1,3 +1,31 @@
1
- import { HttpResourceRef } from '@angular/common/http';
2
- import { DestroyRef } from '@angular/core';
3
- export declare function refresh<T>(resource: HttpResourceRef<T>, destroyRef: DestroyRef, refresh?: number, inactive?: () => boolean): HttpResourceRef<T>;
1
+ import { type HttpResourceRef } from '@angular/common/http';
2
+ import { type DestroyRef, type Injector, type Signal } from '@angular/core';
3
+ /**
4
+ * Refresh configuration for a query resource.
5
+ * - a `number` is shorthand for `{ interval: number }` (poll every n milliseconds)
6
+ * - the object form composes polling with event-driven refresh triggers
7
+ */
8
+ export type RefreshOptions = number | {
9
+ /**
10
+ * Poll interval in milliseconds. Omit (or 0) for no polling — useful when only
11
+ * the event-driven triggers below are wanted.
12
+ */
13
+ interval?: number;
14
+ /**
15
+ * Reload when the page becomes visible again (tab refocused, window restored).
16
+ * @default false
17
+ */
18
+ onFocus?: boolean;
19
+ /**
20
+ * Reload when the browser comes back online.
21
+ * @default false
22
+ */
23
+ onReconnect?: boolean;
24
+ };
25
+ /** @internal Reactive sources + injector for the event-driven refresh triggers. */
26
+ export type RefreshTriggers = {
27
+ injector: Injector;
28
+ visibility: Signal<DocumentVisibilityState>;
29
+ online: Signal<boolean>;
30
+ };
31
+ export declare function refresh<T>(resource: HttpResourceRef<T>, destroyRef: DestroyRef, opt?: RefreshOptions, inactive?: () => boolean, triggers?: RefreshTriggers): HttpResourceRef<T>;
@@ -1,7 +1,9 @@
1
1
  import * as i0 from "@angular/core";
2
2
  export declare class ResourceSensors {
3
3
  readonly networkStatus: import("@mmstack/primitives").NetworkStatusSignal;
4
+ readonly pageVisibility: import("@angular/core").Signal<DocumentVisibilityState>;
4
5
  static ɵfac: i0.ɵɵFactoryDeclaration<ResourceSensors, never>;
5
6
  static ɵprov: i0.ɵɵInjectableDeclaration<ResourceSensors>;
6
7
  }
7
8
  export declare function injectNetworkStatus(): import("@mmstack/primitives").NetworkStatusSignal;
9
+ export declare function injectPageVisibility(): import("@angular/core").Signal<DocumentVisibilityState>;
@@ -0,0 +1,12 @@
1
+ import { type Observable } from 'rxjs';
2
+ /**
3
+ * @internal
4
+ * Single-flight sharing: if a pending observable is already registered under `key`,
5
+ * return it; otherwise create one, share it (replaying the latest event to late
6
+ * subscribers), and deregister it on teardown/settle.
7
+ *
8
+ * Used by both the dedupe interceptor (keyed by full request hash, app-wide) and the
9
+ * cache interceptor (keyed by the CACHE key, guarding the miss/stale-revalidation path)
10
+ * — same mechanism, different keying/scope, so it lives here exactly once.
11
+ */
12
+ export declare function sharePending<T>(pending: Map<string, Observable<T>>, key: string, create: () => Observable<T>): Observable<T>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmstack/resource",
3
- "version": "19.6.1",
3
+ "version": "19.6.3",
4
4
  "keywords": [
5
5
  "angular",
6
6
  "signals",