@mmstack/primitives 19.2.2 → 19.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +1038 -619
- package/fesm2022/mmstack-primitives.mjs +1176 -201
- 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 -2
- 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} +2 -2
- 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,112 @@ 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] = next;
|
|
423
|
+
return cur;
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
: (next) => {
|
|
427
|
+
source.update((cur) => ({ ...cur, [optOrKey]: next }));
|
|
428
|
+
};
|
|
429
|
+
const rest = typeof optOrKey === 'object' ? { ...optOrKey, ...opt } : opt;
|
|
430
|
+
const baseEqual = rest?.equal ?? Object.is;
|
|
431
|
+
let trigger = false;
|
|
432
|
+
const equal = isMutable(source)
|
|
433
|
+
? (a, b) => {
|
|
434
|
+
if (trigger)
|
|
435
|
+
return false;
|
|
436
|
+
return baseEqual(a, b);
|
|
437
|
+
}
|
|
438
|
+
: baseEqual;
|
|
439
|
+
const sig = toWritable(computed(() => from(source()), { ...rest, equal }), (newVal) => onChange(newVal), undefined, { pure: false });
|
|
157
440
|
sig.from = from;
|
|
441
|
+
if (isMutable(source)) {
|
|
442
|
+
sig.mutate = (updater) => {
|
|
443
|
+
trigger = true;
|
|
444
|
+
sig.update(updater);
|
|
445
|
+
trigger = false;
|
|
446
|
+
};
|
|
447
|
+
sig.inline = (updater) => {
|
|
448
|
+
sig.mutate((prev) => {
|
|
449
|
+
updater(prev);
|
|
450
|
+
return prev;
|
|
451
|
+
});
|
|
452
|
+
};
|
|
453
|
+
}
|
|
158
454
|
return sig;
|
|
159
455
|
}
|
|
160
456
|
/**
|
|
@@ -201,6 +497,445 @@ function isDerivation(sig) {
|
|
|
201
497
|
return 'from' in sig;
|
|
202
498
|
}
|
|
203
499
|
|
|
500
|
+
function isWritableSignal(value) {
|
|
501
|
+
return 'set' in value && typeof value.set === 'function';
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* @internal
|
|
505
|
+
* Creates a setter function for a source signal of type `Signal<T[]>` or a function returning `T[]`.
|
|
506
|
+
* @param source The source signal of type `Signal<T[]>` or a function returning `T[]`.
|
|
507
|
+
* @returns
|
|
508
|
+
*/
|
|
509
|
+
function createSetter(source) {
|
|
510
|
+
if (!isWritableSignal(source))
|
|
511
|
+
return () => {
|
|
512
|
+
// noop;
|
|
513
|
+
};
|
|
514
|
+
if (isMutable(source))
|
|
515
|
+
return (value, index) => {
|
|
516
|
+
source.mutate((arr) => {
|
|
517
|
+
arr[index] = value;
|
|
518
|
+
return arr;
|
|
519
|
+
});
|
|
520
|
+
};
|
|
521
|
+
return (value, index) => {
|
|
522
|
+
source.update((arr) => arr.map((v, i) => (i === index ? value : v)));
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Helper to create the derived signal for a specific index.
|
|
528
|
+
* Extracts the cast logic to keep the main loop clean.
|
|
529
|
+
*/
|
|
530
|
+
function createItemSignal(source, index, setter, opt) {
|
|
531
|
+
return derived(
|
|
532
|
+
// We cast to any/Mutable to satisfy the overload signature,
|
|
533
|
+
// but 'derived' internally checks isMutable() for safety.
|
|
534
|
+
source, {
|
|
535
|
+
from: (src) => src[index],
|
|
536
|
+
onChange: (value) => setter(value, index),
|
|
537
|
+
}, opt);
|
|
538
|
+
}
|
|
539
|
+
function indexArray(source, map, opt = {}) {
|
|
540
|
+
const data = isSignal(source) ? source : computed(source);
|
|
541
|
+
const len = computed(() => data().length);
|
|
542
|
+
const setter = createSetter(data);
|
|
543
|
+
const writableData = isWritableSignal(data)
|
|
544
|
+
? data
|
|
545
|
+
: toWritable(data, () => {
|
|
546
|
+
// noop
|
|
547
|
+
});
|
|
548
|
+
if (isWritableSignal(data) && isMutable(data) && !opt.equal) {
|
|
549
|
+
opt.equal = (a, b) => {
|
|
550
|
+
if (a !== b)
|
|
551
|
+
return false; // actually check primitives and references
|
|
552
|
+
return false; // opt out for same refs
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
return linkedSignal({
|
|
556
|
+
source: () => len(),
|
|
557
|
+
computation: (len, prev) => {
|
|
558
|
+
if (!prev)
|
|
559
|
+
return Array.from({ length: len }, (_, i) => map(createItemSignal(writableData, i, setter, opt), i));
|
|
560
|
+
if (len === prev.value.length)
|
|
561
|
+
return prev.value;
|
|
562
|
+
if (len < prev.value.length) {
|
|
563
|
+
if (opt.onDestroy) {
|
|
564
|
+
for (let i = len; i < prev.value.length; i++) {
|
|
565
|
+
opt.onDestroy(prev.value[i]);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return prev.value.slice(0, len);
|
|
569
|
+
}
|
|
570
|
+
const next = prev.value.slice();
|
|
571
|
+
for (let i = prev.value.length; i < len; i++)
|
|
572
|
+
next[i] = map(createItemSignal(writableData, i, setter, opt), i);
|
|
573
|
+
return next;
|
|
574
|
+
},
|
|
575
|
+
equal: (a, b) => a.length === b.length,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* @deprecated use indexArray instead
|
|
580
|
+
*/
|
|
581
|
+
const mapArray = indexArray;
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Reactively maps items from a source array to a new array by value (identity).
|
|
585
|
+
*
|
|
586
|
+
* similar to `Array.prototype.map`, but:
|
|
587
|
+
* 1. The `mapFn` receives the `index` as a Signal.
|
|
588
|
+
* 2. If an item in the `source` array moves to a new position, the *result* of the map function is reused and moved.
|
|
589
|
+
* The `index` signal is updated to the new index.
|
|
590
|
+
* 3. The `mapFn` is only run for *new* items.
|
|
591
|
+
*
|
|
592
|
+
* This is useful for building efficient lists where DOM nodes or heavy instances should be reused
|
|
593
|
+
* when the list is reordered.
|
|
594
|
+
*
|
|
595
|
+
* @param source A `Signal<T[]>` or a function returning `T[]`.
|
|
596
|
+
* @param mapFn The mapping function. Receives the item and its index as a Signal.
|
|
597
|
+
* @param options Optional configuration:
|
|
598
|
+
* - `onDestroy`: A callback invoked when a mapped item is removed from the array.
|
|
599
|
+
* @returns A `Signal<U[]>` containing the mapped array.
|
|
600
|
+
*/
|
|
601
|
+
function keyArray(source, mapFn, options = {}) {
|
|
602
|
+
const sourceSignal = isSignal(source) ? source : computed(source);
|
|
603
|
+
const items = [];
|
|
604
|
+
let mapped = [];
|
|
605
|
+
const indexes = [];
|
|
606
|
+
const getKey = options.key || ((v) => v);
|
|
607
|
+
const newIndices = new Map();
|
|
608
|
+
const temp = [];
|
|
609
|
+
const tempIndexes = [];
|
|
610
|
+
const newIndicesNext = [];
|
|
611
|
+
const newIndexesCache = new Array();
|
|
612
|
+
return computed(() => {
|
|
613
|
+
const newItems = sourceSignal() || [];
|
|
614
|
+
return untracked(() => {
|
|
615
|
+
let i;
|
|
616
|
+
let j;
|
|
617
|
+
const newLen = newItems.length;
|
|
618
|
+
const len = items.length;
|
|
619
|
+
const newMapped = new Array(newLen);
|
|
620
|
+
const newIndexes = newIndexesCache;
|
|
621
|
+
newIndexes.length = 0;
|
|
622
|
+
newIndexes.length = newLen;
|
|
623
|
+
let start;
|
|
624
|
+
let end;
|
|
625
|
+
let newEnd;
|
|
626
|
+
let item;
|
|
627
|
+
let key;
|
|
628
|
+
if (newLen === 0) {
|
|
629
|
+
if (len !== 0) {
|
|
630
|
+
if (options.onDestroy) {
|
|
631
|
+
for (let k = 0; k < len; k++)
|
|
632
|
+
options.onDestroy(mapped[k]);
|
|
633
|
+
}
|
|
634
|
+
items.length = 0;
|
|
635
|
+
mapped = [];
|
|
636
|
+
indexes.length = 0;
|
|
637
|
+
}
|
|
638
|
+
return mapped;
|
|
639
|
+
}
|
|
640
|
+
if (len === 0) {
|
|
641
|
+
for (j = 0; j < newLen; j++) {
|
|
642
|
+
item = newItems[j];
|
|
643
|
+
items[j] = item;
|
|
644
|
+
const indexSignal = signal(j);
|
|
645
|
+
newIndexes[j] = indexSignal;
|
|
646
|
+
newMapped[j] = mapFn(item, indexSignal);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
newIndices.clear();
|
|
651
|
+
temp.length = 0;
|
|
652
|
+
tempIndexes.length = 0;
|
|
653
|
+
newIndicesNext.length = 0;
|
|
654
|
+
for (start = 0, end = Math.min(len, newLen); start < end && getKey(items[start]) === getKey(newItems[start]); start++) {
|
|
655
|
+
newMapped[start] = mapped[start];
|
|
656
|
+
newIndexes[start] = indexes[start];
|
|
657
|
+
}
|
|
658
|
+
for (end = len - 1, newEnd = newLen - 1; end >= start &&
|
|
659
|
+
newEnd >= start &&
|
|
660
|
+
getKey(items[end]) === getKey(newItems[newEnd]); end--, newEnd--) {
|
|
661
|
+
temp[newEnd] = mapped[end];
|
|
662
|
+
tempIndexes[newEnd] = indexes[end];
|
|
663
|
+
}
|
|
664
|
+
for (j = newEnd; j >= start; j--) {
|
|
665
|
+
item = newItems[j];
|
|
666
|
+
key = getKey(item);
|
|
667
|
+
i = newIndices.get(key);
|
|
668
|
+
newIndicesNext[j] = i === undefined ? -1 : i;
|
|
669
|
+
newIndices.set(key, j);
|
|
670
|
+
}
|
|
671
|
+
for (i = start; i <= end; i++) {
|
|
672
|
+
item = items[i];
|
|
673
|
+
key = getKey(item);
|
|
674
|
+
j = newIndices.get(key);
|
|
675
|
+
if (j !== undefined && j !== -1) {
|
|
676
|
+
temp[j] = mapped[i];
|
|
677
|
+
tempIndexes[j] = indexes[i];
|
|
678
|
+
j = newIndicesNext[j];
|
|
679
|
+
newIndices.set(key, j);
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
if (options.onDestroy)
|
|
683
|
+
options.onDestroy(mapped[i]);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// 2) Set all new values
|
|
687
|
+
for (j = start; j < newLen; j++) {
|
|
688
|
+
if (temp[j] !== undefined) {
|
|
689
|
+
newMapped[j] = temp[j];
|
|
690
|
+
newIndexes[j] = tempIndexes[j];
|
|
691
|
+
newIndexes[j].set(j);
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
const indexSignal = signal(j);
|
|
695
|
+
newIndexes[j] = indexSignal;
|
|
696
|
+
newMapped[j] = mapFn(newItems[j], indexSignal);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
items.length = newLen;
|
|
700
|
+
for (let k = 0; k < newLen; k++)
|
|
701
|
+
items[k] = newItems[k];
|
|
702
|
+
}
|
|
703
|
+
mapped = newMapped;
|
|
704
|
+
indexes.length = newLen;
|
|
705
|
+
for (let k = 0; k < newLen; k++)
|
|
706
|
+
indexes[k] = newIndexes[k];
|
|
707
|
+
return mapped;
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function pooledKeys(src) {
|
|
713
|
+
const aBuf = new Set();
|
|
714
|
+
const bBuf = new Set();
|
|
715
|
+
let active = aBuf;
|
|
716
|
+
let spare = bBuf;
|
|
717
|
+
return computed(() => {
|
|
718
|
+
const val = src();
|
|
719
|
+
spare.clear();
|
|
720
|
+
for (const k in val)
|
|
721
|
+
if (Object.prototype.hasOwnProperty.call(val, k))
|
|
722
|
+
spare.add(k);
|
|
723
|
+
if (active.size === spare.size && active.isSubsetOf(spare))
|
|
724
|
+
return active;
|
|
725
|
+
const temp = active;
|
|
726
|
+
active = spare;
|
|
727
|
+
spare = temp;
|
|
728
|
+
return active;
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
function mapObject(source, mapFn, options = {}) {
|
|
732
|
+
const src = isSignal(source) ? source : computed(source);
|
|
733
|
+
const writable = (isWritableSignal(src)
|
|
734
|
+
? src
|
|
735
|
+
: toWritable(src, () => {
|
|
736
|
+
// noop
|
|
737
|
+
})); // maximal overload internally
|
|
738
|
+
return linkedSignal({
|
|
739
|
+
source: pooledKeys(src),
|
|
740
|
+
computation: (next, prev) => {
|
|
741
|
+
const nextObj = {};
|
|
742
|
+
for (const k of next)
|
|
743
|
+
nextObj[k] =
|
|
744
|
+
prev && prev.source.has(k)
|
|
745
|
+
? prev.value[k]
|
|
746
|
+
: mapFn(k, derived(writable, k));
|
|
747
|
+
if (options.onDestroy && prev && prev.source.size)
|
|
748
|
+
for (const k of prev.source)
|
|
749
|
+
if (!next.has(k))
|
|
750
|
+
options.onDestroy(prev.value[k]);
|
|
751
|
+
return nextObj;
|
|
752
|
+
},
|
|
753
|
+
}).asReadonly();
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** Project with optional equality. Pure & sync. */
|
|
757
|
+
const select = (projector, opt) => (src) => computed(() => projector(src()), opt);
|
|
758
|
+
/** Combine with another signal using a projector. */
|
|
759
|
+
const combineWith = (other, project, opt) => (src) => computed(() => project(src(), other()), opt);
|
|
760
|
+
/** Only re-emit when equal(prev, next) is false. */
|
|
761
|
+
const distinct = (equal = Object.is) => (src) => computed(() => src(), { equal });
|
|
762
|
+
/** map to new value */
|
|
763
|
+
const map = (fn) => (src) => computed(() => fn(src()));
|
|
764
|
+
/** filter values, keeping the last value if it was ever available, if first value is filtered will return undefined */
|
|
765
|
+
const filter = (predicate) => (src) => linkedSignal({
|
|
766
|
+
source: src,
|
|
767
|
+
computation: (next, prev) => {
|
|
768
|
+
if (predicate(next))
|
|
769
|
+
return next;
|
|
770
|
+
return prev?.source;
|
|
771
|
+
},
|
|
772
|
+
});
|
|
773
|
+
/** tap into the value */
|
|
774
|
+
const tap = (fn) => (src) => {
|
|
775
|
+
effect(() => fn(src()));
|
|
776
|
+
return src;
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Decorate any `Signal<T>` with a chainable `.pipe(...)` method.
|
|
781
|
+
*
|
|
782
|
+
* @example
|
|
783
|
+
* const s = pipeable(signal(1)); // WritableSignal<number> (+ pipe)
|
|
784
|
+
* const label = s.pipe(n => n * 2, n => `#${n}`); // Signal<string> (+ pipe)
|
|
785
|
+
* label(); // "#2"
|
|
786
|
+
*/
|
|
787
|
+
function pipeable(signal) {
|
|
788
|
+
const internal = signal;
|
|
789
|
+
const mapImpl = (...fns) => {
|
|
790
|
+
const last = fns.at(-1);
|
|
791
|
+
let opt;
|
|
792
|
+
if (last && typeof last !== 'function') {
|
|
793
|
+
fns = fns.slice(0, -1);
|
|
794
|
+
opt = last;
|
|
795
|
+
}
|
|
796
|
+
if (fns.length === 0)
|
|
797
|
+
return internal;
|
|
798
|
+
if (fns.length === 1) {
|
|
799
|
+
const fn = fns[0];
|
|
800
|
+
return pipeable(computed(() => fn(internal()), opt));
|
|
801
|
+
}
|
|
802
|
+
const transformer = (input) => fns.reduce((acc, fn) => fn(acc), input);
|
|
803
|
+
return pipeable(computed(() => transformer(internal()), opt));
|
|
804
|
+
};
|
|
805
|
+
const pipeImpl = (...ops) => {
|
|
806
|
+
if (ops.length === 0)
|
|
807
|
+
return internal;
|
|
808
|
+
return ops.reduce((src, op) => pipeable(op(src)), internal);
|
|
809
|
+
};
|
|
810
|
+
Object.defineProperties(internal, {
|
|
811
|
+
map: {
|
|
812
|
+
value: mapImpl,
|
|
813
|
+
configurable: true,
|
|
814
|
+
enumerable: false,
|
|
815
|
+
writable: false,
|
|
816
|
+
},
|
|
817
|
+
pipe: {
|
|
818
|
+
value: pipeImpl,
|
|
819
|
+
configurable: true,
|
|
820
|
+
enumerable: false,
|
|
821
|
+
writable: false,
|
|
822
|
+
},
|
|
823
|
+
});
|
|
824
|
+
return internal;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Create a new **writable** signal and return it as a `PipableSignal`.
|
|
828
|
+
*
|
|
829
|
+
* The returned value is a `WritableSignal<T>` with `.set`, `.update`, `.asReadonly`
|
|
830
|
+
* still available (via intersection type), plus a chainable `.pipe(...)`.
|
|
831
|
+
*
|
|
832
|
+
* @example
|
|
833
|
+
* const count = piped(1); // WritableSignal<number> (+ pipe)
|
|
834
|
+
* const even = count.pipe(n => n % 2 === 0); // Signal<boolean> (+ pipe)
|
|
835
|
+
* count.update(n => n + 1);
|
|
836
|
+
*/
|
|
837
|
+
function piped(initial, opt) {
|
|
838
|
+
return pipeable(signal(initial, opt));
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function observerSupported$1() {
|
|
842
|
+
return typeof ResizeObserver !== 'undefined';
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Creates a read-only signal that tracks the size of a target DOM element.
|
|
846
|
+
*
|
|
847
|
+
* By default, it observes the `border-box` size to align with `getBoundingClientRect()`,
|
|
848
|
+
* which is used to provide a synchronous initial value if possible.
|
|
849
|
+
*
|
|
850
|
+
* @param target The DOM element (or `ElementRef`, or a `Signal` resolving to one) to observe.
|
|
851
|
+
* @param options Optional configuration including `box` (defaults to 'border-box') and `debugName`.
|
|
852
|
+
* @returns A `Signal<ElementSize | undefined>`.
|
|
853
|
+
*
|
|
854
|
+
* @example
|
|
855
|
+
* ```ts
|
|
856
|
+
* const size = elementSize(elementRef);
|
|
857
|
+
* effect(() => {
|
|
858
|
+
* console.log('Size:', size()?.width, size()?.height);
|
|
859
|
+
* });
|
|
860
|
+
* ```
|
|
861
|
+
*/
|
|
862
|
+
function elementSize(target = inject(ElementRef), opt) {
|
|
863
|
+
const getElement = () => {
|
|
864
|
+
if (isSignal(target)) {
|
|
865
|
+
try {
|
|
866
|
+
const val = target();
|
|
867
|
+
return val instanceof ElementRef ? val.nativeElement : val;
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return target instanceof ElementRef ? target.nativeElement : target;
|
|
874
|
+
};
|
|
875
|
+
const resolveInitialValue = () => {
|
|
876
|
+
if (!observerSupported$1())
|
|
877
|
+
return undefined;
|
|
878
|
+
const el = getElement();
|
|
879
|
+
if (el && el.getBoundingClientRect) {
|
|
880
|
+
const rect = el.getBoundingClientRect();
|
|
881
|
+
return { width: rect.width, height: rect.height };
|
|
882
|
+
}
|
|
883
|
+
return undefined;
|
|
884
|
+
};
|
|
885
|
+
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
886
|
+
return computed(() => untracked(resolveInitialValue), {
|
|
887
|
+
debugName: opt?.debugName,
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
const state = signal(untracked(resolveInitialValue), {
|
|
891
|
+
debugName: opt?.debugName,
|
|
892
|
+
equal: (a, b) => a?.width === b?.width && a?.height === b?.height,
|
|
893
|
+
});
|
|
894
|
+
const targetSignal = isSignal(target) ? target : computed(() => target);
|
|
895
|
+
effect((cleanup) => {
|
|
896
|
+
const el = targetSignal();
|
|
897
|
+
if (el) {
|
|
898
|
+
const nativeEl = el instanceof ElementRef ? el.nativeElement : el;
|
|
899
|
+
const rect = nativeEl.getBoundingClientRect();
|
|
900
|
+
untracked(() => state.set({ width: rect.width, height: rect.height }));
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
untracked(() => state.set(undefined));
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (!observerSupported$1())
|
|
907
|
+
return;
|
|
908
|
+
let observer = null;
|
|
909
|
+
observer = new ResizeObserver(([entry]) => {
|
|
910
|
+
let width = 0;
|
|
911
|
+
let height = 0;
|
|
912
|
+
const boxOption = opt?.box ?? 'border-box';
|
|
913
|
+
if (boxOption === 'border-box' && entry.borderBoxSize?.length > 0) {
|
|
914
|
+
const size = entry.borderBoxSize[0];
|
|
915
|
+
width = size.inlineSize;
|
|
916
|
+
height = size.blockSize;
|
|
917
|
+
}
|
|
918
|
+
else if (boxOption === 'content-box' &&
|
|
919
|
+
entry.contentBoxSize?.length > 0) {
|
|
920
|
+
width = entry.contentBoxSize[0].inlineSize;
|
|
921
|
+
height = entry.contentBoxSize[0].blockSize;
|
|
922
|
+
}
|
|
923
|
+
else {
|
|
924
|
+
width = entry.contentRect.width;
|
|
925
|
+
height = entry.contentRect.height;
|
|
926
|
+
}
|
|
927
|
+
state.set({ width, height });
|
|
928
|
+
});
|
|
929
|
+
observer.observe(el instanceof ElementRef ? el.nativeElement : el, {
|
|
930
|
+
box: opt?.box ?? 'border-box',
|
|
931
|
+
});
|
|
932
|
+
cleanup(() => {
|
|
933
|
+
observer?.disconnect();
|
|
934
|
+
});
|
|
935
|
+
});
|
|
936
|
+
return state.asReadonly();
|
|
937
|
+
}
|
|
938
|
+
|
|
204
939
|
function observerSupported() {
|
|
205
940
|
return typeof IntersectionObserver !== 'undefined';
|
|
206
941
|
}
|
|
@@ -257,7 +992,7 @@ function observerSupported() {
|
|
|
257
992
|
* }
|
|
258
993
|
* ```
|
|
259
994
|
*/
|
|
260
|
-
function elementVisibility(target, opt) {
|
|
995
|
+
function elementVisibility(target = inject(ElementRef), opt) {
|
|
261
996
|
if (isPlatformServer(inject(PLATFORM_ID)) || !observerSupported()) {
|
|
262
997
|
const base = computed(() => undefined, {
|
|
263
998
|
debugName: opt?.debugName,
|
|
@@ -303,141 +1038,6 @@ function elementVisibility(target, opt) {
|
|
|
303
1038
|
return base;
|
|
304
1039
|
}
|
|
305
1040
|
|
|
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
1041
|
/**
|
|
442
1042
|
* Creates a read-only signal that reactively tracks whether a CSS media query
|
|
443
1043
|
* string currently matches.
|
|
@@ -482,11 +1082,11 @@ function isMutable(value) {
|
|
|
482
1082
|
* }
|
|
483
1083
|
* ```
|
|
484
1084
|
*/
|
|
485
|
-
function mediaQuery(query, debugName) {
|
|
1085
|
+
function mediaQuery(query, debugName = 'mediaQuery') {
|
|
486
1086
|
if (isPlatformServer(inject(PLATFORM_ID)))
|
|
487
1087
|
return computed(() => false, { debugName });
|
|
488
1088
|
const mediaQueryList = window.matchMedia(query);
|
|
489
|
-
const state = signal(mediaQueryList.matches, { debugName });
|
|
1089
|
+
const state = signal(mediaQueryList.matches, { debugName: debugName });
|
|
490
1090
|
const handleChange = (event) => {
|
|
491
1091
|
state.set(event.matches);
|
|
492
1092
|
};
|
|
@@ -609,8 +1209,7 @@ function throttle(source, opt) {
|
|
|
609
1209
|
catch {
|
|
610
1210
|
// not in injection context & no destroyRef provided opting out of cleanup
|
|
611
1211
|
}
|
|
612
|
-
const
|
|
613
|
-
updateSourceAction();
|
|
1212
|
+
const tick = () => {
|
|
614
1213
|
if (timeout)
|
|
615
1214
|
return;
|
|
616
1215
|
timeout = setTimeout(() => {
|
|
@@ -619,10 +1218,12 @@ function throttle(source, opt) {
|
|
|
619
1218
|
}, ms);
|
|
620
1219
|
};
|
|
621
1220
|
const set = (value) => {
|
|
622
|
-
|
|
1221
|
+
source.set(value);
|
|
1222
|
+
tick();
|
|
623
1223
|
};
|
|
624
1224
|
const update = (fn) => {
|
|
625
|
-
|
|
1225
|
+
source.update(fn);
|
|
1226
|
+
tick();
|
|
626
1227
|
};
|
|
627
1228
|
const writable = toWritable(computed(() => {
|
|
628
1229
|
trigger();
|
|
@@ -670,12 +1271,12 @@ function mousePosition(opt) {
|
|
|
670
1271
|
x: 0,
|
|
671
1272
|
y: 0,
|
|
672
1273
|
}), {
|
|
673
|
-
debugName: opt?.debugName,
|
|
1274
|
+
debugName: opt?.debugName ?? 'mousePosition',
|
|
674
1275
|
});
|
|
675
1276
|
base.unthrottled = base;
|
|
676
1277
|
return base;
|
|
677
1278
|
}
|
|
678
|
-
const { target = window, coordinateSpace = 'client', touch = false, debugName, throttle = 100, } = opt ?? {};
|
|
1279
|
+
const { target = window, coordinateSpace = 'client', touch = false, debugName = 'mousePosition', throttle = 100, } = opt ?? {};
|
|
679
1280
|
const eventTarget = target instanceof ElementRef ? target.nativeElement : target;
|
|
680
1281
|
if (!eventTarget) {
|
|
681
1282
|
if (isDevMode())
|
|
@@ -736,7 +1337,7 @@ const serverDate = new Date();
|
|
|
736
1337
|
* @param debugName Optional debug name for the signal.
|
|
737
1338
|
* @returns A `NetworkStatusSignal` instance.
|
|
738
1339
|
*/
|
|
739
|
-
function networkStatus(debugName) {
|
|
1340
|
+
function networkStatus(debugName = 'networkStatus') {
|
|
740
1341
|
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
741
1342
|
const sig = computed(() => true, {
|
|
742
1343
|
debugName,
|
|
@@ -803,7 +1404,7 @@ function networkStatus(debugName) {
|
|
|
803
1404
|
* }
|
|
804
1405
|
* ```
|
|
805
1406
|
*/
|
|
806
|
-
function pageVisibility(debugName) {
|
|
1407
|
+
function pageVisibility(debugName = 'pageVisibility') {
|
|
807
1408
|
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
808
1409
|
return computed(() => 'visible', { debugName });
|
|
809
1410
|
}
|
|
@@ -865,12 +1466,12 @@ function scrollPosition(opt) {
|
|
|
865
1466
|
x: 0,
|
|
866
1467
|
y: 0,
|
|
867
1468
|
}), {
|
|
868
|
-
debugName: opt?.debugName,
|
|
1469
|
+
debugName: opt?.debugName ?? 'scrollPosition',
|
|
869
1470
|
});
|
|
870
1471
|
base.unthrottled = base;
|
|
871
1472
|
return base;
|
|
872
1473
|
}
|
|
873
|
-
const { target = window, throttle = 100, debugName } = opt || {};
|
|
1474
|
+
const { target = window, throttle = 100, debugName = 'scrollPosition', } = opt || {};
|
|
874
1475
|
let element;
|
|
875
1476
|
let getScrollPosition;
|
|
876
1477
|
if (target instanceof Window) {
|
|
@@ -956,12 +1557,12 @@ function windowSize(opt) {
|
|
|
956
1557
|
const base = computed(() => ({
|
|
957
1558
|
width: 1024,
|
|
958
1559
|
height: 768,
|
|
959
|
-
}), { debugName: opt?.debugName });
|
|
1560
|
+
}), { debugName: opt?.debugName ?? 'windowSize' });
|
|
960
1561
|
base.unthrottled = base;
|
|
961
1562
|
return base;
|
|
962
1563
|
}
|
|
963
1564
|
const sizeSignal = throttled({ width: window.innerWidth, height: window.innerHeight }, {
|
|
964
|
-
debugName: opt?.debugName,
|
|
1565
|
+
debugName: opt?.debugName ?? 'windowSize',
|
|
965
1566
|
equal: (a, b) => a.width === b.width && a.height === b.height,
|
|
966
1567
|
ms: opt?.throttle ?? 100,
|
|
967
1568
|
});
|
|
@@ -990,18 +1591,303 @@ function sensor(type, options) {
|
|
|
990
1591
|
return networkStatus(options?.debugName);
|
|
991
1592
|
case 'pageVisibility':
|
|
992
1593
|
return pageVisibility(options?.debugName);
|
|
1594
|
+
case 'darkMode':
|
|
993
1595
|
case 'dark-mode':
|
|
994
1596
|
return prefersDarkMode(options?.debugName);
|
|
1597
|
+
case 'reducedMotion':
|
|
995
1598
|
case 'reduced-motion':
|
|
996
1599
|
return prefersReducedMotion(options?.debugName);
|
|
1600
|
+
case 'mediaQuery': {
|
|
1601
|
+
const opt = options;
|
|
1602
|
+
return mediaQuery(opt.query, opt.debugName);
|
|
1603
|
+
}
|
|
997
1604
|
case 'windowSize':
|
|
998
1605
|
return windowSize(options);
|
|
999
1606
|
case 'scrollPosition':
|
|
1000
1607
|
return scrollPosition(options);
|
|
1608
|
+
case 'elementVisibility': {
|
|
1609
|
+
const opt = options;
|
|
1610
|
+
return elementVisibility(opt.target, opt);
|
|
1611
|
+
}
|
|
1612
|
+
case 'elementSize': {
|
|
1613
|
+
const opt = options;
|
|
1614
|
+
return elementSize(opt.target, opt);
|
|
1615
|
+
}
|
|
1001
1616
|
default:
|
|
1002
1617
|
throw new Error(`Unknown sensor type: ${type}`);
|
|
1003
1618
|
}
|
|
1004
1619
|
}
|
|
1620
|
+
function sensors(track, opt) {
|
|
1621
|
+
return track.reduce((result, key) => {
|
|
1622
|
+
result[key] = sensor(key, opt?.[key]);
|
|
1623
|
+
return result;
|
|
1624
|
+
}, {});
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
const IS_STORE = Symbol('MMSTACK::IS_STORE');
|
|
1628
|
+
const PROXY_CACHE = new WeakMap();
|
|
1629
|
+
const SIGNAL_FN_PROP = new Set([
|
|
1630
|
+
'set',
|
|
1631
|
+
'update',
|
|
1632
|
+
'mutate',
|
|
1633
|
+
'inline',
|
|
1634
|
+
'asReadonly',
|
|
1635
|
+
]);
|
|
1636
|
+
const PROXY_CLEANUP = new FinalizationRegistry(({ target, prop }) => {
|
|
1637
|
+
const storeCache = PROXY_CACHE.get(target);
|
|
1638
|
+
if (storeCache)
|
|
1639
|
+
storeCache.delete(prop);
|
|
1640
|
+
});
|
|
1641
|
+
/**
|
|
1642
|
+
* @internal
|
|
1643
|
+
* Validates whether a value is a Signal Store.
|
|
1644
|
+
*/
|
|
1645
|
+
function isStore(value) {
|
|
1646
|
+
return (typeof value === 'function' &&
|
|
1647
|
+
value !== null &&
|
|
1648
|
+
value[IS_STORE] === true);
|
|
1649
|
+
}
|
|
1650
|
+
function isIndexProp(prop) {
|
|
1651
|
+
return typeof prop === 'string' && prop.trim() !== '' && !isNaN(+prop);
|
|
1652
|
+
}
|
|
1653
|
+
function isRecord(value) {
|
|
1654
|
+
if (value === null || typeof value !== 'object')
|
|
1655
|
+
return false;
|
|
1656
|
+
const proto = Object.getPrototypeOf(value);
|
|
1657
|
+
return proto === Object.prototype || proto === null;
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* @internal
|
|
1661
|
+
* Makes an array store
|
|
1662
|
+
*/
|
|
1663
|
+
function toArrayStore(source, injector) {
|
|
1664
|
+
if (isStore(source))
|
|
1665
|
+
return source;
|
|
1666
|
+
const isMutableSource = isMutable(source);
|
|
1667
|
+
const lengthSignal = computed(() => {
|
|
1668
|
+
const v = source();
|
|
1669
|
+
if (!Array.isArray(v))
|
|
1670
|
+
return 0;
|
|
1671
|
+
return v.length;
|
|
1672
|
+
});
|
|
1673
|
+
return new Proxy(source, {
|
|
1674
|
+
has(_, prop) {
|
|
1675
|
+
if (prop === 'length')
|
|
1676
|
+
return true;
|
|
1677
|
+
if (isIndexProp(prop)) {
|
|
1678
|
+
const idx = +prop;
|
|
1679
|
+
return idx >= 0 && idx < untracked(lengthSignal);
|
|
1680
|
+
}
|
|
1681
|
+
return Reflect.has(untracked(source), prop);
|
|
1682
|
+
},
|
|
1683
|
+
ownKeys() {
|
|
1684
|
+
const v = untracked(source);
|
|
1685
|
+
if (!Array.isArray(v))
|
|
1686
|
+
return [];
|
|
1687
|
+
const len = v.length;
|
|
1688
|
+
const arr = new Array(len + 1);
|
|
1689
|
+
for (let i = 0; i < len; i++) {
|
|
1690
|
+
arr[i] = String(i);
|
|
1691
|
+
}
|
|
1692
|
+
arr[len] = 'length';
|
|
1693
|
+
return arr;
|
|
1694
|
+
},
|
|
1695
|
+
getPrototypeOf() {
|
|
1696
|
+
return Array.prototype;
|
|
1697
|
+
},
|
|
1698
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
1699
|
+
const v = untracked(source);
|
|
1700
|
+
if (!Array.isArray(v))
|
|
1701
|
+
return;
|
|
1702
|
+
if (prop === 'length' ||
|
|
1703
|
+
(typeof prop === 'string' && !isNaN(+prop) && +prop < v.length)) {
|
|
1704
|
+
return {
|
|
1705
|
+
enumerable: true,
|
|
1706
|
+
configurable: true, // Required for proxies to dynamic targets
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
return;
|
|
1710
|
+
},
|
|
1711
|
+
get(target, prop, receiver) {
|
|
1712
|
+
if (prop === IS_STORE)
|
|
1713
|
+
return true;
|
|
1714
|
+
if (prop === 'length')
|
|
1715
|
+
return lengthSignal;
|
|
1716
|
+
if (prop === Symbol.iterator) {
|
|
1717
|
+
return function* () {
|
|
1718
|
+
for (let i = 0; i < untracked(lengthSignal); i++) {
|
|
1719
|
+
yield receiver[i];
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
|
|
1724
|
+
return target[prop];
|
|
1725
|
+
if (isIndexProp(prop)) {
|
|
1726
|
+
const idx = +prop;
|
|
1727
|
+
let storeCache = PROXY_CACHE.get(target);
|
|
1728
|
+
if (!storeCache) {
|
|
1729
|
+
storeCache = new Map();
|
|
1730
|
+
PROXY_CACHE.set(target, storeCache);
|
|
1731
|
+
}
|
|
1732
|
+
const cachedRef = storeCache.get(idx);
|
|
1733
|
+
if (cachedRef) {
|
|
1734
|
+
const cached = cachedRef.deref();
|
|
1735
|
+
if (cached)
|
|
1736
|
+
return cached;
|
|
1737
|
+
storeCache.delete(idx);
|
|
1738
|
+
PROXY_CLEANUP.unregister(cachedRef);
|
|
1739
|
+
}
|
|
1740
|
+
const value = untracked(target);
|
|
1741
|
+
const valueIsArray = Array.isArray(value);
|
|
1742
|
+
const valueIsRecord = isRecord(value);
|
|
1743
|
+
const equalFn = (valueIsRecord || valueIsArray) &&
|
|
1744
|
+
isMutableSource &&
|
|
1745
|
+
typeof value[idx] === 'object'
|
|
1746
|
+
? () => false
|
|
1747
|
+
: undefined;
|
|
1748
|
+
const computation = valueIsRecord
|
|
1749
|
+
? derived(target, idx, { equal: equalFn })
|
|
1750
|
+
: derived(target, {
|
|
1751
|
+
from: (v) => v?.[idx],
|
|
1752
|
+
onChange: (newValue) => target.update((v) => {
|
|
1753
|
+
if (v === null || v === undefined)
|
|
1754
|
+
return v;
|
|
1755
|
+
try {
|
|
1756
|
+
v[idx] = newValue;
|
|
1757
|
+
}
|
|
1758
|
+
catch (e) {
|
|
1759
|
+
if (isDevMode())
|
|
1760
|
+
console.error(`[store] Failed to set property "${String(idx)}"`, e);
|
|
1761
|
+
}
|
|
1762
|
+
return v;
|
|
1763
|
+
}),
|
|
1764
|
+
});
|
|
1765
|
+
const proxy = Array.isArray(untracked(computation))
|
|
1766
|
+
? toArrayStore(computation, injector)
|
|
1767
|
+
: toStore(computation, injector);
|
|
1768
|
+
const ref = new WeakRef(proxy);
|
|
1769
|
+
storeCache.set(idx, ref);
|
|
1770
|
+
PROXY_CLEANUP.register(proxy, { target, prop: idx }, ref);
|
|
1771
|
+
return proxy;
|
|
1772
|
+
}
|
|
1773
|
+
return Reflect.get(target, prop, receiver);
|
|
1774
|
+
},
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Converts a Signal into a deep-observable Store.
|
|
1779
|
+
* Accessing nested properties returns a derived Signal of that path.
|
|
1780
|
+
* @example
|
|
1781
|
+
* const state = store({ user: { name: 'John' } });
|
|
1782
|
+
* const nameSignal = state.user.name; // WritableSignal<string>
|
|
1783
|
+
*/
|
|
1784
|
+
function toStore(source, injector) {
|
|
1785
|
+
if (isStore(source))
|
|
1786
|
+
return source;
|
|
1787
|
+
if (!injector)
|
|
1788
|
+
injector = inject(Injector);
|
|
1789
|
+
const writableSource = isWritableSignal(source)
|
|
1790
|
+
? source
|
|
1791
|
+
: toWritable(source, () => {
|
|
1792
|
+
// noop
|
|
1793
|
+
});
|
|
1794
|
+
const isMutableSource = isMutable(writableSource);
|
|
1795
|
+
const s = new Proxy(writableSource, {
|
|
1796
|
+
has(_, prop) {
|
|
1797
|
+
return Reflect.has(untracked(source), prop);
|
|
1798
|
+
},
|
|
1799
|
+
ownKeys() {
|
|
1800
|
+
const v = untracked(source);
|
|
1801
|
+
if (!isRecord(v))
|
|
1802
|
+
return [];
|
|
1803
|
+
return Reflect.ownKeys(v);
|
|
1804
|
+
},
|
|
1805
|
+
getPrototypeOf() {
|
|
1806
|
+
return Object.getPrototypeOf(untracked(source));
|
|
1807
|
+
},
|
|
1808
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
1809
|
+
const value = untracked(source);
|
|
1810
|
+
if (!isRecord(value) || !(prop in value))
|
|
1811
|
+
return;
|
|
1812
|
+
return {
|
|
1813
|
+
enumerable: true,
|
|
1814
|
+
configurable: true,
|
|
1815
|
+
};
|
|
1816
|
+
},
|
|
1817
|
+
get(target, prop) {
|
|
1818
|
+
if (prop === IS_STORE)
|
|
1819
|
+
return true;
|
|
1820
|
+
if (prop === 'asReadonlyStore')
|
|
1821
|
+
return () => {
|
|
1822
|
+
if (!isWritableSignal(source))
|
|
1823
|
+
return s;
|
|
1824
|
+
return untracked(() => toStore(source.asReadonly(), injector));
|
|
1825
|
+
};
|
|
1826
|
+
if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
|
|
1827
|
+
return target[prop];
|
|
1828
|
+
let storeCache = PROXY_CACHE.get(target);
|
|
1829
|
+
if (!storeCache) {
|
|
1830
|
+
storeCache = new Map();
|
|
1831
|
+
PROXY_CACHE.set(target, storeCache);
|
|
1832
|
+
}
|
|
1833
|
+
const cachedRef = storeCache.get(prop);
|
|
1834
|
+
if (cachedRef) {
|
|
1835
|
+
const cached = cachedRef.deref();
|
|
1836
|
+
if (cached)
|
|
1837
|
+
return cached;
|
|
1838
|
+
storeCache.delete(prop);
|
|
1839
|
+
PROXY_CLEANUP.unregister(cachedRef);
|
|
1840
|
+
}
|
|
1841
|
+
const value = untracked(target);
|
|
1842
|
+
const valueIsRecord = isRecord(value);
|
|
1843
|
+
const valueIsArray = Array.isArray(value);
|
|
1844
|
+
const equalFn = (valueIsRecord || valueIsArray) &&
|
|
1845
|
+
isMutableSource &&
|
|
1846
|
+
typeof value[prop] === 'object'
|
|
1847
|
+
? () => false
|
|
1848
|
+
: undefined;
|
|
1849
|
+
const computation = valueIsRecord
|
|
1850
|
+
? derived(target, prop, { equal: equalFn })
|
|
1851
|
+
: derived(target, {
|
|
1852
|
+
from: (v) => v?.[prop],
|
|
1853
|
+
onChange: (newValue) => target.update((v) => {
|
|
1854
|
+
if (v === null || v === undefined)
|
|
1855
|
+
return v;
|
|
1856
|
+
try {
|
|
1857
|
+
v[prop] = newValue;
|
|
1858
|
+
}
|
|
1859
|
+
catch (e) {
|
|
1860
|
+
if (isDevMode())
|
|
1861
|
+
console.error(`[store] Failed to set property "${String(prop)}"`, e);
|
|
1862
|
+
}
|
|
1863
|
+
return v;
|
|
1864
|
+
}),
|
|
1865
|
+
});
|
|
1866
|
+
const proxy = Array.isArray(untracked(computation))
|
|
1867
|
+
? toArrayStore(computation, injector)
|
|
1868
|
+
: toStore(computation, injector);
|
|
1869
|
+
const ref = new WeakRef(proxy);
|
|
1870
|
+
storeCache.set(prop, ref);
|
|
1871
|
+
PROXY_CLEANUP.register(proxy, { target, prop }, ref);
|
|
1872
|
+
return proxy;
|
|
1873
|
+
},
|
|
1874
|
+
});
|
|
1875
|
+
return s;
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Creates a WritableSignalStore from a value.
|
|
1879
|
+
* @see {@link toStore}
|
|
1880
|
+
*/
|
|
1881
|
+
function store(value, opt) {
|
|
1882
|
+
return toStore(signal(value, opt), opt?.injector);
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Creates a MutableSignalStore from a value.
|
|
1886
|
+
* @see {@link toStore}
|
|
1887
|
+
*/
|
|
1888
|
+
function mutableStore(value, opt) {
|
|
1889
|
+
return toStore(mutable(value, opt), opt?.injector);
|
|
1890
|
+
}
|
|
1005
1891
|
|
|
1006
1892
|
// Internal dummy store for server-side rendering
|
|
1007
1893
|
const noopStore = {
|
|
@@ -1064,7 +1950,7 @@ const noopStore = {
|
|
|
1064
1950
|
* }
|
|
1065
1951
|
* ```
|
|
1066
1952
|
*/
|
|
1067
|
-
function stored(fallback, { key, store: providedStore, serialize = JSON.stringify, deserialize = JSON.parse, syncTabs = false, equal = Object.is, onKeyChange = 'load', cleanupOldKey = false, ...rest }) {
|
|
1953
|
+
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
1954
|
const isServer = isPlatformServer(inject(PLATFORM_ID));
|
|
1069
1955
|
const fallbackStore = isServer ? noopStore : localStorage;
|
|
1070
1956
|
const store = providedStore ?? fallbackStore;
|
|
@@ -1078,7 +1964,10 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
1078
1964
|
if (found === null)
|
|
1079
1965
|
return null;
|
|
1080
1966
|
try {
|
|
1081
|
-
|
|
1967
|
+
const deserialized = deserialize(found);
|
|
1968
|
+
if (!validate(deserialized))
|
|
1969
|
+
return null;
|
|
1970
|
+
return deserialized;
|
|
1082
1971
|
}
|
|
1083
1972
|
catch (err) {
|
|
1084
1973
|
if (isDevMode())
|
|
@@ -1128,15 +2017,16 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
1128
2017
|
else {
|
|
1129
2018
|
effect(() => {
|
|
1130
2019
|
const k = keySig();
|
|
2020
|
+
const internalValue = internal();
|
|
1131
2021
|
if (k === prevKey) {
|
|
1132
|
-
return storeValue(k,
|
|
2022
|
+
return storeValue(k, internalValue); // normal operation
|
|
1133
2023
|
}
|
|
1134
2024
|
else {
|
|
1135
2025
|
if (cleanupOldKey)
|
|
1136
2026
|
store.removeItem(prevKey);
|
|
1137
2027
|
const value = getValue(k);
|
|
1138
|
-
internal.set(value); // load new value
|
|
1139
2028
|
prevKey = k;
|
|
2029
|
+
internal.set(value); // load new value
|
|
1140
2030
|
}
|
|
1141
2031
|
});
|
|
1142
2032
|
}
|
|
@@ -1161,41 +2051,122 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
1161
2051
|
return writable;
|
|
1162
2052
|
}
|
|
1163
2053
|
|
|
2054
|
+
class MessageBus {
|
|
2055
|
+
channel = new BroadcastChannel('mmstack-tab-sync-bus');
|
|
2056
|
+
listeners = new Map();
|
|
2057
|
+
subscribe(id, listener) {
|
|
2058
|
+
this.unsubscribe(id); // Ensure no duplicate listeners
|
|
2059
|
+
const wrapped = (ev) => {
|
|
2060
|
+
try {
|
|
2061
|
+
if (ev.data?.id === id)
|
|
2062
|
+
listener(ev.data?.value);
|
|
2063
|
+
}
|
|
2064
|
+
catch {
|
|
2065
|
+
// noop
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
this.channel.addEventListener('message', wrapped);
|
|
2069
|
+
this.listeners.set(id, wrapped);
|
|
2070
|
+
return {
|
|
2071
|
+
unsub: (() => this.unsubscribe(id)).bind(this),
|
|
2072
|
+
post: ((value) => this.channel.postMessage({ id, value })).bind(this),
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
unsubscribe(id) {
|
|
2076
|
+
const listener = this.listeners.get(id);
|
|
2077
|
+
if (!listener)
|
|
2078
|
+
return;
|
|
2079
|
+
this.channel.removeEventListener('message', listener);
|
|
2080
|
+
this.listeners.delete(id);
|
|
2081
|
+
}
|
|
2082
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: MessageBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2083
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: MessageBus, providedIn: 'root' });
|
|
2084
|
+
}
|
|
2085
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: MessageBus, decorators: [{
|
|
2086
|
+
type: Injectable,
|
|
2087
|
+
args: [{
|
|
2088
|
+
providedIn: 'root',
|
|
2089
|
+
}]
|
|
2090
|
+
}] });
|
|
2091
|
+
function generateDeterministicID() {
|
|
2092
|
+
const stack = new Error().stack;
|
|
2093
|
+
if (stack) {
|
|
2094
|
+
// Look for the actual caller (first non-internal frame)
|
|
2095
|
+
const lines = stack.split('\n');
|
|
2096
|
+
for (let i = 2; i < lines.length; i++) {
|
|
2097
|
+
const line = lines[i];
|
|
2098
|
+
if (line && !line.includes('tabSync') && !line.includes('MessageBus')) {
|
|
2099
|
+
let hash = 0;
|
|
2100
|
+
for (let j = 0; j < line.length; j++) {
|
|
2101
|
+
const char = line.charCodeAt(j);
|
|
2102
|
+
hash = (hash << 5) - hash + char;
|
|
2103
|
+
hash = hash & hash;
|
|
2104
|
+
}
|
|
2105
|
+
return `auto-${Math.abs(hash)}`;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
throw new Error('Could not generate deterministic ID, please provide one manually.');
|
|
2110
|
+
}
|
|
1164
2111
|
/**
|
|
1165
|
-
*
|
|
2112
|
+
* Synchronizes a WritableSignal across browser tabs using BroadcastChannel API.
|
|
1166
2113
|
*
|
|
1167
|
-
*
|
|
1168
|
-
*
|
|
2114
|
+
* Creates a shared signal that automatically syncs its value between all tabs
|
|
2115
|
+
* of the same application. When the signal is updated in one tab, all other
|
|
2116
|
+
* tabs will receive the new value automatically.
|
|
1169
2117
|
*
|
|
1170
|
-
* @template T The type of the
|
|
1171
|
-
* @param
|
|
1172
|
-
* @param
|
|
1173
|
-
* @param
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
2118
|
+
* @template T - The type of the WritableSignal
|
|
2119
|
+
* @param sig - The WritableSignal to synchronize across tabs
|
|
2120
|
+
* @param opt - Optional configuration object
|
|
2121
|
+
* @param opt.id - Explicit channel ID for synchronization. If not provided,
|
|
2122
|
+
* a deterministic ID is generated based on the call site.
|
|
2123
|
+
* Use explicit IDs in production for reliability.
|
|
2124
|
+
*
|
|
2125
|
+
* @returns The same WritableSignal instance, now synchronized across tabs
|
|
2126
|
+
*
|
|
2127
|
+
* @throws {Error} When deterministic ID generation fails and no explicit ID is provided
|
|
1176
2128
|
*
|
|
1177
2129
|
* @example
|
|
1178
|
-
* ```
|
|
1179
|
-
*
|
|
1180
|
-
*
|
|
1181
|
-
* async function waitForCount() {
|
|
1182
|
-
* console.log('Waiting for count to be >= 3...');
|
|
1183
|
-
* try {
|
|
1184
|
-
* const finalCount = await until(count, c => c >= 3, { timeout: 5000 });
|
|
1185
|
-
* console.log(`Count reached: ${finalCount}`);
|
|
1186
|
-
* } catch (e: any) { // Ensure 'e' is typed if you access properties like e.message
|
|
1187
|
-
* console.error(e.message); // e.g., "until: Timeout after 5000ms."
|
|
1188
|
-
* }
|
|
1189
|
-
* }
|
|
2130
|
+
* ```typescript
|
|
2131
|
+
* // Basic usage - auto-generates channel ID from call site
|
|
2132
|
+
* const theme = tabSync(signal('dark'));
|
|
1190
2133
|
*
|
|
1191
|
-
* //
|
|
1192
|
-
*
|
|
1193
|
-
* setTimeout(() => count.set(2), 1000);
|
|
1194
|
-
* setTimeout(() => count.set(3), 1500);
|
|
2134
|
+
* // With explicit ID (recommended for production)
|
|
2135
|
+
* const userPrefs = tabSync(signal({ lang: 'en' }), { id: 'user-preferences' });
|
|
1195
2136
|
*
|
|
1196
|
-
*
|
|
2137
|
+
* // Changes in one tab will sync to all other tabs
|
|
2138
|
+
* theme.set('light'); // All tabs will update to 'light'
|
|
1197
2139
|
* ```
|
|
2140
|
+
*
|
|
2141
|
+
* @remarks
|
|
2142
|
+
* - Only works in browser environments (returns original signal on server)
|
|
2143
|
+
* - Uses a single BroadcastChannel for all synchronized signals
|
|
2144
|
+
* - Automatically cleans up listeners when the injection context is destroyed
|
|
2145
|
+
* - Initial signal value after sync setup is not broadcasted to prevent loops
|
|
2146
|
+
*
|
|
1198
2147
|
*/
|
|
2148
|
+
function tabSync(sig, opt) {
|
|
2149
|
+
if (isPlatformServer(inject(PLATFORM_ID)))
|
|
2150
|
+
return sig;
|
|
2151
|
+
const id = opt?.id || generateDeterministicID();
|
|
2152
|
+
const bus = inject(MessageBus);
|
|
2153
|
+
const { unsub, post } = bus.subscribe(id, (next) => sig.set(next));
|
|
2154
|
+
let first = false;
|
|
2155
|
+
const effectRef = effect(() => {
|
|
2156
|
+
const val = sig();
|
|
2157
|
+
if (!first) {
|
|
2158
|
+
first = true;
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
post(val);
|
|
2162
|
+
});
|
|
2163
|
+
inject(DestroyRef).onDestroy(() => {
|
|
2164
|
+
effectRef.destroy();
|
|
2165
|
+
unsub();
|
|
2166
|
+
});
|
|
2167
|
+
return sig;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
1199
2170
|
function until(sourceSignal, predicate, options = {}) {
|
|
1200
2171
|
const injector = options.injector ?? inject(Injector);
|
|
1201
2172
|
return new Promise((resolve, reject) => {
|
|
@@ -1351,6 +2322,8 @@ function withHistory(source, opt) {
|
|
|
1351
2322
|
return;
|
|
1352
2323
|
const valueForRedo = untracked(source);
|
|
1353
2324
|
const valueToRestore = historyStack.at(-1);
|
|
2325
|
+
if (valueToRestore === undefined)
|
|
2326
|
+
return;
|
|
1354
2327
|
originalSet.call(source, valueToRestore);
|
|
1355
2328
|
history.inline((h) => h.pop());
|
|
1356
2329
|
redoArray.inline((r) => r.push(valueForRedo));
|
|
@@ -1361,6 +2334,8 @@ function withHistory(source, opt) {
|
|
|
1361
2334
|
return;
|
|
1362
2335
|
const valueForUndo = untracked(source);
|
|
1363
2336
|
const valueToRestore = redoStack.at(-1);
|
|
2337
|
+
if (valueToRestore === undefined)
|
|
2338
|
+
return;
|
|
1364
2339
|
originalSet.call(source, valueToRestore);
|
|
1365
2340
|
redoArray.inline((r) => r.pop());
|
|
1366
2341
|
history.mutate((h) => {
|
|
@@ -1390,5 +2365,5 @@ function withHistory(source, opt) {
|
|
|
1390
2365
|
* Generated bundle index. Do not edit.
|
|
1391
2366
|
*/
|
|
1392
2367
|
|
|
1393
|
-
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 };
|
|
2368
|
+
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 };
|
|
1394
2369
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|