march-hare 0.13.10 → 0.14.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.
@@ -2,7 +2,7 @@ import { ResourceHandle } from './types.js';
2
2
  import { Cache } from './utils.js';
3
3
  import { AppFetcher } from '../app/types.js';
4
4
  import { Env } from '../boundary/components/env/types.js';
5
- export type { Coalesce, Fetcher, Invocation, ResourceHandle } from './types.js';
5
+ export type { Fetcher, Invocation, ResourceHandle } from './types.js';
6
6
  /**
7
7
  * Evicts cache entries across every Resource constructed in the
8
8
  * current process. Resources register themselves on declaration, so
@@ -44,9 +44,13 @@ export declare function nuke(where?: object): void;
44
44
  * Standalone `shared.Resource` declarations always use an in-memory
45
45
  * cache — reach for `app.Resource` when persistence is required.
46
46
  *
47
- * Concurrent calls fire fresh requests by default. Opt in to in-flight
48
- * sharing per call via `.coalesce(key)` on the thenable returned from
49
- * `context.actions.resource(...)`.
47
+ * Concurrent calls with the same `(Resource, params)` share a single
48
+ * in-flight fetch by default — one network request, every caller
49
+ * resolves with the same payload. The underlying work is refcounted: if
50
+ * every caller aborts, the shared `AbortController` is aborted too.
51
+ * Chain `.isolated()` on the thenable returned from
52
+ * `context.actions.resource(...)` to opt out (own controller, own
53
+ * request) for the rare cases that need it.
50
54
  *
51
55
  * @template E The Env shape (or union) the fetcher's `context.env` is
52
56
  * typed against.
@@ -40,14 +40,6 @@ export type Args<P extends object = Record<never, never>> = {
40
40
  * and a broadcast/multicast-only `dispatch`.
41
41
  */
42
42
  export type Fetcher<T, P extends object = Record<never, never>> = (context: Args<P>) => Promise<T>;
43
- /**
44
- * Per-call coalescing token. Two callers with the same Resource, same
45
- * structural params, and equal `Coalesce` value share a single in-flight
46
- * promise; different tokens (or different params) fire independent
47
- * fetches. Primitives compose naturally via stringification; objects
48
- * are serialised with `JSON.stringify`.
49
- */
50
- export type Coalesce = string | number | bigint | boolean | symbol | object;
51
43
  /**
52
44
  * Config form accepted by `Resource`. The fetcher shorthand
53
45
  * `Resource(fetcher)` is equivalent to `Resource({ fetch: fetcher })`.
@@ -2,7 +2,7 @@ import { Operation, Process, Inspect as ImmInspect, Box } from 'immertation';
2
2
  import { ActionId, Task, Tasks } from '../boundary/components/tasks/types.js';
3
3
  import { Fault } from '../error/types.js';
4
4
  import { Env } from '../boundary/components/env/types.js';
5
- import { Coalesce, Invocation } from '../resource/types.js';
5
+ import { Invocation } from '../resource/types.js';
6
6
  import { WithHandle } from '../with/types.js';
7
7
  import * as React from "react";
8
8
  /**
@@ -26,25 +26,22 @@ type ValueAt<T, K extends PropertyKey> = T extends unknown ? K extends keyof T ?
26
26
  export type Inspect<T, D extends number = 8> = ImmInspect<T> & ([D] extends [0] ? object : {
27
27
  [K in UnionKeys<T> as ValueAt<T, K> extends (...args: unknown[]) => unknown ? never : K]: Inspect<ValueAt<T, K>, DepthLimiter[D]>;
28
28
  });
29
- /**
30
- * Chainable handle returned from `context.actions.resource(invocation)`.
31
- *
32
- * - `.exceeds(duration)` short-circuits the fetch when the per-params
33
- * cache age is within the supplied freshness window.
34
- * - `.coalesce(token)` opts the call into in-flight sharing: any other
35
- * caller with the same Resource, same structural params, and equal
36
- * `token` joins the same promise.
37
- *
38
- * Awaiting the handle (`await context.actions.resource(...)`) triggers
39
- * the fetch with whichever options have been set on the chain.
40
- */
41
29
  /**
42
30
  * Fetch-configured chain returned from `.exceeds(...)` and
43
- * `.coalesce(...)`. Awaiting the chain runs the fetch with whichever
31
+ * `.isolated()`. Awaiting the chain runs the fetch with whichever
44
32
  * options are set; `.evict()` is intentionally absent because the
45
33
  * "configured a fetch then evicted instead" sequence has no coherent
46
34
  * meaning &mdash; eviction is always available off the bare
47
35
  * `context.actions.resource(...)` call.
36
+ *
37
+ * Concurrent callers with the same `(Resource, params)` automatically
38
+ * share a single in-flight fetch &mdash; one network request, every
39
+ * caller resolves with the same payload. The shared fetch runs on a
40
+ * detached `AbortController` so one caller's abort never cancels work
41
+ * other callers are still waiting on; when every caller has released
42
+ * (their `context.task.controller` aborted) the shared controller is
43
+ * aborted too. Chain `.isolated()` to opt out for the rare case that
44
+ * needs an independent request.
48
45
  */
49
46
  export type ResourceFetch<T> = PromiseLike<T> & {
50
47
  /**
@@ -54,22 +51,25 @@ export type ResourceFetch<T> = PromiseLike<T> & {
54
51
  */
55
52
  readonly exceeds: (duration: Temporal.DurationLike) => ResourceFetch<T>;
56
53
  /**
57
- * Join an in-flight fetch for the same `(resource, params, token)`
58
- * tuple. The shared fetch runs against a detached `AbortController`
59
- * so a single caller's abort never cancels work other callers are
60
- * waiting on; each caller still sees its own `context.task.controller`
61
- * abort as a rejection of its personal await.
54
+ * Opt this call out of the default `(Resource, params)` coalesce
55
+ * path. The fetch fires as an independent network request against
56
+ * the caller's own `context.task.controller` &mdash; no joining of
57
+ * any in-flight fetch, no refcounted detached controller. Aborting
58
+ * the caller's task cancels the network exactly as a regular fetch
59
+ * would.
62
60
  *
63
- * `token` is optional &mdash; omit it to share with every other
64
- * untokened caller for the same `(resource, params)` slot.
61
+ * Reach for this only when two callers need parallel fetches with
62
+ * byte-identical params and the difference in intent genuinely can't
63
+ * be modelled by differing params. The default is almost always
64
+ * what you want.
65
65
  */
66
- readonly coalesce: (token?: Coalesce) => ResourceFetch<T>;
66
+ readonly isolated: () => ResourceFetch<T>;
67
67
  };
68
68
  /**
69
69
  * Chainable handle returned from `context.actions.resource(invocation)`.
70
- * Either resolve to the fetched value (`.exceeds`/`.coalesce` + await)
70
+ * Either resolve to the fetched value (`.exceeds`/`.isolated` + await)
71
71
  * or drop the cache slot (`.evict`) &mdash; the two paths are mutually
72
- * exclusive, so once `.exceeds` or `.coalesce` runs the chain narrows
72
+ * exclusive, so once `.exceeds` or `.isolated` runs the chain narrows
73
73
  * to {@link ResourceFetch} and `.evict` is no longer available.
74
74
  */
75
75
  export type ResourceCall<T> = ResourceFetch<T> & {
@@ -175,9 +175,10 @@ export declare class Brand {
175
175
  static readonly Name: unique symbol;
176
176
  /**
177
177
  * Phantom brand identifying lifecycle actions returned by
178
- * `Lifecycle.Mount()`, `Lifecycle.Unmount()`, `Lifecycle.Error()`, and
179
- * `Lifecycle.Update()`. Carries the lifecycle's literal kind so that
180
- * `useAction` can pick distinct overloads &mdash; in particular,
178
+ * `Lifecycle.Mount()`, `Lifecycle.Paint()`, `Lifecycle.Unmount()`,
179
+ * `Lifecycle.Error()`, and `Lifecycle.Update()`. Carries the lifecycle's
180
+ * literal kind so that `useAction` can pick distinct overloads &mdash; in
181
+ * particular,
181
182
  * `Lifecycle.Update` resolves its payload to `Partial<DeepReadonly<D>>`
182
183
  * against the surrounding `useActions` data generic instead of the
183
184
  * factory-level `Record<string, unknown>` placeholder. Without this
@@ -213,6 +214,7 @@ export declare const EnvSymbol: unique symbol;
213
214
  * ```ts
214
215
  * export class Actions {
215
216
  * static Mount = Lifecycle.Mount();
217
+ * static Paint = Lifecycle.Paint();
216
218
  * static Unmount = Lifecycle.Unmount();
217
219
  * static Error = Lifecycle.Error();
218
220
  * static Update = Lifecycle.Update();
@@ -229,6 +231,14 @@ export declare const EnvSymbol: unique symbol;
229
231
  export declare class Lifecycle {
230
232
  /** Creates a Mount lifecycle action. Triggered once on component mount (`useLayoutEffect`). */
231
233
  static Mount(): LifecyclePayload<never, never, "Mount">;
234
+ /**
235
+ * Creates a Paint lifecycle action. Triggered once after the browser has
236
+ * committed the first frame (`useEffect`). Pairs with {@link Lifecycle.Mount}
237
+ * (pre-paint) &mdash; use Paint for work that should not delay the first
238
+ * paint: analytics &ldquo;viewed&rdquo; events, focus management, scroll-into-view,
239
+ * non-blocking prefetch, etc.
240
+ */
241
+ static Paint(): LifecyclePayload<never, never, "Paint">;
232
242
  /** Creates an Unmount lifecycle action. Triggered when the component unmounts. */
233
243
  static Unmount(): LifecyclePayload<never, never, "Unmount">;
234
244
  /** Creates an Error lifecycle action. Triggered when an action throws. Receives `Fault` as payload. */
@@ -407,20 +417,21 @@ export type HandlerPayload<P = unknown, C extends Filter = never, Name extends s
407
417
  (channel: C): ChanneledAction<P, C, Name>;
408
418
  });
409
419
  /**
410
- * Branded type returned by `Lifecycle.Mount`, `Lifecycle.Unmount`,
411
- * `Lifecycle.Error`, and `Lifecycle.Update`. Structurally identical to a
412
- * `HandlerPayload` but carries a phantom `Brand.Lifecycle` brand whose value
413
- * is the lifecycle's literal kind. The brand is what lets `useAction` and
414
- * `Handlers` resolve `Lifecycle.Update`'s payload to `Partial<DeepReadonly<D>>`
415
- * (against the surrounding `useActions` data generic) instead of the
416
- * factory-level `Record<string, unknown>` placeholder &mdash; a user-defined
417
- * `Action<P>("Update")` would have `Name = "Update"` but no `Brand.Lifecycle`,
418
- * so it falls into the generic payload overload as expected.
420
+ * Branded type returned by `Lifecycle.Mount`, `Lifecycle.Paint`,
421
+ * `Lifecycle.Unmount`, `Lifecycle.Error`, and `Lifecycle.Update`.
422
+ * Structurally identical to a `HandlerPayload` but carries a phantom
423
+ * `Brand.Lifecycle` brand whose value is the lifecycle's literal kind. The
424
+ * brand is what lets `useAction` and `Handlers` resolve `Lifecycle.Update`'s
425
+ * payload to `Partial<DeepReadonly<D>>` (against the surrounding `useActions`
426
+ * data generic) instead of the factory-level `Record<string, unknown>`
427
+ * placeholder &mdash; a user-defined `Action<P>("Update")` would have
428
+ * `Name = "Update"` but no `Brand.Lifecycle`, so it falls into the generic
429
+ * payload overload as expected.
419
430
  *
420
431
  * @template P Payload type for the lifecycle.
421
432
  * @template C Channel filter (always `never` for lifecycles &mdash; they are
422
433
  * not channeled).
423
- * @template Name Literal name (`"Mount"`, `"Unmount"`, `"Error"`, `"Update"`).
434
+ * @template Name Literal name (`"Mount"`, `"Paint"`, `"Unmount"`, `"Error"`, `"Update"`).
424
435
  */
425
436
  export type LifecyclePayload<P = unknown, C extends Filter = never, Name extends string = string> = HandlerPayload<P, C, Name> & {
426
437
  readonly [Brand.Lifecycle]: Name;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "march-hare",
3
- "version": "0.13.10",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "packageManager": "yarn@1.22.22",