@mmstack/primitives 22.0.2 → 22.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmstack/primitives",
3
- "version": "22.0.2",
3
+ "version": "22.1.0",
4
4
  "keywords": [
5
5
  "angular",
6
6
  "signals",
@@ -1,4 +1,6 @@
1
- import { ValueEqualityFn, Injector, Signal, CreateSignalOptions, DestroyRef, WritableSignal, EffectRef, EffectCleanupRegisterFn, CreateEffectOptions, ElementRef } from '@angular/core';
1
+ import * as i0 from '@angular/core';
2
+ import { ValueEqualityFn, Injector, Signal, Provider, CreateComputedOptions, EffectCleanupRegisterFn, CreateEffectOptions, EffectRef, CreateSignalOptions, WritableSignal, ResourceRef, DestroyRef, ElementRef } from '@angular/core';
3
+ import * as _mmstack_primitives from '@mmstack/primitives';
2
4
 
3
5
  type CreateChunkedOptions<T> = {
4
6
  /**
@@ -36,6 +38,302 @@ type CreateChunkedOptions<T> = {
36
38
  */
37
39
  declare function chunked<T>(source: Signal<T[]> | (() => T[]), options?: CreateChunkedOptions<T>): Signal<T[]>;
38
40
 
41
+ /**
42
+ * Keep-alive (the Angular analog of React's `<Activity>` / Vue's `<keep-alive>`): the wrapped
43
+ * subtree is mounted ONCE and kept — when `[mmActivity]` is false it's hidden (`display:none`) and
44
+ * its change detection is paused, preserving state (scroll, inputs, a video's position, loaded
45
+ * data); when true it's shown and CD resumes. It is never destroyed until the directive is.
46
+ *
47
+ * It also provides {@link PAUSED_CONTEXT} to the content (= the negation of `visible`), so descendants
48
+ * can pause *effect-driven* or *Observable* work while hidden (CD-detach alone pauses pull-based/template work, not
49
+ * effects/polling). If you're using the pausable primitives this is done automatically
50
+ *
51
+ * ```html
52
+ * <section *mmActivity="tab() === 'editor'"> ...heavy stateful editor... </section>
53
+ * ```
54
+ */
55
+ declare class MmActivity {
56
+ private readonly tpl;
57
+ private readonly vcr;
58
+ private readonly parent;
59
+ /** When false, keep the content mounted but hidden + CD-detached. */
60
+ readonly visible: i0.InputSignal<boolean>;
61
+ /** Paused == not visible — handed to the kept subtree as PAUSED_CONTEXT. */
62
+ private readonly paused;
63
+ private view;
64
+ constructor();
65
+ private apply;
66
+ static ɵfac: i0.ɵɵFactoryDeclaration<MmActivity, never>;
67
+ static ɵdir: i0.ɵɵDirectiveDeclaration<MmActivity, "[mmActivity]", never, { "visible": { "alias": "mmActivity"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
68
+ }
69
+ /**
70
+ * Inject the nearest paused-state signal — `true` while the surrounding subtree is paused (hidden by
71
+ * an Activity boundary). Defaults to a never-paused signal, so callers outside an Activity are
72
+ * unaffected; on the server it is always never-paused, so server-side work (e.g. connector fetches)
73
+ * isn't suppressed. This is the public way to read pause state; the underlying token is intentionally
74
+ * not exported.
75
+ */
76
+ declare function injectPaused(): Signal<boolean>;
77
+ /**
78
+ * Build a provider that supplies a paused-state signal to a subtree — the public way to set up an
79
+ * Activity-style pause boundary (used by `MmActivity` and the app-builder's per-branch injectors).
80
+ */
81
+ declare function providePaused(source: Signal<boolean>): Provider;
82
+
83
+ /**
84
+ * Structural hold-and-swap as a signal. Given a `target` (the desired value — e.g. the
85
+ * subtree/def/key you want to show) and a `ready` predicate, returns a signal that keeps
86
+ * yielding its PREVIOUS value until `ready()` is true, then swaps to the current target.
87
+ *
88
+ * This is the structural counterpart to `keepPrevious`/`commit`: where those hold a *value*
89
+ * through a reload, this holds a *structure* through a swap. The caller mounts the incoming
90
+ * structure off to the side (so its resources can settle and flip `ready`), keeps showing the
91
+ * held previous structure meanwhile, and lets the old one go once `ready` releases the swap.
92
+ *
93
+ * The very first value passes straight through (nothing to hold yet).
94
+ */
95
+ declare function holdUntilReady<T>(target: Signal<T>, ready: () => boolean): Signal<T>;
96
+
97
+ /**
98
+ * How a pausable primitive decides whether it is currently paused:
99
+ * - omitted (the default) or `true` — read the ambient {@link PAUSED_CONTEXT} (via `injector`, or the
100
+ * current injection context). Reaching for a `pausable*` primitive means you want it pausable, so
101
+ * this is the default; outside an Activity boundary there's no `PAUSED_CONTEXT`, so the primitive is
102
+ * returned unwrapped (never pauses, zero overhead). On the server it never pauses either.
103
+ * - a predicate `() => boolean` — used directly. A `Signal<boolean>` satisfies this (signals are
104
+ * callable), and a plain function works OUTSIDE an injection context.
105
+ * - `false` — the explicit opt-out: the primitive is returned UNWRAPPED (no `linkedSignal`, no gate),
106
+ * i.e. exactly the plain primitive with zero overhead.
107
+ */
108
+ type PauseOption = boolean | (() => boolean);
109
+ type PausableOptions = {
110
+ /** Pause source — see {@link PauseOption}. Defaults to `true` (read the ambient `PAUSED_CONTEXT`). */
111
+ readonly pause?: PauseOption;
112
+ /**
113
+ * Injector used to resolve {@link PAUSED_CONTEXT} when `pause` is `true`/omitted and the primitive
114
+ * is created outside an injection context. Ignored for the `false` / predicate forms.
115
+ */
116
+ readonly injector?: Injector;
117
+ };
118
+ /**
119
+ * Resolve a {@link PauseOption} into a pause predicate, or `null` meaning "do not pause".
120
+ * `null` tells the caller to return the bare primitive — no wrapper is created.
121
+ *
122
+ * - omitted/`true` → the ambient {@link PAUSED_CONTEXT} if an Activity boundary provides one (via
123
+ * `opt.injector` or the current injection context), else `null` (the bare primitive, no allocation).
124
+ * The default, because an explicit `pausable*` call wants to be pausable. An explicit `pause: true`
125
+ * with no boundary dev-warns; the omitted default stays quiet. SSR → `null`.
126
+ * - a function → returned as-is (covers `Signal<boolean>`; usable outside an injection context).
127
+ * SSR → `null` here too, detected via `opt.injector` if given, else a `globalThis.window` probe.
128
+ * - `false` → `null` (the explicit opt-out).
129
+ *
130
+ * Encapsulating this here keeps every pausable primitive's branching identical and in one place.
131
+ */
132
+ declare function resolvePause(opt?: PausableOptions): (() => boolean) | null;
133
+ /**
134
+ * Like {@link nestedEffect}, but pausable. While paused the effect does NOT run its body — and,
135
+ * crucially, it reads the pause predicate FIRST, so while paused its dependency set collapses to just
136
+ * the predicate (no churn from the real deps); on resume it re-runs and re-tracks. With no `pause`
137
+ * option it defaults to the ambient `PAUSED_CONTEXT`; `pause: false` makes it a plain `nestedEffect`
138
+ * with zero added overhead.
139
+ */
140
+ declare function pausableEffect(effectFn: (registerCleanup: EffectCleanupRegisterFn) => void, options?: CreateEffectOptions & PausableOptions): EffectRef;
141
+ /**
142
+ * Like `signal`, but pausable. While paused, READS hold the last value; writes still land on the
143
+ * underlying signal and surface on resume. Built on the `keepPrevious`/`hold` shape — a
144
+ * `linkedSignal` gated on the pause predicate, with `set`/`update`/`asReadonly` forwarded to the
145
+ * source signal. With no `pause` option it defaults to the ambient `PAUSED_CONTEXT`; `pause: false`
146
+ * makes it a plain `signal` — no `linkedSignal` is created.
147
+ *
148
+ * NOTE: while paused, `set(x)` followed by a read returns the *held* (pre-pause) value, not `x` — the
149
+ * write lands on the source and surfaces on resume. That is the "freeze the displayed value while
150
+ * hidden" semantics; do not rely on read-after-write while paused.
151
+ */
152
+ declare function pausableSignal<T>(initialValue: T, options?: CreateSignalOptions<T> & PausableOptions): WritableSignal<T>;
153
+ /**
154
+ * Like `computed`, but pausable. While paused it holds its last value AND does not recompute: the
155
+ * computation's dependencies are not read while paused, so a dependency change can't trigger work —
156
+ * on resume it recomputes and re-tracks. The very first read always computes, to seed a value. With
157
+ * no `pause` option it defaults to the ambient `PAUSED_CONTEXT`; `pause: false` makes it a plain
158
+ * `computed`.
159
+ */
160
+ declare function pausableComputed<T>(computation: () => T, options?: CreateComputedOptions<T> & PausableOptions): Signal<T>;
161
+
162
+ /**
163
+ * Handle for an in-progress transition: a `pending` signal (true while the transition's
164
+ * resources are in flight) and a `done` promise that resolves once they all settle.
165
+ */
166
+ type TransitionRef = {
167
+ readonly pending: Signal<boolean>;
168
+ readonly done: Promise<void>;
169
+ };
170
+ /**
171
+ * Returns a `startTransition(fn)` bound to the nearest transition scope. `fn` runs its state
172
+ * mutations (which commit immediately); any resource that reloads as a result holds its value
173
+ * (when `coordinate`/`commit`-wrapped) and reveals together once everything settles. The
174
+ * returned handle exposes a unified `pending` + `done` for the whole operation — for imperative
175
+ * coordination (disable a control, await completion) on top of the declarative hold-and-commit.
176
+ *
177
+ * Must be called in an injection context. This is the *async* generalization (Tier 2): it adds
178
+ * no rendering cost and needs no fork — holding direct/sync readers is a separate, deferred tier.
179
+ */
180
+ declare function injectStartTransition(): (fn: () => void) => TransitionRef;
181
+
182
+ /**
183
+ * What "not ready" means for first-load suspense:
184
+ * - `'value'`: the resource has no value yet (`!hasValue()`). With `keepPrevious`,
185
+ * this stays false through a reload — the previous value holds — so a transition
186
+ * does NOT re-suspend; only the genuine first load shows a placeholder.
187
+ * - `'loading'`: any in-flight request suspends, even a background reload.
188
+ */
189
+ type SuspendType = 'value' | 'loading';
190
+ type RegisterOptions = {
191
+ /**
192
+ * Whether this resource blocks the boundary's first paint (`suspended()`).
193
+ * `true` for things the subtree can't render without (e.g. lazily-loaded component
194
+ * code); `false` for in-region data, which should drive the transition indicator
195
+ * (`pending`) and hold-stale, but NOT blank the whole boundary while it first loads.
196
+ */
197
+ readonly suspends?: boolean;
198
+ };
199
+ /**
200
+ * A transition scope: the set of resources whose async state a boundary coordinates.
201
+ * Provided per-boundary (so nested boundaries are independent — the transition-scoped,
202
+ * not global, registry) with a root default so registration always lands somewhere.
203
+ */
204
+ type TransitionScope = {
205
+ /** The currently-registered resources (read-only view). */
206
+ readonly resources: Signal<readonly ResourceRef<any>[]>;
207
+ /**
208
+ * Any registered resource has a request in flight (`status` is `loading`/`reloading`).
209
+ * This is the transition indicator — true during a reload while `keepPrevious` holds
210
+ * the visible value, so the UI can show "updating…" without unmounting.
211
+ */
212
+ readonly pending: Signal<boolean>;
213
+ /** Any *suspending* resource is not ready — drives the first-load placeholder. */
214
+ suspended(type: SuspendType): boolean;
215
+ add(res: ResourceRef<any>, opt?: RegisterOptions): void;
216
+ remove(res: ResourceRef<any>): void;
217
+ /**
218
+ * Coordinated commit: wraps a value signal so it FREEZES at its last-settled value
219
+ * while the scope is `pending`, then reveals the current value once *everything*
220
+ * settles. Multiple values wrapped this way release together — one consistent frame,
221
+ * never a torn mix of new + stale across resources. Compose over a `keepPrevious`
222
+ * value: keepPrevious holds per-resource, `commit` gates the reveal on the aggregate.
223
+ */
224
+ commit<T>(value: Signal<T>): Signal<T>;
225
+ /**
226
+ * Whether a transaction is currently HOLDING this scope's synchronous display reads (Tier 3).
227
+ * A counter under the hood, so nested transactions compose. Distinct from `pending` (a resource
228
+ * is in flight): `holding` brackets a whole transaction from start to settle.
229
+ */
230
+ readonly holding: Signal<boolean>;
231
+ /** Begin a transaction hold (increment the counter). */
232
+ beginHold(): void;
233
+ /** End a transaction hold (decrement); reveals held values when the counter reaches 0. */
234
+ endHold(): void;
235
+ /**
236
+ * Tier 3 display hold: wraps a value so it FREEZES at its pre-hold value while the scope is
237
+ * `holding`, then reveals the live value when the hold ends. Unlike `commit` (gates on
238
+ * `pending`), this brackets the whole transaction — so a *synchronous* state write made inside
239
+ * the transaction stays visually held until the transaction settles, with no torn frame.
240
+ */
241
+ hold<T>(value: Signal<T>): Signal<T>;
242
+ };
243
+ declare function createTransitionScope(): TransitionScope;
244
+ /** Provide a fresh transition scope at a boundary so its subtree's resources are tracked independently. */
245
+ declare function provideTransitionScope(): Provider;
246
+ declare function injectTransitionScope(): TransitionScope;
247
+ /**
248
+ * Returns a register function bound to the nearest transition scope: it adds a resource
249
+ * to the scope and removes it when the caller's injection context is destroyed. Pass any
250
+ * `ResourceRef` (a query, mutation, or plain Angular resource) through it.
251
+ */
252
+ declare function injectRegisterResource(): <T extends ResourceRef<any>>(res: T, opt?: RegisterOptions) => T;
253
+ /** Convenience: register a resource with the nearest transition scope. Must run in an injection context. */
254
+ declare function registerResource<T extends ResourceRef<any>>(res: T, opt?: RegisterOptions): T;
255
+
256
+ /**
257
+ * Shared **suspense** (readiness) boundary behaviour: reads the *nearest* transition scope and exposes
258
+ * its `pending`/`suspended` state. This is the readiness gate — distinct from the hold-stale *swap*
259
+ * primitives (`TransitionRouterOutlet`, `ab-transition`), which are the actual "transitions". The two
260
+ * concrete components below differ only by whether they provide their own scope, so the logic (and
261
+ * template) live here once.
262
+ *
263
+ * - **First load** (`suspended()`): no value yet → show the `[placeholder]` fallback.
264
+ * - **Reload** (`pending()` but a value is held via `keepPrevious`): keep the real content mounted and
265
+ * surface a busy indicator (`aria-busy`, and an optional `[busy]` slot) instead of flashing back to
266
+ * the placeholder.
267
+ *
268
+ * `type` selects what "not ready" means: `'value'` (default) suspends only until a first value lands
269
+ * then holds through reloads; `'loading'` suspends on every in-flight load (strict suspense).
270
+ */
271
+ declare abstract class SuspenseBoundaryBase {
272
+ protected readonly scope: _mmstack_primitives.TransitionScope;
273
+ /** What counts as "not ready" for the first-load placeholder. Defaults to value-presence. */
274
+ readonly type: i0.InputSignal<SuspendType>;
275
+ protected readonly pending: i0.Signal<boolean>;
276
+ protected readonly suspended: i0.Signal<boolean>;
277
+ static ɵfac: i0.ɵɵFactoryDeclaration<SuspenseBoundaryBase, never>;
278
+ static ɵdir: i0.ɵɵDirectiveDeclaration<SuspenseBoundaryBase, never, never, { "type": { "alias": "type"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
279
+ }
280
+ /**
281
+ * Standalone suspense boundary — **provides its own scope**, so dropping a `<mm-suspense>` anywhere
282
+ * just works: the resources created in its subtree register into it without any extra
283
+ * `provideTransitionScope()`. The common case.
284
+ */
285
+ declare class SuspenseBoundary extends SuspenseBoundaryBase {
286
+ static ɵfac: i0.ɵɵFactoryDeclaration<SuspenseBoundary, never>;
287
+ static ɵcmp: i0.ɵɵComponentDeclaration<SuspenseBoundary, "mm-suspense", never, {}, {}, never, ["[placeholder]", "[busy]", "*"], true, never>;
288
+ }
289
+ /**
290
+ * Unscoped suspense boundary — **reads the ambient scope** instead of providing one. For cases where
291
+ * the resources to coordinate are registered *above* the boundary (e.g. an app-builder page whose
292
+ * manifests/connectors register at a higher injector), so the boundary observes that outer scope
293
+ * rather than opening a fresh one. Pair with a `provideTransitionScope()` (or another boundary) in an
294
+ * ancestor.
295
+ */
296
+ declare class UnscopedSuspenseBoundary extends SuspenseBoundaryBase {
297
+ static ɵfac: i0.ɵɵFactoryDeclaration<UnscopedSuspenseBoundary, never>;
298
+ static ɵcmp: i0.ɵɵComponentDeclaration<UnscopedSuspenseBoundary, "mm-unscoped-suspense", never, {}, {}, never, ["[placeholder]", "[busy]", "*"], true, never>;
299
+ }
300
+
301
+ /**
302
+ * An undo log for a transactional transition. Stateful writes made while the transaction is the
303
+ * active one record their PRE-write value here (once, on first touch); `restore()` rolls them all
304
+ * back (abort), `clear()` keeps them (commit — the writes already landed live).
305
+ */
306
+ type Transaction = {
307
+ /** Record a signal's current value as its rollback point (no-op if already recorded). */
308
+ record(sig: WritableSignal<unknown>): void;
309
+ /** Roll every recorded signal back to its pre-write value (abort). */
310
+ restore(): void;
311
+ /** Drop the log, keeping live writes (commit). */
312
+ clear(): void;
313
+ };
314
+ declare function createTransaction(): Transaction;
315
+ /** The transaction in effect right now, or `null`. Stateful actions consult this to record undo. */
316
+ declare function activeTransaction(): Transaction | null;
317
+ /** Handle for an in-progress transaction (Tier 3): the transition `pending`/`done`, plus `abort`. */
318
+ type TransactionRef = {
319
+ readonly pending: Signal<boolean>;
320
+ readonly done: Promise<void>;
321
+ /** Roll back the staged writes and release the hold without committing. */
322
+ abort(): void;
323
+ };
324
+ /**
325
+ * Returns a `startTransaction(fn)` bound to the nearest transition scope — the Tier 3 sibling of
326
+ * `injectStartTransition`. It HOLDS the scope's synchronous display reads from before `fn` runs
327
+ * (so a state write inside `fn` doesn't flash through), records those writes in an undo log, then:
328
+ * - on settle (the scope's resources go in flight and drain) → release the hold + keep the writes;
329
+ * - on `abort()` → roll the writes back and release the hold.
330
+ *
331
+ * The writes land on LIVE state immediately (so derived variables and connector requests see the
332
+ * new values and refetch); only the *display* is held, via `scope.hold`. Must run in an injection
333
+ * context.
334
+ */
335
+ declare function injectStartTransaction(): (fn: () => void) => TransactionRef;
336
+
39
337
  /**
40
338
  * Options for creating a debounced writable signal.
41
339
  * Extends Angular's `CreateSignalOptions` with a debounce time setting.
@@ -553,6 +851,24 @@ declare function nestedEffect(effectFn: (registerCleanup: EffectCleanupRegisterF
553
851
  bindToFrame?: (parent: Frame | null) => Frame | null;
554
852
  }): EffectRef;
555
853
 
854
+ /**
855
+ * Wraps a signal so it HOLDS its last defined value whenever the source becomes
856
+ * `undefined`, yielding that value instead of the gap. This is the foundation of
857
+ * stale-while-revalidate: a source that drops to `undefined` mid-reload keeps
858
+ * surfacing its previous result rather than flashing empty.
859
+ *
860
+ * Built on `linkedSignal` — the only primitive that hands a computation its own
861
+ * previous output, which is exactly what "hold the previous value" needs.
862
+ *
863
+ * If the source is writable, the wrapper forwards `set`/`update`/`asReadonly` to it,
864
+ * so it stays a drop-in replacement. (Angular's `resource` is itself linkedSignal-backed
865
+ * and exposes a writable `value` for optimistic updates; this preserves that.)
866
+ */
867
+ declare function keepPrevious<T>(value: MutableSignal<T>, opt?: CreateSignalOptions<T>): MutableSignal<T>;
868
+ declare function keepPrevious<T, U>(value: DerivedSignal<T, U>, opt?: CreateSignalOptions<U>): DerivedSignal<T, U>;
869
+ declare function keepPrevious<T>(value: WritableSignal<T>, opt?: CreateSignalOptions<T>): WritableSignal<T>;
870
+ declare function keepPrevious<T>(value: Signal<T>, opt?: CreateSignalOptions<T>): Signal<T>;
871
+
556
872
  /**
557
873
  * Reactively maps items from a source array to a new array, creating stable signals for each item.
558
874
  *
@@ -2028,7 +2344,32 @@ declare function opaque<T extends object>(value: T): Opaque<T>;
2028
2344
  * // value: Opaque<object> — `store` would treat it as an indivisible leaf
2029
2345
  * }
2030
2346
  */
2031
- declare function isOpaque(value: unknown): value is Opaque<object>;
2347
+ declare function isOpaque<T = object>(value: unknown): value is Opaque<T>;
2348
+ /**
2349
+ * @internal Runtime brand carrying a store node's lazily-built leaf probe. Exported (like
2350
+ * {@link OPAQUE}) only so the `{ readonly [LEAF]: () => boolean }` brand on the store types is
2351
+ * nameable in the emitted declarations — not part of the supported surface; use {@link isLeaf}.
2352
+ */
2353
+ declare const LEAF: unique symbol;
2354
+ /**
2355
+ * Reports whether a store node is currently a **leaf** — a terminal value the store does not
2356
+ * descend into (a primitive, `Date`, `RegExp`, {@link opaque} object, class instance, or a
2357
+ * `null`/`undefined` hole when vivification is off) rather than a record/array substore.
2358
+ *
2359
+ * Leaf-ness reflects the node's **live** value: the probe is reactive and memoized, so calling
2360
+ * `isLeaf` inside a `computed`/`effect` re-evaluates when the node's shape changes.
2361
+ *
2362
+ * @internal Exposed for advanced/niche interop only — not part of the supported public surface
2363
+ * and may change without a major version bump.
2364
+ *
2365
+ * @example
2366
+ * const s = store({ name: 'Ada', address: { city: 'London' } });
2367
+ * isLeaf(s.name); // true
2368
+ * isLeaf(s.address); // false — a substore
2369
+ */
2370
+ declare function isLeaf<T = unknown>(value: unknown): value is Signal<T> & {
2371
+ readonly [LEAF]: () => boolean;
2372
+ };
2032
2373
  /**
2033
2374
  * An object marked via {@link opaque} — the store treats it as an indivisible leaf
2034
2375
  * (like a `Date`), returning it whole instead of deep-proxying its keys.
@@ -2040,7 +2381,7 @@ type Opaque<T> = T & {
2040
2381
  type UnwrapOpqaue<T> = T extends {
2041
2382
  readonly [OPAQUE]: true;
2042
2383
  } ? Omit<T, typeof OPAQUE> : T;
2043
- type BaseType = string | number | boolean | symbol | undefined | null | Function | Date | RegExp | {
2384
+ type BaseType = string | number | boolean | symbol | bigint | undefined | null | Function | Date | RegExp | {
2044
2385
  readonly [OPAQUE]: true;
2045
2386
  };
2046
2387
  type Key = string | number;
@@ -2109,16 +2450,22 @@ type MutableSignalStoreObject<T> = Simplify<Readonly<{
2109
2450
  <L extends AnyRecord>(props: L): MutableSignalStore<Simplify<Omit<NonNullable<T>, keyof L> & L>>;
2110
2451
  };
2111
2452
  }>;
2112
- type SignalStore<T> = Signal<UnwrapOpqaue<T>> & (IsAny<T> extends true ? SignalStoreObject<T> : NonNullable<T> extends BaseType ? unknown : NonNullable<T> extends Array<any> ? SignalArrayStore<NonNullable<T>> : SignalStoreObject<T>);
2453
+ type SignalStore<T> = Signal<UnwrapOpqaue<T>> & (IsAny<T> extends true ? SignalStoreObject<T> : NonNullable<T> extends BaseType ? {
2454
+ readonly [LEAF]: () => boolean;
2455
+ } : NonNullable<T> extends any[] ? SignalArrayStore<NonNullable<T>> : SignalStoreObject<T>);
2113
2456
  type WritableSignalStore<T> = WritableSignal<UnwrapOpqaue<T>> & {
2114
2457
  readonly asReadonlyStore: () => SignalStore<T>;
2115
- } & (IsAny<T> extends true ? WritableSignalStoreObject<T> : NonNullable<T> extends BaseType ? unknown : NonNullable<T> extends Array<any> ? WritableArrayStore<NonNullable<T>> : WritableSignalStoreObject<T>);
2458
+ } & (IsAny<T> extends true ? WritableSignalStoreObject<T> : NonNullable<T> extends BaseType ? {
2459
+ readonly [LEAF]: () => boolean;
2460
+ } : NonNullable<T> extends any[] ? WritableArrayStore<NonNullable<T>> : WritableSignalStoreObject<T>);
2116
2461
  type MutableSignalStore<T> = MutableSignal<UnwrapOpqaue<T>> & {
2117
2462
  readonly asReadonlyStore: () => SignalStore<T>;
2118
- } & (IsAny<T> extends true ? MutableSignalStoreObject<T> : NonNullable<T> extends BaseType ? unknown : NonNullable<T> extends Array<any> ? MutableArrayStore<NonNullable<T>> : MutableSignalStoreObject<T>);
2119
- declare function toStore<T extends AnyRecord>(source: MutableSignal<T>, injector?: Injector, vivify?: Vivify): MutableSignalStore<T>;
2120
- declare function toStore<T extends AnyRecord>(source: WritableSignal<T>, injector?: Injector, vivify?: Vivify): WritableSignalStore<T>;
2121
- declare function toStore<T extends AnyRecord>(source: Signal<T>, injector?: Injector, vivify?: Vivify): SignalStore<T>;
2463
+ } & (IsAny<T> extends true ? MutableSignalStoreObject<T> : NonNullable<T> extends BaseType ? {
2464
+ readonly [LEAF]: () => boolean;
2465
+ } : NonNullable<T> extends any[] ? MutableArrayStore<NonNullable<T>> : MutableSignalStoreObject<T>);
2466
+ declare function toStore<T extends AnyRecord>(source: MutableSignal<T>, injector?: Injector, vivify?: Vivify, noUnionLeaves?: boolean): MutableSignalStore<T>;
2467
+ declare function toStore<T extends AnyRecord>(source: WritableSignal<T>, injector?: Injector, vivify?: Vivify, noUnionLeaves?: boolean): WritableSignalStore<T>;
2468
+ declare function toStore<T extends AnyRecord>(source: Signal<T>, injector?: Injector, vivify?: Vivify, noUnionLeaves?: boolean): SignalStore<T>;
2122
2469
  /**
2123
2470
  * Creates a WritableSignalStore from a value.
2124
2471
  * @see {@link toStore}
@@ -2136,6 +2483,14 @@ declare function store<T extends AnyRecord>(value: T, opt?: CreateSignalOptions<
2136
2483
  * explicit `'object'`/`'array'`, or a `() => container` factory. See {@link Vivify}.
2137
2484
  */
2138
2485
  vivify?: Vivify;
2486
+ /**
2487
+ * Performance opt-in: promise that no node ever switches between leaf and substore (i.e. no
2488
+ * unions mixing a primitive with an object/array). With this on, each node's leaf-ness is
2489
+ * resolved once on the first {@link isLeaf} probe and cached as a constant, skipping the
2490
+ * reactive `computed`. If a node's shape does change anyway, {@link isLeaf} keeps its first
2491
+ * answer. Off by default.
2492
+ */
2493
+ noUnionLeaves?: boolean;
2139
2494
  }): WritableSignalStore<T>;
2140
2495
  /**
2141
2496
  * Creates a MutableSignalStore from a value.
@@ -2154,8 +2509,93 @@ declare function mutableStore<T extends AnyRecord>(value: T, opt?: CreateSignalO
2154
2509
  * explicit `'object'`/`'array'`, or a `() => container` factory. See {@link Vivify}.
2155
2510
  */
2156
2511
  vivify?: Vivify;
2512
+ /**
2513
+ * Performance opt-in: promise that no node ever switches between leaf and substore (i.e. no
2514
+ * unions mixing a primitive with an object/array). With this on, each node's leaf-ness is
2515
+ * resolved once on the first {@link isLeaf} probe and cached as a constant, skipping the
2516
+ * reactive `computed`. If a node's shape does change anyway, {@link isLeaf} keeps its first
2517
+ * answer. Off by default.
2518
+ */
2519
+ noUnionLeaves?: boolean;
2157
2520
  }): MutableSignalStore<T>;
2158
2521
 
2522
+ /**
2523
+ * A 3-way merge of a forked value against a changed base: given the common `ancestor` (the base
2524
+ * value the fork last diverged from), `mine` (the fork's current value), and `theirs` (the base
2525
+ * now), return the reconciled value. Used when the base changes mid-fork — and at `commit`.
2526
+ */
2527
+ type ReconcileFn<T> = (ancestor: T, mine: T, theirs: T) => T;
2528
+ /**
2529
+ * How a fork reconciles when the base changes underneath it:
2530
+ * - `'fine'` (default for immutable stores) — per-path 3-way merge ({@link merge3}): keep the
2531
+ * paths the fork edited, take the base's live values for paths it didn't. Survives concurrent
2532
+ * base changes. UNSUPPORTED on a mutable base (in-place mutation defeats `merge3`'s
2533
+ * reference-identity checks — `fork` warns and falls back to `'coarse'`).
2534
+ * - `'coarse'` — whole-value re-link: any base change resets the WHOLE fork (drops staged writes).
2535
+ * The cheapest strategy; correct when the base is held for the fork's lifetime (transitions).
2536
+ * The default for a MUTABLE base.
2537
+ * - a {@link ReconcileFn} — bring your own merge (e.g. Immer patches, array-by-id, CRDT-ish).
2538
+ * NOTE: any reference-based 3-way merge has the same mutable-store problem as `'fine'`; on a
2539
+ * mutable base a custom fn receives `ancestor === theirs` (the same mutated object).
2540
+ */
2541
+ type ForkStrategy<T> = 'fine' | 'coarse' | ReconcileFn<T>;
2542
+ /**
2543
+ * A forked store: an isolated, writable overlay on a base store. Writes stay LOCAL to the fork
2544
+ * (the base is untouched); unedited paths read through to the base. `commit()` flushes the fork's
2545
+ * value onto the base; `discard()` drops the staged writes.
2546
+ *
2547
+ * The mechanism is `linkedSignal`: it holds local writes until its source (the base) changes, then
2548
+ * runs the {@link ForkStrategy} to reconcile. The store interface, deep reads, and deep
2549
+ * copy-on-write writes all come from `toStore` unchanged — the only fork-specific logic is the
2550
+ * reconcile on a base change.
2551
+ *
2552
+ * Reactivity note: the fork reads through a single staged signal, so a read subscribes to the
2553
+ * whole record (coarser than the base store's per-leaf tracking) and the strategy re-runs on any
2554
+ * base change. Free when the base is held (it never ticks); on a live base, `'fine'`'s {@link
2555
+ * merge3} is identity-pruned so it only walks paths that both sides changed.
2556
+ */
2557
+ type Fork<T> = {
2558
+ /** The forked store — use it like any store (read/write/extend). */
2559
+ readonly store: WritableSignalStore<T>;
2560
+ /** Apply the fork's staged value onto the base, then re-link (fork now mirrors the base). */
2561
+ commit(): void;
2562
+ /** Drop staged writes — the fork reads through to the base again. */
2563
+ discard(): void;
2564
+ };
2565
+ /**
2566
+ * Per-path 3-way merge. Reference-equality short-circuits do the work: a subtree the fork never
2567
+ * touched satisfies `mine === ancestor` (structural sharing keeps its identity) → take the live
2568
+ * base; a subtree the base never changed satisfies `theirs === ancestor` → keep the fork's. So it
2569
+ * only deep-walks paths that BOTH sides changed, and on a leaf/array conflict the fork wins.
2570
+ * Arrays are treated atomically (no positional merge — index shifts make that unsafe); supply a
2571
+ * {@link ReconcileFn} for array-aware merging.
2572
+ *
2573
+ * CONTRACT: "unchanged" is detected by REFERENCE identity, not deep equality. `mine` must be a
2574
+ * copy-on-write derivative of `ancestor` — i.e. untouched nodes keep their reference — which the
2575
+ * fork guarantees because writes flow through `toStore` (it rebuilds only the edited path and
2576
+ * shares everything else). Feed it a structurally-equal-but-fresh-reference node for an untouched
2577
+ * path and it will treat that node as edited (recursion/leaf-value checks usually still reconcile,
2578
+ * but a fresh-ref clean node vs a base type-change resolves to the fork's stale value). Primitive
2579
+ * leaves compare by value, so equal primitives are correctly seen as unchanged.
2580
+ */
2581
+ declare function merge3<T>(ancestor: T, mine: T, theirs: T): T;
2582
+ declare function forkStore<T extends Record<string, any>>(base: WritableSignalStore<T>, opt?: {
2583
+ strategy?: ForkStrategy<T>;
2584
+ injector?: Injector;
2585
+ /**
2586
+ * Store config for the FORK's store — NOT inherited from `base` (it's closed over inside
2587
+ * the base's `toStore` and can't be read back). If the base was created with these, pass
2588
+ * the same values or the fork's write semantics will differ:
2589
+ * - `vivify`: without it, a write through a `null`/`undefined` path is silently dropped on
2590
+ * the fork even though the base would have created the container. Match the base.
2591
+ * - `noUnionLeaves`: a perf promise; off just means the slower reactive leaf-probe. NOTE it
2592
+ * is a whole-store guarantee — a fork that flips a node's type (leaf↔substore) violates it,
2593
+ * and on `commit` the base receives the flipped value with stale cached leaf-ness.
2594
+ */
2595
+ vivify?: Vivify;
2596
+ noUnionLeaves?: boolean;
2597
+ }): Fork<T>;
2598
+
2159
2599
  /**
2160
2600
  * Interface for storage mechanisms compatible with the `stored` signal.
2161
2601
  * Matches the essential parts of the `Storage` interface (`localStorage`, `sessionStorage`).
@@ -2583,5 +3023,5 @@ type CreateHistoryOptions<T> = Omit<CreateSignalOptions<T[]>, 'equal'> & {
2583
3023
  */
2584
3024
  declare function withHistory<T>(sourceOrValue: WritableSignal<T> | T, opt?: CreateHistoryOptions<T>): SignalWithHistory<T>;
2585
3025
 
2586
- export { batteryStatus, chunked, clipboard, combineWith, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, filterWith, focusWithin, geolocation, idle, indexArray, isDerivation, isMutable, isOpaque, isStore, keyArray, map, mapArray, mapObject, mediaQuery, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, opaque, orientation, pageVisibility, pairwise, pipeable, piped, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
2587
- export type { BatteryStatus, ClipboardSignal, Computation, CreateChunkedOptions, CreateDebouncedOptions, CreateHistoryOptions, CreatePooledOptions, CreateProvidedPooledOptions, CreateStoredOptions, CreateThrottledOptions, DebouncedSignal, DerivedSignal, ElementSize, ElementSizeOptions, ElementSizeSignal, ElementVisibilityOptions, ElementVisibilitySignal, GeolocationOptions, GeolocationSignal, IdleOptions, IdleSignal, MousePositionOptions, MousePositionSignal, MutableSignal, MutableSignalStore, NetworkStatusSignal, Opaque, PipeableSignal, ScreenOrientation, ScrollPosition, ScrollPositionOptions, ScrollPositionSignal, SignalFromEventOptions, SignalStore, SignalWithHistory, StoredSignal, ThrottledSignal, UntilOptions, Vivify, WindowSize, WindowSizeOptions, WindowSizeSignal, WithVivify, WritableSignalStore };
3026
+ export { MmActivity, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, batteryStatus, chunked, clipboard, combineWith, createTransaction, createTransitionScope, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, filterWith, focusWithin, forkStore, geolocation, holdUntilReady, idle, indexArray, injectPaused, injectRegisterResource, injectStartTransaction, injectStartTransition, injectTransitionScope, isDerivation, isLeaf, isMutable, isOpaque, isStore, keepPrevious, keyArray, map, mapArray, mapObject, mediaQuery, merge3, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, opaque, orientation, pageVisibility, pairwise, pausableComputed, pausableEffect, pausableSignal, pipeable, piped, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, providePaused, provideTransitionScope, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
3027
+ export type { BatteryStatus, ClipboardSignal, Computation, CreateChunkedOptions, CreateDebouncedOptions, CreateHistoryOptions, CreatePooledOptions, CreateProvidedPooledOptions, CreateStoredOptions, CreateThrottledOptions, DebouncedSignal, DerivedSignal, ElementSize, ElementSizeOptions, ElementSizeSignal, ElementVisibilityOptions, ElementVisibilitySignal, Fork, ForkStrategy, GeolocationOptions, GeolocationSignal, IdleOptions, IdleSignal, MousePositionOptions, MousePositionSignal, MutableSignal, MutableSignalStore, NetworkStatusSignal, Opaque, PausableOptions, PauseOption, PipeableSignal, ReconcileFn, RegisterOptions, ScreenOrientation, ScrollPosition, ScrollPositionOptions, ScrollPositionSignal, SignalFromEventOptions, SignalStore, SignalWithHistory, StoredSignal, SuspendType, ThrottledSignal, Transaction, TransactionRef, TransitionRef, TransitionScope, UntilOptions, Vivify, WindowSize, WindowSizeOptions, WindowSizeSignal, WithVivify, WritableSignalStore };