@mmstack/primitives 20.7.0 → 20.7.2
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 +49 -10
- package/fesm2022/mmstack-primitives.mjs +703 -446
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/index.d.ts +265 -117
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { isDevMode, inject, Injector, untracked, effect, DestroyRef,
|
|
2
|
+
import { isDevMode, inject, Injector, untracked, effect, DestroyRef, InjectionToken, TemplateRef, ViewContainerRef, PLATFORM_ID, input, computed, Directive, signal, runInInjectionContext, linkedSignal, afterNextRender, Component, isSignal, ElementRef, Injectable } from '@angular/core';
|
|
3
3
|
import { isPlatformServer } from '@angular/common';
|
|
4
4
|
import { SIGNAL } from '@angular/core/primitives/signals';
|
|
5
5
|
|
|
@@ -155,74 +155,6 @@ function nestedEffect(effectFn, options) {
|
|
|
155
155
|
return ref;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
*
|
|
163
|
-
* @template T The type of items in the array.
|
|
164
|
-
* @param source A `Signal` or a function that returns an array of items to be processed in chunks.
|
|
165
|
-
* @param options Configuration options for chunk size, delay duration, equality function, and injector.
|
|
166
|
-
* @returns A `Signal` that emits the current chunk of items being processed.
|
|
167
|
-
*
|
|
168
|
-
* @example
|
|
169
|
-
* const largeList = signal(Array.from({ length: 1000 }, (_, i) => i));
|
|
170
|
-
* const chunkedList = chunked(largeList, { chunkSize: 100, delay: 100 });
|
|
171
|
-
*/
|
|
172
|
-
function chunked(source, options) {
|
|
173
|
-
const { chunkSize = 50, delay = 'frame', equal, injector } = options || {};
|
|
174
|
-
let delayFn;
|
|
175
|
-
if (delay === 'frame') {
|
|
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
|
-
};
|
|
187
|
-
}
|
|
188
|
-
else if (delay === 'microtask') {
|
|
189
|
-
delayFn = (cb) => {
|
|
190
|
-
let isCancelled = false;
|
|
191
|
-
queueMicrotask(() => {
|
|
192
|
-
if (isCancelled)
|
|
193
|
-
return;
|
|
194
|
-
cb();
|
|
195
|
-
});
|
|
196
|
-
return () => {
|
|
197
|
-
isCancelled = true;
|
|
198
|
-
};
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
delayFn = (cb) => {
|
|
203
|
-
const num = setTimeout(cb, delay);
|
|
204
|
-
return () => clearTimeout(num);
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
const internal = linkedSignal(...(ngDevMode ? [{ debugName: "internal", source,
|
|
208
|
-
computation: (items) => items.slice(0, chunkSize),
|
|
209
|
-
equal }] : [{
|
|
210
|
-
source,
|
|
211
|
-
computation: (items) => items.slice(0, chunkSize),
|
|
212
|
-
equal,
|
|
213
|
-
}]));
|
|
214
|
-
nestedEffect((cleanup) => {
|
|
215
|
-
const fullList = source();
|
|
216
|
-
const current = internal();
|
|
217
|
-
if (current.length >= fullList.length)
|
|
218
|
-
return;
|
|
219
|
-
return cleanup(delayFn(() => untracked(() => internal.set(fullList.slice(0, current.length + chunkSize)))));
|
|
220
|
-
}, {
|
|
221
|
-
injector: injector,
|
|
222
|
-
});
|
|
223
|
-
return internal.asReadonly();
|
|
224
|
-
}
|
|
225
|
-
|
|
226
158
|
/**
|
|
227
159
|
* Whether the subtree a resource/component lives in is currently PAUSED, for Activity / keep-alive.
|
|
228
160
|
* Provided by an Activity boundary (`MmActivity`, or the app-builder's per-branch injector) and read
|
|
@@ -250,6 +182,7 @@ class MmActivity {
|
|
|
250
182
|
tpl = inject(TemplateRef);
|
|
251
183
|
vcr = inject(ViewContainerRef);
|
|
252
184
|
parent = inject(Injector);
|
|
185
|
+
onServer = isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser');
|
|
253
186
|
/** When false, keep the content mounted but hidden + CD-detached. */
|
|
254
187
|
visible = input.required(...(ngDevMode ? [{ debugName: "visible", alias: 'mmActivity' }] : [{ alias: 'mmActivity' }]));
|
|
255
188
|
/** Paused == not visible — handed to the kept subtree as PAUSED_CONTEXT. */
|
|
@@ -272,6 +205,8 @@ class MmActivity {
|
|
|
272
205
|
}),
|
|
273
206
|
});
|
|
274
207
|
}
|
|
208
|
+
if (this.onServer)
|
|
209
|
+
return;
|
|
275
210
|
for (const node of this.view.rootNodes) {
|
|
276
211
|
// covers HTML and SVG roots; text/comment roots can't be styled — their CD is still
|
|
277
212
|
// detached, but prefer an element root for true visual hiding
|
|
@@ -316,24 +251,23 @@ function providePaused(source) {
|
|
|
316
251
|
}
|
|
317
252
|
|
|
318
253
|
/**
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
*
|
|
326
|
-
*
|
|
254
|
+
* @internal Token carrying an app-wide default {@link PauseOption}, set via
|
|
255
|
+
* {@link providePausableOptions}. {@link resolvePause} consults it when the call site didn't
|
|
256
|
+
* specify `pause`, so users can opt every pausable-aware primitive in (or out) from one place.
|
|
257
|
+
*/
|
|
258
|
+
const PAUSABLE_OPTIONS = new InjectionToken('@mmstack/primitives:pausable-options');
|
|
259
|
+
/**
|
|
260
|
+
* Provides an app-wide default {@link PauseOption} for every pausable-aware primitive (the public
|
|
261
|
+
* `pausable*` family plus the opt-in integrations like `stored` / `chunked`). A call-site `pause`
|
|
262
|
+
* always wins; this only fills in when the call didn't specify one.
|
|
327
263
|
*
|
|
328
|
-
*
|
|
264
|
+
* @example
|
|
265
|
+
* // Make everything that can pause honour the ambient Activity boundary by default:
|
|
266
|
+
* providePausableOptions({ pause: true })
|
|
329
267
|
*/
|
|
330
|
-
function
|
|
331
|
-
return
|
|
332
|
-
source: () => ({ t: target(), ready: ready() }),
|
|
333
|
-
computation: (curr, prev) => (prev === undefined || curr.ready ? curr.t : prev.value),
|
|
334
|
-
});
|
|
268
|
+
function providePausableOptions(opt) {
|
|
269
|
+
return { provide: PAUSABLE_OPTIONS, useValue: opt };
|
|
335
270
|
}
|
|
336
|
-
|
|
337
271
|
/**
|
|
338
272
|
* Resolve a {@link PauseOption} into a pause predicate, or `null` meaning "do not pause".
|
|
339
273
|
* `null` tells the caller to return the bare primitive — no wrapper is created.
|
|
@@ -348,11 +282,7 @@ function holdUntilReady(target, ready) {
|
|
|
348
282
|
*
|
|
349
283
|
* Encapsulating this here keeps every pausable primitive's branching identical and in one place.
|
|
350
284
|
*/
|
|
351
|
-
function resolvePause(opt) {
|
|
352
|
-
const explicit = opt?.pause; // distinguish explicit `true` from the omitted default
|
|
353
|
-
const pause = explicit ?? true; // explicit pausable* calls default to pausing
|
|
354
|
-
if (pause === false)
|
|
355
|
-
return null;
|
|
285
|
+
function resolvePause(opt, defaultPause = true) {
|
|
356
286
|
const run = (fn) => opt?.injector ? runInInjectionContext(opt.injector, fn) : fn();
|
|
357
287
|
// `inject` requires an injection context even with `optional: true`. A bare
|
|
358
288
|
// `pausableSignal(0)` (documented as "like `signal`") must degrade to the unwrapped
|
|
@@ -365,6 +295,12 @@ function resolvePause(opt) {
|
|
|
365
295
|
return fallback;
|
|
366
296
|
}
|
|
367
297
|
};
|
|
298
|
+
// A `providePausableOptions(...)` default fills in when the call site didn't specify `pause`.
|
|
299
|
+
const providedPause = tryRun(() => inject(PAUSABLE_OPTIONS, { optional: true })?.pause, undefined);
|
|
300
|
+
const explicit = opt?.pause ?? providedPause;
|
|
301
|
+
const pause = explicit ?? defaultPause; // public pausable* default `true`; opt-in integrations `false`
|
|
302
|
+
if (pause === false)
|
|
303
|
+
return null;
|
|
368
304
|
const onServer = () => typeof pause === 'function' && !opt?.injector
|
|
369
305
|
? typeof globalThis.window === 'undefined'
|
|
370
306
|
: tryRun(() => isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser'), typeof globalThis.window === 'undefined');
|
|
@@ -374,7 +310,7 @@ function resolvePause(opt) {
|
|
|
374
310
|
return null;
|
|
375
311
|
const paused = tryRun(() => inject(PAUSED_CONTEXT, { optional: true }), null);
|
|
376
312
|
if (!paused) {
|
|
377
|
-
if (
|
|
313
|
+
if (opt?.pause === true && isDevMode())
|
|
378
314
|
console.warn('[pausable] `pause: true` but no PAUSED_CONTEXT in scope — not pausing. Provide one via an ' +
|
|
379
315
|
'Activity boundary (`MmActivity` / `providePaused`), or pass a predicate / `pause: false`.');
|
|
380
316
|
return null;
|
|
@@ -450,6 +386,96 @@ function pausableComputed(computation, options) {
|
|
|
450
386
|
return ls.asReadonly();
|
|
451
387
|
}
|
|
452
388
|
|
|
389
|
+
/**
|
|
390
|
+
* 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.
|
|
391
|
+
*
|
|
392
|
+
* 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`.
|
|
393
|
+
*
|
|
394
|
+
* @template T The type of items in the array.
|
|
395
|
+
* @param source A `Signal` or a function that returns an array of items to be processed in chunks.
|
|
396
|
+
* @param options Configuration options for chunk size, delay duration, equality function, and injector.
|
|
397
|
+
* @returns A `Signal` that emits the current chunk of items being processed.
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* const largeList = signal(Array.from({ length: 1000 }, (_, i) => i));
|
|
401
|
+
* const chunkedList = chunked(largeList, { chunkSize: 100, delay: 100 });
|
|
402
|
+
*/
|
|
403
|
+
function chunked(source, options) {
|
|
404
|
+
const { chunkSize = 50, delay = 'frame', equal, injector, pause, } = options || {};
|
|
405
|
+
let delayFn;
|
|
406
|
+
if (delay === 'frame') {
|
|
407
|
+
delayFn =
|
|
408
|
+
typeof requestAnimationFrame === 'function'
|
|
409
|
+
? (callback) => {
|
|
410
|
+
const num = requestAnimationFrame(callback);
|
|
411
|
+
return () => cancelAnimationFrame(num);
|
|
412
|
+
}
|
|
413
|
+
: // SSR: no requestAnimationFrame — approximate a frame with a timeout
|
|
414
|
+
(cb) => {
|
|
415
|
+
const num = setTimeout(cb, 16);
|
|
416
|
+
return () => clearTimeout(num);
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
else if (delay === 'microtask') {
|
|
420
|
+
delayFn = (cb) => {
|
|
421
|
+
let isCancelled = false;
|
|
422
|
+
queueMicrotask(() => {
|
|
423
|
+
if (isCancelled)
|
|
424
|
+
return;
|
|
425
|
+
cb();
|
|
426
|
+
});
|
|
427
|
+
return () => {
|
|
428
|
+
isCancelled = true;
|
|
429
|
+
};
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
delayFn = (cb) => {
|
|
434
|
+
const num = setTimeout(cb, delay);
|
|
435
|
+
return () => clearTimeout(num);
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const internal = linkedSignal(...(ngDevMode ? [{ debugName: "internal", source,
|
|
439
|
+
computation: (items) => items.slice(0, chunkSize),
|
|
440
|
+
equal }] : [{
|
|
441
|
+
source,
|
|
442
|
+
computation: (items) => items.slice(0, chunkSize),
|
|
443
|
+
equal,
|
|
444
|
+
}]));
|
|
445
|
+
const paused = resolvePause({ injector, pause }, false);
|
|
446
|
+
nestedEffect((cleanup) => {
|
|
447
|
+
if (paused?.())
|
|
448
|
+
return;
|
|
449
|
+
const fullList = source();
|
|
450
|
+
const current = internal();
|
|
451
|
+
if (current.length >= fullList.length)
|
|
452
|
+
return;
|
|
453
|
+
return cleanup(delayFn(() => untracked(() => internal.set(fullList.slice(0, current.length + chunkSize)))));
|
|
454
|
+
}, {
|
|
455
|
+
injector,
|
|
456
|
+
});
|
|
457
|
+
return internal.asReadonly();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Structural hold-and-swap as a signal. Given a `target` (the desired value — e.g. the
|
|
462
|
+
* subtree/def/key you want to show) and a `ready` predicate, returns a signal that keeps
|
|
463
|
+
* yielding its PREVIOUS value until `ready()` is true, then swaps to the current target.
|
|
464
|
+
*
|
|
465
|
+
* This is the structural counterpart to `keepPrevious`/`commit`: where those hold a *value*
|
|
466
|
+
* through a reload, this holds a *structure* through a swap. The caller mounts the incoming
|
|
467
|
+
* structure off to the side (so its resources can settle and flip `ready`), keeps showing the
|
|
468
|
+
* held previous structure meanwhile, and lets the old one go once `ready` releases the swap.
|
|
469
|
+
*
|
|
470
|
+
* The very first value passes straight through (nothing to hold yet).
|
|
471
|
+
*/
|
|
472
|
+
function holdUntilReady(target, ready) {
|
|
473
|
+
return linkedSignal({
|
|
474
|
+
source: () => ({ t: target(), ready: ready() }),
|
|
475
|
+
computation: (curr, prev) => (prev === undefined || curr.ready ? curr.t : prev.value),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
453
479
|
const { is } = Object;
|
|
454
480
|
function mutable(initial, opt) {
|
|
455
481
|
const baseEqual = opt?.equal ?? is;
|
|
@@ -651,6 +677,7 @@ function registerResource(res, opt) {
|
|
|
651
677
|
function injectStartTransition() {
|
|
652
678
|
const scope = injectTransitionScope();
|
|
653
679
|
const injector = inject(Injector);
|
|
680
|
+
const onServer = isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser');
|
|
654
681
|
return (fn) => {
|
|
655
682
|
untracked(fn);
|
|
656
683
|
let sawPending = false;
|
|
@@ -665,6 +692,13 @@ function injectStartTransition() {
|
|
|
665
692
|
resolve();
|
|
666
693
|
}
|
|
667
694
|
}, ...(ngDevMode ? [{ debugName: "watcher", injector }] : [{ injector }]));
|
|
695
|
+
if (onServer) {
|
|
696
|
+
if (!untracked(scope.pending)) {
|
|
697
|
+
watcher.destroy();
|
|
698
|
+
resolve();
|
|
699
|
+
}
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
668
702
|
// no-async fallback: once the reactive system has processed the writes (afterNextRender),
|
|
669
703
|
// if nothing ever went in flight, the transition is already complete.
|
|
670
704
|
afterNextRender(() => {
|
|
@@ -692,6 +726,12 @@ function injectStartTransition() {
|
|
|
692
726
|
*
|
|
693
727
|
* `type` selects what "not ready" means: `'value'` (default) suspends only until a first value lands
|
|
694
728
|
* then holds through reloads; `'loading'` suspends on every in-flight load (strict suspense).
|
|
729
|
+
*
|
|
730
|
+
* SSR: the server serializes whatever the scope reports at stabilization, so a registered resource
|
|
731
|
+
* must keep the app unstable until it settles or the placeholder is what gets serialized (then
|
|
732
|
+
* flashes/mismatches on hydration). HttpClient-backed resources, httpResource & all of `@mmstack/resource`
|
|
733
|
+
* do this automatically via the HTTP layer's `PendingTasks` + transfer cache. A custom loader (raw
|
|
734
|
+
* `fetch`/promise/timer) must opt in itself: wrap it with `inject(PendingTasks).run(() => promise)`.
|
|
695
735
|
*/
|
|
696
736
|
class SuspenseBoundaryBase {
|
|
697
737
|
scope = injectTransitionScope();
|
|
@@ -805,6 +845,7 @@ function runInTransaction(txn, fn) {
|
|
|
805
845
|
function injectStartTransaction() {
|
|
806
846
|
const scope = injectTransitionScope();
|
|
807
847
|
const injector = inject(Injector);
|
|
848
|
+
const onServer = isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser');
|
|
808
849
|
return (fn) => {
|
|
809
850
|
const txn = createTransaction();
|
|
810
851
|
// Hold BEFORE the writes, so the display freezes at pre-transaction values.
|
|
@@ -846,11 +887,17 @@ function injectStartTransaction() {
|
|
|
846
887
|
if (sawPending && !p)
|
|
847
888
|
finish(false);
|
|
848
889
|
}, { injector });
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
if (!sawPending && !untracked(scope.pending))
|
|
890
|
+
if (onServer) {
|
|
891
|
+
if (!untracked(scope.pending))
|
|
852
892
|
finish(false);
|
|
853
|
-
}
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
// no-async fallback: if nothing ever went in flight, settle once the writes are processed.
|
|
896
|
+
afterNextRender(() => {
|
|
897
|
+
if (!sawPending && !untracked(scope.pending))
|
|
898
|
+
finish(false);
|
|
899
|
+
}, { injector });
|
|
900
|
+
}
|
|
854
901
|
return {
|
|
855
902
|
pending: scope.pending,
|
|
856
903
|
done,
|
|
@@ -2624,11 +2671,19 @@ function throttle(source, opt) {
|
|
|
2624
2671
|
tick();
|
|
2625
2672
|
};
|
|
2626
2673
|
const update = (fn) => set(fn(untracked(source)));
|
|
2674
|
+
const flush = () => {
|
|
2675
|
+
if (timeout)
|
|
2676
|
+
clearTimeout(timeout);
|
|
2677
|
+
timeout = undefined;
|
|
2678
|
+
pendingTrailing = false;
|
|
2679
|
+
fire();
|
|
2680
|
+
};
|
|
2627
2681
|
const writable = toWritable(computed(() => {
|
|
2628
2682
|
trigger();
|
|
2629
2683
|
return untracked(source);
|
|
2630
2684
|
}, opt), set, update);
|
|
2631
2685
|
writable.original = source.asReadonly();
|
|
2686
|
+
writable.flush = flush;
|
|
2632
2687
|
return writable;
|
|
2633
2688
|
}
|
|
2634
2689
|
|
|
@@ -2852,77 +2907,265 @@ function createOrientation(debugName) {
|
|
|
2852
2907
|
return state.asReadonly();
|
|
2853
2908
|
}
|
|
2854
2909
|
|
|
2910
|
+
const IDLE = {
|
|
2911
|
+
active: false,
|
|
2912
|
+
start: { x: 0, y: 0 },
|
|
2913
|
+
current: { x: 0, y: 0 },
|
|
2914
|
+
delta: { x: 0, y: 0 },
|
|
2915
|
+
pointerId: null,
|
|
2916
|
+
modifiers: { shift: false, alt: false, ctrl: false, meta: false },
|
|
2917
|
+
button: -1,
|
|
2918
|
+
};
|
|
2919
|
+
function stateEqual(a, b) {
|
|
2920
|
+
return (a.active === b.active &&
|
|
2921
|
+
a.pointerId === b.pointerId &&
|
|
2922
|
+
a.current.x === b.current.x &&
|
|
2923
|
+
a.current.y === b.current.y &&
|
|
2924
|
+
a.button === b.button &&
|
|
2925
|
+
a.modifiers.shift === b.modifiers.shift &&
|
|
2926
|
+
a.modifiers.alt === b.modifiers.alt &&
|
|
2927
|
+
a.modifiers.ctrl === b.modifiers.ctrl &&
|
|
2928
|
+
a.modifiers.meta === b.modifiers.meta);
|
|
2929
|
+
}
|
|
2855
2930
|
/**
|
|
2856
|
-
*
|
|
2857
|
-
*
|
|
2858
|
-
*
|
|
2859
|
-
*
|
|
2860
|
-
* The primitive is SSR-safe and automatically cleans up its event listeners
|
|
2861
|
-
* when the creating context is destroyed.
|
|
2862
|
-
*
|
|
2863
|
-
* @param opt Optional debug name for the signal, or a {@link SensorRunOptions} object
|
|
2864
|
-
* (with an optional `injector` for creation outside an injection context).
|
|
2865
|
-
* @returns A read-only `Signal<DocumentVisibilityState>`. On the server,
|
|
2866
|
-
* it returns a static signal with a value of `'visible'`.
|
|
2931
|
+
* Tracks a pointer *gesture* (pointerdown → capture → move → up) as a signal —
|
|
2932
|
+
* the foundation for pointer-based drag/move/resize/marquee on a canvas. Unlike
|
|
2933
|
+
* native HTML5 drag, pointer events fire continuously and coordinates are
|
|
2934
|
+
* reliable. SSR-safe; cleans up its listeners automatically.
|
|
2867
2935
|
*
|
|
2868
2936
|
* @example
|
|
2869
2937
|
* ```ts
|
|
2870
|
-
*
|
|
2871
|
-
*
|
|
2872
|
-
*
|
|
2873
|
-
*
|
|
2874
|
-
* selector: 'app-visibility-tracker',
|
|
2875
|
-
* template: `<p>Page is currently: {{ visibilityState() }}</p>`
|
|
2876
|
-
* })
|
|
2877
|
-
* export class VisibilityTrackerComponent {
|
|
2878
|
-
* readonly visibilityState = pageVisibility();
|
|
2879
|
-
*
|
|
2880
|
-
* constructor() {
|
|
2881
|
-
* effect(() => {
|
|
2882
|
-
* if (this.visibilityState() === 'hidden') {
|
|
2883
|
-
* console.log('Page is hidden, pausing expensive animations...');
|
|
2884
|
-
* } else {
|
|
2885
|
-
* console.log('Page is visible, resuming activity.');
|
|
2886
|
-
* }
|
|
2938
|
+
* const drag = pointerDrag({ activationThreshold: 4 });
|
|
2939
|
+
* const position = computed(() => {
|
|
2940
|
+
* const d = drag();
|
|
2941
|
+
* return d.active ? { x: base.x + d.delta.x, y: base.y + d.delta.y } : base;
|
|
2887
2942
|
* });
|
|
2888
|
-
* }
|
|
2889
|
-
* }
|
|
2890
2943
|
* ```
|
|
2891
2944
|
*/
|
|
2892
|
-
function
|
|
2893
|
-
|
|
2894
|
-
return runInSensorContext(injector, () => createPageVisibility(debugName));
|
|
2945
|
+
function pointerDrag(opt) {
|
|
2946
|
+
return runInSensorContext(opt?.injector, () => createPointerDrag(opt));
|
|
2895
2947
|
}
|
|
2896
|
-
function
|
|
2948
|
+
function createPointerDrag(opt) {
|
|
2897
2949
|
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
2898
|
-
|
|
2950
|
+
const base = computed(() => IDLE, {
|
|
2951
|
+
debugName: opt?.debugName ?? 'pointerDrag',
|
|
2952
|
+
});
|
|
2953
|
+
base.unthrottled = base;
|
|
2954
|
+
base.cancel = () => undefined;
|
|
2955
|
+
return base;
|
|
2899
2956
|
}
|
|
2900
|
-
const
|
|
2901
|
-
const
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2957
|
+
const hostRef = inject((ElementRef), { optional: true });
|
|
2958
|
+
const { target = hostRef?.nativeElement, coordinateSpace = 'client', activationThreshold = 3, throttle = 16, handleSelector, buttons = [0], debugName = 'pointerDrag', } = opt ?? {};
|
|
2959
|
+
const resolve = (t) => {
|
|
2960
|
+
if (!t)
|
|
2961
|
+
return null;
|
|
2962
|
+
return t instanceof ElementRef ? t.nativeElement : t;
|
|
2963
|
+
};
|
|
2964
|
+
if (!isSignal(target) && !resolve(target)) {
|
|
2965
|
+
if (isDevMode())
|
|
2966
|
+
console.warn('pointerDrag: no target element (host ElementRef missing).');
|
|
2967
|
+
const base = computed(() => IDLE, { debugName });
|
|
2968
|
+
base.unthrottled = base;
|
|
2969
|
+
base.cancel = () => undefined;
|
|
2970
|
+
return base;
|
|
2971
|
+
}
|
|
2972
|
+
const state = throttled(IDLE, {
|
|
2973
|
+
ms: throttle,
|
|
2974
|
+
leading: true,
|
|
2975
|
+
trailing: true,
|
|
2976
|
+
equal: stateEqual,
|
|
2977
|
+
debugName,
|
|
2978
|
+
});
|
|
2979
|
+
let startPoint = { x: 0, y: 0 };
|
|
2980
|
+
let activePointerId = null;
|
|
2981
|
+
let activeButton = -1;
|
|
2982
|
+
let activated = false;
|
|
2983
|
+
let gesture = null;
|
|
2984
|
+
const coord = (e) => coordinateSpace === 'page'
|
|
2985
|
+
? { x: e.pageX, y: e.pageY }
|
|
2986
|
+
: { x: e.clientX, y: e.clientY };
|
|
2987
|
+
const mods = (e) => ({
|
|
2988
|
+
shift: e.shiftKey,
|
|
2989
|
+
alt: e.altKey,
|
|
2990
|
+
ctrl: e.ctrlKey,
|
|
2991
|
+
meta: e.metaKey,
|
|
2992
|
+
});
|
|
2993
|
+
const end = () => {
|
|
2994
|
+
gesture?.abort();
|
|
2995
|
+
gesture = null;
|
|
2996
|
+
activePointerId = null;
|
|
2997
|
+
activeButton = -1;
|
|
2998
|
+
activated = false;
|
|
2999
|
+
state.set(IDLE);
|
|
3000
|
+
state.flush(); // terminal transition: reflect IDLE now, not on the trailing edge
|
|
3001
|
+
};
|
|
3002
|
+
const onMove = (e) => {
|
|
3003
|
+
if (e.pointerId !== activePointerId)
|
|
3004
|
+
return;
|
|
3005
|
+
const current = coord(e);
|
|
3006
|
+
const delta = { x: current.x - startPoint.x, y: current.y - startPoint.y };
|
|
3007
|
+
if (!activated && Math.hypot(delta.x, delta.y) >= activationThreshold) {
|
|
3008
|
+
activated = true;
|
|
3009
|
+
}
|
|
3010
|
+
state.set({
|
|
3011
|
+
active: activated,
|
|
3012
|
+
start: startPoint,
|
|
3013
|
+
current,
|
|
3014
|
+
delta,
|
|
3015
|
+
pointerId: activePointerId,
|
|
3016
|
+
modifiers: mods(e),
|
|
3017
|
+
button: activeButton, // pointermove button is -1; keep the down-button
|
|
3018
|
+
});
|
|
3019
|
+
};
|
|
3020
|
+
const onUp = (e) => {
|
|
3021
|
+
if (e.pointerId === activePointerId)
|
|
3022
|
+
end();
|
|
3023
|
+
};
|
|
3024
|
+
const onCancel = (e) => {
|
|
3025
|
+
if (e.pointerId === activePointerId)
|
|
3026
|
+
end();
|
|
3027
|
+
};
|
|
3028
|
+
const onKey = (e) => {
|
|
3029
|
+
if (e.key === 'Escape' && activePointerId !== null)
|
|
3030
|
+
end();
|
|
3031
|
+
};
|
|
3032
|
+
const onDown = (el) => (e) => {
|
|
3033
|
+
if (activePointerId !== null)
|
|
3034
|
+
return;
|
|
3035
|
+
if (!buttons.includes(e.button))
|
|
3036
|
+
return;
|
|
3037
|
+
if (handleSelector && !e.target?.closest?.(handleSelector)) {
|
|
3038
|
+
return;
|
|
3039
|
+
}
|
|
3040
|
+
activePointerId = e.pointerId;
|
|
3041
|
+
activeButton = e.button;
|
|
3042
|
+
activated = false;
|
|
3043
|
+
startPoint = coord(e);
|
|
3044
|
+
try {
|
|
3045
|
+
el.setPointerCapture(e.pointerId);
|
|
3046
|
+
}
|
|
3047
|
+
catch {
|
|
3048
|
+
// capture unsupported (older browsers / test env) — listeners still work
|
|
3049
|
+
}
|
|
3050
|
+
gesture = new AbortController();
|
|
3051
|
+
const signal = gesture.signal;
|
|
3052
|
+
el.addEventListener('pointermove', onMove, { signal });
|
|
3053
|
+
el.addEventListener('pointerup', onUp, { signal });
|
|
3054
|
+
el.addEventListener('pointercancel', onCancel, { signal });
|
|
3055
|
+
el.addEventListener('lostpointercapture', onCancel, {
|
|
3056
|
+
signal,
|
|
3057
|
+
});
|
|
3058
|
+
window.addEventListener('keydown', onKey, { signal });
|
|
3059
|
+
state.set({
|
|
3060
|
+
active: false,
|
|
3061
|
+
start: startPoint,
|
|
3062
|
+
current: startPoint,
|
|
3063
|
+
delta: { x: 0, y: 0 },
|
|
3064
|
+
pointerId: e.pointerId,
|
|
3065
|
+
modifiers: mods(e),
|
|
3066
|
+
button: e.button,
|
|
3067
|
+
});
|
|
3068
|
+
};
|
|
3069
|
+
const attach = (el) => {
|
|
3070
|
+
const controller = new AbortController();
|
|
3071
|
+
el.addEventListener('pointerdown', onDown(el), {
|
|
3072
|
+
signal: controller.signal,
|
|
3073
|
+
});
|
|
3074
|
+
return () => {
|
|
3075
|
+
controller.abort();
|
|
3076
|
+
end();
|
|
3077
|
+
};
|
|
3078
|
+
};
|
|
3079
|
+
if (isSignal(target)) {
|
|
3080
|
+
effect((cleanup) => {
|
|
3081
|
+
const el = resolve(target());
|
|
3082
|
+
if (!el)
|
|
3083
|
+
return;
|
|
3084
|
+
cleanup(attach(el));
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3087
|
+
else {
|
|
3088
|
+
const el = resolve(target);
|
|
3089
|
+
if (el)
|
|
3090
|
+
inject(DestroyRef).onDestroy(attach(el));
|
|
3091
|
+
}
|
|
3092
|
+
const base = state.asReadonly();
|
|
3093
|
+
base.unthrottled = state.original;
|
|
3094
|
+
base.cancel = end;
|
|
3095
|
+
return base;
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
/**
|
|
3099
|
+
* Creates a read-only signal that tracks the page's visibility state.
|
|
3100
|
+
*
|
|
3101
|
+
* It uses the browser's Page Visibility API to reactively report if the
|
|
3102
|
+
* current document is `'visible'`, `'hidden'`, or in another state.
|
|
3103
|
+
* The primitive is SSR-safe and automatically cleans up its event listeners
|
|
3104
|
+
* when the creating context is destroyed.
|
|
3105
|
+
*
|
|
3106
|
+
* @param opt Optional debug name for the signal, or a {@link SensorRunOptions} object
|
|
3107
|
+
* (with an optional `injector` for creation outside an injection context).
|
|
3108
|
+
* @returns A read-only `Signal<DocumentVisibilityState>`. On the server,
|
|
3109
|
+
* it returns a static signal with a value of `'visible'`.
|
|
3110
|
+
*
|
|
3111
|
+
* @example
|
|
3112
|
+
* ```ts
|
|
3113
|
+
* import { Component, effect } from '@angular/core';
|
|
3114
|
+
* import { pageVisibility } from '@mmstack/primitives';
|
|
3115
|
+
*
|
|
3116
|
+
* @Component({
|
|
3117
|
+
* selector: 'app-visibility-tracker',
|
|
3118
|
+
* template: `<p>Page is currently: {{ visibilityState() }}</p>`
|
|
3119
|
+
* })
|
|
3120
|
+
* export class VisibilityTrackerComponent {
|
|
3121
|
+
* readonly visibilityState = pageVisibility();
|
|
3122
|
+
*
|
|
3123
|
+
* constructor() {
|
|
3124
|
+
* effect(() => {
|
|
3125
|
+
* if (this.visibilityState() === 'hidden') {
|
|
3126
|
+
* console.log('Page is hidden, pausing expensive animations...');
|
|
3127
|
+
* } else {
|
|
3128
|
+
* console.log('Page is visible, resuming activity.');
|
|
3129
|
+
* }
|
|
3130
|
+
* });
|
|
3131
|
+
* }
|
|
3132
|
+
* }
|
|
3133
|
+
* ```
|
|
3134
|
+
*/
|
|
3135
|
+
function pageVisibility(opt) {
|
|
3136
|
+
const { debugName = 'pageVisibility', injector } = coerceSensorOptions(opt);
|
|
3137
|
+
return runInSensorContext(injector, () => createPageVisibility(debugName));
|
|
3138
|
+
}
|
|
3139
|
+
function createPageVisibility(debugName) {
|
|
3140
|
+
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
3141
|
+
return computed(() => 'visible', { debugName });
|
|
3142
|
+
}
|
|
3143
|
+
const visibility = signal(document.visibilityState, ...(ngDevMode ? [{ debugName: "visibility", debugName }] : [{ debugName }]));
|
|
3144
|
+
const onVisibilityChange = () => visibility.set(document.visibilityState);
|
|
3145
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
3146
|
+
inject(DestroyRef).onDestroy(() => document.removeEventListener('visibilitychange', onVisibilityChange));
|
|
3147
|
+
return visibility.asReadonly();
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
/**
|
|
3151
|
+
* Creates a read-only signal that tracks the scroll position (x, y) of the window
|
|
3152
|
+
* or a specified HTML element.
|
|
3153
|
+
*
|
|
3154
|
+
* Updates are throttled by default to optimize performance. An `unthrottled`
|
|
3155
|
+
* property is available on the returned signal for direct access to raw updates.
|
|
3156
|
+
* The primitive is SSR-safe and automatically cleans up its event listeners.
|
|
3157
|
+
*
|
|
3158
|
+
* @param options Optional configuration for the scroll sensor.
|
|
3159
|
+
* @returns A `ScrollPositionSignal`. On the server, it returns a static
|
|
3160
|
+
* signal with `{ x: 0, y: 0 }`.
|
|
3161
|
+
*
|
|
3162
|
+
* @example
|
|
3163
|
+
* ```ts
|
|
3164
|
+
* import { Component, effect, ElementRef, viewChild } from '@angular/core';
|
|
3165
|
+
* import { scrollPosition } from '@mmstack/primitives';
|
|
3166
|
+
*
|
|
3167
|
+
* @Component({
|
|
3168
|
+
* selector: 'app-scroll-tracker',
|
|
2926
3169
|
* template: `
|
|
2927
3170
|
* <p>Window Scroll: X: {{ windowScroll().x }}, Y: {{ windowScroll().y }}</p>
|
|
2928
3171
|
* <p>Host Scroll: X: {{ hostScroll().x }}, Y: {{ hostScroll().y }}</p>
|
|
@@ -3078,6 +3321,8 @@ function sensor(type, options) {
|
|
|
3078
3321
|
switch (type) {
|
|
3079
3322
|
case 'mousePosition':
|
|
3080
3323
|
return mousePosition(opts);
|
|
3324
|
+
case 'pointerDrag':
|
|
3325
|
+
return pointerDrag(opts);
|
|
3081
3326
|
case 'networkStatus':
|
|
3082
3327
|
return networkStatus(opts);
|
|
3083
3328
|
case 'pageVisibility':
|
|
@@ -3195,6 +3440,52 @@ function signalFromEvent(target, eventName, initial, projectOrOpt, maybeOpt) {
|
|
|
3195
3440
|
return untracked(() => state.asReadonly());
|
|
3196
3441
|
}
|
|
3197
3442
|
|
|
3443
|
+
const IS_STORE = Symbol('@mmstack/primitives::store/IS_STORE');
|
|
3444
|
+
const SCOPE_PARENT = Symbol('@mmstack/primitives::store/SCOPE_PARENT');
|
|
3445
|
+
/**
|
|
3446
|
+
* @internal Brand carrying a store's writability ('mutable' | 'writable' | 'readonly'), stamped
|
|
3447
|
+
* on every store proxy. Read by `extendStore` instead of re-deriving via `isWritableSignal`,
|
|
3448
|
+
* which would mis-classify a readonly scoped store (its backing `toWritable` still has a `set`).
|
|
3449
|
+
*/
|
|
3450
|
+
const STORE_KIND = Symbol('@mmstack/primitives::store/STORE_KIND');
|
|
3451
|
+
/**
|
|
3452
|
+
* @internal Brand exposing the injector a store was built with, so `extendStore` inherits it the
|
|
3453
|
+
* same way `store.extend(...)` does (via closure) — no injection context needed at the call site.
|
|
3454
|
+
*/
|
|
3455
|
+
const STORE_INJECTOR = Symbol('@mmstack/primitives::store/STORE_INJECTOR');
|
|
3456
|
+
const SIGNAL_FN_PROP = new Set([
|
|
3457
|
+
'set',
|
|
3458
|
+
'update',
|
|
3459
|
+
'mutate',
|
|
3460
|
+
'inline',
|
|
3461
|
+
'asReadonly',
|
|
3462
|
+
]);
|
|
3463
|
+
/**
|
|
3464
|
+
* @internal
|
|
3465
|
+
* Test-only handle on the proxy cache (deliberately NOT re-exported from the public barrel).
|
|
3466
|
+
* Maps a store's backing signal to its lazily-built child proxies, each held via a `WeakRef`.
|
|
3467
|
+
*/
|
|
3468
|
+
const PROXY_CACHE = new WeakMap();
|
|
3469
|
+
/**
|
|
3470
|
+
* @internal
|
|
3471
|
+
* Test-only handle on the finalization registry (deliberately NOT re-exported from the public
|
|
3472
|
+
* barrel). Prunes a cache entry once its proxy is reclaimed by the GC.
|
|
3473
|
+
*/
|
|
3474
|
+
const PROXY_CLEANUP = new FinalizationRegistry(({ target, prop }) => {
|
|
3475
|
+
const storeCache = PROXY_CACHE.get(target);
|
|
3476
|
+
if (storeCache)
|
|
3477
|
+
storeCache.delete(prop);
|
|
3478
|
+
});
|
|
3479
|
+
/**
|
|
3480
|
+
* @internal
|
|
3481
|
+
* Validates whether a value is a Signal Store.
|
|
3482
|
+
*/
|
|
3483
|
+
function isStore(value) {
|
|
3484
|
+
return (typeof value === 'function' &&
|
|
3485
|
+
value !== null &&
|
|
3486
|
+
value[IS_STORE] === true);
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3198
3489
|
/**
|
|
3199
3490
|
* Runtime marker + compile-time brand for an opaque value. A `const`-declared `Symbol`
|
|
3200
3491
|
* has a `unique symbol` type, so the same symbol serves as both the property key written
|
|
@@ -3236,12 +3527,13 @@ function isOpaque(value) {
|
|
|
3236
3527
|
value !== null &&
|
|
3237
3528
|
value[OPAQUE] === true);
|
|
3238
3529
|
}
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3530
|
+
|
|
3531
|
+
function isRecord(value) {
|
|
3532
|
+
if (value === null || typeof value !== 'object' || isOpaque(value))
|
|
3533
|
+
return false;
|
|
3534
|
+
const proto = Object.getPrototypeOf(value);
|
|
3535
|
+
return proto === Object.prototype || proto === null;
|
|
3536
|
+
}
|
|
3245
3537
|
/**
|
|
3246
3538
|
* @internal Whether a value is a terminal leaf: a concrete non-record/non-array value always is;
|
|
3247
3539
|
* `null`/`undefined` is a leaf only when vivification is disabled (with vivify on it can still
|
|
@@ -3254,6 +3546,65 @@ function isLeafValue(value, vivifyEnabled) {
|
|
|
3254
3546
|
return true; // opaque always wins — even arrays
|
|
3255
3547
|
return !Array.isArray(value) && !isRecord(value);
|
|
3256
3548
|
}
|
|
3549
|
+
/**
|
|
3550
|
+
* @internal
|
|
3551
|
+
* Resolves the vivify shape for a node from its current value: a present record/array is a
|
|
3552
|
+
* certainty we keep (cached in the derivation, so it survives the value being nulled); an
|
|
3553
|
+
* unknown value (`null`/`undefined`) defers to the caller's option. Off stays off.
|
|
3554
|
+
*/
|
|
3555
|
+
function resolveVivify(sample, option) {
|
|
3556
|
+
if (!option)
|
|
3557
|
+
return false;
|
|
3558
|
+
if (Array.isArray(sample))
|
|
3559
|
+
return 'array';
|
|
3560
|
+
if (isRecord(sample))
|
|
3561
|
+
return 'object';
|
|
3562
|
+
return 'auto';
|
|
3563
|
+
}
|
|
3564
|
+
function hasOwnKey(value, key) {
|
|
3565
|
+
return value != null && Object.hasOwn(value, key);
|
|
3566
|
+
}
|
|
3567
|
+
/**
|
|
3568
|
+
* @internal
|
|
3569
|
+
* Builds the `onChange` for the fallback (non-record container) derivation branch. For an
|
|
3570
|
+
* immutable source the container is copied before the write — returning the same mutated
|
|
3571
|
+
* reference would let the source's equality cut propagation (leaving child signals permanently
|
|
3572
|
+
* stale) and alias the caller's original object, breaking the structural-sharing contract
|
|
3573
|
+
* `forkStore` relies on. For a mutable source the write goes through `mutate`, so the chain's
|
|
3574
|
+
* force-notify engages (plain `update` with the same reference would never notify).
|
|
3575
|
+
*/
|
|
3576
|
+
function createFallbackOnChange(target, prop, vivifyFn, isMutableSource) {
|
|
3577
|
+
const write = (newValue) => (v) => {
|
|
3578
|
+
const container = vivifyFn(v, prop);
|
|
3579
|
+
if (container === null || container === undefined)
|
|
3580
|
+
return container;
|
|
3581
|
+
const next = isMutableSource
|
|
3582
|
+
? container
|
|
3583
|
+
: Array.isArray(container)
|
|
3584
|
+
? container.slice()
|
|
3585
|
+
: isRecord(container)
|
|
3586
|
+
? { ...container }
|
|
3587
|
+
: container; // non-plain leaf (Date/class instance): legacy in-place attempt
|
|
3588
|
+
try {
|
|
3589
|
+
next[prop] = newValue;
|
|
3590
|
+
}
|
|
3591
|
+
catch (e) {
|
|
3592
|
+
if (isDevMode())
|
|
3593
|
+
console.error(`[store] Failed to set property "${String(prop)}"`, e);
|
|
3594
|
+
}
|
|
3595
|
+
return next;
|
|
3596
|
+
};
|
|
3597
|
+
return isMutableSource
|
|
3598
|
+
? (newValue) => target.mutate(write(newValue))
|
|
3599
|
+
: (newValue) => target.update(write(newValue));
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
/**
|
|
3603
|
+
* @internal Runtime brand carrying a store node's lazily-built leaf probe. Exported (like
|
|
3604
|
+
* {@link OPAQUE}) only so the `{ readonly [LEAF]: () => boolean }` brand on the store types is
|
|
3605
|
+
* nameable in the emitted declarations — not part of the supported surface; use {@link isLeaf}.
|
|
3606
|
+
*/
|
|
3607
|
+
const LEAF = Symbol('@mmstack/primitives::store/LEAF');
|
|
3257
3608
|
/**
|
|
3258
3609
|
* @internal Constant leaf probes for nodes whose leaf-ness is statically known, so the reactive
|
|
3259
3610
|
* `computed` can be skipped entirely.
|
|
@@ -3311,227 +3662,72 @@ function markAsLeaf(sig, value, vivifyEnabled, noUnionLeaves) {
|
|
|
3311
3662
|
function isLeaf(value) {
|
|
3312
3663
|
return isStore(value) && value[LEAF]?.() === true;
|
|
3313
3664
|
}
|
|
3314
|
-
|
|
3315
|
-
const SCOPE_PARENT = Symbol('@mmstack/primitives::store/SCOPE_PARENT');
|
|
3316
|
-
/**
|
|
3317
|
-
* @internal
|
|
3318
|
-
* Test-only handle on the proxy cache (deliberately NOT re-exported from the public barrel).
|
|
3319
|
-
* Maps a store's backing signal to its lazily-built child proxies, each held via a `WeakRef`.
|
|
3320
|
-
*/
|
|
3321
|
-
const PROXY_CACHE = new WeakMap();
|
|
3322
|
-
const SIGNAL_FN_PROP = new Set([
|
|
3323
|
-
'set',
|
|
3324
|
-
'update',
|
|
3325
|
-
'mutate',
|
|
3326
|
-
'inline',
|
|
3327
|
-
'asReadonly',
|
|
3328
|
-
]);
|
|
3665
|
+
|
|
3329
3666
|
/**
|
|
3330
|
-
* @internal
|
|
3331
|
-
*
|
|
3332
|
-
*
|
|
3667
|
+
* @internal Reads (or lazily builds + caches) the child node proxy for `prop` on `target`,
|
|
3668
|
+
* holding it via a `WeakRef` and registering it for finalizer-driven cache pruning. The cache
|
|
3669
|
+
* is keyed per backing signal, so child identity is stable across repeat reads.
|
|
3333
3670
|
*/
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
if (storeCache)
|
|
3671
|
+
function getCachedChild(target, prop, build) {
|
|
3672
|
+
let storeCache = PROXY_CACHE.get(target);
|
|
3673
|
+
if (!storeCache) {
|
|
3674
|
+
storeCache = new Map();
|
|
3675
|
+
PROXY_CACHE.set(target, storeCache);
|
|
3676
|
+
}
|
|
3677
|
+
const cachedRef = storeCache.get(prop);
|
|
3678
|
+
if (cachedRef) {
|
|
3679
|
+
const cached = cachedRef.deref();
|
|
3680
|
+
if (cached)
|
|
3681
|
+
return cached;
|
|
3337
3682
|
storeCache.delete(prop);
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
return
|
|
3345
|
-
value !== null &&
|
|
3346
|
-
value[IS_STORE] === true);
|
|
3347
|
-
}
|
|
3348
|
-
function isRecord(value) {
|
|
3349
|
-
if (value === null || typeof value !== 'object' || isOpaque(value))
|
|
3350
|
-
return false;
|
|
3351
|
-
const proto = Object.getPrototypeOf(value);
|
|
3352
|
-
return proto === Object.prototype || proto === null;
|
|
3353
|
-
}
|
|
3354
|
-
/**
|
|
3355
|
-
* @internal
|
|
3356
|
-
* Resolves the vivify shape for a node from its current value: a present record/array is a
|
|
3357
|
-
* certainty we keep (cached in the derivation, so it survives the value being nulled); an
|
|
3358
|
-
* unknown value (`null`/`undefined`) defers to the caller's option. Off stays off.
|
|
3359
|
-
*/
|
|
3360
|
-
function resolveVivify(sample, option) {
|
|
3361
|
-
if (!option)
|
|
3362
|
-
return false;
|
|
3363
|
-
if (Array.isArray(sample))
|
|
3364
|
-
return 'array';
|
|
3365
|
-
if (isRecord(sample))
|
|
3366
|
-
return 'object';
|
|
3367
|
-
return 'auto';
|
|
3368
|
-
}
|
|
3369
|
-
function hasOwnKey(value, key) {
|
|
3370
|
-
return value != null && Object.hasOwn(value, key);
|
|
3371
|
-
}
|
|
3372
|
-
/**
|
|
3373
|
-
* @internal
|
|
3374
|
-
* Builds the `onChange` for the fallback (non-record container) derivation branch. For an
|
|
3375
|
-
* immutable source the container is copied before the write — returning the same mutated
|
|
3376
|
-
* reference would let the source's equality cut propagation (leaving child signals permanently
|
|
3377
|
-
* stale) and alias the caller's original object, breaking the structural-sharing contract
|
|
3378
|
-
* `forkStore` relies on. For a mutable source the write goes through `mutate`, so the chain's
|
|
3379
|
-
* force-notify engages (plain `update` with the same reference would never notify).
|
|
3380
|
-
*/
|
|
3381
|
-
function createFallbackOnChange(target, prop, vivifyFn, isMutableSource) {
|
|
3382
|
-
const write = (newValue) => (v) => {
|
|
3383
|
-
const container = vivifyFn(v, prop);
|
|
3384
|
-
if (container === null || container === undefined)
|
|
3385
|
-
return container;
|
|
3386
|
-
const next = isMutableSource
|
|
3387
|
-
? container
|
|
3388
|
-
: Array.isArray(container)
|
|
3389
|
-
? container.slice()
|
|
3390
|
-
: isRecord(container)
|
|
3391
|
-
? { ...container }
|
|
3392
|
-
: container; // non-plain leaf (Date/class instance): legacy in-place attempt
|
|
3393
|
-
try {
|
|
3394
|
-
next[prop] = newValue;
|
|
3395
|
-
}
|
|
3396
|
-
catch (e) {
|
|
3397
|
-
if (isDevMode())
|
|
3398
|
-
console.error(`[store] Failed to set property "${String(prop)}"`, e);
|
|
3399
|
-
}
|
|
3400
|
-
return next;
|
|
3401
|
-
};
|
|
3402
|
-
return isMutableSource
|
|
3403
|
-
? (newValue) => target.mutate(write(newValue))
|
|
3404
|
-
: (newValue) => target.update(write(newValue));
|
|
3683
|
+
PROXY_CLEANUP.unregister(cachedRef);
|
|
3684
|
+
}
|
|
3685
|
+
const proxy = build();
|
|
3686
|
+
const ref = new WeakRef(proxy);
|
|
3687
|
+
storeCache.set(prop, ref);
|
|
3688
|
+
PROXY_CLEANUP.register(proxy, { target, prop }, ref);
|
|
3689
|
+
return proxy;
|
|
3405
3690
|
}
|
|
3406
3691
|
/**
|
|
3407
|
-
* @internal
|
|
3408
|
-
*
|
|
3692
|
+
* @internal Builds the derived child signal for `prop` and wraps it as an array/object substore.
|
|
3693
|
+
* A record parent reads the key directly; any other container goes through the fallback `from`/
|
|
3694
|
+
* `onChange` path. Shared verbatim by the array and object proxies — the only place a child node
|
|
3695
|
+
* is constructed.
|
|
3409
3696
|
*/
|
|
3410
|
-
function
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
const
|
|
3414
|
-
const
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
const v = untracked(source);
|
|
3434
|
-
if (!Array.isArray(v))
|
|
3435
|
-
return [];
|
|
3436
|
-
const len = v.length;
|
|
3437
|
-
const arr = new Array(len + 1);
|
|
3438
|
-
for (let i = 0; i < len; i++) {
|
|
3439
|
-
arr[i] = String(i);
|
|
3440
|
-
}
|
|
3441
|
-
arr[len] = 'length';
|
|
3442
|
-
return arr;
|
|
3443
|
-
},
|
|
3444
|
-
getPrototypeOf() {
|
|
3445
|
-
return Array.prototype;
|
|
3446
|
-
},
|
|
3447
|
-
getOwnPropertyDescriptor(_, prop) {
|
|
3448
|
-
const v = untracked(source);
|
|
3449
|
-
if (!Array.isArray(v))
|
|
3450
|
-
return;
|
|
3451
|
-
if (prop === 'length' ||
|
|
3452
|
-
(typeof prop === 'string' && !isNaN(+prop) && +prop < v.length)) {
|
|
3453
|
-
return {
|
|
3454
|
-
enumerable: true,
|
|
3455
|
-
configurable: true, // Required for proxies to dynamic targets
|
|
3456
|
-
};
|
|
3457
|
-
}
|
|
3458
|
-
return;
|
|
3459
|
-
},
|
|
3460
|
-
get(target, prop, receiver) {
|
|
3461
|
-
if (prop === IS_STORE)
|
|
3462
|
-
return true;
|
|
3463
|
-
if (prop === 'length')
|
|
3464
|
-
return lengthSignal;
|
|
3465
|
-
if (prop === Symbol.iterator) {
|
|
3466
|
-
return function* () {
|
|
3467
|
-
// read length reactively: a spread/for-of inside a computed/effect must re-run
|
|
3468
|
-
// when items are added or removed, not only when already-read elements change
|
|
3469
|
-
for (let i = 0; i < lengthSignal(); i++) {
|
|
3470
|
-
yield receiver[i];
|
|
3471
|
-
}
|
|
3472
|
-
};
|
|
3473
|
-
}
|
|
3474
|
-
if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
|
|
3475
|
-
return target[prop];
|
|
3476
|
-
if (isIndexProp(prop)) {
|
|
3477
|
-
const idx = +prop;
|
|
3478
|
-
let storeCache = PROXY_CACHE.get(target);
|
|
3479
|
-
if (!storeCache) {
|
|
3480
|
-
storeCache = new Map();
|
|
3481
|
-
PROXY_CACHE.set(target, storeCache);
|
|
3482
|
-
}
|
|
3483
|
-
const cachedRef = storeCache.get(idx);
|
|
3484
|
-
if (cachedRef) {
|
|
3485
|
-
const cached = cachedRef.deref();
|
|
3486
|
-
if (cached)
|
|
3487
|
-
return cached;
|
|
3488
|
-
storeCache.delete(idx);
|
|
3489
|
-
PROXY_CLEANUP.unregister(cachedRef);
|
|
3490
|
-
}
|
|
3491
|
-
const value = untracked(target);
|
|
3492
|
-
const valueIsArray = Array.isArray(value);
|
|
3493
|
-
const valueIsRecord = isRecord(value);
|
|
3494
|
-
const nodeVivify = resolveVivify(value, vivify);
|
|
3495
|
-
const vivifyFn = createVivify(nodeVivify);
|
|
3496
|
-
const equalFn = (valueIsRecord || valueIsArray) &&
|
|
3497
|
-
isMutableSource &&
|
|
3498
|
-
typeof value[idx] === 'object'
|
|
3499
|
-
? () => false
|
|
3500
|
-
: undefined;
|
|
3501
|
-
const computation = valueIsRecord
|
|
3502
|
-
? derived(target, idx, {
|
|
3503
|
-
equal: equalFn,
|
|
3504
|
-
vivify: nodeVivify,
|
|
3505
|
-
})
|
|
3506
|
-
: derived(target, {
|
|
3507
|
-
from: (v) => v?.[idx],
|
|
3508
|
-
onChange: createFallbackOnChange(target, idx, vivifyFn, isMutableSource),
|
|
3509
|
-
equal: equalFn,
|
|
3510
|
-
});
|
|
3511
|
-
const childSample = untracked(computation);
|
|
3512
|
-
const childVivify = resolveVivify(childSample, vivify);
|
|
3513
|
-
const proxy = Array.isArray(childSample) && !isOpaque(childSample)
|
|
3514
|
-
? toArrayStore(computation, injector, childVivify, noUnionLeaves)
|
|
3515
|
-
: toStore(computation, injector, childVivify, noUnionLeaves);
|
|
3516
|
-
markAsLeaf(proxy, computation, childVivify !== false, noUnionLeaves);
|
|
3517
|
-
const ref = new WeakRef(proxy);
|
|
3518
|
-
storeCache.set(idx, ref);
|
|
3519
|
-
PROXY_CLEANUP.register(proxy, { target, prop: idx }, ref);
|
|
3520
|
-
return proxy;
|
|
3521
|
-
}
|
|
3522
|
-
return Reflect.get(target, prop, receiver);
|
|
3523
|
-
},
|
|
3524
|
-
});
|
|
3697
|
+
function buildChildNode(target, prop, isMutableSource, injector, vivify, noUnionLeaves) {
|
|
3698
|
+
const value = untracked(target);
|
|
3699
|
+
const valueIsRecord = isRecord(value);
|
|
3700
|
+
const valueIsArray = Array.isArray(value);
|
|
3701
|
+
const nodeVivify = resolveVivify(value, vivify);
|
|
3702
|
+
const vivifyFn = createVivify(nodeVivify);
|
|
3703
|
+
const equalFn = (valueIsRecord || valueIsArray) &&
|
|
3704
|
+
isMutableSource &&
|
|
3705
|
+
typeof value[prop] === 'object'
|
|
3706
|
+
? () => false
|
|
3707
|
+
: undefined;
|
|
3708
|
+
const computation = valueIsRecord
|
|
3709
|
+
? derived(target, prop, { equal: equalFn, vivify: nodeVivify })
|
|
3710
|
+
: derived(target, {
|
|
3711
|
+
from: (v) => v?.[prop],
|
|
3712
|
+
onChange: createFallbackOnChange(target, prop, vivifyFn, isMutableSource),
|
|
3713
|
+
equal: equalFn,
|
|
3714
|
+
});
|
|
3715
|
+
const childSample = untracked(computation);
|
|
3716
|
+
const childVivify = resolveVivify(childSample, vivify);
|
|
3717
|
+
const proxy = toStore(computation, injector, childVivify, noUnionLeaves);
|
|
3718
|
+
markAsLeaf(proxy, computation, childVivify !== false, noUnionLeaves);
|
|
3719
|
+
return proxy;
|
|
3525
3720
|
}
|
|
3526
3721
|
/**
|
|
3527
3722
|
* Converts a Signal into a deep-observable Store.
|
|
3528
3723
|
* Accessing nested properties returns a derived Signal of that path.
|
|
3529
3724
|
*
|
|
3530
3725
|
* @remarks
|
|
3531
|
-
* A
|
|
3532
|
-
*
|
|
3533
|
-
*
|
|
3534
|
-
*
|
|
3726
|
+
* A node's *container kind* (array / record / primitive) is tracked reactively via a per-node
|
|
3727
|
+
* `kind` computed, so the same proxy serves all three and a union node that flips between an
|
|
3728
|
+
* array and a record keeps working. Flips are route-forward: after a flip the node behaves as
|
|
3729
|
+
* its new kind on the next access, while child proxies cached under the old shape go stale and
|
|
3730
|
+
* are pruned by the GC.
|
|
3535
3731
|
*
|
|
3536
3732
|
* @example
|
|
3537
3733
|
* const state = store({ user: { name: 'John' } });
|
|
@@ -3549,92 +3745,114 @@ function toStore(source, injector, vivify = false, noUnionLeaves = false) {
|
|
|
3549
3745
|
});
|
|
3550
3746
|
const isWritableSource = isWritableSignal(source);
|
|
3551
3747
|
const isMutableSource = isWritableSource && isMutable(writableSource);
|
|
3748
|
+
const kind = computed(() => {
|
|
3749
|
+
const v = source();
|
|
3750
|
+
if (Array.isArray(v) && !isOpaque(v))
|
|
3751
|
+
return 'array';
|
|
3752
|
+
if (isRecord(v))
|
|
3753
|
+
return 'record';
|
|
3754
|
+
return 'primitive';
|
|
3755
|
+
}, ...(ngDevMode ? [{ debugName: "kind" }] : []));
|
|
3756
|
+
// built lazily so non-array nodes never allocate it
|
|
3757
|
+
let length;
|
|
3758
|
+
const arrayLength = () => (length ??= computed(() => {
|
|
3759
|
+
const v = source();
|
|
3760
|
+
return Array.isArray(v) ? v.length : 0;
|
|
3761
|
+
}));
|
|
3552
3762
|
const s = new Proxy(writableSource, {
|
|
3553
3763
|
has(_, prop) {
|
|
3554
|
-
|
|
3764
|
+
const v = untracked(source);
|
|
3765
|
+
if (untracked(kind) === 'array') {
|
|
3766
|
+
if (prop === 'length')
|
|
3767
|
+
return true;
|
|
3768
|
+
if (isIndexProp(prop)) {
|
|
3769
|
+
const idx = +prop;
|
|
3770
|
+
return idx >= 0 && idx < v.length;
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
// nullish node values are routinely descended with vivify on — `in` must not throw
|
|
3774
|
+
return v == null ? false : Reflect.has(v, prop);
|
|
3555
3775
|
},
|
|
3556
3776
|
ownKeys() {
|
|
3557
3777
|
const v = untracked(source);
|
|
3778
|
+
if (untracked(kind) === 'array') {
|
|
3779
|
+
const len = v.length;
|
|
3780
|
+
const arr = new Array(len + 1);
|
|
3781
|
+
for (let i = 0; i < len; i++)
|
|
3782
|
+
arr[i] = String(i);
|
|
3783
|
+
arr[len] = 'length';
|
|
3784
|
+
return arr;
|
|
3785
|
+
}
|
|
3558
3786
|
if (!isRecord(v))
|
|
3559
3787
|
return [];
|
|
3560
3788
|
return Reflect.ownKeys(v);
|
|
3561
3789
|
},
|
|
3562
3790
|
getPrototypeOf() {
|
|
3563
|
-
|
|
3791
|
+
if (untracked(kind) === 'array')
|
|
3792
|
+
return Array.prototype;
|
|
3793
|
+
const v = untracked(source);
|
|
3794
|
+
return v == null ? Object.prototype : Object.getPrototypeOf(v);
|
|
3564
3795
|
},
|
|
3565
3796
|
getOwnPropertyDescriptor(_, prop) {
|
|
3566
|
-
const
|
|
3567
|
-
if (
|
|
3797
|
+
const v = untracked(source);
|
|
3798
|
+
if (untracked(kind) === 'array') {
|
|
3799
|
+
if (prop === 'length' ||
|
|
3800
|
+
(typeof prop === 'string' && !isNaN(+prop) && +prop < v.length))
|
|
3801
|
+
return { enumerable: true, configurable: true };
|
|
3568
3802
|
return;
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
};
|
|
3803
|
+
}
|
|
3804
|
+
if (!isRecord(v) || !(prop in v))
|
|
3805
|
+
return;
|
|
3806
|
+
return { enumerable: true, configurable: true };
|
|
3573
3807
|
},
|
|
3574
|
-
get(target, prop) {
|
|
3808
|
+
get(target, prop, receiver) {
|
|
3575
3809
|
if (prop === IS_STORE)
|
|
3576
3810
|
return true;
|
|
3811
|
+
if (prop === STORE_KIND)
|
|
3812
|
+
return isMutableSource
|
|
3813
|
+
? 'mutable'
|
|
3814
|
+
: isWritableSource
|
|
3815
|
+
? 'writable'
|
|
3816
|
+
: 'readonly';
|
|
3817
|
+
if (prop === STORE_INJECTOR)
|
|
3818
|
+
return injector;
|
|
3577
3819
|
if (prop === 'asReadonlyStore')
|
|
3578
3820
|
return () => {
|
|
3579
3821
|
if (!isWritableSource)
|
|
3580
3822
|
return s;
|
|
3581
3823
|
return untracked(() => toStore(source.asReadonly(), injector, vivify, noUnionLeaves));
|
|
3582
3824
|
};
|
|
3583
|
-
|
|
3825
|
+
const k = untracked(kind);
|
|
3826
|
+
if (prop === 'extend' && k !== 'array')
|
|
3584
3827
|
return (seed) => scopedStore(s, seed, isMutableSource
|
|
3585
3828
|
? 'mutable'
|
|
3586
3829
|
: isWritableSource
|
|
3587
3830
|
? 'writable'
|
|
3588
3831
|
: 'readonly', injector);
|
|
3832
|
+
if (k === 'array') {
|
|
3833
|
+
if (prop === 'length')
|
|
3834
|
+
return arrayLength();
|
|
3835
|
+
if (prop === Symbol.iterator)
|
|
3836
|
+
return function* () {
|
|
3837
|
+
// read length reactively: a spread/for-of inside a computed/effect must re-run
|
|
3838
|
+
// when items are added or removed, not only when already-read elements change
|
|
3839
|
+
const len = arrayLength();
|
|
3840
|
+
for (let i = 0; i < len(); i++)
|
|
3841
|
+
yield receiver[i];
|
|
3842
|
+
};
|
|
3843
|
+
}
|
|
3589
3844
|
if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
|
|
3590
3845
|
return target[prop];
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
PROXY_CACHE.set(target, storeCache);
|
|
3595
|
-
}
|
|
3596
|
-
const cachedRef = storeCache.get(prop);
|
|
3597
|
-
if (cachedRef) {
|
|
3598
|
-
const cached = cachedRef.deref();
|
|
3599
|
-
if (cached)
|
|
3600
|
-
return cached;
|
|
3601
|
-
storeCache.delete(prop);
|
|
3602
|
-
PROXY_CLEANUP.unregister(cachedRef);
|
|
3603
|
-
}
|
|
3604
|
-
const value = untracked(target);
|
|
3605
|
-
const valueIsRecord = isRecord(value);
|
|
3606
|
-
const valueIsArray = Array.isArray(value);
|
|
3607
|
-
const nodeVivify = resolveVivify(value, vivify);
|
|
3608
|
-
const vivifyFn = createVivify(nodeVivify);
|
|
3609
|
-
const equalFn = (valueIsRecord || valueIsArray) &&
|
|
3610
|
-
isMutableSource &&
|
|
3611
|
-
typeof value[prop] === 'object'
|
|
3612
|
-
? () => false
|
|
3613
|
-
: undefined;
|
|
3614
|
-
const computation = valueIsRecord
|
|
3615
|
-
? derived(target, prop, { equal: equalFn, vivify: nodeVivify })
|
|
3616
|
-
: derived(target, {
|
|
3617
|
-
from: (v) => v?.[prop],
|
|
3618
|
-
onChange: createFallbackOnChange(target, prop, vivifyFn, isMutableSource),
|
|
3619
|
-
equal: equalFn,
|
|
3620
|
-
});
|
|
3621
|
-
const childSample = untracked(computation);
|
|
3622
|
-
const childVivify = resolveVivify(childSample, vivify);
|
|
3623
|
-
const proxy = Array.isArray(childSample) && !isOpaque(childSample)
|
|
3624
|
-
? toArrayStore(computation, injector, childVivify, noUnionLeaves)
|
|
3625
|
-
: toStore(computation, injector, childVivify, noUnionLeaves);
|
|
3626
|
-
markAsLeaf(proxy, computation, childVivify !== false, noUnionLeaves);
|
|
3627
|
-
const ref = new WeakRef(proxy);
|
|
3628
|
-
storeCache.set(prop, ref);
|
|
3629
|
-
PROXY_CLEANUP.register(proxy, { target, prop }, ref);
|
|
3630
|
-
return proxy;
|
|
3846
|
+
if (k === 'array' && !isIndexProp(prop))
|
|
3847
|
+
return Reflect.get(target, prop, receiver);
|
|
3848
|
+
return getCachedChild(target, prop, () => buildChildNode(target, k === 'array' ? +prop : prop, isMutableSource, injector, vivify, noUnionLeaves));
|
|
3631
3849
|
},
|
|
3632
3850
|
});
|
|
3633
3851
|
return s;
|
|
3634
3852
|
}
|
|
3635
3853
|
/**
|
|
3636
3854
|
* @internal
|
|
3637
|
-
* Backs `
|
|
3855
|
+
* Backs `extendStore(...)`. Builds a scoped overlay over `parent`: the local layer (the seed
|
|
3638
3856
|
* plus any keys created later) is its own signal and `parent` is its own signal, so the getter
|
|
3639
3857
|
* routes each key by consulting BOTH — local first, then parent, else local (so a write to an
|
|
3640
3858
|
* as-yet-unknown key lands locally). Inherited keys return the parent's own sub-store (shared
|
|
@@ -3681,6 +3899,10 @@ function scopedStore(parent, seed, kind, injector) {
|
|
|
3681
3899
|
get(target, prop) {
|
|
3682
3900
|
if (prop === IS_STORE)
|
|
3683
3901
|
return true;
|
|
3902
|
+
if (prop === STORE_KIND)
|
|
3903
|
+
return kind;
|
|
3904
|
+
if (prop === STORE_INJECTOR)
|
|
3905
|
+
return injector;
|
|
3684
3906
|
if (prop === SCOPE_PARENT)
|
|
3685
3907
|
return parent;
|
|
3686
3908
|
if (prop === 'extend')
|
|
@@ -3713,6 +3935,28 @@ function scopedStore(parent, seed, kind, injector) {
|
|
|
3713
3935
|
});
|
|
3714
3936
|
return scope;
|
|
3715
3937
|
}
|
|
3938
|
+
/** @internal Reads a store's writability brand, falling back to signal inspection if unbranded. */
|
|
3939
|
+
function storeKind(s) {
|
|
3940
|
+
return (s[STORE_KIND] ??
|
|
3941
|
+
(isWritableSignal(s) ? (isMutable(s) ? 'mutable' : 'writable') : 'readonly'));
|
|
3942
|
+
}
|
|
3943
|
+
/**
|
|
3944
|
+
* Extends a store with extra keys via a scoped overlay, returning a new store that reads through
|
|
3945
|
+
* to the parent for inherited keys (shared identity + two-way) while holding the new keys locally.
|
|
3946
|
+
*
|
|
3947
|
+
* The typesafe successor to the deprecated `store.extend(...)` method — moving it off the proxy
|
|
3948
|
+
* frees the `extend` key for use as a normal record key. Writability (readonly/writable/mutable)
|
|
3949
|
+
* is inherited from `store`.
|
|
3950
|
+
*
|
|
3951
|
+
* @example
|
|
3952
|
+
* const base = store({ count: 0 });
|
|
3953
|
+
* const scoped = extendStore(base, { label: 'live' });
|
|
3954
|
+
* scoped.count.set(1); // writes through to base
|
|
3955
|
+
* scoped.label.set('x'); // stays local
|
|
3956
|
+
*/
|
|
3957
|
+
function extendStore(store, source, injector) {
|
|
3958
|
+
return scopedStore(store, source, storeKind(store), injector ?? store[STORE_INJECTOR] ?? inject(Injector));
|
|
3959
|
+
}
|
|
3716
3960
|
/**
|
|
3717
3961
|
* Creates a WritableSignalStore from a value.
|
|
3718
3962
|
* @see {@link toStore}
|
|
@@ -3798,6 +4042,26 @@ function forkStore(base, opt) {
|
|
|
3798
4042
|
};
|
|
3799
4043
|
}
|
|
3800
4044
|
|
|
4045
|
+
/**
|
|
4046
|
+
* @internal The plain-`effect` sibling of the public {@link pausableEffect} (which is built on
|
|
4047
|
+
* `nestedEffect`). For infra utilities that own a single top-level effect/subscription and don't
|
|
4048
|
+
* need frame/nesting semantics. Opt-in (default off): with no `pause` (call site or
|
|
4049
|
+
* `providePausableOptions` default) it returns a bare `effect` (zero overhead, byte-identical to
|
|
4050
|
+
* today); otherwise it gates the body on the resolved predicate — read FIRST so the dependency set
|
|
4051
|
+
* collapses to just the predicate while paused, re-tracking on resume. Deliberately NOT re-exported
|
|
4052
|
+
* from the public barrel.
|
|
4053
|
+
*/
|
|
4054
|
+
function pausablePureEffect(effectFn, options) {
|
|
4055
|
+
const paused = resolvePause(options, false);
|
|
4056
|
+
if (!paused)
|
|
4057
|
+
return effect(effectFn, options);
|
|
4058
|
+
return effect((registerCleanup) => {
|
|
4059
|
+
if (paused())
|
|
4060
|
+
return;
|
|
4061
|
+
effectFn(registerCleanup);
|
|
4062
|
+
}, options);
|
|
4063
|
+
}
|
|
4064
|
+
|
|
3801
4065
|
// Internal dummy store for server-side rendering
|
|
3802
4066
|
const noopStore = {
|
|
3803
4067
|
getItem: () => null,
|
|
@@ -3859,8 +4123,9 @@ const noopStore = {
|
|
|
3859
4123
|
* }
|
|
3860
4124
|
* ```
|
|
3861
4125
|
*/
|
|
3862
|
-
function stored(fallback, { key, store: providedStore, serialize = JSON.stringify, deserialize = JSON.parse, syncTabs = false, equal = Object.is, onKeyChange = 'load', cleanupOldKey = false, validate = () => true, ...rest }) {
|
|
3863
|
-
const
|
|
4126
|
+
function stored(fallback, { key, store: providedStore, serialize = JSON.stringify, deserialize = JSON.parse, syncTabs = false, equal = Object.is, onKeyChange = 'load', cleanupOldKey = false, validate = () => true, pause, injector: providedInjector, ...rest }) {
|
|
4127
|
+
const injector = providedInjector ?? inject(Injector);
|
|
4128
|
+
const isServer = isPlatformServer(injector.get(PLATFORM_ID));
|
|
3864
4129
|
const fallbackStore = isServer ? noopStore : localStorage;
|
|
3865
4130
|
const store = providedStore ?? fallbackStore;
|
|
3866
4131
|
const keySig = typeof key === 'string'
|
|
@@ -3868,8 +4133,6 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
3868
4133
|
: isSignal(key)
|
|
3869
4134
|
? key
|
|
3870
4135
|
: computed(key);
|
|
3871
|
-
// "no stored value" marker — distinct from `null`/`undefined`, so a nullable `T` can
|
|
3872
|
-
// round-trip a legitimate `null` through `set` instead of it acting like `clear()`
|
|
3873
4136
|
const EMPTY = Symbol();
|
|
3874
4137
|
const getValue = (key) => {
|
|
3875
4138
|
const found = store.getItem(key);
|
|
@@ -3923,7 +4186,7 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
3923
4186
|
}]));
|
|
3924
4187
|
let prevKey = initialKey;
|
|
3925
4188
|
if (onKeyChange === 'store') {
|
|
3926
|
-
|
|
4189
|
+
pausablePureEffect(() => {
|
|
3927
4190
|
const k = keySig();
|
|
3928
4191
|
storeValue(k, internal());
|
|
3929
4192
|
if (prevKey !== k) {
|
|
@@ -3931,10 +4194,10 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
3931
4194
|
store.removeItem(prevKey);
|
|
3932
4195
|
prevKey = k;
|
|
3933
4196
|
}
|
|
3934
|
-
});
|
|
4197
|
+
}, { injector, pause });
|
|
3935
4198
|
}
|
|
3936
4199
|
else {
|
|
3937
|
-
|
|
4200
|
+
pausablePureEffect(() => {
|
|
3938
4201
|
const k = keySig();
|
|
3939
4202
|
const internalValue = internal();
|
|
3940
4203
|
if (k === prevKey) {
|
|
@@ -3947,14 +4210,11 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
3947
4210
|
prevKey = k;
|
|
3948
4211
|
internal.set(value); // load new value
|
|
3949
4212
|
}
|
|
3950
|
-
});
|
|
4213
|
+
}, { injector, pause });
|
|
3951
4214
|
}
|
|
3952
4215
|
if (syncTabs && !isServer) {
|
|
3953
|
-
const destroyRef =
|
|
4216
|
+
const destroyRef = injector.get(DestroyRef);
|
|
3954
4217
|
const sync = (e) => {
|
|
3955
|
-
// `storage` events only describe Web Storage — ignore events for a different
|
|
3956
|
-
// storage area (or any event when a custom adapter is configured), otherwise an
|
|
3957
|
-
// unrelated localStorage write with the same key string corrupts our state
|
|
3958
4218
|
if (e.storageArea !== store)
|
|
3959
4219
|
return;
|
|
3960
4220
|
if (e.key !== untracked(keySig))
|
|
@@ -4083,29 +4343,26 @@ function generateDeterministicID() {
|
|
|
4083
4343
|
*
|
|
4084
4344
|
*/
|
|
4085
4345
|
function tabSync(sig, opt) {
|
|
4086
|
-
|
|
4346
|
+
const optObj = typeof opt === 'object' ? opt : undefined;
|
|
4347
|
+
const injector = optObj?.injector ?? inject(Injector);
|
|
4348
|
+
if (isPlatformServer(injector.get(PLATFORM_ID)))
|
|
4087
4349
|
return sig;
|
|
4088
4350
|
const id = typeof opt === 'string' ? opt : (opt?.id ?? generateDeterministicID());
|
|
4089
|
-
const bus =
|
|
4090
|
-
// The last value applied from a remote tab. The outbound effect skips (exactly) the run
|
|
4091
|
-
// caused by that write — without this, an inbound object (a fresh structured clone, so
|
|
4092
|
-
// never reference-equal) would be re-posted, and two tabs would ping-pong forever.
|
|
4351
|
+
const bus = injector.get(MessageBus);
|
|
4093
4352
|
const NONE = Symbol();
|
|
4094
4353
|
let received = NONE;
|
|
4095
4354
|
const { unsub, post } = bus.subscribe(id, (next) => {
|
|
4096
4355
|
const before = untracked(sig);
|
|
4097
4356
|
received = next;
|
|
4098
4357
|
sig.set(next);
|
|
4099
|
-
// Equality-suppressed write (e.g. an identical primitive): no effect run will follow,
|
|
4100
|
-
// so clear the marker — it must not swallow a later, genuinely local change.
|
|
4101
4358
|
if (untracked(sig) === before)
|
|
4102
4359
|
received = NONE;
|
|
4103
4360
|
});
|
|
4104
|
-
let
|
|
4361
|
+
let firstDone = false;
|
|
4105
4362
|
const effectRef = effect(() => {
|
|
4106
4363
|
const val = sig();
|
|
4107
|
-
if (!
|
|
4108
|
-
|
|
4364
|
+
if (!firstDone) {
|
|
4365
|
+
firstDone = true;
|
|
4109
4366
|
return;
|
|
4110
4367
|
}
|
|
4111
4368
|
if (val === received) {
|
|
@@ -4114,8 +4371,8 @@ function tabSync(sig, opt) {
|
|
|
4114
4371
|
}
|
|
4115
4372
|
received = NONE;
|
|
4116
4373
|
post(val);
|
|
4117
|
-
}, ...(ngDevMode ? [{ debugName: "effectRef" }] : []));
|
|
4118
|
-
|
|
4374
|
+
}, ...(ngDevMode ? [{ debugName: "effectRef", injector }] : [{ injector }]));
|
|
4375
|
+
injector.get(DestroyRef).onDestroy(() => {
|
|
4119
4376
|
effectRef.destroy();
|
|
4120
4377
|
unsub();
|
|
4121
4378
|
});
|
|
@@ -4319,5 +4576,5 @@ function withHistory(sourceOrValue, opt) {
|
|
|
4319
4576
|
* Generated bundle index. Do not edit.
|
|
4320
4577
|
*/
|
|
4321
4578
|
|
|
4322
|
-
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 };
|
|
4579
|
+
export { MmActivity, PAUSABLE_OPTIONS, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, batteryStatus, chunked, clipboard, combineWith, 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 };
|
|
4323
4580
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|