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