@mmstack/primitives 21.4.1 → 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/README.md +75 -2
- package/fesm2022/mmstack-primitives.mjs +505 -8
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mmstack-primitives.d.ts +315 -10
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { isDevMode, inject, Injector, untracked, effect, DestroyRef, InjectionToken, TemplateRef, ViewContainerRef, PLATFORM_ID, input, computed, Directive, signal, runInInjectionContext, linkedSignal, afterNextRender, Component, isWritableSignal as isWritableSignal$2, isSignal,
|
|
2
|
+
import { isDevMode, inject, Injector, untracked, effect, DestroyRef, InjectionToken, TemplateRef, ViewContainerRef, PLATFORM_ID, input, computed, Directive, signal, runInInjectionContext, linkedSignal, afterNextRender, PendingTasks, Component, ElementRef, isWritableSignal as isWritableSignal$2, isSignal, Injectable } from '@angular/core';
|
|
3
3
|
import { isPlatformServer } from '@angular/common';
|
|
4
4
|
import { SIGNAL } from '@angular/core/primitives/signals';
|
|
5
5
|
|
|
6
|
-
const frameStack = [];
|
|
6
|
+
const frameStack$1 = [];
|
|
7
7
|
function currentFrame() {
|
|
8
|
-
return frameStack.at(-1) ?? null;
|
|
8
|
+
return frameStack$1.at(-1) ?? null;
|
|
9
9
|
}
|
|
10
10
|
function clearFrame(frame, userCleanups) {
|
|
11
11
|
frame.parent = null;
|
|
@@ -31,10 +31,10 @@ function clearFrame(frame, userCleanups) {
|
|
|
31
31
|
frame.children.clear();
|
|
32
32
|
}
|
|
33
33
|
function pushFrame(frame) {
|
|
34
|
-
return frameStack.push(frame);
|
|
34
|
+
return frameStack$1.push(frame);
|
|
35
35
|
}
|
|
36
36
|
function popFrame() {
|
|
37
|
-
return frameStack.pop();
|
|
37
|
+
return frameStack$1.pop();
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -445,6 +445,79 @@ function chunked(source, options) {
|
|
|
445
445
|
return internal.asReadonly();
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
+
/**
|
|
449
|
+
* `useDeferredValue` for signals: returns a signal that HOLDS its previous value when
|
|
450
|
+
* `source` changes and catches up at lower priority (after paint / on idle), so an
|
|
451
|
+
* expensive subtree keyed off the deferred value never blocks the urgent update that
|
|
452
|
+
* caused the change — type into a filter, the input echoes instantly, the big list
|
|
453
|
+
* re-renders a beat later.
|
|
454
|
+
*
|
|
455
|
+
* ```ts
|
|
456
|
+
* const query = signal('');
|
|
457
|
+
* const deferredQuery = deferredValue(query);
|
|
458
|
+
* const results = computed(() => expensiveFilter(items(), deferredQuery()));
|
|
459
|
+
* // template: <input [(ngModel)]="query" /> stays responsive; results lag one paint
|
|
460
|
+
* // deferredQuery.pending() → dim the stale list while it catches up
|
|
461
|
+
* ```
|
|
462
|
+
*
|
|
463
|
+
* Rapid changes coalesce: each change reschedules the catch-up, so only the LATEST
|
|
464
|
+
* source value is ever applied (no intermediate churn in the expensive subtree).
|
|
465
|
+
* On the server this is a synchronous pass-through — SSR renders once, so deferral
|
|
466
|
+
* would just mean rendering stale content.
|
|
467
|
+
*
|
|
468
|
+
* This is a scheduling tool, not an async one — for async work compose `latest()`;
|
|
469
|
+
* for coordinated multi-resource reveals use a transition scope.
|
|
470
|
+
*/
|
|
471
|
+
function deferredValue(source, opt) {
|
|
472
|
+
const injector = opt?.injector ?? inject(Injector);
|
|
473
|
+
const equal = opt?.equal ?? Object.is;
|
|
474
|
+
if (injector.get(PLATFORM_ID) === 'server') {
|
|
475
|
+
const passthrough = computed(() => source());
|
|
476
|
+
passthrough.pending = computed(() => false, ...(ngDevMode ? [{ debugName: "pending" }] : /* istanbul ignore next */ []));
|
|
477
|
+
return passthrough;
|
|
478
|
+
}
|
|
479
|
+
const schedule = resolveScheduler(opt?.strategy ?? 'afterRender', injector);
|
|
480
|
+
const out = signal(untracked(source), { ...(ngDevMode ? { debugName: "out" } : /* istanbul ignore next */ {}), equal });
|
|
481
|
+
let cancel = null;
|
|
482
|
+
const watch = effect(() => {
|
|
483
|
+
const v = source();
|
|
484
|
+
cancel?.(); // latest wins: rapid changes coalesce into one catch-up
|
|
485
|
+
cancel = schedule(() => {
|
|
486
|
+
cancel = null;
|
|
487
|
+
out.set(v);
|
|
488
|
+
});
|
|
489
|
+
}, { ...(ngDevMode ? { debugName: "watch" } : /* istanbul ignore next */ {}), injector });
|
|
490
|
+
injector.get(DestroyRef).onDestroy(() => {
|
|
491
|
+
watch.destroy();
|
|
492
|
+
cancel?.();
|
|
493
|
+
cancel = null;
|
|
494
|
+
});
|
|
495
|
+
const result = computed(() => out());
|
|
496
|
+
// "behind" is a value comparison, not a schedule flag: an equal-valued catch-up
|
|
497
|
+
// (e.g. type a char, delete it before the deferred view caught up) is not pending
|
|
498
|
+
result.pending = computed(() => !equal(out(), source()), ...(ngDevMode ? [{ debugName: "pending" }] : /* istanbul ignore next */ []));
|
|
499
|
+
return result;
|
|
500
|
+
}
|
|
501
|
+
function resolveScheduler(strategy, injector) {
|
|
502
|
+
if (typeof strategy === 'function')
|
|
503
|
+
return strategy;
|
|
504
|
+
if (strategy === 'idle') {
|
|
505
|
+
return (cb) => {
|
|
506
|
+
const ric = globalThis.requestIdleCallback;
|
|
507
|
+
if (ric) {
|
|
508
|
+
const id = ric(() => cb());
|
|
509
|
+
return () => globalThis.cancelIdleCallback?.(id);
|
|
510
|
+
}
|
|
511
|
+
const id = setTimeout(cb, 0);
|
|
512
|
+
return () => clearTimeout(id);
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
return (cb) => {
|
|
516
|
+
const ref = afterNextRender({ read: cb }, { injector });
|
|
517
|
+
return () => ref.destroy();
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
448
521
|
/**
|
|
449
522
|
* Structural hold-and-swap as a signal. Given a `target` (the desired value — e.g. the
|
|
450
523
|
* subtree/def/key you want to show) and a `ready` predicate, returns a signal that keeps
|
|
@@ -541,6 +614,17 @@ function createTransitionScope() {
|
|
|
541
614
|
source: () => ({ v: value(), settled: !pending() }),
|
|
542
615
|
computation: (curr, prev) => curr.settled || prev === undefined ? curr.v : prev.value,
|
|
543
616
|
}),
|
|
617
|
+
abortPending: () => untracked(() => {
|
|
618
|
+
let aborted = 0;
|
|
619
|
+
for (const { ref } of list()) {
|
|
620
|
+
const s = ref.status();
|
|
621
|
+
if ((s === 'loading' || s === 'reloading') && ref.abort) {
|
|
622
|
+
ref.abort();
|
|
623
|
+
aborted++;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return aborted;
|
|
627
|
+
}),
|
|
544
628
|
holding,
|
|
545
629
|
beginHold: () => untracked(() => holdCount.update((c) => c + 1)),
|
|
546
630
|
endHold: () => untracked(() => holdCount.update((c) => (c > 0 ? c - 1 : 0))),
|
|
@@ -562,6 +646,7 @@ function createNoopScope() {
|
|
|
562
646
|
// noop
|
|
563
647
|
},
|
|
564
648
|
commit: (value) => value,
|
|
649
|
+
abortPending: () => 0,
|
|
565
650
|
holding: computed(() => false),
|
|
566
651
|
beginHold: () => {
|
|
567
652
|
// noop
|
|
@@ -573,9 +658,49 @@ function createNoopScope() {
|
|
|
573
658
|
};
|
|
574
659
|
}
|
|
575
660
|
const TRANSITION_SCOPE = new InjectionToken('@mmstack/primitives:transition-scope');
|
|
661
|
+
/**
|
|
662
|
+
* The scope→`PendingTasks` bridge: while `scope.pending()` is true, hold an Angular
|
|
663
|
+
* pending task so SSR serialization waits for the scope's in-flight loads — HTTP loads
|
|
664
|
+
* already do this via HttpClient, but CUSTOM loaders (a `latest()` over a hand-rolled
|
|
665
|
+
* promise, a non-HTTP resource) would otherwise let the server render a boundary
|
|
666
|
+
* mid-load. Wired automatically by `provideTransitionScope` /
|
|
667
|
+
* `provideForwardingTransitionScope`; call it yourself only for scopes you construct
|
|
668
|
+
* directly with `createTransitionScope()`.
|
|
669
|
+
*
|
|
670
|
+
* Server-only by design: on the browser, tying `ApplicationRef.isStable` to every load
|
|
671
|
+
* would stall stability-gated machinery (testability, hydration timing) for no benefit.
|
|
672
|
+
*/
|
|
673
|
+
function bridgeScopeToPendingTasks(scope, injector) {
|
|
674
|
+
const run = (fn) => injector ? runInInjectionContext(injector, fn) : fn();
|
|
675
|
+
run(() => {
|
|
676
|
+
if (inject(PLATFORM_ID) !== 'server')
|
|
677
|
+
return;
|
|
678
|
+
const tasks = inject(PendingTasks);
|
|
679
|
+
let done = null;
|
|
680
|
+
effect(() => {
|
|
681
|
+
if (scope.pending())
|
|
682
|
+
done ??= tasks.add();
|
|
683
|
+
else {
|
|
684
|
+
done?.();
|
|
685
|
+
done = null;
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
inject(DestroyRef).onDestroy(() => {
|
|
689
|
+
done?.();
|
|
690
|
+
done = null;
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
}
|
|
576
694
|
/** Provide a fresh transition scope at a boundary so its subtree's resources are tracked independently. */
|
|
577
695
|
function provideTransitionScope() {
|
|
578
|
-
return {
|
|
696
|
+
return {
|
|
697
|
+
provide: TRANSITION_SCOPE,
|
|
698
|
+
useFactory: () => {
|
|
699
|
+
const scope = createTransitionScope();
|
|
700
|
+
bridgeScopeToPendingTasks(scope);
|
|
701
|
+
return scope;
|
|
702
|
+
},
|
|
703
|
+
};
|
|
579
704
|
}
|
|
580
705
|
function injectTransitionScope() {
|
|
581
706
|
const scope = inject(TRANSITION_SCOPE, { optional: true });
|
|
@@ -611,6 +736,7 @@ function createForwardingScope() {
|
|
|
611
736
|
source: () => ({ v: value(), settled: !eff().pending() }),
|
|
612
737
|
computation: (curr, prev) => curr.settled || prev === undefined ? curr.v : prev.value,
|
|
613
738
|
}),
|
|
739
|
+
abortPending: () => (untracked(target) ?? own).abortPending(),
|
|
614
740
|
holding: computed(() => eff().holding()),
|
|
615
741
|
beginHold: () => (untracked(target) ?? own).beginHold(),
|
|
616
742
|
endHold: () => (untracked(target) ?? own).endHold(),
|
|
@@ -622,7 +748,14 @@ function createForwardingScope() {
|
|
|
622
748
|
}
|
|
623
749
|
/** Provide a forwarding transition scope at a boundary (used by the transition outlet). */
|
|
624
750
|
function provideForwardingTransitionScope() {
|
|
625
|
-
return {
|
|
751
|
+
return {
|
|
752
|
+
provide: TRANSITION_SCOPE,
|
|
753
|
+
useFactory: () => {
|
|
754
|
+
const scope = createForwardingScope();
|
|
755
|
+
bridgeScopeToPendingTasks(scope);
|
|
756
|
+
return scope;
|
|
757
|
+
},
|
|
758
|
+
};
|
|
626
759
|
}
|
|
627
760
|
/** Read the transition scope reachable from `injector`, or null if none is provided there. */
|
|
628
761
|
function getTransitionScope(injector) {
|
|
@@ -679,6 +812,135 @@ function registerResource(res, opt) {
|
|
|
679
812
|
return injectRegisterResource()(res, opt);
|
|
680
813
|
}
|
|
681
814
|
|
|
815
|
+
const frameStack = [];
|
|
816
|
+
/**
|
|
817
|
+
* Thrown by `use()` to short-circuit a computation whose input has no value yet; caught
|
|
818
|
+
* by the owning `latest()`. Identity-compared, so user code must not swallow it — avoid
|
|
819
|
+
* broad `try/catch` around `use()` calls.
|
|
820
|
+
*/
|
|
821
|
+
const BLOCKED = new Error('[mmstack/primitives] latest() blocked — internal sentinel, do not catch');
|
|
822
|
+
/**
|
|
823
|
+
* Reads a resource inside a `latest()` computation: returns its value and reports it to
|
|
824
|
+
* the enclosing collector, so the derivation's aggregate `pending`/`status`/`error`
|
|
825
|
+
* include it. When the resource has no value yet (first load) or is in an error state,
|
|
826
|
+
* the computation short-circuits — code after this call simply doesn't run this round —
|
|
827
|
+
* which is what lets you write the happy path with no `undefined` checks:
|
|
828
|
+
*
|
|
829
|
+
* ```ts
|
|
830
|
+
* const fullName = latest(() => {
|
|
831
|
+
* const u = use(user); // waterfalls compose:
|
|
832
|
+
* const org = use(orgFor(u)); // orgFor(u) is only read once `user` has a value
|
|
833
|
+
* return `${u.name} @ ${org.name}`;
|
|
834
|
+
* });
|
|
835
|
+
* ```
|
|
836
|
+
*
|
|
837
|
+
* Must be called synchronously within `latest()` — like `inject()`, it throws elsewhere.
|
|
838
|
+
*/
|
|
839
|
+
function use(res) {
|
|
840
|
+
const frame = frameStack.at(-1);
|
|
841
|
+
if (!frame) {
|
|
842
|
+
throw new Error('[mmstack/primitives] use() must be called synchronously within a latest() computation');
|
|
843
|
+
}
|
|
844
|
+
if (!frame.seen.has(res)) {
|
|
845
|
+
frame.seen.add(res);
|
|
846
|
+
frame.deps.push(res);
|
|
847
|
+
}
|
|
848
|
+
// status() is read tracked even on the short-circuit paths, so the owning computed
|
|
849
|
+
// re-evaluates when the load settles / the error clears.
|
|
850
|
+
if (res.status() === 'error') {
|
|
851
|
+
frame.errors.push(res.error?.());
|
|
852
|
+
throw BLOCKED;
|
|
853
|
+
}
|
|
854
|
+
if (!res.hasValue())
|
|
855
|
+
throw BLOCKED;
|
|
856
|
+
return res.value();
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* An async derivation over resources: evaluates `fn` inside a collector frame so that
|
|
860
|
+
* every `use()` read registers as a member, and exposes the result with resource
|
|
861
|
+
* semantics — the value holds its previous state while anything it read is in flight
|
|
862
|
+
* (never flashing empty), `pending` aggregates the members' in-flight state, and the
|
|
863
|
+
* whole thing is itself a `UseSource`, so `latest`s nest and propagate.
|
|
864
|
+
*
|
|
865
|
+
* ```ts
|
|
866
|
+
* const fullName = latest(() => `${use(user).name} @ ${use(org).name}`);
|
|
867
|
+
* fullName(); // held value — undefined only before the first successful run
|
|
868
|
+
* fullName.pending(); // true while user OR org (re)loads
|
|
869
|
+
* ```
|
|
870
|
+
*
|
|
871
|
+
* Evaluation is a plain `computed` under the hood: lazy, pure, no effects, usable
|
|
872
|
+
* outside any injection context (`register` is the only DI-touching option).
|
|
873
|
+
*/
|
|
874
|
+
function latest(fn, opt) {
|
|
875
|
+
const evaluation = computed(() => {
|
|
876
|
+
const frame = { deps: [], seen: new Set(), errors: [] };
|
|
877
|
+
frameStack.push(frame);
|
|
878
|
+
try {
|
|
879
|
+
const value = fn();
|
|
880
|
+
return { kind: 'value', value, deps: frame.deps, errors: frame.errors };
|
|
881
|
+
}
|
|
882
|
+
catch (e) {
|
|
883
|
+
if (e === BLOCKED)
|
|
884
|
+
return { kind: 'blocked', deps: frame.deps, errors: frame.errors };
|
|
885
|
+
return {
|
|
886
|
+
kind: 'thrown',
|
|
887
|
+
thrown: e,
|
|
888
|
+
deps: frame.deps,
|
|
889
|
+
errors: frame.errors,
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
finally {
|
|
893
|
+
frameStack.pop();
|
|
894
|
+
}
|
|
895
|
+
}, opt?.debugName ? { debugName: `${opt.debugName}:evaluation` } : undefined);
|
|
896
|
+
const equal = opt?.equal ?? Object.is;
|
|
897
|
+
// The stale-while-revalidate atom: holds the last successful result through blocked /
|
|
898
|
+
// errored rounds. `equal` gates notification, so an in-flight cycle that lands on an
|
|
899
|
+
// equal value never ripples to consumers — while `pending` (independent) still cycles.
|
|
900
|
+
const held = linkedSignal({ ...(ngDevMode ? { debugName: "held" } : /* istanbul ignore next */ {}), source: evaluation,
|
|
901
|
+
computation: (ev, prev) => ev.kind === 'value'
|
|
902
|
+
? { has: true, v: ev.value }
|
|
903
|
+
: (prev?.value ?? { has: false, v: undefined }),
|
|
904
|
+
equal: (a, b) => a.has === b.has && (!a.has || equal(a.v, b.v)) });
|
|
905
|
+
const value = computed(() => held().v, opt?.debugName ? { debugName: opt.debugName } : undefined);
|
|
906
|
+
const pending = computed(() => evaluation().deps.some((d) => {
|
|
907
|
+
const s = d.status();
|
|
908
|
+
return s === 'loading' || s === 'reloading';
|
|
909
|
+
}), ...(ngDevMode ? [{ debugName: "pending" }] : /* istanbul ignore next */ []));
|
|
910
|
+
const status = computed(() => {
|
|
911
|
+
const ev = evaluation();
|
|
912
|
+
if (ev.kind === 'thrown' || ev.errors.length > 0)
|
|
913
|
+
return 'error';
|
|
914
|
+
if (pending())
|
|
915
|
+
return held().has ? 'reloading' : 'loading';
|
|
916
|
+
return ev.kind === 'value' ? 'resolved' : 'idle';
|
|
917
|
+
}, ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
|
|
918
|
+
const error = computed(() => {
|
|
919
|
+
const ev = evaluation();
|
|
920
|
+
return ev.kind === 'thrown' ? ev.thrown : ev.errors.at(0);
|
|
921
|
+
}, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
922
|
+
const result = Object.assign(value, {
|
|
923
|
+
value,
|
|
924
|
+
status,
|
|
925
|
+
pending,
|
|
926
|
+
isLoading: pending,
|
|
927
|
+
error,
|
|
928
|
+
hasValue: () => held().has,
|
|
929
|
+
});
|
|
930
|
+
if (opt?.register) {
|
|
931
|
+
const register = () => {
|
|
932
|
+
const scope = injectTransitionScope();
|
|
933
|
+
scope.add(result, { suspends: opt.register === 'suspend' });
|
|
934
|
+
inject(DestroyRef).onDestroy(() => scope.remove(result));
|
|
935
|
+
};
|
|
936
|
+
if (opt.injector)
|
|
937
|
+
runInInjectionContext(opt.injector, register);
|
|
938
|
+
else
|
|
939
|
+
register();
|
|
940
|
+
}
|
|
941
|
+
return result;
|
|
942
|
+
}
|
|
943
|
+
|
|
682
944
|
/**
|
|
683
945
|
* Returns a `startTransition(fn)` bound to the nearest transition scope. `fn` runs its state
|
|
684
946
|
* mutations (which commit immediately); any resource that reloads as a result holds its value
|
|
@@ -1101,6 +1363,57 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
1101
1363
|
}]
|
|
1102
1364
|
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "mmTransition", required: true }] }], immediate: [{ type: i0.Input, args: [{ isSignal: true, alias: "mmTransitionImmediate", required: false }] }], viewTransition: [{ type: i0.Input, args: [{ isSignal: true, alias: "mmTransitionViewTransition", required: false }] }] } });
|
|
1103
1365
|
|
|
1366
|
+
/**
|
|
1367
|
+
* Per-element morphs on held swaps: assigns `view-transition-name` reactively, so when
|
|
1368
|
+
* a swap wrapped in `document.startViewTransition` flips views (`*mmTransition`'s
|
|
1369
|
+
* `mmTransitionViewTransition`, or the transition outlet's view-transition option), the
|
|
1370
|
+
* browser pairs same-named elements across the outgoing and incoming views and MORPHS
|
|
1371
|
+
* them instead of cross-fading the whole boundary.
|
|
1372
|
+
*
|
|
1373
|
+
* ```html
|
|
1374
|
+
* <!-- outgoing view (list) and incoming view (detail) both name the hero image: -->
|
|
1375
|
+
* <img [mmViewTransitionName]="'hero-' + item().id" [src]="item().img" />
|
|
1376
|
+
* ```
|
|
1377
|
+
*
|
|
1378
|
+
* Why this works with holds: both views coexist in the DOM during a hold, but the
|
|
1379
|
+
* incoming one is `display: none` — elements without boxes aren't captured, so the
|
|
1380
|
+
* same name on both sides is legal at each capture point (old visible at snapshot,
|
|
1381
|
+
* new visible after the swap). No arming/cleanup dance needed.
|
|
1382
|
+
*
|
|
1383
|
+
* The name is normalized to a valid CSS custom-ident (invalid characters → `-`, a
|
|
1384
|
+
* leading digit gets a `_` prefix). An empty string / `'none'` clears the name — use
|
|
1385
|
+
* that to opt an element out conditionally. One rule remains YOURS to keep: a name
|
|
1386
|
+
* must be unique among elements VISIBLE at capture time (two rendered instances of the
|
|
1387
|
+
* same named element make the browser skip the whole transition) — derive names from
|
|
1388
|
+
* ids for anything that can repeat.
|
|
1389
|
+
*/
|
|
1390
|
+
class MmViewTransitionName {
|
|
1391
|
+
mmViewTransitionName = input.required(...(ngDevMode ? [{ debugName: "mmViewTransitionName" }] : /* istanbul ignore next */ []));
|
|
1392
|
+
constructor() {
|
|
1393
|
+
const el = inject(ElementRef).nativeElement;
|
|
1394
|
+
effect(() => {
|
|
1395
|
+
const name = normalizeIdent(this.mmViewTransitionName());
|
|
1396
|
+
if (name)
|
|
1397
|
+
el.style.setProperty('view-transition-name', name);
|
|
1398
|
+
else
|
|
1399
|
+
el.style.removeProperty('view-transition-name');
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: MmViewTransitionName, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1403
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.12", type: MmViewTransitionName, isStandalone: true, selector: "[mmViewTransitionName]", inputs: { mmViewTransitionName: { classPropertyName: "mmViewTransitionName", publicName: "mmViewTransitionName", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
|
|
1404
|
+
}
|
|
1405
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: MmViewTransitionName, decorators: [{
|
|
1406
|
+
type: Directive,
|
|
1407
|
+
args: [{ selector: '[mmViewTransitionName]' }]
|
|
1408
|
+
}], ctorParameters: () => [], propDecorators: { mmViewTransitionName: [{ type: i0.Input, args: [{ isSignal: true, alias: "mmViewTransitionName", required: true }] }] } });
|
|
1409
|
+
/** @internal `''`/`'none'` clear; otherwise coerce into a valid custom-ident. */
|
|
1410
|
+
function normalizeIdent(raw) {
|
|
1411
|
+
if (!raw || raw === 'none')
|
|
1412
|
+
return null;
|
|
1413
|
+
const cleaned = raw.replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
1414
|
+
return /^\d/.test(cleaned) ? `_${cleaned}` : cleaned;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1104
1417
|
/**
|
|
1105
1418
|
* @internal
|
|
1106
1419
|
*/
|
|
@@ -4285,6 +4598,190 @@ function forkStore(base, opt) {
|
|
|
4285
4598
|
};
|
|
4286
4599
|
}
|
|
4287
4600
|
|
|
4601
|
+
function generateOrigin() {
|
|
4602
|
+
if (globalThis.crypto?.randomUUID)
|
|
4603
|
+
return globalThis.crypto.randomUUID();
|
|
4604
|
+
return Math.random().toString(36).substring(2);
|
|
4605
|
+
}
|
|
4606
|
+
const isPlainArray = (v) => Array.isArray(v) && !isOpaque(v);
|
|
4607
|
+
/**
|
|
4608
|
+
* Reference-identity-pruned structural diff — the same short-circuit discipline as `merge3`:
|
|
4609
|
+
* an untouched subtree kept its reference (the store's copy-on-write contract), so the walk
|
|
4610
|
+
* descends only where refs differ. O(changed paths), not O(tree).
|
|
4611
|
+
*/
|
|
4612
|
+
function diffNode(prev, next, path, ops) {
|
|
4613
|
+
if (Object.is(prev, next))
|
|
4614
|
+
return;
|
|
4615
|
+
if (isRecord(prev) && isRecord(next)) {
|
|
4616
|
+
for (const key of Object.keys(prev)) {
|
|
4617
|
+
if (!Object.hasOwn(next, key))
|
|
4618
|
+
ops.push({ kind: 'delete', path: [...path, key], prev: prev[key] });
|
|
4619
|
+
}
|
|
4620
|
+
for (const key of Object.keys(next)) {
|
|
4621
|
+
if (!Object.hasOwn(prev, key)) {
|
|
4622
|
+
// added key: deliberately NO `prev` property (absent ≠ undefined)
|
|
4623
|
+
ops.push({ kind: 'set', path: [...path, key], next: next[key] });
|
|
4624
|
+
}
|
|
4625
|
+
else {
|
|
4626
|
+
diffNode(prev[key], next[key], [...path, key], ops);
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
return;
|
|
4630
|
+
}
|
|
4631
|
+
if (isPlainArray(prev) && isPlainArray(next)) {
|
|
4632
|
+
// same length → per-index descent (matches `arr[i].x.set(...)` writes); a length
|
|
4633
|
+
// change is a whole unit — index attribution lies under insert/remove/reorder
|
|
4634
|
+
if (prev.length === next.length) {
|
|
4635
|
+
for (let i = 0; i < next.length; i++)
|
|
4636
|
+
diffNode(prev[i], next[i], [...path, i], ops);
|
|
4637
|
+
return;
|
|
4638
|
+
}
|
|
4639
|
+
ops.push({ kind: 'set', path, prev, next });
|
|
4640
|
+
return;
|
|
4641
|
+
}
|
|
4642
|
+
// leaf / type change / opaque — one unit, prev present (the slot existed)
|
|
4643
|
+
ops.push({ kind: 'set', path, prev, next });
|
|
4644
|
+
}
|
|
4645
|
+
/** Immutably applies one op along its path, vivifying missing containers `'auto'`-style. */
|
|
4646
|
+
function applyAt(container, path, idx, op) {
|
|
4647
|
+
const seg = path[idx];
|
|
4648
|
+
const base = isPlainArray(container)
|
|
4649
|
+
? container.slice()
|
|
4650
|
+
: isRecord(container)
|
|
4651
|
+
? { ...container }
|
|
4652
|
+
: typeof seg === 'number'
|
|
4653
|
+
? []
|
|
4654
|
+
: {};
|
|
4655
|
+
if (idx === path.length - 1) {
|
|
4656
|
+
if (op.kind === 'delete') {
|
|
4657
|
+
// arrays never receive deletes (length changes travel as whole-array sets)
|
|
4658
|
+
delete base[seg];
|
|
4659
|
+
}
|
|
4660
|
+
else {
|
|
4661
|
+
base[seg] = op.next;
|
|
4662
|
+
}
|
|
4663
|
+
return base;
|
|
4664
|
+
}
|
|
4665
|
+
base[seg] = applyAt(base[seg], path, idx + 1, op);
|
|
4666
|
+
return base;
|
|
4667
|
+
}
|
|
4668
|
+
/**
|
|
4669
|
+
* Inverts a batch for undo: reversed order, `set`↔its own inverse (an add — a `set` with no
|
|
4670
|
+
* `prev` — inverts to a `delete`; a `delete` inverts to a `set` restoring `prev`). Feed the
|
|
4671
|
+
* result to {@link OpLog.apply}. Requires the ops' `prev`s, which in-memory batches always
|
|
4672
|
+
* carry — a wire-serialized batch that stripped them is not invertible.
|
|
4673
|
+
*/
|
|
4674
|
+
function invertBatch(batch) {
|
|
4675
|
+
const ops = Array.isArray(batch) ? batch : batch.ops;
|
|
4676
|
+
const inverted = [];
|
|
4677
|
+
for (let i = ops.length - 1; i >= 0; i--) {
|
|
4678
|
+
const op = ops[i];
|
|
4679
|
+
if (op.kind === 'delete') {
|
|
4680
|
+
inverted.push({ kind: 'set', path: op.path, next: op.prev, prev: undefined });
|
|
4681
|
+
continue;
|
|
4682
|
+
}
|
|
4683
|
+
if (!Object.hasOwn(op, 'prev')) {
|
|
4684
|
+
inverted.push({ kind: 'delete', path: op.path, prev: op.next });
|
|
4685
|
+
}
|
|
4686
|
+
else {
|
|
4687
|
+
inverted.push({ kind: 'set', path: op.path, next: op.prev, prev: op.next });
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
4690
|
+
return inverted;
|
|
4691
|
+
}
|
|
4692
|
+
/**
|
|
4693
|
+
* Observes a copy-on-write signal (a `store`'s root, or any `WritableSignal` holding
|
|
4694
|
+
* immutably-updated objects) and emits its changes as minimal structural op batches — the
|
|
4695
|
+
* shared substrate for sync (ship batches, `apply` remote ones), persistence (journal
|
|
4696
|
+
* batches, replay on boot), undo ({@link invertBatch}), and devtools (`latest`).
|
|
4697
|
+
*
|
|
4698
|
+
* Zero store-core involvement and zero cost when unused: emission is a reference-pruned diff
|
|
4699
|
+
* of the root value per tick (structural sharing makes it O(changed paths)), driven by one
|
|
4700
|
+
* effect. A batch therefore coalesces everything written in one tick — for coarser,
|
|
4701
|
+
* intentional units, stage writes on a `forkStore` and `commit()` (one set → one batch).
|
|
4702
|
+
*
|
|
4703
|
+
* NOT supported on mutable stores/signals: in-place mutation keeps reference identity, which
|
|
4704
|
+
* defeats the diff (same reason `forkStore`'s `'fine'` strategy refuses them) — a dev-mode
|
|
4705
|
+
* warning fires and nothing emits.
|
|
4706
|
+
*
|
|
4707
|
+
* ```ts
|
|
4708
|
+
* const s = store({ todos: [{ done: false }] });
|
|
4709
|
+
* const log = opLog(s, { origin: 'tab-a' });
|
|
4710
|
+
* log.subscribe((b) => channel.postMessage(encode(b))); // ship
|
|
4711
|
+
* channel.onmessage = (m) => log.apply(decode(m.data)); // apply — echo-free
|
|
4712
|
+
* s.todos[0].done.set(true); // → { kind: 'set', path: ['todos', 0, 'done'], … }
|
|
4713
|
+
* ```
|
|
4714
|
+
*/
|
|
4715
|
+
function opLog(source, opt) {
|
|
4716
|
+
const injector = opt?.injector ?? inject(Injector);
|
|
4717
|
+
const origin = opt?.origin ?? generateOrigin();
|
|
4718
|
+
// a store proxy's `has` trap answers for the VALUE's keys, so `isMutable`'s `'mutate' in`
|
|
4719
|
+
// probe can't see the brand — ask the store's own kind symbol first
|
|
4720
|
+
const storeKind = source[STORE_KIND];
|
|
4721
|
+
const mutableSource = storeKind ? storeKind === 'mutable' : isMutable(source);
|
|
4722
|
+
if (isDevMode() && mutableSource) {
|
|
4723
|
+
console.warn('[@mmstack/primitives] opLog observes copy-on-write updates via reference identity — a MUTABLE store/signal mutates in place, so changes are invisible to it. Use an immutable store, or set whole values.');
|
|
4724
|
+
}
|
|
4725
|
+
let prevRoot = untracked(source);
|
|
4726
|
+
let version = 0;
|
|
4727
|
+
let destroyed = false;
|
|
4728
|
+
const subscribers = new Set();
|
|
4729
|
+
const latest = signal(null, ...(ngDevMode ? [{ debugName: "latest" }] : /* istanbul ignore next */ []));
|
|
4730
|
+
/** Diff now, emit if there's a delta, advance the baseline. */
|
|
4731
|
+
const flush = () => {
|
|
4732
|
+
if (destroyed)
|
|
4733
|
+
return;
|
|
4734
|
+
const next = untracked(source);
|
|
4735
|
+
if (Object.is(prevRoot, next))
|
|
4736
|
+
return;
|
|
4737
|
+
const ops = [];
|
|
4738
|
+
diffNode(prevRoot, next, [], ops);
|
|
4739
|
+
prevRoot = next;
|
|
4740
|
+
if (!ops.length)
|
|
4741
|
+
return; // fresh refs, equal values — spurious-write tolerance
|
|
4742
|
+
const batch = { origin, version: ++version, ops };
|
|
4743
|
+
latest.set(batch);
|
|
4744
|
+
for (const cb of [...subscribers])
|
|
4745
|
+
cb(batch);
|
|
4746
|
+
};
|
|
4747
|
+
const ref = effect(() => {
|
|
4748
|
+
source(); // track every commit…
|
|
4749
|
+
untracked(flush); // …and emit the delta since the last flush
|
|
4750
|
+
}, { ...(ngDevMode ? { debugName: "ref" } : /* istanbul ignore next */ {}), injector: opt?.injector });
|
|
4751
|
+
return {
|
|
4752
|
+
latest: latest.asReadonly(),
|
|
4753
|
+
subscribe: (cb) => {
|
|
4754
|
+
subscribers.add(cb);
|
|
4755
|
+
return () => subscribers.delete(cb);
|
|
4756
|
+
},
|
|
4757
|
+
apply: (batchOrOps) => {
|
|
4758
|
+
const ops = Array.isArray(batchOrOps)
|
|
4759
|
+
? batchOrOps
|
|
4760
|
+
: batchOrOps.ops;
|
|
4761
|
+
if (!ops.length)
|
|
4762
|
+
return;
|
|
4763
|
+
// pending local writes must emit BEFORE the baseline advances past them
|
|
4764
|
+
flush();
|
|
4765
|
+
let root = untracked(source);
|
|
4766
|
+
for (const op of ops) {
|
|
4767
|
+
if (op.path.length === 0) {
|
|
4768
|
+
if (op.kind === 'set')
|
|
4769
|
+
root = op.next;
|
|
4770
|
+
continue; // a root delete is meaningless — ignore
|
|
4771
|
+
}
|
|
4772
|
+
root = applyAt(root, op.path, 0, op);
|
|
4773
|
+
}
|
|
4774
|
+
source.set(root);
|
|
4775
|
+
prevRoot = root; // baseline advance: an applied batch never echoes
|
|
4776
|
+
},
|
|
4777
|
+
destroy: () => {
|
|
4778
|
+
destroyed = true;
|
|
4779
|
+
subscribers.clear();
|
|
4780
|
+
ref.destroy();
|
|
4781
|
+
},
|
|
4782
|
+
};
|
|
4783
|
+
}
|
|
4784
|
+
|
|
4288
4785
|
/**
|
|
4289
4786
|
* @internal The plain-`effect` sibling of the public {@link pausableEffect} (which is built on
|
|
4290
4787
|
* `nestedEffect`). For infra utilities that own a single top-level effect/subscription and don't
|
|
@@ -4810,5 +5307,5 @@ function withHistory(sourceOrValue, opt) {
|
|
|
4810
5307
|
* Generated bundle index. Do not edit.
|
|
4811
5308
|
*/
|
|
4812
5309
|
|
|
4813
|
-
export { MmActivity, MmTransition, 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 };
|
|
5310
|
+
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 };
|
|
4814
5311
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|