@mmstack/primitives 20.10.1 → 20.11.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/index.d.ts CHANGED
@@ -224,6 +224,56 @@ type DeferredSignal<T> = Signal<T> & {
224
224
  */
225
225
  declare function deferredValue<T>(source: Signal<T>, opt?: DeferredValueOptions<T>): DeferredSignal<T>;
226
226
 
227
+ /**
228
+ * Optional observability seam for the concurrency layer (idea/concurrency-devtools.md). A
229
+ * listener, provided via {@link provideConcurrencyInstrumentation}, receives events as
230
+ * transition scopes coordinate pending/suspense/transaction windows and register resources.
231
+ * Zero-cost when absent: the taps are `listener?.hook(...)` behind a once-resolved optional
232
+ * inject, so nothing is allocated or measured unless a listener is installed.
233
+ *
234
+ * Span-shaped hooks (`*Start`) return an opaque handle passed back to their `*End`, and carry
235
+ * `at` epoch-ms stamps — deliberately isomorphic to the telemetry `startSpan`/`SpanHandle` SPI,
236
+ * so a telemetry consumer maps one-to-one.
237
+ */
238
+ type ConcurrencyInstrumentation = {
239
+ pendingStart?(e: {
240
+ scope: string;
241
+ resources: number;
242
+ at: number;
243
+ }): unknown;
244
+ pendingEnd?(handle: unknown, e: {
245
+ at: number;
246
+ }): void;
247
+ transactionStart?(e: {
248
+ scope: string;
249
+ at: number;
250
+ }): unknown;
251
+ transactionEnd?(handle: unknown, e: {
252
+ at: number;
253
+ }): void;
254
+ resourceRegistered?(e: {
255
+ scope: string;
256
+ suspends: boolean;
257
+ }): void;
258
+ resourceRemoved?(e: {
259
+ scope: string;
260
+ }): void;
261
+ abortPending?(e: {
262
+ scope: string;
263
+ aborted: number;
264
+ at: number;
265
+ }): void;
266
+ };
267
+ declare const CONCURRENCY_INSTRUMENTATION: InjectionToken<ConcurrencyInstrumentation>;
268
+ declare function provideConcurrencyInstrumentation(listener: ConcurrencyInstrumentation): Provider;
269
+ /**
270
+ * Chrome DevTools "Performance" custom-tracks preset (idea/concurrency-devtools.md): writes a
271
+ * `performance.measure` for each pending/transaction window onto an "mmstack" extension track,
272
+ * so reactive coordination shows up on the Performance panel timeline. Dev-only, zero backend,
273
+ * no dependencies. Give each measure the scope name for readability.
274
+ */
275
+ declare function perfCustomTracks(track?: string): ConcurrencyInstrumentation;
276
+
227
277
  /**
228
278
  * Structural hold-and-swap as a signal. Given a `target` (the desired value — e.g. the
229
279
  * subtree/def/key you want to show) and a `ready` predicate, returns a signal that keeps
@@ -455,7 +505,13 @@ type TransitionScope = {
455
505
  */
456
506
  hold<T>(value: Signal<T>): Signal<T>;
457
507
  };
458
- declare function createTransitionScope(): TransitionScope;
508
+ type CreateTransitionScopeOptions = {
509
+ /** Scope identity for instrumentation events (idea/concurrency-devtools.md). */
510
+ readonly name?: string;
511
+ /** Optional observability listener; taps are no-ops when omitted (zero cost). */
512
+ readonly instrumentation?: ConcurrencyInstrumentation;
513
+ };
514
+ declare function createTransitionScope(opt?: CreateTransitionScopeOptions): TransitionScope;
459
515
  /**
460
516
  * The scope→`PendingTasks` bridge: while `scope.pending()` is true, hold an Angular
461
517
  * pending task so SSR serialization waits for the scope's in-flight loads — HTTP loads
@@ -470,7 +526,7 @@ declare function createTransitionScope(): TransitionScope;
470
526
  */
471
527
  declare function bridgeScopeToPendingTasks(scope: TransitionScope, injector?: Injector): void;
472
528
  /** Provide a fresh transition scope at a boundary so its subtree's resources are tracked independently. */
473
- declare function provideTransitionScope(): Provider;
529
+ declare function provideTransitionScope(opt?: CreateTransitionScopeOptions): Provider;
474
530
  declare function injectTransitionScope(): TransitionScope;
475
531
  /**
476
532
  * A transition scope that can be re-pointed at a delegate target at runtime. Reads and
@@ -837,23 +893,6 @@ type MutableSignal<T> = WritableSignal<T> & {
837
893
  * This is because a `.mutate()` call notifies its dependents that it has changed, but if the
838
894
  * reference to a derived object hasn't changed, the `computed` signal will not trigger its
839
895
  * own dependents by default.
840
- *
841
- * @example
842
- * ```ts
843
- * const state = mutable({ user: { name: 'John' }, lastUpdated: new Date() });
844
- *
845
- * // ✅ CORRECT: Deriving a primitive value works as expected.
846
- * const name = computed(() => state().user.name);
847
- *
848
- * // ❌ INCORRECT: This will not update reliably after the first change.
849
- * const userObject = computed(() => state().user);
850
- *
851
- * // ✅ CORRECT: For object derivations, `equal: false` is required.
852
- * const userObjectFixed = computed(() => state().user, { equal: false });
853
- *
854
- * // This mutation will now correctly trigger effects depending on `userObjectFixed`.
855
- * state.mutate(s => s.lastUpdated = new Date());
856
- * ```
857
896
  */
858
897
  declare function mutable<T>(): MutableSignal<T | undefined>;
859
898
  declare function mutable<T>(initial: T): MutableSignal<T>;
@@ -1800,10 +1839,10 @@ declare function clipboard(opt?: string | SensorRunOptions): ClipboardSignal;
1800
1839
  /**
1801
1840
  * Represents the size of an element.
1802
1841
  */
1803
- interface ElementSize {
1842
+ type ElementSize = {
1804
1843
  width: number;
1805
1844
  height: number;
1806
- }
1845
+ };
1807
1846
  /**
1808
1847
  * Options for configuring the `elementSize` sensor.
1809
1848
  */
@@ -2762,6 +2801,128 @@ type ResolvableTarget = EventTargetLike | Signal<EventTargetLike | null>;
2762
2801
  declare function signalFromEvent<TEvent extends Event>(target: ResolvableTarget, eventName: string, initial: TEvent | null, opt?: SignalFromEventOptions): Signal<TEvent | null>;
2763
2802
  declare function signalFromEvent<TEvent extends Event, U>(target: ResolvableTarget, eventName: string, initial: U, project: (event: TEvent) => U, opt?: SignalFromEventOptions): Signal<U>;
2764
2803
 
2804
+ type Key$2 = string | number;
2805
+ /**
2806
+ * One structural operation. `set` on a key that did not previously exist carries NO `prev`
2807
+ * property (an absent key is not the same as a key holding `undefined` — the merge3 lesson),
2808
+ * which is what lets {@link invertBatch} invert an add into a delete.
2809
+ */
2810
+ type StoreOp = {
2811
+ kind: 'set';
2812
+ path: readonly Key$2[];
2813
+ next: unknown;
2814
+ prev?: unknown;
2815
+ } | {
2816
+ kind: 'delete';
2817
+ path: readonly Key$2[];
2818
+ prev: unknown;
2819
+ };
2820
+ /** One emission: every op derived from one commit window (a tick), in path order. */
2821
+ type OpBatch = {
2822
+ /** Identifies the emitting log — filter your own batches on a shared transport. */
2823
+ readonly origin: string;
2824
+ /** Per-log monotonic batch counter. */
2825
+ readonly version: number;
2826
+ readonly ops: readonly StoreOp[];
2827
+ };
2828
+ /**
2829
+ * Drives an {@link opLog}'s emission reaction. Given the `run` closure (which reads the source in
2830
+ * a tracking context and flushes the delta), a driver arranges for `run` to execute now and again
2831
+ * on every subsequent change, returning a handle that stops it. The default driver is an Angular
2832
+ * `effect` (needs an injector). Supply a custom driver to run an opLog with NO injector; a
2833
+ * renderer-independent one built on `@angular/core/primitives/signals` `createWatch` ships as
2834
+ * `microtaskOpLogDriver` from `@mmstack/worker/host` (the Web Worker seam).
2835
+ */
2836
+ type OpLogDriver = (run: () => void) => {
2837
+ destroy(): void;
2838
+ };
2839
+ type CreateOpLogOptions = {
2840
+ /** Transport identity for emitted batches. Defaults to a random id. */
2841
+ readonly origin?: string;
2842
+ /** Injection context for the default effect-based driver (required outside one). */
2843
+ readonly injector?: Injector;
2844
+ /**
2845
+ * Replaces the default Angular-`effect` emission driver. Supply a custom driver (e.g.
2846
+ * `microtaskOpLogDriver` from `@mmstack/worker/host`) to run an opLog with NO injector. When
2847
+ * given, `injector` is ignored and no injection context is required.
2848
+ */
2849
+ readonly driver?: OpLogDriver;
2850
+ };
2851
+ type OpLog<T extends object> = {
2852
+ /**
2853
+ * Ordered, lossless delivery of every emitted batch. Synchronous — don't write back into
2854
+ * the observed source from inside a callback (route remote data through {@link OpLog.apply}).
2855
+ */
2856
+ subscribe(cb: (batch: OpBatch) => void): () => void;
2857
+ /** The most recent batch — a lossy sampling view (devtools); use `subscribe` for transport. */
2858
+ readonly latest: Signal<OpBatch | null>;
2859
+ /**
2860
+ * Synchronously diff the source and emit any pending change NOW, rather than waiting for the
2861
+ * driver's scheduled run (an app tick, or a custom driver's microtask). Idempotent
2862
+ * and coalescing: writes since the last emission compose into one batch, and a `flush()` with
2863
+ * nothing pending is a no-op. Use it to make emission deterministic — the worker host calls it
2864
+ * to settle its mirror synchronously (tests), and it underpins the flush-before-apply honesty of
2865
+ * {@link OpLog.apply}. Independent of the driver: a later scheduled run simply finds no diff.
2866
+ */
2867
+ flush(): void;
2868
+ /**
2869
+ * Applies ops (a remote batch, a persisted journal entry, an {@link invertBatch} result)
2870
+ * atomically: ONE `set`, one notification wave. Also advances this log's diff baseline in
2871
+ * the same step, so an applied batch produces NO echo emission — sync loops terminate by
2872
+ * construction. Local writes pending in the current tick are flushed (emitted) first, so
2873
+ * they are never silently folded into the applied baseline.
2874
+ */
2875
+ apply(ops: OpBatch | readonly StoreOp[]): void;
2876
+ /** Stops observing and drops subscribers. Also happens when the injection context dies. */
2877
+ destroy(): void;
2878
+ };
2879
+ /**
2880
+ * Pure, store-free application of ops onto a plain root value, returning the next immutable root
2881
+ * (structural-sharing along op paths, missing containers vivified `'auto'`-style). This is the
2882
+ * same transform {@link OpLog.apply} runs, extracted so a replica can fold a received batch into
2883
+ * a value WITHOUT owning a diffing {@link opLog} — e.g. the worker-graph read-replica seam.
2884
+ * Accepts a batch or a bare op list.
2885
+ */
2886
+ declare function applyOps<T>(root: T, ops: OpBatch | readonly StoreOp[]): T;
2887
+ /**
2888
+ * Pure reference-pruned structural diff of two roots into minimal ops (the emission core of
2889
+ * {@link opLog}, exported so code outside a log can produce a batch — e.g. diffing a scratch
2890
+ * draft against a replica's current value to route a write to its owner). Trusts the
2891
+ * copy-on-write contract: an untouched subtree that kept its reference is skipped.
2892
+ */
2893
+ declare function diffOps(prev: unknown, next: unknown): StoreOp[];
2894
+ /**
2895
+ * Inverts a batch for undo: reversed order, `set`↔its own inverse (an add — a `set` with no
2896
+ * `prev` — inverts to a `delete`; a `delete` inverts to a `set` restoring `prev`). Feed the
2897
+ * result to {@link OpLog.apply}. Requires the ops' `prev`s, which in-memory batches always
2898
+ * carry — a wire-serialized batch that stripped them is not invertible.
2899
+ */
2900
+ declare function invertBatch(batch: OpBatch | readonly StoreOp[]): StoreOp[];
2901
+ /**
2902
+ * Observes a copy-on-write signal (a `store`'s root, or any `WritableSignal` holding
2903
+ * immutably-updated objects) and emits its changes as minimal structural op batches — the
2904
+ * shared substrate for sync (ship batches, `apply` remote ones), persistence (journal
2905
+ * batches, replay on boot), undo ({@link invertBatch}), and devtools (`latest`).
2906
+ *
2907
+ * Zero store-core involvement and zero cost when unused: emission is a reference-pruned diff
2908
+ * of the root value per tick (structural sharing makes it O(changed paths)), driven by one
2909
+ * effect. A batch therefore coalesces everything written in one tick — for coarser,
2910
+ * intentional units, stage writes on a `forkStore` and `commit()` (one set → one batch).
2911
+ *
2912
+ * NOT supported on mutable stores/signals: in-place mutation keeps reference identity, which
2913
+ * defeats the diff (same reason `forkStore`'s `'fine'` strategy refuses them) — a dev-mode
2914
+ * warning fires and nothing emits.
2915
+ *
2916
+ * ```ts
2917
+ * const s = store({ todos: [{ done: false }] });
2918
+ * const log = opLog(s, { origin: 'tab-a' });
2919
+ * log.subscribe((b) => channel.postMessage(encode(b))); // ship
2920
+ * channel.onmessage = (m) => log.apply(decode(m.data)); // apply — echo-free
2921
+ * s.todos[0].done.set(true); // → { kind: 'set', path: ['todos', 0, 'done'], … }
2922
+ * ```
2923
+ */
2924
+ declare function opLog<T extends object>(source: WritableSignal<T>, opt?: CreateOpLogOptions): OpLog<T>;
2925
+
2765
2926
  /**
2766
2927
  * @internal Runtime brand carrying a store node's lazily-built leaf probe. Exported (like
2767
2928
  * {@link OPAQUE}) only so the `{ readonly [LEAF]: () => boolean }` brand on the store types is
@@ -3093,6 +3254,8 @@ type Fork<T> = {
3093
3254
  commit(): void;
3094
3255
  /** Drop staged writes — the fork reads through to the base again. */
3095
3256
  discard(): void;
3257
+ /** The staged delta vs the CURRENT base, as structural ops (inspect, persist, invert). */
3258
+ ops(): StoreOp[];
3096
3259
  };
3097
3260
  /**
3098
3261
  * Per-path 3-way merge. Reference-equality short-circuits do the work: a subtree the fork never
@@ -3119,127 +3282,313 @@ type ForkStoreOptions<T> = toStoreOptions & {
3119
3282
  };
3120
3283
  declare function forkStore<T extends Record<string, any>>(base: WritableSignalStore<T>, opt?: ForkStoreOptions<T>): Fork<T>;
3121
3284
 
3285
+ /** Hybrid logical clock stamp: physical epoch ms + logical counter for same-ms ordering. */
3286
+ type Hlc = {
3287
+ readonly p: number;
3288
+ readonly l: number;
3289
+ };
3290
+ /** Total order over stamps alone; ties break on `writer` via {@link compareTotal}. */
3291
+ declare function compareHlc(a: Hlc, b: Hlc): number;
3292
+ /** The protocol's total order: (hlc.p, hlc.l, writer). Never returns 0 for distinct writers. */
3293
+ declare function compareTotal(a: Hlc, writerA: string, b: Hlc, writerB: string): number;
3294
+ type HlcClock = {
3295
+ /** Stamp for a locally-emitted envelope: monotonic even when wall time stalls or rewinds. */
3296
+ next(): Hlc;
3297
+ /** Fold an observed remote stamp in, so subsequent local stamps sort after it. */
3298
+ observe(remote: Hlc): void;
3299
+ };
3300
+ /**
3301
+ * HLC per Kulkarni et al.: convergence never depends on wall clocks, but LWW fairness
3302
+ * degrades under large skew, so observing a remote clock far ahead warns in dev mode.
3303
+ */
3304
+ declare function createHlcClock(now?: () => number): HlcClock;
3305
+
3306
+ declare const OP_PROTO_VERSION = 1;
3122
3307
  type Key = string | number;
3123
3308
  /**
3124
- * One structural operation. `set` on a key that did not previously exist carries NO `prev`
3125
- * property (an absent key is not the same as a key holding `undefined` the merge3 lesson),
3126
- * which is what lets {@link invertBatch} invert an add into a delete.
3309
+ * The wire/journal record (op-protocol RFC §3). `writer` is an opaque principal pseudonym
3310
+ * natural identity never enters the envelope; `origin` identifies the emitting log instance.
3127
3311
  */
3128
- type StoreOp = {
3129
- kind: 'set';
3130
- path: readonly Key[];
3131
- next: unknown;
3132
- prev?: unknown;
3133
- } | {
3134
- kind: 'delete';
3135
- path: readonly Key[];
3136
- prev: unknown;
3137
- };
3138
- /** One emission: every op derived from one commit window (a tick), in path order. */
3139
- type OpBatch = {
3140
- /** Identifies the emitting log — filter your own batches on a shared transport. */
3312
+ type OpEnvelope = {
3313
+ readonly proto: number;
3141
3314
  readonly origin: string;
3142
- /** Per-log monotonic batch counter. */
3315
+ readonly writer: string;
3143
3316
  readonly version: number;
3317
+ readonly hlc: Hlc;
3318
+ readonly policyVersion: number;
3144
3319
  readonly ops: readonly StoreOp[];
3145
3320
  };
3321
+ declare const CONFLICT_BRAND = "~mmstackConflict";
3146
3322
  /**
3147
- * Drives an {@link opLog}'s emission reaction. Given the `run` closure (which reads the source in
3148
- * a tracking context and flushes the delta), a driver arranges for `run` to execute now and again
3149
- * on every subsequent change, returning a handle that stops it. The default driver is an Angular
3150
- * `effect` (needs an injector). Supply a custom driver to run an opLog with NO injector; a
3151
- * renderer-independent one built on `@angular/core/primitives/signals` `createWatch` ships as
3152
- * `microtaskOpLogDriver` from `@mmstack/worker/host` (the Web Worker seam).
3323
+ * A preserved (jj-style) conflict: both sides survive as data, sync never blocks, and
3324
+ * resolution is just a later write. String-branded so it survives structured clone.
3153
3325
  */
3154
- type OpLogDriver = (run: () => void) => {
3155
- destroy(): void;
3326
+ type Conflicted<T = unknown> = {
3327
+ readonly [CONFLICT_BRAND]: true;
3328
+ readonly mine: T;
3329
+ readonly theirs: T;
3330
+ readonly ancestor?: T;
3156
3331
  };
3157
- type CreateOpLogOptions = {
3158
- /** Transport identity for emitted batches. Defaults to a random id. */
3159
- readonly origin?: string;
3160
- /** Injection context for the default effect-based driver (required outside one). */
3161
- readonly injector?: Injector;
3332
+ declare function isConflicted<T = unknown>(value: unknown): value is Conflicted<T>;
3333
+ type MergeContext = {
3334
+ readonly path: readonly Key[];
3335
+ };
3336
+ /**
3337
+ * Resolves a concurrent set-vs-set collision. Called with a deterministic argument order
3338
+ * (`mine` = the side winning the total order) so every peer computes the same value.
3339
+ */
3340
+ type MergeFn = (ancestor: unknown, mine: unknown, theirs: unknown, ctx: MergeContext) => unknown;
3341
+ type MergePolicyEntry = {
3342
+ /** `'todos.*.title'` or a segment array; `'*'` matches exactly one segment. */
3343
+ readonly path: string | readonly Key[];
3344
+ readonly merge: MergeFn;
3345
+ };
3346
+ declare const lww: MergeFn;
3347
+ declare const mergeThree: MergeFn;
3348
+ declare const preserve: MergeFn;
3349
+ /**
3350
+ * Identity-aware array merge (op-protocol RFC §12 v0): reconciles two concurrent versions of
3351
+ * an array item-wise by a user-provided identity, instead of last-writer-wins on the whole
3352
+ * array. Items are matched by key; per-item fields merge via `merge3` against the ancestor
3353
+ * item; items added on either side survive; an item removed on either side and unedited on
3354
+ * the other stays removed. Item ORDER follows `mine` (the total-order winner), with `theirs`-
3355
+ * only additions appended — positional merging is out of scope (fractional indexing is the
3356
+ * known upgrade if dogfooding demands it). Arrays still TRAVEL as whole-value sets; identity
3357
+ * only shapes conflict resolution, so the wire format is untouched.
3358
+ */
3359
+ declare function keyedArray(identity: (item: unknown) => unknown, opt?: {
3360
+ item?: MergeFn;
3361
+ }): MergeFn;
3362
+ type ConvergingApply = {
3162
3363
  /**
3163
- * Replaces the default Angular-`effect` emission driver. Supply a custom driver (e.g.
3164
- * `microtaskOpLogDriver` from `@mmstack/worker/host`) to run an opLog with NO injector. When
3165
- * given, `injector` is ignored and no injection context is required.
3364
+ * Fold an envelope into the register map and return the ops the local store must apply
3365
+ * (post-dominance, post-policy, including replays of newer descendant winners). Pass
3366
+ * `local: true` for envelopes this peer emitted itself: registered, nothing returned.
3166
3367
  */
3368
+ ingest(env: OpEnvelope, opt?: {
3369
+ local?: boolean;
3370
+ }): StoreOp[];
3371
+ /** Drop all registers (snapshot compaction / rehydration boundary). */
3372
+ reset(): void;
3373
+ };
3374
+ /**
3375
+ * The unsequenced-topology convergence core (op-protocol RFC §4): a per-path last-writer-wins
3376
+ * register map over the total order (hlc, writer), with subtree dominance. Order-independent:
3377
+ * any arrival order of the same envelope set yields the same state.
3378
+ */
3379
+ declare function createConvergingApply(opt?: {
3380
+ policies?: readonly MergePolicyEntry[];
3381
+ }): ConvergingApply;
3382
+ type RebaseResult<T = unknown> = {
3383
+ root: T;
3384
+ /** Pending batches re-based onto the remote state, `prev`s refreshed. */
3385
+ pending: StoreOp[][];
3386
+ };
3387
+ /**
3388
+ * The shared rebase routine (op-protocol RFC §5): invert pending, apply remote, re-apply
3389
+ * pending through the merge policies. Pure — branching's `rebase()` and the sequenced relay
3390
+ * client both call this.
3391
+ */
3392
+ declare function rebaseOps<T>(root: T, pending: readonly (readonly StoreOp[])[], remote: readonly StoreOp[], policies?: readonly MergePolicyEntry[]): RebaseResult<T>;
3393
+ /**
3394
+ * A per-path-policy `ForkStrategy` for `forkStore`: a three-way reconcile built from the
3395
+ * shared rebase (invert mine → apply theirs' delta → re-apply mine through the policies).
3396
+ * Paths only one side touched resolve like `merge3`; paths BOTH touched go through the
3397
+ * matching {@link MergePolicyEntry} (`lww` default — fork wins, matching `'fine'`; or
3398
+ * `mergeThree` / `preserve` / custom). Same copy-on-write contract as `'fine'`.
3399
+ */
3400
+ declare function policyStrategy<T>(policies: readonly MergePolicyEntry[]): (ancestor: T, mine: T, theirs: T) => T;
3401
+ type OpSyncOptions = {
3402
+ /** Opaque principal pseudonym — provided by the app, never minted here (RFC §3). */
3403
+ readonly writer: string;
3404
+ readonly origin?: string;
3405
+ readonly policyVersion?: number;
3406
+ readonly policies?: readonly MergePolicyEntry[];
3407
+ readonly clock?: HlcClock;
3408
+ readonly injector?: Injector;
3167
3409
  readonly driver?: OpLogDriver;
3410
+ /** A version gap from a known origin (missed envelopes) — the resync hook. */
3411
+ readonly onGap?: (origin: string, expected: number, got: number) => void;
3168
3412
  };
3169
- type OpLog<T extends object> = {
3413
+ type OpSync<T = unknown> = {
3414
+ readonly origin: string;
3415
+ /** Locally-emitted envelopes, ready for a transport. */
3416
+ subscribe(cb: (env: OpEnvelope) => void): () => void;
3417
+ /** Converging apply of a remote envelope (echo-free; own-origin envelopes are ignored). */
3418
+ receive(env: OpEnvelope): void;
3419
+ /** Synchronously emit any pending local delta now. */
3420
+ flush(): void;
3421
+ /** Per-origin latest versions — the handshake watermark. */
3422
+ watermark(): Record<string, number>;
3423
+ /** The current root + watermark, for answering a peer's hello. */
3424
+ snapshot(): {
3425
+ root: T;
3426
+ wm: Record<string, number>;
3427
+ };
3170
3428
  /**
3171
- * Ordered, lossless delivery of every emitted batch. Synchronousdon't write back into
3172
- * the observed source from inside a callback (route remote data through {@link OpLog.apply}).
3429
+ * Emit the CURRENT root as a root-set envelopethe fresh-room seed of the relay
3430
+ * contract (a room's snapshot root becomes complete once seeded).
3173
3431
  */
3174
- subscribe(cb: (batch: OpBatch) => void): () => void;
3175
- /** The most recent batch — a lossy sampling view (devtools); use `subscribe` for transport. */
3176
- readonly latest: Signal<OpBatch | null>;
3432
+ seed(): void;
3177
3433
  /**
3178
- * Synchronously diff the source and emit any pending change NOW, rather than waiting for the
3179
- * driver's scheduled run (an app tick, or a custom driver's microtask). Idempotent
3180
- * and coalescing: writes since the last emission compose into one batch, and a `flush()` with
3181
- * nothing pending is a no-op. Use it to make emission deterministic — the worker host calls it
3182
- * to settle its mirror synchronously (tests), and it underpins the flush-before-apply honesty of
3183
- * {@link OpLog.apply}. Independent of the driver: a later scheduled run simply finds no diff.
3434
+ * Replace local state with a peer's snapshot, atomically (one notification wave).
3435
+ * Local envelopes the snapshot doesn't cover (per its watermark) are re-applied on
3436
+ * top, so writes made before hydration are never silently lost.
3184
3437
  */
3185
- flush(): void;
3438
+ hydrate(root: T, wm?: Record<string, number>): void;
3439
+ destroy(): void;
3440
+ };
3441
+ declare function opSync<T extends object>(source: WritableSignal<T>, opt: OpSyncOptions): OpSync<T>;
3442
+
3443
+ type StoreHistory = {
3444
+ readonly canUndo: Signal<boolean>;
3445
+ readonly canRedo: Signal<boolean>;
3446
+ /** Revert the most recent tracked change; a no-op when nothing is undoable. */
3447
+ undo(): void;
3448
+ /** Re-apply the most recently undone change. */
3449
+ redo(): void;
3450
+ /** Forget all tracked history (e.g. after a save boundary). */
3451
+ clear(): void;
3452
+ destroy(): void;
3453
+ };
3454
+ type StoreHistoryOptions = CreateOpLogOptions & {
3455
+ /** Max entries kept per stack (default 100). */
3456
+ readonly limit?: number;
3186
3457
  /**
3187
- * Applies ops (a remote batch, a persisted journal entry, an {@link invertBatch} result)
3188
- * atomically: ONE `set`, one notification wave. Also advances this log's diff baseline in
3189
- * the same step, so an applied batch produces NO echo emissionsync loops terminate by
3190
- * construction. Local writes pending in the current tick are flushed (emitted) first, so
3191
- * they are never silently folded into the applied baseline.
3458
+ * The change stream to track. Defaults to self-diffing `source` (every change to the store
3459
+ * becomes undoable). For collaborative-safe undo, pass a sync client's LOCAL envelope stream
3460
+ * (e.g. an `opSync`'s `subscribe`, which fires only for this peer's own writes) remote
3461
+ * peers' changes then never land on your undo stack.
3192
3462
  */
3193
- apply(ops: OpBatch | readonly StoreOp[]): void;
3194
- /** Stops observing and drops subscribers. Also happens when the injection context dies. */
3195
- destroy(): void;
3463
+ readonly track?: {
3464
+ subscribe(cb: (batch: OpBatch) => void): () => void;
3465
+ };
3196
3466
  };
3197
3467
  /**
3198
- * Pure, store-free application of ops onto a plain root value, returning the next immutable root
3199
- * (structural-sharing along op paths, missing containers vivified `'auto'`-style). This is the
3200
- * same transform {@link OpLog.apply} runs, extracted so a replica can fold a received batch into
3201
- * a value WITHOUT owning a diffing {@link opLog} e.g. the worker-graph read-replica seam.
3202
- * Accepts a batch or a bare op list.
3468
+ * Undo/redo for a copy-on-write store, built on the op-log: each tracked change is stored as
3469
+ * its inverse batch, so `undo()` is one `apply` and history costs only the diffs, not full
3470
+ * snapshots. Redoing is invert-of-the-inverse. A new edit made after an undo clears the redo
3471
+ * stack (linear history). Applying a redo/undo does not itself re-enter history.
3472
+ *
3473
+ * Composes with sync for collaborative undo: pass `track: syncClient` so only YOUR writes are
3474
+ * undoable, while `undo()` emits a normal op that propagates to peers (it writes through the
3475
+ * store, which the sync client picks up).
3203
3476
  */
3204
- declare function applyOps<T>(root: T, ops: OpBatch | readonly StoreOp[]): T;
3477
+ declare function storeHistory<T extends object>(source: WritableSignal<T>, opt?: StoreHistoryOptions): StoreHistory;
3478
+
3479
+ type MaybePromise<T> = T | Promise<T>;
3205
3480
  /**
3206
- * Pure reference-pruned structural diff of two roots into minimal ops (the emission core of
3207
- * {@link opLog}, exported so code outside a log can produce a batch e.g. diffing a scratch
3208
- * draft against a replica's current value to route a write to its owner). Trusts the
3209
- * copy-on-write contract: an untouched subtree that kept its reference is skipped.
3481
+ * The minimal async key/value contract persistence needs. Deliberately matches `idb-keyval`'s
3482
+ * top-level `get`/`set`/`del` so its module drops in with no wrapper (`persist(s, { key, store:
3483
+ * idbKeyval })`). Any store backed by structured clone (idb-keyval, Dexie) can hold complex values
3484
+ * without a serialize hook. A Dexie table needs a tiny adapter because it names things differently:
3485
+ *
3486
+ * ```ts
3487
+ * const table = db.table<{ key: string; value: unknown }>('kv');
3488
+ * const asyncStore: AsyncStore = {
3489
+ * get: (k) => table.get(k).then((r) => r?.value),
3490
+ * set: (k, v) => table.put({ key: k, value: v }).then(() => undefined),
3491
+ * del: (k) => table.delete(k),
3492
+ * };
3493
+ * ```
3210
3494
  */
3211
- declare function diffOps(prev: unknown, next: unknown): StoreOp[];
3495
+ type AsyncStore = {
3496
+ get(key: string): MaybePromise<unknown>;
3497
+ set(key: string, value: unknown): MaybePromise<void>;
3498
+ del(key: string): MaybePromise<void>;
3499
+ };
3500
+ /** Persistence options — the reader-side settings, independent of how the store was created. */
3501
+ type PersistOptions<T> = {
3502
+ /** Storage key for this store's snapshot. Required per call. */
3503
+ readonly key: string;
3504
+ /** The async backend. Falls back to the provided default (see {@link providePersistedStoreOptions}). */
3505
+ readonly store?: AsyncStore;
3506
+ /** Encode before writing. Default identity: structured-clone backends keep complex values. */
3507
+ readonly serialize?: (value: T) => unknown;
3508
+ /** Decode after reading. Default identity. */
3509
+ readonly deserialize?: (raw: unknown) => T;
3510
+ /**
3511
+ * Current schema version of the persisted value. When set, snapshots are written wrapped in a
3512
+ * small version envelope, and a snapshot stamped with an older version is passed through
3513
+ * {@link PersistOptions.migrate} on boot before it is adopted. A snapshot from a *newer* version
3514
+ * than this build is left untouched (a newer client wrote it).
3515
+ */
3516
+ readonly version?: number;
3517
+ /**
3518
+ * Bring a snapshot from an older `version` up to the current shape. It runs during boot, which
3519
+ * is already async, so it may be async too: lazy-import the migration ladder here and only pay
3520
+ * for it when there is old data to migrate. Receives the decoded old value and the version it
3521
+ * was written with (`0` for a pre-versioning snapshot).
3522
+ */
3523
+ readonly migrate?: (data: unknown, fromVersion: number) => MaybePromise<T>;
3524
+ /** Coalesce writes by this many ms (default 300). A flush/teardown always writes immediately. */
3525
+ readonly writeDebounceMs?: number;
3526
+ readonly injector?: Injector;
3527
+ };
3528
+ type PersistedStoreOptions<T extends object> = CreateSignalOptions<T> & toStoreOptions & PersistOptions<T>;
3212
3529
  /**
3213
- * Inverts a batch for undo: reversed order, `set`↔its own inverse (an add — a `set` with no
3214
- * `prev` inverts to a `delete`; a `delete` inverts to a `set` restoring `prev`). Feed the
3215
- * result to {@link OpLog.apply}. Requires the ops' `prev`s, which in-memory batches always
3216
- * carry — a wire-serialized batch that stripped them is not invertible.
3530
+ * App-wide defaults for {@link persist} / {@link persistedStore}. Only cross-type settings live
3531
+ * here; `serialize`/`deserialize` are per-call because they depend on the store's value type.
3217
3532
  */
3218
- declare function invertBatch(batch: OpBatch | readonly StoreOp[]): StoreOp[];
3533
+ type PersistedStoreDefaults = {
3534
+ readonly store?: AsyncStore;
3535
+ readonly writeDebounceMs?: number;
3536
+ };
3537
+ declare const PERSISTED_STORE_OPTIONS: InjectionToken<PersistedStoreDefaults>;
3219
3538
  /**
3220
- * Observes a copy-on-write signal (a `store`'s root, or any `WritableSignal` holding
3221
- * immutably-updated objects) and emits its changes as minimal structural op batches — the
3222
- * shared substrate for sync (ship batches, `apply` remote ones), persistence (journal
3223
- * batches, replay on boot), undo ({@link invertBatch}), and devtools (`latest`).
3539
+ * Wire the {@link AsyncStore} backend (and any shared debounce) once, override per call. The
3540
+ * typical use is to install idb-keyval at bootstrap so every `persist`/`persistedStore` persists
3541
+ * without re-passing the backend.
3224
3542
  *
3225
- * Zero store-core involvement and zero cost when unused: emission is a reference-pruned diff
3226
- * of the root value per tick (structural sharing makes it O(changed paths)), driven by one
3227
- * effect. A batch therefore coalesces everything written in one tick — for coarser,
3228
- * intentional units, stage writes on a `forkStore` and `commit()` (one set → one batch).
3543
+ * @example
3544
+ * import * as idbKeyval from 'idb-keyval';
3545
+ * providePersistedStoreOptions({ store: idbKeyval });
3546
+ */
3547
+ declare function providePersistedStoreOptions(opt: PersistedStoreDefaults): Provider;
3548
+ /** Persistence controls for a store, from {@link persist}. */
3549
+ type PersistHandle = {
3550
+ /**
3551
+ * `false` until the first read from the backend settles (or immediately `true` on the server
3552
+ * and when no backend is configured). Gate first paint on it if a stale-flash matters.
3553
+ */
3554
+ readonly hydrated: Signal<boolean>;
3555
+ /** Force any pending debounced write to the backend now. */
3556
+ flush(): Promise<void>;
3557
+ /** Remove the snapshot from the backend and reset the store to the value it held when attached. */
3558
+ clear(): Promise<void>;
3559
+ };
3560
+ /**
3561
+ * A store plus its persistence controls. Shaped like {@link Fork} (a `.store` field, not the
3562
+ * store itself) because the store is a proxy where any property access resolves a child path,
3563
+ * so controls cannot live on it directly.
3564
+ */
3565
+ type PersistedStore<T extends object> = {
3566
+ /** The live store. Reads are synchronous; it holds the initial value until hydration lands. */
3567
+ readonly store: WritableSignalStore<T>;
3568
+ } & PersistHandle;
3569
+ /**
3570
+ * Attach durable local persistence to an EXISTING store: its whole-value snapshot is written to an
3571
+ * async backend (IndexedDB via idb-keyval or Dexie) and restored on boot. A reader over the store,
3572
+ * so it composes with the other op-log readers (`tabSync`, `@mmstack/mesh`) on the same store — a
3573
+ * persisted, synced graph is just two readers. Local durability, not sync.
3229
3574
  *
3230
- * NOT supported on mutable stores/signals: in-place mutation keeps reference identity, which
3231
- * defeats the diff (same reason `forkStore`'s `'fine'` strategy refuses them) a dev-mode
3232
- * warning fires and nothing emits.
3575
+ * Because the backend is async, hydration cannot precede the first read: the store keeps its current
3576
+ * value, then adopts the persisted snapshot once the backend answers, UNLESS a write happened first
3577
+ * (an explicit boot-time write wins over stale disk). Writes are coalesced and flushed on teardown
3578
+ * and on page hide, so the last change is never lost. On the server it is a no-op.
3233
3579
  *
3234
- * ```ts
3235
- * const s = store({ todos: [{ done: false }] });
3236
- * const log = opLog(s, { origin: 'tab-a' });
3237
- * log.subscribe((b) => channel.postMessage(encode(b))); // ship
3238
- * channel.onmessage = (m) => log.apply(decode(m.data)); // apply — echo-free
3239
- * s.todos[0].done.set(true); // → { kind: 'set', path: ['todos', 0, 'done'], … }
3240
- * ```
3580
+ * When the persisted shape evolves, pass `version` and a `migrate` hook: an older snapshot is
3581
+ * brought forward on boot before it is adopted, then re-persisted in the new shape. Because boot is
3582
+ * already async, `migrate` may be async, so the migration ladder can be lazy-imported.
3241
3583
  */
3242
- declare function opLog<T extends object>(source: WritableSignal<T>, opt?: CreateOpLogOptions): OpLog<T>;
3584
+ declare function persist<T extends object>(source: WritableSignalStore<T>, opt: PersistOptions<T>): PersistHandle;
3585
+ /**
3586
+ * A `store` with {@link persist} already attached: a whole-value snapshot persisted to an async
3587
+ * backend and restored on boot. Equivalent to `const s = store(initial); persist(s, opt)` — reach
3588
+ * for `persist` directly when you want persistence on a store you already have (e.g. to also
3589
+ * `meshSync` it).
3590
+ */
3591
+ declare function persistedStore<T extends object>(initial: T, opt: PersistedStoreOptions<T>): PersistedStore<T>;
3243
3592
 
3244
3593
  /** Identity selector for keyed array reconciliation: a property name, or a function per item. */
3245
3594
  type ReconcileKey = string | ((item: any) => unknown);
@@ -3448,10 +3797,25 @@ type SyncSignalOptions = {
3448
3797
  */
3449
3798
  injector?: Injector;
3450
3799
  };
3800
+ /**
3801
+ * Store mode (`tabSync(store, …)`): syncs structural OPS instead of whole values — concurrent
3802
+ * edits to different leaves merge instead of clobbering, and a joining tab hydrates from a
3803
+ * peer via the hello exchange (up-to-date / snapshot; op-protocol RFC §6).
3804
+ */
3805
+ type StoreTabSyncOptions = SyncSignalOptions & {
3806
+ /** Principal pseudonym on emitted envelopes. Tabs share one user, so a default is fine. */
3807
+ writer?: string;
3808
+ /** Per-path merge policies (`lww` default; `mergeThree`, `preserve`, or custom). */
3809
+ policies?: readonly MergePolicyEntry[];
3810
+ /** How long a joining tab waits for a peer's answer before deciding it IS the base. */
3811
+ helloTimeoutMs?: number;
3812
+ /** Max response jitter — first responder wins, others cancel. */
3813
+ jitterMs?: number;
3814
+ };
3451
3815
  /**
3452
3816
  * @example tabSync(signal('dark'), { id: 'theme' })
3453
3817
  */
3454
- declare function tabSync<T extends WritableSignal<any>>(sig: T, opt: SyncSignalOptions | string): T;
3818
+ declare function tabSync<T extends WritableSignal<any>>(sig: T, opt: StoreTabSyncOptions | SyncSignalOptions | string): T;
3455
3819
  /**
3456
3820
  * @deprecated Use `tabSync` with `SyncSignalOptions` instead and pass the options as the second argument
3457
3821
  * @throws {Error} When deterministic ID generation fails and no explicit ID is provided
@@ -3741,5 +4105,5 @@ type CreateHistoryOptions<T> = Omit<CreateSignalOptions<T[]>, 'equal'> & {
3741
4105
  */
3742
4106
  declare function withHistory<T>(sourceOrValue: WritableSignal<T> | T, opt?: CreateHistoryOptions<T>): SignalWithHistory<T>;
3743
4107
 
3744
- export { MmActivity, MmTransition, MmViewTransitionName, PAUSABLE_OPTIONS, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, applyOps, batteryStatus, bridgeScopeToPendingTasks, chunked, clipboard, combineWith, createAttributedPending, createForwardingScope, createStoreContext, createTransaction, createTransitionScope, debounce, debounced, deferredValue, derived, diffOps, 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, projection, provideForwardingTransitionScope, providePausableOptions, providePaused, provideTransitionScope, reconcile, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, use, windowSize, withHistory };
3745
- 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, OpLogDriver, Opaque, PausableOptions, PauseOption, PipeableSignal, PointerDragOptions, PointerDragSignal, PointerDragState, PointerModifiers, PointerPoint, ProjectionOptions, ReconcileFn, ReconcileKey, 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 };
4108
+ export { CONCURRENCY_INSTRUMENTATION, MmActivity, MmTransition, MmViewTransitionName, OP_PROTO_VERSION, PAUSABLE_OPTIONS, PERSISTED_STORE_OPTIONS, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, applyOps, batteryStatus, bridgeScopeToPendingTasks, chunked, clipboard, combineWith, compareHlc, compareTotal, createAttributedPending, createConvergingApply, createForwardingScope, createHlcClock, createStoreContext, createTransaction, createTransitionScope, debounce, debounced, deferredValue, derived, diffOps, distinct, elementSize, elementVisibility, extendStore, filter, filterWith, focusWithin, forkStore, geolocation, getTransitionScope, holdUntilReady, idle, indexArray, injectPaused, injectRegisterResource, injectStartTransaction, injectStartTransition, injectTransitionScope, invertBatch, isConflicted, isDerivation, isLeaf, isMutable, isOpaque, isStore, keepPrevious, keyArray, keyedArray, latest, lww, map, mapArray, mapObject, mediaQuery, merge3, mergeThree, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, opLog, opSync, opaque, orientation, pageVisibility, pairwise, pausableComputed, pausableEffect, pausableSignal, perfCustomTracks, persist, persistedStore, pipeable, piped, pointerDrag, policyStrategy, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, preserve, projection, provideConcurrencyInstrumentation, provideForwardingTransitionScope, providePausableOptions, providePaused, providePersistedStoreOptions, provideTransitionScope, rebaseOps, reconcile, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, storeHistory, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, use, windowSize, withHistory };
4109
+ export type { AsyncStore, BatteryStatus, ClipboardSignal, Computation, ConcurrencyInstrumentation, Conflicted, ConvergingApply, CreateChunkedOptions, CreateDebouncedOptions, CreateHistoryOptions, CreateLatestOptions, CreateOpLogOptions, CreatePooledOptions, CreateProvidedPooledOptions, CreateStoredOptions, CreateThrottledOptions, CreateTransitionScopeOptions, DebouncedSignal, DeferStrategy, DeferredSignal, DeferredValueOptions, DerivedSignal, ElementSize, ElementSizeOptions, ElementSizeSignal, ElementVisibilityOptions, ElementVisibilitySignal, ExtendStoreOptions, Fork, ForkStoreOptions, ForkStrategy, ForwardingTransitionScope, Frame, GeolocationOptions, GeolocationSignal, Hlc, HlcClock, IdleOptions, IdleSignal, LatestSignal, MergeContext, MergeFn, MergePolicyEntry, MmTransitionContext, MousePositionOptions, MousePositionSignal, MutableSignal, MutableSignalStore, NetworkStatusSignal, OpBatch, OpEnvelope, OpLog, OpLogDriver, OpSync, OpSyncOptions, Opaque, PausableOptions, PauseOption, PersistHandle, PersistOptions, PersistedStore, PersistedStoreDefaults, PersistedStoreOptions, PipeableSignal, PointerDragOptions, PointerDragSignal, PointerDragState, PointerModifiers, PointerPoint, ProjectionOptions, RebaseResult, ReconcileFn, ReconcileKey, RegisterOptions, ResourceLike, ScreenOrientation, ScreenOrientationState, ScrollPosition, ScrollPositionOptions, ScrollPositionSignal, SensorRunOptions, SignalFromEventOptions, SignalStore, SignalWithHistory, StoreHistory, StoreHistoryOptions, StoreOp, StoreOptions, StoreTabSyncOptions, StoredSignal, SuspendType, SyncSignalOptions, ThrottledSignal, Transaction, TransactionRef, TransitionRef, TransitionScope, UntilOptions, UseSource, Vivify, WindowSize, WindowSizeOptions, WindowSizeSignal, WithVivify, WritableSignalStore, toStoreOptions };