@mmstack/primitives 21.0.12 → 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();
|
|
@@ -1576,6 +1634,14 @@ const SIGNAL_FN_PROP = new Set([
|
|
|
1576
1634
|
'inline',
|
|
1577
1635
|
'asReadonly',
|
|
1578
1636
|
]);
|
|
1637
|
+
/**
|
|
1638
|
+
* Validates whether a value is a Signal Store.
|
|
1639
|
+
*/
|
|
1640
|
+
function isStore(value) {
|
|
1641
|
+
return (typeof value === 'object' &&
|
|
1642
|
+
value !== null &&
|
|
1643
|
+
value[IS_STORE] === true);
|
|
1644
|
+
}
|
|
1579
1645
|
/**
|
|
1580
1646
|
* @experimental This API is experimental and may change or be removed in future releases.
|
|
1581
1647
|
* Converts a Signal into a deep-observable Store.
|
|
@@ -1585,7 +1651,7 @@ const SIGNAL_FN_PROP = new Set([
|
|
|
1585
1651
|
* const nameSignal = state.user.name; // WritableSignal<string>
|
|
1586
1652
|
*/
|
|
1587
1653
|
function toStore(source, injector) {
|
|
1588
|
-
if (source
|
|
1654
|
+
if (isStore(source))
|
|
1589
1655
|
return source;
|
|
1590
1656
|
if (!injector)
|
|
1591
1657
|
injector = inject(Injector);
|
|
@@ -2145,5 +2211,5 @@ function withHistory(source, opt) {
|
|
|
2145
2211
|
* Generated bundle index. Do not edit.
|
|
2146
2212
|
*/
|
|
2147
2213
|
|
|
2148
|
-
export { combineWith, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, indexArray, isDerivation, isMutable, 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 };
|
|
2149
2215
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|