@mmstack/primitives 21.2.1 → 21.3.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 +59 -11
- package/fesm2022/mmstack-primitives.mjs +665 -379
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mmstack-primitives.d.ts +339 -135
|
@@ -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, isWritableSignal as isWritableSignal$2, 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,70 +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" } : /* istanbul ignore next */ {}), source,
|
|
208
|
-
computation: (items) => items.slice(0, chunkSize),
|
|
209
|
-
equal });
|
|
210
|
-
nestedEffect((cleanup) => {
|
|
211
|
-
const fullList = source();
|
|
212
|
-
const current = internal();
|
|
213
|
-
if (current.length >= fullList.length)
|
|
214
|
-
return;
|
|
215
|
-
return cleanup(delayFn(() => untracked(() => internal.set(fullList.slice(0, current.length + chunkSize)))));
|
|
216
|
-
}, {
|
|
217
|
-
injector: injector,
|
|
218
|
-
});
|
|
219
|
-
return internal.asReadonly();
|
|
220
|
-
}
|
|
221
|
-
|
|
222
158
|
/**
|
|
223
159
|
* Whether the subtree a resource/component lives in is currently PAUSED, for Activity / keep-alive.
|
|
224
160
|
* Provided by an Activity boundary (`MmActivity`, or the app-builder's per-branch injector) and read
|
|
@@ -315,24 +251,23 @@ function providePaused(source) {
|
|
|
315
251
|
}
|
|
316
252
|
|
|
317
253
|
/**
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
*
|
|
325
|
-
*
|
|
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.
|
|
326
263
|
*
|
|
327
|
-
*
|
|
264
|
+
* @example
|
|
265
|
+
* // Make everything that can pause honour the ambient Activity boundary by default:
|
|
266
|
+
* providePausableOptions({ pause: true })
|
|
328
267
|
*/
|
|
329
|
-
function
|
|
330
|
-
return
|
|
331
|
-
source: () => ({ t: target(), ready: ready() }),
|
|
332
|
-
computation: (curr, prev) => (prev === undefined || curr.ready ? curr.t : prev.value),
|
|
333
|
-
});
|
|
268
|
+
function providePausableOptions(opt) {
|
|
269
|
+
return { provide: PAUSABLE_OPTIONS, useValue: opt };
|
|
334
270
|
}
|
|
335
|
-
|
|
336
271
|
/**
|
|
337
272
|
* Resolve a {@link PauseOption} into a pause predicate, or `null` meaning "do not pause".
|
|
338
273
|
* `null` tells the caller to return the bare primitive — no wrapper is created.
|
|
@@ -347,11 +282,7 @@ function holdUntilReady(target, ready) {
|
|
|
347
282
|
*
|
|
348
283
|
* Encapsulating this here keeps every pausable primitive's branching identical and in one place.
|
|
349
284
|
*/
|
|
350
|
-
function resolvePause(opt) {
|
|
351
|
-
const explicit = opt?.pause; // distinguish explicit `true` from the omitted default
|
|
352
|
-
const pause = explicit ?? true; // explicit pausable* calls default to pausing
|
|
353
|
-
if (pause === false)
|
|
354
|
-
return null;
|
|
285
|
+
function resolvePause(opt, defaultPause = true) {
|
|
355
286
|
const run = (fn) => opt?.injector ? runInInjectionContext(opt.injector, fn) : fn();
|
|
356
287
|
// `inject` requires an injection context even with `optional: true`. A bare
|
|
357
288
|
// `pausableSignal(0)` (documented as "like `signal`") must degrade to the unwrapped
|
|
@@ -364,6 +295,12 @@ function resolvePause(opt) {
|
|
|
364
295
|
return fallback;
|
|
365
296
|
}
|
|
366
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;
|
|
367
304
|
const onServer = () => typeof pause === 'function' && !opt?.injector
|
|
368
305
|
? typeof globalThis.window === 'undefined'
|
|
369
306
|
: tryRun(() => isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser'), typeof globalThis.window === 'undefined');
|
|
@@ -373,7 +310,7 @@ function resolvePause(opt) {
|
|
|
373
310
|
return null;
|
|
374
311
|
const paused = tryRun(() => inject(PAUSED_CONTEXT, { optional: true }), null);
|
|
375
312
|
if (!paused) {
|
|
376
|
-
if (
|
|
313
|
+
if (opt?.pause === true && isDevMode())
|
|
377
314
|
console.warn('[pausable] `pause: true` but no PAUSED_CONTEXT in scope — not pausing. Provide one via an ' +
|
|
378
315
|
'Activity boundary (`MmActivity` / `providePaused`), or pass a predicate / `pause: false`.');
|
|
379
316
|
return null;
|
|
@@ -441,6 +378,92 @@ function pausableComputed(computation, options) {
|
|
|
441
378
|
return ls.asReadonly();
|
|
442
379
|
}
|
|
443
380
|
|
|
381
|
+
/**
|
|
382
|
+
* 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.
|
|
383
|
+
*
|
|
384
|
+
* 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`.
|
|
385
|
+
*
|
|
386
|
+
* @template T The type of items in the array.
|
|
387
|
+
* @param source A `Signal` or a function that returns an array of items to be processed in chunks.
|
|
388
|
+
* @param options Configuration options for chunk size, delay duration, equality function, and injector.
|
|
389
|
+
* @returns A `Signal` that emits the current chunk of items being processed.
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* const largeList = signal(Array.from({ length: 1000 }, (_, i) => i));
|
|
393
|
+
* const chunkedList = chunked(largeList, { chunkSize: 100, delay: 100 });
|
|
394
|
+
*/
|
|
395
|
+
function chunked(source, options) {
|
|
396
|
+
const { chunkSize = 50, delay = 'frame', equal, injector, pause, } = options || {};
|
|
397
|
+
let delayFn;
|
|
398
|
+
if (delay === 'frame') {
|
|
399
|
+
delayFn =
|
|
400
|
+
typeof requestAnimationFrame === 'function'
|
|
401
|
+
? (callback) => {
|
|
402
|
+
const num = requestAnimationFrame(callback);
|
|
403
|
+
return () => cancelAnimationFrame(num);
|
|
404
|
+
}
|
|
405
|
+
: // SSR: no requestAnimationFrame — approximate a frame with a timeout
|
|
406
|
+
(cb) => {
|
|
407
|
+
const num = setTimeout(cb, 16);
|
|
408
|
+
return () => clearTimeout(num);
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
else if (delay === 'microtask') {
|
|
412
|
+
delayFn = (cb) => {
|
|
413
|
+
let isCancelled = false;
|
|
414
|
+
queueMicrotask(() => {
|
|
415
|
+
if (isCancelled)
|
|
416
|
+
return;
|
|
417
|
+
cb();
|
|
418
|
+
});
|
|
419
|
+
return () => {
|
|
420
|
+
isCancelled = true;
|
|
421
|
+
};
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
delayFn = (cb) => {
|
|
426
|
+
const num = setTimeout(cb, delay);
|
|
427
|
+
return () => clearTimeout(num);
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const internal = linkedSignal({ ...(ngDevMode ? { debugName: "internal" } : /* istanbul ignore next */ {}), source,
|
|
431
|
+
computation: (items) => items.slice(0, chunkSize),
|
|
432
|
+
equal });
|
|
433
|
+
const paused = resolvePause({ injector, pause }, false);
|
|
434
|
+
nestedEffect((cleanup) => {
|
|
435
|
+
if (paused?.())
|
|
436
|
+
return;
|
|
437
|
+
const fullList = source();
|
|
438
|
+
const current = internal();
|
|
439
|
+
if (current.length >= fullList.length)
|
|
440
|
+
return;
|
|
441
|
+
return cleanup(delayFn(() => untracked(() => internal.set(fullList.slice(0, current.length + chunkSize)))));
|
|
442
|
+
}, {
|
|
443
|
+
injector,
|
|
444
|
+
});
|
|
445
|
+
return internal.asReadonly();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Structural hold-and-swap as a signal. Given a `target` (the desired value — e.g. the
|
|
450
|
+
* subtree/def/key you want to show) and a `ready` predicate, returns a signal that keeps
|
|
451
|
+
* yielding its PREVIOUS value until `ready()` is true, then swaps to the current target.
|
|
452
|
+
*
|
|
453
|
+
* This is the structural counterpart to `keepPrevious`/`commit`: where those hold a *value*
|
|
454
|
+
* through a reload, this holds a *structure* through a swap. The caller mounts the incoming
|
|
455
|
+
* structure off to the side (so its resources can settle and flip `ready`), keeps showing the
|
|
456
|
+
* held previous structure meanwhile, and lets the old one go once `ready` releases the swap.
|
|
457
|
+
*
|
|
458
|
+
* The very first value passes straight through (nothing to hold yet).
|
|
459
|
+
*/
|
|
460
|
+
function holdUntilReady(target, ready) {
|
|
461
|
+
return linkedSignal({
|
|
462
|
+
source: () => ({ t: target(), ready: ready() }),
|
|
463
|
+
computation: (curr, prev) => (prev === undefined || curr.ready ? curr.t : prev.value),
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
444
467
|
const { is } = Object;
|
|
445
468
|
function mutable(initial, opt) {
|
|
446
469
|
const baseEqual = opt?.equal ?? is;
|
|
@@ -691,6 +714,12 @@ function injectStartTransition() {
|
|
|
691
714
|
*
|
|
692
715
|
* `type` selects what "not ready" means: `'value'` (default) suspends only until a first value lands
|
|
693
716
|
* then holds through reloads; `'loading'` suspends on every in-flight load (strict suspense).
|
|
717
|
+
*
|
|
718
|
+
* SSR: the server serializes whatever the scope reports at stabilization, so a registered resource
|
|
719
|
+
* must keep the app unstable until it settles or the placeholder is what gets serialized (then
|
|
720
|
+
* flashes/mismatches on hydration). HttpClient-backed resources, httpResource & all of `@mmstack/resource`
|
|
721
|
+
* do this automatically via the HTTP layer's `PendingTasks` + transfer cache. A custom loader (raw
|
|
722
|
+
* `fetch`/promise/timer) must opt in itself: wrap it with `inject(PendingTasks).run(() => promise)`.
|
|
694
723
|
*/
|
|
695
724
|
class SuspenseBoundaryBase {
|
|
696
725
|
scope = injectTransitionScope();
|
|
@@ -2625,11 +2654,19 @@ function throttle(source, opt) {
|
|
|
2625
2654
|
tick();
|
|
2626
2655
|
};
|
|
2627
2656
|
const update = (fn) => set(fn(untracked(source)));
|
|
2657
|
+
const flush = () => {
|
|
2658
|
+
if (timeout)
|
|
2659
|
+
clearTimeout(timeout);
|
|
2660
|
+
timeout = undefined;
|
|
2661
|
+
pendingTrailing = false;
|
|
2662
|
+
fire();
|
|
2663
|
+
};
|
|
2628
2664
|
const writable = toWritable(computed(() => {
|
|
2629
2665
|
trigger();
|
|
2630
2666
|
return untracked(source);
|
|
2631
2667
|
}, opt), set, update);
|
|
2632
2668
|
writable.original = source.asReadonly();
|
|
2669
|
+
writable.flush = flush;
|
|
2633
2670
|
return writable;
|
|
2634
2671
|
}
|
|
2635
2672
|
|
|
@@ -2848,6 +2885,213 @@ function createOrientation(debugName) {
|
|
|
2848
2885
|
return state.asReadonly();
|
|
2849
2886
|
}
|
|
2850
2887
|
|
|
2888
|
+
const IDLE = {
|
|
2889
|
+
active: false,
|
|
2890
|
+
start: { x: 0, y: 0 },
|
|
2891
|
+
current: { x: 0, y: 0 },
|
|
2892
|
+
delta: { x: 0, y: 0 },
|
|
2893
|
+
pointerId: null,
|
|
2894
|
+
modifiers: { shift: false, alt: false, ctrl: false, meta: false },
|
|
2895
|
+
button: -1,
|
|
2896
|
+
pointerType: '',
|
|
2897
|
+
origin: null,
|
|
2898
|
+
};
|
|
2899
|
+
function stateEqual(a, b) {
|
|
2900
|
+
return (a.active === b.active &&
|
|
2901
|
+
a.pointerId === b.pointerId &&
|
|
2902
|
+
a.current.x === b.current.x &&
|
|
2903
|
+
a.current.y === b.current.y &&
|
|
2904
|
+
a.button === b.button &&
|
|
2905
|
+
a.pointerType === b.pointerType &&
|
|
2906
|
+
a.origin === b.origin &&
|
|
2907
|
+
a.modifiers.shift === b.modifiers.shift &&
|
|
2908
|
+
a.modifiers.alt === b.modifiers.alt &&
|
|
2909
|
+
a.modifiers.ctrl === b.modifiers.ctrl &&
|
|
2910
|
+
a.modifiers.meta === b.modifiers.meta);
|
|
2911
|
+
}
|
|
2912
|
+
/**
|
|
2913
|
+
* Tracks a pointer *gesture* (pointerdown → capture → move → up) as a signal —
|
|
2914
|
+
* the foundation for pointer-based drag/move/resize/marquee on a canvas. Unlike
|
|
2915
|
+
* native HTML5 drag, pointer events fire continuously and coordinates are
|
|
2916
|
+
* reliable. SSR-safe; cleans up its listeners automatically.
|
|
2917
|
+
*
|
|
2918
|
+
* @example
|
|
2919
|
+
* ```ts
|
|
2920
|
+
* const drag = pointerDrag({ activationThreshold: 4 });
|
|
2921
|
+
* const position = computed(() => {
|
|
2922
|
+
* const d = drag();
|
|
2923
|
+
* return d.active ? { x: base.x + d.delta.x, y: base.y + d.delta.y } : base;
|
|
2924
|
+
* });
|
|
2925
|
+
* ```
|
|
2926
|
+
*/
|
|
2927
|
+
function pointerDrag(opt) {
|
|
2928
|
+
return runInSensorContext(opt?.injector, () => createPointerDrag(opt));
|
|
2929
|
+
}
|
|
2930
|
+
function createPointerDrag(opt) {
|
|
2931
|
+
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
2932
|
+
const base = computed(() => IDLE, {
|
|
2933
|
+
debugName: opt?.debugName ?? 'pointerDrag',
|
|
2934
|
+
});
|
|
2935
|
+
base.unthrottled = base;
|
|
2936
|
+
base.cancel = () => undefined;
|
|
2937
|
+
return base;
|
|
2938
|
+
}
|
|
2939
|
+
const hostRef = inject((ElementRef), { optional: true });
|
|
2940
|
+
const { target = hostRef?.nativeElement, coordinateSpace = 'client', activationThreshold = 3, throttle = 16, handleSelector, buttons = [0], stopPropagation = false, debugName = 'pointerDrag', } = opt ?? {};
|
|
2941
|
+
const resolve = (t) => {
|
|
2942
|
+
if (!t)
|
|
2943
|
+
return null;
|
|
2944
|
+
return t instanceof ElementRef ? t.nativeElement : t;
|
|
2945
|
+
};
|
|
2946
|
+
if (!isSignal(target) && !resolve(target)) {
|
|
2947
|
+
if (isDevMode())
|
|
2948
|
+
console.warn('pointerDrag: no target element (host ElementRef missing).');
|
|
2949
|
+
const base = computed(() => IDLE, { debugName });
|
|
2950
|
+
base.unthrottled = base;
|
|
2951
|
+
base.cancel = () => undefined;
|
|
2952
|
+
return base;
|
|
2953
|
+
}
|
|
2954
|
+
const state = throttled(IDLE, {
|
|
2955
|
+
ms: throttle,
|
|
2956
|
+
leading: true,
|
|
2957
|
+
trailing: true,
|
|
2958
|
+
equal: stateEqual,
|
|
2959
|
+
debugName,
|
|
2960
|
+
});
|
|
2961
|
+
const threshold2 = activationThreshold * activationThreshold;
|
|
2962
|
+
let startPoint = { x: 0, y: 0 };
|
|
2963
|
+
let activePointerId = null;
|
|
2964
|
+
let activeButton = -1;
|
|
2965
|
+
let activePointerType = '';
|
|
2966
|
+
let activeOrigin = null;
|
|
2967
|
+
let activated = false;
|
|
2968
|
+
let gesture = null;
|
|
2969
|
+
const coord = (e) => coordinateSpace === 'page'
|
|
2970
|
+
? { x: e.pageX, y: e.pageY }
|
|
2971
|
+
: { x: e.clientX, y: e.clientY };
|
|
2972
|
+
const mods = (e) => ({
|
|
2973
|
+
shift: e.shiftKey,
|
|
2974
|
+
alt: e.altKey,
|
|
2975
|
+
ctrl: e.ctrlKey,
|
|
2976
|
+
meta: e.metaKey,
|
|
2977
|
+
});
|
|
2978
|
+
const end = () => {
|
|
2979
|
+
gesture?.abort();
|
|
2980
|
+
gesture = null;
|
|
2981
|
+
activePointerId = null;
|
|
2982
|
+
activeButton = -1;
|
|
2983
|
+
activePointerType = '';
|
|
2984
|
+
activeOrigin = null;
|
|
2985
|
+
activated = false;
|
|
2986
|
+
state.set(IDLE);
|
|
2987
|
+
state.flush(); // terminal transition: reflect IDLE now, not on the trailing edge
|
|
2988
|
+
};
|
|
2989
|
+
const onMove = (e) => {
|
|
2990
|
+
if (e.pointerId !== activePointerId)
|
|
2991
|
+
return;
|
|
2992
|
+
const current = coord(e);
|
|
2993
|
+
const delta = { x: current.x - startPoint.x, y: current.y - startPoint.y };
|
|
2994
|
+
if (!activated && delta.x * delta.x + delta.y * delta.y >= threshold2) {
|
|
2995
|
+
activated = true; // squared compare — no sqrt on the pre-activation path
|
|
2996
|
+
}
|
|
2997
|
+
state.set({
|
|
2998
|
+
active: activated,
|
|
2999
|
+
start: startPoint,
|
|
3000
|
+
current,
|
|
3001
|
+
delta,
|
|
3002
|
+
pointerId: activePointerId,
|
|
3003
|
+
modifiers: mods(e),
|
|
3004
|
+
button: activeButton, // pointermove button is -1; keep the down-button
|
|
3005
|
+
pointerType: activePointerType,
|
|
3006
|
+
origin: activeOrigin,
|
|
3007
|
+
});
|
|
3008
|
+
};
|
|
3009
|
+
const onUp = (e) => {
|
|
3010
|
+
if (e.pointerId === activePointerId)
|
|
3011
|
+
end();
|
|
3012
|
+
};
|
|
3013
|
+
const onCancel = (e) => {
|
|
3014
|
+
if (e.pointerId === activePointerId)
|
|
3015
|
+
end();
|
|
3016
|
+
};
|
|
3017
|
+
const onKey = (e) => {
|
|
3018
|
+
if (e.key === 'Escape' && activePointerId !== null)
|
|
3019
|
+
end();
|
|
3020
|
+
};
|
|
3021
|
+
const onDown = (el) => (e) => {
|
|
3022
|
+
if (activePointerId !== null)
|
|
3023
|
+
return;
|
|
3024
|
+
if (!buttons.includes(e.button))
|
|
3025
|
+
return;
|
|
3026
|
+
const matched = handleSelector
|
|
3027
|
+
? e.target?.closest?.(handleSelector)
|
|
3028
|
+
: el;
|
|
3029
|
+
if (!matched)
|
|
3030
|
+
return; // handleSelector set but pointerdown landed outside a handle
|
|
3031
|
+
if (stopPropagation)
|
|
3032
|
+
e.stopPropagation(); // claim it: an outer sensor won't also start
|
|
3033
|
+
activePointerId = e.pointerId;
|
|
3034
|
+
activeButton = e.button;
|
|
3035
|
+
activePointerType = e.pointerType;
|
|
3036
|
+
activeOrigin = matched;
|
|
3037
|
+
activated = false;
|
|
3038
|
+
startPoint = coord(e);
|
|
3039
|
+
try {
|
|
3040
|
+
el.setPointerCapture(e.pointerId);
|
|
3041
|
+
}
|
|
3042
|
+
catch {
|
|
3043
|
+
// capture unsupported (older browsers / test env) — listeners still work
|
|
3044
|
+
}
|
|
3045
|
+
gesture = new AbortController();
|
|
3046
|
+
const signal = gesture.signal;
|
|
3047
|
+
el.addEventListener('pointermove', onMove, { signal });
|
|
3048
|
+
el.addEventListener('pointerup', onUp, { signal });
|
|
3049
|
+
el.addEventListener('pointercancel', onCancel, { signal });
|
|
3050
|
+
el.addEventListener('lostpointercapture', onCancel, {
|
|
3051
|
+
signal,
|
|
3052
|
+
});
|
|
3053
|
+
window.addEventListener('keydown', onKey, { signal });
|
|
3054
|
+
state.set({
|
|
3055
|
+
active: false,
|
|
3056
|
+
start: startPoint,
|
|
3057
|
+
current: startPoint,
|
|
3058
|
+
delta: { x: 0, y: 0 },
|
|
3059
|
+
pointerId: e.pointerId,
|
|
3060
|
+
modifiers: mods(e),
|
|
3061
|
+
button: e.button,
|
|
3062
|
+
pointerType: activePointerType,
|
|
3063
|
+
origin: activeOrigin,
|
|
3064
|
+
});
|
|
3065
|
+
};
|
|
3066
|
+
const attach = (el) => {
|
|
3067
|
+
const controller = new AbortController();
|
|
3068
|
+
el.addEventListener('pointerdown', onDown(el), {
|
|
3069
|
+
signal: controller.signal,
|
|
3070
|
+
});
|
|
3071
|
+
return () => {
|
|
3072
|
+
controller.abort();
|
|
3073
|
+
end();
|
|
3074
|
+
};
|
|
3075
|
+
};
|
|
3076
|
+
if (isSignal(target)) {
|
|
3077
|
+
effect((cleanup) => {
|
|
3078
|
+
const el = resolve(target());
|
|
3079
|
+
if (!el)
|
|
3080
|
+
return;
|
|
3081
|
+
cleanup(attach(el));
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
3084
|
+
else {
|
|
3085
|
+
const el = resolve(target);
|
|
3086
|
+
if (el)
|
|
3087
|
+
inject(DestroyRef).onDestroy(attach(el));
|
|
3088
|
+
}
|
|
3089
|
+
const base = state.asReadonly();
|
|
3090
|
+
base.unthrottled = state.original;
|
|
3091
|
+
base.cancel = end;
|
|
3092
|
+
return base;
|
|
3093
|
+
}
|
|
3094
|
+
|
|
2851
3095
|
/**
|
|
2852
3096
|
* Creates a read-only signal that tracks the page's visibility state.
|
|
2853
3097
|
*
|
|
@@ -3074,6 +3318,8 @@ function sensor(type, options) {
|
|
|
3074
3318
|
switch (type) {
|
|
3075
3319
|
case 'mousePosition':
|
|
3076
3320
|
return mousePosition(opts);
|
|
3321
|
+
case 'pointerDrag':
|
|
3322
|
+
return pointerDrag(opts);
|
|
3077
3323
|
case 'networkStatus':
|
|
3078
3324
|
return networkStatus(opts);
|
|
3079
3325
|
case 'pageVisibility':
|
|
@@ -3191,9 +3437,6 @@ function signalFromEvent(target, eventName, initial, projectOrOpt, maybeOpt) {
|
|
|
3191
3437
|
return untracked(() => state.asReadonly());
|
|
3192
3438
|
}
|
|
3193
3439
|
|
|
3194
|
-
function isWritableSignal(value) {
|
|
3195
|
-
return isWritableSignal$2(value);
|
|
3196
|
-
}
|
|
3197
3440
|
/**
|
|
3198
3441
|
* Runtime marker + compile-time brand for an opaque value. A `const`-declared `Symbol`
|
|
3199
3442
|
* has a `unique symbol` type, so the same symbol serves as both the property key written
|
|
@@ -3235,12 +3478,16 @@ function isOpaque(value) {
|
|
|
3235
3478
|
value !== null &&
|
|
3236
3479
|
value[OPAQUE] === true);
|
|
3237
3480
|
}
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3481
|
+
|
|
3482
|
+
function isWritableSignal(value) {
|
|
3483
|
+
return isWritableSignal$2(value);
|
|
3484
|
+
}
|
|
3485
|
+
function isRecord(value) {
|
|
3486
|
+
if (value === null || typeof value !== 'object' || isOpaque(value))
|
|
3487
|
+
return false;
|
|
3488
|
+
const proto = Object.getPrototypeOf(value);
|
|
3489
|
+
return proto === Object.prototype || proto === null;
|
|
3490
|
+
}
|
|
3244
3491
|
/**
|
|
3245
3492
|
* @internal Whether a value is a terminal leaf: a concrete non-record/non-array value always is;
|
|
3246
3493
|
* `null`/`undefined` is a leaf only when vivification is disabled (with vivify on it can still
|
|
@@ -3253,6 +3500,65 @@ function isLeafValue(value, vivifyEnabled) {
|
|
|
3253
3500
|
return true; // opaque always wins — even arrays
|
|
3254
3501
|
return !Array.isArray(value) && !isRecord(value);
|
|
3255
3502
|
}
|
|
3503
|
+
/**
|
|
3504
|
+
* @internal
|
|
3505
|
+
* Resolves the vivify shape for a node from its current value: a present record/array is a
|
|
3506
|
+
* certainty we keep (cached in the derivation, so it survives the value being nulled); an
|
|
3507
|
+
* unknown value (`null`/`undefined`) defers to the caller's option. Off stays off.
|
|
3508
|
+
*/
|
|
3509
|
+
function resolveVivify(sample, option) {
|
|
3510
|
+
if (!option)
|
|
3511
|
+
return false;
|
|
3512
|
+
if (Array.isArray(sample))
|
|
3513
|
+
return 'array';
|
|
3514
|
+
if (isRecord(sample))
|
|
3515
|
+
return 'object';
|
|
3516
|
+
return 'auto';
|
|
3517
|
+
}
|
|
3518
|
+
function hasOwnKey(value, key) {
|
|
3519
|
+
return value != null && Object.hasOwn(value, key);
|
|
3520
|
+
}
|
|
3521
|
+
/**
|
|
3522
|
+
* @internal
|
|
3523
|
+
* Builds the `onChange` for the fallback (non-record container) derivation branch. For an
|
|
3524
|
+
* immutable source the container is copied before the write — returning the same mutated
|
|
3525
|
+
* reference would let the source's equality cut propagation (leaving child signals permanently
|
|
3526
|
+
* stale) and alias the caller's original object, breaking the structural-sharing contract
|
|
3527
|
+
* `forkStore` relies on. For a mutable source the write goes through `mutate`, so the chain's
|
|
3528
|
+
* force-notify engages (plain `update` with the same reference would never notify).
|
|
3529
|
+
*/
|
|
3530
|
+
function createFallbackOnChange(target, prop, vivifyFn, isMutableSource) {
|
|
3531
|
+
const write = (newValue) => (v) => {
|
|
3532
|
+
const container = vivifyFn(v, prop);
|
|
3533
|
+
if (container === null || container === undefined)
|
|
3534
|
+
return container;
|
|
3535
|
+
const next = isMutableSource
|
|
3536
|
+
? container
|
|
3537
|
+
: Array.isArray(container)
|
|
3538
|
+
? container.slice()
|
|
3539
|
+
: isRecord(container)
|
|
3540
|
+
? { ...container }
|
|
3541
|
+
: container; // non-plain leaf (Date/class instance): legacy in-place attempt
|
|
3542
|
+
try {
|
|
3543
|
+
next[prop] = newValue;
|
|
3544
|
+
}
|
|
3545
|
+
catch (e) {
|
|
3546
|
+
if (isDevMode())
|
|
3547
|
+
console.error(`[store] Failed to set property "${String(prop)}"`, e);
|
|
3548
|
+
}
|
|
3549
|
+
return next;
|
|
3550
|
+
};
|
|
3551
|
+
return isMutableSource
|
|
3552
|
+
? (newValue) => target.mutate(write(newValue))
|
|
3553
|
+
: (newValue) => target.update(write(newValue));
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
/**
|
|
3557
|
+
* @internal Runtime brand carrying a store node's lazily-built leaf probe. Exported (like
|
|
3558
|
+
* {@link OPAQUE}) only so the `{ readonly [LEAF]: () => boolean }` brand on the store types is
|
|
3559
|
+
* nameable in the emitted declarations — not part of the supported surface; use {@link isLeaf}.
|
|
3560
|
+
*/
|
|
3561
|
+
const LEAF = Symbol('@mmstack/primitives::store/LEAF');
|
|
3256
3562
|
/**
|
|
3257
3563
|
* @internal Constant leaf probes for nodes whose leaf-ness is statically known, so the reactive
|
|
3258
3564
|
* `computed` can be skipped entirely.
|
|
@@ -3310,14 +3616,17 @@ function markAsLeaf(sig, value, vivifyEnabled, noUnionLeaves) {
|
|
|
3310
3616
|
function isLeaf(value) {
|
|
3311
3617
|
return isStore(value) && value[LEAF]?.() === true;
|
|
3312
3618
|
}
|
|
3619
|
+
|
|
3313
3620
|
const IS_STORE = Symbol('@mmstack/primitives::store/IS_STORE');
|
|
3314
3621
|
const SCOPE_PARENT = Symbol('@mmstack/primitives::store/SCOPE_PARENT');
|
|
3622
|
+
const STORE_SHARED_GLOBALS = Symbol('@mmstack/primitives::store/STORE_SHARED_GLOBALS');
|
|
3623
|
+
const STORE_SHARED_OPTIONS = Symbol('@mmstack/primitives::store/STORE_SHARED_OPTIONS');
|
|
3315
3624
|
/**
|
|
3316
|
-
* @internal
|
|
3317
|
-
*
|
|
3318
|
-
*
|
|
3625
|
+
* @internal Brand carrying a store's writability ('mutable' | 'writable' | 'readonly'), stamped
|
|
3626
|
+
* on every store proxy. Read by `extendStore` instead of re-deriving via `isWritableSignal`,
|
|
3627
|
+
* which would mis-classify a readonly scoped store (its backing `toWritable` still has a `set`).
|
|
3319
3628
|
*/
|
|
3320
|
-
const
|
|
3629
|
+
const STORE_KIND = Symbol('@mmstack/primitives::store/STORE_KIND');
|
|
3321
3630
|
const SIGNAL_FN_PROP = new Set([
|
|
3322
3631
|
'set',
|
|
3323
3632
|
'update',
|
|
@@ -3325,15 +3634,20 @@ const SIGNAL_FN_PROP = new Set([
|
|
|
3325
3634
|
'inline',
|
|
3326
3635
|
'asReadonly',
|
|
3327
3636
|
]);
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3637
|
+
const PROXY_CACHE_TOKEN = new InjectionToken('@mmstack/primitives:store-proxy-cache', {
|
|
3638
|
+
providedIn: 'root',
|
|
3639
|
+
factory: () => new WeakMap(),
|
|
3640
|
+
});
|
|
3641
|
+
const PROXY_CLEANUP_TOKEN = new InjectionToken('@mmstack/primitives:store-proxy-cleanup', {
|
|
3642
|
+
providedIn: 'root',
|
|
3643
|
+
factory: () => {
|
|
3644
|
+
const cache = inject(PROXY_CACHE_TOKEN);
|
|
3645
|
+
return new FinalizationRegistry(({ target, prop }) => {
|
|
3646
|
+
const store = cache.get(target);
|
|
3647
|
+
if (store)
|
|
3648
|
+
store.delete(prop);
|
|
3649
|
+
});
|
|
3650
|
+
},
|
|
3337
3651
|
});
|
|
3338
3652
|
/**
|
|
3339
3653
|
* @internal
|
|
@@ -3344,199 +3658,78 @@ function isStore(value) {
|
|
|
3344
3658
|
value !== null &&
|
|
3345
3659
|
value[IS_STORE] === true);
|
|
3346
3660
|
}
|
|
3347
|
-
|
|
3348
|
-
if (value === null || typeof value !== 'object' || isOpaque(value))
|
|
3349
|
-
return false;
|
|
3350
|
-
const proto = Object.getPrototypeOf(value);
|
|
3351
|
-
return proto === Object.prototype || proto === null;
|
|
3352
|
-
}
|
|
3353
|
-
/**
|
|
3354
|
-
* @internal
|
|
3355
|
-
* Resolves the vivify shape for a node from its current value: a present record/array is a
|
|
3356
|
-
* certainty we keep (cached in the derivation, so it survives the value being nulled); an
|
|
3357
|
-
* unknown value (`null`/`undefined`) defers to the caller's option. Off stays off.
|
|
3358
|
-
*/
|
|
3359
|
-
function resolveVivify(sample, option) {
|
|
3360
|
-
if (!option)
|
|
3361
|
-
return false;
|
|
3362
|
-
if (Array.isArray(sample))
|
|
3363
|
-
return 'array';
|
|
3364
|
-
if (isRecord(sample))
|
|
3365
|
-
return 'object';
|
|
3366
|
-
return 'auto';
|
|
3367
|
-
}
|
|
3368
|
-
function hasOwnKey(value, key) {
|
|
3369
|
-
return value != null && Object.hasOwn(value, key);
|
|
3370
|
-
}
|
|
3661
|
+
|
|
3371
3662
|
/**
|
|
3372
|
-
* @internal
|
|
3373
|
-
*
|
|
3374
|
-
*
|
|
3375
|
-
* reference would let the source's equality cut propagation (leaving child signals permanently
|
|
3376
|
-
* stale) and alias the caller's original object, breaking the structural-sharing contract
|
|
3377
|
-
* `forkStore` relies on. For a mutable source the write goes through `mutate`, so the chain's
|
|
3378
|
-
* force-notify engages (plain `update` with the same reference would never notify).
|
|
3663
|
+
* @internal Reads (or lazily builds + caches) the child node proxy for `prop` on `target`,
|
|
3664
|
+
* holding it via a `WeakRef` and registering it for finalizer-driven cache pruning. The cache
|
|
3665
|
+
* is keyed per backing signal, so child identity is stable across repeat reads.
|
|
3379
3666
|
*/
|
|
3380
|
-
function
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
return next;
|
|
3400
|
-
};
|
|
3401
|
-
return isMutableSource
|
|
3402
|
-
? (newValue) => target.mutate(write(newValue))
|
|
3403
|
-
: (newValue) => target.update(write(newValue));
|
|
3667
|
+
function getCachedChild(target, prop, build, cache, cleanupRegistry) {
|
|
3668
|
+
let storeCache = cache.get(target);
|
|
3669
|
+
if (!storeCache) {
|
|
3670
|
+
storeCache = new Map();
|
|
3671
|
+
cache.set(target, storeCache);
|
|
3672
|
+
}
|
|
3673
|
+
const cachedRef = storeCache.get(prop);
|
|
3674
|
+
if (cachedRef) {
|
|
3675
|
+
const cached = cachedRef.deref();
|
|
3676
|
+
if (cached)
|
|
3677
|
+
return cached;
|
|
3678
|
+
storeCache.delete(prop);
|
|
3679
|
+
cleanupRegistry.unregister(cachedRef);
|
|
3680
|
+
}
|
|
3681
|
+
const proxy = build();
|
|
3682
|
+
const ref = new WeakRef(proxy);
|
|
3683
|
+
storeCache.set(prop, ref);
|
|
3684
|
+
cleanupRegistry.register(proxy, { target, prop }, ref);
|
|
3685
|
+
return proxy;
|
|
3404
3686
|
}
|
|
3405
3687
|
/**
|
|
3406
|
-
* @internal
|
|
3407
|
-
*
|
|
3688
|
+
* @internal Builds the derived child signal for `prop` and wraps it as an array/object substore.
|
|
3689
|
+
* A record parent reads the key directly; any other container goes through the fallback `from`/
|
|
3690
|
+
* `onChange` path. Shared verbatim by the array and object proxies — the only place a child node
|
|
3691
|
+
* is constructed.
|
|
3408
3692
|
*/
|
|
3409
|
-
function
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
const
|
|
3413
|
-
const
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
const v = untracked(source);
|
|
3433
|
-
if (!Array.isArray(v))
|
|
3434
|
-
return [];
|
|
3435
|
-
const len = v.length;
|
|
3436
|
-
const arr = new Array(len + 1);
|
|
3437
|
-
for (let i = 0; i < len; i++) {
|
|
3438
|
-
arr[i] = String(i);
|
|
3439
|
-
}
|
|
3440
|
-
arr[len] = 'length';
|
|
3441
|
-
return arr;
|
|
3442
|
-
},
|
|
3443
|
-
getPrototypeOf() {
|
|
3444
|
-
return Array.prototype;
|
|
3445
|
-
},
|
|
3446
|
-
getOwnPropertyDescriptor(_, prop) {
|
|
3447
|
-
const v = untracked(source);
|
|
3448
|
-
if (!Array.isArray(v))
|
|
3449
|
-
return;
|
|
3450
|
-
if (prop === 'length' ||
|
|
3451
|
-
(typeof prop === 'string' && !isNaN(+prop) && +prop < v.length)) {
|
|
3452
|
-
return {
|
|
3453
|
-
enumerable: true,
|
|
3454
|
-
configurable: true, // Required for proxies to dynamic targets
|
|
3455
|
-
};
|
|
3456
|
-
}
|
|
3457
|
-
return;
|
|
3458
|
-
},
|
|
3459
|
-
get(target, prop, receiver) {
|
|
3460
|
-
if (prop === IS_STORE)
|
|
3461
|
-
return true;
|
|
3462
|
-
if (prop === 'length')
|
|
3463
|
-
return lengthSignal;
|
|
3464
|
-
if (prop === Symbol.iterator) {
|
|
3465
|
-
return function* () {
|
|
3466
|
-
// read length reactively: a spread/for-of inside a computed/effect must re-run
|
|
3467
|
-
// when items are added or removed, not only when already-read elements change
|
|
3468
|
-
for (let i = 0; i < lengthSignal(); i++) {
|
|
3469
|
-
yield receiver[i];
|
|
3470
|
-
}
|
|
3471
|
-
};
|
|
3472
|
-
}
|
|
3473
|
-
if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
|
|
3474
|
-
return target[prop];
|
|
3475
|
-
if (isIndexProp(prop)) {
|
|
3476
|
-
const idx = +prop;
|
|
3477
|
-
let storeCache = PROXY_CACHE.get(target);
|
|
3478
|
-
if (!storeCache) {
|
|
3479
|
-
storeCache = new Map();
|
|
3480
|
-
PROXY_CACHE.set(target, storeCache);
|
|
3481
|
-
}
|
|
3482
|
-
const cachedRef = storeCache.get(idx);
|
|
3483
|
-
if (cachedRef) {
|
|
3484
|
-
const cached = cachedRef.deref();
|
|
3485
|
-
if (cached)
|
|
3486
|
-
return cached;
|
|
3487
|
-
storeCache.delete(idx);
|
|
3488
|
-
PROXY_CLEANUP.unregister(cachedRef);
|
|
3489
|
-
}
|
|
3490
|
-
const value = untracked(target);
|
|
3491
|
-
const valueIsArray = Array.isArray(value);
|
|
3492
|
-
const valueIsRecord = isRecord(value);
|
|
3493
|
-
const nodeVivify = resolveVivify(value, vivify);
|
|
3494
|
-
const vivifyFn = createVivify(nodeVivify);
|
|
3495
|
-
const equalFn = (valueIsRecord || valueIsArray) &&
|
|
3496
|
-
isMutableSource &&
|
|
3497
|
-
typeof value[idx] === 'object'
|
|
3498
|
-
? () => false
|
|
3499
|
-
: undefined;
|
|
3500
|
-
const computation = valueIsRecord
|
|
3501
|
-
? derived(target, idx, {
|
|
3502
|
-
equal: equalFn,
|
|
3503
|
-
vivify: nodeVivify,
|
|
3504
|
-
})
|
|
3505
|
-
: derived(target, {
|
|
3506
|
-
from: (v) => v?.[idx],
|
|
3507
|
-
onChange: createFallbackOnChange(target, idx, vivifyFn, isMutableSource),
|
|
3508
|
-
equal: equalFn,
|
|
3509
|
-
});
|
|
3510
|
-
const childSample = untracked(computation);
|
|
3511
|
-
const childVivify = resolveVivify(childSample, vivify);
|
|
3512
|
-
const proxy = Array.isArray(childSample) && !isOpaque(childSample)
|
|
3513
|
-
? toArrayStore(computation, injector, childVivify, noUnionLeaves)
|
|
3514
|
-
: toStore(computation, injector, childVivify, noUnionLeaves);
|
|
3515
|
-
markAsLeaf(proxy, computation, childVivify !== false, noUnionLeaves);
|
|
3516
|
-
const ref = new WeakRef(proxy);
|
|
3517
|
-
storeCache.set(idx, ref);
|
|
3518
|
-
PROXY_CLEANUP.register(proxy, { target, prop: idx }, ref);
|
|
3519
|
-
return proxy;
|
|
3520
|
-
}
|
|
3521
|
-
return Reflect.get(target, prop, receiver);
|
|
3522
|
-
},
|
|
3523
|
-
});
|
|
3693
|
+
function buildChildNode(target, prop, isMutableSource, options) {
|
|
3694
|
+
const value = untracked(target);
|
|
3695
|
+
const valueIsRecord = isRecord(value);
|
|
3696
|
+
const valueIsArray = Array.isArray(value);
|
|
3697
|
+
const nodeVivify = resolveVivify(value, options.vivify);
|
|
3698
|
+
const vivifyFn = createVivify(nodeVivify);
|
|
3699
|
+
const equalFn = (valueIsRecord || valueIsArray) &&
|
|
3700
|
+
isMutableSource &&
|
|
3701
|
+
typeof value[prop] === 'object'
|
|
3702
|
+
? () => false
|
|
3703
|
+
: undefined;
|
|
3704
|
+
const computation = valueIsRecord
|
|
3705
|
+
? derived(target, prop, { equal: equalFn, vivify: nodeVivify })
|
|
3706
|
+
: derived(target, {
|
|
3707
|
+
from: (v) => v?.[prop],
|
|
3708
|
+
onChange: createFallbackOnChange(target, prop, vivifyFn, isMutableSource),
|
|
3709
|
+
equal: equalFn,
|
|
3710
|
+
});
|
|
3711
|
+
const childSample = untracked(computation);
|
|
3712
|
+
const childVivify = resolveVivify(childSample, options.vivify);
|
|
3713
|
+
const proxy = toStore(computation, options);
|
|
3714
|
+
markAsLeaf(proxy, computation, childVivify !== false, options.noUnionLeaves);
|
|
3715
|
+
return proxy;
|
|
3524
3716
|
}
|
|
3525
3717
|
/**
|
|
3526
3718
|
* Converts a Signal into a deep-observable Store.
|
|
3527
3719
|
* Accessing nested properties returns a derived Signal of that path.
|
|
3528
3720
|
*
|
|
3529
3721
|
* @remarks
|
|
3530
|
-
* A
|
|
3531
|
-
*
|
|
3532
|
-
*
|
|
3533
|
-
*
|
|
3722
|
+
* A node's *container kind* (array / record / primitive) is tracked reactively via a per-node
|
|
3723
|
+
* `kind` computed, so the same proxy serves all three and a union node that flips between an
|
|
3724
|
+
* array and a record keeps working. Flips are route-forward: after a flip the node behaves as
|
|
3725
|
+
* its new kind on the next access, while child proxies cached under the old shape go stale and
|
|
3726
|
+
* are pruned by the GC.
|
|
3534
3727
|
*
|
|
3535
3728
|
* @example
|
|
3536
3729
|
* const state = store({ user: { name: 'John' } });
|
|
3537
3730
|
* const nameSignal = state.user.name; // WritableSignal<string>
|
|
3538
3731
|
*/
|
|
3539
|
-
function toStore(source, injector, vivify = false, noUnionLeaves = false) {
|
|
3732
|
+
function toStore(source, { injector, vivify = false, noUnionLeaves = false, ...rest } = {}) {
|
|
3540
3733
|
if (isStore(source))
|
|
3541
3734
|
return source;
|
|
3542
3735
|
if (!injector)
|
|
@@ -3548,106 +3741,140 @@ function toStore(source, injector, vivify = false, noUnionLeaves = false) {
|
|
|
3548
3741
|
});
|
|
3549
3742
|
const isWritableSource = isWritableSignal(source);
|
|
3550
3743
|
const isMutableSource = isWritableSource && isMutable(writableSource);
|
|
3744
|
+
const kind = computed(() => {
|
|
3745
|
+
const v = source();
|
|
3746
|
+
if (Array.isArray(v) && !isOpaque(v))
|
|
3747
|
+
return 'array';
|
|
3748
|
+
if (isRecord(v))
|
|
3749
|
+
return 'record';
|
|
3750
|
+
return 'primitive';
|
|
3751
|
+
}, ...(ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []));
|
|
3752
|
+
const STORE_OPTIONS = {
|
|
3753
|
+
injector,
|
|
3754
|
+
vivify,
|
|
3755
|
+
noUnionLeaves,
|
|
3756
|
+
[STORE_SHARED_GLOBALS]: {
|
|
3757
|
+
cache: rest[STORE_SHARED_GLOBALS]?.cache ?? injector.get(PROXY_CACHE_TOKEN),
|
|
3758
|
+
registry: rest[STORE_SHARED_GLOBALS]?.registry ??
|
|
3759
|
+
injector.get(PROXY_CLEANUP_TOKEN),
|
|
3760
|
+
},
|
|
3761
|
+
};
|
|
3762
|
+
// built lazily so non-array nodes never allocate it
|
|
3763
|
+
let length;
|
|
3764
|
+
const arrayLength = () => (length ??= computed(() => {
|
|
3765
|
+
const v = source();
|
|
3766
|
+
return Array.isArray(v) ? v.length : 0;
|
|
3767
|
+
}));
|
|
3551
3768
|
const s = new Proxy(writableSource, {
|
|
3552
3769
|
has(_, prop) {
|
|
3553
|
-
|
|
3770
|
+
const v = untracked(source);
|
|
3771
|
+
if (untracked(kind) === 'array') {
|
|
3772
|
+
if (prop === 'length')
|
|
3773
|
+
return true;
|
|
3774
|
+
if (isIndexProp(prop)) {
|
|
3775
|
+
const idx = +prop;
|
|
3776
|
+
return idx >= 0 && idx < v.length;
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
// nullish node values are routinely descended with vivify on — `in` must not throw
|
|
3780
|
+
return v == null ? false : Reflect.has(v, prop);
|
|
3554
3781
|
},
|
|
3555
3782
|
ownKeys() {
|
|
3556
3783
|
const v = untracked(source);
|
|
3784
|
+
if (untracked(kind) === 'array') {
|
|
3785
|
+
const len = v.length;
|
|
3786
|
+
const arr = new Array(len + 1);
|
|
3787
|
+
for (let i = 0; i < len; i++)
|
|
3788
|
+
arr[i] = String(i);
|
|
3789
|
+
arr[len] = 'length';
|
|
3790
|
+
return arr;
|
|
3791
|
+
}
|
|
3557
3792
|
if (!isRecord(v))
|
|
3558
3793
|
return [];
|
|
3559
3794
|
return Reflect.ownKeys(v);
|
|
3560
3795
|
},
|
|
3561
3796
|
getPrototypeOf() {
|
|
3562
|
-
|
|
3797
|
+
if (untracked(kind) === 'array')
|
|
3798
|
+
return Array.prototype;
|
|
3799
|
+
const v = untracked(source);
|
|
3800
|
+
return v == null ? Object.prototype : Object.getPrototypeOf(v);
|
|
3563
3801
|
},
|
|
3564
3802
|
getOwnPropertyDescriptor(_, prop) {
|
|
3565
|
-
const
|
|
3566
|
-
if (
|
|
3803
|
+
const v = untracked(source);
|
|
3804
|
+
if (untracked(kind) === 'array') {
|
|
3805
|
+
if (prop === 'length' ||
|
|
3806
|
+
(typeof prop === 'string' && !isNaN(+prop) && +prop < v.length))
|
|
3807
|
+
return { enumerable: true, configurable: true };
|
|
3567
3808
|
return;
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
};
|
|
3809
|
+
}
|
|
3810
|
+
if (!isRecord(v) || !(prop in v))
|
|
3811
|
+
return;
|
|
3812
|
+
return { enumerable: true, configurable: true };
|
|
3572
3813
|
},
|
|
3573
|
-
get(target, prop) {
|
|
3574
|
-
if (prop ===
|
|
3575
|
-
|
|
3814
|
+
get(target, prop, receiver) {
|
|
3815
|
+
if (typeof prop === 'symbol') {
|
|
3816
|
+
if (prop === IS_STORE)
|
|
3817
|
+
return true;
|
|
3818
|
+
if (prop === STORE_KIND)
|
|
3819
|
+
return isMutableSource
|
|
3820
|
+
? 'mutable'
|
|
3821
|
+
: isWritableSource
|
|
3822
|
+
? 'writable'
|
|
3823
|
+
: 'readonly';
|
|
3824
|
+
if (prop === STORE_SHARED_OPTIONS)
|
|
3825
|
+
return STORE_OPTIONS;
|
|
3826
|
+
}
|
|
3576
3827
|
if (prop === 'asReadonlyStore')
|
|
3577
3828
|
return () => {
|
|
3578
3829
|
if (!isWritableSource)
|
|
3579
3830
|
return s;
|
|
3580
|
-
return untracked(() => toStore(source.asReadonly(), injector, vivify, noUnionLeaves));
|
|
3831
|
+
return untracked(() => toStore(source.asReadonly(), { injector, vivify, noUnionLeaves }));
|
|
3581
3832
|
};
|
|
3582
|
-
|
|
3833
|
+
const k = untracked(kind);
|
|
3834
|
+
if (prop === 'extend' && k !== 'array')
|
|
3583
3835
|
return (seed) => scopedStore(s, seed, isMutableSource
|
|
3584
3836
|
? 'mutable'
|
|
3585
3837
|
: isWritableSource
|
|
3586
3838
|
? 'writable'
|
|
3587
|
-
: 'readonly',
|
|
3839
|
+
: 'readonly', STORE_OPTIONS);
|
|
3840
|
+
if (k === 'array') {
|
|
3841
|
+
if (prop === 'length')
|
|
3842
|
+
return arrayLength();
|
|
3843
|
+
if (prop === Symbol.iterator)
|
|
3844
|
+
return function* () {
|
|
3845
|
+
// read length reactively: a spread/for-of inside a computed/effect must re-run
|
|
3846
|
+
// when items are added or removed, not only when already-read elements change
|
|
3847
|
+
const len = arrayLength();
|
|
3848
|
+
for (let i = 0; i < len(); i++)
|
|
3849
|
+
yield receiver[i];
|
|
3850
|
+
};
|
|
3851
|
+
}
|
|
3588
3852
|
if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
|
|
3589
3853
|
return target[prop];
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
PROXY_CACHE.set(target, storeCache);
|
|
3594
|
-
}
|
|
3595
|
-
const cachedRef = storeCache.get(prop);
|
|
3596
|
-
if (cachedRef) {
|
|
3597
|
-
const cached = cachedRef.deref();
|
|
3598
|
-
if (cached)
|
|
3599
|
-
return cached;
|
|
3600
|
-
storeCache.delete(prop);
|
|
3601
|
-
PROXY_CLEANUP.unregister(cachedRef);
|
|
3602
|
-
}
|
|
3603
|
-
const value = untracked(target);
|
|
3604
|
-
const valueIsRecord = isRecord(value);
|
|
3605
|
-
const valueIsArray = Array.isArray(value);
|
|
3606
|
-
const nodeVivify = resolveVivify(value, vivify);
|
|
3607
|
-
const vivifyFn = createVivify(nodeVivify);
|
|
3608
|
-
const equalFn = (valueIsRecord || valueIsArray) &&
|
|
3609
|
-
isMutableSource &&
|
|
3610
|
-
typeof value[prop] === 'object'
|
|
3611
|
-
? () => false
|
|
3612
|
-
: undefined;
|
|
3613
|
-
const computation = valueIsRecord
|
|
3614
|
-
? derived(target, prop, { equal: equalFn, vivify: nodeVivify })
|
|
3615
|
-
: derived(target, {
|
|
3616
|
-
from: (v) => v?.[prop],
|
|
3617
|
-
onChange: createFallbackOnChange(target, prop, vivifyFn, isMutableSource),
|
|
3618
|
-
equal: equalFn,
|
|
3619
|
-
});
|
|
3620
|
-
const childSample = untracked(computation);
|
|
3621
|
-
const childVivify = resolveVivify(childSample, vivify);
|
|
3622
|
-
const proxy = Array.isArray(childSample) && !isOpaque(childSample)
|
|
3623
|
-
? toArrayStore(computation, injector, childVivify, noUnionLeaves)
|
|
3624
|
-
: toStore(computation, injector, childVivify, noUnionLeaves);
|
|
3625
|
-
markAsLeaf(proxy, computation, childVivify !== false, noUnionLeaves);
|
|
3626
|
-
const ref = new WeakRef(proxy);
|
|
3627
|
-
storeCache.set(prop, ref);
|
|
3628
|
-
PROXY_CLEANUP.register(proxy, { target, prop }, ref);
|
|
3629
|
-
return proxy;
|
|
3854
|
+
if (k === 'array' && !isIndexProp(prop))
|
|
3855
|
+
return Reflect.get(target, prop, receiver);
|
|
3856
|
+
return getCachedChild(target, prop, () => buildChildNode(target, k === 'array' ? +prop : prop, isMutableSource, STORE_OPTIONS), STORE_OPTIONS[STORE_SHARED_GLOBALS].cache, STORE_OPTIONS[STORE_SHARED_GLOBALS].registry);
|
|
3630
3857
|
},
|
|
3631
3858
|
});
|
|
3632
3859
|
return s;
|
|
3633
3860
|
}
|
|
3634
3861
|
/**
|
|
3635
3862
|
* @internal
|
|
3636
|
-
* Backs `
|
|
3863
|
+
* Backs `extendStore(...)`. Builds a scoped overlay over `parent`: the local layer (the seed
|
|
3637
3864
|
* plus any keys created later) is its own signal and `parent` is its own signal, so the getter
|
|
3638
3865
|
* routes each key by consulting BOTH — local first, then parent, else local (so a write to an
|
|
3639
3866
|
* as-yet-unknown key lands locally). Inherited keys return the parent's own sub-store (shared
|
|
3640
3867
|
* identity + two-way), while local keys never propagate upward. A merged `computed` is derived
|
|
3641
3868
|
* only for whole-object reads / `has` / iteration — never for routing.
|
|
3642
3869
|
*/
|
|
3643
|
-
function scopedStore(parent, seed, kind,
|
|
3870
|
+
function scopedStore(parent, seed, kind, options) {
|
|
3644
3871
|
const local = isSignal(seed)
|
|
3645
|
-
? toStore(seed,
|
|
3872
|
+
? toStore(seed, options)
|
|
3646
3873
|
: kind === 'mutable'
|
|
3647
|
-
? mutableStore(seed,
|
|
3874
|
+
? mutableStore(seed, options)
|
|
3648
3875
|
: kind === 'readonly'
|
|
3649
|
-
? store(seed,
|
|
3650
|
-
: store(seed,
|
|
3876
|
+
? store(seed, options).asReadonlyStore()
|
|
3877
|
+
: store(seed, options);
|
|
3651
3878
|
const localValue = () => untracked(local);
|
|
3652
3879
|
const parentValue = () => untracked(parent);
|
|
3653
3880
|
const view = computed(() => ({
|
|
@@ -3678,14 +3905,20 @@ function scopedStore(parent, seed, kind, injector) {
|
|
|
3678
3905
|
}
|
|
3679
3906
|
const scope = new Proxy(base, {
|
|
3680
3907
|
get(target, prop) {
|
|
3681
|
-
if (prop ===
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3908
|
+
if (typeof prop === 'symbol') {
|
|
3909
|
+
if (prop === IS_STORE)
|
|
3910
|
+
return true;
|
|
3911
|
+
if (prop === STORE_KIND)
|
|
3912
|
+
return kind;
|
|
3913
|
+
if (prop === SCOPE_PARENT)
|
|
3914
|
+
return parent;
|
|
3915
|
+
if (prop === STORE_SHARED_OPTIONS)
|
|
3916
|
+
return options;
|
|
3917
|
+
}
|
|
3685
3918
|
if (prop === 'extend')
|
|
3686
|
-
return (childSeed) => scopedStore(scope, childSeed, kind,
|
|
3919
|
+
return (childSeed) => scopedStore(scope, childSeed, kind, options);
|
|
3687
3920
|
if (prop === 'asReadonlyStore')
|
|
3688
|
-
return () => toStore(computed(() => ({ ...parent(), ...local() })),
|
|
3921
|
+
return () => toStore(computed(() => ({ ...parent(), ...local() })), options);
|
|
3689
3922
|
if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
|
|
3690
3923
|
return target[prop];
|
|
3691
3924
|
// Route by consulting both signals: local first, then parent, else local (new → local).
|
|
@@ -3712,19 +3945,53 @@ function scopedStore(parent, seed, kind, injector) {
|
|
|
3712
3945
|
});
|
|
3713
3946
|
return scope;
|
|
3714
3947
|
}
|
|
3948
|
+
/** @internal Reads a store's writability brand, falling back to signal inspection if unbranded. */
|
|
3949
|
+
function storeKind(s) {
|
|
3950
|
+
return (s[STORE_KIND] ??
|
|
3951
|
+
(isWritableSignal(s) ? (isMutable(s) ? 'mutable' : 'writable') : 'readonly'));
|
|
3952
|
+
}
|
|
3953
|
+
/**
|
|
3954
|
+
* Extends a store with extra keys via a scoped overlay, returning a new store that reads through
|
|
3955
|
+
* to the parent for inherited keys (shared identity + two-way) while holding the new keys locally.
|
|
3956
|
+
*
|
|
3957
|
+
* The typesafe successor to the deprecated `store.extend(...)` method — moving it off the proxy
|
|
3958
|
+
* frees the `extend` key for use as a normal record key. Writability (readonly/writable/mutable)
|
|
3959
|
+
* is inherited from `store`.
|
|
3960
|
+
*
|
|
3961
|
+
* @example
|
|
3962
|
+
* const base = store({ count: 0 });
|
|
3963
|
+
* const scoped = extendStore(base, { label: 'live' });
|
|
3964
|
+
* scoped.count.set(1); // writes through to base
|
|
3965
|
+
* scoped.label.set('x'); // stays local
|
|
3966
|
+
*/
|
|
3967
|
+
function extendStore(store, source, options) {
|
|
3968
|
+
const opt = {
|
|
3969
|
+
...store[STORE_SHARED_OPTIONS],
|
|
3970
|
+
...options,
|
|
3971
|
+
};
|
|
3972
|
+
return scopedStore(store, source, storeKind(store), opt);
|
|
3973
|
+
}
|
|
3715
3974
|
/**
|
|
3716
3975
|
* Creates a WritableSignalStore from a value.
|
|
3717
3976
|
* @see {@link toStore}
|
|
3718
3977
|
*/
|
|
3719
3978
|
function store(value, opt) {
|
|
3720
|
-
return toStore(signal(value, opt),
|
|
3979
|
+
return toStore(signal(value, opt), {
|
|
3980
|
+
vivify: false,
|
|
3981
|
+
noUnionLeaves: false,
|
|
3982
|
+
...opt,
|
|
3983
|
+
});
|
|
3721
3984
|
}
|
|
3722
3985
|
/**
|
|
3723
3986
|
* Creates a MutableSignalStore from a value.
|
|
3724
3987
|
* @see {@link toStore}
|
|
3725
3988
|
*/
|
|
3726
3989
|
function mutableStore(value, opt) {
|
|
3727
|
-
return toStore(mutable(value, opt),
|
|
3990
|
+
return toStore(mutable(value, opt), {
|
|
3991
|
+
vivify: false,
|
|
3992
|
+
noUnionLeaves: false,
|
|
3993
|
+
...opt,
|
|
3994
|
+
});
|
|
3728
3995
|
}
|
|
3729
3996
|
|
|
3730
3997
|
function isPlainRecord(value) {
|
|
@@ -3786,7 +4053,13 @@ function forkStore(base, opt) {
|
|
|
3786
4053
|
const merge = reconcile;
|
|
3787
4054
|
const staged = linkedSignal({ ...(ngDevMode ? { debugName: "staged" } : /* istanbul ignore next */ {}), source: () => base(),
|
|
3788
4055
|
computation: (theirs, prev) => prev === undefined ? theirs : merge(prev.source, prev.value, theirs) });
|
|
3789
|
-
|
|
4056
|
+
// Inherit the base's shared options (injector, vivify, noUnionLeaves + the
|
|
4057
|
+
// proxy cache/registry), same as extendStore — a fork should vivify like its
|
|
4058
|
+
// base and share its injector-scoped cache. `opt` overrides (advanced use).
|
|
4059
|
+
const store = toStore(staged, {
|
|
4060
|
+
...base[STORE_SHARED_OPTIONS],
|
|
4061
|
+
...opt,
|
|
4062
|
+
});
|
|
3790
4063
|
return {
|
|
3791
4064
|
store,
|
|
3792
4065
|
commit: () => base.set(untracked(staged)),
|
|
@@ -3794,6 +4067,26 @@ function forkStore(base, opt) {
|
|
|
3794
4067
|
};
|
|
3795
4068
|
}
|
|
3796
4069
|
|
|
4070
|
+
/**
|
|
4071
|
+
* @internal The plain-`effect` sibling of the public {@link pausableEffect} (which is built on
|
|
4072
|
+
* `nestedEffect`). For infra utilities that own a single top-level effect/subscription and don't
|
|
4073
|
+
* need frame/nesting semantics. Opt-in (default off): with no `pause` (call site or
|
|
4074
|
+
* `providePausableOptions` default) it returns a bare `effect` (zero overhead, byte-identical to
|
|
4075
|
+
* today); otherwise it gates the body on the resolved predicate — read FIRST so the dependency set
|
|
4076
|
+
* collapses to just the predicate while paused, re-tracking on resume. Deliberately NOT re-exported
|
|
4077
|
+
* from the public barrel.
|
|
4078
|
+
*/
|
|
4079
|
+
function pausablePureEffect(effectFn, options) {
|
|
4080
|
+
const paused = resolvePause(options, false);
|
|
4081
|
+
if (!paused)
|
|
4082
|
+
return effect(effectFn, options);
|
|
4083
|
+
return effect((registerCleanup) => {
|
|
4084
|
+
if (paused())
|
|
4085
|
+
return;
|
|
4086
|
+
effectFn(registerCleanup);
|
|
4087
|
+
}, options);
|
|
4088
|
+
}
|
|
4089
|
+
|
|
3797
4090
|
// Internal dummy store for server-side rendering
|
|
3798
4091
|
const noopStore = {
|
|
3799
4092
|
getItem: () => null,
|
|
@@ -3855,8 +4148,9 @@ const noopStore = {
|
|
|
3855
4148
|
* }
|
|
3856
4149
|
* ```
|
|
3857
4150
|
*/
|
|
3858
|
-
function stored(fallback, { key, store: providedStore, serialize = JSON.stringify, deserialize = JSON.parse, syncTabs = false, equal = Object.is, onKeyChange = 'load', cleanupOldKey = false, validate = () => true, ...rest }) {
|
|
3859
|
-
const
|
|
4151
|
+
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 }) {
|
|
4152
|
+
const injector = providedInjector ?? inject(Injector);
|
|
4153
|
+
const isServer = isPlatformServer(injector.get(PLATFORM_ID));
|
|
3860
4154
|
const fallbackStore = isServer ? noopStore : localStorage;
|
|
3861
4155
|
const store = providedStore ?? fallbackStore;
|
|
3862
4156
|
const keySig = typeof key === 'string'
|
|
@@ -3864,8 +4158,6 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
3864
4158
|
: isSignal(key)
|
|
3865
4159
|
? key
|
|
3866
4160
|
: computed(key);
|
|
3867
|
-
// "no stored value" marker — distinct from `null`/`undefined`, so a nullable `T` can
|
|
3868
|
-
// round-trip a legitimate `null` through `set` instead of it acting like `clear()`
|
|
3869
4161
|
const EMPTY = Symbol();
|
|
3870
4162
|
const getValue = (key) => {
|
|
3871
4163
|
const found = store.getItem(key);
|
|
@@ -3910,7 +4202,7 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
3910
4202
|
} });
|
|
3911
4203
|
let prevKey = initialKey;
|
|
3912
4204
|
if (onKeyChange === 'store') {
|
|
3913
|
-
|
|
4205
|
+
pausablePureEffect(() => {
|
|
3914
4206
|
const k = keySig();
|
|
3915
4207
|
storeValue(k, internal());
|
|
3916
4208
|
if (prevKey !== k) {
|
|
@@ -3918,10 +4210,10 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
3918
4210
|
store.removeItem(prevKey);
|
|
3919
4211
|
prevKey = k;
|
|
3920
4212
|
}
|
|
3921
|
-
});
|
|
4213
|
+
}, { injector, pause });
|
|
3922
4214
|
}
|
|
3923
4215
|
else {
|
|
3924
|
-
|
|
4216
|
+
pausablePureEffect(() => {
|
|
3925
4217
|
const k = keySig();
|
|
3926
4218
|
const internalValue = internal();
|
|
3927
4219
|
if (k === prevKey) {
|
|
@@ -3934,14 +4226,11 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
3934
4226
|
prevKey = k;
|
|
3935
4227
|
internal.set(value); // load new value
|
|
3936
4228
|
}
|
|
3937
|
-
});
|
|
4229
|
+
}, { injector, pause });
|
|
3938
4230
|
}
|
|
3939
4231
|
if (syncTabs && !isServer) {
|
|
3940
|
-
const destroyRef =
|
|
4232
|
+
const destroyRef = injector.get(DestroyRef);
|
|
3941
4233
|
const sync = (e) => {
|
|
3942
|
-
// `storage` events only describe Web Storage — ignore events for a different
|
|
3943
|
-
// storage area (or any event when a custom adapter is configured), otherwise an
|
|
3944
|
-
// unrelated localStorage write with the same key string corrupts our state
|
|
3945
4234
|
if (e.storageArea !== store)
|
|
3946
4235
|
return;
|
|
3947
4236
|
if (e.key !== untracked(keySig))
|
|
@@ -4070,29 +4359,26 @@ function generateDeterministicID() {
|
|
|
4070
4359
|
*
|
|
4071
4360
|
*/
|
|
4072
4361
|
function tabSync(sig, opt) {
|
|
4073
|
-
|
|
4362
|
+
const optObj = typeof opt === 'object' ? opt : undefined;
|
|
4363
|
+
const injector = optObj?.injector ?? inject(Injector);
|
|
4364
|
+
if (isPlatformServer(injector.get(PLATFORM_ID)))
|
|
4074
4365
|
return sig;
|
|
4075
4366
|
const id = typeof opt === 'string' ? opt : (opt?.id ?? generateDeterministicID());
|
|
4076
|
-
const bus =
|
|
4077
|
-
// The last value applied from a remote tab. The outbound effect skips (exactly) the run
|
|
4078
|
-
// caused by that write — without this, an inbound object (a fresh structured clone, so
|
|
4079
|
-
// never reference-equal) would be re-posted, and two tabs would ping-pong forever.
|
|
4367
|
+
const bus = injector.get(MessageBus);
|
|
4080
4368
|
const NONE = Symbol();
|
|
4081
4369
|
let received = NONE;
|
|
4082
4370
|
const { unsub, post } = bus.subscribe(id, (next) => {
|
|
4083
4371
|
const before = untracked(sig);
|
|
4084
4372
|
received = next;
|
|
4085
4373
|
sig.set(next);
|
|
4086
|
-
// Equality-suppressed write (e.g. an identical primitive): no effect run will follow,
|
|
4087
|
-
// so clear the marker — it must not swallow a later, genuinely local change.
|
|
4088
4374
|
if (untracked(sig) === before)
|
|
4089
4375
|
received = NONE;
|
|
4090
4376
|
});
|
|
4091
|
-
let
|
|
4377
|
+
let firstDone = false;
|
|
4092
4378
|
const effectRef = effect(() => {
|
|
4093
4379
|
const val = sig();
|
|
4094
|
-
if (!
|
|
4095
|
-
|
|
4380
|
+
if (!firstDone) {
|
|
4381
|
+
firstDone = true;
|
|
4096
4382
|
return;
|
|
4097
4383
|
}
|
|
4098
4384
|
if (val === received) {
|
|
@@ -4101,8 +4387,8 @@ function tabSync(sig, opt) {
|
|
|
4101
4387
|
}
|
|
4102
4388
|
received = NONE;
|
|
4103
4389
|
post(val);
|
|
4104
|
-
}, ...(ngDevMode ?
|
|
4105
|
-
|
|
4390
|
+
}, { ...(ngDevMode ? { debugName: "effectRef" } : /* istanbul ignore next */ {}), injector });
|
|
4391
|
+
injector.get(DestroyRef).onDestroy(() => {
|
|
4106
4392
|
effectRef.destroy();
|
|
4107
4393
|
unsub();
|
|
4108
4394
|
});
|
|
@@ -4306,5 +4592,5 @@ function withHistory(sourceOrValue, opt) {
|
|
|
4306
4592
|
* Generated bundle index. Do not edit.
|
|
4307
4593
|
*/
|
|
4308
4594
|
|
|
4309
|
-
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 };
|
|
4595
|
+
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 };
|
|
4310
4596
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|