@mmstack/primitives 22.1.1 → 22.2.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 +2 -0
- package/fesm2022/mmstack-primitives.mjs +588 -265
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/package.json +2 -2
- package/types/mmstack-primitives.d.ts +128 -107
|
@@ -80,7 +80,7 @@ function popFrame() {
|
|
|
80
80
|
* ]);
|
|
81
81
|
*
|
|
82
82
|
* // The fine-grained mapped list
|
|
83
|
-
* const mappedUsers =
|
|
83
|
+
* const mappedUsers = indexArray(
|
|
84
84
|
* users,
|
|
85
85
|
* (userSignal, index) => {
|
|
86
86
|
* // 1. Create a fine-grained SIDE EFFECT for *this item*
|
|
@@ -101,7 +101,7 @@ function popFrame() {
|
|
|
101
101
|
* };
|
|
102
102
|
* },
|
|
103
103
|
* {
|
|
104
|
-
* // 3. Tell
|
|
104
|
+
* // 3. Tell indexArray HOW to clean up when an item is removed, this needs to be manual as it's not a nestedEffect itself
|
|
105
105
|
* onDestroy: (mappedItem) => {
|
|
106
106
|
* mappedItem.destroyEffect();
|
|
107
107
|
* }
|
|
@@ -158,7 +158,7 @@ function nestedEffect(effectFn, options) {
|
|
|
158
158
|
/**
|
|
159
159
|
* Creates a new `Signal` that processes an array of items in time-sliced chunks. This is useful for handling large lists without blocking the main thread.
|
|
160
160
|
*
|
|
161
|
-
* The returned signal will initially contain the first `chunkSize` items from the source array. It will then schedule updates to include additional chunks of items based on the specified `
|
|
161
|
+
* The returned signal will initially contain the first `chunkSize` items from the source array. It will then schedule updates to include additional chunks of items based on the specified `delay`.
|
|
162
162
|
*
|
|
163
163
|
* @template T The type of items in the array.
|
|
164
164
|
* @param source A `Signal` or a function that returns an array of items to be processed in chunks.
|
|
@@ -167,16 +167,23 @@ function nestedEffect(effectFn, options) {
|
|
|
167
167
|
*
|
|
168
168
|
* @example
|
|
169
169
|
* const largeList = signal(Array.from({ length: 1000 }, (_, i) => i));
|
|
170
|
-
* const chunkedList = chunked(largeList, { chunkSize: 100,
|
|
170
|
+
* const chunkedList = chunked(largeList, { chunkSize: 100, delay: 100 });
|
|
171
171
|
*/
|
|
172
172
|
function chunked(source, options) {
|
|
173
173
|
const { chunkSize = 50, delay = 'frame', equal, injector } = options || {};
|
|
174
174
|
let delayFn;
|
|
175
175
|
if (delay === 'frame') {
|
|
176
|
-
delayFn =
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
176
|
+
delayFn =
|
|
177
|
+
typeof requestAnimationFrame === 'function'
|
|
178
|
+
? (callback) => {
|
|
179
|
+
const num = requestAnimationFrame(callback);
|
|
180
|
+
return () => cancelAnimationFrame(num);
|
|
181
|
+
}
|
|
182
|
+
: // SSR: no requestAnimationFrame — approximate a frame with a timeout
|
|
183
|
+
(cb) => {
|
|
184
|
+
const num = setTimeout(cb, 16);
|
|
185
|
+
return () => clearTimeout(num);
|
|
186
|
+
};
|
|
180
187
|
}
|
|
181
188
|
else if (delay === 'microtask') {
|
|
182
189
|
delayFn = (cb) => {
|
|
@@ -263,7 +270,9 @@ class MmActivity {
|
|
|
263
270
|
});
|
|
264
271
|
}
|
|
265
272
|
for (const node of this.view.rootNodes) {
|
|
266
|
-
|
|
273
|
+
// covers HTML and SVG roots; text/comment roots can't be styled — their CD is still
|
|
274
|
+
// detached, but prefer an element root for true visual hiding
|
|
275
|
+
if (node instanceof HTMLElement || node instanceof SVGElement)
|
|
267
276
|
node.style.display = visible ? '' : 'none';
|
|
268
277
|
}
|
|
269
278
|
if (visible)
|
|
@@ -271,10 +280,10 @@ class MmActivity {
|
|
|
271
280
|
else
|
|
272
281
|
this.view.detach();
|
|
273
282
|
}
|
|
274
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.
|
|
275
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.
|
|
283
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MmActivity, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
284
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: MmActivity, isStandalone: true, selector: "[mmActivity]", inputs: { visible: { classPropertyName: "visible", publicName: "mmActivity", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
|
|
276
285
|
}
|
|
277
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.
|
|
286
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MmActivity, decorators: [{
|
|
278
287
|
type: Directive,
|
|
279
288
|
args: [{
|
|
280
289
|
selector: '[mmActivity]',
|
|
@@ -342,14 +351,25 @@ function resolvePause(opt) {
|
|
|
342
351
|
if (pause === false)
|
|
343
352
|
return null;
|
|
344
353
|
const run = (fn) => opt?.injector ? runInInjectionContext(opt.injector, fn) : fn();
|
|
354
|
+
// `inject` requires an injection context even with `optional: true`. A bare
|
|
355
|
+
// `pausableSignal(0)` (documented as "like `signal`") must degrade to the unwrapped
|
|
356
|
+
// primitive outside DI, not throw NG0203 — so injection failures fall back gracefully.
|
|
357
|
+
const tryRun = (fn, fallback) => {
|
|
358
|
+
try {
|
|
359
|
+
return run(fn);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return fallback;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
345
365
|
const onServer = () => typeof pause === 'function' && !opt?.injector
|
|
346
366
|
? typeof globalThis.window === 'undefined'
|
|
347
|
-
:
|
|
367
|
+
: tryRun(() => isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser'), typeof globalThis.window === 'undefined');
|
|
348
368
|
if (typeof pause === 'function')
|
|
349
369
|
return onServer() ? null : pause;
|
|
350
370
|
if (onServer())
|
|
351
371
|
return null;
|
|
352
|
-
const paused =
|
|
372
|
+
const paused = tryRun(() => inject(PAUSED_CONTEXT, { optional: true }), null);
|
|
353
373
|
if (!paused) {
|
|
354
374
|
if (explicit === true && isDevMode())
|
|
355
375
|
console.warn('[pausable] `pause: true` but no PAUSED_CONTEXT in scope — not pausing. Provide one via an ' +
|
|
@@ -378,8 +398,9 @@ function pausableEffect(effectFn, options) {
|
|
|
378
398
|
/**
|
|
379
399
|
* Like `signal`, but pausable. While paused, READS hold the last value; writes still land on the
|
|
380
400
|
* underlying signal and surface on resume. Built on the `keepPrevious`/`hold` shape — a
|
|
381
|
-
* `linkedSignal` gated on the pause predicate, with `set`/`update
|
|
382
|
-
*
|
|
401
|
+
* `linkedSignal` gated on the pause predicate, with `set`/`update` forwarded to the source signal.
|
|
402
|
+
* `asReadonly()` returns the held (gated) view, so both views of the signal agree while paused.
|
|
403
|
+
* With no `pause` option it defaults to the ambient `PAUSED_CONTEXT`; `pause: false`
|
|
383
404
|
* makes it a plain `signal` — no `linkedSignal` is created.
|
|
384
405
|
*
|
|
385
406
|
* NOTE: while paused, `set(x)` followed by a read returns the *held* (pre-pause) value, not `x` — the
|
|
@@ -396,7 +417,8 @@ function pausableSignal(initialValue, options) {
|
|
|
396
417
|
equal: options?.equal });
|
|
397
418
|
read.set = src.set;
|
|
398
419
|
read.update = src.update;
|
|
399
|
-
|
|
420
|
+
// NOTE: `asReadonly` deliberately stays the linkedSignal's own (the held view) — the
|
|
421
|
+
// source's readonly view would show live values while the signal itself shows held ones.
|
|
400
422
|
return read;
|
|
401
423
|
}
|
|
402
424
|
/**
|
|
@@ -433,8 +455,12 @@ function mutable(initial, opt) {
|
|
|
433
455
|
const internalUpdate = sig.update;
|
|
434
456
|
sig.mutate = (updater) => {
|
|
435
457
|
cnt++;
|
|
436
|
-
|
|
437
|
-
|
|
458
|
+
try {
|
|
459
|
+
internalUpdate(updater);
|
|
460
|
+
}
|
|
461
|
+
finally {
|
|
462
|
+
cnt--;
|
|
463
|
+
}
|
|
438
464
|
};
|
|
439
465
|
sig.inline = (updater) => {
|
|
440
466
|
sig.mutate((prev) => {
|
|
@@ -524,7 +550,7 @@ function createNoopScope() {
|
|
|
524
550
|
hold: (value) => value,
|
|
525
551
|
};
|
|
526
552
|
}
|
|
527
|
-
const TRANSITION_SCOPE = new InjectionToken('@mmstack/
|
|
553
|
+
const TRANSITION_SCOPE = new InjectionToken('@mmstack/primitives:transition-scope');
|
|
528
554
|
/** Provide a fresh transition scope at a boundary so its subtree's resources are tracked independently. */
|
|
529
555
|
function provideTransitionScope() {
|
|
530
556
|
return { provide: TRANSITION_SCOPE, useFactory: createTransitionScope };
|
|
@@ -533,12 +559,54 @@ function injectTransitionScope() {
|
|
|
533
559
|
const scope = inject(TRANSITION_SCOPE, { optional: true });
|
|
534
560
|
if (!scope) {
|
|
535
561
|
if (isDevMode())
|
|
536
|
-
console.warn('[mmstack/
|
|
562
|
+
console.warn('[mmstack/primitives] No transition scope in context — registration/tracking here is a no-op. ' +
|
|
537
563
|
'Use a <mm-suspense> boundary or provideTransitionScope() in an ancestor.');
|
|
538
564
|
return createNoopScope();
|
|
539
565
|
}
|
|
540
566
|
return scope;
|
|
541
567
|
}
|
|
568
|
+
function createForwardingScope() {
|
|
569
|
+
const own = createTransitionScope();
|
|
570
|
+
const target = signal(null, /* @ts-ignore */
|
|
571
|
+
...(ngDevMode ? [{ debugName: "target" }] : /* istanbul ignore next */ []));
|
|
572
|
+
const eff = () => target() ?? own;
|
|
573
|
+
const owners = new Map();
|
|
574
|
+
return {
|
|
575
|
+
setTarget: (t) => target.set(t),
|
|
576
|
+
resources: computed(() => eff().resources()),
|
|
577
|
+
pending: computed(() => eff().pending()),
|
|
578
|
+
suspended: (type) => eff().suspended(type),
|
|
579
|
+
add: (ref, opt) => {
|
|
580
|
+
const t = untracked(target) ?? own;
|
|
581
|
+
owners.set(ref, t);
|
|
582
|
+
t.add(ref, opt);
|
|
583
|
+
},
|
|
584
|
+
remove: (ref) => {
|
|
585
|
+
const t = owners.get(ref) ?? untracked(target) ?? own;
|
|
586
|
+
t.remove(ref);
|
|
587
|
+
owners.delete(ref);
|
|
588
|
+
},
|
|
589
|
+
commit: (value) => linkedSignal({
|
|
590
|
+
source: () => ({ v: value(), settled: !eff().pending() }),
|
|
591
|
+
computation: (curr, prev) => curr.settled || prev === undefined ? curr.v : prev.value,
|
|
592
|
+
}),
|
|
593
|
+
holding: computed(() => eff().holding()),
|
|
594
|
+
beginHold: () => (untracked(target) ?? own).beginHold(),
|
|
595
|
+
endHold: () => (untracked(target) ?? own).endHold(),
|
|
596
|
+
hold: (value) => linkedSignal({
|
|
597
|
+
source: () => ({ v: value(), held: eff().holding() }),
|
|
598
|
+
computation: (curr, prev) => prev !== undefined && curr.held ? prev.value : curr.v,
|
|
599
|
+
}),
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
/** Provide a forwarding transition scope at a boundary (used by the transition outlet). */
|
|
603
|
+
function provideForwardingTransitionScope() {
|
|
604
|
+
return { provide: TRANSITION_SCOPE, useFactory: createForwardingScope };
|
|
605
|
+
}
|
|
606
|
+
/** Read the transition scope reachable from `injector`, or null if none is provided there. */
|
|
607
|
+
function getTransitionScope(injector) {
|
|
608
|
+
return injector.get(TRANSITION_SCOPE, null);
|
|
609
|
+
}
|
|
542
610
|
/**
|
|
543
611
|
* Returns a register function bound to the nearest transition scope: it adds a resource
|
|
544
612
|
* to the scope and removes it when the caller's injection context is destroyed. Pass any
|
|
@@ -567,6 +635,11 @@ function registerResource(res, opt) {
|
|
|
567
635
|
*
|
|
568
636
|
* Must be called in an injection context. This is the *async* generalization (Tier 2): it adds
|
|
569
637
|
* no rendering cost and needs no fork — holding direct/sync readers is a separate, deferred tier.
|
|
638
|
+
*
|
|
639
|
+
* Caveat: work must go in flight by the first post-write render to be awaited. A loader that
|
|
640
|
+
* starts later (a debounced request signal, a chained/deferred resource) is not attributable to
|
|
641
|
+
* this transition — the no-async fallback will have already resolved `done`. Trigger such work
|
|
642
|
+
* eagerly inside `fn`, or coordinate it separately.
|
|
570
643
|
*/
|
|
571
644
|
function injectStartTransition() {
|
|
572
645
|
const scope = injectTransitionScope();
|
|
@@ -621,10 +694,10 @@ class SuspenseBoundaryBase {
|
|
|
621
694
|
pending = this.scope.pending;
|
|
622
695
|
suspended = computed(() => this.scope.suspended(this.type()), /* @ts-ignore */
|
|
623
696
|
...(ngDevMode ? [{ debugName: "suspended" }] : /* istanbul ignore next */ []));
|
|
624
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.
|
|
625
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.
|
|
697
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SuspenseBoundaryBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
698
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: SuspenseBoundaryBase, isStandalone: true, inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
626
699
|
}
|
|
627
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.
|
|
700
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SuspenseBoundaryBase, decorators: [{
|
|
628
701
|
type: Directive
|
|
629
702
|
}], propDecorators: { type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }] } });
|
|
630
703
|
const SUSPENSE_TEMPLATE = `
|
|
@@ -652,10 +725,10 @@ const SUSPENSE_HOST = {
|
|
|
652
725
|
* `provideTransitionScope()`. The common case.
|
|
653
726
|
*/
|
|
654
727
|
class SuspenseBoundary extends SuspenseBoundaryBase {
|
|
655
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.
|
|
656
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.
|
|
728
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SuspenseBoundary, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
729
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", 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
730
|
}
|
|
658
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.
|
|
731
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SuspenseBoundary, decorators: [{
|
|
659
732
|
type: Component,
|
|
660
733
|
args: [{ selector: 'mm-suspense', template: SUSPENSE_TEMPLATE, host: SUSPENSE_HOST, providers: [provideTransitionScope()], styles: [":host{display:contents}\n"] }]
|
|
661
734
|
}] });
|
|
@@ -667,10 +740,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImpor
|
|
|
667
740
|
* ancestor.
|
|
668
741
|
*/
|
|
669
742
|
class UnscopedSuspenseBoundary extends SuspenseBoundaryBase {
|
|
670
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.
|
|
671
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.
|
|
743
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: UnscopedSuspenseBoundary, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
744
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", 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
745
|
}
|
|
673
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.
|
|
746
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: UnscopedSuspenseBoundary, decorators: [{
|
|
674
747
|
type: Component,
|
|
675
748
|
args: [{ selector: 'mm-unscoped-suspense', template: SUSPENSE_TEMPLATE, host: SUSPENSE_HOST, styles: [":host{display:contents}\n"] }]
|
|
676
749
|
}] });
|
|
@@ -718,6 +791,11 @@ function runInTransaction(txn, fn) {
|
|
|
718
791
|
* The writes land on LIVE state immediately (so derived variables and connector requests see the
|
|
719
792
|
* new values and refetch); only the *display* is held, via `scope.hold`. Must run in an injection
|
|
720
793
|
* context.
|
|
794
|
+
*
|
|
795
|
+
* Caveat: work must go in flight by the first post-write render to be part of the transaction. A
|
|
796
|
+
* loader that starts later (a debounced request signal, a chained/deferred resource) is not
|
|
797
|
+
* attributable to it — the no-async fallback will have already committed and released the hold,
|
|
798
|
+
* after which `abort()` is a no-op. Trigger such work eagerly inside `fn`.
|
|
721
799
|
*/
|
|
722
800
|
function injectStartTransaction() {
|
|
723
801
|
const scope = injectTransitionScope();
|
|
@@ -727,7 +805,15 @@ function injectStartTransaction() {
|
|
|
727
805
|
// Hold BEFORE the writes, so the display freezes at pre-transaction values.
|
|
728
806
|
scope.beginHold();
|
|
729
807
|
let finished = false;
|
|
808
|
+
// eslint-disable-next-line prefer-const -- assigned in try/catch, but needs to be declared here for the `finally` block to see it
|
|
730
809
|
let watcher;
|
|
810
|
+
let resolveDone;
|
|
811
|
+
const done = new Promise((resolve) => {
|
|
812
|
+
resolveDone = resolve;
|
|
813
|
+
});
|
|
814
|
+
// Every exit path funnels through here, so `done` always settles — including `abort()`
|
|
815
|
+
// and a throwing transaction body (which would otherwise leak the hold forever and
|
|
816
|
+
// freeze the boundary with no recovery).
|
|
731
817
|
const finish = (restore) => {
|
|
732
818
|
if (finished)
|
|
733
819
|
return;
|
|
@@ -738,27 +824,28 @@ function injectStartTransaction() {
|
|
|
738
824
|
else
|
|
739
825
|
txn.clear();
|
|
740
826
|
scope.endHold();
|
|
827
|
+
resolveDone();
|
|
741
828
|
};
|
|
742
|
-
|
|
829
|
+
try {
|
|
830
|
+
runInTransaction(txn, fn);
|
|
831
|
+
}
|
|
832
|
+
catch (e) {
|
|
833
|
+
finish(true);
|
|
834
|
+
throw e;
|
|
835
|
+
}
|
|
743
836
|
let sawPending = false;
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
if (!sawPending && !untracked(scope.pending)) {
|
|
757
|
-
finish(false);
|
|
758
|
-
resolve();
|
|
759
|
-
}
|
|
760
|
-
}, { injector });
|
|
761
|
-
});
|
|
837
|
+
watcher = effect(() => {
|
|
838
|
+
const p = scope.pending();
|
|
839
|
+
if (p)
|
|
840
|
+
sawPending = true;
|
|
841
|
+
if (sawPending && !p)
|
|
842
|
+
finish(false);
|
|
843
|
+
}, { injector });
|
|
844
|
+
// no-async fallback: if nothing ever went in flight, settle once the writes are processed.
|
|
845
|
+
afterNextRender(() => {
|
|
846
|
+
if (!sawPending && !untracked(scope.pending))
|
|
847
|
+
finish(false);
|
|
848
|
+
}, { injector });
|
|
762
849
|
return {
|
|
763
850
|
pending: scope.pending,
|
|
764
851
|
done,
|
|
@@ -767,6 +854,17 @@ function injectStartTransaction() {
|
|
|
767
854
|
};
|
|
768
855
|
}
|
|
769
856
|
|
|
857
|
+
/**
|
|
858
|
+
* @internal
|
|
859
|
+
*/
|
|
860
|
+
function getSignalEquality(sig) {
|
|
861
|
+
const internal = sig[SIGNAL];
|
|
862
|
+
if (internal && typeof internal.equal === 'function') {
|
|
863
|
+
return internal.equal;
|
|
864
|
+
}
|
|
865
|
+
return Object.is; // Default equality check
|
|
866
|
+
}
|
|
867
|
+
|
|
770
868
|
/**
|
|
771
869
|
* Converts a read-only `Signal` into a `WritableSignal` by providing custom `set` and, optionally, `update` functions.
|
|
772
870
|
* This can be useful for creating controlled write access to a signal that is otherwise read-only.
|
|
@@ -865,6 +963,7 @@ function debounced(initial, opt) {
|
|
|
865
963
|
* ```
|
|
866
964
|
*/
|
|
867
965
|
function debounce(source, opt) {
|
|
966
|
+
const eq = opt?.equal ?? getSignalEquality(source);
|
|
868
967
|
const ms = opt?.ms ?? 0;
|
|
869
968
|
const trigger = signal(false, /* @ts-ignore */
|
|
870
969
|
...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
|
|
@@ -880,25 +979,25 @@ function debounce(source, opt) {
|
|
|
880
979
|
catch {
|
|
881
980
|
// not in injection context & no destroyRef provided opting out of cleanup
|
|
882
981
|
}
|
|
883
|
-
const
|
|
982
|
+
const set = (next) => {
|
|
983
|
+
const isEqual = eq(untracked(source), next);
|
|
984
|
+
if (!timeout && isEqual)
|
|
985
|
+
return; // nothing to do
|
|
884
986
|
if (timeout)
|
|
885
|
-
clearTimeout(timeout);
|
|
886
|
-
|
|
987
|
+
clearTimeout(timeout); // clear pending
|
|
988
|
+
if (!isEqual)
|
|
989
|
+
source.set(next);
|
|
887
990
|
timeout = setTimeout(() => {
|
|
991
|
+
timeout = undefined;
|
|
888
992
|
trigger.update((c) => !c);
|
|
889
993
|
}, ms);
|
|
890
994
|
};
|
|
891
|
-
const
|
|
892
|
-
triggerFn(value);
|
|
893
|
-
};
|
|
894
|
-
const update = (fn) => {
|
|
895
|
-
triggerFn(fn(untracked(source)));
|
|
896
|
-
};
|
|
995
|
+
const update = (fn) => set(fn(untracked(source)));
|
|
897
996
|
const writable = toWritable(computed(() => {
|
|
898
997
|
trigger();
|
|
899
998
|
return untracked(source);
|
|
900
999
|
}, opt), set, update);
|
|
901
|
-
writable.original = source;
|
|
1000
|
+
writable.original = source.asReadonly();
|
|
902
1001
|
return writable;
|
|
903
1002
|
}
|
|
904
1003
|
|
|
@@ -1069,8 +1168,18 @@ function derived(source, optOrKey, opt) {
|
|
|
1069
1168
|
if (isMutable(source)) {
|
|
1070
1169
|
sig.mutate = (updater) => {
|
|
1071
1170
|
cnt++;
|
|
1072
|
-
|
|
1073
|
-
|
|
1171
|
+
try {
|
|
1172
|
+
sig.update(updater);
|
|
1173
|
+
// The wrapped computed evaluates its `equal` lazily — at the next read, which would
|
|
1174
|
+
// normally happen after `cnt` has already dropped back to 0. For a reference-stable
|
|
1175
|
+
// mutation that read compares the same object to itself and the version never bumps,
|
|
1176
|
+
// so dependents are never notified. Reading here, while equality is still suppressed,
|
|
1177
|
+
// forces the recompute (and version bump) inside the mutate window.
|
|
1178
|
+
untracked(sig);
|
|
1179
|
+
}
|
|
1180
|
+
finally {
|
|
1181
|
+
cnt--;
|
|
1182
|
+
}
|
|
1074
1183
|
};
|
|
1075
1184
|
sig.inline = (updater) => {
|
|
1076
1185
|
sig.mutate((prev) => {
|
|
@@ -1126,16 +1235,43 @@ function isDerivation(sig) {
|
|
|
1126
1235
|
}
|
|
1127
1236
|
|
|
1128
1237
|
function keepPrevious(src, opt) {
|
|
1238
|
+
const mutableSrc = isWritableSignal$2(src) && isMutable(src);
|
|
1239
|
+
// For a mutable source the linkedSignal's equality must be suppressible: a forwarded
|
|
1240
|
+
// `mutate` keeps the same reference, which default equality would otherwise swallow.
|
|
1241
|
+
let cnt = 0;
|
|
1242
|
+
const baseEqual = opt?.equal;
|
|
1243
|
+
const equal = mutableSrc
|
|
1244
|
+
? (a, b) => cnt > 0 ? false : baseEqual ? baseEqual(a, b) : Object.is(a, b)
|
|
1245
|
+
: baseEqual;
|
|
1129
1246
|
const persisted = linkedSignal({ ...(ngDevMode ? { debugName: "persisted" } : /* istanbul ignore next */ {}), ...opt,
|
|
1130
1247
|
source: () => src(),
|
|
1131
|
-
computation: (next, prev) => next === undefined && prev !== undefined ? prev.value : next
|
|
1248
|
+
computation: (next, prev) => next === undefined && prev !== undefined ? prev.value : next,
|
|
1249
|
+
equal });
|
|
1132
1250
|
if (isWritableSignal$2(src)) {
|
|
1133
1251
|
persisted.set = src.set;
|
|
1134
1252
|
persisted.update = src.update;
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1253
|
+
// NOTE: `asReadonly` deliberately stays the linkedSignal's own — returning the
|
|
1254
|
+
// source's readonly view would reintroduce the `undefined` flashes this wrapper exists
|
|
1255
|
+
// to prevent.
|
|
1256
|
+
if (mutableSrc) {
|
|
1257
|
+
persisted.mutate = (updater) => {
|
|
1258
|
+
cnt++;
|
|
1259
|
+
try {
|
|
1260
|
+
src.mutate(updater);
|
|
1261
|
+
// force the recompute while equality is suppressed, so the reference-stable
|
|
1262
|
+
// mutation bumps the wrapper's version (see derived.ts for the same pattern)
|
|
1263
|
+
untracked(persisted);
|
|
1264
|
+
}
|
|
1265
|
+
finally {
|
|
1266
|
+
cnt--;
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
persisted.inline = (updater) => {
|
|
1270
|
+
persisted.mutate((prev) => {
|
|
1271
|
+
updater(prev);
|
|
1272
|
+
return prev;
|
|
1273
|
+
});
|
|
1274
|
+
};
|
|
1139
1275
|
}
|
|
1140
1276
|
if (isDerivation(src)) {
|
|
1141
1277
|
persisted.from = src.from;
|
|
@@ -1193,13 +1329,18 @@ function indexArray(source, map, opt = {}) {
|
|
|
1193
1329
|
: toWritable(data, () => {
|
|
1194
1330
|
// noop
|
|
1195
1331
|
});
|
|
1332
|
+
// copy before defaulting `equal` — assigning onto `opt` would mutate a caller-owned
|
|
1333
|
+
// (possibly shared/reused) options object
|
|
1196
1334
|
if (isWritableSignal$1(data) && isMutable(data) && !opt.equal) {
|
|
1197
|
-
opt
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1335
|
+
opt = {
|
|
1336
|
+
...opt,
|
|
1337
|
+
equal: (a, b) => {
|
|
1338
|
+
if (typeof a !== typeof b)
|
|
1339
|
+
return false;
|
|
1340
|
+
if (typeof a === 'object' || typeof a === 'function')
|
|
1341
|
+
return false;
|
|
1342
|
+
return a === b;
|
|
1343
|
+
},
|
|
1203
1344
|
};
|
|
1204
1345
|
}
|
|
1205
1346
|
return linkedSignal({
|
|
@@ -1395,8 +1536,17 @@ function pooledKeys(src) {
|
|
|
1395
1536
|
for (const k in val)
|
|
1396
1537
|
if (Object.prototype.hasOwnProperty.call(val, k))
|
|
1397
1538
|
spare.add(k);
|
|
1398
|
-
if (active.size === spare.size
|
|
1399
|
-
|
|
1539
|
+
if (active.size === spare.size) {
|
|
1540
|
+
let subset = true;
|
|
1541
|
+
for (const k of active) {
|
|
1542
|
+
if (!spare.has(k)) {
|
|
1543
|
+
subset = false;
|
|
1544
|
+
break;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
if (subset)
|
|
1548
|
+
return active;
|
|
1549
|
+
}
|
|
1400
1550
|
const temp = active;
|
|
1401
1551
|
active = spare;
|
|
1402
1552
|
spare = temp;
|
|
@@ -1496,7 +1646,7 @@ const filter = (predicate) => (src) => linkedSignal({
|
|
|
1496
1646
|
computation: (next, prev) => {
|
|
1497
1647
|
if (predicate(next))
|
|
1498
1648
|
return next;
|
|
1499
|
-
return prev?.
|
|
1649
|
+
return prev?.value;
|
|
1500
1650
|
},
|
|
1501
1651
|
});
|
|
1502
1652
|
/**
|
|
@@ -1532,7 +1682,7 @@ const tap = (fn, injector) => (src) => {
|
|
|
1532
1682
|
*/
|
|
1533
1683
|
const filterWith = (predicate, initial) => (src) => linkedSignal({
|
|
1534
1684
|
source: src,
|
|
1535
|
-
computation: (next, prev) => predicate(next) ? next :
|
|
1685
|
+
computation: (next, prev) => predicate(next) ? next : prev ? prev.value : initial,
|
|
1536
1686
|
});
|
|
1537
1687
|
/**
|
|
1538
1688
|
* Emit `initial` on the first read, then mirror the source on every subsequent
|
|
@@ -1581,7 +1731,7 @@ const pairwise = () => (src) => linkedSignal({
|
|
|
1581
1731
|
*/
|
|
1582
1732
|
const scan = (reducer, seed) => (src) => linkedSignal({
|
|
1583
1733
|
source: src,
|
|
1584
|
-
computation: (next, prev) => reducer(prev
|
|
1734
|
+
computation: (next, prev) => reducer(prev ? prev.value : seed, next),
|
|
1585
1735
|
});
|
|
1586
1736
|
|
|
1587
1737
|
/**
|
|
@@ -1632,7 +1782,7 @@ function pipeable(signal) {
|
|
|
1632
1782
|
return internal;
|
|
1633
1783
|
}
|
|
1634
1784
|
/**
|
|
1635
|
-
* Create a new **writable** signal and return it as a `
|
|
1785
|
+
* Create a new **writable** signal and return it as a `PipeableSignal`.
|
|
1636
1786
|
*
|
|
1637
1787
|
* The returned value is a `WritableSignal<T>` with `.set`, `.update`, `.asReadonly`
|
|
1638
1788
|
* still available (via intersection type), plus a chainable `.pipe(...)`.
|
|
@@ -1736,6 +1886,20 @@ function pooledMap(optOrComputation, signalOpt) {
|
|
|
1736
1886
|
return pooled(toPooledOptions(optOrComputation, createEmptyMap, resetClearable, signalOpt));
|
|
1737
1887
|
}
|
|
1738
1888
|
|
|
1889
|
+
/**
|
|
1890
|
+
* @internal Run a sensor factory inside `injector` when provided, else in the ambient
|
|
1891
|
+
* injection context. Keeps every sensor's escape hatch identical and in one place.
|
|
1892
|
+
*/
|
|
1893
|
+
function runInSensorContext(injector, fn) {
|
|
1894
|
+
return injector ? runInInjectionContext(injector, fn) : fn();
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* @internal Normalize the legacy positional `debugName: string` form into {@link SensorRunOptions}.
|
|
1898
|
+
*/
|
|
1899
|
+
function coerceSensorOptions(opt) {
|
|
1900
|
+
return typeof opt === 'string' ? { debugName: opt } : (opt ?? {});
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1739
1903
|
const EVENTS = [
|
|
1740
1904
|
'chargingchange',
|
|
1741
1905
|
'levelchange',
|
|
@@ -1757,7 +1921,11 @@ const EVENTS = [
|
|
|
1757
1921
|
* });
|
|
1758
1922
|
* ```
|
|
1759
1923
|
*/
|
|
1760
|
-
function batteryStatus(
|
|
1924
|
+
function batteryStatus(opt) {
|
|
1925
|
+
const { debugName = 'batteryStatus', injector } = coerceSensorOptions(opt);
|
|
1926
|
+
return runInSensorContext(injector, () => createBatteryStatus(debugName));
|
|
1927
|
+
}
|
|
1928
|
+
function createBatteryStatus(debugName) {
|
|
1761
1929
|
if (isPlatformServer(inject(PLATFORM_ID)) ||
|
|
1762
1930
|
typeof navigator === 'undefined' ||
|
|
1763
1931
|
typeof navigator.getBattery !== 'function') {
|
|
@@ -1766,7 +1934,9 @@ function batteryStatus(debugName = 'batteryStatus') {
|
|
|
1766
1934
|
const state = signal(null, { ...(ngDevMode ? { debugName: "state" } : /* istanbul ignore next */ {}), debugName });
|
|
1767
1935
|
const abortController = new AbortController();
|
|
1768
1936
|
inject(DestroyRef).onDestroy(() => abortController.abort());
|
|
1769
|
-
navigator
|
|
1937
|
+
navigator
|
|
1938
|
+
.getBattery()
|
|
1939
|
+
.then((battery) => {
|
|
1770
1940
|
if (abortController.signal.aborted)
|
|
1771
1941
|
return;
|
|
1772
1942
|
const read = () => ({
|
|
@@ -1782,6 +1952,10 @@ function batteryStatus(debugName = 'batteryStatus') {
|
|
|
1782
1952
|
signal: abortController.signal,
|
|
1783
1953
|
});
|
|
1784
1954
|
}
|
|
1955
|
+
})
|
|
1956
|
+
.catch(() => {
|
|
1957
|
+
// getBattery() rejects (NotAllowedError) when the `battery` permissions-policy is
|
|
1958
|
+
// disallowed, e.g. in cross-origin iframes — stay `null`, same as unsupported.
|
|
1785
1959
|
});
|
|
1786
1960
|
return state.asReadonly();
|
|
1787
1961
|
}
|
|
@@ -1797,7 +1971,11 @@ function batteryStatus(debugName = 'batteryStatus') {
|
|
|
1797
1971
|
* in browsers that gate it. Errors from `navigator.clipboard.readText` are
|
|
1798
1972
|
* swallowed silently to keep the signal value stable.
|
|
1799
1973
|
*/
|
|
1800
|
-
function clipboard(
|
|
1974
|
+
function clipboard(opt) {
|
|
1975
|
+
const { debugName = 'clipboard', injector } = coerceSensorOptions(opt);
|
|
1976
|
+
return runInSensorContext(injector, () => createClipboard(debugName));
|
|
1977
|
+
}
|
|
1978
|
+
function createClipboard(debugName) {
|
|
1801
1979
|
if (isPlatformServer(inject(PLATFORM_ID)) ||
|
|
1802
1980
|
typeof navigator === 'undefined' ||
|
|
1803
1981
|
!navigator.clipboard) {
|
|
@@ -1849,7 +2027,13 @@ function observerSupported$1() {
|
|
|
1849
2027
|
* });
|
|
1850
2028
|
* ```
|
|
1851
2029
|
*/
|
|
1852
|
-
function elementSize(target
|
|
2030
|
+
function elementSize(target, opt) {
|
|
2031
|
+
return runInSensorContext(opt?.injector, () =>
|
|
2032
|
+
// the host-element default must resolve INSIDE the sensor context, not as a
|
|
2033
|
+
// parameter default (which would run before the injector wrapper)
|
|
2034
|
+
createElementSize(target ?? inject(ElementRef), opt));
|
|
2035
|
+
}
|
|
2036
|
+
function createElementSize(target, opt) {
|
|
1853
2037
|
const getElement = () => {
|
|
1854
2038
|
if (isSignal(target)) {
|
|
1855
2039
|
try {
|
|
@@ -1863,8 +2047,8 @@ function elementSize(target = inject(ElementRef), opt) {
|
|
|
1863
2047
|
return target instanceof ElementRef ? target.nativeElement : target;
|
|
1864
2048
|
};
|
|
1865
2049
|
const resolveInitialValue = () => {
|
|
1866
|
-
|
|
1867
|
-
|
|
2050
|
+
// measuring needs only getBoundingClientRect — ResizeObserver support gates
|
|
2051
|
+
// live updates, not the initial read
|
|
1868
2052
|
const el = getElement();
|
|
1869
2053
|
if (el && el.getBoundingClientRect) {
|
|
1870
2054
|
const rect = el.getBoundingClientRect();
|
|
@@ -1984,7 +2168,13 @@ function observerSupported() {
|
|
|
1984
2168
|
* }
|
|
1985
2169
|
* ```
|
|
1986
2170
|
*/
|
|
1987
|
-
function elementVisibility(target
|
|
2171
|
+
function elementVisibility(target, opt) {
|
|
2172
|
+
return runInSensorContext(opt?.injector, () =>
|
|
2173
|
+
// the host-element default must resolve INSIDE the sensor context, not as a
|
|
2174
|
+
// parameter default (which would run before the injector wrapper)
|
|
2175
|
+
createElementVisibility(target ?? inject(ElementRef), opt));
|
|
2176
|
+
}
|
|
2177
|
+
function createElementVisibility(target, opt) {
|
|
1988
2178
|
if (isPlatformServer(inject(PLATFORM_ID)) || !observerSupported()) {
|
|
1989
2179
|
const base = computed(() => undefined, {
|
|
1990
2180
|
debugName: opt?.debugName,
|
|
@@ -2054,11 +2244,18 @@ function unwrap$1(target) {
|
|
|
2054
2244
|
* }
|
|
2055
2245
|
* ```
|
|
2056
2246
|
*/
|
|
2057
|
-
function focusWithin(target
|
|
2247
|
+
function focusWithin(target, opt) {
|
|
2248
|
+
return runInSensorContext(opt?.injector, () =>
|
|
2249
|
+
// the host-element default must resolve INSIDE the sensor context, not as a
|
|
2250
|
+
// parameter default (which would run before the injector wrapper)
|
|
2251
|
+
createFocusWithin(target ?? inject(ElementRef), opt));
|
|
2252
|
+
}
|
|
2253
|
+
function createFocusWithin(target, opt) {
|
|
2254
|
+
const debugName = opt?.debugName ?? 'focusWithin';
|
|
2058
2255
|
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
2059
|
-
return computed(() => false, { debugName
|
|
2256
|
+
return computed(() => false, { debugName });
|
|
2060
2257
|
}
|
|
2061
|
-
const state = signal(false, { debugName:
|
|
2258
|
+
const state = signal(false, { ...(ngDevMode ? { debugName: "state" } : /* istanbul ignore next */ {}), debugName });
|
|
2062
2259
|
const attach = (el) => {
|
|
2063
2260
|
state.set(el.contains(document.activeElement));
|
|
2064
2261
|
const abortController = new AbortController();
|
|
@@ -2106,6 +2303,9 @@ function focusWithin(target = inject(ElementRef)) {
|
|
|
2106
2303
|
* ```
|
|
2107
2304
|
*/
|
|
2108
2305
|
function geolocation(opt) {
|
|
2306
|
+
return runInSensorContext(opt?.injector, () => createGeolocation(opt));
|
|
2307
|
+
}
|
|
2308
|
+
function createGeolocation(opt) {
|
|
2109
2309
|
if (isPlatformServer(inject(PLATFORM_ID)) || typeof navigator === 'undefined' || !navigator.geolocation) {
|
|
2110
2310
|
const sig = computed(() => null, {
|
|
2111
2311
|
debugName: opt?.debugName ?? 'geolocation',
|
|
@@ -2169,6 +2369,9 @@ const serverDate$1 = new Date();
|
|
|
2169
2369
|
* ```
|
|
2170
2370
|
*/
|
|
2171
2371
|
function idle(opt) {
|
|
2372
|
+
return runInSensorContext(opt?.injector, () => createIdle(opt));
|
|
2373
|
+
}
|
|
2374
|
+
function createIdle(opt) {
|
|
2172
2375
|
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
2173
2376
|
const sig = computed(() => false, {
|
|
2174
2377
|
debugName: opt?.debugName ?? 'idle',
|
|
@@ -2260,7 +2463,11 @@ function idle(opt) {
|
|
|
2260
2463
|
* }
|
|
2261
2464
|
* ```
|
|
2262
2465
|
*/
|
|
2263
|
-
function mediaQuery(query,
|
|
2466
|
+
function mediaQuery(query, opt) {
|
|
2467
|
+
const { debugName = 'mediaQuery', injector } = coerceSensorOptions(opt);
|
|
2468
|
+
return runInSensorContext(injector, () => createMediaQuery(query, debugName));
|
|
2469
|
+
}
|
|
2470
|
+
function createMediaQuery(query, debugName) {
|
|
2264
2471
|
if (isPlatformServer(inject(PLATFORM_ID)) ||
|
|
2265
2472
|
typeof window === 'undefined' ||
|
|
2266
2473
|
typeof window.matchMedia !== 'function' // jsdom doesn't implement matchMedia
|
|
@@ -2298,8 +2505,8 @@ function mediaQuery(query, debugName = 'mediaQuery') {
|
|
|
2298
2505
|
* });
|
|
2299
2506
|
* ```
|
|
2300
2507
|
*/
|
|
2301
|
-
function prefersDarkMode(
|
|
2302
|
-
return mediaQuery('(prefers-color-scheme: dark)',
|
|
2508
|
+
function prefersDarkMode(opt) {
|
|
2509
|
+
return mediaQuery('(prefers-color-scheme: dark)', opt);
|
|
2303
2510
|
}
|
|
2304
2511
|
/**
|
|
2305
2512
|
* Creates a read-only signal that tracks the user's OS/browser preference
|
|
@@ -2326,8 +2533,8 @@ function prefersDarkMode(debugName) {
|
|
|
2326
2533
|
* });
|
|
2327
2534
|
* ```
|
|
2328
2535
|
*/
|
|
2329
|
-
function prefersReducedMotion(
|
|
2330
|
-
return mediaQuery('(prefers-reduced-motion: reduce)',
|
|
2536
|
+
function prefersReducedMotion(opt) {
|
|
2537
|
+
return mediaQuery('(prefers-reduced-motion: reduce)', opt);
|
|
2331
2538
|
}
|
|
2332
2539
|
|
|
2333
2540
|
/**
|
|
@@ -2376,6 +2583,7 @@ function throttled(initial, opt) {
|
|
|
2376
2583
|
* // after the 500ms cooldown.
|
|
2377
2584
|
*/
|
|
2378
2585
|
function throttle(source, opt) {
|
|
2586
|
+
const eq = opt?.equal ?? getSignalEquality(source);
|
|
2379
2587
|
const ms = opt?.ms ?? 0;
|
|
2380
2588
|
const leading = opt?.leading ?? false;
|
|
2381
2589
|
const trailing = opt?.trailing ?? true;
|
|
@@ -2402,31 +2610,32 @@ function throttle(source, opt) {
|
|
|
2402
2610
|
fire();
|
|
2403
2611
|
else
|
|
2404
2612
|
pendingTrailing = trailing;
|
|
2405
|
-
|
|
2613
|
+
const onWindowEnd = () => {
|
|
2406
2614
|
timeout = undefined;
|
|
2407
2615
|
if (trailing && pendingTrailing) {
|
|
2408
2616
|
pendingTrailing = false;
|
|
2409
2617
|
fire();
|
|
2618
|
+
timeout = setTimeout(onWindowEnd, ms);
|
|
2410
2619
|
}
|
|
2411
|
-
}
|
|
2620
|
+
};
|
|
2621
|
+
timeout = setTimeout(onWindowEnd, ms);
|
|
2412
2622
|
return;
|
|
2413
2623
|
}
|
|
2414
2624
|
if (trailing)
|
|
2415
2625
|
pendingTrailing = true;
|
|
2416
2626
|
};
|
|
2417
|
-
const set = (
|
|
2418
|
-
source
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
const update = (fn) => {
|
|
2422
|
-
source.update(fn);
|
|
2627
|
+
const set = (next) => {
|
|
2628
|
+
if (eq(untracked(source), next))
|
|
2629
|
+
return;
|
|
2630
|
+
source.set(next);
|
|
2423
2631
|
tick();
|
|
2424
2632
|
};
|
|
2633
|
+
const update = (fn) => set(fn(untracked(source)));
|
|
2425
2634
|
const writable = toWritable(computed(() => {
|
|
2426
2635
|
trigger();
|
|
2427
2636
|
return untracked(source);
|
|
2428
2637
|
}, opt), set, update);
|
|
2429
|
-
writable.original = source;
|
|
2638
|
+
writable.original = source.asReadonly();
|
|
2430
2639
|
return writable;
|
|
2431
2640
|
}
|
|
2432
2641
|
|
|
@@ -2463,6 +2672,9 @@ function throttle(source, opt) {
|
|
|
2463
2672
|
* ```
|
|
2464
2673
|
*/
|
|
2465
2674
|
function mousePosition(opt) {
|
|
2675
|
+
return runInSensorContext(opt?.injector, () => createMousePosition(opt));
|
|
2676
|
+
}
|
|
2677
|
+
function createMousePosition(opt) {
|
|
2466
2678
|
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
2467
2679
|
const base = computed(() => ({
|
|
2468
2680
|
x: 0,
|
|
@@ -2474,8 +2686,12 @@ function mousePosition(opt) {
|
|
|
2474
2686
|
return base;
|
|
2475
2687
|
}
|
|
2476
2688
|
const { target = window, coordinateSpace = 'client', touch = false, debugName = 'mousePosition', throttle = 100, } = opt ?? {};
|
|
2477
|
-
const
|
|
2478
|
-
|
|
2689
|
+
const resolve = (t) => {
|
|
2690
|
+
if (!t)
|
|
2691
|
+
return null;
|
|
2692
|
+
return t instanceof ElementRef ? t.nativeElement : t;
|
|
2693
|
+
};
|
|
2694
|
+
if (!isSignal(target) && !resolve(target)) {
|
|
2479
2695
|
if (isDevMode())
|
|
2480
2696
|
console.warn('mousePosition: Target element not found.');
|
|
2481
2697
|
const base = computed(() => ({
|
|
@@ -2498,7 +2714,7 @@ function mousePosition(opt) {
|
|
|
2498
2714
|
x = coordinateSpace === 'page' ? event.pageX : event.clientX;
|
|
2499
2715
|
y = coordinateSpace === 'page' ? event.pageY : event.clientY;
|
|
2500
2716
|
}
|
|
2501
|
-
else if (event.touches
|
|
2717
|
+
else if (event.touches?.length > 0) {
|
|
2502
2718
|
const firstTouch = event.touches[0];
|
|
2503
2719
|
x = coordinateSpace === 'page' ? firstTouch.pageX : firstTouch.clientX;
|
|
2504
2720
|
y = coordinateSpace === 'page' ? firstTouch.pageY : firstTouch.clientY;
|
|
@@ -2508,16 +2724,36 @@ function mousePosition(opt) {
|
|
|
2508
2724
|
}
|
|
2509
2725
|
pos.set({ x, y });
|
|
2510
2726
|
};
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2727
|
+
// passive: the handler never calls preventDefault, and a non-passive touchmove on
|
|
2728
|
+
// window forces the browser to wait on JS before scrolling (scroll jank on touch)
|
|
2729
|
+
const attach = (el) => {
|
|
2730
|
+
const controller = new AbortController();
|
|
2731
|
+
el.addEventListener('mousemove', updatePosition, {
|
|
2732
|
+
passive: true,
|
|
2733
|
+
signal: controller.signal,
|
|
2734
|
+
});
|
|
2517
2735
|
if (touch) {
|
|
2518
|
-
|
|
2736
|
+
el.addEventListener('touchmove', updatePosition, {
|
|
2737
|
+
passive: true,
|
|
2738
|
+
signal: controller.signal,
|
|
2739
|
+
});
|
|
2519
2740
|
}
|
|
2520
|
-
|
|
2741
|
+
return () => controller.abort();
|
|
2742
|
+
};
|
|
2743
|
+
if (isSignal(target)) {
|
|
2744
|
+
// re-attach whenever the signal resolves to a (new) element — covers viewChild
|
|
2745
|
+
effect((cleanup) => {
|
|
2746
|
+
const el = resolve(target());
|
|
2747
|
+
if (!el)
|
|
2748
|
+
return;
|
|
2749
|
+
cleanup(attach(el));
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2752
|
+
else {
|
|
2753
|
+
const el = resolve(target);
|
|
2754
|
+
if (el)
|
|
2755
|
+
inject(DestroyRef).onDestroy(attach(el));
|
|
2756
|
+
}
|
|
2521
2757
|
const base = pos.asReadonly();
|
|
2522
2758
|
base.unthrottled = pos.original;
|
|
2523
2759
|
return base;
|
|
@@ -2531,7 +2767,8 @@ const serverDate = new Date();
|
|
|
2531
2767
|
* An additional `since` signal is attached, tracking when the status last changed.
|
|
2532
2768
|
* It's SSR-safe and automatically cleans up its event listeners.
|
|
2533
2769
|
*
|
|
2534
|
-
* @param
|
|
2770
|
+
* @param opt Optional debug name for the signal, or a {@link SensorRunOptions} object
|
|
2771
|
+
* (with an optional `injector` for creation outside an injection context).
|
|
2535
2772
|
* @returns A `NetworkStatusSignal` instance.
|
|
2536
2773
|
*
|
|
2537
2774
|
* @example
|
|
@@ -2542,7 +2779,11 @@ const serverDate = new Date();
|
|
|
2542
2779
|
* });
|
|
2543
2780
|
* ```
|
|
2544
2781
|
*/
|
|
2545
|
-
function networkStatus(
|
|
2782
|
+
function networkStatus(opt) {
|
|
2783
|
+
const { debugName = 'networkStatus', injector } = coerceSensorOptions(opt);
|
|
2784
|
+
return runInSensorContext(injector, () => createNetworkStatus(debugName));
|
|
2785
|
+
}
|
|
2786
|
+
function createNetworkStatus(debugName) {
|
|
2546
2787
|
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
2547
2788
|
const sig = computed(() => true, {
|
|
2548
2789
|
debugName,
|
|
@@ -2592,7 +2833,11 @@ const SSR_FALLBACK = {
|
|
|
2592
2833
|
* });
|
|
2593
2834
|
* ```
|
|
2594
2835
|
*/
|
|
2595
|
-
function orientation(
|
|
2836
|
+
function orientation(opt) {
|
|
2837
|
+
const { debugName = 'orientation', injector } = coerceSensorOptions(opt);
|
|
2838
|
+
return runInSensorContext(injector, () => createOrientation(debugName));
|
|
2839
|
+
}
|
|
2840
|
+
function createOrientation(debugName) {
|
|
2596
2841
|
if (isPlatformServer(inject(PLATFORM_ID)) ||
|
|
2597
2842
|
typeof screen === 'undefined' ||
|
|
2598
2843
|
!screen.orientation) {
|
|
@@ -2619,7 +2864,8 @@ function orientation(debugName = 'orientation') {
|
|
|
2619
2864
|
* The primitive is SSR-safe and automatically cleans up its event listeners
|
|
2620
2865
|
* when the creating context is destroyed.
|
|
2621
2866
|
*
|
|
2622
|
-
* @param
|
|
2867
|
+
* @param opt Optional debug name for the signal, or a {@link SensorRunOptions} object
|
|
2868
|
+
* (with an optional `injector` for creation outside an injection context).
|
|
2623
2869
|
* @returns A read-only `Signal<DocumentVisibilityState>`. On the server,
|
|
2624
2870
|
* it returns a static signal with a value of `'visible'`.
|
|
2625
2871
|
*
|
|
@@ -2647,7 +2893,11 @@ function orientation(debugName = 'orientation') {
|
|
|
2647
2893
|
* }
|
|
2648
2894
|
* ```
|
|
2649
2895
|
*/
|
|
2650
|
-
function pageVisibility(
|
|
2896
|
+
function pageVisibility(opt) {
|
|
2897
|
+
const { debugName = 'pageVisibility', injector } = coerceSensorOptions(opt);
|
|
2898
|
+
return runInSensorContext(injector, () => createPageVisibility(debugName));
|
|
2899
|
+
}
|
|
2900
|
+
function createPageVisibility(debugName) {
|
|
2651
2901
|
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
2652
2902
|
return computed(() => 'visible', { debugName });
|
|
2653
2903
|
}
|
|
@@ -2679,31 +2929,25 @@ function pageVisibility(debugName = 'pageVisibility') {
|
|
|
2679
2929
|
* selector: 'app-scroll-tracker',
|
|
2680
2930
|
* template: `
|
|
2681
2931
|
* <p>Window Scroll: X: {{ windowScroll().x }}, Y: {{ windowScroll().y }}</p>
|
|
2682
|
-
* <
|
|
2683
|
-
* <div style="height: 400px; width: 400px;">Scroll me!</div>
|
|
2684
|
-
* </div>
|
|
2685
|
-
* @if (divScroll()) {
|
|
2686
|
-
* <p>Div Scroll: X: {{ divScroll().x }}, Y: {{ divScroll().y }}</p>
|
|
2687
|
-
* }
|
|
2932
|
+
* <p>Host Scroll: X: {{ hostScroll().x }}, Y: {{ hostScroll().y }}</p>
|
|
2688
2933
|
* `
|
|
2689
2934
|
* })
|
|
2690
2935
|
* export class ScrollTrackerComponent {
|
|
2691
2936
|
* readonly windowScroll = scrollPosition(); // Defaults to window
|
|
2937
|
+
* // Signal targets (e.g. viewChild) attach once the element exists:
|
|
2692
2938
|
* readonly scrollableDiv = viewChild<ElementRef<HTMLDivElement>>('scrollableDiv');
|
|
2693
|
-
* readonly divScroll = scrollPosition({ target: this.scrollableDiv
|
|
2939
|
+
* readonly divScroll = scrollPosition({ target: this.scrollableDiv });
|
|
2694
2940
|
*
|
|
2695
2941
|
* constructor() {
|
|
2696
|
-
* effect(() =>
|
|
2697
|
-
* console.log('Window scrolled to:', this.windowScroll());
|
|
2698
|
-
* if (this.divScroll()) {
|
|
2699
|
-
* console.log('Div scrolled to:', this.divScroll());
|
|
2700
|
-
* }
|
|
2701
|
-
* });
|
|
2942
|
+
* effect(() => console.log('Window scrolled to:', this.windowScroll()));
|
|
2702
2943
|
* }
|
|
2703
2944
|
* }
|
|
2704
2945
|
* ```
|
|
2705
2946
|
*/
|
|
2706
2947
|
function scrollPosition(opt) {
|
|
2948
|
+
return runInSensorContext(opt?.injector, () => createScrollPosition(opt));
|
|
2949
|
+
}
|
|
2950
|
+
function createScrollPosition(opt) {
|
|
2707
2951
|
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
2708
2952
|
const base = computed(() => ({
|
|
2709
2953
|
x: 0,
|
|
@@ -2715,43 +2959,44 @@ function scrollPosition(opt) {
|
|
|
2715
2959
|
return base;
|
|
2716
2960
|
}
|
|
2717
2961
|
const { target = globalThis.window, throttle = 100, debugName = 'scrollPosition', } = opt || {};
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
return {
|
|
2733
|
-
x: target.nativeElement.scrollLeft,
|
|
2734
|
-
y: target.nativeElement.scrollTop,
|
|
2735
|
-
};
|
|
2736
|
-
};
|
|
2737
|
-
}
|
|
2738
|
-
else {
|
|
2739
|
-
element = target;
|
|
2740
|
-
getScrollPosition = () => {
|
|
2741
|
-
return {
|
|
2742
|
-
x: target.scrollLeft,
|
|
2743
|
-
y: target.scrollTop,
|
|
2744
|
-
};
|
|
2745
|
-
};
|
|
2746
|
-
}
|
|
2747
|
-
const state = throttled(getScrollPosition(), {
|
|
2962
|
+
const resolve = (t) => {
|
|
2963
|
+
if (!t)
|
|
2964
|
+
return null;
|
|
2965
|
+
return t instanceof ElementRef ? t.nativeElement : t;
|
|
2966
|
+
};
|
|
2967
|
+
const isWindow = (el) => el.window === el;
|
|
2968
|
+
const readPosition = (el) => isWindow(el)
|
|
2969
|
+
? {
|
|
2970
|
+
x: el.scrollX ?? el.pageXOffset ?? 0,
|
|
2971
|
+
y: el.scrollY ?? el.pageYOffset ?? 0,
|
|
2972
|
+
}
|
|
2973
|
+
: { x: el.scrollLeft, y: el.scrollTop };
|
|
2974
|
+
const initial = resolve(isSignal(target) ? untracked(target) : target);
|
|
2975
|
+
const state = throttled(initial ? readPosition(initial) : { x: 0, y: 0 }, {
|
|
2748
2976
|
debugName,
|
|
2749
2977
|
equal: (a, b) => a.x === b.x && a.y === b.y,
|
|
2750
2978
|
ms: throttle,
|
|
2751
2979
|
});
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2980
|
+
if (isSignal(target)) {
|
|
2981
|
+
// re-attach whenever the signal resolves to a (new) element — covers viewChild
|
|
2982
|
+
effect((cleanup) => {
|
|
2983
|
+
const el = resolve(target());
|
|
2984
|
+
if (!el)
|
|
2985
|
+
return;
|
|
2986
|
+
state.set(readPosition(el)); // sync to the new element immediately
|
|
2987
|
+
const onScroll = () => state.set(readPosition(el));
|
|
2988
|
+
el.addEventListener('scroll', onScroll, { passive: true });
|
|
2989
|
+
cleanup(() => el.removeEventListener('scroll', onScroll));
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
2992
|
+
else {
|
|
2993
|
+
const el = resolve(target);
|
|
2994
|
+
if (el) {
|
|
2995
|
+
const onScroll = () => state.set(readPosition(el));
|
|
2996
|
+
el.addEventListener('scroll', onScroll, { passive: true });
|
|
2997
|
+
inject(DestroyRef).onDestroy(() => el.removeEventListener('scroll', onScroll));
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
2755
3000
|
const base = state.asReadonly();
|
|
2756
3001
|
base.unthrottled = state.original;
|
|
2757
3002
|
return base;
|
|
@@ -2799,6 +3044,9 @@ function scrollPosition(opt) {
|
|
|
2799
3044
|
* ```
|
|
2800
3045
|
*/
|
|
2801
3046
|
function windowSize(opt) {
|
|
3047
|
+
return runInSensorContext(opt?.injector, () => createWindowSize(opt));
|
|
3048
|
+
}
|
|
3049
|
+
function createWindowSize(opt) {
|
|
2802
3050
|
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
2803
3051
|
const base = computed(() => ({
|
|
2804
3052
|
width: 1024,
|
|
@@ -2835,17 +3083,19 @@ function sensor(type, options) {
|
|
|
2835
3083
|
case 'mousePosition':
|
|
2836
3084
|
return mousePosition(opts);
|
|
2837
3085
|
case 'networkStatus':
|
|
2838
|
-
return networkStatus(opts
|
|
3086
|
+
return networkStatus(opts);
|
|
2839
3087
|
case 'pageVisibility':
|
|
2840
|
-
return pageVisibility(opts
|
|
3088
|
+
return pageVisibility(opts);
|
|
2841
3089
|
case 'darkMode':
|
|
2842
3090
|
case 'dark-mode':
|
|
2843
|
-
return prefersDarkMode(opts
|
|
3091
|
+
return prefersDarkMode(opts);
|
|
2844
3092
|
case 'reducedMotion':
|
|
2845
3093
|
case 'reduced-motion':
|
|
2846
|
-
return prefersReducedMotion(opts
|
|
3094
|
+
return prefersReducedMotion(opts);
|
|
2847
3095
|
case 'mediaQuery':
|
|
2848
|
-
|
|
3096
|
+
if (typeof opts?.query !== 'string')
|
|
3097
|
+
throw new Error(`sensor('mediaQuery') requires a 'query' option, e.g. sensor('mediaQuery', { query: '(min-width: 1024px)' })`);
|
|
3098
|
+
return mediaQuery(opts.query, opts);
|
|
2849
3099
|
case 'windowSize':
|
|
2850
3100
|
return windowSize(opts);
|
|
2851
3101
|
case 'scrollPosition':
|
|
@@ -2857,15 +3107,15 @@ function sensor(type, options) {
|
|
|
2857
3107
|
case 'geolocation':
|
|
2858
3108
|
return geolocation(opts);
|
|
2859
3109
|
case 'clipboard':
|
|
2860
|
-
return clipboard(opts
|
|
3110
|
+
return clipboard(opts);
|
|
2861
3111
|
case 'orientation':
|
|
2862
|
-
return orientation(opts
|
|
3112
|
+
return orientation(opts);
|
|
2863
3113
|
case 'batteryStatus':
|
|
2864
|
-
return batteryStatus(opts
|
|
3114
|
+
return batteryStatus(opts);
|
|
2865
3115
|
case 'idle':
|
|
2866
3116
|
return idle(opts);
|
|
2867
3117
|
case 'focusWithin':
|
|
2868
|
-
return focusWithin(opts?.target);
|
|
3118
|
+
return focusWithin(opts?.target, opts);
|
|
2869
3119
|
default:
|
|
2870
3120
|
throw new Error(`Unknown sensor type: ${type}`);
|
|
2871
3121
|
}
|
|
@@ -2919,16 +3169,24 @@ function signalFromEvent(target, eventName, initial, projectOrOpt, maybeOpt) {
|
|
|
2919
3169
|
else
|
|
2920
3170
|
state.set(event);
|
|
2921
3171
|
};
|
|
2922
|
-
const { destroyRef: providedDestroyRef,
|
|
3172
|
+
const { destroyRef: providedDestroyRef,
|
|
3173
|
+
// strip non-listener keys so they don't leak into addEventListener options
|
|
3174
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
3175
|
+
injector: _injector,
|
|
3176
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
3177
|
+
debugName: _debugName, ...listenerOpts } = opt ?? {};
|
|
2923
3178
|
if (isSignal(target)) {
|
|
2924
3179
|
const targetSig = target;
|
|
2925
|
-
effect((cleanup) => {
|
|
3180
|
+
const effectRef = effect((cleanup) => {
|
|
2926
3181
|
const resolved = unwrap(targetSig());
|
|
2927
3182
|
if (!resolved)
|
|
2928
3183
|
return;
|
|
2929
3184
|
resolved.addEventListener(eventName, handler, listenerOpts);
|
|
2930
3185
|
cleanup(() => resolved.removeEventListener(eventName, handler, listenerOpts));
|
|
2931
|
-
}, { injector });
|
|
3186
|
+
}, { ...(ngDevMode ? { debugName: "effectRef" } : /* istanbul ignore next */ {}), injector });
|
|
3187
|
+
// honor an explicit destroyRef for signal targets too — the effect would otherwise
|
|
3188
|
+
// only follow the injector's lifetime, contradicting the documented option
|
|
3189
|
+
providedDestroyRef?.onDestroy(() => effectRef.destroy());
|
|
2932
3190
|
}
|
|
2933
3191
|
else {
|
|
2934
3192
|
const resolved = unwrap(target);
|
|
@@ -3017,7 +3275,8 @@ function alwaysFalse() {
|
|
|
3017
3275
|
* @internal Attaches a lazy, memoized leaf probe to a store node. The probe (`() => boolean`)
|
|
3018
3276
|
* closes over the node's value signal and its (stable) vivify setting, building the backing
|
|
3019
3277
|
* `computed` on first call so leaf-ness tracks the live value reactively without taxing every
|
|
3020
|
-
* node access.
|
|
3278
|
+
* node access. Under `noUnionLeaves` the caller promises shapes never flip, so the probe is
|
|
3279
|
+
* resolved once from the first sample and frozen as a constant. Idempotent.
|
|
3021
3280
|
*/
|
|
3022
3281
|
function markAsLeaf(sig, value, vivifyEnabled, noUnionLeaves) {
|
|
3023
3282
|
if (typeof sig[LEAF] !== 'function') {
|
|
@@ -3025,13 +3284,11 @@ function markAsLeaf(sig, value, vivifyEnabled, noUnionLeaves) {
|
|
|
3025
3284
|
const probe = () => {
|
|
3026
3285
|
if (memo)
|
|
3027
3286
|
return memo();
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
: alwaysFalse
|
|
3034
|
-
: computed(() => isLeafValue(value(), vivifyEnabled));
|
|
3287
|
+
memo = noUnionLeaves
|
|
3288
|
+
? isLeafValue(untracked(value), vivifyEnabled)
|
|
3289
|
+
? alwaysTrue
|
|
3290
|
+
: alwaysFalse
|
|
3291
|
+
: computed(() => isLeafValue(value(), vivifyEnabled));
|
|
3035
3292
|
return memo();
|
|
3036
3293
|
};
|
|
3037
3294
|
Object.defineProperty(sig, LEAF, {
|
|
@@ -3119,6 +3376,40 @@ function resolveVivify(sample, option) {
|
|
|
3119
3376
|
function hasOwnKey(value, key) {
|
|
3120
3377
|
return value != null && Object.hasOwn(value, key);
|
|
3121
3378
|
}
|
|
3379
|
+
/**
|
|
3380
|
+
* @internal
|
|
3381
|
+
* Builds the `onChange` for the fallback (non-record container) derivation branch. For an
|
|
3382
|
+
* immutable source the container is copied before the write — returning the same mutated
|
|
3383
|
+
* reference would let the source's equality cut propagation (leaving child signals permanently
|
|
3384
|
+
* stale) and alias the caller's original object, breaking the structural-sharing contract
|
|
3385
|
+
* `forkStore` relies on. For a mutable source the write goes through `mutate`, so the chain's
|
|
3386
|
+
* force-notify engages (plain `update` with the same reference would never notify).
|
|
3387
|
+
*/
|
|
3388
|
+
function createFallbackOnChange(target, prop, vivifyFn, isMutableSource) {
|
|
3389
|
+
const write = (newValue) => (v) => {
|
|
3390
|
+
const container = vivifyFn(v, prop);
|
|
3391
|
+
if (container === null || container === undefined)
|
|
3392
|
+
return container;
|
|
3393
|
+
const next = isMutableSource
|
|
3394
|
+
? container
|
|
3395
|
+
: Array.isArray(container)
|
|
3396
|
+
? container.slice()
|
|
3397
|
+
: isRecord(container)
|
|
3398
|
+
? { ...container }
|
|
3399
|
+
: container; // non-plain leaf (Date/class instance): legacy in-place attempt
|
|
3400
|
+
try {
|
|
3401
|
+
next[prop] = newValue;
|
|
3402
|
+
}
|
|
3403
|
+
catch (e) {
|
|
3404
|
+
if (isDevMode())
|
|
3405
|
+
console.error(`[store] Failed to set property "${String(prop)}"`, e);
|
|
3406
|
+
}
|
|
3407
|
+
return next;
|
|
3408
|
+
};
|
|
3409
|
+
return isMutableSource
|
|
3410
|
+
? (newValue) => target.mutate(write(newValue))
|
|
3411
|
+
: (newValue) => target.update(write(newValue));
|
|
3412
|
+
}
|
|
3122
3413
|
/**
|
|
3123
3414
|
* @internal
|
|
3124
3415
|
* Makes an array store
|
|
@@ -3142,7 +3433,9 @@ function toArrayStore(source, injector, vivify, noUnionLeaves = false) {
|
|
|
3142
3433
|
const idx = +prop;
|
|
3143
3434
|
return idx >= 0 && idx < untracked(lengthSignal);
|
|
3144
3435
|
}
|
|
3145
|
-
|
|
3436
|
+
const v = untracked(source);
|
|
3437
|
+
// nullish node values are routinely descended with vivify on — `in` must not throw
|
|
3438
|
+
return v == null ? false : Reflect.has(v, prop);
|
|
3146
3439
|
},
|
|
3147
3440
|
ownKeys() {
|
|
3148
3441
|
const v = untracked(source);
|
|
@@ -3179,7 +3472,9 @@ function toArrayStore(source, injector, vivify, noUnionLeaves = false) {
|
|
|
3179
3472
|
return lengthSignal;
|
|
3180
3473
|
if (prop === Symbol.iterator) {
|
|
3181
3474
|
return function* () {
|
|
3182
|
-
|
|
3475
|
+
// read length reactively: a spread/for-of inside a computed/effect must re-run
|
|
3476
|
+
// when items are added or removed, not only when already-read elements change
|
|
3477
|
+
for (let i = 0; i < lengthSignal(); i++) {
|
|
3183
3478
|
yield receiver[i];
|
|
3184
3479
|
}
|
|
3185
3480
|
};
|
|
@@ -3218,19 +3513,8 @@ function toArrayStore(source, injector, vivify, noUnionLeaves = false) {
|
|
|
3218
3513
|
})
|
|
3219
3514
|
: derived(target, {
|
|
3220
3515
|
from: (v) => v?.[idx],
|
|
3221
|
-
onChange: (
|
|
3222
|
-
|
|
3223
|
-
if (container === null || container === undefined)
|
|
3224
|
-
return container;
|
|
3225
|
-
try {
|
|
3226
|
-
container[idx] = newValue;
|
|
3227
|
-
}
|
|
3228
|
-
catch (e) {
|
|
3229
|
-
if (isDevMode())
|
|
3230
|
-
console.error(`[store] Failed to set property "${String(idx)}"`, e);
|
|
3231
|
-
}
|
|
3232
|
-
return container;
|
|
3233
|
-
}),
|
|
3516
|
+
onChange: createFallbackOnChange(target, idx, vivifyFn, isMutableSource),
|
|
3517
|
+
equal: equalFn,
|
|
3234
3518
|
});
|
|
3235
3519
|
const childSample = untracked(computation);
|
|
3236
3520
|
const childVivify = resolveVivify(childSample, vivify);
|
|
@@ -3250,6 +3534,13 @@ function toArrayStore(source, injector, vivify, noUnionLeaves = false) {
|
|
|
3250
3534
|
/**
|
|
3251
3535
|
* Converts a Signal into a deep-observable Store.
|
|
3252
3536
|
* Accessing nested properties returns a derived Signal of that path.
|
|
3537
|
+
*
|
|
3538
|
+
* @remarks
|
|
3539
|
+
* A child's *container kind* (array store vs object store) is resolved when the child is
|
|
3540
|
+
* first accessed and cached with the proxy. Leaf↔substore flips are tracked reactively, but a
|
|
3541
|
+
* union-typed node that later flips between an array and a record keeps its original trap set —
|
|
3542
|
+
* if you need that, re-model the union as `{ kind: ..., value: ... }` instead.
|
|
3543
|
+
*
|
|
3253
3544
|
* @example
|
|
3254
3545
|
* const state = store({ user: { name: 'John' } });
|
|
3255
3546
|
* const nameSignal = state.user.name; // WritableSignal<string>
|
|
@@ -3332,19 +3623,8 @@ function toStore(source, injector, vivify = false, noUnionLeaves = false) {
|
|
|
3332
3623
|
? derived(target, prop, { equal: equalFn, vivify: nodeVivify })
|
|
3333
3624
|
: derived(target, {
|
|
3334
3625
|
from: (v) => v?.[prop],
|
|
3335
|
-
onChange: (
|
|
3336
|
-
|
|
3337
|
-
if (container === null || container === undefined)
|
|
3338
|
-
return container;
|
|
3339
|
-
try {
|
|
3340
|
-
container[prop] = newValue;
|
|
3341
|
-
}
|
|
3342
|
-
catch (e) {
|
|
3343
|
-
if (isDevMode())
|
|
3344
|
-
console.error(`[store] Failed to set property "${String(prop)}"`, e);
|
|
3345
|
-
}
|
|
3346
|
-
return container;
|
|
3347
|
-
}),
|
|
3626
|
+
onChange: createFallbackOnChange(target, prop, vivifyFn, isMutableSource),
|
|
3627
|
+
equal: equalFn,
|
|
3348
3628
|
});
|
|
3349
3629
|
const childSample = untracked(computation);
|
|
3350
3630
|
const childVivify = resolveVivify(childSample, vivify);
|
|
@@ -3487,7 +3767,12 @@ function merge3(ancestor, mine, theirs) {
|
|
|
3487
3767
|
if (isPlainRecord(mine) && isPlainRecord(theirs) && isPlainRecord(ancestor)) {
|
|
3488
3768
|
const out = { ...theirs };
|
|
3489
3769
|
for (const key of new Set([...Object.keys(mine), ...Object.keys(theirs)])) {
|
|
3490
|
-
|
|
3770
|
+
const merged = merge3(ancestor[key], mine[key], theirs[key]);
|
|
3771
|
+
// a key deleted on the fork must commit as ABSENT, not as an explicit `undefined`
|
|
3772
|
+
if (merged === undefined && !(key in mine))
|
|
3773
|
+
delete out[key];
|
|
3774
|
+
else
|
|
3775
|
+
out[key] = merged;
|
|
3491
3776
|
}
|
|
3492
3777
|
return out;
|
|
3493
3778
|
}
|
|
@@ -3539,8 +3824,8 @@ const noopStore = {
|
|
|
3539
3824
|
*
|
|
3540
3825
|
* @template T The type of value held by the signal and stored (after serialization).
|
|
3541
3826
|
* @param fallback The default value of type `T` to use when no value is found in storage
|
|
3542
|
-
* or when deserialization fails.
|
|
3543
|
-
*
|
|
3827
|
+
* or when deserialization fails. A stored value (including a legitimate `null` for a
|
|
3828
|
+
* nullable `T`) always round-trips; the fallback only surfaces when the entry is absent.
|
|
3544
3829
|
* @param options Configuration options (`CreateStoredOptions<T>`). Requires at least the `key`.
|
|
3545
3830
|
* @returns A `StoredSignal<T>` instance. This signal behaves like a standard `WritableSignal<T>`,
|
|
3546
3831
|
* but its value is persisted. It includes a `.clear()` method to remove the item from storage
|
|
@@ -3553,7 +3838,8 @@ const noopStore = {
|
|
|
3553
3838
|
* - **Error Handling:** Catches and logs errors during serialization/deserialization in dev mode.
|
|
3554
3839
|
* - **Tab Sync:** If `syncTabs` is true, listens to `storage` events to keep the signal value
|
|
3555
3840
|
* consistent across browser tabs using the same key. Cleanup is handled automatically
|
|
3556
|
-
* using `DestroyRef`.
|
|
3841
|
+
* using `DestroyRef`. Web Storage only: the `storage` event never fires for custom `store`
|
|
3842
|
+
* adapters, so `syncTabs` has no effect with one.
|
|
3557
3843
|
* - **Removal:** Use the `.clear()` method on the returned signal to remove the item from storage.
|
|
3558
3844
|
* Setting the signal to the fallback value will store the fallback value, not remove the item.
|
|
3559
3845
|
*
|
|
@@ -3588,25 +3874,28 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
3588
3874
|
: isSignal(key)
|
|
3589
3875
|
? key
|
|
3590
3876
|
: computed(key);
|
|
3877
|
+
// "no stored value" marker — distinct from `null`/`undefined`, so a nullable `T` can
|
|
3878
|
+
// round-trip a legitimate `null` through `set` instead of it acting like `clear()`
|
|
3879
|
+
const EMPTY = Symbol();
|
|
3591
3880
|
const getValue = (key) => {
|
|
3592
3881
|
const found = store.getItem(key);
|
|
3593
3882
|
if (found === null)
|
|
3594
|
-
return
|
|
3883
|
+
return EMPTY;
|
|
3595
3884
|
try {
|
|
3596
3885
|
const deserialized = deserialize(found);
|
|
3597
3886
|
if (!validate(deserialized))
|
|
3598
|
-
return
|
|
3887
|
+
return EMPTY;
|
|
3599
3888
|
return deserialized;
|
|
3600
3889
|
}
|
|
3601
3890
|
catch (err) {
|
|
3602
3891
|
if (isDevMode())
|
|
3603
3892
|
console.error(`Failed to parse stored value for key "${key}":`, err);
|
|
3604
|
-
return
|
|
3893
|
+
return EMPTY;
|
|
3605
3894
|
}
|
|
3606
3895
|
};
|
|
3607
3896
|
const storeValue = (key, value) => {
|
|
3608
3897
|
try {
|
|
3609
|
-
if (value ===
|
|
3898
|
+
if (value === EMPTY)
|
|
3610
3899
|
return store.removeItem(key);
|
|
3611
3900
|
const serialized = serialize(value);
|
|
3612
3901
|
store.setItem(key, serialized);
|
|
@@ -3623,9 +3912,9 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
3623
3912
|
const initialKey = untracked(keySig);
|
|
3624
3913
|
const internal = signal(getValue(initialKey), { ...(ngDevMode ? { debugName: "internal" } : /* istanbul ignore next */ {}), ...opt,
|
|
3625
3914
|
equal: (a, b) => {
|
|
3626
|
-
if (a ===
|
|
3915
|
+
if (a === EMPTY && b === EMPTY)
|
|
3627
3916
|
return true;
|
|
3628
|
-
if (a ===
|
|
3917
|
+
if (a === EMPTY || b === EMPTY)
|
|
3629
3918
|
return false;
|
|
3630
3919
|
return equal(a, b);
|
|
3631
3920
|
} });
|
|
@@ -3660,19 +3949,27 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
3660
3949
|
if (syncTabs && !isServer) {
|
|
3661
3950
|
const destroyRef = inject(DestroyRef);
|
|
3662
3951
|
const sync = (e) => {
|
|
3952
|
+
// `storage` events only describe Web Storage — ignore events for a different
|
|
3953
|
+
// storage area (or any event when a custom adapter is configured), otherwise an
|
|
3954
|
+
// unrelated localStorage write with the same key string corrupts our state
|
|
3955
|
+
if (e.storageArea !== store)
|
|
3956
|
+
return;
|
|
3663
3957
|
if (e.key !== untracked(keySig))
|
|
3664
3958
|
return;
|
|
3665
3959
|
if (e.newValue === null)
|
|
3666
|
-
internal.set(
|
|
3960
|
+
internal.set(EMPTY);
|
|
3667
3961
|
else
|
|
3668
3962
|
internal.set(getValue(e.key));
|
|
3669
3963
|
};
|
|
3670
3964
|
window.addEventListener('storage', sync);
|
|
3671
3965
|
destroyRef.onDestroy(() => window.removeEventListener('storage', sync));
|
|
3672
3966
|
}
|
|
3673
|
-
const writable = toWritable(computed(() =>
|
|
3967
|
+
const writable = toWritable(computed(() => {
|
|
3968
|
+
const v = internal();
|
|
3969
|
+
return v === EMPTY ? fallback : v;
|
|
3970
|
+
}, opt), internal.set);
|
|
3674
3971
|
writable.clear = () => {
|
|
3675
|
-
internal.set(
|
|
3972
|
+
internal.set(EMPTY);
|
|
3676
3973
|
};
|
|
3677
3974
|
writable.key = keySig;
|
|
3678
3975
|
return writable;
|
|
@@ -3682,7 +3979,6 @@ class MessageBus {
|
|
|
3682
3979
|
channel = new BroadcastChannel('mmstack-tab-sync-bus');
|
|
3683
3980
|
listeners = new Map();
|
|
3684
3981
|
subscribe(id, listener) {
|
|
3685
|
-
this.unsubscribe(id); // Ensure no duplicate listeners
|
|
3686
3982
|
const wrapped = (ev) => {
|
|
3687
3983
|
try {
|
|
3688
3984
|
if (ev.data?.id === id)
|
|
@@ -3693,28 +3989,43 @@ class MessageBus {
|
|
|
3693
3989
|
}
|
|
3694
3990
|
};
|
|
3695
3991
|
this.channel.addEventListener('message', wrapped);
|
|
3696
|
-
this.listeners.
|
|
3992
|
+
let set = this.listeners.get(id);
|
|
3993
|
+
if (!set) {
|
|
3994
|
+
set = new Set();
|
|
3995
|
+
this.listeners.set(id, set);
|
|
3996
|
+
}
|
|
3997
|
+
set.add(wrapped);
|
|
3697
3998
|
return {
|
|
3698
|
-
unsub: (
|
|
3699
|
-
|
|
3999
|
+
unsub: () => {
|
|
4000
|
+
this.channel.removeEventListener('message', wrapped);
|
|
4001
|
+
const cur = this.listeners.get(id);
|
|
4002
|
+
if (!cur)
|
|
4003
|
+
return;
|
|
4004
|
+
cur.delete(wrapped);
|
|
4005
|
+
if (cur.size === 0)
|
|
4006
|
+
this.listeners.delete(id);
|
|
4007
|
+
},
|
|
4008
|
+
post: (value) => this.channel.postMessage({ id, value }),
|
|
3700
4009
|
};
|
|
3701
4010
|
}
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
return;
|
|
3706
|
-
this.channel.removeEventListener('message', listener);
|
|
3707
|
-
this.listeners.delete(id);
|
|
4011
|
+
ngOnDestroy() {
|
|
4012
|
+
this.channel.close();
|
|
4013
|
+
this.listeners.clear();
|
|
3708
4014
|
}
|
|
3709
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.
|
|
3710
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.
|
|
4015
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MessageBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4016
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MessageBus, providedIn: 'root' });
|
|
3711
4017
|
}
|
|
3712
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.
|
|
4018
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MessageBus, decorators: [{
|
|
3713
4019
|
type: Injectable,
|
|
3714
4020
|
args: [{
|
|
3715
4021
|
providedIn: 'root',
|
|
3716
4022
|
}]
|
|
3717
4023
|
}] });
|
|
4024
|
+
/**
|
|
4025
|
+
* @deprecated The generated id hashes the call-site stack line, which collides when a shared
|
|
4026
|
+
* helper calls {@link tabSync} for multiple signals and diverges across minified builds during
|
|
4027
|
+
* a rolling deploy. Pass an explicit `{ id }` instead.
|
|
4028
|
+
*/
|
|
3718
4029
|
function generateDeterministicID() {
|
|
3719
4030
|
const stack = new Error().stack;
|
|
3720
4031
|
if (stack) {
|
|
@@ -3752,10 +4063,8 @@ function generateDeterministicID() {
|
|
|
3752
4063
|
*
|
|
3753
4064
|
* @example
|
|
3754
4065
|
* ```typescript
|
|
3755
|
-
* //
|
|
3756
|
-
* const theme = tabSync(signal('dark'));
|
|
3757
|
-
*
|
|
3758
|
-
* // With explicit ID (recommended for production)
|
|
4066
|
+
* // With explicit ID (recommended)
|
|
4067
|
+
* const theme = tabSync(signal('dark'), { id: 'theme' });
|
|
3759
4068
|
* const userPrefs = tabSync(signal({ lang: 'en' }), { id: 'user-preferences' });
|
|
3760
4069
|
*
|
|
3761
4070
|
* // Changes in one tab will sync to all other tabs
|
|
@@ -3767,6 +4076,7 @@ function generateDeterministicID() {
|
|
|
3767
4076
|
* - Uses a single BroadcastChannel for all synchronized signals
|
|
3768
4077
|
* - Automatically cleans up listeners when the injection context is destroyed
|
|
3769
4078
|
* - Initial signal value after sync setup is not broadcasted to prevent loops
|
|
4079
|
+
* - Received values are not re-broadcast, so tabs never echo each other's updates
|
|
3770
4080
|
*
|
|
3771
4081
|
*/
|
|
3772
4082
|
function tabSync(sig, opt) {
|
|
@@ -3774,7 +4084,20 @@ function tabSync(sig, opt) {
|
|
|
3774
4084
|
return sig;
|
|
3775
4085
|
const id = typeof opt === 'string' ? opt : (opt?.id ?? generateDeterministicID());
|
|
3776
4086
|
const bus = inject(MessageBus);
|
|
3777
|
-
|
|
4087
|
+
// The last value applied from a remote tab. The outbound effect skips (exactly) the run
|
|
4088
|
+
// caused by that write — without this, an inbound object (a fresh structured clone, so
|
|
4089
|
+
// never reference-equal) would be re-posted, and two tabs would ping-pong forever.
|
|
4090
|
+
const NONE = Symbol();
|
|
4091
|
+
let received = NONE;
|
|
4092
|
+
const { unsub, post } = bus.subscribe(id, (next) => {
|
|
4093
|
+
const before = untracked(sig);
|
|
4094
|
+
received = next;
|
|
4095
|
+
sig.set(next);
|
|
4096
|
+
// Equality-suppressed write (e.g. an identical primitive): no effect run will follow,
|
|
4097
|
+
// so clear the marker — it must not swallow a later, genuinely local change.
|
|
4098
|
+
if (untracked(sig) === before)
|
|
4099
|
+
received = NONE;
|
|
4100
|
+
});
|
|
3778
4101
|
let first = false;
|
|
3779
4102
|
const effectRef = effect(() => {
|
|
3780
4103
|
const val = sig();
|
|
@@ -3782,6 +4105,11 @@ function tabSync(sig, opt) {
|
|
|
3782
4105
|
first = true;
|
|
3783
4106
|
return;
|
|
3784
4107
|
}
|
|
4108
|
+
if (val === received) {
|
|
4109
|
+
received = NONE;
|
|
4110
|
+
return;
|
|
4111
|
+
}
|
|
4112
|
+
received = NONE;
|
|
3785
4113
|
post(val);
|
|
3786
4114
|
}, /* @ts-ignore */
|
|
3787
4115
|
...(ngDevMode ? [{ debugName: "effectRef" }] : /* istanbul ignore next */ []));
|
|
@@ -3793,7 +4121,6 @@ function tabSync(sig, opt) {
|
|
|
3793
4121
|
}
|
|
3794
4122
|
|
|
3795
4123
|
function until(sourceSignal, predicate, options = {}) {
|
|
3796
|
-
const injector = options.injector ?? inject(Injector);
|
|
3797
4124
|
return new Promise((resolve, reject) => {
|
|
3798
4125
|
let effectRef;
|
|
3799
4126
|
let timeoutId;
|
|
@@ -3830,6 +4157,14 @@ function until(sourceSignal, predicate, options = {}) {
|
|
|
3830
4157
|
cleanupAndResolve(initialValue);
|
|
3831
4158
|
return;
|
|
3832
4159
|
}
|
|
4160
|
+
let injector;
|
|
4161
|
+
try {
|
|
4162
|
+
injector = options.injector ?? inject(Injector);
|
|
4163
|
+
}
|
|
4164
|
+
catch {
|
|
4165
|
+
cleanupAndReject('until: No injector available — provide options.injector when calling outside an injection context.');
|
|
4166
|
+
return;
|
|
4167
|
+
}
|
|
3833
4168
|
if (options?.timeout !== undefined) {
|
|
3834
4169
|
timeoutId = setTimeout(() => cleanupAndReject(`until: Timeout after ${options.timeout}ms.`), options.timeout);
|
|
3835
4170
|
}
|
|
@@ -3847,17 +4182,6 @@ function until(sourceSignal, predicate, options = {}) {
|
|
|
3847
4182
|
});
|
|
3848
4183
|
}
|
|
3849
4184
|
|
|
3850
|
-
/**
|
|
3851
|
-
* @interal
|
|
3852
|
-
*/
|
|
3853
|
-
function getSignalEquality(sig) {
|
|
3854
|
-
const internal = sig[SIGNAL];
|
|
3855
|
-
if (internal && typeof internal.equal === 'function') {
|
|
3856
|
-
return internal.equal;
|
|
3857
|
-
}
|
|
3858
|
-
return Object.is; // Default equality check
|
|
3859
|
-
}
|
|
3860
|
-
|
|
3861
4185
|
/**
|
|
3862
4186
|
* Enhances an existing `WritableSignal` by adding a complete undo/redo history
|
|
3863
4187
|
* stack and an API to control it.
|
|
@@ -3906,9 +4230,10 @@ function getSignalEquality(sig) {
|
|
|
3906
4230
|
* ```
|
|
3907
4231
|
*/
|
|
3908
4232
|
function withHistory(sourceOrValue, opt) {
|
|
3909
|
-
const equal =
|
|
3910
|
-
|
|
3911
|
-
|
|
4233
|
+
const equal = opt?.equal ??
|
|
4234
|
+
(isSignal(sourceOrValue)
|
|
4235
|
+
? getSignalEquality(sourceOrValue)
|
|
4236
|
+
: Object.is);
|
|
3912
4237
|
const source = isSignal(sourceOrValue)
|
|
3913
4238
|
? sourceOrValue
|
|
3914
4239
|
: signal(sourceOrValue);
|
|
@@ -3953,9 +4278,8 @@ function withHistory(sourceOrValue, opt) {
|
|
|
3953
4278
|
if (historyStack.length === 0)
|
|
3954
4279
|
return;
|
|
3955
4280
|
const valueForRedo = untracked(source);
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
return;
|
|
4281
|
+
// length checked above — a legitimately `undefined` entry must still restore
|
|
4282
|
+
const valueToRestore = historyStack[historyStack.length - 1];
|
|
3959
4283
|
originalSet.call(source, valueToRestore);
|
|
3960
4284
|
history.inline((h) => h.pop());
|
|
3961
4285
|
redoArray.mutate((r) => {
|
|
@@ -3969,9 +4293,8 @@ function withHistory(sourceOrValue, opt) {
|
|
|
3969
4293
|
if (redoStack.length === 0)
|
|
3970
4294
|
return;
|
|
3971
4295
|
const valueForUndo = untracked(source);
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
return;
|
|
4296
|
+
// length checked above — a legitimately `undefined` entry must still restore
|
|
4297
|
+
const valueToRestore = redoStack[redoStack.length - 1];
|
|
3975
4298
|
originalSet.call(source, valueToRestore);
|
|
3976
4299
|
redoArray.inline((r) => r.pop());
|
|
3977
4300
|
history.mutate((h) => {
|
|
@@ -3997,5 +4320,5 @@ function withHistory(sourceOrValue, opt) {
|
|
|
3997
4320
|
* Generated bundle index. Do not edit.
|
|
3998
4321
|
*/
|
|
3999
4322
|
|
|
4000
|
-
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 };
|
|
4323
|
+
export { MmActivity, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, batteryStatus, chunked, clipboard, combineWith, createForwardingScope, createTransaction, createTransitionScope, debounce, debounced, derived, distinct, elementSize, elementVisibility, 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, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, provideForwardingTransitionScope, providePaused, provideTransitionScope, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
|
|
4001
4324
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|