@mmstack/primitives 21.4.0 → 21.5.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": "21.4.0",
3
+ "version": "21.5.0",
4
4
  "keywords": [
5
5
  "angular",
6
6
  "signals",
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, Injector, CreateComputedOptions, Signal, EffectCleanupRegisterFn, CreateEffectOptions, EffectRef, CreateSignalOptions, WritableSignal, Provider, ValueEqualityFn, ResourceRef, DestroyRef, ElementRef } from '@angular/core';
2
+ import { InjectionToken, Injector, CreateComputedOptions, Signal, EffectCleanupRegisterFn, CreateEffectOptions, EffectRef, CreateSignalOptions, WritableSignal, Provider, ValueEqualityFn, ResourceStatus, DestroyRef, ElementRef } from '@angular/core';
3
3
  import * as _mmstack_primitives from '@mmstack/primitives';
4
4
 
5
5
  /**
@@ -174,6 +174,56 @@ declare function injectPaused(): Signal<boolean>;
174
174
  */
175
175
  declare function providePaused(source: Signal<boolean>): Provider;
176
176
 
177
+ /**
178
+ * How the catch-up write is scheduled (the "lower priority" of the deferral):
179
+ * - `'afterRender'` (default): after the next render — the urgent update (e.g. the
180
+ * keystroke echo) paints first, the expensive subtree catches up right after.
181
+ * - `'idle'`: `requestIdleCallback` (macrotask fallback) — catch up when the frame has
182
+ * budget; keeps continuous input smooth at the cost of a laggier deferred view.
183
+ * - A function: custom scheduler — call the callback when it's time to catch up and
184
+ * return a canceller (also the test seam).
185
+ */
186
+ type DeferStrategy = 'afterRender' | 'idle' | ((cb: () => void) => () => void);
187
+ type DeferredValueOptions<T> = {
188
+ readonly strategy?: DeferStrategy;
189
+ /** Equality for the deferred value — an equal catch-up never notifies consumers. */
190
+ readonly equal?: ValueEqualityFn<T>;
191
+ readonly injector?: Injector;
192
+ };
193
+ /**
194
+ * The deferred view of a source signal: callable as the lagging value, with `pending`
195
+ * reporting whether a catch-up is still owed (source has moved ahead) — the
196
+ * `useDeferredValue`/`isStale` pair.
197
+ */
198
+ type DeferredSignal<T> = Signal<T> & {
199
+ /** True while the deferred value is behind the source (a catch-up is scheduled). */
200
+ readonly pending: Signal<boolean>;
201
+ };
202
+ /**
203
+ * `useDeferredValue` for signals: returns a signal that HOLDS its previous value when
204
+ * `source` changes and catches up at lower priority (after paint / on idle), so an
205
+ * expensive subtree keyed off the deferred value never blocks the urgent update that
206
+ * caused the change — type into a filter, the input echoes instantly, the big list
207
+ * re-renders a beat later.
208
+ *
209
+ * ```ts
210
+ * const query = signal('');
211
+ * const deferredQuery = deferredValue(query);
212
+ * const results = computed(() => expensiveFilter(items(), deferredQuery()));
213
+ * // template: <input [(ngModel)]="query" /> stays responsive; results lag one paint
214
+ * // deferredQuery.pending() → dim the stale list while it catches up
215
+ * ```
216
+ *
217
+ * Rapid changes coalesce: each change reschedules the catch-up, so only the LATEST
218
+ * source value is ever applied (no intermediate churn in the expensive subtree).
219
+ * On the server this is a synchronous pass-through — SSR renders once, so deferral
220
+ * would just mean rendering stale content.
221
+ *
222
+ * This is a scheduling tool, not an async one — for async work compose `latest()`;
223
+ * for coordinated multi-resource reveals use a transition scope.
224
+ */
225
+ declare function deferredValue<T>(source: Signal<T>, opt?: DeferredValueOptions<T>): DeferredSignal<T>;
226
+
177
227
  /**
178
228
  * Structural hold-and-swap as a signal. Given a `target` (the desired value — e.g. the
179
229
  * subtree/def/key you want to show) and a `ready` predicate, returns a signal that keeps
@@ -188,6 +238,95 @@ declare function providePaused(source: Signal<boolean>): Provider;
188
238
  */
189
239
  declare function holdUntilReady<T>(target: Signal<T>, ready: () => boolean): Signal<T>;
190
240
 
241
+ /**
242
+ * What `use()` accepts: any status-bearing async value — an Angular `ResourceRef`,
243
+ * an `@mmstack/resource` query/mutation, or another `latest()` result (so async
244
+ * derivations nest). Purely structural; no class or brand required.
245
+ */
246
+ type UseSource<T> = {
247
+ readonly status: Signal<ResourceStatus>;
248
+ readonly value: Signal<T | undefined>;
249
+ hasValue(): boolean;
250
+ readonly error?: Signal<unknown>;
251
+ };
252
+ /**
253
+ * An async derivation: callable as a signal of the latest successfully-computed value
254
+ * (held through in-flight recomputes — the stale-while-revalidate atom), with the
255
+ * aggregate async state of everything it `use()`d. Satisfies both `UseSource` (so it
256
+ * nests inside another `latest`) and the transition scope's `ResourceLike` surface
257
+ * (so it registers into boundaries like any resource).
258
+ */
259
+ type LatestSignal<T> = Signal<T | undefined> & {
260
+ /** The held value — same signal as the callable itself. */
261
+ readonly value: Signal<T | undefined>;
262
+ /**
263
+ * Aggregate status. `error` wins (any used member errored, or the computation threw);
264
+ * otherwise in-flight work maps to `reloading` (a value is held) / `loading` (first
265
+ * load); a completed computation is `resolved`; blocked-with-nothing-in-flight (e.g.
266
+ * a member is `idle`) is `idle`.
267
+ */
268
+ readonly status: Signal<ResourceStatus>;
269
+ /** Any used member has a request in flight (`loading`/`reloading`) — the aggregate transition indicator. */
270
+ readonly pending: Signal<boolean>;
271
+ /** Alias of `pending`, for the `ResourceRef`-shaped surface. */
272
+ readonly isLoading: Signal<boolean>;
273
+ /**
274
+ * The computation's own thrown error, or the first used member's error (in read
275
+ * order). `undefined` when healthy. The held value stays readable through an error.
276
+ */
277
+ readonly error: Signal<unknown>;
278
+ /** Whether a value has ever been produced (and is therefore held). */
279
+ hasValue(): boolean;
280
+ };
281
+ type CreateLatestOptions<T> = {
282
+ /** Equality for the held value: an in-flight cycle that recomputes to an equal value never notifies consumers (while `pending` still reports the flight). */
283
+ readonly equal?: ValueEqualityFn<T>;
284
+ /**
285
+ * Auto-registration into the nearest transition scope (same vocabulary as resource
286
+ * options): `'indicator'` drives `pending`/hold-stale only, `'suspend'` also gates the
287
+ * boundary's first-load placeholder. Requires an injection context (or `injector`).
288
+ */
289
+ readonly register?: false | 'indicator' | 'suspend';
290
+ /** Injection context for `register`, when created outside one. */
291
+ readonly injector?: Injector;
292
+ readonly debugName?: string;
293
+ };
294
+ /**
295
+ * Reads a resource inside a `latest()` computation: returns its value and reports it to
296
+ * the enclosing collector, so the derivation's aggregate `pending`/`status`/`error`
297
+ * include it. When the resource has no value yet (first load) or is in an error state,
298
+ * the computation short-circuits — code after this call simply doesn't run this round —
299
+ * which is what lets you write the happy path with no `undefined` checks:
300
+ *
301
+ * ```ts
302
+ * const fullName = latest(() => {
303
+ * const u = use(user); // waterfalls compose:
304
+ * const org = use(orgFor(u)); // orgFor(u) is only read once `user` has a value
305
+ * return `${u.name} @ ${org.name}`;
306
+ * });
307
+ * ```
308
+ *
309
+ * Must be called synchronously within `latest()` — like `inject()`, it throws elsewhere.
310
+ */
311
+ declare function use<T>(res: UseSource<T>): T;
312
+ /**
313
+ * An async derivation over resources: evaluates `fn` inside a collector frame so that
314
+ * every `use()` read registers as a member, and exposes the result with resource
315
+ * semantics — the value holds its previous state while anything it read is in flight
316
+ * (never flashing empty), `pending` aggregates the members' in-flight state, and the
317
+ * whole thing is itself a `UseSource`, so `latest`s nest and propagate.
318
+ *
319
+ * ```ts
320
+ * const fullName = latest(() => `${use(user).name} @ ${use(org).name}`);
321
+ * fullName(); // held value — undefined only before the first successful run
322
+ * fullName.pending(); // true while user OR org (re)loads
323
+ * ```
324
+ *
325
+ * Evaluation is a plain `computed` under the hood: lazy, pure, no effects, usable
326
+ * outside any injection context (`register` is the only DI-touching option).
327
+ */
328
+ declare function latest<T>(fn: () => T, opt?: CreateLatestOptions<T>): LatestSignal<T>;
329
+
191
330
  /**
192
331
  * Handle for an in-progress transition: a `pending` signal (true while the transition's OWN
193
332
  * resources are in flight — loads already in flight when it started are not attributed) and a
@@ -214,6 +353,22 @@ type TransitionRef = {
214
353
  */
215
354
  declare function injectStartTransition(): (fn: () => void) => TransitionRef;
216
355
 
356
+ /**
357
+ * The structural surface a transition scope actually reads — everything a `ResourceRef`
358
+ * has, so any resource (query, mutation, plain Angular `resource`) passes as-is, but also
359
+ * satisfied by status-bearing derivations like `latest()`, so those register too.
360
+ *
361
+ * `abort` is the optional cancellation seam: a resource that knows how to tear down its
362
+ * in-flight work exposes it (`queryResource` does; mutations deliberately don't — a POST
363
+ * can't be unsent), and {@link TransitionScope.abortPending} calls it. Resources without
364
+ * it are simply left to settle.
365
+ */
366
+ type ResourceLike = {
367
+ readonly status: Signal<ResourceStatus>;
368
+ readonly isLoading: Signal<boolean>;
369
+ hasValue(): boolean;
370
+ abort?(): void;
371
+ };
217
372
  /**
218
373
  * What "not ready" means for first-load suspense:
219
374
  * - `'value'`: the resource has no value yet (`!hasValue()`). With `keepPrevious`,
@@ -238,7 +393,7 @@ type RegisterOptions = {
238
393
  */
239
394
  type TransitionScope = {
240
395
  /** The currently-registered resources (read-only view). */
241
- readonly resources: Signal<readonly ResourceRef<any>[]>;
396
+ readonly resources: Signal<readonly ResourceLike[]>;
242
397
  /**
243
398
  * Any registered resource has a request in flight (`status` is `loading`/`reloading`).
244
399
  * This is the transition indicator — true during a reload while `keepPrevious` holds
@@ -247,8 +402,8 @@ type TransitionScope = {
247
402
  readonly pending: Signal<boolean>;
248
403
  /** Any *suspending* resource is not ready — drives the first-load placeholder. */
249
404
  suspended(type: SuspendType): boolean;
250
- add(res: ResourceRef<any>, opt?: RegisterOptions): void;
251
- remove(res: ResourceRef<any>): void;
405
+ add(res: ResourceLike, opt?: RegisterOptions): void;
406
+ remove(res: ResourceLike): void;
252
407
  /**
253
408
  * Coordinated commit: wraps a value signal so it FREEZES at its last-settled value
254
409
  * while the scope is `pending`, then reveals the current value once *everything*
@@ -257,6 +412,31 @@ type TransitionScope = {
257
412
  * value: keepPrevious holds per-resource, `commit` gates the reveal on the aggregate.
258
413
  */
259
414
  commit<T>(value: Signal<T>): Signal<T>;
415
+ /**
416
+ * THE CANCELLATION CONTRACT, and its manual lever for shared-scope cases.
417
+ *
418
+ * What holds by construction (no call needed):
419
+ * - **View-scoped work dies with its view.** A superseded transition (outlet or
420
+ * `*mmTransition`) destroys the hidden incoming view and its injector; resources
421
+ * created there are destroyed, which aborts their in-flight loads.
422
+ * - **Abort is real, all the way down.** Deduped HTTP requests are refCounted — when
423
+ * the last consumer lets go the request itself is torn down — and an aborted
424
+ * response can never settle into the query cache (cache writes happen on the
425
+ * subscriber side of the interceptor chain).
426
+ *
427
+ * What this method adds: resources registered in a scope that OUTLIVES the transition
428
+ * (a shared/root scope) aren't view-scoped, so nothing destroys them on supersede.
429
+ * `abortPending()` walks the registered resources and calls `abort()` on every
430
+ * in-flight one that exposes it ({@link ResourceLike.abort} — queries do, mutations
431
+ * deliberately don't, and a shared resource aborts for ALL its readers, so call this
432
+ * on interactions that invalidate the pending work, not as a reflex).
433
+ *
434
+ * Honest limit (true for every JS framework): only I/O is cancellable — an
435
+ * already-running synchronous computation cannot be preempted.
436
+ *
437
+ * @returns how many resources were actually aborted.
438
+ */
439
+ abortPending(): number;
260
440
  /**
261
441
  * Whether a transaction is currently HOLDING this scope's synchronous display reads (Tier 3).
262
442
  * A counter under the hood, so nested transactions compose. Distinct from `pending` (a resource
@@ -276,6 +456,19 @@ type TransitionScope = {
276
456
  hold<T>(value: Signal<T>): Signal<T>;
277
457
  };
278
458
  declare function createTransitionScope(): TransitionScope;
459
+ /**
460
+ * The scope→`PendingTasks` bridge: while `scope.pending()` is true, hold an Angular
461
+ * pending task so SSR serialization waits for the scope's in-flight loads — HTTP loads
462
+ * already do this via HttpClient, but CUSTOM loaders (a `latest()` over a hand-rolled
463
+ * promise, a non-HTTP resource) would otherwise let the server render a boundary
464
+ * mid-load. Wired automatically by `provideTransitionScope` /
465
+ * `provideForwardingTransitionScope`; call it yourself only for scopes you construct
466
+ * directly with `createTransitionScope()`.
467
+ *
468
+ * Server-only by design: on the browser, tying `ApplicationRef.isStable` to every load
469
+ * would stall stability-gated machinery (testability, hydration timing) for no benefit.
470
+ */
471
+ declare function bridgeScopeToPendingTasks(scope: TransitionScope, injector?: Injector): void;
279
472
  /** Provide a fresh transition scope at a boundary so its subtree's resources are tracked independently. */
280
473
  declare function provideTransitionScope(): Provider;
281
474
  declare function injectTransitionScope(): TransitionScope;
@@ -307,9 +500,9 @@ declare function createAttributedPending(scope: TransitionScope): Signal<boolean
307
500
  * to the scope and removes it when the caller's injection context is destroyed. Pass any
308
501
  * `ResourceRef` (a query, mutation, or plain Angular resource) through it.
309
502
  */
310
- declare function injectRegisterResource(): <T extends ResourceRef<any>>(res: T, opt?: RegisterOptions) => T;
503
+ declare function injectRegisterResource(): <T extends ResourceLike>(res: T, opt?: RegisterOptions) => T;
311
504
  /** Convenience: register a resource with the nearest transition scope. Must run in an injection context. */
312
- declare function registerResource<T extends ResourceRef<any>>(res: T, opt?: RegisterOptions): T;
505
+ declare function registerResource<T extends ResourceLike>(res: T, opt?: RegisterOptions): T;
313
506
 
314
507
  /**
315
508
  * Shared **suspense** (readiness) boundary behaviour: reads the *nearest* transition scope and exposes
@@ -403,6 +596,98 @@ type TransactionRef = {
403
596
  */
404
597
  declare function injectStartTransaction(): (fn: () => void) => TransactionRef;
405
598
 
599
+ type MmTransitionContext<T> = {
600
+ readonly $implicit: T;
601
+ readonly mmTransition: T;
602
+ };
603
+ /**
604
+ * Generic hold-and-swap: the non-router `TransitionRouterOutlet`. When the bound value changes,
605
+ * the OLD view stays mounted and visible (it keeps its old context value — that's the hold) while
606
+ * the NEW view mounts hidden with its **own transition scope**; resources created in the incoming
607
+ * subtree register into that scope just by existing, and once they've gone in flight and settled
608
+ * the views swap in one frame. Tabs, wizard steps, master-detail — any branch change that would
609
+ * otherwise flash a loading state.
610
+ *
611
+ * ```html
612
+ * <div *mmTransition="selectedTab(); let tab">
613
+ * @switch (tab) { ... }
614
+ * </div>
615
+ * ```
616
+ *
617
+ * Distinct from `<mm-suspense>` (the readiness gate): suspense decides placeholder-vs-content
618
+ * *within* one branch, but can't stop an `@switch` from unmounting the old branch the instant the
619
+ * value flips. This directive is the swap itself — the old branch survives until the new one is
620
+ * ready. Compose them freely: suspense inside a transitioned branch handles its first load.
621
+ *
622
+ * Semantics mirror the outlet: the first render is immediate (nothing to hold); an interrupting
623
+ * value change mid-hold destroys the half-ready hidden view and re-targets; a branch that loads
624
+ * nothing swaps right after its first render. Per-view scopes mean the outgoing branch's
625
+ * background work can never delay the swap. Set `mmTransitionImmediate` to skip holding, and
626
+ * `mmTransitionViewTransition` to wrap the swap in `document.startViewTransition` (feature
627
+ * detected). On the server every change swaps immediately.
628
+ */
629
+ declare class MmTransition<T> {
630
+ private readonly tpl;
631
+ private readonly vcr;
632
+ private readonly parent;
633
+ private readonly onServer;
634
+ /** The value whose changes are transitioned. Each view keeps the value it was created with. */
635
+ readonly value: i0.InputSignal<T>;
636
+ /** Skip holding entirely — every change swaps at once (the plain re-render behavior). */
637
+ readonly immediate: i0.InputSignal<boolean>;
638
+ /** Wrap the swap in the View Transitions API for an animated cross-fade (feature detected). */
639
+ readonly viewTransition: i0.InputSignal<boolean>;
640
+ private current;
641
+ private incoming;
642
+ /** Bumped on every re-target/teardown so a superseded (possibly deferred) swap can't commit. */
643
+ private swapEpoch;
644
+ private readonly holding;
645
+ /** True while an incoming view is mounted hidden, waiting to settle. */
646
+ readonly pending: Signal<boolean>;
647
+ static ngTemplateContextGuard<T>(dir: MmTransition<T>, ctx: unknown): ctx is MmTransitionContext<T>;
648
+ constructor();
649
+ private onValue;
650
+ private commitSwap;
651
+ /** The actual swap: destroy the old view, reveal the new one. Always instant. */
652
+ private finishSwap;
653
+ private dropIncoming;
654
+ private createView;
655
+ private setHidden;
656
+ static ɵfac: i0.ɵɵFactoryDeclaration<MmTransition<any>, never>;
657
+ static ɵdir: i0.ɵɵDirectiveDeclaration<MmTransition<any>, "[mmTransition]", ["mmTransition"], { "value": { "alias": "mmTransition"; "required": true; "isSignal": true; }; "immediate": { "alias": "mmTransitionImmediate"; "required": false; "isSignal": true; }; "viewTransition": { "alias": "mmTransitionViewTransition"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
658
+ }
659
+
660
+ /**
661
+ * Per-element morphs on held swaps: assigns `view-transition-name` reactively, so when
662
+ * a swap wrapped in `document.startViewTransition` flips views (`*mmTransition`'s
663
+ * `mmTransitionViewTransition`, or the transition outlet's view-transition option), the
664
+ * browser pairs same-named elements across the outgoing and incoming views and MORPHS
665
+ * them instead of cross-fading the whole boundary.
666
+ *
667
+ * ```html
668
+ * <!-- outgoing view (list) and incoming view (detail) both name the hero image: -->
669
+ * <img [mmViewTransitionName]="'hero-' + item().id" [src]="item().img" />
670
+ * ```
671
+ *
672
+ * Why this works with holds: both views coexist in the DOM during a hold, but the
673
+ * incoming one is `display: none` — elements without boxes aren't captured, so the
674
+ * same name on both sides is legal at each capture point (old visible at snapshot,
675
+ * new visible after the swap). No arming/cleanup dance needed.
676
+ *
677
+ * The name is normalized to a valid CSS custom-ident (invalid characters → `-`, a
678
+ * leading digit gets a `_` prefix). An empty string / `'none'` clears the name — use
679
+ * that to opt an element out conditionally. One rule remains YOURS to keep: a name
680
+ * must be unique among elements VISIBLE at capture time (two rendered instances of the
681
+ * same named element make the browser skip the whole transition) — derive names from
682
+ * ids for anything that can repeat.
683
+ */
684
+ declare class MmViewTransitionName {
685
+ readonly mmViewTransitionName: i0.InputSignal<string>;
686
+ constructor();
687
+ static ɵfac: i0.ɵɵFactoryDeclaration<MmViewTransitionName, never>;
688
+ static ɵdir: i0.ɵɵDirectiveDeclaration<MmViewTransitionName, "[mmViewTransitionName]", never, { "mmViewTransitionName": { "alias": "mmViewTransitionName"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
689
+ }
690
+
406
691
  /**
407
692
  * Options for creating a debounced writable signal.
408
693
  * Extends Angular's `CreateSignalOptions` with a debounce time setting.
@@ -2551,8 +2836,8 @@ type UnwrapOpaque<T> = T extends {
2551
2836
  type BaseType = string | number | boolean | symbol | bigint | undefined | null | Function | Date | RegExp | {
2552
2837
  readonly [OPAQUE]: true;
2553
2838
  };
2554
- type Key = string | number;
2555
- type AnyRecord = Record<Key, any>;
2839
+ type Key$1 = string | number;
2840
+ type AnyRecord = Record<Key$1, any>;
2556
2841
  /**
2557
2842
  * @internal Resolves to `true` only for `any`. In a conditional type, `any` distributes across
2558
2843
  * *both* branches (`unknown | object`), and `unknown | X` collapses to `unknown` — which would
@@ -2808,6 +3093,87 @@ type ForkStoreOptions<T> = toStoreOptions & {
2808
3093
  };
2809
3094
  declare function forkStore<T extends Record<string, any>>(base: WritableSignalStore<T>, opt?: ForkStoreOptions<T>): Fork<T>;
2810
3095
 
3096
+ type Key = string | number;
3097
+ /**
3098
+ * One structural operation. `set` on a key that did not previously exist carries NO `prev`
3099
+ * property (an absent key is not the same as a key holding `undefined` — the merge3 lesson),
3100
+ * which is what lets {@link invertBatch} invert an add into a delete.
3101
+ */
3102
+ type StoreOp = {
3103
+ kind: 'set';
3104
+ path: readonly Key[];
3105
+ next: unknown;
3106
+ prev?: unknown;
3107
+ } | {
3108
+ kind: 'delete';
3109
+ path: readonly Key[];
3110
+ prev: unknown;
3111
+ };
3112
+ /** One emission: every op derived from one commit window (a tick), in path order. */
3113
+ type OpBatch = {
3114
+ /** Identifies the emitting log — filter your own batches on a shared transport. */
3115
+ readonly origin: string;
3116
+ /** Per-log monotonic batch counter. */
3117
+ readonly version: number;
3118
+ readonly ops: readonly StoreOp[];
3119
+ };
3120
+ type CreateOpLogOptions = {
3121
+ /** Transport identity for emitted batches. Defaults to a random id. */
3122
+ readonly origin?: string;
3123
+ /** Injection context for the observing effect (required outside one). */
3124
+ readonly injector?: Injector;
3125
+ };
3126
+ type OpLog<T extends object> = {
3127
+ /**
3128
+ * Ordered, lossless delivery of every emitted batch. Synchronous — don't write back into
3129
+ * the observed source from inside a callback (route remote data through {@link OpLog.apply}).
3130
+ */
3131
+ subscribe(cb: (batch: OpBatch) => void): () => void;
3132
+ /** The most recent batch — a lossy sampling view (devtools); use `subscribe` for transport. */
3133
+ readonly latest: Signal<OpBatch | null>;
3134
+ /**
3135
+ * Applies ops (a remote batch, a persisted journal entry, an {@link invertBatch} result)
3136
+ * atomically: ONE `set`, one notification wave. Also advances this log's diff baseline in
3137
+ * the same step, so an applied batch produces NO echo emission — sync loops terminate by
3138
+ * construction. Local writes pending in the current tick are flushed (emitted) first, so
3139
+ * they are never silently folded into the applied baseline.
3140
+ */
3141
+ apply(ops: OpBatch | readonly StoreOp[]): void;
3142
+ /** Stops observing and drops subscribers. Also happens when the injection context dies. */
3143
+ destroy(): void;
3144
+ };
3145
+ /**
3146
+ * Inverts a batch for undo: reversed order, `set`↔its own inverse (an add — a `set` with no
3147
+ * `prev` — inverts to a `delete`; a `delete` inverts to a `set` restoring `prev`). Feed the
3148
+ * result to {@link OpLog.apply}. Requires the ops' `prev`s, which in-memory batches always
3149
+ * carry — a wire-serialized batch that stripped them is not invertible.
3150
+ */
3151
+ declare function invertBatch(batch: OpBatch | readonly StoreOp[]): StoreOp[];
3152
+ /**
3153
+ * Observes a copy-on-write signal (a `store`'s root, or any `WritableSignal` holding
3154
+ * immutably-updated objects) and emits its changes as minimal structural op batches — the
3155
+ * shared substrate for sync (ship batches, `apply` remote ones), persistence (journal
3156
+ * batches, replay on boot), undo ({@link invertBatch}), and devtools (`latest`).
3157
+ *
3158
+ * Zero store-core involvement and zero cost when unused: emission is a reference-pruned diff
3159
+ * of the root value per tick (structural sharing makes it O(changed paths)), driven by one
3160
+ * effect. A batch therefore coalesces everything written in one tick — for coarser,
3161
+ * intentional units, stage writes on a `forkStore` and `commit()` (one set → one batch).
3162
+ *
3163
+ * NOT supported on mutable stores/signals: in-place mutation keeps reference identity, which
3164
+ * defeats the diff (same reason `forkStore`'s `'fine'` strategy refuses them) — a dev-mode
3165
+ * warning fires and nothing emits.
3166
+ *
3167
+ * ```ts
3168
+ * const s = store({ todos: [{ done: false }] });
3169
+ * const log = opLog(s, { origin: 'tab-a' });
3170
+ * log.subscribe((b) => channel.postMessage(encode(b))); // ship
3171
+ * channel.onmessage = (m) => log.apply(decode(m.data)); // apply — echo-free
3172
+ * s.todos[0].done.set(true); // → { kind: 'set', path: ['todos', 0, 'done'], … }
3173
+ * ```
3174
+ */
3175
+ declare function opLog<T extends object>(source: WritableSignal<T>, opt?: CreateOpLogOptions): OpLog<T>;
3176
+
2811
3177
  /**
2812
3178
  * Interface for storage mechanisms compatible with the `stored` signal.
2813
3179
  * Matches the essential parts of the `Storage` interface (`localStorage`, `sessionStorage`).
@@ -3266,5 +3632,5 @@ type CreateHistoryOptions<T> = Omit<CreateSignalOptions<T[]>, 'equal'> & {
3266
3632
  */
3267
3633
  declare function withHistory<T>(sourceOrValue: WritableSignal<T> | T, opt?: CreateHistoryOptions<T>): SignalWithHistory<T>;
3268
3634
 
3269
- export { MmActivity, PAUSABLE_OPTIONS, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, batteryStatus, chunked, clipboard, combineWith, createAttributedPending, createForwardingScope, createTransaction, createTransitionScope, debounce, debounced, derived, distinct, elementSize, elementVisibility, extendStore, filter, filterWith, focusWithin, forkStore, geolocation, getTransitionScope, 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, pointerDrag, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, provideForwardingTransitionScope, providePausableOptions, providePaused, provideTransitionScope, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
3270
- export type { BatteryStatus, ClipboardSignal, Computation, CreateChunkedOptions, CreateDebouncedOptions, CreateHistoryOptions, CreatePooledOptions, CreateProvidedPooledOptions, CreateStoredOptions, CreateThrottledOptions, DebouncedSignal, DerivedSignal, ElementSize, ElementSizeOptions, ElementSizeSignal, ElementVisibilityOptions, ElementVisibilitySignal, ExtendStoreOptions, Fork, ForkStoreOptions, ForkStrategy, ForwardingTransitionScope, Frame, GeolocationOptions, GeolocationSignal, IdleOptions, IdleSignal, MousePositionOptions, MousePositionSignal, MutableSignal, MutableSignalStore, NetworkStatusSignal, Opaque, PausableOptions, PauseOption, PipeableSignal, PointerDragOptions, PointerDragSignal, PointerDragState, PointerModifiers, PointerPoint, ReconcileFn, RegisterOptions, ScreenOrientation, ScreenOrientationState, ScrollPosition, ScrollPositionOptions, ScrollPositionSignal, SensorRunOptions, SignalFromEventOptions, SignalStore, SignalWithHistory, StoreOptions, StoredSignal, SuspendType, ThrottledSignal, Transaction, TransactionRef, TransitionRef, TransitionScope, UntilOptions, Vivify, WindowSize, WindowSizeOptions, WindowSizeSignal, WithVivify, WritableSignalStore, toStoreOptions };
3635
+ export { MmActivity, MmTransition, MmViewTransitionName, PAUSABLE_OPTIONS, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, batteryStatus, bridgeScopeToPendingTasks, chunked, clipboard, combineWith, createAttributedPending, createForwardingScope, createTransaction, createTransitionScope, debounce, debounced, deferredValue, derived, distinct, elementSize, elementVisibility, extendStore, filter, filterWith, focusWithin, forkStore, geolocation, getTransitionScope, holdUntilReady, idle, indexArray, injectPaused, injectRegisterResource, injectStartTransaction, injectStartTransition, injectTransitionScope, invertBatch, isDerivation, isLeaf, isMutable, isOpaque, isStore, keepPrevious, keyArray, latest, map, mapArray, mapObject, mediaQuery, merge3, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, opLog, opaque, orientation, pageVisibility, pairwise, pausableComputed, pausableEffect, pausableSignal, pipeable, piped, pointerDrag, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, provideForwardingTransitionScope, providePausableOptions, providePaused, provideTransitionScope, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, use, windowSize, withHistory };
3636
+ export type { BatteryStatus, ClipboardSignal, Computation, CreateChunkedOptions, CreateDebouncedOptions, CreateHistoryOptions, CreateLatestOptions, CreateOpLogOptions, CreatePooledOptions, CreateProvidedPooledOptions, CreateStoredOptions, CreateThrottledOptions, DebouncedSignal, DeferStrategy, DeferredSignal, DeferredValueOptions, DerivedSignal, ElementSize, ElementSizeOptions, ElementSizeSignal, ElementVisibilityOptions, ElementVisibilitySignal, ExtendStoreOptions, Fork, ForkStoreOptions, ForkStrategy, ForwardingTransitionScope, Frame, GeolocationOptions, GeolocationSignal, IdleOptions, IdleSignal, LatestSignal, MmTransitionContext, MousePositionOptions, MousePositionSignal, MutableSignal, MutableSignalStore, NetworkStatusSignal, OpBatch, OpLog, Opaque, PausableOptions, PauseOption, PipeableSignal, PointerDragOptions, PointerDragSignal, PointerDragState, PointerModifiers, PointerPoint, ReconcileFn, RegisterOptions, ResourceLike, ScreenOrientation, ScreenOrientationState, ScrollPosition, ScrollPositionOptions, ScrollPositionSignal, SensorRunOptions, SignalFromEventOptions, SignalStore, SignalWithHistory, StoreOp, StoreOptions, StoredSignal, SuspendType, ThrottledSignal, Transaction, TransactionRef, TransitionRef, TransitionScope, UntilOptions, UseSource, Vivify, WindowSize, WindowSizeOptions, WindowSizeSignal, WithVivify, WritableSignalStore, toStoreOptions };