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