@mmstack/resource 22.1.5 → 22.2.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/README.md +56 -11
- package/fesm2022/mmstack-resource.mjs +1097 -1013
- package/fesm2022/mmstack-resource.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mmstack-resource.d.ts +88 -17
package/README.md
CHANGED
|
@@ -279,6 +279,22 @@ mutationResource(
|
|
|
279
279
|
|
|
280
280
|
The `TCTX` returned from `onMutate` flows into `onError` / `onSuccess` / `onSettled`. The optional `initialCtx` second arg to `.mutate(value, initialCtx)` flows into `onMutate` as its second argument.
|
|
281
281
|
|
|
282
|
+
### Awaiting a mutation (`mutateAsync`)
|
|
283
|
+
|
|
284
|
+
`.mutate()` is fire-and-forget. When you need to `await` the outcome — a form submit handler, an async validator — use `.mutateAsync()`, which returns a `Promise` that resolves with the result or rejects with the error. The lifecycle hooks still run exactly as with `.mutate()`.
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
try {
|
|
288
|
+
const saved = await createPost.mutateAsync(post);
|
|
289
|
+
router.navigate(['/posts', saved.id]);
|
|
290
|
+
} catch (e) {
|
|
291
|
+
if (e instanceof MutationCancelledError) return; // never ran — see below
|
|
292
|
+
toast.error('Failed to save');
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
If the mutation never completes — superseded by a newer one (latest-wins), dropped from the queue (`clearQueue()` / a `key` change), abandoned on `destroy()`, or its `request()` returned `undefined` — the promise rejects with a `MutationCancelledError` whose `.type` (`MutationCancellationReason`) tells you which. Awaiters that don't expect cancellation should rethrow it. (Plain `.mutate()` has no promise, so it never produces an unhandled rejection.)
|
|
297
|
+
|
|
282
298
|
### Queuing
|
|
283
299
|
|
|
284
300
|
By default, calling `.mutate()` while another mutation is in flight starts immediately — concurrent mutations run in parallel. With `queue: true`, mutations are serialized:
|
|
@@ -295,7 +311,7 @@ After a successful mutation, related query caches usually need refreshing. Inste
|
|
|
295
311
|
|
|
296
312
|
```typescript
|
|
297
313
|
mutationResource((p: Post) => ({ url: '/api/posts', method: 'POST', body: p }), {
|
|
298
|
-
invalidates: ['/api/posts'], // every cached
|
|
314
|
+
invalidates: ['/api/posts'], // every cached entry under /api/posts (any method, params, subpaths, varyHeaders variants)
|
|
299
315
|
});
|
|
300
316
|
|
|
301
317
|
// or derived from the result:
|
|
@@ -304,7 +320,7 @@ mutationResource(request, {
|
|
|
304
320
|
});
|
|
305
321
|
```
|
|
306
322
|
|
|
307
|
-
Strings are URL prefixes matched against
|
|
323
|
+
Strings are URL prefixes matched against the request URL of every cached entry, regardless of HTTP method (so a POST-bodied search cached under the same URL is cleared too). Plain prefix matching also catches sibling paths sharing the prefix (`/api/posts-archive`) — pass `'/api/posts/'` or an exact URL to narrow. Keys a custom `hash` merely *prepends* a namespace to (e.g. a tenant/`sub`) are still matched; keys that abandon the auto shape entirely need an `invalidateMatcher: (urlPrefix) => (key) => boolean` (set per-mutation or globally via `provideMutationResourceOptions`), or manual `injectQueryCache().invalidateWhere`.
|
|
308
324
|
|
|
309
325
|
### Re-firing with an identical body (`triggerOnSameRequest`)
|
|
310
326
|
|
|
@@ -316,13 +332,42 @@ mutationResource(request, { triggerOnSameRequest: true });
|
|
|
316
332
|
|
|
317
333
|
A mutation also honours the `register` option — but it registers the **mutation ref itself** into the transition scope (its internal query is never registered), so a `<mm-suspense>`/transition reacts to the mutation's own `pending` state. See [transitions & Suspense](#transitions--suspense).
|
|
318
334
|
|
|
335
|
+
### File uploads & progress (`multipart/form-data`)
|
|
336
|
+
|
|
337
|
+
There's no special upload API — return a `FormData` body and `HttpClient` sets the `multipart/form-data` boundary for you. Opt into upload progress with `reportProgress: true` and read the `progress` signal (an `HttpProgressEvent`).
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
const upload = mutationResource<UploadResult, UploadResult, FormData>((form) => ({
|
|
341
|
+
url: '/api/upload',
|
|
342
|
+
method: 'POST',
|
|
343
|
+
body: form,
|
|
344
|
+
reportProgress: true, // opt in to progress events
|
|
345
|
+
}));
|
|
346
|
+
|
|
347
|
+
// trigger it:
|
|
348
|
+
const form = new FormData();
|
|
349
|
+
form.append('file', file);
|
|
350
|
+
upload.mutate(form);
|
|
351
|
+
|
|
352
|
+
// derive a percentage in a computed / template:
|
|
353
|
+
readonly pct = computed(() => {
|
|
354
|
+
const p = upload.progress();
|
|
355
|
+
return p?.total ? Math.round((p.loaded / p.total) * 100) : null;
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
`FormData`, `File`, and `Blob` bodies are hashed structurally for dedup/cache keys (a `File` by name + type + size + lastModified), so distinct files never collide. To re-upload the **same** file while one is already in flight, pair it with `triggerOnSameRequest: true`.
|
|
360
|
+
|
|
319
361
|
### Return shape (`MutationResourceRef<T, TMutation>`)
|
|
320
362
|
|
|
321
|
-
| Member | Type
|
|
322
|
-
| --------------------------------------------- |
|
|
323
|
-
| `mutate` | `(value, ctx?) => void`
|
|
324
|
-
| `
|
|
325
|
-
| `
|
|
363
|
+
| Member | Type | Notes |
|
|
364
|
+
| --------------------------------------------- | ------------------------------------------ | ------------------------------------------------------ |
|
|
365
|
+
| `mutate` | `(value, ctx?) => void` | Trigger the mutation (fire-and-forget). |
|
|
366
|
+
| `mutateAsync` | `(value, ctx?) => Promise<TResult>` | Trigger and `await` the result; rejects with the error or a `MutationCancelledError`. |
|
|
367
|
+
| `current` | `Signal<TMutation \| null>` | The value currently being mutated (or `null` if idle). |
|
|
368
|
+
| `progress` | `Signal<HttpProgressEvent \| undefined>` | Upload/download progress when `reportProgress: true`. |
|
|
369
|
+
| `status` / `error` / `isLoading` / `disabled` | as in `QueryResourceRef` | – |
|
|
370
|
+
| `headers` / `statusCode` | as in `QueryResourceRef` | Response metadata, when available. |
|
|
326
371
|
|
|
327
372
|
(Mutations deliberately don't expose `value`, `hasValue`, `set`, `update`, or `prefetch` — those don't make sense for one-off writes.)
|
|
328
373
|
|
|
@@ -431,14 +476,14 @@ With `syncTabs: true`, cache invalidations and updates broadcast via `BroadcastC
|
|
|
431
476
|
|
|
432
477
|
```typescript
|
|
433
478
|
const cache = injectQueryCache<MyResponse>();
|
|
434
|
-
cache.
|
|
435
|
-
cache.invalidatePrefix('GET:/api/posts'); // drop every key under a URL prefix
|
|
479
|
+
cache.invalidateUrlPrefix('/api/posts'); // drop every entry under a URL prefix, any method
|
|
436
480
|
cache.invalidateWhere((key) => key.includes('userId=42')); // arbitrary predicates
|
|
481
|
+
cache.invalidatePrefix('raw-key-prefix'); // match the raw key string from its start
|
|
437
482
|
cache.clear(); // drop EVERYTHING — memory, persisted rows, other tabs
|
|
438
483
|
cache.store(key, value, staleTime, ttl); // imperative write
|
|
439
484
|
```
|
|
440
485
|
|
|
441
|
-
Auto-generated keys have the shape `${method}
|
|
486
|
+
Auto-generated keys have the shape `${method}${SEP}${url}${SEP}${responseType}[${SEP}params][${SEP}body][${SEP}vary]`, where `SEP` is a content-rare control-character delimiter (treat keys as opaque — don't hand-build them). `invalidateUrlPrefix(urlPrefix)` is the common move: it recovers the URL field structurally, so it matches **any** HTTP method and even keys a custom `cache.hash` prepends a namespace to. For a fully-custom key scheme it takes an optional `match` (`(urlPrefix) => (key) => boolean`). Call `clear()` on logout so no prior user's responses survive. For observability there's a read-only `cache.stats()` signal (`{ size, hits, misses }`) — handy for a debug panel; it deliberately exposes no mutation surface.
|
|
442
487
|
|
|
443
488
|
Prefer the declarative [`invalidates`](#declarative-invalidation-invalidates) option on `mutationResource` for the common "mutation succeeded → refresh related queries" case.
|
|
444
489
|
|
|
@@ -624,7 +669,7 @@ Declarative — the common case:
|
|
|
624
669
|
|
|
625
670
|
```typescript
|
|
626
671
|
mutationResource((p: Post) => ({ url: '/posts', method: 'POST', body: p }), {
|
|
627
|
-
invalidates: ['/posts'], // every cached
|
|
672
|
+
invalidates: ['/posts'], // every cached entry under /posts, any method + params + subpaths + vary variants
|
|
628
673
|
});
|
|
629
674
|
```
|
|
630
675
|
|