@mmstack/primitives 21.0.13 → 21.0.14
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.
|
@@ -1,8 +1,214 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
2
|
+
import { isDevMode, inject, Injector, untracked, effect, DestroyRef, linkedSignal, computed, signal, isWritableSignal as isWritableSignal$1, isSignal, ElementRef, PLATFORM_ID, Injectable, runInInjectionContext } from '@angular/core';
|
|
3
3
|
import { isPlatformServer } from '@angular/common';
|
|
4
4
|
import { SIGNAL } from '@angular/core/primitives/signals';
|
|
5
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({ ...(ngDevMode ? { debugName: "internal" } : {}), source,
|
|
198
|
+
computation: (items) => items.slice(0, chunkSize),
|
|
199
|
+
equal });
|
|
200
|
+
nestedEffect((cleanup) => {
|
|
201
|
+
const fullList = source();
|
|
202
|
+
const current = internal();
|
|
203
|
+
if (current.length >= fullList.length)
|
|
204
|
+
return;
|
|
205
|
+
return cleanup(delayFn(() => untracked(() => internal.set(fullList.slice(0, current.length + chunkSize)))));
|
|
206
|
+
}, {
|
|
207
|
+
injector: injector,
|
|
208
|
+
});
|
|
209
|
+
return internal.asReadonly();
|
|
210
|
+
}
|
|
211
|
+
|
|
6
212
|
/**
|
|
7
213
|
* Converts a read-only `Signal` into a `WritableSignal` by providing custom `set` and, optionally, `update` functions.
|
|
8
214
|
* This can be useful for creating controlled write access to a signal that is otherwise read-only.
|
|
@@ -115,19 +321,19 @@ function debounce(source, opt) {
|
|
|
115
321
|
catch {
|
|
116
322
|
// not in injection context & no destroyRef provided opting out of cleanup
|
|
117
323
|
}
|
|
118
|
-
const triggerFn = (
|
|
324
|
+
const triggerFn = (next) => {
|
|
119
325
|
if (timeout)
|
|
120
326
|
clearTimeout(timeout);
|
|
121
|
-
|
|
327
|
+
source.set(next);
|
|
122
328
|
timeout = setTimeout(() => {
|
|
123
329
|
trigger.update((c) => !c);
|
|
124
330
|
}, ms);
|
|
125
331
|
};
|
|
126
332
|
const set = (value) => {
|
|
127
|
-
triggerFn(
|
|
333
|
+
triggerFn(value);
|
|
128
334
|
};
|
|
129
335
|
const update = (fn) => {
|
|
130
|
-
triggerFn((
|
|
336
|
+
triggerFn(fn(untracked(source)));
|
|
131
337
|
};
|
|
132
338
|
const writable = toWritable(computed(() => {
|
|
133
339
|
trigger();
|
|
@@ -289,155 +495,6 @@ function isDerivation(sig) {
|
|
|
289
495
|
return 'from' in sig;
|
|
290
496
|
}
|
|
291
497
|
|
|
292
|
-
const frameStack = [];
|
|
293
|
-
function currentFrame() {
|
|
294
|
-
return frameStack.at(-1) ?? null;
|
|
295
|
-
}
|
|
296
|
-
function clearFrame(frame, userCleanups) {
|
|
297
|
-
frame.parent = null;
|
|
298
|
-
for (const fn of userCleanups) {
|
|
299
|
-
try {
|
|
300
|
-
fn();
|
|
301
|
-
}
|
|
302
|
-
catch (e) {
|
|
303
|
-
if (isDevMode())
|
|
304
|
-
console.error('Error destroying nested effect:', e);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
userCleanups.length = 0;
|
|
308
|
-
for (const child of frame.children) {
|
|
309
|
-
try {
|
|
310
|
-
child.destroy();
|
|
311
|
-
}
|
|
312
|
-
catch (e) {
|
|
313
|
-
if (isDevMode())
|
|
314
|
-
console.error('Error destroying nested effect:', e);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
frame.children.clear();
|
|
318
|
-
}
|
|
319
|
-
function pushFrame(frame) {
|
|
320
|
-
return frameStack.push(frame);
|
|
321
|
-
}
|
|
322
|
-
function popFrame() {
|
|
323
|
-
return frameStack.pop();
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Creates an effect that can be nested, similar to SolidJS's `createEffect`.
|
|
328
|
-
*
|
|
329
|
-
* This primitive enables true hierarchical reactivity. A `nestedEffect` created
|
|
330
|
-
* within another `nestedEffect` is automatically destroyed and recreated when
|
|
331
|
-
* the parent re-runs.
|
|
332
|
-
*
|
|
333
|
-
* It automatically handles injector propagation and lifetime management, allowing
|
|
334
|
-
* you to create fine-grained, conditional side-effects that only track
|
|
335
|
-
* dependencies when they are "live".
|
|
336
|
-
*
|
|
337
|
-
* @param effectFn The side-effect function, which receives a cleanup register function.
|
|
338
|
-
* @param options (Optional) Angular's `CreateEffectOptions`.
|
|
339
|
-
* @returns An `EffectRef` for the created effect.
|
|
340
|
-
*
|
|
341
|
-
* @example
|
|
342
|
-
* ```ts
|
|
343
|
-
* // Assume `coldGuard` changes rarely, but `hotSignal` changes often.
|
|
344
|
-
* const coldGuard = signal(false);
|
|
345
|
-
* const hotSignal = signal(0);
|
|
346
|
-
*
|
|
347
|
-
* nestedEffect(() => {
|
|
348
|
-
* // This outer effect only tracks `coldGuard`.
|
|
349
|
-
* if (coldGuard()) {
|
|
350
|
-
*
|
|
351
|
-
* // This inner effect is CREATED when coldGuard is true
|
|
352
|
-
* // and DESTROYED when it becomes false.
|
|
353
|
-
* nestedEffect(() => {
|
|
354
|
-
* // It only tracks `hotSignal` while it exists.
|
|
355
|
-
* console.log('Hot signal is:', hotSignal());
|
|
356
|
-
* });
|
|
357
|
-
* }
|
|
358
|
-
* // If `coldGuard` is false, this outer effect does not track `hotSignal`.
|
|
359
|
-
* });
|
|
360
|
-
* ```
|
|
361
|
-
* @example
|
|
362
|
-
* ```ts
|
|
363
|
-
* const users = signal([
|
|
364
|
-
* { id: 1, name: 'Alice' },
|
|
365
|
-
* { id: 2, name: 'Bob' }
|
|
366
|
-
* ]);
|
|
367
|
-
*
|
|
368
|
-
* // The fine-grained mapped list
|
|
369
|
-
* const mappedUsers = mapArray(
|
|
370
|
-
* users,
|
|
371
|
-
* (userSignal, index) => {
|
|
372
|
-
* // 1. Create a fine-grained SIDE EFFECT for *this item*
|
|
373
|
-
* // This effect's lifetime is now tied to this specific item. created once on init of this index.
|
|
374
|
-
* const effectRef = nestedEffect(() => {
|
|
375
|
-
* // This only runs if *this* userSignal changes,
|
|
376
|
-
* // not if the whole list changes.
|
|
377
|
-
* console.log(`User ${index} updated:`, userSignal().name);
|
|
378
|
-
* });
|
|
379
|
-
*
|
|
380
|
-
* // 2. Return the data AND the cleanup logic
|
|
381
|
-
* return {
|
|
382
|
-
* // The mapped data
|
|
383
|
-
* label: computed(() => `User: ${userSignal().name}`),
|
|
384
|
-
*
|
|
385
|
-
* // The cleanup function
|
|
386
|
-
* destroyEffect: () => effectRef.destroy()
|
|
387
|
-
* };
|
|
388
|
-
* },
|
|
389
|
-
* {
|
|
390
|
-
* // 3. Tell mapArray HOW to clean up when an item is removed, this needs to be manual as it's not a nestedEffect itself
|
|
391
|
-
* onDestroy: (mappedItem) => {
|
|
392
|
-
* mappedItem.destroyEffect();
|
|
393
|
-
* }
|
|
394
|
-
* }
|
|
395
|
-
* );
|
|
396
|
-
* ```
|
|
397
|
-
*/
|
|
398
|
-
function nestedEffect(effectFn, options) {
|
|
399
|
-
const bindToFrame = options?.bindToFrame ?? ((parent) => parent);
|
|
400
|
-
const parent = bindToFrame(currentFrame());
|
|
401
|
-
const injector = options?.injector ?? parent?.injector ?? inject(Injector);
|
|
402
|
-
let isDestroyed = false;
|
|
403
|
-
const srcRef = untracked(() => {
|
|
404
|
-
return effect((cleanup) => {
|
|
405
|
-
if (isDestroyed)
|
|
406
|
-
return;
|
|
407
|
-
const frame = {
|
|
408
|
-
injector,
|
|
409
|
-
parent,
|
|
410
|
-
children: new Set(),
|
|
411
|
-
};
|
|
412
|
-
const userCleanups = [];
|
|
413
|
-
pushFrame(frame);
|
|
414
|
-
try {
|
|
415
|
-
effectFn((fn) => userCleanups.push(fn));
|
|
416
|
-
}
|
|
417
|
-
finally {
|
|
418
|
-
popFrame();
|
|
419
|
-
}
|
|
420
|
-
return cleanup(() => clearFrame(frame, userCleanups));
|
|
421
|
-
}, {
|
|
422
|
-
...options,
|
|
423
|
-
injector,
|
|
424
|
-
manualCleanup: options?.manualCleanup ?? !!parent,
|
|
425
|
-
});
|
|
426
|
-
});
|
|
427
|
-
const ref = {
|
|
428
|
-
destroy: () => {
|
|
429
|
-
if (isDestroyed)
|
|
430
|
-
return;
|
|
431
|
-
isDestroyed = true;
|
|
432
|
-
parent?.children.delete(ref);
|
|
433
|
-
srcRef.destroy();
|
|
434
|
-
},
|
|
435
|
-
};
|
|
436
|
-
parent?.children.add(ref);
|
|
437
|
-
injector.get(DestroyRef).onDestroy(() => ref.destroy());
|
|
438
|
-
return ref;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
498
|
function isWritableSignal(value) {
|
|
442
499
|
return isWritableSignal$1(value);
|
|
443
500
|
}
|
|
@@ -1149,8 +1206,7 @@ function throttle(source, opt) {
|
|
|
1149
1206
|
catch {
|
|
1150
1207
|
// not in injection context & no destroyRef provided opting out of cleanup
|
|
1151
1208
|
}
|
|
1152
|
-
const
|
|
1153
|
-
updateSourceAction();
|
|
1209
|
+
const tick = () => {
|
|
1154
1210
|
if (timeout)
|
|
1155
1211
|
return;
|
|
1156
1212
|
timeout = setTimeout(() => {
|
|
@@ -1159,10 +1215,12 @@ function throttle(source, opt) {
|
|
|
1159
1215
|
}, ms);
|
|
1160
1216
|
};
|
|
1161
1217
|
const set = (value) => {
|
|
1162
|
-
|
|
1218
|
+
source.set(value);
|
|
1219
|
+
tick();
|
|
1163
1220
|
};
|
|
1164
1221
|
const update = (fn) => {
|
|
1165
|
-
|
|
1222
|
+
source.update(fn);
|
|
1223
|
+
tick();
|
|
1166
1224
|
};
|
|
1167
1225
|
const writable = toWritable(computed(() => {
|
|
1168
1226
|
trigger();
|
|
@@ -2153,5 +2211,5 @@ function withHistory(source, opt) {
|
|
|
2153
2211
|
* Generated bundle index. Do not edit.
|
|
2154
2212
|
*/
|
|
2155
2213
|
|
|
2156
|
-
export { 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 };
|
|
2214
|
+
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 };
|
|
2157
2215
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|