@mmstack/primitives 20.7.1 → 20.8.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 +691 -405
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/index.d.ts +339 -135
- 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
|
|
@@ -319,24 +251,23 @@ function providePaused(source) {
|
|
|
319
251
|
}
|
|
320
252
|
|
|
321
253
|
/**
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
*
|
|
329
|
-
*
|
|
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.
|
|
330
263
|
*
|
|
331
|
-
*
|
|
264
|
+
* @example
|
|
265
|
+
* // Make everything that can pause honour the ambient Activity boundary by default:
|
|
266
|
+
* providePausableOptions({ pause: true })
|
|
332
267
|
*/
|
|
333
|
-
function
|
|
334
|
-
return
|
|
335
|
-
source: () => ({ t: target(), ready: ready() }),
|
|
336
|
-
computation: (curr, prev) => (prev === undefined || curr.ready ? curr.t : prev.value),
|
|
337
|
-
});
|
|
268
|
+
function providePausableOptions(opt) {
|
|
269
|
+
return { provide: PAUSABLE_OPTIONS, useValue: opt };
|
|
338
270
|
}
|
|
339
|
-
|
|
340
271
|
/**
|
|
341
272
|
* Resolve a {@link PauseOption} into a pause predicate, or `null` meaning "do not pause".
|
|
342
273
|
* `null` tells the caller to return the bare primitive — no wrapper is created.
|
|
@@ -351,11 +282,7 @@ function holdUntilReady(target, ready) {
|
|
|
351
282
|
*
|
|
352
283
|
* Encapsulating this here keeps every pausable primitive's branching identical and in one place.
|
|
353
284
|
*/
|
|
354
|
-
function resolvePause(opt) {
|
|
355
|
-
const explicit = opt?.pause; // distinguish explicit `true` from the omitted default
|
|
356
|
-
const pause = explicit ?? true; // explicit pausable* calls default to pausing
|
|
357
|
-
if (pause === false)
|
|
358
|
-
return null;
|
|
285
|
+
function resolvePause(opt, defaultPause = true) {
|
|
359
286
|
const run = (fn) => opt?.injector ? runInInjectionContext(opt.injector, fn) : fn();
|
|
360
287
|
// `inject` requires an injection context even with `optional: true`. A bare
|
|
361
288
|
// `pausableSignal(0)` (documented as "like `signal`") must degrade to the unwrapped
|
|
@@ -368,6 +295,12 @@ function resolvePause(opt) {
|
|
|
368
295
|
return fallback;
|
|
369
296
|
}
|
|
370
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;
|
|
371
304
|
const onServer = () => typeof pause === 'function' && !opt?.injector
|
|
372
305
|
? typeof globalThis.window === 'undefined'
|
|
373
306
|
: tryRun(() => isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser'), typeof globalThis.window === 'undefined');
|
|
@@ -377,7 +310,7 @@ function resolvePause(opt) {
|
|
|
377
310
|
return null;
|
|
378
311
|
const paused = tryRun(() => inject(PAUSED_CONTEXT, { optional: true }), null);
|
|
379
312
|
if (!paused) {
|
|
380
|
-
if (
|
|
313
|
+
if (opt?.pause === true && isDevMode())
|
|
381
314
|
console.warn('[pausable] `pause: true` but no PAUSED_CONTEXT in scope — not pausing. Provide one via an ' +
|
|
382
315
|
'Activity boundary (`MmActivity` / `providePaused`), or pass a predicate / `pause: false`.');
|
|
383
316
|
return null;
|
|
@@ -453,6 +386,96 @@ function pausableComputed(computation, options) {
|
|
|
453
386
|
return ls.asReadonly();
|
|
454
387
|
}
|
|
455
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
|
+
|
|
456
479
|
const { is } = Object;
|
|
457
480
|
function mutable(initial, opt) {
|
|
458
481
|
const baseEqual = opt?.equal ?? is;
|
|
@@ -703,6 +726,12 @@ function injectStartTransition() {
|
|
|
703
726
|
*
|
|
704
727
|
* `type` selects what "not ready" means: `'value'` (default) suspends only until a first value lands
|
|
705
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)`.
|
|
706
735
|
*/
|
|
707
736
|
class SuspenseBoundaryBase {
|
|
708
737
|
scope = injectTransitionScope();
|
|
@@ -2642,11 +2671,19 @@ function throttle(source, opt) {
|
|
|
2642
2671
|
tick();
|
|
2643
2672
|
};
|
|
2644
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
|
+
};
|
|
2645
2681
|
const writable = toWritable(computed(() => {
|
|
2646
2682
|
trigger();
|
|
2647
2683
|
return untracked(source);
|
|
2648
2684
|
}, opt), set, update);
|
|
2649
2685
|
writable.original = source.asReadonly();
|
|
2686
|
+
writable.flush = flush;
|
|
2650
2687
|
return writable;
|
|
2651
2688
|
}
|
|
2652
2689
|
|
|
@@ -2870,6 +2907,213 @@ function createOrientation(debugName) {
|
|
|
2870
2907
|
return state.asReadonly();
|
|
2871
2908
|
}
|
|
2872
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
|
+
pointerType: '',
|
|
2919
|
+
origin: null,
|
|
2920
|
+
};
|
|
2921
|
+
function stateEqual(a, b) {
|
|
2922
|
+
return (a.active === b.active &&
|
|
2923
|
+
a.pointerId === b.pointerId &&
|
|
2924
|
+
a.current.x === b.current.x &&
|
|
2925
|
+
a.current.y === b.current.y &&
|
|
2926
|
+
a.button === b.button &&
|
|
2927
|
+
a.pointerType === b.pointerType &&
|
|
2928
|
+
a.origin === b.origin &&
|
|
2929
|
+
a.modifiers.shift === b.modifiers.shift &&
|
|
2930
|
+
a.modifiers.alt === b.modifiers.alt &&
|
|
2931
|
+
a.modifiers.ctrl === b.modifiers.ctrl &&
|
|
2932
|
+
a.modifiers.meta === b.modifiers.meta);
|
|
2933
|
+
}
|
|
2934
|
+
/**
|
|
2935
|
+
* Tracks a pointer *gesture* (pointerdown → capture → move → up) as a signal —
|
|
2936
|
+
* the foundation for pointer-based drag/move/resize/marquee on a canvas. Unlike
|
|
2937
|
+
* native HTML5 drag, pointer events fire continuously and coordinates are
|
|
2938
|
+
* reliable. SSR-safe; cleans up its listeners automatically.
|
|
2939
|
+
*
|
|
2940
|
+
* @example
|
|
2941
|
+
* ```ts
|
|
2942
|
+
* const drag = pointerDrag({ activationThreshold: 4 });
|
|
2943
|
+
* const position = computed(() => {
|
|
2944
|
+
* const d = drag();
|
|
2945
|
+
* return d.active ? { x: base.x + d.delta.x, y: base.y + d.delta.y } : base;
|
|
2946
|
+
* });
|
|
2947
|
+
* ```
|
|
2948
|
+
*/
|
|
2949
|
+
function pointerDrag(opt) {
|
|
2950
|
+
return runInSensorContext(opt?.injector, () => createPointerDrag(opt));
|
|
2951
|
+
}
|
|
2952
|
+
function createPointerDrag(opt) {
|
|
2953
|
+
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
2954
|
+
const base = computed(() => IDLE, {
|
|
2955
|
+
debugName: opt?.debugName ?? 'pointerDrag',
|
|
2956
|
+
});
|
|
2957
|
+
base.unthrottled = base;
|
|
2958
|
+
base.cancel = () => undefined;
|
|
2959
|
+
return base;
|
|
2960
|
+
}
|
|
2961
|
+
const hostRef = inject((ElementRef), { optional: true });
|
|
2962
|
+
const { target = hostRef?.nativeElement, coordinateSpace = 'client', activationThreshold = 3, throttle = 16, handleSelector, buttons = [0], stopPropagation = false, debugName = 'pointerDrag', } = opt ?? {};
|
|
2963
|
+
const resolve = (t) => {
|
|
2964
|
+
if (!t)
|
|
2965
|
+
return null;
|
|
2966
|
+
return t instanceof ElementRef ? t.nativeElement : t;
|
|
2967
|
+
};
|
|
2968
|
+
if (!isSignal(target) && !resolve(target)) {
|
|
2969
|
+
if (isDevMode())
|
|
2970
|
+
console.warn('pointerDrag: no target element (host ElementRef missing).');
|
|
2971
|
+
const base = computed(() => IDLE, { debugName });
|
|
2972
|
+
base.unthrottled = base;
|
|
2973
|
+
base.cancel = () => undefined;
|
|
2974
|
+
return base;
|
|
2975
|
+
}
|
|
2976
|
+
const state = throttled(IDLE, {
|
|
2977
|
+
ms: throttle,
|
|
2978
|
+
leading: true,
|
|
2979
|
+
trailing: true,
|
|
2980
|
+
equal: stateEqual,
|
|
2981
|
+
debugName,
|
|
2982
|
+
});
|
|
2983
|
+
const threshold2 = activationThreshold * activationThreshold;
|
|
2984
|
+
let startPoint = { x: 0, y: 0 };
|
|
2985
|
+
let activePointerId = null;
|
|
2986
|
+
let activeButton = -1;
|
|
2987
|
+
let activePointerType = '';
|
|
2988
|
+
let activeOrigin = null;
|
|
2989
|
+
let activated = false;
|
|
2990
|
+
let gesture = null;
|
|
2991
|
+
const coord = (e) => coordinateSpace === 'page'
|
|
2992
|
+
? { x: e.pageX, y: e.pageY }
|
|
2993
|
+
: { x: e.clientX, y: e.clientY };
|
|
2994
|
+
const mods = (e) => ({
|
|
2995
|
+
shift: e.shiftKey,
|
|
2996
|
+
alt: e.altKey,
|
|
2997
|
+
ctrl: e.ctrlKey,
|
|
2998
|
+
meta: e.metaKey,
|
|
2999
|
+
});
|
|
3000
|
+
const end = () => {
|
|
3001
|
+
gesture?.abort();
|
|
3002
|
+
gesture = null;
|
|
3003
|
+
activePointerId = null;
|
|
3004
|
+
activeButton = -1;
|
|
3005
|
+
activePointerType = '';
|
|
3006
|
+
activeOrigin = null;
|
|
3007
|
+
activated = false;
|
|
3008
|
+
state.set(IDLE);
|
|
3009
|
+
state.flush(); // terminal transition: reflect IDLE now, not on the trailing edge
|
|
3010
|
+
};
|
|
3011
|
+
const onMove = (e) => {
|
|
3012
|
+
if (e.pointerId !== activePointerId)
|
|
3013
|
+
return;
|
|
3014
|
+
const current = coord(e);
|
|
3015
|
+
const delta = { x: current.x - startPoint.x, y: current.y - startPoint.y };
|
|
3016
|
+
if (!activated && delta.x * delta.x + delta.y * delta.y >= threshold2) {
|
|
3017
|
+
activated = true; // squared compare — no sqrt on the pre-activation path
|
|
3018
|
+
}
|
|
3019
|
+
state.set({
|
|
3020
|
+
active: activated,
|
|
3021
|
+
start: startPoint,
|
|
3022
|
+
current,
|
|
3023
|
+
delta,
|
|
3024
|
+
pointerId: activePointerId,
|
|
3025
|
+
modifiers: mods(e),
|
|
3026
|
+
button: activeButton, // pointermove button is -1; keep the down-button
|
|
3027
|
+
pointerType: activePointerType,
|
|
3028
|
+
origin: activeOrigin,
|
|
3029
|
+
});
|
|
3030
|
+
};
|
|
3031
|
+
const onUp = (e) => {
|
|
3032
|
+
if (e.pointerId === activePointerId)
|
|
3033
|
+
end();
|
|
3034
|
+
};
|
|
3035
|
+
const onCancel = (e) => {
|
|
3036
|
+
if (e.pointerId === activePointerId)
|
|
3037
|
+
end();
|
|
3038
|
+
};
|
|
3039
|
+
const onKey = (e) => {
|
|
3040
|
+
if (e.key === 'Escape' && activePointerId !== null)
|
|
3041
|
+
end();
|
|
3042
|
+
};
|
|
3043
|
+
const onDown = (el) => (e) => {
|
|
3044
|
+
if (activePointerId !== null)
|
|
3045
|
+
return;
|
|
3046
|
+
if (!buttons.includes(e.button))
|
|
3047
|
+
return;
|
|
3048
|
+
const matched = handleSelector
|
|
3049
|
+
? e.target?.closest?.(handleSelector)
|
|
3050
|
+
: el;
|
|
3051
|
+
if (!matched)
|
|
3052
|
+
return; // handleSelector set but pointerdown landed outside a handle
|
|
3053
|
+
if (stopPropagation)
|
|
3054
|
+
e.stopPropagation(); // claim it: an outer sensor won't also start
|
|
3055
|
+
activePointerId = e.pointerId;
|
|
3056
|
+
activeButton = e.button;
|
|
3057
|
+
activePointerType = e.pointerType;
|
|
3058
|
+
activeOrigin = matched;
|
|
3059
|
+
activated = false;
|
|
3060
|
+
startPoint = coord(e);
|
|
3061
|
+
try {
|
|
3062
|
+
el.setPointerCapture(e.pointerId);
|
|
3063
|
+
}
|
|
3064
|
+
catch {
|
|
3065
|
+
// capture unsupported (older browsers / test env) — listeners still work
|
|
3066
|
+
}
|
|
3067
|
+
gesture = new AbortController();
|
|
3068
|
+
const signal = gesture.signal;
|
|
3069
|
+
el.addEventListener('pointermove', onMove, { signal });
|
|
3070
|
+
el.addEventListener('pointerup', onUp, { signal });
|
|
3071
|
+
el.addEventListener('pointercancel', onCancel, { signal });
|
|
3072
|
+
el.addEventListener('lostpointercapture', onCancel, {
|
|
3073
|
+
signal,
|
|
3074
|
+
});
|
|
3075
|
+
window.addEventListener('keydown', onKey, { signal });
|
|
3076
|
+
state.set({
|
|
3077
|
+
active: false,
|
|
3078
|
+
start: startPoint,
|
|
3079
|
+
current: startPoint,
|
|
3080
|
+
delta: { x: 0, y: 0 },
|
|
3081
|
+
pointerId: e.pointerId,
|
|
3082
|
+
modifiers: mods(e),
|
|
3083
|
+
button: e.button,
|
|
3084
|
+
pointerType: activePointerType,
|
|
3085
|
+
origin: activeOrigin,
|
|
3086
|
+
});
|
|
3087
|
+
};
|
|
3088
|
+
const attach = (el) => {
|
|
3089
|
+
const controller = new AbortController();
|
|
3090
|
+
el.addEventListener('pointerdown', onDown(el), {
|
|
3091
|
+
signal: controller.signal,
|
|
3092
|
+
});
|
|
3093
|
+
return () => {
|
|
3094
|
+
controller.abort();
|
|
3095
|
+
end();
|
|
3096
|
+
};
|
|
3097
|
+
};
|
|
3098
|
+
if (isSignal(target)) {
|
|
3099
|
+
effect((cleanup) => {
|
|
3100
|
+
const el = resolve(target());
|
|
3101
|
+
if (!el)
|
|
3102
|
+
return;
|
|
3103
|
+
cleanup(attach(el));
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
else {
|
|
3107
|
+
const el = resolve(target);
|
|
3108
|
+
if (el)
|
|
3109
|
+
inject(DestroyRef).onDestroy(attach(el));
|
|
3110
|
+
}
|
|
3111
|
+
const base = state.asReadonly();
|
|
3112
|
+
base.unthrottled = state.original;
|
|
3113
|
+
base.cancel = end;
|
|
3114
|
+
return base;
|
|
3115
|
+
}
|
|
3116
|
+
|
|
2873
3117
|
/**
|
|
2874
3118
|
* Creates a read-only signal that tracks the page's visibility state.
|
|
2875
3119
|
*
|
|
@@ -3096,6 +3340,8 @@ function sensor(type, options) {
|
|
|
3096
3340
|
switch (type) {
|
|
3097
3341
|
case 'mousePosition':
|
|
3098
3342
|
return mousePosition(opts);
|
|
3343
|
+
case 'pointerDrag':
|
|
3344
|
+
return pointerDrag(opts);
|
|
3099
3345
|
case 'networkStatus':
|
|
3100
3346
|
return networkStatus(opts);
|
|
3101
3347
|
case 'pageVisibility':
|
|
@@ -3213,13 +3459,55 @@ function signalFromEvent(target, eventName, initial, projectOrOpt, maybeOpt) {
|
|
|
3213
3459
|
return untracked(() => state.asReadonly());
|
|
3214
3460
|
}
|
|
3215
3461
|
|
|
3462
|
+
const IS_STORE = Symbol('@mmstack/primitives::store/IS_STORE');
|
|
3463
|
+
const SCOPE_PARENT = Symbol('@mmstack/primitives::store/SCOPE_PARENT');
|
|
3464
|
+
const STORE_SHARED_GLOBALS = Symbol('@mmstack/primitives::store/STORE_SHARED_GLOBALS');
|
|
3465
|
+
const STORE_SHARED_OPTIONS = Symbol('@mmstack/primitives::store/STORE_SHARED_OPTIONS');
|
|
3216
3466
|
/**
|
|
3217
|
-
*
|
|
3218
|
-
*
|
|
3219
|
-
*
|
|
3467
|
+
* @internal Brand carrying a store's writability ('mutable' | 'writable' | 'readonly'), stamped
|
|
3468
|
+
* on every store proxy. Read by `extendStore` instead of re-deriving via `isWritableSignal`,
|
|
3469
|
+
* which would mis-classify a readonly scoped store (its backing `toWritable` still has a `set`).
|
|
3220
3470
|
*/
|
|
3221
|
-
const
|
|
3222
|
-
|
|
3471
|
+
const STORE_KIND = Symbol('@mmstack/primitives::store/STORE_KIND');
|
|
3472
|
+
const SIGNAL_FN_PROP = new Set([
|
|
3473
|
+
'set',
|
|
3474
|
+
'update',
|
|
3475
|
+
'mutate',
|
|
3476
|
+
'inline',
|
|
3477
|
+
'asReadonly',
|
|
3478
|
+
]);
|
|
3479
|
+
const PROXY_CACHE_TOKEN = new InjectionToken('@mmstack/primitives:store-proxy-cache', {
|
|
3480
|
+
providedIn: 'root',
|
|
3481
|
+
factory: () => new WeakMap(),
|
|
3482
|
+
});
|
|
3483
|
+
const PROXY_CLEANUP_TOKEN = new InjectionToken('@mmstack/primitives:store-proxy-cleanup', {
|
|
3484
|
+
providedIn: 'root',
|
|
3485
|
+
factory: () => {
|
|
3486
|
+
const cache = inject(PROXY_CACHE_TOKEN);
|
|
3487
|
+
return new FinalizationRegistry(({ target, prop }) => {
|
|
3488
|
+
const store = cache.get(target);
|
|
3489
|
+
if (store)
|
|
3490
|
+
store.delete(prop);
|
|
3491
|
+
});
|
|
3492
|
+
},
|
|
3493
|
+
});
|
|
3494
|
+
/**
|
|
3495
|
+
* @internal
|
|
3496
|
+
* Validates whether a value is a Signal Store.
|
|
3497
|
+
*/
|
|
3498
|
+
function isStore(value) {
|
|
3499
|
+
return (typeof value === 'function' &&
|
|
3500
|
+
value !== null &&
|
|
3501
|
+
value[IS_STORE] === true);
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3504
|
+
/**
|
|
3505
|
+
* Runtime marker + compile-time brand for an opaque value. A `const`-declared `Symbol`
|
|
3506
|
+
* has a `unique symbol` type, so the same symbol serves as both the property key written
|
|
3507
|
+
* by {@link opaque} and the type-level brand carried by {@link Opaque}.
|
|
3508
|
+
*/
|
|
3509
|
+
const OPAQUE = Symbol('@mmstack/primitives::store/OPAQUE');
|
|
3510
|
+
/**
|
|
3223
3511
|
* Marks a plain object as opaque so {@link store} treats it as an indivisible leaf
|
|
3224
3512
|
* (returned whole, never deep-proxied) — the same way it treats a `Date` or `RegExp`.
|
|
3225
3513
|
* The marker is a non-enumerable symbol, so it never appears in spreads or iteration.
|
|
@@ -3254,12 +3542,13 @@ function isOpaque(value) {
|
|
|
3254
3542
|
value !== null &&
|
|
3255
3543
|
value[OPAQUE] === true);
|
|
3256
3544
|
}
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3545
|
+
|
|
3546
|
+
function isRecord(value) {
|
|
3547
|
+
if (value === null || typeof value !== 'object' || isOpaque(value))
|
|
3548
|
+
return false;
|
|
3549
|
+
const proto = Object.getPrototypeOf(value);
|
|
3550
|
+
return proto === Object.prototype || proto === null;
|
|
3551
|
+
}
|
|
3263
3552
|
/**
|
|
3264
3553
|
* @internal Whether a value is a terminal leaf: a concrete non-record/non-array value always is;
|
|
3265
3554
|
* `null`/`undefined` is a leaf only when vivification is disabled (with vivify on it can still
|
|
@@ -3272,6 +3561,65 @@ function isLeafValue(value, vivifyEnabled) {
|
|
|
3272
3561
|
return true; // opaque always wins — even arrays
|
|
3273
3562
|
return !Array.isArray(value) && !isRecord(value);
|
|
3274
3563
|
}
|
|
3564
|
+
/**
|
|
3565
|
+
* @internal
|
|
3566
|
+
* Resolves the vivify shape for a node from its current value: a present record/array is a
|
|
3567
|
+
* certainty we keep (cached in the derivation, so it survives the value being nulled); an
|
|
3568
|
+
* unknown value (`null`/`undefined`) defers to the caller's option. Off stays off.
|
|
3569
|
+
*/
|
|
3570
|
+
function resolveVivify(sample, option) {
|
|
3571
|
+
if (!option)
|
|
3572
|
+
return false;
|
|
3573
|
+
if (Array.isArray(sample))
|
|
3574
|
+
return 'array';
|
|
3575
|
+
if (isRecord(sample))
|
|
3576
|
+
return 'object';
|
|
3577
|
+
return 'auto';
|
|
3578
|
+
}
|
|
3579
|
+
function hasOwnKey(value, key) {
|
|
3580
|
+
return value != null && Object.hasOwn(value, key);
|
|
3581
|
+
}
|
|
3582
|
+
/**
|
|
3583
|
+
* @internal
|
|
3584
|
+
* Builds the `onChange` for the fallback (non-record container) derivation branch. For an
|
|
3585
|
+
* immutable source the container is copied before the write — returning the same mutated
|
|
3586
|
+
* reference would let the source's equality cut propagation (leaving child signals permanently
|
|
3587
|
+
* stale) and alias the caller's original object, breaking the structural-sharing contract
|
|
3588
|
+
* `forkStore` relies on. For a mutable source the write goes through `mutate`, so the chain's
|
|
3589
|
+
* force-notify engages (plain `update` with the same reference would never notify).
|
|
3590
|
+
*/
|
|
3591
|
+
function createFallbackOnChange(target, prop, vivifyFn, isMutableSource) {
|
|
3592
|
+
const write = (newValue) => (v) => {
|
|
3593
|
+
const container = vivifyFn(v, prop);
|
|
3594
|
+
if (container === null || container === undefined)
|
|
3595
|
+
return container;
|
|
3596
|
+
const next = isMutableSource
|
|
3597
|
+
? container
|
|
3598
|
+
: Array.isArray(container)
|
|
3599
|
+
? container.slice()
|
|
3600
|
+
: isRecord(container)
|
|
3601
|
+
? { ...container }
|
|
3602
|
+
: container; // non-plain leaf (Date/class instance): legacy in-place attempt
|
|
3603
|
+
try {
|
|
3604
|
+
next[prop] = newValue;
|
|
3605
|
+
}
|
|
3606
|
+
catch (e) {
|
|
3607
|
+
if (isDevMode())
|
|
3608
|
+
console.error(`[store] Failed to set property "${String(prop)}"`, e);
|
|
3609
|
+
}
|
|
3610
|
+
return next;
|
|
3611
|
+
};
|
|
3612
|
+
return isMutableSource
|
|
3613
|
+
? (newValue) => target.mutate(write(newValue))
|
|
3614
|
+
: (newValue) => target.update(write(newValue));
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
/**
|
|
3618
|
+
* @internal Runtime brand carrying a store node's lazily-built leaf probe. Exported (like
|
|
3619
|
+
* {@link OPAQUE}) only so the `{ readonly [LEAF]: () => boolean }` brand on the store types is
|
|
3620
|
+
* nameable in the emitted declarations — not part of the supported surface; use {@link isLeaf}.
|
|
3621
|
+
*/
|
|
3622
|
+
const LEAF = Symbol('@mmstack/primitives::store/LEAF');
|
|
3275
3623
|
/**
|
|
3276
3624
|
* @internal Constant leaf probes for nodes whose leaf-ness is statically known, so the reactive
|
|
3277
3625
|
* `computed` can be skipped entirely.
|
|
@@ -3329,233 +3677,78 @@ function markAsLeaf(sig, value, vivifyEnabled, noUnionLeaves) {
|
|
|
3329
3677
|
function isLeaf(value) {
|
|
3330
3678
|
return isStore(value) && value[LEAF]?.() === true;
|
|
3331
3679
|
}
|
|
3332
|
-
|
|
3333
|
-
const SCOPE_PARENT = Symbol('@mmstack/primitives::store/SCOPE_PARENT');
|
|
3334
|
-
/**
|
|
3335
|
-
* @internal
|
|
3336
|
-
* Test-only handle on the proxy cache (deliberately NOT re-exported from the public barrel).
|
|
3337
|
-
* Maps a store's backing signal to its lazily-built child proxies, each held via a `WeakRef`.
|
|
3338
|
-
*/
|
|
3339
|
-
const PROXY_CACHE = new WeakMap();
|
|
3340
|
-
const SIGNAL_FN_PROP = new Set([
|
|
3341
|
-
'set',
|
|
3342
|
-
'update',
|
|
3343
|
-
'mutate',
|
|
3344
|
-
'inline',
|
|
3345
|
-
'asReadonly',
|
|
3346
|
-
]);
|
|
3680
|
+
|
|
3347
3681
|
/**
|
|
3348
|
-
* @internal
|
|
3349
|
-
*
|
|
3350
|
-
*
|
|
3682
|
+
* @internal Reads (or lazily builds + caches) the child node proxy for `prop` on `target`,
|
|
3683
|
+
* holding it via a `WeakRef` and registering it for finalizer-driven cache pruning. The cache
|
|
3684
|
+
* is keyed per backing signal, so child identity is stable across repeat reads.
|
|
3351
3685
|
*/
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
if (storeCache)
|
|
3686
|
+
function getCachedChild(target, prop, build, cache, cleanupRegistry) {
|
|
3687
|
+
let storeCache = cache.get(target);
|
|
3688
|
+
if (!storeCache) {
|
|
3689
|
+
storeCache = new Map();
|
|
3690
|
+
cache.set(target, storeCache);
|
|
3691
|
+
}
|
|
3692
|
+
const cachedRef = storeCache.get(prop);
|
|
3693
|
+
if (cachedRef) {
|
|
3694
|
+
const cached = cachedRef.deref();
|
|
3695
|
+
if (cached)
|
|
3696
|
+
return cached;
|
|
3355
3697
|
storeCache.delete(prop);
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
return
|
|
3363
|
-
value !== null &&
|
|
3364
|
-
value[IS_STORE] === true);
|
|
3365
|
-
}
|
|
3366
|
-
function isRecord(value) {
|
|
3367
|
-
if (value === null || typeof value !== 'object' || isOpaque(value))
|
|
3368
|
-
return false;
|
|
3369
|
-
const proto = Object.getPrototypeOf(value);
|
|
3370
|
-
return proto === Object.prototype || proto === null;
|
|
3371
|
-
}
|
|
3372
|
-
/**
|
|
3373
|
-
* @internal
|
|
3374
|
-
* Resolves the vivify shape for a node from its current value: a present record/array is a
|
|
3375
|
-
* certainty we keep (cached in the derivation, so it survives the value being nulled); an
|
|
3376
|
-
* unknown value (`null`/`undefined`) defers to the caller's option. Off stays off.
|
|
3377
|
-
*/
|
|
3378
|
-
function resolveVivify(sample, option) {
|
|
3379
|
-
if (!option)
|
|
3380
|
-
return false;
|
|
3381
|
-
if (Array.isArray(sample))
|
|
3382
|
-
return 'array';
|
|
3383
|
-
if (isRecord(sample))
|
|
3384
|
-
return 'object';
|
|
3385
|
-
return 'auto';
|
|
3386
|
-
}
|
|
3387
|
-
function hasOwnKey(value, key) {
|
|
3388
|
-
return value != null && Object.hasOwn(value, key);
|
|
3389
|
-
}
|
|
3390
|
-
/**
|
|
3391
|
-
* @internal
|
|
3392
|
-
* Builds the `onChange` for the fallback (non-record container) derivation branch. For an
|
|
3393
|
-
* immutable source the container is copied before the write — returning the same mutated
|
|
3394
|
-
* reference would let the source's equality cut propagation (leaving child signals permanently
|
|
3395
|
-
* stale) and alias the caller's original object, breaking the structural-sharing contract
|
|
3396
|
-
* `forkStore` relies on. For a mutable source the write goes through `mutate`, so the chain's
|
|
3397
|
-
* force-notify engages (plain `update` with the same reference would never notify).
|
|
3398
|
-
*/
|
|
3399
|
-
function createFallbackOnChange(target, prop, vivifyFn, isMutableSource) {
|
|
3400
|
-
const write = (newValue) => (v) => {
|
|
3401
|
-
const container = vivifyFn(v, prop);
|
|
3402
|
-
if (container === null || container === undefined)
|
|
3403
|
-
return container;
|
|
3404
|
-
const next = isMutableSource
|
|
3405
|
-
? container
|
|
3406
|
-
: Array.isArray(container)
|
|
3407
|
-
? container.slice()
|
|
3408
|
-
: isRecord(container)
|
|
3409
|
-
? { ...container }
|
|
3410
|
-
: container; // non-plain leaf (Date/class instance): legacy in-place attempt
|
|
3411
|
-
try {
|
|
3412
|
-
next[prop] = newValue;
|
|
3413
|
-
}
|
|
3414
|
-
catch (e) {
|
|
3415
|
-
if (isDevMode())
|
|
3416
|
-
console.error(`[store] Failed to set property "${String(prop)}"`, e);
|
|
3417
|
-
}
|
|
3418
|
-
return next;
|
|
3419
|
-
};
|
|
3420
|
-
return isMutableSource
|
|
3421
|
-
? (newValue) => target.mutate(write(newValue))
|
|
3422
|
-
: (newValue) => target.update(write(newValue));
|
|
3698
|
+
cleanupRegistry.unregister(cachedRef);
|
|
3699
|
+
}
|
|
3700
|
+
const proxy = build();
|
|
3701
|
+
const ref = new WeakRef(proxy);
|
|
3702
|
+
storeCache.set(prop, ref);
|
|
3703
|
+
cleanupRegistry.register(proxy, { target, prop }, ref);
|
|
3704
|
+
return proxy;
|
|
3423
3705
|
}
|
|
3424
3706
|
/**
|
|
3425
|
-
* @internal
|
|
3426
|
-
*
|
|
3707
|
+
* @internal Builds the derived child signal for `prop` and wraps it as an array/object substore.
|
|
3708
|
+
* A record parent reads the key directly; any other container goes through the fallback `from`/
|
|
3709
|
+
* `onChange` path. Shared verbatim by the array and object proxies — the only place a child node
|
|
3710
|
+
* is constructed.
|
|
3427
3711
|
*/
|
|
3428
|
-
function
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
const
|
|
3432
|
-
const
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
const v = untracked(source);
|
|
3452
|
-
if (!Array.isArray(v))
|
|
3453
|
-
return [];
|
|
3454
|
-
const len = v.length;
|
|
3455
|
-
const arr = new Array(len + 1);
|
|
3456
|
-
for (let i = 0; i < len; i++) {
|
|
3457
|
-
arr[i] = String(i);
|
|
3458
|
-
}
|
|
3459
|
-
arr[len] = 'length';
|
|
3460
|
-
return arr;
|
|
3461
|
-
},
|
|
3462
|
-
getPrototypeOf() {
|
|
3463
|
-
return Array.prototype;
|
|
3464
|
-
},
|
|
3465
|
-
getOwnPropertyDescriptor(_, prop) {
|
|
3466
|
-
const v = untracked(source);
|
|
3467
|
-
if (!Array.isArray(v))
|
|
3468
|
-
return;
|
|
3469
|
-
if (prop === 'length' ||
|
|
3470
|
-
(typeof prop === 'string' && !isNaN(+prop) && +prop < v.length)) {
|
|
3471
|
-
return {
|
|
3472
|
-
enumerable: true,
|
|
3473
|
-
configurable: true, // Required for proxies to dynamic targets
|
|
3474
|
-
};
|
|
3475
|
-
}
|
|
3476
|
-
return;
|
|
3477
|
-
},
|
|
3478
|
-
get(target, prop, receiver) {
|
|
3479
|
-
if (prop === IS_STORE)
|
|
3480
|
-
return true;
|
|
3481
|
-
if (prop === 'length')
|
|
3482
|
-
return lengthSignal;
|
|
3483
|
-
if (prop === Symbol.iterator) {
|
|
3484
|
-
return function* () {
|
|
3485
|
-
// read length reactively: a spread/for-of inside a computed/effect must re-run
|
|
3486
|
-
// when items are added or removed, not only when already-read elements change
|
|
3487
|
-
for (let i = 0; i < lengthSignal(); i++) {
|
|
3488
|
-
yield receiver[i];
|
|
3489
|
-
}
|
|
3490
|
-
};
|
|
3491
|
-
}
|
|
3492
|
-
if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
|
|
3493
|
-
return target[prop];
|
|
3494
|
-
if (isIndexProp(prop)) {
|
|
3495
|
-
const idx = +prop;
|
|
3496
|
-
let storeCache = PROXY_CACHE.get(target);
|
|
3497
|
-
if (!storeCache) {
|
|
3498
|
-
storeCache = new Map();
|
|
3499
|
-
PROXY_CACHE.set(target, storeCache);
|
|
3500
|
-
}
|
|
3501
|
-
const cachedRef = storeCache.get(idx);
|
|
3502
|
-
if (cachedRef) {
|
|
3503
|
-
const cached = cachedRef.deref();
|
|
3504
|
-
if (cached)
|
|
3505
|
-
return cached;
|
|
3506
|
-
storeCache.delete(idx);
|
|
3507
|
-
PROXY_CLEANUP.unregister(cachedRef);
|
|
3508
|
-
}
|
|
3509
|
-
const value = untracked(target);
|
|
3510
|
-
const valueIsArray = Array.isArray(value);
|
|
3511
|
-
const valueIsRecord = isRecord(value);
|
|
3512
|
-
const nodeVivify = resolveVivify(value, vivify);
|
|
3513
|
-
const vivifyFn = createVivify(nodeVivify);
|
|
3514
|
-
const equalFn = (valueIsRecord || valueIsArray) &&
|
|
3515
|
-
isMutableSource &&
|
|
3516
|
-
typeof value[idx] === 'object'
|
|
3517
|
-
? () => false
|
|
3518
|
-
: undefined;
|
|
3519
|
-
const computation = valueIsRecord
|
|
3520
|
-
? derived(target, idx, {
|
|
3521
|
-
equal: equalFn,
|
|
3522
|
-
vivify: nodeVivify,
|
|
3523
|
-
})
|
|
3524
|
-
: derived(target, {
|
|
3525
|
-
from: (v) => v?.[idx],
|
|
3526
|
-
onChange: createFallbackOnChange(target, idx, vivifyFn, isMutableSource),
|
|
3527
|
-
equal: equalFn,
|
|
3528
|
-
});
|
|
3529
|
-
const childSample = untracked(computation);
|
|
3530
|
-
const childVivify = resolveVivify(childSample, vivify);
|
|
3531
|
-
const proxy = Array.isArray(childSample) && !isOpaque(childSample)
|
|
3532
|
-
? toArrayStore(computation, injector, childVivify, noUnionLeaves)
|
|
3533
|
-
: toStore(computation, injector, childVivify, noUnionLeaves);
|
|
3534
|
-
markAsLeaf(proxy, computation, childVivify !== false, noUnionLeaves);
|
|
3535
|
-
const ref = new WeakRef(proxy);
|
|
3536
|
-
storeCache.set(idx, ref);
|
|
3537
|
-
PROXY_CLEANUP.register(proxy, { target, prop: idx }, ref);
|
|
3538
|
-
return proxy;
|
|
3539
|
-
}
|
|
3540
|
-
return Reflect.get(target, prop, receiver);
|
|
3541
|
-
},
|
|
3542
|
-
});
|
|
3712
|
+
function buildChildNode(target, prop, isMutableSource, options) {
|
|
3713
|
+
const value = untracked(target);
|
|
3714
|
+
const valueIsRecord = isRecord(value);
|
|
3715
|
+
const valueIsArray = Array.isArray(value);
|
|
3716
|
+
const nodeVivify = resolveVivify(value, options.vivify);
|
|
3717
|
+
const vivifyFn = createVivify(nodeVivify);
|
|
3718
|
+
const equalFn = (valueIsRecord || valueIsArray) &&
|
|
3719
|
+
isMutableSource &&
|
|
3720
|
+
typeof value[prop] === 'object'
|
|
3721
|
+
? () => false
|
|
3722
|
+
: undefined;
|
|
3723
|
+
const computation = valueIsRecord
|
|
3724
|
+
? derived(target, prop, { equal: equalFn, vivify: nodeVivify })
|
|
3725
|
+
: derived(target, {
|
|
3726
|
+
from: (v) => v?.[prop],
|
|
3727
|
+
onChange: createFallbackOnChange(target, prop, vivifyFn, isMutableSource),
|
|
3728
|
+
equal: equalFn,
|
|
3729
|
+
});
|
|
3730
|
+
const childSample = untracked(computation);
|
|
3731
|
+
const childVivify = resolveVivify(childSample, options.vivify);
|
|
3732
|
+
const proxy = toStore(computation, options);
|
|
3733
|
+
markAsLeaf(proxy, computation, childVivify !== false, options.noUnionLeaves);
|
|
3734
|
+
return proxy;
|
|
3543
3735
|
}
|
|
3544
3736
|
/**
|
|
3545
3737
|
* Converts a Signal into a deep-observable Store.
|
|
3546
3738
|
* Accessing nested properties returns a derived Signal of that path.
|
|
3547
3739
|
*
|
|
3548
3740
|
* @remarks
|
|
3549
|
-
* A
|
|
3550
|
-
*
|
|
3551
|
-
*
|
|
3552
|
-
*
|
|
3741
|
+
* A node's *container kind* (array / record / primitive) is tracked reactively via a per-node
|
|
3742
|
+
* `kind` computed, so the same proxy serves all three and a union node that flips between an
|
|
3743
|
+
* array and a record keeps working. Flips are route-forward: after a flip the node behaves as
|
|
3744
|
+
* its new kind on the next access, while child proxies cached under the old shape go stale and
|
|
3745
|
+
* are pruned by the GC.
|
|
3553
3746
|
*
|
|
3554
3747
|
* @example
|
|
3555
3748
|
* const state = store({ user: { name: 'John' } });
|
|
3556
3749
|
* const nameSignal = state.user.name; // WritableSignal<string>
|
|
3557
3750
|
*/
|
|
3558
|
-
function toStore(source, injector, vivify = false, noUnionLeaves = false) {
|
|
3751
|
+
function toStore(source, { injector, vivify = false, noUnionLeaves = false, ...rest } = {}) {
|
|
3559
3752
|
if (isStore(source))
|
|
3560
3753
|
return source;
|
|
3561
3754
|
if (!injector)
|
|
@@ -3567,106 +3760,140 @@ function toStore(source, injector, vivify = false, noUnionLeaves = false) {
|
|
|
3567
3760
|
});
|
|
3568
3761
|
const isWritableSource = isWritableSignal(source);
|
|
3569
3762
|
const isMutableSource = isWritableSource && isMutable(writableSource);
|
|
3763
|
+
const kind = computed(() => {
|
|
3764
|
+
const v = source();
|
|
3765
|
+
if (Array.isArray(v) && !isOpaque(v))
|
|
3766
|
+
return 'array';
|
|
3767
|
+
if (isRecord(v))
|
|
3768
|
+
return 'record';
|
|
3769
|
+
return 'primitive';
|
|
3770
|
+
}, ...(ngDevMode ? [{ debugName: "kind" }] : []));
|
|
3771
|
+
const STORE_OPTIONS = {
|
|
3772
|
+
injector,
|
|
3773
|
+
vivify,
|
|
3774
|
+
noUnionLeaves,
|
|
3775
|
+
[STORE_SHARED_GLOBALS]: {
|
|
3776
|
+
cache: rest[STORE_SHARED_GLOBALS]?.cache ?? injector.get(PROXY_CACHE_TOKEN),
|
|
3777
|
+
registry: rest[STORE_SHARED_GLOBALS]?.registry ??
|
|
3778
|
+
injector.get(PROXY_CLEANUP_TOKEN),
|
|
3779
|
+
},
|
|
3780
|
+
};
|
|
3781
|
+
// built lazily so non-array nodes never allocate it
|
|
3782
|
+
let length;
|
|
3783
|
+
const arrayLength = () => (length ??= computed(() => {
|
|
3784
|
+
const v = source();
|
|
3785
|
+
return Array.isArray(v) ? v.length : 0;
|
|
3786
|
+
}));
|
|
3570
3787
|
const s = new Proxy(writableSource, {
|
|
3571
3788
|
has(_, prop) {
|
|
3572
|
-
|
|
3789
|
+
const v = untracked(source);
|
|
3790
|
+
if (untracked(kind) === 'array') {
|
|
3791
|
+
if (prop === 'length')
|
|
3792
|
+
return true;
|
|
3793
|
+
if (isIndexProp(prop)) {
|
|
3794
|
+
const idx = +prop;
|
|
3795
|
+
return idx >= 0 && idx < v.length;
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
// nullish node values are routinely descended with vivify on — `in` must not throw
|
|
3799
|
+
return v == null ? false : Reflect.has(v, prop);
|
|
3573
3800
|
},
|
|
3574
3801
|
ownKeys() {
|
|
3575
3802
|
const v = untracked(source);
|
|
3803
|
+
if (untracked(kind) === 'array') {
|
|
3804
|
+
const len = v.length;
|
|
3805
|
+
const arr = new Array(len + 1);
|
|
3806
|
+
for (let i = 0; i < len; i++)
|
|
3807
|
+
arr[i] = String(i);
|
|
3808
|
+
arr[len] = 'length';
|
|
3809
|
+
return arr;
|
|
3810
|
+
}
|
|
3576
3811
|
if (!isRecord(v))
|
|
3577
3812
|
return [];
|
|
3578
3813
|
return Reflect.ownKeys(v);
|
|
3579
3814
|
},
|
|
3580
3815
|
getPrototypeOf() {
|
|
3581
|
-
|
|
3816
|
+
if (untracked(kind) === 'array')
|
|
3817
|
+
return Array.prototype;
|
|
3818
|
+
const v = untracked(source);
|
|
3819
|
+
return v == null ? Object.prototype : Object.getPrototypeOf(v);
|
|
3582
3820
|
},
|
|
3583
3821
|
getOwnPropertyDescriptor(_, prop) {
|
|
3584
|
-
const
|
|
3585
|
-
if (
|
|
3822
|
+
const v = untracked(source);
|
|
3823
|
+
if (untracked(kind) === 'array') {
|
|
3824
|
+
if (prop === 'length' ||
|
|
3825
|
+
(typeof prop === 'string' && !isNaN(+prop) && +prop < v.length))
|
|
3826
|
+
return { enumerable: true, configurable: true };
|
|
3586
3827
|
return;
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
};
|
|
3828
|
+
}
|
|
3829
|
+
if (!isRecord(v) || !(prop in v))
|
|
3830
|
+
return;
|
|
3831
|
+
return { enumerable: true, configurable: true };
|
|
3591
3832
|
},
|
|
3592
|
-
get(target, prop) {
|
|
3593
|
-
if (prop ===
|
|
3594
|
-
|
|
3833
|
+
get(target, prop, receiver) {
|
|
3834
|
+
if (typeof prop === 'symbol') {
|
|
3835
|
+
if (prop === IS_STORE)
|
|
3836
|
+
return true;
|
|
3837
|
+
if (prop === STORE_KIND)
|
|
3838
|
+
return isMutableSource
|
|
3839
|
+
? 'mutable'
|
|
3840
|
+
: isWritableSource
|
|
3841
|
+
? 'writable'
|
|
3842
|
+
: 'readonly';
|
|
3843
|
+
if (prop === STORE_SHARED_OPTIONS)
|
|
3844
|
+
return STORE_OPTIONS;
|
|
3845
|
+
}
|
|
3595
3846
|
if (prop === 'asReadonlyStore')
|
|
3596
3847
|
return () => {
|
|
3597
3848
|
if (!isWritableSource)
|
|
3598
3849
|
return s;
|
|
3599
|
-
return untracked(() => toStore(source.asReadonly(), injector, vivify, noUnionLeaves));
|
|
3850
|
+
return untracked(() => toStore(source.asReadonly(), { injector, vivify, noUnionLeaves }));
|
|
3600
3851
|
};
|
|
3601
|
-
|
|
3852
|
+
const k = untracked(kind);
|
|
3853
|
+
if (prop === 'extend' && k !== 'array')
|
|
3602
3854
|
return (seed) => scopedStore(s, seed, isMutableSource
|
|
3603
3855
|
? 'mutable'
|
|
3604
3856
|
: isWritableSource
|
|
3605
3857
|
? 'writable'
|
|
3606
|
-
: 'readonly',
|
|
3858
|
+
: 'readonly', STORE_OPTIONS);
|
|
3859
|
+
if (k === 'array') {
|
|
3860
|
+
if (prop === 'length')
|
|
3861
|
+
return arrayLength();
|
|
3862
|
+
if (prop === Symbol.iterator)
|
|
3863
|
+
return function* () {
|
|
3864
|
+
// read length reactively: a spread/for-of inside a computed/effect must re-run
|
|
3865
|
+
// when items are added or removed, not only when already-read elements change
|
|
3866
|
+
const len = arrayLength();
|
|
3867
|
+
for (let i = 0; i < len(); i++)
|
|
3868
|
+
yield receiver[i];
|
|
3869
|
+
};
|
|
3870
|
+
}
|
|
3607
3871
|
if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
|
|
3608
3872
|
return target[prop];
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
PROXY_CACHE.set(target, storeCache);
|
|
3613
|
-
}
|
|
3614
|
-
const cachedRef = storeCache.get(prop);
|
|
3615
|
-
if (cachedRef) {
|
|
3616
|
-
const cached = cachedRef.deref();
|
|
3617
|
-
if (cached)
|
|
3618
|
-
return cached;
|
|
3619
|
-
storeCache.delete(prop);
|
|
3620
|
-
PROXY_CLEANUP.unregister(cachedRef);
|
|
3621
|
-
}
|
|
3622
|
-
const value = untracked(target);
|
|
3623
|
-
const valueIsRecord = isRecord(value);
|
|
3624
|
-
const valueIsArray = Array.isArray(value);
|
|
3625
|
-
const nodeVivify = resolveVivify(value, vivify);
|
|
3626
|
-
const vivifyFn = createVivify(nodeVivify);
|
|
3627
|
-
const equalFn = (valueIsRecord || valueIsArray) &&
|
|
3628
|
-
isMutableSource &&
|
|
3629
|
-
typeof value[prop] === 'object'
|
|
3630
|
-
? () => false
|
|
3631
|
-
: undefined;
|
|
3632
|
-
const computation = valueIsRecord
|
|
3633
|
-
? derived(target, prop, { equal: equalFn, vivify: nodeVivify })
|
|
3634
|
-
: derived(target, {
|
|
3635
|
-
from: (v) => v?.[prop],
|
|
3636
|
-
onChange: createFallbackOnChange(target, prop, vivifyFn, isMutableSource),
|
|
3637
|
-
equal: equalFn,
|
|
3638
|
-
});
|
|
3639
|
-
const childSample = untracked(computation);
|
|
3640
|
-
const childVivify = resolveVivify(childSample, vivify);
|
|
3641
|
-
const proxy = Array.isArray(childSample) && !isOpaque(childSample)
|
|
3642
|
-
? toArrayStore(computation, injector, childVivify, noUnionLeaves)
|
|
3643
|
-
: toStore(computation, injector, childVivify, noUnionLeaves);
|
|
3644
|
-
markAsLeaf(proxy, computation, childVivify !== false, noUnionLeaves);
|
|
3645
|
-
const ref = new WeakRef(proxy);
|
|
3646
|
-
storeCache.set(prop, ref);
|
|
3647
|
-
PROXY_CLEANUP.register(proxy, { target, prop }, ref);
|
|
3648
|
-
return proxy;
|
|
3873
|
+
if (k === 'array' && !isIndexProp(prop))
|
|
3874
|
+
return Reflect.get(target, prop, receiver);
|
|
3875
|
+
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);
|
|
3649
3876
|
},
|
|
3650
3877
|
});
|
|
3651
3878
|
return s;
|
|
3652
3879
|
}
|
|
3653
3880
|
/**
|
|
3654
3881
|
* @internal
|
|
3655
|
-
* Backs `
|
|
3882
|
+
* Backs `extendStore(...)`. Builds a scoped overlay over `parent`: the local layer (the seed
|
|
3656
3883
|
* plus any keys created later) is its own signal and `parent` is its own signal, so the getter
|
|
3657
3884
|
* routes each key by consulting BOTH — local first, then parent, else local (so a write to an
|
|
3658
3885
|
* as-yet-unknown key lands locally). Inherited keys return the parent's own sub-store (shared
|
|
3659
3886
|
* identity + two-way), while local keys never propagate upward. A merged `computed` is derived
|
|
3660
3887
|
* only for whole-object reads / `has` / iteration — never for routing.
|
|
3661
3888
|
*/
|
|
3662
|
-
function scopedStore(parent, seed, kind,
|
|
3889
|
+
function scopedStore(parent, seed, kind, options) {
|
|
3663
3890
|
const local = isSignal(seed)
|
|
3664
|
-
? toStore(seed,
|
|
3891
|
+
? toStore(seed, options)
|
|
3665
3892
|
: kind === 'mutable'
|
|
3666
|
-
? mutableStore(seed,
|
|
3893
|
+
? mutableStore(seed, options)
|
|
3667
3894
|
: kind === 'readonly'
|
|
3668
|
-
? store(seed,
|
|
3669
|
-
: store(seed,
|
|
3895
|
+
? store(seed, options).asReadonlyStore()
|
|
3896
|
+
: store(seed, options);
|
|
3670
3897
|
const localValue = () => untracked(local);
|
|
3671
3898
|
const parentValue = () => untracked(parent);
|
|
3672
3899
|
const view = computed(() => ({
|
|
@@ -3697,14 +3924,20 @@ function scopedStore(parent, seed, kind, injector) {
|
|
|
3697
3924
|
}
|
|
3698
3925
|
const scope = new Proxy(base, {
|
|
3699
3926
|
get(target, prop) {
|
|
3700
|
-
if (prop ===
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3927
|
+
if (typeof prop === 'symbol') {
|
|
3928
|
+
if (prop === IS_STORE)
|
|
3929
|
+
return true;
|
|
3930
|
+
if (prop === STORE_KIND)
|
|
3931
|
+
return kind;
|
|
3932
|
+
if (prop === SCOPE_PARENT)
|
|
3933
|
+
return parent;
|
|
3934
|
+
if (prop === STORE_SHARED_OPTIONS)
|
|
3935
|
+
return options;
|
|
3936
|
+
}
|
|
3704
3937
|
if (prop === 'extend')
|
|
3705
|
-
return (childSeed) => scopedStore(scope, childSeed, kind,
|
|
3938
|
+
return (childSeed) => scopedStore(scope, childSeed, kind, options);
|
|
3706
3939
|
if (prop === 'asReadonlyStore')
|
|
3707
|
-
return () => toStore(computed(() => ({ ...parent(), ...local() })),
|
|
3940
|
+
return () => toStore(computed(() => ({ ...parent(), ...local() })), options);
|
|
3708
3941
|
if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
|
|
3709
3942
|
return target[prop];
|
|
3710
3943
|
// Route by consulting both signals: local first, then parent, else local (new → local).
|
|
@@ -3731,19 +3964,53 @@ function scopedStore(parent, seed, kind, injector) {
|
|
|
3731
3964
|
});
|
|
3732
3965
|
return scope;
|
|
3733
3966
|
}
|
|
3967
|
+
/** @internal Reads a store's writability brand, falling back to signal inspection if unbranded. */
|
|
3968
|
+
function storeKind(s) {
|
|
3969
|
+
return (s[STORE_KIND] ??
|
|
3970
|
+
(isWritableSignal(s) ? (isMutable(s) ? 'mutable' : 'writable') : 'readonly'));
|
|
3971
|
+
}
|
|
3972
|
+
/**
|
|
3973
|
+
* Extends a store with extra keys via a scoped overlay, returning a new store that reads through
|
|
3974
|
+
* to the parent for inherited keys (shared identity + two-way) while holding the new keys locally.
|
|
3975
|
+
*
|
|
3976
|
+
* The typesafe successor to the deprecated `store.extend(...)` method — moving it off the proxy
|
|
3977
|
+
* frees the `extend` key for use as a normal record key. Writability (readonly/writable/mutable)
|
|
3978
|
+
* is inherited from `store`.
|
|
3979
|
+
*
|
|
3980
|
+
* @example
|
|
3981
|
+
* const base = store({ count: 0 });
|
|
3982
|
+
* const scoped = extendStore(base, { label: 'live' });
|
|
3983
|
+
* scoped.count.set(1); // writes through to base
|
|
3984
|
+
* scoped.label.set('x'); // stays local
|
|
3985
|
+
*/
|
|
3986
|
+
function extendStore(store, source, options) {
|
|
3987
|
+
const opt = {
|
|
3988
|
+
...store[STORE_SHARED_OPTIONS],
|
|
3989
|
+
...options,
|
|
3990
|
+
};
|
|
3991
|
+
return scopedStore(store, source, storeKind(store), opt);
|
|
3992
|
+
}
|
|
3734
3993
|
/**
|
|
3735
3994
|
* Creates a WritableSignalStore from a value.
|
|
3736
3995
|
* @see {@link toStore}
|
|
3737
3996
|
*/
|
|
3738
3997
|
function store(value, opt) {
|
|
3739
|
-
return toStore(signal(value, opt),
|
|
3998
|
+
return toStore(signal(value, opt), {
|
|
3999
|
+
vivify: false,
|
|
4000
|
+
noUnionLeaves: false,
|
|
4001
|
+
...opt,
|
|
4002
|
+
});
|
|
3740
4003
|
}
|
|
3741
4004
|
/**
|
|
3742
4005
|
* Creates a MutableSignalStore from a value.
|
|
3743
4006
|
* @see {@link toStore}
|
|
3744
4007
|
*/
|
|
3745
4008
|
function mutableStore(value, opt) {
|
|
3746
|
-
return toStore(mutable(value, opt),
|
|
4009
|
+
return toStore(mutable(value, opt), {
|
|
4010
|
+
vivify: false,
|
|
4011
|
+
noUnionLeaves: false,
|
|
4012
|
+
...opt,
|
|
4013
|
+
});
|
|
3747
4014
|
}
|
|
3748
4015
|
|
|
3749
4016
|
function isPlainRecord(value) {
|
|
@@ -3808,7 +4075,13 @@ function forkStore(base, opt) {
|
|
|
3808
4075
|
source: () => base(),
|
|
3809
4076
|
computation: (theirs, prev) => prev === undefined ? theirs : merge(prev.source, prev.value, theirs),
|
|
3810
4077
|
}]));
|
|
3811
|
-
|
|
4078
|
+
// Inherit the base's shared options (injector, vivify, noUnionLeaves + the
|
|
4079
|
+
// proxy cache/registry), same as extendStore — a fork should vivify like its
|
|
4080
|
+
// base and share its injector-scoped cache. `opt` overrides (advanced use).
|
|
4081
|
+
const store = toStore(staged, {
|
|
4082
|
+
...base[STORE_SHARED_OPTIONS],
|
|
4083
|
+
...opt,
|
|
4084
|
+
});
|
|
3812
4085
|
return {
|
|
3813
4086
|
store,
|
|
3814
4087
|
commit: () => base.set(untracked(staged)),
|
|
@@ -3816,6 +4089,26 @@ function forkStore(base, opt) {
|
|
|
3816
4089
|
};
|
|
3817
4090
|
}
|
|
3818
4091
|
|
|
4092
|
+
/**
|
|
4093
|
+
* @internal The plain-`effect` sibling of the public {@link pausableEffect} (which is built on
|
|
4094
|
+
* `nestedEffect`). For infra utilities that own a single top-level effect/subscription and don't
|
|
4095
|
+
* need frame/nesting semantics. Opt-in (default off): with no `pause` (call site or
|
|
4096
|
+
* `providePausableOptions` default) it returns a bare `effect` (zero overhead, byte-identical to
|
|
4097
|
+
* today); otherwise it gates the body on the resolved predicate — read FIRST so the dependency set
|
|
4098
|
+
* collapses to just the predicate while paused, re-tracking on resume. Deliberately NOT re-exported
|
|
4099
|
+
* from the public barrel.
|
|
4100
|
+
*/
|
|
4101
|
+
function pausablePureEffect(effectFn, options) {
|
|
4102
|
+
const paused = resolvePause(options, false);
|
|
4103
|
+
if (!paused)
|
|
4104
|
+
return effect(effectFn, options);
|
|
4105
|
+
return effect((registerCleanup) => {
|
|
4106
|
+
if (paused())
|
|
4107
|
+
return;
|
|
4108
|
+
effectFn(registerCleanup);
|
|
4109
|
+
}, options);
|
|
4110
|
+
}
|
|
4111
|
+
|
|
3819
4112
|
// Internal dummy store for server-side rendering
|
|
3820
4113
|
const noopStore = {
|
|
3821
4114
|
getItem: () => null,
|
|
@@ -3877,8 +4170,9 @@ const noopStore = {
|
|
|
3877
4170
|
* }
|
|
3878
4171
|
* ```
|
|
3879
4172
|
*/
|
|
3880
|
-
function stored(fallback, { key, store: providedStore, serialize = JSON.stringify, deserialize = JSON.parse, syncTabs = false, equal = Object.is, onKeyChange = 'load', cleanupOldKey = false, validate = () => true, ...rest }) {
|
|
3881
|
-
const
|
|
4173
|
+
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 }) {
|
|
4174
|
+
const injector = providedInjector ?? inject(Injector);
|
|
4175
|
+
const isServer = isPlatformServer(injector.get(PLATFORM_ID));
|
|
3882
4176
|
const fallbackStore = isServer ? noopStore : localStorage;
|
|
3883
4177
|
const store = providedStore ?? fallbackStore;
|
|
3884
4178
|
const keySig = typeof key === 'string'
|
|
@@ -3886,8 +4180,6 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
3886
4180
|
: isSignal(key)
|
|
3887
4181
|
? key
|
|
3888
4182
|
: computed(key);
|
|
3889
|
-
// "no stored value" marker — distinct from `null`/`undefined`, so a nullable `T` can
|
|
3890
|
-
// round-trip a legitimate `null` through `set` instead of it acting like `clear()`
|
|
3891
4183
|
const EMPTY = Symbol();
|
|
3892
4184
|
const getValue = (key) => {
|
|
3893
4185
|
const found = store.getItem(key);
|
|
@@ -3941,7 +4233,7 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
3941
4233
|
}]));
|
|
3942
4234
|
let prevKey = initialKey;
|
|
3943
4235
|
if (onKeyChange === 'store') {
|
|
3944
|
-
|
|
4236
|
+
pausablePureEffect(() => {
|
|
3945
4237
|
const k = keySig();
|
|
3946
4238
|
storeValue(k, internal());
|
|
3947
4239
|
if (prevKey !== k) {
|
|
@@ -3949,10 +4241,10 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
3949
4241
|
store.removeItem(prevKey);
|
|
3950
4242
|
prevKey = k;
|
|
3951
4243
|
}
|
|
3952
|
-
});
|
|
4244
|
+
}, { injector, pause });
|
|
3953
4245
|
}
|
|
3954
4246
|
else {
|
|
3955
|
-
|
|
4247
|
+
pausablePureEffect(() => {
|
|
3956
4248
|
const k = keySig();
|
|
3957
4249
|
const internalValue = internal();
|
|
3958
4250
|
if (k === prevKey) {
|
|
@@ -3965,14 +4257,11 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
3965
4257
|
prevKey = k;
|
|
3966
4258
|
internal.set(value); // load new value
|
|
3967
4259
|
}
|
|
3968
|
-
});
|
|
4260
|
+
}, { injector, pause });
|
|
3969
4261
|
}
|
|
3970
4262
|
if (syncTabs && !isServer) {
|
|
3971
|
-
const destroyRef =
|
|
4263
|
+
const destroyRef = injector.get(DestroyRef);
|
|
3972
4264
|
const sync = (e) => {
|
|
3973
|
-
// `storage` events only describe Web Storage — ignore events for a different
|
|
3974
|
-
// storage area (or any event when a custom adapter is configured), otherwise an
|
|
3975
|
-
// unrelated localStorage write with the same key string corrupts our state
|
|
3976
4265
|
if (e.storageArea !== store)
|
|
3977
4266
|
return;
|
|
3978
4267
|
if (e.key !== untracked(keySig))
|
|
@@ -4101,29 +4390,26 @@ function generateDeterministicID() {
|
|
|
4101
4390
|
*
|
|
4102
4391
|
*/
|
|
4103
4392
|
function tabSync(sig, opt) {
|
|
4104
|
-
|
|
4393
|
+
const optObj = typeof opt === 'object' ? opt : undefined;
|
|
4394
|
+
const injector = optObj?.injector ?? inject(Injector);
|
|
4395
|
+
if (isPlatformServer(injector.get(PLATFORM_ID)))
|
|
4105
4396
|
return sig;
|
|
4106
4397
|
const id = typeof opt === 'string' ? opt : (opt?.id ?? generateDeterministicID());
|
|
4107
|
-
const bus =
|
|
4108
|
-
// The last value applied from a remote tab. The outbound effect skips (exactly) the run
|
|
4109
|
-
// caused by that write — without this, an inbound object (a fresh structured clone, so
|
|
4110
|
-
// never reference-equal) would be re-posted, and two tabs would ping-pong forever.
|
|
4398
|
+
const bus = injector.get(MessageBus);
|
|
4111
4399
|
const NONE = Symbol();
|
|
4112
4400
|
let received = NONE;
|
|
4113
4401
|
const { unsub, post } = bus.subscribe(id, (next) => {
|
|
4114
4402
|
const before = untracked(sig);
|
|
4115
4403
|
received = next;
|
|
4116
4404
|
sig.set(next);
|
|
4117
|
-
// Equality-suppressed write (e.g. an identical primitive): no effect run will follow,
|
|
4118
|
-
// so clear the marker — it must not swallow a later, genuinely local change.
|
|
4119
4405
|
if (untracked(sig) === before)
|
|
4120
4406
|
received = NONE;
|
|
4121
4407
|
});
|
|
4122
|
-
let
|
|
4408
|
+
let firstDone = false;
|
|
4123
4409
|
const effectRef = effect(() => {
|
|
4124
4410
|
const val = sig();
|
|
4125
|
-
if (!
|
|
4126
|
-
|
|
4411
|
+
if (!firstDone) {
|
|
4412
|
+
firstDone = true;
|
|
4127
4413
|
return;
|
|
4128
4414
|
}
|
|
4129
4415
|
if (val === received) {
|
|
@@ -4132,8 +4418,8 @@ function tabSync(sig, opt) {
|
|
|
4132
4418
|
}
|
|
4133
4419
|
received = NONE;
|
|
4134
4420
|
post(val);
|
|
4135
|
-
}, ...(ngDevMode ? [{ debugName: "effectRef" }] : []));
|
|
4136
|
-
|
|
4421
|
+
}, ...(ngDevMode ? [{ debugName: "effectRef", injector }] : [{ injector }]));
|
|
4422
|
+
injector.get(DestroyRef).onDestroy(() => {
|
|
4137
4423
|
effectRef.destroy();
|
|
4138
4424
|
unsub();
|
|
4139
4425
|
});
|
|
@@ -4337,5 +4623,5 @@ function withHistory(sourceOrValue, opt) {
|
|
|
4337
4623
|
* Generated bundle index. Do not edit.
|
|
4338
4624
|
*/
|
|
4339
4625
|
|
|
4340
|
-
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 };
|
|
4626
|
+
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 };
|
|
4341
4627
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|