@mmstack/primitives 19.2.3 → 19.3.1
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/LICENSE +21 -21
- package/README.md +1038 -621
- package/fesm2022/mmstack-primitives.mjs +1173 -198
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/index.d.ts +6 -2
- package/lib/chunked.d.ts +36 -0
- package/lib/derived.d.ts +51 -3
- package/lib/effect/frame-stack.d.ts +10 -0
- package/lib/effect/index.d.ts +1 -0
- package/lib/effect/nested-effect.d.ts +77 -0
- package/lib/get-signal-equality.d.ts +1 -1
- package/lib/mappers/index-array.d.ts +54 -0
- package/lib/mappers/index.d.ts +3 -0
- package/lib/mappers/key-array.d.ts +28 -0
- package/lib/mappers/map-object.d.ts +15 -0
- package/lib/mappers/util.d.ts +9 -0
- package/lib/pipeable/operators.d.ts +14 -0
- package/lib/pipeable/pipeble.d.ts +23 -0
- package/lib/pipeable/public_api.d.ts +3 -0
- package/lib/pipeable/types.d.ts +62 -0
- package/lib/sensors/element-size.d.ts +35 -0
- package/lib/{element-visibility.d.ts → sensors/element-visibility.d.ts} +1 -1
- package/lib/sensors/index.d.ts +2 -0
- package/lib/sensors/media-query.d.ts +1 -1
- package/lib/sensors/mouse-position.d.ts +1 -1
- package/lib/sensors/network-status.d.ts +1 -1
- package/lib/sensors/page-visibility.d.ts +1 -1
- package/lib/sensors/sensor.d.ts +108 -22
- package/lib/sensors/window-size.d.ts +1 -1
- package/lib/store.d.ts +58 -0
- package/lib/stored.d.ts +6 -2
- package/lib/tabSync.d.ts +56 -0
- package/lib/throttled.d.ts +1 -1
- package/lib/to-writable.d.ts +9 -2
- package/lib/until.d.ts +28 -23
- package/package.json +6 -7
- package/lib/map-array.d.ts +0 -61
|
@@ -1,7 +1,216 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { isDevMode, inject, Injector, untracked, effect, DestroyRef, linkedSignal, computed, signal, isSignal, ElementRef, PLATFORM_ID, Injectable, runInInjectionContext } from '@angular/core';
|
|
2
3
|
import { isPlatformServer } from '@angular/common';
|
|
3
4
|
import { SIGNAL } from '@angular/core/primitives/signals';
|
|
4
5
|
|
|
6
|
+
const frameStack = [];
|
|
7
|
+
function currentFrame() {
|
|
8
|
+
return frameStack.at(-1) ?? null;
|
|
9
|
+
}
|
|
10
|
+
function clearFrame(frame, userCleanups) {
|
|
11
|
+
frame.parent = null;
|
|
12
|
+
for (const fn of userCleanups) {
|
|
13
|
+
try {
|
|
14
|
+
fn();
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
if (isDevMode())
|
|
18
|
+
console.error('Error destroying nested effect:', e);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
userCleanups.length = 0;
|
|
22
|
+
for (const child of frame.children) {
|
|
23
|
+
try {
|
|
24
|
+
child.destroy();
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
if (isDevMode())
|
|
28
|
+
console.error('Error destroying nested effect:', e);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
frame.children.clear();
|
|
32
|
+
}
|
|
33
|
+
function pushFrame(frame) {
|
|
34
|
+
return frameStack.push(frame);
|
|
35
|
+
}
|
|
36
|
+
function popFrame() {
|
|
37
|
+
return frameStack.pop();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates an effect that can be nested, similar to SolidJS's `createEffect`.
|
|
42
|
+
*
|
|
43
|
+
* This primitive enables true hierarchical reactivity. A `nestedEffect` created
|
|
44
|
+
* within another `nestedEffect` is automatically destroyed and recreated when
|
|
45
|
+
* the parent re-runs.
|
|
46
|
+
*
|
|
47
|
+
* It automatically handles injector propagation and lifetime management, allowing
|
|
48
|
+
* you to create fine-grained, conditional side-effects that only track
|
|
49
|
+
* dependencies when they are "live".
|
|
50
|
+
*
|
|
51
|
+
* @param effectFn The side-effect function, which receives a cleanup register function.
|
|
52
|
+
* @param options (Optional) Angular's `CreateEffectOptions`.
|
|
53
|
+
* @returns An `EffectRef` for the created effect.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* // Assume `coldGuard` changes rarely, but `hotSignal` changes often.
|
|
58
|
+
* const coldGuard = signal(false);
|
|
59
|
+
* const hotSignal = signal(0);
|
|
60
|
+
*
|
|
61
|
+
* nestedEffect(() => {
|
|
62
|
+
* // This outer effect only tracks `coldGuard`.
|
|
63
|
+
* if (coldGuard()) {
|
|
64
|
+
*
|
|
65
|
+
* // This inner effect is CREATED when coldGuard is true
|
|
66
|
+
* // and DESTROYED when it becomes false.
|
|
67
|
+
* nestedEffect(() => {
|
|
68
|
+
* // It only tracks `hotSignal` while it exists.
|
|
69
|
+
* console.log('Hot signal is:', hotSignal());
|
|
70
|
+
* });
|
|
71
|
+
* }
|
|
72
|
+
* // If `coldGuard` is false, this outer effect does not track `hotSignal`.
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* const users = signal([
|
|
78
|
+
* { id: 1, name: 'Alice' },
|
|
79
|
+
* { id: 2, name: 'Bob' }
|
|
80
|
+
* ]);
|
|
81
|
+
*
|
|
82
|
+
* // The fine-grained mapped list
|
|
83
|
+
* const mappedUsers = mapArray(
|
|
84
|
+
* users,
|
|
85
|
+
* (userSignal, index) => {
|
|
86
|
+
* // 1. Create a fine-grained SIDE EFFECT for *this item*
|
|
87
|
+
* // This effect's lifetime is now tied to this specific item. created once on init of this index.
|
|
88
|
+
* const effectRef = nestedEffect(() => {
|
|
89
|
+
* // This only runs if *this* userSignal changes,
|
|
90
|
+
* // not if the whole list changes.
|
|
91
|
+
* console.log(`User ${index} updated:`, userSignal().name);
|
|
92
|
+
* });
|
|
93
|
+
*
|
|
94
|
+
* // 2. Return the data AND the cleanup logic
|
|
95
|
+
* return {
|
|
96
|
+
* // The mapped data
|
|
97
|
+
* label: computed(() => `User: ${userSignal().name}`),
|
|
98
|
+
*
|
|
99
|
+
* // The cleanup function
|
|
100
|
+
* destroyEffect: () => effectRef.destroy()
|
|
101
|
+
* };
|
|
102
|
+
* },
|
|
103
|
+
* {
|
|
104
|
+
* // 3. Tell mapArray HOW to clean up when an item is removed, this needs to be manual as it's not a nestedEffect itself
|
|
105
|
+
* onDestroy: (mappedItem) => {
|
|
106
|
+
* mappedItem.destroyEffect();
|
|
107
|
+
* }
|
|
108
|
+
* }
|
|
109
|
+
* );
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
function nestedEffect(effectFn, options) {
|
|
113
|
+
const bindToFrame = options?.bindToFrame ?? ((parent) => parent);
|
|
114
|
+
const parent = bindToFrame(currentFrame());
|
|
115
|
+
const injector = options?.injector ?? parent?.injector ?? inject(Injector);
|
|
116
|
+
let isDestroyed = false;
|
|
117
|
+
const srcRef = untracked(() => {
|
|
118
|
+
return effect((cleanup) => {
|
|
119
|
+
if (isDestroyed)
|
|
120
|
+
return;
|
|
121
|
+
const frame = {
|
|
122
|
+
injector,
|
|
123
|
+
parent,
|
|
124
|
+
children: new Set(),
|
|
125
|
+
};
|
|
126
|
+
const userCleanups = [];
|
|
127
|
+
pushFrame(frame);
|
|
128
|
+
try {
|
|
129
|
+
effectFn((fn) => userCleanups.push(fn));
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
popFrame();
|
|
133
|
+
}
|
|
134
|
+
return cleanup(() => clearFrame(frame, userCleanups));
|
|
135
|
+
}, {
|
|
136
|
+
...options,
|
|
137
|
+
injector,
|
|
138
|
+
manualCleanup: options?.manualCleanup ?? !!parent,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
const ref = {
|
|
142
|
+
destroy: () => {
|
|
143
|
+
if (isDestroyed)
|
|
144
|
+
return;
|
|
145
|
+
isDestroyed = true;
|
|
146
|
+
parent?.children.delete(ref);
|
|
147
|
+
srcRef.destroy();
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
parent?.children.add(ref);
|
|
151
|
+
injector.get(DestroyRef).onDestroy(() => ref.destroy());
|
|
152
|
+
return ref;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 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.
|
|
157
|
+
*
|
|
158
|
+
* 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 `duration`.
|
|
159
|
+
*
|
|
160
|
+
* @template T The type of items in the array.
|
|
161
|
+
* @param source A `Signal` or a function that returns an array of items to be processed in chunks.
|
|
162
|
+
* @param options Configuration options for chunk size, delay duration, equality function, and injector.
|
|
163
|
+
* @returns A `Signal` that emits the current chunk of items being processed.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* const largeList = signal(Array.from({ length: 1000 }, (_, i) => i));
|
|
167
|
+
* const chunkedList = chunked(largeList, { chunkSize: 100, duration: 100 });
|
|
168
|
+
*/
|
|
169
|
+
function chunked(source, options) {
|
|
170
|
+
const { chunkSize = 50, delay = 'frame', equal, injector } = options || {};
|
|
171
|
+
let delayFn;
|
|
172
|
+
if (delay === 'frame') {
|
|
173
|
+
delayFn = (callback) => {
|
|
174
|
+
const num = requestAnimationFrame(callback);
|
|
175
|
+
return () => cancelAnimationFrame(num);
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
else if (delay === 'microtask') {
|
|
179
|
+
delayFn = (cb) => {
|
|
180
|
+
let isCancelled = false;
|
|
181
|
+
queueMicrotask(() => {
|
|
182
|
+
if (isCancelled)
|
|
183
|
+
return;
|
|
184
|
+
cb();
|
|
185
|
+
});
|
|
186
|
+
return () => {
|
|
187
|
+
isCancelled = true;
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
delayFn = (cb) => {
|
|
193
|
+
const num = setTimeout(cb, delay);
|
|
194
|
+
return () => clearTimeout(num);
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const internal = linkedSignal({
|
|
198
|
+
source,
|
|
199
|
+
computation: (items) => items.slice(0, chunkSize),
|
|
200
|
+
equal,
|
|
201
|
+
});
|
|
202
|
+
nestedEffect((cleanup) => {
|
|
203
|
+
const fullList = source();
|
|
204
|
+
const current = internal();
|
|
205
|
+
if (current.length >= fullList.length)
|
|
206
|
+
return;
|
|
207
|
+
return cleanup(delayFn(() => untracked(() => internal.set(fullList.slice(0, current.length + chunkSize)))));
|
|
208
|
+
}, {
|
|
209
|
+
injector: injector,
|
|
210
|
+
});
|
|
211
|
+
return internal.asReadonly();
|
|
212
|
+
}
|
|
213
|
+
|
|
5
214
|
/**
|
|
6
215
|
* Converts a read-only `Signal` into a `WritableSignal` by providing custom `set` and, optionally, `update` functions.
|
|
7
216
|
* This can be useful for creating controlled write access to a signal that is otherwise read-only.
|
|
@@ -30,9 +239,9 @@ import { SIGNAL } from '@angular/core/primitives/signals';
|
|
|
30
239
|
*
|
|
31
240
|
* writableSignal.set(5); // sets value of originalValue.a to 5 & triggers all signals
|
|
32
241
|
*/
|
|
33
|
-
function toWritable(
|
|
34
|
-
const internal =
|
|
35
|
-
internal.asReadonly = () =>
|
|
242
|
+
function toWritable(source, set, update, opt) {
|
|
243
|
+
const internal = (opt?.pure !== false ? computed(source) : source);
|
|
244
|
+
internal.asReadonly = () => source;
|
|
36
245
|
internal.set = set;
|
|
37
246
|
internal.update = update ?? ((updater) => set(updater(untracked(internal))));
|
|
38
247
|
return internal;
|
|
@@ -114,19 +323,19 @@ function debounce(source, opt) {
|
|
|
114
323
|
catch {
|
|
115
324
|
// not in injection context & no destroyRef provided opting out of cleanup
|
|
116
325
|
}
|
|
117
|
-
const triggerFn = (
|
|
326
|
+
const triggerFn = (next) => {
|
|
118
327
|
if (timeout)
|
|
119
328
|
clearTimeout(timeout);
|
|
120
|
-
|
|
329
|
+
source.set(next);
|
|
121
330
|
timeout = setTimeout(() => {
|
|
122
331
|
trigger.update((c) => !c);
|
|
123
332
|
}, ms);
|
|
124
333
|
};
|
|
125
334
|
const set = (value) => {
|
|
126
|
-
triggerFn(
|
|
335
|
+
triggerFn(value);
|
|
127
336
|
};
|
|
128
337
|
const update = (fn) => {
|
|
129
|
-
triggerFn((
|
|
338
|
+
triggerFn(fn(untracked(source)));
|
|
130
339
|
};
|
|
131
340
|
const writable = toWritable(computed(() => {
|
|
132
341
|
trigger();
|
|
@@ -136,25 +345,113 @@ function debounce(source, opt) {
|
|
|
136
345
|
return writable;
|
|
137
346
|
}
|
|
138
347
|
|
|
348
|
+
const { is } = Object;
|
|
349
|
+
function mutable(initial, opt) {
|
|
350
|
+
const baseEqual = opt?.equal ?? is;
|
|
351
|
+
let trigger = false;
|
|
352
|
+
const equal = (a, b) => {
|
|
353
|
+
if (trigger)
|
|
354
|
+
return false;
|
|
355
|
+
return baseEqual(a, b);
|
|
356
|
+
};
|
|
357
|
+
const sig = signal(initial, {
|
|
358
|
+
...opt,
|
|
359
|
+
equal,
|
|
360
|
+
});
|
|
361
|
+
const internalUpdate = sig.update;
|
|
362
|
+
sig.mutate = (updater) => {
|
|
363
|
+
trigger = true;
|
|
364
|
+
internalUpdate(updater);
|
|
365
|
+
trigger = false;
|
|
366
|
+
};
|
|
367
|
+
sig.inline = (updater) => {
|
|
368
|
+
sig.mutate((prev) => {
|
|
369
|
+
updater(prev);
|
|
370
|
+
return prev;
|
|
371
|
+
});
|
|
372
|
+
};
|
|
373
|
+
return sig;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Type guard function to check if a given `WritableSignal` is a `MutableSignal`. This is useful
|
|
377
|
+
* for situations where you need to conditionally use the `mutate` or `inline` methods.
|
|
378
|
+
*
|
|
379
|
+
* @typeParam T - The type of the signal's value (optional, defaults to `any`).
|
|
380
|
+
* @param value - The `WritableSignal` to check.
|
|
381
|
+
* @returns `true` if the signal is a `MutableSignal`, `false` otherwise.
|
|
382
|
+
*
|
|
383
|
+
* @example
|
|
384
|
+
* const mySignal = signal(0);
|
|
385
|
+
* const myMutableSignal = mutable(0);
|
|
386
|
+
*
|
|
387
|
+
* if (isMutable(mySignal)) {
|
|
388
|
+
* mySignal.mutate(x => x + 1); // This would cause a type error, as mySignal is not a MutableSignal.
|
|
389
|
+
* }
|
|
390
|
+
*
|
|
391
|
+
* if (isMutable(myMutableSignal)) {
|
|
392
|
+
* myMutableSignal.mutate(x => x + 1); // This is safe.
|
|
393
|
+
* }
|
|
394
|
+
*/
|
|
395
|
+
function isMutable(value) {
|
|
396
|
+
return 'mutate' in value && typeof value.mutate === 'function';
|
|
397
|
+
}
|
|
398
|
+
|
|
139
399
|
function derived(source, optOrKey, opt) {
|
|
140
400
|
const isArray = Array.isArray(untracked(source)) && typeof optOrKey === 'number';
|
|
141
401
|
const from = typeof optOrKey === 'object' ? optOrKey.from : (v) => v[optOrKey];
|
|
142
402
|
const onChange = typeof optOrKey === 'object'
|
|
143
403
|
? optOrKey.onChange
|
|
144
404
|
: isArray
|
|
145
|
-
? (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
405
|
+
? isMutable(source)
|
|
406
|
+
? (next) => {
|
|
407
|
+
source.mutate((cur) => {
|
|
408
|
+
cur[optOrKey] = next;
|
|
409
|
+
return cur;
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
: (next) => {
|
|
413
|
+
source.update((cur) => {
|
|
414
|
+
const newArray = [...cur];
|
|
415
|
+
newArray[optOrKey] = next;
|
|
416
|
+
return newArray;
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
: isMutable(source)
|
|
420
|
+
? (next) => {
|
|
421
|
+
source.mutate((cur) => {
|
|
422
|
+
cur[optOrKey] =
|
|
423
|
+
next;
|
|
424
|
+
return cur;
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
: (next) => {
|
|
428
|
+
source.update((cur) => ({ ...cur, [optOrKey]: next }));
|
|
429
|
+
};
|
|
430
|
+
const rest = typeof optOrKey === 'object' ? { ...optOrKey, ...opt } : opt;
|
|
431
|
+
const baseEqual = rest?.equal ?? Object.is;
|
|
432
|
+
let trigger = false;
|
|
433
|
+
const equal = isMutable(source)
|
|
434
|
+
? (a, b) => {
|
|
435
|
+
if (trigger)
|
|
436
|
+
return false;
|
|
437
|
+
return baseEqual(a, b);
|
|
438
|
+
}
|
|
439
|
+
: baseEqual;
|
|
440
|
+
const sig = toWritable(computed(() => from(source()), { ...rest, equal }), (newVal) => onChange(newVal), undefined, { pure: false });
|
|
157
441
|
sig.from = from;
|
|
442
|
+
if (isMutable(source)) {
|
|
443
|
+
sig.mutate = (updater) => {
|
|
444
|
+
trigger = true;
|
|
445
|
+
sig.update(updater);
|
|
446
|
+
trigger = false;
|
|
447
|
+
};
|
|
448
|
+
sig.inline = (updater) => {
|
|
449
|
+
sig.mutate((prev) => {
|
|
450
|
+
updater(prev);
|
|
451
|
+
return prev;
|
|
452
|
+
});
|
|
453
|
+
};
|
|
454
|
+
}
|
|
158
455
|
return sig;
|
|
159
456
|
}
|
|
160
457
|
/**
|
|
@@ -201,6 +498,445 @@ function isDerivation(sig) {
|
|
|
201
498
|
return 'from' in sig;
|
|
202
499
|
}
|
|
203
500
|
|
|
501
|
+
function isWritableSignal(value) {
|
|
502
|
+
return 'set' in value && typeof value.set === 'function';
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* @internal
|
|
506
|
+
* Creates a setter function for a source signal of type `Signal<T[]>` or a function returning `T[]`.
|
|
507
|
+
* @param source The source signal of type `Signal<T[]>` or a function returning `T[]`.
|
|
508
|
+
* @returns
|
|
509
|
+
*/
|
|
510
|
+
function createSetter(source) {
|
|
511
|
+
if (!isWritableSignal(source))
|
|
512
|
+
return () => {
|
|
513
|
+
// noop;
|
|
514
|
+
};
|
|
515
|
+
if (isMutable(source))
|
|
516
|
+
return (value, index) => {
|
|
517
|
+
source.mutate((arr) => {
|
|
518
|
+
arr[index] = value;
|
|
519
|
+
return arr;
|
|
520
|
+
});
|
|
521
|
+
};
|
|
522
|
+
return (value, index) => {
|
|
523
|
+
source.update((arr) => arr.map((v, i) => (i === index ? value : v)));
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Helper to create the derived signal for a specific index.
|
|
529
|
+
* Extracts the cast logic to keep the main loop clean.
|
|
530
|
+
*/
|
|
531
|
+
function createItemSignal(source, index, setter, opt) {
|
|
532
|
+
return derived(
|
|
533
|
+
// We cast to any/Mutable to satisfy the overload signature,
|
|
534
|
+
// but 'derived' internally checks isMutable() for safety.
|
|
535
|
+
source, {
|
|
536
|
+
from: (src) => src[index],
|
|
537
|
+
onChange: (value) => setter(value, index),
|
|
538
|
+
}, opt);
|
|
539
|
+
}
|
|
540
|
+
function indexArray(source, map, opt = {}) {
|
|
541
|
+
const data = isSignal(source) ? source : computed(source);
|
|
542
|
+
const len = computed(() => data().length);
|
|
543
|
+
const setter = createSetter(data);
|
|
544
|
+
const writableData = isWritableSignal(data)
|
|
545
|
+
? data
|
|
546
|
+
: toWritable(data, () => {
|
|
547
|
+
// noop
|
|
548
|
+
});
|
|
549
|
+
if (isWritableSignal(data) && isMutable(data) && !opt.equal) {
|
|
550
|
+
opt.equal = (a, b) => {
|
|
551
|
+
if (a !== b)
|
|
552
|
+
return false; // actually check primitives and references
|
|
553
|
+
return false; // opt out for same refs
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
return linkedSignal({
|
|
557
|
+
source: () => len(),
|
|
558
|
+
computation: (len, prev) => {
|
|
559
|
+
if (!prev)
|
|
560
|
+
return Array.from({ length: len }, (_, i) => map(createItemSignal(writableData, i, setter, opt), i));
|
|
561
|
+
if (len === prev.value.length)
|
|
562
|
+
return prev.value;
|
|
563
|
+
if (len < prev.value.length) {
|
|
564
|
+
if (opt.onDestroy) {
|
|
565
|
+
for (let i = len; i < prev.value.length; i++) {
|
|
566
|
+
opt.onDestroy(prev.value[i]);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return prev.value.slice(0, len);
|
|
570
|
+
}
|
|
571
|
+
const next = prev.value.slice();
|
|
572
|
+
for (let i = prev.value.length; i < len; i++)
|
|
573
|
+
next[i] = map(createItemSignal(writableData, i, setter, opt), i);
|
|
574
|
+
return next;
|
|
575
|
+
},
|
|
576
|
+
equal: (a, b) => a.length === b.length,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* @deprecated use indexArray instead
|
|
581
|
+
*/
|
|
582
|
+
const mapArray = indexArray;
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Reactively maps items from a source array to a new array by value (identity).
|
|
586
|
+
*
|
|
587
|
+
* similar to `Array.prototype.map`, but:
|
|
588
|
+
* 1. The `mapFn` receives the `index` as a Signal.
|
|
589
|
+
* 2. If an item in the `source` array moves to a new position, the *result* of the map function is reused and moved.
|
|
590
|
+
* The `index` signal is updated to the new index.
|
|
591
|
+
* 3. The `mapFn` is only run for *new* items.
|
|
592
|
+
*
|
|
593
|
+
* This is useful for building efficient lists where DOM nodes or heavy instances should be reused
|
|
594
|
+
* when the list is reordered.
|
|
595
|
+
*
|
|
596
|
+
* @param source A `Signal<T[]>` or a function returning `T[]`.
|
|
597
|
+
* @param mapFn The mapping function. Receives the item and its index as a Signal.
|
|
598
|
+
* @param options Optional configuration:
|
|
599
|
+
* - `onDestroy`: A callback invoked when a mapped item is removed from the array.
|
|
600
|
+
* @returns A `Signal<U[]>` containing the mapped array.
|
|
601
|
+
*/
|
|
602
|
+
function keyArray(source, mapFn, options = {}) {
|
|
603
|
+
const sourceSignal = isSignal(source) ? source : computed(source);
|
|
604
|
+
const items = [];
|
|
605
|
+
let mapped = [];
|
|
606
|
+
const indexes = [];
|
|
607
|
+
const getKey = options.key || ((v) => v);
|
|
608
|
+
const newIndices = new Map();
|
|
609
|
+
const temp = [];
|
|
610
|
+
const tempIndexes = [];
|
|
611
|
+
const newIndicesNext = [];
|
|
612
|
+
const newIndexesCache = new Array();
|
|
613
|
+
return computed(() => {
|
|
614
|
+
const newItems = sourceSignal() || [];
|
|
615
|
+
return untracked(() => {
|
|
616
|
+
let i;
|
|
617
|
+
let j;
|
|
618
|
+
const newLen = newItems.length;
|
|
619
|
+
const len = items.length;
|
|
620
|
+
const newMapped = new Array(newLen);
|
|
621
|
+
const newIndexes = newIndexesCache;
|
|
622
|
+
newIndexes.length = 0;
|
|
623
|
+
newIndexes.length = newLen;
|
|
624
|
+
let start;
|
|
625
|
+
let end;
|
|
626
|
+
let newEnd;
|
|
627
|
+
let item;
|
|
628
|
+
let key;
|
|
629
|
+
if (newLen === 0) {
|
|
630
|
+
if (len !== 0) {
|
|
631
|
+
if (options.onDestroy) {
|
|
632
|
+
for (let k = 0; k < len; k++)
|
|
633
|
+
options.onDestroy(mapped[k]);
|
|
634
|
+
}
|
|
635
|
+
items.length = 0;
|
|
636
|
+
mapped = [];
|
|
637
|
+
indexes.length = 0;
|
|
638
|
+
}
|
|
639
|
+
return mapped;
|
|
640
|
+
}
|
|
641
|
+
if (len === 0) {
|
|
642
|
+
for (j = 0; j < newLen; j++) {
|
|
643
|
+
item = newItems[j];
|
|
644
|
+
items[j] = item;
|
|
645
|
+
const indexSignal = signal(j);
|
|
646
|
+
newIndexes[j] = indexSignal;
|
|
647
|
+
newMapped[j] = mapFn(item, indexSignal);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
newIndices.clear();
|
|
652
|
+
temp.length = 0;
|
|
653
|
+
tempIndexes.length = 0;
|
|
654
|
+
newIndicesNext.length = 0;
|
|
655
|
+
for (start = 0, end = Math.min(len, newLen); start < end && getKey(items[start]) === getKey(newItems[start]); start++) {
|
|
656
|
+
newMapped[start] = mapped[start];
|
|
657
|
+
newIndexes[start] = indexes[start];
|
|
658
|
+
}
|
|
659
|
+
for (end = len - 1, newEnd = newLen - 1; end >= start &&
|
|
660
|
+
newEnd >= start &&
|
|
661
|
+
getKey(items[end]) === getKey(newItems[newEnd]); end--, newEnd--) {
|
|
662
|
+
temp[newEnd] = mapped[end];
|
|
663
|
+
tempIndexes[newEnd] = indexes[end];
|
|
664
|
+
}
|
|
665
|
+
for (j = newEnd; j >= start; j--) {
|
|
666
|
+
item = newItems[j];
|
|
667
|
+
key = getKey(item);
|
|
668
|
+
i = newIndices.get(key);
|
|
669
|
+
newIndicesNext[j] = i === undefined ? -1 : i;
|
|
670
|
+
newIndices.set(key, j);
|
|
671
|
+
}
|
|
672
|
+
for (i = start; i <= end; i++) {
|
|
673
|
+
item = items[i];
|
|
674
|
+
key = getKey(item);
|
|
675
|
+
j = newIndices.get(key);
|
|
676
|
+
if (j !== undefined && j !== -1) {
|
|
677
|
+
temp[j] = mapped[i];
|
|
678
|
+
tempIndexes[j] = indexes[i];
|
|
679
|
+
j = newIndicesNext[j];
|
|
680
|
+
newIndices.set(key, j);
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
if (options.onDestroy)
|
|
684
|
+
options.onDestroy(mapped[i]);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
// 2) Set all new values
|
|
688
|
+
for (j = start; j < newLen; j++) {
|
|
689
|
+
if (temp[j] !== undefined) {
|
|
690
|
+
newMapped[j] = temp[j];
|
|
691
|
+
newIndexes[j] = tempIndexes[j];
|
|
692
|
+
newIndexes[j].set(j);
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
const indexSignal = signal(j);
|
|
696
|
+
newIndexes[j] = indexSignal;
|
|
697
|
+
newMapped[j] = mapFn(newItems[j], indexSignal);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
items.length = newLen;
|
|
701
|
+
for (let k = 0; k < newLen; k++)
|
|
702
|
+
items[k] = newItems[k];
|
|
703
|
+
}
|
|
704
|
+
mapped = newMapped;
|
|
705
|
+
indexes.length = newLen;
|
|
706
|
+
for (let k = 0; k < newLen; k++)
|
|
707
|
+
indexes[k] = newIndexes[k];
|
|
708
|
+
return mapped;
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function pooledKeys(src) {
|
|
714
|
+
const aBuf = new Set();
|
|
715
|
+
const bBuf = new Set();
|
|
716
|
+
let active = aBuf;
|
|
717
|
+
let spare = bBuf;
|
|
718
|
+
return computed(() => {
|
|
719
|
+
const val = src();
|
|
720
|
+
spare.clear();
|
|
721
|
+
for (const k in val)
|
|
722
|
+
if (Object.prototype.hasOwnProperty.call(val, k))
|
|
723
|
+
spare.add(k);
|
|
724
|
+
if (active.size === spare.size && active.isSubsetOf(spare))
|
|
725
|
+
return active;
|
|
726
|
+
const temp = active;
|
|
727
|
+
active = spare;
|
|
728
|
+
spare = temp;
|
|
729
|
+
return active;
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
function mapObject(source, mapFn, options = {}) {
|
|
733
|
+
const src = isSignal(source) ? source : computed(source);
|
|
734
|
+
const writable = (isWritableSignal(src)
|
|
735
|
+
? src
|
|
736
|
+
: toWritable(src, () => {
|
|
737
|
+
// noop
|
|
738
|
+
})); // maximal overload internally
|
|
739
|
+
return linkedSignal({
|
|
740
|
+
source: pooledKeys(src),
|
|
741
|
+
computation: (next, prev) => {
|
|
742
|
+
const nextObj = {};
|
|
743
|
+
for (const k of next)
|
|
744
|
+
nextObj[k] =
|
|
745
|
+
prev && prev.source.has(k)
|
|
746
|
+
? prev.value[k]
|
|
747
|
+
: mapFn(k, derived(writable, k));
|
|
748
|
+
if (options.onDestroy && prev && prev.source.size)
|
|
749
|
+
for (const k of prev.source)
|
|
750
|
+
if (!next.has(k))
|
|
751
|
+
options.onDestroy(prev.value[k]);
|
|
752
|
+
return nextObj;
|
|
753
|
+
},
|
|
754
|
+
}).asReadonly();
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** Project with optional equality. Pure & sync. */
|
|
758
|
+
const select = (projector, opt) => (src) => computed(() => projector(src()), opt);
|
|
759
|
+
/** Combine with another signal using a projector. */
|
|
760
|
+
const combineWith = (other, project, opt) => (src) => computed(() => project(src(), other()), opt);
|
|
761
|
+
/** Only re-emit when equal(prev, next) is false. */
|
|
762
|
+
const distinct = (equal = Object.is) => (src) => computed(() => src(), { equal });
|
|
763
|
+
/** map to new value */
|
|
764
|
+
const map = (fn) => (src) => computed(() => fn(src()));
|
|
765
|
+
/** filter values, keeping the last value if it was ever available, if first value is filtered will return undefined */
|
|
766
|
+
const filter = (predicate) => (src) => linkedSignal({
|
|
767
|
+
source: src,
|
|
768
|
+
computation: (next, prev) => {
|
|
769
|
+
if (predicate(next))
|
|
770
|
+
return next;
|
|
771
|
+
return prev?.source;
|
|
772
|
+
},
|
|
773
|
+
});
|
|
774
|
+
/** tap into the value */
|
|
775
|
+
const tap = (fn) => (src) => {
|
|
776
|
+
effect(() => fn(src()));
|
|
777
|
+
return src;
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Decorate any `Signal<T>` with a chainable `.pipe(...)` method.
|
|
782
|
+
*
|
|
783
|
+
* @example
|
|
784
|
+
* const s = pipeable(signal(1)); // WritableSignal<number> (+ pipe)
|
|
785
|
+
* const label = s.pipe(n => n * 2, n => `#${n}`); // Signal<string> (+ pipe)
|
|
786
|
+
* label(); // "#2"
|
|
787
|
+
*/
|
|
788
|
+
function pipeable(signal) {
|
|
789
|
+
const internal = signal;
|
|
790
|
+
const mapImpl = (...fns) => {
|
|
791
|
+
const last = fns.at(-1);
|
|
792
|
+
let opt;
|
|
793
|
+
if (last && typeof last !== 'function') {
|
|
794
|
+
fns = fns.slice(0, -1);
|
|
795
|
+
opt = last;
|
|
796
|
+
}
|
|
797
|
+
if (fns.length === 0)
|
|
798
|
+
return internal;
|
|
799
|
+
if (fns.length === 1) {
|
|
800
|
+
const fn = fns[0];
|
|
801
|
+
return pipeable(computed(() => fn(internal()), opt));
|
|
802
|
+
}
|
|
803
|
+
const transformer = (input) => fns.reduce((acc, fn) => fn(acc), input);
|
|
804
|
+
return pipeable(computed(() => transformer(internal()), opt));
|
|
805
|
+
};
|
|
806
|
+
const pipeImpl = (...ops) => {
|
|
807
|
+
if (ops.length === 0)
|
|
808
|
+
return internal;
|
|
809
|
+
return ops.reduce((src, op) => pipeable(op(src)), internal);
|
|
810
|
+
};
|
|
811
|
+
Object.defineProperties(internal, {
|
|
812
|
+
map: {
|
|
813
|
+
value: mapImpl,
|
|
814
|
+
configurable: true,
|
|
815
|
+
enumerable: false,
|
|
816
|
+
writable: false,
|
|
817
|
+
},
|
|
818
|
+
pipe: {
|
|
819
|
+
value: pipeImpl,
|
|
820
|
+
configurable: true,
|
|
821
|
+
enumerable: false,
|
|
822
|
+
writable: false,
|
|
823
|
+
},
|
|
824
|
+
});
|
|
825
|
+
return internal;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Create a new **writable** signal and return it as a `PipableSignal`.
|
|
829
|
+
*
|
|
830
|
+
* The returned value is a `WritableSignal<T>` with `.set`, `.update`, `.asReadonly`
|
|
831
|
+
* still available (via intersection type), plus a chainable `.pipe(...)`.
|
|
832
|
+
*
|
|
833
|
+
* @example
|
|
834
|
+
* const count = piped(1); // WritableSignal<number> (+ pipe)
|
|
835
|
+
* const even = count.pipe(n => n % 2 === 0); // Signal<boolean> (+ pipe)
|
|
836
|
+
* count.update(n => n + 1);
|
|
837
|
+
*/
|
|
838
|
+
function piped(initial, opt) {
|
|
839
|
+
return pipeable(signal(initial, opt));
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function observerSupported$1() {
|
|
843
|
+
return typeof ResizeObserver !== 'undefined';
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Creates a read-only signal that tracks the size of a target DOM element.
|
|
847
|
+
*
|
|
848
|
+
* By default, it observes the `border-box` size to align with `getBoundingClientRect()`,
|
|
849
|
+
* which is used to provide a synchronous initial value if possible.
|
|
850
|
+
*
|
|
851
|
+
* @param target The DOM element (or `ElementRef`, or a `Signal` resolving to one) to observe.
|
|
852
|
+
* @param options Optional configuration including `box` (defaults to 'border-box') and `debugName`.
|
|
853
|
+
* @returns A `Signal<ElementSize | undefined>`.
|
|
854
|
+
*
|
|
855
|
+
* @example
|
|
856
|
+
* ```ts
|
|
857
|
+
* const size = elementSize(elementRef);
|
|
858
|
+
* effect(() => {
|
|
859
|
+
* console.log('Size:', size()?.width, size()?.height);
|
|
860
|
+
* });
|
|
861
|
+
* ```
|
|
862
|
+
*/
|
|
863
|
+
function elementSize(target = inject(ElementRef), opt) {
|
|
864
|
+
const getElement = () => {
|
|
865
|
+
if (isSignal(target)) {
|
|
866
|
+
try {
|
|
867
|
+
const val = target();
|
|
868
|
+
return val instanceof ElementRef ? val.nativeElement : val;
|
|
869
|
+
}
|
|
870
|
+
catch {
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return target instanceof ElementRef ? target.nativeElement : target;
|
|
875
|
+
};
|
|
876
|
+
const resolveInitialValue = () => {
|
|
877
|
+
if (!observerSupported$1())
|
|
878
|
+
return undefined;
|
|
879
|
+
const el = getElement();
|
|
880
|
+
if (el && el.getBoundingClientRect) {
|
|
881
|
+
const rect = el.getBoundingClientRect();
|
|
882
|
+
return { width: rect.width, height: rect.height };
|
|
883
|
+
}
|
|
884
|
+
return undefined;
|
|
885
|
+
};
|
|
886
|
+
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
887
|
+
return computed(() => untracked(resolveInitialValue), {
|
|
888
|
+
debugName: opt?.debugName,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
const state = signal(untracked(resolveInitialValue), {
|
|
892
|
+
debugName: opt?.debugName,
|
|
893
|
+
equal: (a, b) => a?.width === b?.width && a?.height === b?.height,
|
|
894
|
+
});
|
|
895
|
+
const targetSignal = isSignal(target) ? target : computed(() => target);
|
|
896
|
+
effect((cleanup) => {
|
|
897
|
+
const el = targetSignal();
|
|
898
|
+
if (el) {
|
|
899
|
+
const nativeEl = el instanceof ElementRef ? el.nativeElement : el;
|
|
900
|
+
const rect = nativeEl.getBoundingClientRect();
|
|
901
|
+
untracked(() => state.set({ width: rect.width, height: rect.height }));
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
untracked(() => state.set(undefined));
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (!observerSupported$1())
|
|
908
|
+
return;
|
|
909
|
+
let observer = null;
|
|
910
|
+
observer = new ResizeObserver(([entry]) => {
|
|
911
|
+
let width = 0;
|
|
912
|
+
let height = 0;
|
|
913
|
+
const boxOption = opt?.box ?? 'border-box';
|
|
914
|
+
if (boxOption === 'border-box' && entry.borderBoxSize?.length > 0) {
|
|
915
|
+
const size = entry.borderBoxSize[0];
|
|
916
|
+
width = size.inlineSize;
|
|
917
|
+
height = size.blockSize;
|
|
918
|
+
}
|
|
919
|
+
else if (boxOption === 'content-box' &&
|
|
920
|
+
entry.contentBoxSize?.length > 0) {
|
|
921
|
+
width = entry.contentBoxSize[0].inlineSize;
|
|
922
|
+
height = entry.contentBoxSize[0].blockSize;
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
width = entry.contentRect.width;
|
|
926
|
+
height = entry.contentRect.height;
|
|
927
|
+
}
|
|
928
|
+
state.set({ width, height });
|
|
929
|
+
});
|
|
930
|
+
observer.observe(el instanceof ElementRef ? el.nativeElement : el, {
|
|
931
|
+
box: opt?.box ?? 'border-box',
|
|
932
|
+
});
|
|
933
|
+
cleanup(() => {
|
|
934
|
+
observer?.disconnect();
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
return state.asReadonly();
|
|
938
|
+
}
|
|
939
|
+
|
|
204
940
|
function observerSupported() {
|
|
205
941
|
return typeof IntersectionObserver !== 'undefined';
|
|
206
942
|
}
|
|
@@ -303,141 +1039,6 @@ function elementVisibility(target = inject(ElementRef), opt) {
|
|
|
303
1039
|
return base;
|
|
304
1040
|
}
|
|
305
1041
|
|
|
306
|
-
/**
|
|
307
|
-
* Reactively maps items from a source array (or signal of an array) using a provided mapping function.
|
|
308
|
-
*
|
|
309
|
-
* This function serves a similar purpose to SolidJS's `mapArray` by providing stability
|
|
310
|
-
* for mapped items. It receives a source function returning an array (or a Signal<T[]>)
|
|
311
|
-
* and a mapping function.
|
|
312
|
-
*
|
|
313
|
-
* For each item in the source array, it creates a stable `computed` signal representing
|
|
314
|
-
* that item's value at its current index. This stable signal (`Signal<T>`) is passed
|
|
315
|
-
* to the mapping function. This ensures that downstream computations or components
|
|
316
|
-
* depending on the mapped result only re-render or re-calculate for the specific items
|
|
317
|
-
* that have changed, or when items are added/removed, rather than re-evaluating everything
|
|
318
|
-
* when the source array reference changes but items remain the same.
|
|
319
|
-
*
|
|
320
|
-
* It efficiently handles changes in the source array's length by reusing existing mapped
|
|
321
|
-
* results when possible, slicing when the array shrinks, and appending new mapped items
|
|
322
|
-
* when it grows.
|
|
323
|
-
*
|
|
324
|
-
* @template T The type of items in the source array.
|
|
325
|
-
* @template U The type of items in the resulting mapped array.
|
|
326
|
-
*
|
|
327
|
-
* @param source A function returning the source array `T[]`, or a `Signal<T[]>` itself.
|
|
328
|
-
* The `mapArray` function will reactively update based on changes to this source.
|
|
329
|
-
* @param map The mapping function. It is called for each item in the source array.
|
|
330
|
-
* It receives:
|
|
331
|
-
* - `value`: A stable `Signal<T>` representing the item at the current index.
|
|
332
|
-
* Use this signal within your mapping logic if you need reactivity
|
|
333
|
-
* tied to the specific item's value changes.
|
|
334
|
-
* - `index`: The number index of the item in the array.
|
|
335
|
-
* It should return the mapped value `U`.
|
|
336
|
-
* @param [opt] Optional `CreateSignalOptions<T>`. These options are passed directly
|
|
337
|
-
* to the `computed` signal created for each individual item (`Signal<T>`).
|
|
338
|
-
* This allows specifying options like a custom `equal` function for item comparison.
|
|
339
|
-
*
|
|
340
|
-
* @returns A `Signal<U[]>` containing the mapped array. This signal updates whenever
|
|
341
|
-
* the source array changes (either length or the values of its items).
|
|
342
|
-
*
|
|
343
|
-
* @example
|
|
344
|
-
* ```ts
|
|
345
|
-
* const sourceItems = signal([
|
|
346
|
-
* { id: 1, name: 'Apple' },
|
|
347
|
-
* { id: 2, name: 'Banana' }
|
|
348
|
-
* ]);
|
|
349
|
-
*
|
|
350
|
-
* const mappedItems = mapArray(
|
|
351
|
-
* sourceItems,
|
|
352
|
-
* (itemSignal, index) => {
|
|
353
|
-
* // itemSignal is stable for a given item based on its index.
|
|
354
|
-
* // We create a computed here to react to changes in the item's name.
|
|
355
|
-
* return computed(() => `${index}: ${itemSignal().name.toUpperCase()}`);
|
|
356
|
-
* },
|
|
357
|
-
* // Example optional options (e.g., custom equality for item signals)
|
|
358
|
-
* { equal: (a, b) => a.id === b.id && a.name === b.name }
|
|
359
|
-
* );
|
|
360
|
-
* ```
|
|
361
|
-
* @remarks
|
|
362
|
-
* This function achieves its high performance by leveraging the new `linkedSignal`
|
|
363
|
-
* API from Angular, which allows for efficient memoization and reuse of array items.
|
|
364
|
-
*/
|
|
365
|
-
function mapArray(source, map, opt) {
|
|
366
|
-
const data = isSignal(source) ? source : computed(source);
|
|
367
|
-
const len = computed(() => data().length);
|
|
368
|
-
return linkedSignal({
|
|
369
|
-
source: () => len(),
|
|
370
|
-
computation: (len, prev) => {
|
|
371
|
-
if (!prev)
|
|
372
|
-
return Array.from({ length: len }, (_, i) => map(computed(() => source()[i], opt), i));
|
|
373
|
-
if (len === prev.value.length)
|
|
374
|
-
return prev.value;
|
|
375
|
-
if (len < prev.value.length) {
|
|
376
|
-
return prev.value.slice(0, len);
|
|
377
|
-
}
|
|
378
|
-
else {
|
|
379
|
-
const next = [...prev.value];
|
|
380
|
-
for (let i = prev.value.length; i < len; i++) {
|
|
381
|
-
next[i] = map(computed(() => source()[i], opt), i);
|
|
382
|
-
}
|
|
383
|
-
return next;
|
|
384
|
-
}
|
|
385
|
-
},
|
|
386
|
-
equal: (a, b) => a.length === b.length,
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const { is } = Object;
|
|
391
|
-
function mutable(initial, opt) {
|
|
392
|
-
const baseEqual = opt?.equal ?? is;
|
|
393
|
-
let trigger = false;
|
|
394
|
-
const equal = (a, b) => {
|
|
395
|
-
if (trigger)
|
|
396
|
-
return false;
|
|
397
|
-
return baseEqual(a, b);
|
|
398
|
-
};
|
|
399
|
-
const sig = signal(initial, {
|
|
400
|
-
...opt,
|
|
401
|
-
equal,
|
|
402
|
-
});
|
|
403
|
-
const internalUpdate = sig.update;
|
|
404
|
-
sig.mutate = (updater) => {
|
|
405
|
-
trigger = true;
|
|
406
|
-
internalUpdate(updater);
|
|
407
|
-
trigger = false;
|
|
408
|
-
};
|
|
409
|
-
sig.inline = (updater) => {
|
|
410
|
-
sig.mutate((prev) => {
|
|
411
|
-
updater(prev);
|
|
412
|
-
return prev;
|
|
413
|
-
});
|
|
414
|
-
};
|
|
415
|
-
return sig;
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* Type guard function to check if a given `WritableSignal` is a `MutableSignal`. This is useful
|
|
419
|
-
* for situations where you need to conditionally use the `mutate` or `inline` methods.
|
|
420
|
-
*
|
|
421
|
-
* @typeParam T - The type of the signal's value (optional, defaults to `any`).
|
|
422
|
-
* @param value - The `WritableSignal` to check.
|
|
423
|
-
* @returns `true` if the signal is a `MutableSignal`, `false` otherwise.
|
|
424
|
-
*
|
|
425
|
-
* @example
|
|
426
|
-
* const mySignal = signal(0);
|
|
427
|
-
* const myMutableSignal = mutable(0);
|
|
428
|
-
*
|
|
429
|
-
* if (isMutable(mySignal)) {
|
|
430
|
-
* mySignal.mutate(x => x + 1); // This would cause a type error, as mySignal is not a MutableSignal.
|
|
431
|
-
* }
|
|
432
|
-
*
|
|
433
|
-
* if (isMutable(myMutableSignal)) {
|
|
434
|
-
* myMutableSignal.mutate(x => x + 1); // This is safe.
|
|
435
|
-
* }
|
|
436
|
-
*/
|
|
437
|
-
function isMutable(value) {
|
|
438
|
-
return 'mutate' in value && typeof value.mutate === 'function';
|
|
439
|
-
}
|
|
440
|
-
|
|
441
1042
|
/**
|
|
442
1043
|
* Creates a read-only signal that reactively tracks whether a CSS media query
|
|
443
1044
|
* string currently matches.
|
|
@@ -482,11 +1083,11 @@ function isMutable(value) {
|
|
|
482
1083
|
* }
|
|
483
1084
|
* ```
|
|
484
1085
|
*/
|
|
485
|
-
function mediaQuery(query, debugName) {
|
|
1086
|
+
function mediaQuery(query, debugName = 'mediaQuery') {
|
|
486
1087
|
if (isPlatformServer(inject(PLATFORM_ID)))
|
|
487
1088
|
return computed(() => false, { debugName });
|
|
488
1089
|
const mediaQueryList = window.matchMedia(query);
|
|
489
|
-
const state = signal(mediaQueryList.matches, { debugName });
|
|
1090
|
+
const state = signal(mediaQueryList.matches, { debugName: debugName });
|
|
490
1091
|
const handleChange = (event) => {
|
|
491
1092
|
state.set(event.matches);
|
|
492
1093
|
};
|
|
@@ -609,8 +1210,7 @@ function throttle(source, opt) {
|
|
|
609
1210
|
catch {
|
|
610
1211
|
// not in injection context & no destroyRef provided opting out of cleanup
|
|
611
1212
|
}
|
|
612
|
-
const
|
|
613
|
-
updateSourceAction();
|
|
1213
|
+
const tick = () => {
|
|
614
1214
|
if (timeout)
|
|
615
1215
|
return;
|
|
616
1216
|
timeout = setTimeout(() => {
|
|
@@ -619,10 +1219,12 @@ function throttle(source, opt) {
|
|
|
619
1219
|
}, ms);
|
|
620
1220
|
};
|
|
621
1221
|
const set = (value) => {
|
|
622
|
-
|
|
1222
|
+
source.set(value);
|
|
1223
|
+
tick();
|
|
623
1224
|
};
|
|
624
1225
|
const update = (fn) => {
|
|
625
|
-
|
|
1226
|
+
source.update(fn);
|
|
1227
|
+
tick();
|
|
626
1228
|
};
|
|
627
1229
|
const writable = toWritable(computed(() => {
|
|
628
1230
|
trigger();
|
|
@@ -670,12 +1272,12 @@ function mousePosition(opt) {
|
|
|
670
1272
|
x: 0,
|
|
671
1273
|
y: 0,
|
|
672
1274
|
}), {
|
|
673
|
-
debugName: opt?.debugName,
|
|
1275
|
+
debugName: opt?.debugName ?? 'mousePosition',
|
|
674
1276
|
});
|
|
675
1277
|
base.unthrottled = base;
|
|
676
1278
|
return base;
|
|
677
1279
|
}
|
|
678
|
-
const { target = window, coordinateSpace = 'client', touch = false, debugName, throttle = 100, } = opt ?? {};
|
|
1280
|
+
const { target = window, coordinateSpace = 'client', touch = false, debugName = 'mousePosition', throttle = 100, } = opt ?? {};
|
|
679
1281
|
const eventTarget = target instanceof ElementRef ? target.nativeElement : target;
|
|
680
1282
|
if (!eventTarget) {
|
|
681
1283
|
if (isDevMode())
|
|
@@ -736,7 +1338,7 @@ const serverDate = new Date();
|
|
|
736
1338
|
* @param debugName Optional debug name for the signal.
|
|
737
1339
|
* @returns A `NetworkStatusSignal` instance.
|
|
738
1340
|
*/
|
|
739
|
-
function networkStatus(debugName) {
|
|
1341
|
+
function networkStatus(debugName = 'networkStatus') {
|
|
740
1342
|
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
741
1343
|
const sig = computed(() => true, {
|
|
742
1344
|
debugName,
|
|
@@ -803,7 +1405,7 @@ function networkStatus(debugName) {
|
|
|
803
1405
|
* }
|
|
804
1406
|
* ```
|
|
805
1407
|
*/
|
|
806
|
-
function pageVisibility(debugName) {
|
|
1408
|
+
function pageVisibility(debugName = 'pageVisibility') {
|
|
807
1409
|
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
808
1410
|
return computed(() => 'visible', { debugName });
|
|
809
1411
|
}
|
|
@@ -865,12 +1467,12 @@ function scrollPosition(opt) {
|
|
|
865
1467
|
x: 0,
|
|
866
1468
|
y: 0,
|
|
867
1469
|
}), {
|
|
868
|
-
debugName: opt?.debugName,
|
|
1470
|
+
debugName: opt?.debugName ?? 'scrollPosition',
|
|
869
1471
|
});
|
|
870
1472
|
base.unthrottled = base;
|
|
871
1473
|
return base;
|
|
872
1474
|
}
|
|
873
|
-
const { target = window, throttle = 100, debugName } = opt || {};
|
|
1475
|
+
const { target = window, throttle = 100, debugName = 'scrollPosition', } = opt || {};
|
|
874
1476
|
let element;
|
|
875
1477
|
let getScrollPosition;
|
|
876
1478
|
if (target instanceof Window) {
|
|
@@ -956,12 +1558,12 @@ function windowSize(opt) {
|
|
|
956
1558
|
const base = computed(() => ({
|
|
957
1559
|
width: 1024,
|
|
958
1560
|
height: 768,
|
|
959
|
-
}), { debugName: opt?.debugName });
|
|
1561
|
+
}), { debugName: opt?.debugName ?? 'windowSize' });
|
|
960
1562
|
base.unthrottled = base;
|
|
961
1563
|
return base;
|
|
962
1564
|
}
|
|
963
1565
|
const sizeSignal = throttled({ width: window.innerWidth, height: window.innerHeight }, {
|
|
964
|
-
debugName: opt?.debugName,
|
|
1566
|
+
debugName: opt?.debugName ?? 'windowSize',
|
|
965
1567
|
equal: (a, b) => a.width === b.width && a.height === b.height,
|
|
966
1568
|
ms: opt?.throttle ?? 100,
|
|
967
1569
|
});
|
|
@@ -990,18 +1592,303 @@ function sensor(type, options) {
|
|
|
990
1592
|
return networkStatus(options?.debugName);
|
|
991
1593
|
case 'pageVisibility':
|
|
992
1594
|
return pageVisibility(options?.debugName);
|
|
1595
|
+
case 'darkMode':
|
|
993
1596
|
case 'dark-mode':
|
|
994
1597
|
return prefersDarkMode(options?.debugName);
|
|
1598
|
+
case 'reducedMotion':
|
|
995
1599
|
case 'reduced-motion':
|
|
996
1600
|
return prefersReducedMotion(options?.debugName);
|
|
1601
|
+
case 'mediaQuery': {
|
|
1602
|
+
const opt = options;
|
|
1603
|
+
return mediaQuery(opt.query, opt.debugName);
|
|
1604
|
+
}
|
|
997
1605
|
case 'windowSize':
|
|
998
1606
|
return windowSize(options);
|
|
999
1607
|
case 'scrollPosition':
|
|
1000
1608
|
return scrollPosition(options);
|
|
1609
|
+
case 'elementVisibility': {
|
|
1610
|
+
const opt = options;
|
|
1611
|
+
return elementVisibility(opt.target, opt);
|
|
1612
|
+
}
|
|
1613
|
+
case 'elementSize': {
|
|
1614
|
+
const opt = options;
|
|
1615
|
+
return elementSize(opt.target, opt);
|
|
1616
|
+
}
|
|
1001
1617
|
default:
|
|
1002
1618
|
throw new Error(`Unknown sensor type: ${type}`);
|
|
1003
1619
|
}
|
|
1004
1620
|
}
|
|
1621
|
+
function sensors(track, opt) {
|
|
1622
|
+
return track.reduce((result, key) => {
|
|
1623
|
+
result[key] = sensor(key, opt?.[key]);
|
|
1624
|
+
return result;
|
|
1625
|
+
}, {});
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
const IS_STORE = Symbol('MMSTACK::IS_STORE');
|
|
1629
|
+
const PROXY_CACHE = new WeakMap();
|
|
1630
|
+
const SIGNAL_FN_PROP = new Set([
|
|
1631
|
+
'set',
|
|
1632
|
+
'update',
|
|
1633
|
+
'mutate',
|
|
1634
|
+
'inline',
|
|
1635
|
+
'asReadonly',
|
|
1636
|
+
]);
|
|
1637
|
+
const PROXY_CLEANUP = new FinalizationRegistry(({ target, prop }) => {
|
|
1638
|
+
const storeCache = PROXY_CACHE.get(target);
|
|
1639
|
+
if (storeCache)
|
|
1640
|
+
storeCache.delete(prop);
|
|
1641
|
+
});
|
|
1642
|
+
/**
|
|
1643
|
+
* @internal
|
|
1644
|
+
* Validates whether a value is a Signal Store.
|
|
1645
|
+
*/
|
|
1646
|
+
function isStore(value) {
|
|
1647
|
+
return (typeof value === 'function' &&
|
|
1648
|
+
value !== null &&
|
|
1649
|
+
value[IS_STORE] === true);
|
|
1650
|
+
}
|
|
1651
|
+
function isIndexProp(prop) {
|
|
1652
|
+
return typeof prop === 'string' && prop.trim() !== '' && !isNaN(+prop);
|
|
1653
|
+
}
|
|
1654
|
+
function isRecord(value) {
|
|
1655
|
+
if (value === null || typeof value !== 'object')
|
|
1656
|
+
return false;
|
|
1657
|
+
const proto = Object.getPrototypeOf(value);
|
|
1658
|
+
return proto === Object.prototype || proto === null;
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* @internal
|
|
1662
|
+
* Makes an array store
|
|
1663
|
+
*/
|
|
1664
|
+
function toArrayStore(source, injector) {
|
|
1665
|
+
if (isStore(source))
|
|
1666
|
+
return source;
|
|
1667
|
+
const isMutableSource = isMutable(source);
|
|
1668
|
+
const lengthSignal = computed(() => {
|
|
1669
|
+
const v = source();
|
|
1670
|
+
if (!Array.isArray(v))
|
|
1671
|
+
return 0;
|
|
1672
|
+
return v.length;
|
|
1673
|
+
});
|
|
1674
|
+
return new Proxy(source, {
|
|
1675
|
+
has(_, prop) {
|
|
1676
|
+
if (prop === 'length')
|
|
1677
|
+
return true;
|
|
1678
|
+
if (isIndexProp(prop)) {
|
|
1679
|
+
const idx = +prop;
|
|
1680
|
+
return idx >= 0 && idx < untracked(lengthSignal);
|
|
1681
|
+
}
|
|
1682
|
+
return Reflect.has(untracked(source), prop);
|
|
1683
|
+
},
|
|
1684
|
+
ownKeys() {
|
|
1685
|
+
const v = untracked(source);
|
|
1686
|
+
if (!Array.isArray(v))
|
|
1687
|
+
return [];
|
|
1688
|
+
const len = v.length;
|
|
1689
|
+
const arr = new Array(len + 1);
|
|
1690
|
+
for (let i = 0; i < len; i++) {
|
|
1691
|
+
arr[i] = String(i);
|
|
1692
|
+
}
|
|
1693
|
+
arr[len] = 'length';
|
|
1694
|
+
return arr;
|
|
1695
|
+
},
|
|
1696
|
+
getPrototypeOf() {
|
|
1697
|
+
return Array.prototype;
|
|
1698
|
+
},
|
|
1699
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
1700
|
+
const v = untracked(source);
|
|
1701
|
+
if (!Array.isArray(v))
|
|
1702
|
+
return;
|
|
1703
|
+
if (prop === 'length' ||
|
|
1704
|
+
(typeof prop === 'string' && !isNaN(+prop) && +prop < v.length)) {
|
|
1705
|
+
return {
|
|
1706
|
+
enumerable: true,
|
|
1707
|
+
configurable: true, // Required for proxies to dynamic targets
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
return;
|
|
1711
|
+
},
|
|
1712
|
+
get(target, prop, receiver) {
|
|
1713
|
+
if (prop === IS_STORE)
|
|
1714
|
+
return true;
|
|
1715
|
+
if (prop === 'length')
|
|
1716
|
+
return lengthSignal;
|
|
1717
|
+
if (prop === Symbol.iterator) {
|
|
1718
|
+
return function* () {
|
|
1719
|
+
for (let i = 0; i < untracked(lengthSignal); i++) {
|
|
1720
|
+
yield receiver[i];
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
|
|
1725
|
+
return target[prop];
|
|
1726
|
+
if (isIndexProp(prop)) {
|
|
1727
|
+
const idx = +prop;
|
|
1728
|
+
let storeCache = PROXY_CACHE.get(target);
|
|
1729
|
+
if (!storeCache) {
|
|
1730
|
+
storeCache = new Map();
|
|
1731
|
+
PROXY_CACHE.set(target, storeCache);
|
|
1732
|
+
}
|
|
1733
|
+
const cachedRef = storeCache.get(idx);
|
|
1734
|
+
if (cachedRef) {
|
|
1735
|
+
const cached = cachedRef.deref();
|
|
1736
|
+
if (cached)
|
|
1737
|
+
return cached;
|
|
1738
|
+
storeCache.delete(idx);
|
|
1739
|
+
PROXY_CLEANUP.unregister(cachedRef);
|
|
1740
|
+
}
|
|
1741
|
+
const value = untracked(target);
|
|
1742
|
+
const valueIsArray = Array.isArray(value);
|
|
1743
|
+
const valueIsRecord = isRecord(value);
|
|
1744
|
+
const equalFn = (valueIsRecord || valueIsArray) &&
|
|
1745
|
+
isMutableSource &&
|
|
1746
|
+
typeof value[idx] === 'object'
|
|
1747
|
+
? () => false
|
|
1748
|
+
: undefined;
|
|
1749
|
+
const computation = valueIsRecord
|
|
1750
|
+
? derived(target, idx, { equal: equalFn })
|
|
1751
|
+
: derived(target, {
|
|
1752
|
+
from: (v) => v?.[idx],
|
|
1753
|
+
onChange: (newValue) => target.update((v) => {
|
|
1754
|
+
if (v === null || v === undefined)
|
|
1755
|
+
return v;
|
|
1756
|
+
try {
|
|
1757
|
+
v[idx] = newValue;
|
|
1758
|
+
}
|
|
1759
|
+
catch (e) {
|
|
1760
|
+
if (isDevMode())
|
|
1761
|
+
console.error(`[store] Failed to set property "${String(idx)}"`, e);
|
|
1762
|
+
}
|
|
1763
|
+
return v;
|
|
1764
|
+
}),
|
|
1765
|
+
});
|
|
1766
|
+
const proxy = Array.isArray(untracked(computation))
|
|
1767
|
+
? toArrayStore(computation, injector)
|
|
1768
|
+
: toStore(computation, injector);
|
|
1769
|
+
const ref = new WeakRef(proxy);
|
|
1770
|
+
storeCache.set(idx, ref);
|
|
1771
|
+
PROXY_CLEANUP.register(proxy, { target, prop: idx }, ref);
|
|
1772
|
+
return proxy;
|
|
1773
|
+
}
|
|
1774
|
+
return Reflect.get(target, prop, receiver);
|
|
1775
|
+
},
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Converts a Signal into a deep-observable Store.
|
|
1780
|
+
* Accessing nested properties returns a derived Signal of that path.
|
|
1781
|
+
* @example
|
|
1782
|
+
* const state = store({ user: { name: 'John' } });
|
|
1783
|
+
* const nameSignal = state.user.name; // WritableSignal<string>
|
|
1784
|
+
*/
|
|
1785
|
+
function toStore(source, injector) {
|
|
1786
|
+
if (isStore(source))
|
|
1787
|
+
return source;
|
|
1788
|
+
if (!injector)
|
|
1789
|
+
injector = inject(Injector);
|
|
1790
|
+
const writableSource = isWritableSignal(source)
|
|
1791
|
+
? source
|
|
1792
|
+
: toWritable(source, () => {
|
|
1793
|
+
// noop
|
|
1794
|
+
});
|
|
1795
|
+
const isMutableSource = isMutable(writableSource);
|
|
1796
|
+
const s = new Proxy(writableSource, {
|
|
1797
|
+
has(_, prop) {
|
|
1798
|
+
return Reflect.has(untracked(source), prop);
|
|
1799
|
+
},
|
|
1800
|
+
ownKeys() {
|
|
1801
|
+
const v = untracked(source);
|
|
1802
|
+
if (!isRecord(v))
|
|
1803
|
+
return [];
|
|
1804
|
+
return Reflect.ownKeys(v);
|
|
1805
|
+
},
|
|
1806
|
+
getPrototypeOf() {
|
|
1807
|
+
return Object.getPrototypeOf(untracked(source));
|
|
1808
|
+
},
|
|
1809
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
1810
|
+
const value = untracked(source);
|
|
1811
|
+
if (!isRecord(value) || !(prop in value))
|
|
1812
|
+
return;
|
|
1813
|
+
return {
|
|
1814
|
+
enumerable: true,
|
|
1815
|
+
configurable: true,
|
|
1816
|
+
};
|
|
1817
|
+
},
|
|
1818
|
+
get(target, prop) {
|
|
1819
|
+
if (prop === IS_STORE)
|
|
1820
|
+
return true;
|
|
1821
|
+
if (prop === 'asReadonlyStore')
|
|
1822
|
+
return () => {
|
|
1823
|
+
if (!isWritableSignal(source))
|
|
1824
|
+
return s;
|
|
1825
|
+
return untracked(() => toStore(source.asReadonly(), injector));
|
|
1826
|
+
};
|
|
1827
|
+
if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
|
|
1828
|
+
return target[prop];
|
|
1829
|
+
let storeCache = PROXY_CACHE.get(target);
|
|
1830
|
+
if (!storeCache) {
|
|
1831
|
+
storeCache = new Map();
|
|
1832
|
+
PROXY_CACHE.set(target, storeCache);
|
|
1833
|
+
}
|
|
1834
|
+
const cachedRef = storeCache.get(prop);
|
|
1835
|
+
if (cachedRef) {
|
|
1836
|
+
const cached = cachedRef.deref();
|
|
1837
|
+
if (cached)
|
|
1838
|
+
return cached;
|
|
1839
|
+
storeCache.delete(prop);
|
|
1840
|
+
PROXY_CLEANUP.unregister(cachedRef);
|
|
1841
|
+
}
|
|
1842
|
+
const value = untracked(target);
|
|
1843
|
+
const valueIsRecord = isRecord(value);
|
|
1844
|
+
const valueIsArray = Array.isArray(value);
|
|
1845
|
+
const equalFn = (valueIsRecord || valueIsArray) &&
|
|
1846
|
+
isMutableSource &&
|
|
1847
|
+
typeof value[prop] === 'object'
|
|
1848
|
+
? () => false
|
|
1849
|
+
: undefined;
|
|
1850
|
+
const computation = valueIsRecord
|
|
1851
|
+
? derived(target, prop, { equal: equalFn })
|
|
1852
|
+
: derived(target, {
|
|
1853
|
+
from: (v) => v?.[prop],
|
|
1854
|
+
onChange: (newValue) => target.update((v) => {
|
|
1855
|
+
if (v === null || v === undefined)
|
|
1856
|
+
return v;
|
|
1857
|
+
try {
|
|
1858
|
+
v[prop] = newValue;
|
|
1859
|
+
}
|
|
1860
|
+
catch (e) {
|
|
1861
|
+
if (isDevMode())
|
|
1862
|
+
console.error(`[store] Failed to set property "${String(prop)}"`, e);
|
|
1863
|
+
}
|
|
1864
|
+
return v;
|
|
1865
|
+
}),
|
|
1866
|
+
});
|
|
1867
|
+
const proxy = Array.isArray(untracked(computation))
|
|
1868
|
+
? toArrayStore(computation, injector)
|
|
1869
|
+
: toStore(computation, injector);
|
|
1870
|
+
const ref = new WeakRef(proxy);
|
|
1871
|
+
storeCache.set(prop, ref);
|
|
1872
|
+
PROXY_CLEANUP.register(proxy, { target, prop }, ref);
|
|
1873
|
+
return proxy;
|
|
1874
|
+
},
|
|
1875
|
+
});
|
|
1876
|
+
return s;
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Creates a WritableSignalStore from a value.
|
|
1880
|
+
* @see {@link toStore}
|
|
1881
|
+
*/
|
|
1882
|
+
function store(value, opt) {
|
|
1883
|
+
return toStore(signal(value, opt), opt?.injector);
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Creates a MutableSignalStore from a value.
|
|
1887
|
+
* @see {@link toStore}
|
|
1888
|
+
*/
|
|
1889
|
+
function mutableStore(value, opt) {
|
|
1890
|
+
return toStore(mutable(value, opt), opt?.injector);
|
|
1891
|
+
}
|
|
1005
1892
|
|
|
1006
1893
|
// Internal dummy store for server-side rendering
|
|
1007
1894
|
const noopStore = {
|
|
@@ -1064,7 +1951,7 @@ const noopStore = {
|
|
|
1064
1951
|
* }
|
|
1065
1952
|
* ```
|
|
1066
1953
|
*/
|
|
1067
|
-
function stored(fallback, { key, store: providedStore, serialize = JSON.stringify, deserialize = JSON.parse, syncTabs = false, equal = Object.is, onKeyChange = 'load', cleanupOldKey = false, ...rest }) {
|
|
1954
|
+
function stored(fallback, { key, store: providedStore, serialize = JSON.stringify, deserialize = JSON.parse, syncTabs = false, equal = Object.is, onKeyChange = 'load', cleanupOldKey = false, validate = () => true, ...rest }) {
|
|
1068
1955
|
const isServer = isPlatformServer(inject(PLATFORM_ID));
|
|
1069
1956
|
const fallbackStore = isServer ? noopStore : localStorage;
|
|
1070
1957
|
const store = providedStore ?? fallbackStore;
|
|
@@ -1078,7 +1965,10 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
1078
1965
|
if (found === null)
|
|
1079
1966
|
return null;
|
|
1080
1967
|
try {
|
|
1081
|
-
|
|
1968
|
+
const deserialized = deserialize(found);
|
|
1969
|
+
if (!validate(deserialized))
|
|
1970
|
+
return null;
|
|
1971
|
+
return deserialized;
|
|
1082
1972
|
}
|
|
1083
1973
|
catch (err) {
|
|
1084
1974
|
if (isDevMode())
|
|
@@ -1162,41 +2052,122 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
1162
2052
|
return writable;
|
|
1163
2053
|
}
|
|
1164
2054
|
|
|
2055
|
+
class MessageBus {
|
|
2056
|
+
channel = new BroadcastChannel('mmstack-tab-sync-bus');
|
|
2057
|
+
listeners = new Map();
|
|
2058
|
+
subscribe(id, listener) {
|
|
2059
|
+
this.unsubscribe(id); // Ensure no duplicate listeners
|
|
2060
|
+
const wrapped = (ev) => {
|
|
2061
|
+
try {
|
|
2062
|
+
if (ev.data?.id === id)
|
|
2063
|
+
listener(ev.data?.value);
|
|
2064
|
+
}
|
|
2065
|
+
catch {
|
|
2066
|
+
// noop
|
|
2067
|
+
}
|
|
2068
|
+
};
|
|
2069
|
+
this.channel.addEventListener('message', wrapped);
|
|
2070
|
+
this.listeners.set(id, wrapped);
|
|
2071
|
+
return {
|
|
2072
|
+
unsub: (() => this.unsubscribe(id)).bind(this),
|
|
2073
|
+
post: ((value) => this.channel.postMessage({ id, value })).bind(this),
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
unsubscribe(id) {
|
|
2077
|
+
const listener = this.listeners.get(id);
|
|
2078
|
+
if (!listener)
|
|
2079
|
+
return;
|
|
2080
|
+
this.channel.removeEventListener('message', listener);
|
|
2081
|
+
this.listeners.delete(id);
|
|
2082
|
+
}
|
|
2083
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: MessageBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2084
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: MessageBus, providedIn: 'root' });
|
|
2085
|
+
}
|
|
2086
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: MessageBus, decorators: [{
|
|
2087
|
+
type: Injectable,
|
|
2088
|
+
args: [{
|
|
2089
|
+
providedIn: 'root',
|
|
2090
|
+
}]
|
|
2091
|
+
}] });
|
|
2092
|
+
function generateDeterministicID() {
|
|
2093
|
+
const stack = new Error().stack;
|
|
2094
|
+
if (stack) {
|
|
2095
|
+
// Look for the actual caller (first non-internal frame)
|
|
2096
|
+
const lines = stack.split('\n');
|
|
2097
|
+
for (let i = 2; i < lines.length; i++) {
|
|
2098
|
+
const line = lines[i];
|
|
2099
|
+
if (line && !line.includes('tabSync') && !line.includes('MessageBus')) {
|
|
2100
|
+
let hash = 0;
|
|
2101
|
+
for (let j = 0; j < line.length; j++) {
|
|
2102
|
+
const char = line.charCodeAt(j);
|
|
2103
|
+
hash = (hash << 5) - hash + char;
|
|
2104
|
+
hash = hash & hash;
|
|
2105
|
+
}
|
|
2106
|
+
return `auto-${Math.abs(hash)}`;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
throw new Error('Could not generate deterministic ID, please provide one manually.');
|
|
2111
|
+
}
|
|
1165
2112
|
/**
|
|
1166
|
-
*
|
|
2113
|
+
* Synchronizes a WritableSignal across browser tabs using BroadcastChannel API.
|
|
1167
2114
|
*
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
2115
|
+
* Creates a shared signal that automatically syncs its value between all tabs
|
|
2116
|
+
* of the same application. When the signal is updated in one tab, all other
|
|
2117
|
+
* tabs will receive the new value automatically.
|
|
1170
2118
|
*
|
|
1171
|
-
* @template T The type of the
|
|
1172
|
-
* @param
|
|
1173
|
-
* @param
|
|
1174
|
-
* @param
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
2119
|
+
* @template T - The type of the WritableSignal
|
|
2120
|
+
* @param sig - The WritableSignal to synchronize across tabs
|
|
2121
|
+
* @param opt - Optional configuration object
|
|
2122
|
+
* @param opt.id - Explicit channel ID for synchronization. If not provided,
|
|
2123
|
+
* a deterministic ID is generated based on the call site.
|
|
2124
|
+
* Use explicit IDs in production for reliability.
|
|
2125
|
+
*
|
|
2126
|
+
* @returns The same WritableSignal instance, now synchronized across tabs
|
|
2127
|
+
*
|
|
2128
|
+
* @throws {Error} When deterministic ID generation fails and no explicit ID is provided
|
|
1177
2129
|
*
|
|
1178
2130
|
* @example
|
|
1179
|
-
* ```
|
|
1180
|
-
*
|
|
1181
|
-
*
|
|
1182
|
-
* async function waitForCount() {
|
|
1183
|
-
* console.log('Waiting for count to be >= 3...');
|
|
1184
|
-
* try {
|
|
1185
|
-
* const finalCount = await until(count, c => c >= 3, { timeout: 5000 });
|
|
1186
|
-
* console.log(`Count reached: ${finalCount}`);
|
|
1187
|
-
* } catch (e: any) { // Ensure 'e' is typed if you access properties like e.message
|
|
1188
|
-
* console.error(e.message); // e.g., "until: Timeout after 5000ms."
|
|
1189
|
-
* }
|
|
1190
|
-
* }
|
|
2131
|
+
* ```typescript
|
|
2132
|
+
* // Basic usage - auto-generates channel ID from call site
|
|
2133
|
+
* const theme = tabSync(signal('dark'));
|
|
1191
2134
|
*
|
|
1192
|
-
* //
|
|
1193
|
-
*
|
|
1194
|
-
* setTimeout(() => count.set(2), 1000);
|
|
1195
|
-
* setTimeout(() => count.set(3), 1500);
|
|
2135
|
+
* // With explicit ID (recommended for production)
|
|
2136
|
+
* const userPrefs = tabSync(signal({ lang: 'en' }), { id: 'user-preferences' });
|
|
1196
2137
|
*
|
|
1197
|
-
*
|
|
2138
|
+
* // Changes in one tab will sync to all other tabs
|
|
2139
|
+
* theme.set('light'); // All tabs will update to 'light'
|
|
1198
2140
|
* ```
|
|
2141
|
+
*
|
|
2142
|
+
* @remarks
|
|
2143
|
+
* - Only works in browser environments (returns original signal on server)
|
|
2144
|
+
* - Uses a single BroadcastChannel for all synchronized signals
|
|
2145
|
+
* - Automatically cleans up listeners when the injection context is destroyed
|
|
2146
|
+
* - Initial signal value after sync setup is not broadcasted to prevent loops
|
|
2147
|
+
*
|
|
1199
2148
|
*/
|
|
2149
|
+
function tabSync(sig, opt) {
|
|
2150
|
+
if (isPlatformServer(inject(PLATFORM_ID)))
|
|
2151
|
+
return sig;
|
|
2152
|
+
const id = opt?.id || generateDeterministicID();
|
|
2153
|
+
const bus = inject(MessageBus);
|
|
2154
|
+
const { unsub, post } = bus.subscribe(id, (next) => sig.set(next));
|
|
2155
|
+
let first = false;
|
|
2156
|
+
const effectRef = effect(() => {
|
|
2157
|
+
const val = sig();
|
|
2158
|
+
if (!first) {
|
|
2159
|
+
first = true;
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
post(val);
|
|
2163
|
+
});
|
|
2164
|
+
inject(DestroyRef).onDestroy(() => {
|
|
2165
|
+
effectRef.destroy();
|
|
2166
|
+
unsub();
|
|
2167
|
+
});
|
|
2168
|
+
return sig;
|
|
2169
|
+
}
|
|
2170
|
+
|
|
1200
2171
|
function until(sourceSignal, predicate, options = {}) {
|
|
1201
2172
|
const injector = options.injector ?? inject(Injector);
|
|
1202
2173
|
return new Promise((resolve, reject) => {
|
|
@@ -1352,6 +2323,8 @@ function withHistory(source, opt) {
|
|
|
1352
2323
|
return;
|
|
1353
2324
|
const valueForRedo = untracked(source);
|
|
1354
2325
|
const valueToRestore = historyStack.at(-1);
|
|
2326
|
+
if (valueToRestore === undefined)
|
|
2327
|
+
return;
|
|
1355
2328
|
originalSet.call(source, valueToRestore);
|
|
1356
2329
|
history.inline((h) => h.pop());
|
|
1357
2330
|
redoArray.inline((r) => r.push(valueForRedo));
|
|
@@ -1362,6 +2335,8 @@ function withHistory(source, opt) {
|
|
|
1362
2335
|
return;
|
|
1363
2336
|
const valueForUndo = untracked(source);
|
|
1364
2337
|
const valueToRestore = redoStack.at(-1);
|
|
2338
|
+
if (valueToRestore === undefined)
|
|
2339
|
+
return;
|
|
1365
2340
|
originalSet.call(source, valueToRestore);
|
|
1366
2341
|
redoArray.inline((r) => r.pop());
|
|
1367
2342
|
history.mutate((h) => {
|
|
@@ -1391,5 +2366,5 @@ function withHistory(source, opt) {
|
|
|
1391
2366
|
* Generated bundle index. Do not edit.
|
|
1392
2367
|
*/
|
|
1393
2368
|
|
|
1394
|
-
export { debounce, debounced, derived, elementVisibility, isDerivation, isMutable, mapArray, mediaQuery, mousePosition, mutable, networkStatus, pageVisibility, prefersDarkMode, prefersReducedMotion, scrollPosition, sensor, stored, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toWritable, until, windowSize, withHistory };
|
|
2369
|
+
export { chunked, combineWith, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, indexArray, isDerivation, isMutable, isStore, keyArray, map, mapArray, mapObject, mediaQuery, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, pageVisibility, pipeable, piped, prefersDarkMode, prefersReducedMotion, scrollPosition, select, sensor, sensors, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
|
|
1395
2370
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|