@mmstack/primitives 19.3.10 → 19.4.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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { isDevMode, inject, Injector, untracked, effect, DestroyRef, linkedSignal, computed, signal, isSignal, PLATFORM_ID, ElementRef, Injectable, runInInjectionContext } from '@angular/core';
2
+ import { isDevMode, inject, Injector, untracked, effect, DestroyRef, linkedSignal, InjectionToken, TemplateRef, ViewContainerRef, input, computed, Directive, signal, PLATFORM_ID, runInInjectionContext, ResourceStatus, afterNextRender, Component, isSignal, ElementRef, Injectable } from '@angular/core';
3
3
  import { isPlatformServer } from '@angular/common';
4
4
  import { SIGNAL } from '@angular/core/primitives/signals';
5
5
 
@@ -214,6 +214,559 @@ function chunked(source, options) {
214
214
  return internal.asReadonly();
215
215
  }
216
216
 
217
+ /**
218
+ * Whether the subtree a resource/component lives in is currently PAUSED, for Activity / keep-alive.
219
+ * Provided by an Activity boundary (`MmActivity`, or the app-builder's per-branch injector) and read
220
+ * — only at instantiation — by anything that should pause its background work while paused (a resource
221
+ * returning its `paused` token, a `<video>` pausing playback, the pausable primitives, …). Absent
222
+ * unless an Activity boundary provides one — read it via `injectPaused()`, which falls back to a
223
+ * never-paused signal, so code that isn't inside an Activity boundary is unaffected.
224
+ */
225
+ const PAUSED_CONTEXT = new InjectionToken('@mmstack/primitives:paused-context');
226
+ /**
227
+ * Keep-alive (the Angular analog of React's `<Activity>` / Vue's `<keep-alive>`): the wrapped
228
+ * subtree is mounted ONCE and kept — when `[mmActivity]` is false it's hidden (`display:none`) and
229
+ * its change detection is paused, preserving state (scroll, inputs, a video's position, loaded
230
+ * data); when true it's shown and CD resumes. It is never destroyed until the directive is.
231
+ *
232
+ * It also provides {@link PAUSED_CONTEXT} to the content (= the negation of `visible`), so descendants
233
+ * can pause *effect-driven* or *Observable* work while hidden (CD-detach alone pauses pull-based/template work, not
234
+ * effects/polling). If you're using the pausable primitives this is done automatically
235
+ *
236
+ * ```html
237
+ * <section *mmActivity="tab() === 'editor'"> ...heavy stateful editor... </section>
238
+ * ```
239
+ */
240
+ class MmActivity {
241
+ tpl = inject(TemplateRef);
242
+ vcr = inject(ViewContainerRef);
243
+ parent = inject(Injector);
244
+ /** When false, keep the content mounted but hidden + CD-detached. */
245
+ visible = input.required({ alias: 'mmActivity' });
246
+ /** Paused == not visible — handed to the kept subtree as PAUSED_CONTEXT. */
247
+ paused = computed(() => !this.visible());
248
+ view = null;
249
+ constructor() {
250
+ effect(() => {
251
+ const visible = this.visible();
252
+ untracked(() => this.apply(visible));
253
+ });
254
+ }
255
+ apply(visible) {
256
+ if (!this.view) {
257
+ // Created once, kept for the directive's lifetime. The content gets PAUSED_CONTEXT = !visible,
258
+ // so resources/components inside can pause their effect-driven work while hidden.
259
+ this.view = this.vcr.createEmbeddedView(this.tpl, {}, {
260
+ injector: Injector.create({
261
+ parent: this.parent,
262
+ providers: [providePaused(this.paused)],
263
+ }),
264
+ });
265
+ }
266
+ for (const node of this.view.rootNodes) {
267
+ if (node instanceof HTMLElement)
268
+ node.style.display = visible ? '' : 'none';
269
+ }
270
+ if (visible)
271
+ this.view.reattach();
272
+ else
273
+ this.view.detach();
274
+ }
275
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: MmActivity, deps: [], target: i0.ɵɵFactoryTarget.Directive });
276
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.19", type: MmActivity, isStandalone: true, selector: "[mmActivity]", inputs: { visible: { classPropertyName: "visible", publicName: "mmActivity", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
277
+ }
278
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: MmActivity, decorators: [{
279
+ type: Directive,
280
+ args: [{
281
+ selector: '[mmActivity]',
282
+ }]
283
+ }], ctorParameters: () => [] });
284
+ // Shared never-paused signal returned outside a boundary / on the server (SSR renders the full tree,
285
+ // nothing is paused). Readonly so a consumer can't cast-and-`.set()` the shared default for everyone.
286
+ const NEVER_PAUSED = signal(false).asReadonly();
287
+ /**
288
+ * Inject the nearest paused-state signal — `true` while the surrounding subtree is paused (hidden by
289
+ * an Activity boundary). Defaults to a never-paused signal, so callers outside an Activity are
290
+ * unaffected; on the server it is always never-paused, so server-side work (e.g. connector fetches)
291
+ * isn't suppressed. This is the public way to read pause state; the underlying token is intentionally
292
+ * not exported.
293
+ */
294
+ function injectPaused() {
295
+ if (isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser'))
296
+ return NEVER_PAUSED;
297
+ return inject(PAUSED_CONTEXT, { optional: true }) ?? NEVER_PAUSED;
298
+ }
299
+ /**
300
+ * Build a provider that supplies a paused-state signal to a subtree — the public way to set up an
301
+ * Activity-style pause boundary (used by `MmActivity` and the app-builder's per-branch injectors).
302
+ */
303
+ function providePaused(source) {
304
+ return { provide: PAUSED_CONTEXT, useValue: source };
305
+ }
306
+
307
+ /**
308
+ * Structural hold-and-swap as a signal. Given a `target` (the desired value — e.g. the
309
+ * subtree/def/key you want to show) and a `ready` predicate, returns a signal that keeps
310
+ * yielding its PREVIOUS value until `ready()` is true, then swaps to the current target.
311
+ *
312
+ * This is the structural counterpart to `keepPrevious`/`commit`: where those hold a *value*
313
+ * through a reload, this holds a *structure* through a swap. The caller mounts the incoming
314
+ * structure off to the side (so its resources can settle and flip `ready`), keeps showing the
315
+ * held previous structure meanwhile, and lets the old one go once `ready` releases the swap.
316
+ *
317
+ * The very first value passes straight through (nothing to hold yet).
318
+ */
319
+ function holdUntilReady(target, ready) {
320
+ return linkedSignal({
321
+ source: () => ({ t: target(), ready: ready() }),
322
+ computation: (curr, prev) => (prev === undefined || curr.ready ? curr.t : prev.value),
323
+ });
324
+ }
325
+
326
+ /**
327
+ * Resolve a {@link PauseOption} into a pause predicate, or `null` meaning "do not pause".
328
+ * `null` tells the caller to return the bare primitive — no wrapper is created.
329
+ *
330
+ * - omitted/`true` → the ambient {@link PAUSED_CONTEXT} if an Activity boundary provides one (via
331
+ * `opt.injector` or the current injection context), else `null` (the bare primitive, no allocation).
332
+ * The default, because an explicit `pausable*` call wants to be pausable. An explicit `pause: true`
333
+ * with no boundary dev-warns; the omitted default stays quiet. SSR → `null`.
334
+ * - a function → returned as-is (covers `Signal<boolean>`; usable outside an injection context).
335
+ * SSR → `null` here too, detected via `opt.injector` if given, else a `globalThis.window` probe.
336
+ * - `false` → `null` (the explicit opt-out).
337
+ *
338
+ * Encapsulating this here keeps every pausable primitive's branching identical and in one place.
339
+ */
340
+ function resolvePause(opt) {
341
+ const explicit = opt?.pause; // distinguish explicit `true` from the omitted default
342
+ const pause = explicit ?? true; // explicit pausable* calls default to pausing
343
+ if (pause === false)
344
+ return null;
345
+ const run = (fn) => opt?.injector ? runInInjectionContext(opt.injector, fn) : fn();
346
+ const onServer = () => typeof pause === 'function' && !opt?.injector
347
+ ? typeof globalThis.window === 'undefined'
348
+ : run(() => isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser'));
349
+ if (typeof pause === 'function')
350
+ return onServer() ? null : pause;
351
+ if (onServer())
352
+ return null;
353
+ const paused = run(() => inject(PAUSED_CONTEXT, { optional: true }));
354
+ if (!paused) {
355
+ if (explicit === true && isDevMode())
356
+ console.warn('[pausable] `pause: true` but no PAUSED_CONTEXT in scope — not pausing. Provide one via an ' +
357
+ 'Activity boundary (`MmActivity` / `providePaused`), or pass a predicate / `pause: false`.');
358
+ return null;
359
+ }
360
+ return paused;
361
+ }
362
+ /**
363
+ * Like {@link nestedEffect}, but pausable. While paused the effect does NOT run its body — and,
364
+ * crucially, it reads the pause predicate FIRST, so while paused its dependency set collapses to just
365
+ * the predicate (no churn from the real deps); on resume it re-runs and re-tracks. With no `pause`
366
+ * option it defaults to the ambient `PAUSED_CONTEXT`; `pause: false` makes it a plain `nestedEffect`
367
+ * with zero added overhead.
368
+ */
369
+ function pausableEffect(effectFn, options) {
370
+ const paused = resolvePause(options);
371
+ if (!paused)
372
+ return nestedEffect(effectFn, options);
373
+ return nestedEffect((registerCleanup) => {
374
+ if (paused())
375
+ return; // read FIRST → while paused, deps collapse to just the predicate
376
+ effectFn(registerCleanup);
377
+ }, options);
378
+ }
379
+ /**
380
+ * Like `signal`, but pausable. While paused, READS hold the last value; writes still land on the
381
+ * underlying signal and surface on resume. Built on the `keepPrevious`/`hold` shape — a
382
+ * `linkedSignal` gated on the pause predicate, with `set`/`update`/`asReadonly` forwarded to the
383
+ * source signal. With no `pause` option it defaults to the ambient `PAUSED_CONTEXT`; `pause: false`
384
+ * makes it a plain `signal` — no `linkedSignal` is created.
385
+ *
386
+ * NOTE: while paused, `set(x)` followed by a read returns the *held* (pre-pause) value, not `x` — the
387
+ * write lands on the source and surfaces on resume. That is the "freeze the displayed value while
388
+ * hidden" semantics; do not rely on read-after-write while paused.
389
+ */
390
+ function pausableSignal(initialValue, options) {
391
+ const paused = resolvePause(options);
392
+ const src = signal(initialValue, options);
393
+ if (!paused)
394
+ return src;
395
+ const read = linkedSignal({
396
+ source: () => ({ v: src(), paused: paused() }),
397
+ computation: (curr, prev) => prev !== undefined && curr.paused ? prev.value : curr.v,
398
+ equal: options?.equal,
399
+ });
400
+ read.set = src.set;
401
+ read.update = src.update;
402
+ read.asReadonly = src.asReadonly;
403
+ return read;
404
+ }
405
+ /**
406
+ * Like `computed`, but pausable. While paused it holds its last value AND does not recompute: the
407
+ * computation's dependencies are not read while paused, so a dependency change can't trigger work —
408
+ * on resume it recomputes and re-tracks. The very first read always computes, to seed a value. With
409
+ * no `pause` option it defaults to the ambient `PAUSED_CONTEXT`; `pause: false` makes it a plain
410
+ * `computed`.
411
+ */
412
+ function pausableComputed(computation, options) {
413
+ const paused = resolvePause(options);
414
+ if (!paused)
415
+ return computed(computation, options);
416
+ const HELD = Symbol('paused-hold');
417
+ const ls = linkedSignal({
418
+ source: () => (paused() ? HELD : computation()),
419
+ computation: (next, prev) => next !== HELD ? next : prev !== undefined ? prev.value : computation(),
420
+ equal: options?.equal,
421
+ });
422
+ return ls.asReadonly();
423
+ }
424
+
425
+ const { is } = Object;
426
+ function mutable(initial, opt) {
427
+ const baseEqual = opt?.equal ?? is;
428
+ let cnt = 0;
429
+ const equal = (a, b) => {
430
+ if (cnt > 0)
431
+ return false;
432
+ return baseEqual(a, b);
433
+ };
434
+ const sig = signal(initial, {
435
+ ...opt,
436
+ equal,
437
+ });
438
+ const internalUpdate = sig.update;
439
+ sig.mutate = (updater) => {
440
+ cnt++;
441
+ internalUpdate(updater);
442
+ cnt--;
443
+ };
444
+ sig.inline = (updater) => {
445
+ sig.mutate((prev) => {
446
+ updater(prev);
447
+ return prev;
448
+ });
449
+ };
450
+ return sig;
451
+ }
452
+ /**
453
+ * Type guard function to check if a given `WritableSignal` is a `MutableSignal`. This is useful
454
+ * for situations where you need to conditionally use the `mutate` or `inline` methods.
455
+ *
456
+ * @typeParam T - The type of the signal's value (optional, defaults to `any`).
457
+ * @param value - The `WritableSignal` to check.
458
+ * @returns `true` if the signal is a `MutableSignal`, `false` otherwise.
459
+ *
460
+ * @example
461
+ * const mySignal = signal(0);
462
+ * const myMutableSignal = mutable(0);
463
+ *
464
+ * if (isMutable(mySignal)) {
465
+ * mySignal.mutate(x => x + 1); // This would cause a type error, as mySignal is not a MutableSignal.
466
+ * }
467
+ *
468
+ * if (isMutable(myMutableSignal)) {
469
+ * myMutableSignal.mutate(x => x + 1); // This is safe.
470
+ * }
471
+ */
472
+ function isMutable(value) {
473
+ return 'mutate' in value && typeof value.mutate === 'function';
474
+ }
475
+
476
+ function createTransitionScope() {
477
+ const list = mutable([]);
478
+ const pending = computed(() => list().some(({ ref }) => {
479
+ const s = ref.status();
480
+ return s === ResourceStatus.Loading || s === ResourceStatus.Reloading;
481
+ }));
482
+ const holdCount = signal(0);
483
+ const holding = computed(() => holdCount() > 0);
484
+ return {
485
+ resources: computed(() => list().map((e) => e.ref)),
486
+ pending,
487
+ suspended: (type) => list().some(({ ref, suspends }) => suspends && (type === 'loading' ? ref.isLoading() : !ref.hasValue())),
488
+ add: (ref, opt) => untracked(() => list.inline((c) => c.push({ ref, suspends: opt?.suspends ?? true }))),
489
+ remove: (ref) => untracked(() => list.inline((c) => {
490
+ const i = c.findIndex((e) => e.ref === ref);
491
+ if (i !== -1)
492
+ c.splice(i, 1);
493
+ })),
494
+ commit: (value) => linkedSignal({
495
+ source: () => ({ v: value(), settled: !pending() }),
496
+ computation: (curr, prev) => curr.settled || prev === undefined ? curr.v : prev.value,
497
+ }),
498
+ holding,
499
+ beginHold: () => untracked(() => holdCount.update((c) => c + 1)),
500
+ endHold: () => untracked(() => holdCount.update((c) => (c > 0 ? c - 1 : 0))),
501
+ hold: (value) => linkedSignal({
502
+ source: () => ({ v: value(), held: holding() }),
503
+ computation: (curr, prev) => prev !== undefined && curr.held ? prev.value : curr.v,
504
+ }),
505
+ };
506
+ }
507
+ function createNoopScope() {
508
+ return {
509
+ resources: computed(() => []),
510
+ pending: computed(() => false),
511
+ suspended: () => false,
512
+ add: () => {
513
+ // noop
514
+ },
515
+ remove: () => {
516
+ // noop
517
+ },
518
+ commit: (value) => value,
519
+ holding: computed(() => false),
520
+ beginHold: () => {
521
+ // noop
522
+ },
523
+ endHold: () => {
524
+ // noop
525
+ },
526
+ hold: (value) => value,
527
+ };
528
+ }
529
+ const TRANSITION_SCOPE = new InjectionToken('@mmstack/resource:transition-scope');
530
+ /** Provide a fresh transition scope at a boundary so its subtree's resources are tracked independently. */
531
+ function provideTransitionScope() {
532
+ return { provide: TRANSITION_SCOPE, useFactory: createTransitionScope };
533
+ }
534
+ function injectTransitionScope() {
535
+ const scope = inject(TRANSITION_SCOPE, { optional: true });
536
+ if (!scope) {
537
+ if (isDevMode())
538
+ console.warn('[mmstack/resource] No transition scope in context — registration/tracking here is a no-op. ' +
539
+ 'Use a <mm-suspense> boundary or provideTransitionScope() in an ancestor.');
540
+ return createNoopScope();
541
+ }
542
+ return scope;
543
+ }
544
+ /**
545
+ * Returns a register function bound to the nearest transition scope: it adds a resource
546
+ * to the scope and removes it when the caller's injection context is destroyed. Pass any
547
+ * `ResourceRef` (a query, mutation, or plain Angular resource) through it.
548
+ */
549
+ function injectRegisterResource() {
550
+ const scope = injectTransitionScope();
551
+ const destroyRef = inject(DestroyRef);
552
+ return (res, opt) => {
553
+ scope.add(res, opt);
554
+ destroyRef.onDestroy(() => scope.remove(res));
555
+ return res;
556
+ };
557
+ }
558
+ /** Convenience: register a resource with the nearest transition scope. Must run in an injection context. */
559
+ function registerResource(res, opt) {
560
+ return injectRegisterResource()(res, opt);
561
+ }
562
+
563
+ /**
564
+ * Returns a `startTransition(fn)` bound to the nearest transition scope. `fn` runs its state
565
+ * mutations (which commit immediately); any resource that reloads as a result holds its value
566
+ * (when `coordinate`/`commit`-wrapped) and reveals together once everything settles. The
567
+ * returned handle exposes a unified `pending` + `done` for the whole operation — for imperative
568
+ * coordination (disable a control, await completion) on top of the declarative hold-and-commit.
569
+ *
570
+ * Must be called in an injection context. This is the *async* generalization (Tier 2): it adds
571
+ * no rendering cost and needs no fork — holding direct/sync readers is a separate, deferred tier.
572
+ */
573
+ function injectStartTransition() {
574
+ const scope = injectTransitionScope();
575
+ const injector = inject(Injector);
576
+ return (fn) => {
577
+ untracked(fn);
578
+ let sawPending = false;
579
+ const done = new Promise((resolve) => {
580
+ const watcher = effect(() => {
581
+ const p = scope.pending();
582
+ if (p)
583
+ sawPending = true;
584
+ // settle: requests went in flight and then drained
585
+ if (sawPending && !p) {
586
+ watcher.destroy();
587
+ resolve();
588
+ }
589
+ }, { injector });
590
+ // no-async fallback: once the reactive system has processed the writes (afterNextRender),
591
+ // if nothing ever went in flight, the transition is already complete.
592
+ afterNextRender(() => {
593
+ if (!sawPending && !untracked(scope.pending)) {
594
+ watcher.destroy();
595
+ resolve();
596
+ }
597
+ }, { injector });
598
+ });
599
+ return { pending: scope.pending, done };
600
+ };
601
+ }
602
+
603
+ /**
604
+ * Shared **suspense** (readiness) boundary behaviour: reads the *nearest* transition scope and exposes
605
+ * its `pending`/`suspended` state. This is the readiness gate — distinct from the hold-stale *swap*
606
+ * primitives (`TransitionRouterOutlet`, `ab-transition`), which are the actual "transitions". The two
607
+ * concrete components below differ only by whether they provide their own scope, so the logic (and
608
+ * template) live here once.
609
+ *
610
+ * - **First load** (`suspended()`): no value yet → show the `[placeholder]` fallback.
611
+ * - **Reload** (`pending()` but a value is held via `keepPrevious`): keep the real content mounted and
612
+ * surface a busy indicator (`aria-busy`, and an optional `[busy]` slot) instead of flashing back to
613
+ * the placeholder.
614
+ *
615
+ * `type` selects what "not ready" means: `'value'` (default) suspends only until a first value lands
616
+ * then holds through reloads; `'loading'` suspends on every in-flight load (strict suspense).
617
+ */
618
+ class SuspenseBoundaryBase {
619
+ scope = injectTransitionScope();
620
+ /** What counts as "not ready" for the first-load placeholder. Defaults to value-presence. */
621
+ type = input('value');
622
+ pending = this.scope.pending;
623
+ suspended = computed(() => this.scope.suspended(this.type()));
624
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: SuspenseBoundaryBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
625
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.19", type: SuspenseBoundaryBase, isStandalone: true, inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
626
+ }
627
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: SuspenseBoundaryBase, decorators: [{
628
+ type: Directive
629
+ }] });
630
+ const SUSPENSE_TEMPLATE = `
631
+ @if (suspended()) {
632
+ <ng-content select="[placeholder]"><span>Loading…</span></ng-content>
633
+ } @else {
634
+ @if (pending()) {
635
+ <ng-content select="[busy]" />
636
+ }
637
+ <ng-content />
638
+ }
639
+ `;
640
+ // `display: contents` so the boundary adds no box of its own.
641
+ const SUSPENSE_STYLES = `
642
+ :host {
643
+ display: contents;
644
+ }
645
+ `;
646
+ const SUSPENSE_HOST = {
647
+ '[attr.aria-busy]': 'pending() ? true : null',
648
+ };
649
+ /**
650
+ * Standalone suspense boundary — **provides its own scope**, so dropping a `<mm-suspense>` anywhere
651
+ * just works: the resources created in its subtree register into it without any extra
652
+ * `provideTransitionScope()`. The common case.
653
+ */
654
+ class SuspenseBoundary extends SuspenseBoundaryBase {
655
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: SuspenseBoundary, deps: null, target: i0.ɵɵFactoryTarget.Component });
656
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.19", type: SuspenseBoundary, isStandalone: true, selector: "mm-suspense", host: { properties: { "attr.aria-busy": "pending() ? true : null" } }, providers: [provideTransitionScope()], usesInheritance: true, ngImport: i0, template: "\n @if (suspended()) {\n <ng-content select=\"[placeholder]\"><span>Loading\u2026</span></ng-content>\n } @else {\n @if (pending()) {\n <ng-content select=\"[busy]\" />\n }\n <ng-content />\n }\n", isInline: true, styles: [":host{display:contents}\n"] });
657
+ }
658
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: SuspenseBoundary, decorators: [{
659
+ type: Component,
660
+ args: [{ selector: 'mm-suspense', template: SUSPENSE_TEMPLATE, host: SUSPENSE_HOST, providers: [provideTransitionScope()], styles: [":host{display:contents}\n"] }]
661
+ }] });
662
+ /**
663
+ * Unscoped suspense boundary — **reads the ambient scope** instead of providing one. For cases where
664
+ * the resources to coordinate are registered *above* the boundary (e.g. an app-builder page whose
665
+ * manifests/connectors register at a higher injector), so the boundary observes that outer scope
666
+ * rather than opening a fresh one. Pair with a `provideTransitionScope()` (or another boundary) in an
667
+ * ancestor.
668
+ */
669
+ class UnscopedSuspenseBoundary extends SuspenseBoundaryBase {
670
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: UnscopedSuspenseBoundary, deps: null, target: i0.ɵɵFactoryTarget.Component });
671
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.19", type: UnscopedSuspenseBoundary, isStandalone: true, selector: "mm-unscoped-suspense", host: { properties: { "attr.aria-busy": "pending() ? true : null" } }, usesInheritance: true, ngImport: i0, template: "\n @if (suspended()) {\n <ng-content select=\"[placeholder]\"><span>Loading\u2026</span></ng-content>\n } @else {\n @if (pending()) {\n <ng-content select=\"[busy]\" />\n }\n <ng-content />\n }\n", isInline: true, styles: [":host{display:contents}\n"] });
672
+ }
673
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: UnscopedSuspenseBoundary, decorators: [{
674
+ type: Component,
675
+ args: [{ selector: 'mm-unscoped-suspense', template: SUSPENSE_TEMPLATE, host: SUSPENSE_HOST, styles: [":host{display:contents}\n"] }]
676
+ }] });
677
+
678
+ function createTransaction() {
679
+ const log = new Map();
680
+ return {
681
+ record: (sig) => {
682
+ if (!log.has(sig))
683
+ log.set(sig, untracked(sig));
684
+ },
685
+ restore: () => untracked(() => {
686
+ for (const [sig, old] of log)
687
+ sig.set(old);
688
+ log.clear();
689
+ }),
690
+ clear: () => log.clear(),
691
+ };
692
+ }
693
+ // The currently-active transaction, set only for the synchronous duration of a `startTransaction`
694
+ // body (so stateful actions running inside it can record their writes). Module-level + sync
695
+ // set/reset is the honest shape: a transaction is call-scoped, not structural-per-injector.
696
+ let active = null;
697
+ /** The transaction in effect right now, or `null`. Stateful actions consult this to record undo. */
698
+ function activeTransaction() {
699
+ return active;
700
+ }
701
+ function runInTransaction(txn, fn) {
702
+ const prev = active;
703
+ active = txn;
704
+ try {
705
+ untracked(fn);
706
+ }
707
+ finally {
708
+ active = prev;
709
+ }
710
+ }
711
+ /**
712
+ * Returns a `startTransaction(fn)` bound to the nearest transition scope — the Tier 3 sibling of
713
+ * `injectStartTransition`. It HOLDS the scope's synchronous display reads from before `fn` runs
714
+ * (so a state write inside `fn` doesn't flash through), records those writes in an undo log, then:
715
+ * - on settle (the scope's resources go in flight and drain) → release the hold + keep the writes;
716
+ * - on `abort()` → roll the writes back and release the hold.
717
+ *
718
+ * The writes land on LIVE state immediately (so derived variables and connector requests see the
719
+ * new values and refetch); only the *display* is held, via `scope.hold`. Must run in an injection
720
+ * context.
721
+ */
722
+ function injectStartTransaction() {
723
+ const scope = injectTransitionScope();
724
+ const injector = inject(Injector);
725
+ return (fn) => {
726
+ const txn = createTransaction();
727
+ // Hold BEFORE the writes, so the display freezes at pre-transaction values.
728
+ scope.beginHold();
729
+ let finished = false;
730
+ let watcher;
731
+ const finish = (restore) => {
732
+ if (finished)
733
+ return;
734
+ finished = true;
735
+ watcher?.destroy();
736
+ if (restore)
737
+ txn.restore();
738
+ else
739
+ txn.clear();
740
+ scope.endHold();
741
+ };
742
+ runInTransaction(txn, fn);
743
+ let sawPending = false;
744
+ const done = new Promise((resolve) => {
745
+ watcher = effect(() => {
746
+ const p = scope.pending();
747
+ if (p)
748
+ sawPending = true;
749
+ if (sawPending && !p) {
750
+ finish(false);
751
+ resolve();
752
+ }
753
+ }, { injector });
754
+ // no-async fallback: if nothing ever went in flight, settle once the writes are processed.
755
+ afterNextRender(() => {
756
+ if (!sawPending && !untracked(scope.pending)) {
757
+ finish(false);
758
+ resolve();
759
+ }
760
+ }, { injector });
761
+ });
762
+ return {
763
+ pending: scope.pending,
764
+ done,
765
+ abort: () => finish(true),
766
+ };
767
+ };
768
+ }
769
+
217
770
  /**
218
771
  * Converts a read-only `Signal` into a `WritableSignal` by providing custom `set` and, optionally, `update` functions.
219
772
  * This can be useful for creating controlled write access to a signal that is otherwise read-only.
@@ -348,57 +901,6 @@ function debounce(source, opt) {
348
901
  return writable;
349
902
  }
350
903
 
351
- const { is } = Object;
352
- function mutable(initial, opt) {
353
- const baseEqual = opt?.equal ?? is;
354
- let cnt = 0;
355
- const equal = (a, b) => {
356
- if (cnt > 0)
357
- return false;
358
- return baseEqual(a, b);
359
- };
360
- const sig = signal(initial, {
361
- ...opt,
362
- equal,
363
- });
364
- const internalUpdate = sig.update;
365
- sig.mutate = (updater) => {
366
- cnt++;
367
- internalUpdate(updater);
368
- cnt--;
369
- };
370
- sig.inline = (updater) => {
371
- sig.mutate((prev) => {
372
- updater(prev);
373
- return prev;
374
- });
375
- };
376
- return sig;
377
- }
378
- /**
379
- * Type guard function to check if a given `WritableSignal` is a `MutableSignal`. This is useful
380
- * for situations where you need to conditionally use the `mutate` or `inline` methods.
381
- *
382
- * @typeParam T - The type of the signal's value (optional, defaults to `any`).
383
- * @param value - The `WritableSignal` to check.
384
- * @returns `true` if the signal is a `MutableSignal`, `false` otherwise.
385
- *
386
- * @example
387
- * const mySignal = signal(0);
388
- * const myMutableSignal = mutable(0);
389
- *
390
- * if (isMutable(mySignal)) {
391
- * mySignal.mutate(x => x + 1); // This would cause a type error, as mySignal is not a MutableSignal.
392
- * }
393
- *
394
- * if (isMutable(myMutableSignal)) {
395
- * myMutableSignal.mutate(x => x + 1); // This is safe.
396
- * }
397
- */
398
- function isMutable(value) {
399
- return 'mutate' in value && typeof value.mutate === 'function';
400
- }
401
-
402
904
  /**
403
905
  * @internal
404
906
  * Type guard for an array-index-like property key: a non-empty string that parses to a finite
@@ -622,7 +1124,7 @@ function isDerivation(sig) {
622
1124
  return 'from' in sig;
623
1125
  }
624
1126
 
625
- function isWritableSignal(value) {
1127
+ function isWritableSignal$1(value) {
626
1128
  return 'set' in value && typeof value.set === 'function';
627
1129
  }
628
1130
  /**
@@ -632,7 +1134,7 @@ function isWritableSignal(value) {
632
1134
  * @returns
633
1135
  */
634
1136
  function createSetter(source) {
635
- if (!isWritableSignal(source))
1137
+ if (!isWritableSignal$1(source))
636
1138
  return () => {
637
1139
  // noop;
638
1140
  };
@@ -648,6 +1150,27 @@ function createSetter(source) {
648
1150
  };
649
1151
  }
650
1152
 
1153
+ function keepPrevious(src, opt) {
1154
+ const persisted = linkedSignal({
1155
+ ...opt,
1156
+ source: () => src(),
1157
+ computation: (next, prev) => next === undefined && prev !== undefined ? prev.value : next,
1158
+ });
1159
+ if (isWritableSignal$1(src)) {
1160
+ persisted.set = src.set;
1161
+ persisted.update = src.update;
1162
+ persisted.asReadonly = src.asReadonly;
1163
+ if (isMutable(src)) {
1164
+ persisted.mutate = src.mutate;
1165
+ persisted.inline = src.inline;
1166
+ }
1167
+ if (isDerivation(src)) {
1168
+ persisted.from = src.from;
1169
+ }
1170
+ }
1171
+ return persisted;
1172
+ }
1173
+
651
1174
  /**
652
1175
  * Helper to create the derived signal for a specific index.
653
1176
  * Extracts the cast logic to keep the main loop clean.
@@ -665,12 +1188,12 @@ function indexArray(source, map, opt = {}) {
665
1188
  const data = isSignal(source) ? source : computed(source);
666
1189
  const len = computed(() => data().length);
667
1190
  const setter = createSetter(data);
668
- const writableData = isWritableSignal(data)
1191
+ const writableData = isWritableSignal$1(data)
669
1192
  ? data
670
1193
  : toWritable(data, () => {
671
1194
  // noop
672
1195
  });
673
- if (isWritableSignal(data) && isMutable(data) && !opt.equal) {
1196
+ if (isWritableSignal$1(data) && isMutable(data) && !opt.equal) {
674
1197
  opt.equal = (a, b) => {
675
1198
  if (typeof a !== typeof b)
676
1199
  return false;
@@ -880,7 +1403,7 @@ function pooledKeys(src) {
880
1403
  }
881
1404
  function mapObject(source, mapFn, options = {}) {
882
1405
  const src = isSignal(source) ? source : computed(source);
883
- const writable = (isWritableSignal(src)
1406
+ const writable = (isWritableSignal$1(src)
884
1407
  ? src
885
1408
  : toWritable(src, () => {
886
1409
  // noop
@@ -2402,6 +2925,9 @@ function signalFromEvent(target, eventName, initial, projectOrOpt, maybeOpt) {
2402
2925
  return untracked(() => state.asReadonly());
2403
2926
  }
2404
2927
 
2928
+ function isWritableSignal(value) {
2929
+ return isSignal(value) && typeof value.set === 'function';
2930
+ }
2405
2931
  /**
2406
2932
  * Runtime marker + compile-time brand for an opaque value. A `const`-declared `Symbol`
2407
2933
  * has a `unique symbol` type, so the same symbol serves as both the property key written
@@ -2443,6 +2969,82 @@ function isOpaque(value) {
2443
2969
  value !== null &&
2444
2970
  value[OPAQUE] === true);
2445
2971
  }
2972
+ /**
2973
+ * @internal Runtime brand carrying a store node's lazily-built leaf probe. Exported (like
2974
+ * {@link OPAQUE}) only so the `{ readonly [LEAF]: () => boolean }` brand on the store types is
2975
+ * nameable in the emitted declarations — not part of the supported surface; use {@link isLeaf}.
2976
+ */
2977
+ const LEAF = Symbol('@mmstack/primitives::store/LEAF');
2978
+ /**
2979
+ * @internal Whether a value is a terminal leaf: a concrete non-record/non-array value always is;
2980
+ * `null`/`undefined` is a leaf only when vivification is disabled (with vivify on it can still
2981
+ * materialize a container, so it stays a descendable substore).
2982
+ */
2983
+ function isLeafValue(value, vivifyEnabled) {
2984
+ if (value == null)
2985
+ return !vivifyEnabled;
2986
+ if (isOpaque(value))
2987
+ return true; // opaque always wins — even arrays
2988
+ return !Array.isArray(value) && !isRecord(value);
2989
+ }
2990
+ /**
2991
+ * @internal Constant leaf probes for nodes whose leaf-ness is statically known, so the reactive
2992
+ * `computed` can be skipped entirely.
2993
+ */
2994
+ function alwaysTrue() {
2995
+ return true;
2996
+ }
2997
+ function alwaysFalse() {
2998
+ return false;
2999
+ }
3000
+ /**
3001
+ * @internal Attaches a lazy, memoized leaf probe to a store node. The probe (`() => boolean`)
3002
+ * closes over the node's value signal and its (stable) vivify setting, building the backing
3003
+ * `computed` on first call so leaf-ness tracks the live value reactively without taxing every
3004
+ * node access. Idempotent.
3005
+ */
3006
+ function markAsLeaf(sig, value, vivifyEnabled, noUnionLeaves) {
3007
+ if (typeof sig[LEAF] !== 'function') {
3008
+ let memo;
3009
+ const probe = () => {
3010
+ if (memo)
3011
+ return memo();
3012
+ const v = untracked(value);
3013
+ memo =
3014
+ isOpaque(v) || (v == null && !vivifyEnabled) || noUnionLeaves
3015
+ ? isLeafValue(v, vivifyEnabled)
3016
+ ? alwaysTrue
3017
+ : alwaysFalse
3018
+ : computed(() => isLeafValue(value(), vivifyEnabled));
3019
+ return memo();
3020
+ };
3021
+ Object.defineProperty(sig, LEAF, {
3022
+ value: probe,
3023
+ enumerable: false,
3024
+ configurable: true,
3025
+ });
3026
+ }
3027
+ return sig;
3028
+ }
3029
+ /**
3030
+ * Reports whether a store node is currently a **leaf** — a terminal value the store does not
3031
+ * descend into (a primitive, `Date`, `RegExp`, {@link opaque} object, class instance, or a
3032
+ * `null`/`undefined` hole when vivification is off) rather than a record/array substore.
3033
+ *
3034
+ * Leaf-ness reflects the node's **live** value: the probe is reactive and memoized, so calling
3035
+ * `isLeaf` inside a `computed`/`effect` re-evaluates when the node's shape changes.
3036
+ *
3037
+ * @internal Exposed for advanced/niche interop only — not part of the supported public surface
3038
+ * and may change without a major version bump.
3039
+ *
3040
+ * @example
3041
+ * const s = store({ name: 'Ada', address: { city: 'London' } });
3042
+ * isLeaf(s.name); // true
3043
+ * isLeaf(s.address); // false — a substore
3044
+ */
3045
+ function isLeaf(value) {
3046
+ return isStore(value) && value[LEAF]?.() === true;
3047
+ }
2446
3048
  const IS_STORE = Symbol('@mmstack/primitives::store/IS_STORE');
2447
3049
  const SCOPE_PARENT = Symbol('@mmstack/primitives::store/SCOPE_PARENT');
2448
3050
  /**
@@ -2505,7 +3107,7 @@ function hasOwnKey(value, key) {
2505
3107
  * @internal
2506
3108
  * Makes an array store
2507
3109
  */
2508
- function toArrayStore(source, injector, vivify) {
3110
+ function toArrayStore(source, injector, vivify, noUnionLeaves = false) {
2509
3111
  if (isStore(source))
2510
3112
  return source;
2511
3113
  const isMutableSource = isMutable(source);
@@ -2615,9 +3217,10 @@ function toArrayStore(source, injector, vivify) {
2615
3217
  });
2616
3218
  const childSample = untracked(computation);
2617
3219
  const childVivify = resolveVivify(childSample, vivify);
2618
- const proxy = Array.isArray(childSample)
2619
- ? toArrayStore(computation, injector, childVivify)
2620
- : toStore(computation, injector, childVivify);
3220
+ const proxy = Array.isArray(childSample) && !isOpaque(childSample)
3221
+ ? toArrayStore(computation, injector, childVivify, noUnionLeaves)
3222
+ : toStore(computation, injector, childVivify, noUnionLeaves);
3223
+ markAsLeaf(proxy, computation, childVivify !== false, noUnionLeaves);
2621
3224
  const ref = new WeakRef(proxy);
2622
3225
  storeCache.set(idx, ref);
2623
3226
  PROXY_CLEANUP.register(proxy, { target, prop: idx }, ref);
@@ -2634,7 +3237,7 @@ function toArrayStore(source, injector, vivify) {
2634
3237
  * const state = store({ user: { name: 'John' } });
2635
3238
  * const nameSignal = state.user.name; // WritableSignal<string>
2636
3239
  */
2637
- function toStore(source, injector, vivify = false) {
3240
+ function toStore(source, injector, vivify = false, noUnionLeaves = false) {
2638
3241
  if (isStore(source))
2639
3242
  return source;
2640
3243
  if (!injector)
@@ -2675,7 +3278,7 @@ function toStore(source, injector, vivify = false) {
2675
3278
  return () => {
2676
3279
  if (!isWritableSource)
2677
3280
  return s;
2678
- return untracked(() => toStore(source.asReadonly(), injector, vivify));
3281
+ return untracked(() => toStore(source.asReadonly(), injector, vivify, noUnionLeaves));
2679
3282
  };
2680
3283
  if (prop === 'extend')
2681
3284
  return (seed) => scopedStore(s, seed, isMutableSource
@@ -2728,9 +3331,10 @@ function toStore(source, injector, vivify = false) {
2728
3331
  });
2729
3332
  const childSample = untracked(computation);
2730
3333
  const childVivify = resolveVivify(childSample, vivify);
2731
- const proxy = Array.isArray(childSample)
2732
- ? toArrayStore(computation, injector, childVivify)
2733
- : toStore(computation, injector, childVivify);
3334
+ const proxy = Array.isArray(childSample) && !isOpaque(childSample)
3335
+ ? toArrayStore(computation, injector, childVivify, noUnionLeaves)
3336
+ : toStore(computation, injector, childVivify, noUnionLeaves);
3337
+ markAsLeaf(proxy, computation, childVivify !== false, noUnionLeaves);
2734
3338
  const ref = new WeakRef(proxy);
2735
3339
  storeCache.set(prop, ref);
2736
3340
  PROXY_CLEANUP.register(proxy, { target, prop }, ref);
@@ -2774,7 +3378,9 @@ function scopedStore(parent, seed, kind, injector) {
2774
3378
  layer[key].set(next[key]);
2775
3379
  }
2776
3380
  };
2777
- const base = toWritable(view, kind === 'readonly' ? () => undefined : splitSet, undefined, { pure: false });
3381
+ const base = toWritable(view, kind === 'readonly' ? () => undefined : splitSet, undefined, {
3382
+ pure: false,
3383
+ });
2778
3384
  if (kind === 'mutable') {
2779
3385
  base.mutate = (updater) => splitSet(updater(untracked(view)));
2780
3386
  base.inline = (updater) => base.mutate((prev) => {
@@ -2823,14 +3429,78 @@ function scopedStore(parent, seed, kind, injector) {
2823
3429
  * @see {@link toStore}
2824
3430
  */
2825
3431
  function store(value, opt) {
2826
- return toStore(signal(value, opt), opt?.injector, opt?.vivify ?? false);
3432
+ return toStore(signal(value, opt), opt?.injector, opt?.vivify ?? false, opt?.noUnionLeaves ?? false);
2827
3433
  }
2828
3434
  /**
2829
3435
  * Creates a MutableSignalStore from a value.
2830
3436
  * @see {@link toStore}
2831
3437
  */
2832
3438
  function mutableStore(value, opt) {
2833
- return toStore(mutable(value, opt), opt?.injector, opt?.vivify ?? false);
3439
+ return toStore(mutable(value, opt), opt?.injector, opt?.vivify ?? false, opt?.noUnionLeaves ?? false);
3440
+ }
3441
+
3442
+ function isPlainRecord(value) {
3443
+ if (value === null || typeof value !== 'object')
3444
+ return false;
3445
+ const proto = Object.getPrototypeOf(value);
3446
+ return proto === Object.prototype || proto === null;
3447
+ }
3448
+ /**
3449
+ * Per-path 3-way merge. Reference-equality short-circuits do the work: a subtree the fork never
3450
+ * touched satisfies `mine === ancestor` (structural sharing keeps its identity) → take the live
3451
+ * base; a subtree the base never changed satisfies `theirs === ancestor` → keep the fork's. So it
3452
+ * only deep-walks paths that BOTH sides changed, and on a leaf/array conflict the fork wins.
3453
+ * Arrays are treated atomically (no positional merge — index shifts make that unsafe); supply a
3454
+ * {@link ReconcileFn} for array-aware merging.
3455
+ *
3456
+ * CONTRACT: "unchanged" is detected by REFERENCE identity, not deep equality. `mine` must be a
3457
+ * copy-on-write derivative of `ancestor` — i.e. untouched nodes keep their reference — which the
3458
+ * fork guarantees because writes flow through `toStore` (it rebuilds only the edited path and
3459
+ * shares everything else). Feed it a structurally-equal-but-fresh-reference node for an untouched
3460
+ * path and it will treat that node as edited (recursion/leaf-value checks usually still reconcile,
3461
+ * but a fresh-ref clean node vs a base type-change resolves to the fork's stale value). Primitive
3462
+ * leaves compare by value, so equal primitives are correctly seen as unchanged.
3463
+ */
3464
+ function merge3(ancestor, mine, theirs) {
3465
+ if (Object.is(mine, theirs) || Object.is(mine, ancestor))
3466
+ return theirs; // unedited → live base
3467
+ if (Object.is(theirs, ancestor))
3468
+ return mine; // base unchanged here → keep the fork's edit
3469
+ if (isPlainRecord(mine) && isPlainRecord(theirs) && isPlainRecord(ancestor)) {
3470
+ const out = { ...theirs };
3471
+ for (const key of new Set([...Object.keys(mine), ...Object.keys(theirs)])) {
3472
+ out[key] = merge3(ancestor[key], mine[key], theirs[key]);
3473
+ }
3474
+ return out;
3475
+ }
3476
+ return mine; // leaf / array / type-mismatch conflict → local wins
3477
+ }
3478
+ function forkStore(base, opt) {
3479
+ // A mutable base mutates in place, so its value reference is stable across changes — which defeats merge3's identity-based change detection
3480
+ const mutableBase = typeof base.mutate === 'function';
3481
+ let strategy = opt?.strategy ?? (mutableBase ? 'coarse' : 'fine');
3482
+ if (mutableBase && strategy === 'fine') {
3483
+ if (isDevMode())
3484
+ console.warn("[fork] strategy 'fine' relies on reference-identity change detection, but the base is a " +
3485
+ "mutable store (in-place mutation keeps the same reference) — falling back to 'coarse'.");
3486
+ strategy = 'coarse';
3487
+ }
3488
+ const reconcile = strategy === 'coarse'
3489
+ ? (_ancestor, _mine, theirs) => theirs // re-link to the new base (whole-value reset)
3490
+ : strategy === 'fine'
3491
+ ? merge3
3492
+ : strategy;
3493
+ const merge = reconcile;
3494
+ const staged = linkedSignal({
3495
+ source: () => base(),
3496
+ computation: (theirs, prev) => prev === undefined ? theirs : merge(prev.source, prev.value, theirs),
3497
+ });
3498
+ const store = toStore(staged, opt?.injector, opt?.vivify, opt?.noUnionLeaves);
3499
+ return {
3500
+ store,
3501
+ commit: () => base.set(untracked(staged)),
3502
+ discard: () => staged.set(untracked(base)),
3503
+ };
2834
3504
  }
2835
3505
 
2836
3506
  // Internal dummy store for server-side rendering
@@ -3309,5 +3979,5 @@ function withHistory(sourceOrValue, opt) {
3309
3979
  * Generated bundle index. Do not edit.
3310
3980
  */
3311
3981
 
3312
- export { batteryStatus, chunked, clipboard, combineWith, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, filterWith, focusWithin, geolocation, idle, indexArray, isDerivation, isMutable, isOpaque, isStore, keyArray, map, mapArray, mapObject, mediaQuery, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, opaque, orientation, pageVisibility, pairwise, pipeable, piped, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
3982
+ 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 };
3313
3983
  //# sourceMappingURL=mmstack-primitives.mjs.map