@mmstack/primitives 22.0.3 → 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/README.md +229 -1
- package/fesm2022/mmstack-primitives.mjs +650 -60
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mmstack-primitives.d.ts +399 -6
package/package.json
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
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
|
*
|
|
@@ -2136,17 +2452,17 @@ type MutableSignalStoreObject<T> = Simplify<Readonly<{
|
|
|
2136
2452
|
}>;
|
|
2137
2453
|
type SignalStore<T> = Signal<UnwrapOpqaue<T>> & (IsAny<T> extends true ? SignalStoreObject<T> : NonNullable<T> extends BaseType ? {
|
|
2138
2454
|
readonly [LEAF]: () => boolean;
|
|
2139
|
-
} : NonNullable<T> extends
|
|
2455
|
+
} : NonNullable<T> extends any[] ? SignalArrayStore<NonNullable<T>> : SignalStoreObject<T>);
|
|
2140
2456
|
type WritableSignalStore<T> = WritableSignal<UnwrapOpqaue<T>> & {
|
|
2141
2457
|
readonly asReadonlyStore: () => SignalStore<T>;
|
|
2142
2458
|
} & (IsAny<T> extends true ? WritableSignalStoreObject<T> : NonNullable<T> extends BaseType ? {
|
|
2143
2459
|
readonly [LEAF]: () => boolean;
|
|
2144
|
-
} : NonNullable<T> extends
|
|
2460
|
+
} : NonNullable<T> extends any[] ? WritableArrayStore<NonNullable<T>> : WritableSignalStoreObject<T>);
|
|
2145
2461
|
type MutableSignalStore<T> = MutableSignal<UnwrapOpqaue<T>> & {
|
|
2146
2462
|
readonly asReadonlyStore: () => SignalStore<T>;
|
|
2147
2463
|
} & (IsAny<T> extends true ? MutableSignalStoreObject<T> : NonNullable<T> extends BaseType ? {
|
|
2148
2464
|
readonly [LEAF]: () => boolean;
|
|
2149
|
-
} : NonNullable<T> extends
|
|
2465
|
+
} : NonNullable<T> extends any[] ? MutableArrayStore<NonNullable<T>> : MutableSignalStoreObject<T>);
|
|
2150
2466
|
declare function toStore<T extends AnyRecord>(source: MutableSignal<T>, injector?: Injector, vivify?: Vivify, noUnionLeaves?: boolean): MutableSignalStore<T>;
|
|
2151
2467
|
declare function toStore<T extends AnyRecord>(source: WritableSignal<T>, injector?: Injector, vivify?: Vivify, noUnionLeaves?: boolean): WritableSignalStore<T>;
|
|
2152
2468
|
declare function toStore<T extends AnyRecord>(source: Signal<T>, injector?: Injector, vivify?: Vivify, noUnionLeaves?: boolean): SignalStore<T>;
|
|
@@ -2203,6 +2519,83 @@ declare function mutableStore<T extends AnyRecord>(value: T, opt?: CreateSignalO
|
|
|
2203
2519
|
noUnionLeaves?: boolean;
|
|
2204
2520
|
}): MutableSignalStore<T>;
|
|
2205
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
|
+
|
|
2206
2599
|
/**
|
|
2207
2600
|
* Interface for storage mechanisms compatible with the `stored` signal.
|
|
2208
2601
|
* Matches the essential parts of the `Storage` interface (`localStorage`, `sessionStorage`).
|
|
@@ -2630,5 +3023,5 @@ type CreateHistoryOptions<T> = Omit<CreateSignalOptions<T[]>, 'equal'> & {
|
|
|
2630
3023
|
*/
|
|
2631
3024
|
declare function withHistory<T>(sourceOrValue: WritableSignal<T> | T, opt?: CreateHistoryOptions<T>): SignalWithHistory<T>;
|
|
2632
3025
|
|
|
2633
|
-
export { batteryStatus, chunked, clipboard, combineWith, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, filterWith, focusWithin, geolocation, idle, indexArray, isDerivation, isLeaf, 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 };
|
|
2634
|
-
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 };
|