@mmstack/resource 19.5.0 → 19.6.0

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,4 +1,5 @@
1
1
  export * from './lib/manual-query';
2
2
  export * from './lib/mutation-resource';
3
+ export * from './lib/options';
3
4
  export * from './lib/query-resource';
4
5
  export * from './lib/util/public_api';
@@ -2,8 +2,29 @@ import { HttpResourceRequest } from '@angular/common/http';
2
2
  import { Injector } from '@angular/core';
3
3
  import { QueryResourceOptions, QueryResourceRef } from './query-resource';
4
4
  /**
5
- * A reference to a manually triggered query resource. This type extends the standard `QueryResourceRef`
6
- * with an additional `trigger` method that allows you to manually trigger the resource request.
5
+ * A reference to a manually triggered query resource. Extends
6
+ * {@link QueryResourceRef} with a `trigger()` method that runs the request
7
+ * imperatively and returns a `Promise<TResult>`. Useful when a request should
8
+ * only happen on user action (search submit, button click) rather than on
9
+ * every reactive change of the request inputs.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const search = manualQueryResource<SearchResults>(() => ({
14
+ * url: '/api/search',
15
+ * params: { q: query() },
16
+ * }));
17
+ *
18
+ * async function onSubmit() {
19
+ * try {
20
+ * const results = await search.trigger();
21
+ * showResults(results);
22
+ * } catch (err) {
23
+ * toast.error('Search failed');
24
+ * }
25
+ * }
26
+ * ```
27
+ *
7
28
  * @see QueryResourceRef
8
29
  */
9
30
  export type ManualQueryResourceRef<TResult> = QueryResourceRef<TResult> & {
@@ -19,6 +40,17 @@ export type ManualQueryResourceRef<TResult> = QueryResourceRef<TResult> & {
19
40
  * `HttpResourceOptions` and add features like `keepPrevious`, `refresh`, `retry`,
20
41
  * `onError`, `circuitBreaker`, and `cache`. Additionally, when a `defaultValue` is provided, the resource's value will always be defined, even if the underlying HTTP request fails or is disabled.
21
42
  * @returns An `ManualQueryResourceRef` instance, which extends the basic `QueryResourceRef` with additional features.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * const search = manualQueryResource<SearchResults>(
47
+ * () => ({ url: '/api/search', params: { q: query() } }),
48
+ * { defaultValue: { hits: [], total: 0 } },
49
+ * );
50
+ *
51
+ * // search.value() is always SearchResults (never undefined) thanks to defaultValue.
52
+ * const results = await search.trigger();
53
+ * ```
22
54
  */
23
55
  export declare function manualQueryResource<TResult, TRaw = TResult>(request: () => HttpResourceRequest | string | undefined | void, options: QueryResourceOptions<TResult, TRaw> & {
24
56
  defaultValue: NoInfer<TResult>;
@@ -34,5 +66,22 @@ export declare function manualQueryResource<TResult, TRaw = TResult>(request: ()
34
66
  * `HttpResourceOptions` and add features like `keepPrevious`, `refresh`, `retry`,
35
67
  * `onError`, `circuitBreaker`, and `cache`.
36
68
  * @returns An `ManualQueryResourceRef` instance, which extends the basic `QueryResourceRef` with additional features.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * const exportReport = manualQueryResource<Report>(() => ({
73
+ * url: '/api/reports/export',
74
+ * params: { range: range() },
75
+ * }));
76
+ *
77
+ * async function onExportClick() {
78
+ * try {
79
+ * const report = await exportReport.trigger();
80
+ * download(report);
81
+ * } catch (err) {
82
+ * toast.error('Export failed');
83
+ * }
84
+ * }
85
+ * ```
37
86
  */
38
87
  export declare function manualQueryResource<TResult, TRaw = TResult>(request: () => HttpResourceRequest | string | undefined | void, options?: QueryResourceOptions<TResult, TRaw>): ManualQueryResourceRef<TResult | undefined>;
@@ -1,5 +1,5 @@
1
1
  import { type HttpResourceRequest } from '@angular/common/http';
2
- import { type Signal, type ValueEqualityFn } from '@angular/core';
2
+ import { type Provider, type Signal, type ValueEqualityFn } from '@angular/core';
3
3
  import { type QueryResourceOptions, type QueryResourceRef } from './query-resource';
4
4
  /**
5
5
  * @internal
@@ -12,11 +12,29 @@ type NextRequest<TMethod extends HttpResourceRequest['method'], TMutation> = TMe
12
12
  method: TMethod;
13
13
  };
14
14
  /**
15
- * Options for configuring a `mutationResource`.
15
+ * Options for configuring a `mutationResource`. Inherits from
16
+ * `QueryResourceOptions` (minus options that don't apply to mutations:
17
+ * `equal`, `keepPrevious`, `refresh`, `cache`) and adds lifecycle callbacks
18
+ * (`onMutate`, `onError`, `onSuccess`, `onSettled`) for managing optimistic
19
+ * updates, rollback, and side effects.
16
20
  *
17
21
  * @typeParam TResult - The type of the expected result from the mutation.
18
22
  * @typeParam TRaw - The raw response type from the HTTP request (defaults to TResult).
19
23
  * @typeParam TCTX - The type of the context value returned by `onMutate`.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const options: MutationResourceOptions<User, User, Partial<User>, { previous: User | null }> = {
28
+ * onMutate: (patch) => {
29
+ * const previous = current();
30
+ * current.update((u) => (u ? { ...u, ...patch } : u)); // optimistic
31
+ * return { previous };
32
+ * },
33
+ * onError: (_err, { previous }) => current.set(previous), // rollback
34
+ * onSuccess: (saved) => toast.success(`Updated ${saved.name}`),
35
+ * queue: true, // serialize requests when offline / circuit open
36
+ * };
37
+ * ```
20
38
  */
21
39
  export type MutationResourceOptions<TResult, TRaw = TResult, TMutation = TResult, TCTX = void, TICTX = TCTX, TError = unknown> = Omit<QueryResourceOptions<TResult, TRaw>, 'equal' | 'onError' | 'keepPrevious' | 'refresh' | 'cache'> & {
22
40
  /**
@@ -51,10 +69,29 @@ export type MutationResourceOptions<TResult, TRaw = TResult, TMutation = TResult
51
69
  equal?: ValueEqualityFn<TMutation>;
52
70
  };
53
71
  /**
54
- * Represents a mutation resource created by `mutationResource`. Extends `QueryResourceRef`
55
- * but removes methods that don't make sense for mutations (like `prefetch`, `value`, etc.).
72
+ * Layer 2 (mutation): default options for every `mutationResource`, inheriting + overriding the
73
+ * common defaults from `provideResourceOptions`. Per-call options override these in turn.
74
+ */
75
+ export declare function provideMutationResourceOptions(valueOrFn: Partial<MutationResourceOptions<any, any, any, any, any, any>> | (() => Partial<MutationResourceOptions<any, any, any, any, any, any>>)): Provider;
76
+ /**
77
+ * Represents a mutation resource created by `mutationResource`. Extends
78
+ * `QueryResourceRef` but strips methods that don't make sense for one-off
79
+ * writes (`prefetch`, `value`, `hasValue`, `set`, `update`) and adds `mutate()`
80
+ * for triggering a mutation plus `current()` for tracking the in-flight value.
56
81
  *
57
82
  * @typeParam TResult - The type of the expected result from the mutation.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * const updateUser = mutationResource<User, User, Partial<User>>(...);
87
+ *
88
+ * effect(() => console.log('mutating:', updateUser.current()));
89
+ * effect(() => {
90
+ * if (updateUser.status() === 'error') toast.error(updateUser.error());
91
+ * });
92
+ *
93
+ * updateUser.mutate({ name: 'Alice' });
94
+ * ```
58
95
  */
59
96
  export type MutationResourceRef<TResult, TMutation = TResult, TICTX = void> = Omit<QueryResourceRef<TResult>, 'prefetch' | 'value' | 'hasValue' | 'set' | 'update'> & {
60
97
  /**
@@ -88,6 +125,36 @@ export type MutationResourceRef<TResult, TMutation = TResult, TICTX = void> = Om
88
125
  * @typeParam TMethod - The HTTP method to be used for the mutation (defaults to `HttpResourceRequest['method']`).
89
126
  * @returns A `MutationResourceRef` instance, which provides methods for triggering the mutation
90
127
  * and observing its status.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * // Basic PATCH mutation
132
+ * const updateUser = mutationResource<User, User, Partial<User>>(
133
+ * (body) => ({ url: `/api/users/${userId()}`, method: 'PATCH', body }),
134
+ * {
135
+ * onSuccess: (saved) => toast.success(`Updated ${saved.name}`),
136
+ * onError: (err) => toast.error(err),
137
+ * },
138
+ * );
139
+ *
140
+ * updateUser.mutate({ name: 'Alice' });
141
+ * ```
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * // Optimistic update with rollback via the `ctx` returned from `onMutate`
146
+ * const updateUser = mutationResource<User, User, Partial<User>, { prev: User | null }>(
147
+ * (body) => ({ url: `/api/users/${userId()}`, method: 'PATCH', body }),
148
+ * {
149
+ * onMutate: (patch) => {
150
+ * const prev = current();
151
+ * current.update((u) => (u ? { ...u, ...patch } : u));
152
+ * return { prev };
153
+ * },
154
+ * onError: (_err, { prev }) => current.set(prev),
155
+ * },
156
+ * );
157
+ * ```
91
158
  */
92
- export declare function mutationResource<TResult, TRaw = TResult, TMutation = TResult, TCTX = void, TICTX = TCTX, TMethod extends HttpResourceRequest['method'] = HttpResourceRequest['method']>(request: (params: TMutation) => Omit<NextRequest<TMethod, TMutation>, 'body'> | undefined | void, options?: MutationResourceOptions<TResult, TRaw, TMutation, TCTX, TICTX>): MutationResourceRef<TResult, TMutation, TICTX>;
159
+ export declare function mutationResource<TResult, TRaw = TResult, TMutation = TResult, TCTX = void, TICTX = TCTX, TMethod extends HttpResourceRequest['method'] = HttpResourceRequest['method']>(request: (params: TMutation) => Omit<NextRequest<TMethod, TMutation>, 'body'> | undefined | void, options0?: MutationResourceOptions<TResult, TRaw, TMutation, TCTX, TICTX>): MutationResourceRef<TResult, TMutation, TICTX>;
93
160
  export {};
@@ -0,0 +1,38 @@
1
+ import { InjectionToken, type Injector, type Provider, type ResourceRef } from '@angular/core';
2
+ import { type RegisterOptions } from '@mmstack/primitives';
3
+ import { type CircuitBreakerOptions, type RetryOptions } from './util';
4
+ /**
5
+ * Auto-registration into the nearest transition scope, as a resource OPTION:
6
+ * - `true` — register for the pending indicator + hold-stale (does NOT block first paint);
7
+ * - `{ suspends: true }` — register as *suspending* (the boundary holds its placeholder until
8
+ * this resource has a value), i.e. full Suspense;
9
+ * - `{ suspends: false }` — same as `true`;
10
+ * - `false` / omitted — don't register.
11
+ *
12
+ * Defaultable via `provideResourceOptions` / `provideQueryResourceOptions` and overridable
13
+ * (including opting out with `false`) per call — so a dev can make "all queries participate in
14
+ * transitions" the default and turn it off for the odd one.
15
+ */
16
+ export type TransitionRegistration = boolean | RegisterOptions;
17
+ /** Options common to every resource kind (the base layer for the options-injection system). */
18
+ export type CommonResourceOptions = {
19
+ /** Auto-registration into the nearest transition scope. */
20
+ readonly register?: TransitionRegistration;
21
+ /** Retry failed requests. */
22
+ readonly retry?: RetryOptions;
23
+ /** Configure a circuit breaker for the resource. */
24
+ readonly circuitBreaker?: CircuitBreakerOptions | true;
25
+ /** Trigger a request even when the request parameters are unchanged. @default false */
26
+ readonly triggerOnSameRequest?: boolean;
27
+ };
28
+ /** Layer 1: defaults that apply to ALL resource kinds. Type-specific providers inherit + override these. */
29
+ export declare function provideResourceOptions(valueOrFn: CommonResourceOptions | (() => CommonResourceOptions)): Provider;
30
+ export declare function injectResourceOptions(injector?: Injector): CommonResourceOptions;
31
+ /** Shared helper for the type-specific providers (query/mutation), so precedence is identical. */
32
+ export declare function provideTypedResourceOptions<T>(token: InjectionToken<T>, valueOrFn: T | (() => T)): Provider;
33
+ /**
34
+ * Applies a resolved `register` option to a freshly-created resource — adds it to the nearest
35
+ * transition scope and removes it on destroy. Runs in the resource's injection context (or the
36
+ * provided `injector`), since registration needs `TRANSITION_SCOPE` + `DestroyRef`.
37
+ */
38
+ export declare function applyResourceRegistration(ref: ResourceRef<unknown>, register: TransitionRegistration | undefined, injector?: Injector): void;
@@ -1,6 +1,6 @@
1
- import { HttpHeaders, type HttpResourceOptions, type HttpResourceRef, type HttpResourceRequest } from '@angular/common/http';
2
- import { Signal, WritableSignal } from '@angular/core';
3
- import { CircuitBreakerOptions, type RetryOptions } from './util';
1
+ import { type HttpHeaders, type HttpResourceOptions, type HttpResourceRef, type HttpResourceRequest } from '@angular/common/http';
2
+ import { type Provider, type Signal, type WritableSignal } from '@angular/core';
3
+ import { type CommonResourceOptions } from './options';
4
4
  /**
5
5
  * Options for configuring caching behavior of a `queryResource`.
6
6
  * - `true`: Enables caching with default settings.
@@ -44,9 +44,24 @@ type ResourceCacheOptions = true | {
44
44
  persist?: boolean;
45
45
  };
46
46
  /**
47
- * Options for configuring a `queryResource`.
47
+ * Options for configuring a `queryResource`. Extends Angular's
48
+ * `HttpResourceOptions` with caching, retries, refresh intervals, circuit
49
+ * breakers, and lifecycle callbacks. See the linked properties below for the
50
+ * full list.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * const options: QueryResourceOptions<User> = {
55
+ * defaultValue: { id: 0, name: 'Anonymous' },
56
+ * cache: { ttl: 60_000, staleTime: 10_000 },
57
+ * refresh: 30_000,
58
+ * retry: { max: 3 },
59
+ * circuitBreaker: true,
60
+ * onError: (err, retry, isFinal) => isFinal && toast.error(err),
61
+ * };
62
+ * ```
48
63
  */
49
- export type QueryResourceOptions<TResult, TRaw = TResult> = HttpResourceOptions<TResult, TRaw> & {
64
+ export type QueryResourceOptions<TResult, TRaw = TResult> = HttpResourceOptions<TResult, TRaw> & CommonResourceOptions & {
50
65
  /**
51
66
  * Whether to keep the previous value of the resource while a refresh is in progress.
52
67
  * Defaults to `false`. Also keeps status & headers while refreshing.
@@ -57,10 +72,6 @@ export type QueryResourceOptions<TResult, TRaw = TResult> = HttpResourceOptions<
57
72
  * refresh its data at the specified interval.
58
73
  */
59
74
  refresh?: number;
60
- /**
61
- * Options for retrying failed requests.
62
- */
63
- retry?: RetryOptions;
64
75
  /**
65
76
  * Called on every failed attempt, including each retry.
66
77
  *
@@ -72,28 +83,73 @@ export type QueryResourceOptions<TResult, TRaw = TResult> = HttpResourceOptions<
72
83
  * "user actually needs to know" side effects (toasts, error reporting).
73
84
  */
74
85
  onError?: (err: unknown, retryCount: number, isFinal: boolean) => void;
75
- /**
76
- * Options for configuring a circuit breaker for the resource.
77
- */
78
- circuitBreaker?: CircuitBreakerOptions | true;
79
86
  /**
80
87
  * Options for enabling and configuring caching for the resource.
81
88
  */
82
89
  cache?: ResourceCacheOptions;
83
90
  /**
84
- * Trigger a request every time the request function is triggered, even if the request parameters are the same.
85
- * @default false
91
+ * Comparison of request object
86
92
  */
87
- triggerOnSameRequest?: boolean;
93
+ equalRequest?: (a: HttpResourceRequest, b: HttpResourceRequest) => boolean;
88
94
  };
95
+ /**
96
+ * Layer 2 (query): default options for every `queryResource`, inheriting + overriding the
97
+ * common defaults from `provideResourceOptions`. Per-call options override these in turn.
98
+ */
99
+ export declare function provideQueryResourceOptions(valueOrFn: Partial<QueryResourceOptions<any, any>> | (() => Partial<QueryResourceOptions<any, any>>)): Provider;
89
100
  /**
90
101
  * The reason a query resource is currently in the `disabled` state, or `null`
91
102
  * if it is enabled. Useful for branching UI on cause (e.g. "offline" vs
92
103
  * "circuit tripped" vs "nothing to fetch yet").
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * effect(() => {
108
+ * switch (user.disabledReason()) {
109
+ * case 'offline': return toast.warn('You are offline');
110
+ * case 'circuit-open': return toast.warn('Service temporarily unavailable');
111
+ * case 'no-request': return; // expected — request signal returned undefined
112
+ * case null: return; // resource is enabled
113
+ * }
114
+ * });
115
+ * ```
93
116
  */
94
117
  export type DisabledReason = 'offline' | 'circuit-open' | 'no-request';
95
118
  /**
96
- * Represents a resource created by `queryResource`. Extends `HttpResourceRef` with additional properties.
119
+ * Returned from a resource's request fn to PAUSE it: the resource holds its current value and last
120
+ * request (so it does not refetch on resume), and stops background work (no polling, no refetch
121
+ * while paused). Distinct from returning `undefined` (DISABLE), which drops the request — a
122
+ * disabled resource may refetch when re-enabled, a paused one resumes exactly where it left off.
123
+ *
124
+ * The request fn receives a {@link RequestContext} and can just return `ctx.paused`.
125
+ */
126
+ export declare const PAUSED: unique symbol;
127
+ /**
128
+ * Context passed to a resource's request fn. An object (not positional args) so it can grow
129
+ * without changing the call signature. Today it carries {@link PAUSED} so the fn can return it.
130
+ */
131
+ export type RequestContext = {
132
+ readonly paused: typeof PAUSED;
133
+ };
134
+ /** The request fn shape: build a request, or return `undefined` (disable) / `ctx.paused` (pause). */
135
+ export type ResourceRequestFn = (ctx: RequestContext) => HttpResourceRequest | string | undefined | void | typeof PAUSED;
136
+ /**
137
+ * Represents a resource created by `queryResource`. Extends `HttpResourceRef`
138
+ * with `disabled` / `disabledReason` signals, writable `headers` / `statusCode`
139
+ * (so optimistic updates can patch them), and `prefetch()` for proactive cache
140
+ * warm-up.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * const user = queryResource<User>(() => `/api/users/${userId()}`);
145
+ *
146
+ * effect(() => {
147
+ * if (user.status() === 'resolved') console.log(user.value());
148
+ * });
149
+ *
150
+ * // Warm the cache before navigating
151
+ * onMouseEnter(() => user.prefetch());
152
+ * ```
97
153
  */
98
154
  export type QueryResourceRef<TResult> = Omit<HttpResourceRef<TResult>, 'headers' | 'statusCode'> & {
99
155
  /**
@@ -132,8 +188,20 @@ export type QueryResourceRef<TResult> = Omit<HttpResourceRef<TResult>, 'headers'
132
188
  * `HttpResourceOptions` and add features like `keepPrevious`, `refresh`, `retry`,
133
189
  * `onError`, `circuitBreaker`, and `cache`. Additionally, when a `defaultValue` is provided, the resource's value will always be defined, even if the underlying HTTP request fails or is disabled.
134
190
  * @returns An `QueryResourceRef` instance, which extends the basic `HttpResourceRef` with additional features.
191
+ *
192
+ * @example
193
+ * ```ts
194
+ * const userId = signal(1);
195
+ *
196
+ * const user = queryResource<User>(
197
+ * () => `/api/users/${userId()}`,
198
+ * { defaultValue: { id: 0, name: 'Anonymous' } },
199
+ * );
200
+ *
201
+ * user.value(); // always User — never undefined, even before the first fetch resolves
202
+ * ```
135
203
  */
136
- export declare function queryResource<TResult, TRaw = TResult>(request: () => HttpResourceRequest | string | undefined | void, options: QueryResourceOptions<TResult, TRaw> & {
204
+ export declare function queryResource<TResult, TRaw = TResult>(request: ResourceRequestFn, options: QueryResourceOptions<TResult, TRaw> & {
137
205
  defaultValue: NoInfer<TResult>;
138
206
  }): QueryResourceRef<TResult>;
139
207
  /**
@@ -147,6 +215,24 @@ export declare function queryResource<TResult, TRaw = TResult>(request: () => Ht
147
215
  * `HttpResourceOptions` and add features like `keepPrevious`, `refresh`, `retry`,
148
216
  * `onError`, `circuitBreaker`, and `cache`.
149
217
  * @returns An `QueryResourceRef` instance, which extends the basic `HttpResourceRef` with additional features.
218
+ *
219
+ * @example
220
+ * ```ts
221
+ * const userId = signal<number | undefined>(undefined);
222
+ *
223
+ * const user = queryResource<User>(
224
+ * () => userId() ? `/api/users/${userId()}` : undefined,
225
+ * {
226
+ * cache: { ttl: 60_000, staleTime: 10_000 },
227
+ * refresh: 30_000,
228
+ * retry: { max: 3 },
229
+ * },
230
+ * );
231
+ *
232
+ * user.value(); // User | undefined
233
+ * user.status(); // 'idle' | 'loading' | 'resolved' | 'error'
234
+ * user.disabledReason(); // null while enabled; 'offline' / 'circuit-open' / 'no-request' otherwise
235
+ * ```
150
236
  */
151
- export declare function queryResource<TResult, TRaw = TResult>(request: () => HttpResourceRequest | string | undefined | void, options?: QueryResourceOptions<TResult, TRaw>): QueryResourceRef<TResult | undefined>;
237
+ export declare function queryResource<TResult, TRaw = TResult>(request: ResourceRequestFn, options?: QueryResourceOptions<TResult, TRaw>): QueryResourceRef<TResult | undefined>;
152
238
  export {};
@@ -1,3 +1,3 @@
1
1
  import { HttpResourceRef } from '@angular/common/http';
2
2
  import { DestroyRef } from '@angular/core';
3
- export declare function refresh<T>(resource: HttpResourceRef<T>, destroyRef: DestroyRef, refresh?: number): HttpResourceRef<T>;
3
+ export declare function refresh<T>(resource: HttpResourceRef<T>, destroyRef: DestroyRef, refresh?: number, inactive?: () => boolean): HttpResourceRef<T>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmstack/resource",
3
- "version": "19.5.0",
3
+ "version": "19.6.0",
4
4
  "keywords": [
5
5
  "angular",
6
6
  "signals",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "homepage": "https://github.com/mihajm/mmstack/blob/master/packages/resource",
19
19
  "dependencies": {
20
- "@mmstack/primitives": "^19.3.0",
20
+ "@mmstack/primitives": "^19.4.0",
21
21
  "tslib": "^2.3.0"
22
22
  },
23
23
  "peerDependencies": {