@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 { computed, untracked, signal, inject, DestroyRef, isDevMode, Injector, effect, isWritableSignal as isWritableSignal$1, isSignal, linkedSignal, ElementRef, PLATFORM_ID, Injectable, runInInjectionContext } from '@angular/core';
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 = (afterClean) => {
324
+ const triggerFn = (next) => {
119
325
  if (timeout)
120
326
  clearTimeout(timeout);
121
- afterClean();
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(() => source.set(value));
333
+ triggerFn(value);
128
334
  };
129
335
  const update = (fn) => {
130
- triggerFn(() => source.update(fn));
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 triggerFn = (updateSourceAction) => {
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
- triggerFn(() => source.set(value));
1218
+ source.set(value);
1219
+ tick();
1163
1220
  };
1164
1221
  const update = (fn) => {
1165
- triggerFn(() => source.update(fn));
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