@mmstack/primitives 19.0.4 → 19.0.5

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Miha Mulec
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,229 @@
1
- # mmstack/primitives
1
+ # @mmstack/primitives
2
2
 
3
- A library for signal primitives, still working on the docs ;)
3
+ A collection of utility functions and primitives designed to enhance development with Angular Signals, providing helpful patterns and inspired by features from other reactive libraries. All value helpers also use pure derivations (no effects/RxJS).
4
+
5
+ [![npm version](https://badge.fury.io/js/%40mmstack%2Fprimitives.svg)](https://badge.fury.io/js/%40mmstack%2Fprimitives)
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @mmstack/primitives
11
+ ```
12
+
13
+ ## Primitives
14
+
15
+ This library provides the following primitives:
16
+
17
+ - `debounced` - Creates a writable signal whose value updates are debounced after set/update.
18
+ - `mutable` - A signal variant allowing in-place mutations while triggering updates.
19
+ - `stored` - Creates a signal synchronized with persistent storage (e.g., localStorage).
20
+ - `mapArray` - Maps a reactive array efficently into an array of stable derivations.
21
+ - `toWritable` - Converts a read-only signal to writable using custom write logic.
22
+ - `derived` - Creates a signal with two-way binding to a source signal.
23
+
24
+ ---
25
+
26
+ ### debounced
27
+
28
+ Creates a WritableSignal where the propagation of its value (after calls to .set() or .update()) is delayed. The publicly readable signal value updates only after a specified time (ms) has passed without further set/update calls. It also includes an .original property, which is a Signal reflecting the value immediately after set/update is called.
29
+
30
+ ```typescript
31
+ import { Component, signal, effect } from '@angular/core';
32
+ import { debounced } from '@mmstack/primitives';
33
+ import { FormsModule } from '@angular/forms';
34
+
35
+ @Component({
36
+ selector: 'app-debounced',
37
+ template: `<input [(ngModel)]="searchTerm" />`,
38
+ })
39
+ export class SearchComponent {
40
+ searchTerm = debounced('', { ms: 300 }); // Debounce for 300ms
41
+
42
+ constructor() {
43
+ effect(() => {
44
+ // Runs 300ms after the user stops typing
45
+ console.log('Perform search for:', this.searchTerm());
46
+ });
47
+ effect(() => {
48
+ // Runs immediately on input change
49
+ console.log('Input value:', this.searchTerm.original());
50
+ });
51
+ }
52
+ }
53
+ ```
54
+
55
+ ### mutable
56
+
57
+ Creates a MutableSignal, a signal variant designed for scenarios where you want to perform in-place mutations on objects or arrays held within the signal, while still ensuring Angular's change detection is correctly triggered. It provides .mutate() and .inline() methods alongside the standard .set() and .update(). Please note that any computeds, which resolve non-primitive values from a mutable require equals to be set to false.
58
+
59
+ ```typescript
60
+ import { Component, computed, effect } from '@angular/core';
61
+ import { mutable } from '@mmstack/primitives';
62
+ import { FormsModule } from '@angular/forms';
63
+
64
+ @Component({
65
+ selector: 'app-mutable',
66
+ template: ` <button (click)="incrementAge()">inc</button> `,
67
+ })
68
+ export class SearchComponent {
69
+ user = mutable({ name: { first: 'John', last: 'Doe' }, age: 30 });
70
+
71
+ constructor() {
72
+ effect(() => {
73
+ // Runs every time user is mutated
74
+ console.log(this.user());
75
+ });
76
+
77
+ const age = computed(() => this.user().age);
78
+
79
+ effect(() => {
80
+ // Runs every time age changes
81
+ console.log(age());
82
+ });
83
+
84
+ const name = computed(() => this.user().name);
85
+ effect(() => {
86
+ // Doesnt run if user changes, unless name is destructured
87
+ console.log(name());
88
+ });
89
+
90
+ const name2 = computed(() => this.user().name, {
91
+ equal: () => false,
92
+ });
93
+
94
+ effect(() => {
95
+ // Runs every time user changes (even if name did not change)
96
+ console.log(name2());
97
+ });
98
+ }
99
+
100
+ incrementAge() {
101
+ user.mutate((prev) => {
102
+ prev.age++;
103
+ return prev;
104
+ });
105
+ }
106
+
107
+ incrementInline() {
108
+ user.inline((prev) => {
109
+ prev.age++;
110
+ });
111
+ }
112
+ }
113
+ ```
114
+
115
+ ### stored
116
+
117
+ Creates a WritableSignal whose state is automatically synchronized with persistent storage (like localStorage or sessionStorage), providing a fallback value when no data is found or fails to parse.
118
+
119
+ It handles Server-Side Rendering (SSR) gracefully, allows dynamic storage keys, custom serialization/deserialization, custom storage providers, and optional synchronization across browser tabs via the storage event. It returns a StoredSignal<T> which includes a .clear() method and a reactive .key signal.
120
+
121
+ ```typescript
122
+ import { Component, effect, signal } from '@angular/core';
123
+ import { stored } from '@mmstack/primitives';
124
+ // import { FormsModule } from '@angular/forms'; // Needed for ngModel
125
+
126
+ @Component({
127
+ selector: 'app-theme-selector',
128
+ standalone: true,
129
+ // imports: [FormsModule], // Import if using ngModel
130
+ template: `
131
+ Theme:
132
+ <select [value]="theme()" (change)="theme.set($event.target.value)">
133
+ <option value="light">Light</option>
134
+ <option value="dark">Dark</option>
135
+ <option value="system">System</option>
136
+ </select>
137
+ <button (click)="theme.clear()">Reset Theme</button>
138
+ <p>Using storage key: {{ theme.key() }}</p>
139
+ `,
140
+ })
141
+ export class ThemeSelectorComponent {
142
+ // Persist theme preference in localStorage, default to 'system'
143
+ theme = stored<'light' | 'dark' | 'system'>('system', {
144
+ key: 'user-theme',
145
+ syncTabs: true, // Sync theme choice across tabs
146
+ });
147
+
148
+ constructor() {
149
+ effect(() => {
150
+ console.log(`Theme set to: ${this.theme()}`);
151
+ // Logic to apply theme (e.g., add class to body)
152
+ document.body.className = `theme-${this.theme()}`;
153
+ });
154
+ }
155
+ }
156
+ ```
157
+
158
+ ### mapArray
159
+
160
+ Reactive map helper that stabilizes a source array Signal by length. It provides stability by giving the mapping function a stable Signal<T> for each item based on its index. Sub signals are not re-created, rather they propagate value updates through. This is particularly useful for rendering lists (@for) as it minimizes DOM changes when array items change identity but represent the same conceptual entity.
161
+
162
+ ```typescript
163
+ import { Component, signal } from '@angular/core';
164
+ import { mapArray } from '@mmstack/primitives';
165
+
166
+ @Component({
167
+ selector: 'app-map-demo',
168
+ template: `
169
+ <ul>
170
+ @for (item of displayItems(); track item) {
171
+ <li>{{ item() }}</li>
172
+ }
173
+ </ul>
174
+ <button (click)="addItem()">Add</button>
175
+ <button (click)="updateFirst()">Update First</button>
176
+ `,
177
+ })
178
+ export class ListComponent {
179
+ sourceItems = signal([
180
+ { id: 1, name: 'A' },
181
+ { id: 2, name: 'B' },
182
+ ]);
183
+
184
+ readonly displayItems = mapArray(this.sourceItems, (child, index) => computed(() => `Item ${index}: ${child().name}`));
185
+
186
+ addItem() {
187
+ this.sourceItems.update((items) => [...items, { id: Date.now(), name: String.fromCharCode(67 + items.length - 2) }]);
188
+ }
189
+
190
+ updateFirst() {
191
+ this.sourceItems.update((items) => {
192
+ items[0] = { ...items[0], name: items[0].name + '+' };
193
+ return [...items]; // New array, but mapArray keeps stable signals
194
+ });
195
+ }
196
+ }
197
+ ```
198
+
199
+ ### toWritable
200
+
201
+ A utility function that converts a read-only Signal into a WritableSignal by allowing you to provide custom implementations for the .set() and .update() methods. This is useful for creating controlled write access to signals that are naturally read-only (like those created by computed). This is used under the hood in derived.
202
+
203
+ ```typescript
204
+ import { Component, signal, effect } from '@angular/core';
205
+ import { toWritable } from '@mmstack/primitives';
206
+
207
+ const user = signal({ name: 'John' });
208
+
209
+ const name = toWritable(
210
+ computed(() => user().name),
211
+ (name) => user.update((prev) => ({ ...prev, name })),
212
+ ); // WritableSignal<string> bound to user signal
213
+ ```
214
+
215
+ ### derived
216
+
217
+ Creates a WritableSignal that represents a part of another source WritableSignal (e.g., an object property or an array element), enabling two-way data binding. Changes to the source update the derived signal, and changes to the derived signal (via .set() or .update()) update the source signal accordingly.
218
+
219
+ ```typescript
220
+ const user = signal({ name: 'John' });
221
+
222
+ const name = derived(user, 'name'); // WritableSignal<string>, which updates user signal & reacts to changes in the name property
223
+
224
+ // Full syntax example
225
+ const name2 = derived(user, {
226
+ from: (u) => u.name,
227
+ onChange: (name) => user.update((prev) => ({ ...prev, name })),
228
+ });
229
+ ```
@@ -1,4 +1,5 @@
1
- import { untracked, signal, computed, isSignal, linkedSignal } from '@angular/core';
1
+ import { untracked, signal, computed, isSignal, linkedSignal, inject, PLATFORM_ID, isDevMode, effect, DestroyRef } from '@angular/core';
2
+ import { isPlatformServer } from '@angular/common';
2
3
 
3
4
  /**
4
5
  * Converts a read-only `Signal` into a `WritableSignal` by providing custom `set` and, optionally, `update` functions.
@@ -36,38 +37,88 @@ function toWritable(signal, set, update) {
36
37
  return internal;
37
38
  }
38
39
 
39
- function debounced(value, opt) {
40
- const sig = signal(value, opt);
41
- const ms = opt?.ms ?? 300;
42
- let timeout;
43
- const originalSet = sig.set;
44
- const originalUpdate = sig.update;
40
+ /**
41
+ * Creates a `WritableSignal` whose publicly readable value is updated only after
42
+ * a specified debounce period (`ms`) has passed since the last call to its
43
+ * `.set()` or `.update()` method.
44
+ *
45
+ * This implementation avoids using `effect` by leveraging intermediate `computed`
46
+ * signals and a custom `equal` function to delay value propagation based on a timer.
47
+ *
48
+ * @template T The type of value the signal holds.
49
+ * @param initial The initial value of the signal.
50
+ * @param opt Options for signal creation, including:
51
+ * - `ms`: The debounce time in milliseconds. Defaults to 0 if omitted (no debounce).
52
+ * - Other `CreateSignalOptions` (like `equal`) are passed to underlying signals.
53
+ * @returns A `DebouncedSignal<T>` instance. Its readable value updates are debounced,
54
+ * and it includes an `.original` property providing immediate access to the latest set value.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * import { effect } from '@angular/core';
59
+ *
60
+ * // Create a debounced signal with a 500ms delay
61
+ * const query = debounced('', { ms: 500 });
62
+ *
63
+ * effect(() => {
64
+ * // This effect runs 500ms after the last change to 'query'
65
+ * console.log('Debounced Query:', query());
66
+ * });
67
+ *
68
+ * effect(() => {
69
+ * // This effect runs immediately when 'query.original' changes
70
+ * console.log('Original Query:', query.original());
71
+ * });
72
+ *
73
+ * console.log('Setting query to "a"');
74
+ * query.set('a');
75
+ * // Output: Original Query: a
76
+ *
77
+ * setTimeout(() => {
78
+ * console.log('Setting query to "ab"');
79
+ * query.set('ab');
80
+ * // Output: Original Query: ab
81
+ * }, 200); // Before debounce timeout
82
+ *
83
+ * setTimeout(() => {
84
+ * console.log('Setting query to "abc"');
85
+ * query.set('abc');
86
+ * // Output: Original Query: abc
87
+ * }, 400); // Before debounce timeout
88
+ *
89
+ * // ~500ms after the *last* set (at 400ms), the debounced effect runs:
90
+ * // Output (at ~900ms): Debounced Query: abc
91
+ * ```
92
+ */
93
+ function debounced(initial, opt) {
94
+ const internal = signal(initial, opt);
95
+ const ms = opt.ms ?? 0;
45
96
  const trigger = signal(false);
46
- // Set on the original signal, then trigger the debounced update
97
+ let timeout;
47
98
  const set = (value) => {
48
- originalSet(value); // Update the *original* signal immediately
49
99
  if (timeout)
50
100
  clearTimeout(timeout);
101
+ internal.set(value);
51
102
  timeout = setTimeout(() => {
52
- trigger.update((cur) => !cur); // Trigger the computed signal
103
+ trigger.update((c) => !c);
53
104
  }, ms);
54
105
  };
55
- // Update on the original signal, then trigger the debounced update
56
106
  const update = (fn) => {
57
- originalUpdate(fn); // Update the *original* signal immediately
58
107
  if (timeout)
59
108
  clearTimeout(timeout);
109
+ internal.update(fn);
60
110
  timeout = setTimeout(() => {
61
- trigger.update((cur) => !cur); // Trigger the computed signal
111
+ trigger.update((c) => !c);
62
112
  }, ms);
63
113
  };
64
- // Create a computed signal that depends on the trigger.
65
- // This computed signal is what provides the debounced behavior.
66
- const writable = toWritable(computed(() => {
67
- trigger();
68
- return untracked(sig);
69
- }), set, update);
70
- writable.original = sig;
114
+ const stable = computed(() => ({
115
+ trigger: trigger(),
116
+ value: internal(),
117
+ }), {
118
+ equal: (a, b) => a.trigger === b.trigger,
119
+ });
120
+ const writable = toWritable(computed(() => stable().value, opt), set, update);
121
+ writable.original = internal;
71
122
  return writable;
72
123
  }
73
124
 
@@ -132,32 +183,84 @@ function isDerivation(sig) {
132
183
  return 'from' in sig;
133
184
  }
134
185
 
135
- function createReconciler(source, opt) {
136
- const map = opt?.map ?? ((source, index) => source[index]);
137
- return (length, prev) => {
138
- if (!prev)
139
- return Array.from({ length }, (_, i) => computed(() => map(source(), i), opt));
140
- if (length === prev.source)
141
- return prev.value;
142
- if (length < prev.source) {
143
- return prev.value.slice(0, length);
144
- }
145
- else {
146
- const next = [...prev.value];
147
- for (let i = prev.source; i < length; i++) {
148
- next.push(computed(() => map(source(), i), opt));
149
- }
150
- return next;
151
- }
152
- };
153
- }
154
- function mapArray(source, opt) {
186
+ /**
187
+ * Reactively maps items from a source array (or signal of an array) using a provided mapping function.
188
+ *
189
+ * This function serves a similar purpose to SolidJS's `mapArray` by providing stability
190
+ * for mapped items. It receives a source function returning an array (or a Signal<T[]>)
191
+ * and a mapping function.
192
+ *
193
+ * For each item in the source array, it creates a stable `computed` signal representing
194
+ * that item's value at its current index. This stable signal (`Signal<T>`) is passed
195
+ * to the mapping function. This ensures that downstream computations or components
196
+ * depending on the mapped result only re-render or re-calculate for the specific items
197
+ * that have changed, or when items are added/removed, rather than re-evaluating everything
198
+ * when the source array reference changes but items remain the same.
199
+ *
200
+ * It efficiently handles changes in the source array's length by reusing existing mapped
201
+ * results when possible, slicing when the array shrinks, and appending new mapped items
202
+ * when it grows.
203
+ *
204
+ * @template T The type of items in the source array.
205
+ * @template U The type of items in the resulting mapped array.
206
+ *
207
+ * @param source A function returning the source array `T[]`, or a `Signal<T[]>` itself.
208
+ * The `mapArray` function will reactively update based on changes to this source.
209
+ * @param map The mapping function. It is called for each item in the source array.
210
+ * It receives:
211
+ * - `value`: A stable `Signal<T>` representing the item at the current index.
212
+ * Use this signal within your mapping logic if you need reactivity
213
+ * tied to the specific item's value changes.
214
+ * - `index`: The number index of the item in the array.
215
+ * It should return the mapped value `U`.
216
+ * @param [opt] Optional `CreateSignalOptions<T>`. These options are passed directly
217
+ * to the `computed` signal created for each individual item (`Signal<T>`).
218
+ * This allows specifying options like a custom `equal` function for item comparison.
219
+ *
220
+ * @returns A `Signal<U[]>` containing the mapped array. This signal updates whenever
221
+ * the source array changes (either length or the values of its items).
222
+ *
223
+ * @example
224
+ * ```ts
225
+ * const sourceItems = signal([
226
+ * { id: 1, name: 'Apple' },
227
+ * { id: 2, name: 'Banana' }
228
+ * ]);
229
+ *
230
+ * const mappedItems = mapArray(
231
+ * sourceItems,
232
+ * (itemSignal, index) => {
233
+ * // itemSignal is stable for a given item based on its index.
234
+ * // We create a computed here to react to changes in the item's name.
235
+ * return computed(() => `${index}: ${itemSignal().name.toUpperCase()}`);
236
+ * },
237
+ * // Example optional options (e.g., custom equality for item signals)
238
+ * { equal: (a, b) => a.id === b.id && a.name === b.name }
239
+ * );
240
+ * ```
241
+ */
242
+ function mapArray(source, map, opt) {
155
243
  const data = isSignal(source) ? source : computed(source);
156
- const length = computed(() => data().length);
157
- const reconciler = createReconciler(data, opt);
244
+ const len = computed(() => data().length);
158
245
  return linkedSignal({
159
- source: () => length(),
160
- computation: (len, prev) => reconciler(len, prev),
246
+ source: () => len(),
247
+ computation: (len, prev) => {
248
+ if (!prev)
249
+ return Array.from({ length: len }, (_, i) => map(computed(() => source()[i], opt), i));
250
+ if (len === prev.value.length)
251
+ return prev.value;
252
+ if (len < prev.value.length) {
253
+ return prev.value.slice(0, len);
254
+ }
255
+ else {
256
+ const next = [...prev.value];
257
+ for (let i = prev.value.length; i < len; i++) {
258
+ next[i] = map(computed(() => source()[i], opt), i);
259
+ }
260
+ return next;
261
+ }
262
+ },
263
+ equal: (a, b) => a.length === b.length,
161
264
  });
162
265
  }
163
266
 
@@ -212,9 +315,140 @@ function isMutable(value) {
212
315
  return 'mutate' in value && typeof value.mutate === 'function';
213
316
  }
214
317
 
318
+ // Internal dummy store for server-side rendering
319
+ const noopStore = {
320
+ getItem: () => null,
321
+ setItem: () => {
322
+ /* noop */
323
+ },
324
+ removeItem: () => {
325
+ /* noop */
326
+ },
327
+ };
328
+ /**
329
+ * Creates a `WritableSignal` whose state is automatically synchronized with persistent storage
330
+ * (like `localStorage` or `sessionStorage`).
331
+ *
332
+ * It handles Server-Side Rendering (SSR) gracefully, allows dynamic storage keys,
333
+ * custom serialization/deserialization, custom storage providers, and optional
334
+ * synchronization across browser tabs.
335
+ *
336
+ * @template T The type of value held by the signal and stored (after serialization).
337
+ * @param fallback The default value of type `T` to use when no value is found in storage
338
+ * or when deserialization fails. The signal's value will never be `null` or `undefined`
339
+ * publicly, it will always revert to this fallback.
340
+ * @param options Configuration options (`CreateStoredOptions<T>`). Requires at least the `key`.
341
+ * @returns A `StoredSignal<T>` instance. This signal behaves like a standard `WritableSignal<T>`,
342
+ * but its value is persisted. It includes a `.clear()` method to remove the item from storage
343
+ * and a `.key` signal providing the current storage key.
344
+ *
345
+ * @remarks
346
+ * - **Persistence:** The signal automatically saves its value to storage whenever the signal's
347
+ * value or its configured `key` changes. This is managed internally using `effect`.
348
+ * - **SSR Safety:** Detects server environments and uses a no-op storage, preventing errors.
349
+ * - **Error Handling:** Catches and logs errors during serialization/deserialization in dev mode.
350
+ * - **Tab Sync:** If `syncTabs` is true, listens to `storage` events to keep the signal value
351
+ * consistent across browser tabs using the same key. Cleanup is handled automatically
352
+ * using `DestroyRef`.
353
+ * - **Removal:** Use the `.clear()` method on the returned signal to remove the item from storage.
354
+ * Setting the signal to the fallback value will store the fallback value, not remove the item.
355
+ *
356
+ * @example
357
+ * ```ts
358
+ * import { Component, effect, signal } from '@angular/core';
359
+ * import { stored } from '@mmstack/primitives'; // Adjust import path
360
+ *
361
+ * @Component({
362
+ * selector: 'app-settings',
363
+ * standalone: true,
364
+ * template: `
365
+ * Theme:
366
+ * <select [ngModel]="theme()" (ngModelChange)="theme.set($event)">
367
+ * <option value="light">Light</option>
368
+ * <option value="dark">Dark</option>
369
+ * </select>
370
+ * <button (click)="theme.clear()">Clear Theme Setting</button>
371
+ * <p>Storage Key Used: {{ theme.key() }}</p>
372
+ * ` // Requires FormsModule for ngModel
373
+ * })
374
+ * export class SettingsComponent {
375
+ * theme = stored<'light' | 'dark'>('light', { key: 'app-theme', syncTabs: true });
376
+ * }
377
+ * ```
378
+ */
379
+ function stored(fallback, { key, store: providedStore, serialize = JSON.stringify, deserialize = JSON.parse, syncTabs = false, equal = Object.is, ...rest }) {
380
+ const isServer = isPlatformServer(inject(PLATFORM_ID));
381
+ const fallbackStore = isServer ? noopStore : localStorage;
382
+ const store = providedStore ?? fallbackStore;
383
+ const keySig = typeof key === 'string'
384
+ ? computed(() => key)
385
+ : isSignal(key)
386
+ ? key
387
+ : computed(key);
388
+ const getValue = (key) => {
389
+ const found = store.getItem(key);
390
+ if (found === null)
391
+ return null;
392
+ try {
393
+ return deserialize(found);
394
+ }
395
+ catch (err) {
396
+ if (isDevMode())
397
+ console.error(`Failed to parse stored value for key "${key}":`, err);
398
+ return null;
399
+ }
400
+ };
401
+ const storeValue = (key, value) => {
402
+ try {
403
+ if (value === null)
404
+ return store.removeItem(key);
405
+ const serialized = serialize(value);
406
+ store.setItem(key, serialized);
407
+ }
408
+ catch (err) {
409
+ if (isDevMode())
410
+ console.error(`Failed to store value for key "${key}":`, err);
411
+ }
412
+ };
413
+ const opt = {
414
+ ...rest,
415
+ equal,
416
+ };
417
+ const internal = signal(getValue(untracked(keySig)), {
418
+ ...opt,
419
+ equal: (a, b) => {
420
+ if (a === null && b === null)
421
+ return true;
422
+ if (a === null || b === null)
423
+ return false;
424
+ return equal(a, b);
425
+ },
426
+ });
427
+ effect(() => storeValue(keySig(), internal()));
428
+ if (syncTabs && !isServer) {
429
+ const destroyRef = inject(DestroyRef);
430
+ const sync = (e) => {
431
+ if (e.key !== untracked(keySig))
432
+ return;
433
+ if (e.newValue === null)
434
+ internal.set(null);
435
+ else
436
+ internal.set(getValue(e.key));
437
+ };
438
+ window.addEventListener('storage', sync);
439
+ destroyRef.onDestroy(() => window.removeEventListener('storage', sync));
440
+ }
441
+ const writable = toWritable(computed(() => internal() ?? fallback, opt), internal.set);
442
+ writable.clear = () => {
443
+ internal.set(null);
444
+ };
445
+ writable.key = keySig;
446
+ return writable;
447
+ }
448
+
215
449
  /**
216
450
  * Generated bundle index. Do not edit.
217
451
  */
218
452
 
219
- export { debounced, derived, isDerivation, isMutable, mapArray, mutable, toFakeDerivation, toFakeSignalDerivation, toWritable };
453
+ export { debounced, derived, isDerivation, isMutable, mapArray, mutable, stored, toFakeDerivation, toFakeSignalDerivation, toWritable };
220
454
  //# sourceMappingURL=mmstack-primitives.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"mmstack-primitives.mjs","sources":["../../../../packages/primitives/src/lib/to-writable.ts","../../../../packages/primitives/src/lib/debounced.ts","../../../../packages/primitives/src/lib/derived.ts","../../../../packages/primitives/src/lib/map-array.ts","../../../../packages/primitives/src/lib/mutable.ts","../../../../packages/primitives/src/mmstack-primitives.ts"],"sourcesContent":["import { Signal, untracked, WritableSignal } from '@angular/core';\n\n/**\n * Converts a read-only `Signal` into a `WritableSignal` by providing custom `set` and, optionally, `update` functions.\n * This can be useful for creating controlled write access to a signal that is otherwise read-only.\n *\n * @typeParam T - The type of value held by the signal.\n *\n * @param signal - The read-only `Signal` to be made writable.\n * @param set - A function that will be used to set the signal's value. This function *must* handle\n * the actual update mechanism (e.g., updating a backing store, emitting an event, etc.).\n * @param update - (Optional) A function that will be used to update the signal's value based on its\n * previous value. If not provided, a default `update` implementation is used that\n * calls the provided `set` function with the result of the updater function. The\n * default implementation uses `untracked` to avoid creating unnecessary dependencies\n * within the updater function.\n *\n * @returns A `WritableSignal` that uses the provided `set` and `update` functions. The `asReadonly`\n * method of the returned signal will still return the original read-only signal.\n *\n * @example\n * // Basic usage: Making a read-only signal writable with a custom set function.\n * const originalValue = signal({a: 0});\n * const readOnlySignal = computed(() => originalValue().a);\n * const writableSignal = toWritable(readOnlySignal, (newValue) => {\n * originalValue.update((prev) => { ...prev, a: newValue });\n * });\n *\n * writableSignal.set(5); // sets value of originalValue.a to 5 & triggers all signals\n */\nexport function toWritable<T>(\n signal: Signal<T>,\n set: (value: T) => void,\n update?: (updater: (value: T) => T) => void,\n): WritableSignal<T> {\n const internal = signal as WritableSignal<T>;\n internal.asReadonly = () => signal;\n internal.set = set;\n internal.update = update ?? ((updater) => set(updater(untracked(internal))));\n\n return internal;\n}\n","import {\n computed,\n type CreateSignalOptions,\n signal,\n untracked,\n type WritableSignal,\n} from '@angular/core';\nimport { toWritable } from './to-writable';\n\n/**\n * A `DebouncedSignal` is a special type of `WritableSignal` that delays updates\n * to its value. This is useful for scenarios where you want to avoid\n * frequent updates, such as responding to user input in a search field.\n * It keeps a reference to the original `WritableSignal` via the `original` property.\n *\n * @typeParam T - The type of value held by the signal.\n */\nexport type DebouncedSignal<T> = WritableSignal<T> & {\n /**\n * A reference to the original, un-debounced `WritableSignal`. This allows\n * you to access the immediate value (without the debounce delay) if needed,\n * and also ensures that any direct modifications to the original signal\n * are reflected in the debounced signal after the debounce period.\n */\n original: WritableSignal<T>;\n};\n\n/**\n * Options for creating a debounced signal.\n *\n * @typeParam T - The type of value held by the signal.\n */\nexport type CreateDebouncedOptions<T> = CreateSignalOptions<T> & {\n /**\n * The debounce delay in milliseconds. Defaults to 300.\n */\n ms?: number;\n};\n\n/**\n * Creates a debounced signal. The signal's value will only be propagated to\n * subscribers after a specified delay (debounce time) has passed since the last\n * time it was set or updated. Crucially, updates to the *original* signal\n * are also debounced.\n *\n * @see {@link DebouncedSignal}\n * @see {@link CreateDebouncedOptions}\n *\n * @example\n * ```typescript\n * // Create a debounced signal with an initial value and a custom delay.\n * const searchTerm = debounced('initial value', { ms: 500 });\n *\n * // Update the debounced signal. The actual update will be delayed by 500ms.\n * searchTerm.set('new value');\n *\n * // Access the original, un-debounced signal.\n * console.log(searchTerm.original()); // Outputs 'new value' (immediately)\n * // ... after 500ms ...\n * console.log(searchTerm()); // Outputs 'new value' (debounced)\n *\n * // Directly update the *original* signal.\n * searchTerm.original.set('direct update');\n * console.log(searchTerm.original()); // Outputs 'direct update' (immediately)\n * console.log(searchTerm()); // Outputs 'new value' (still debounced from the previous set)\n * // ... after 500ms ...\n * console.log(searchTerm()); // Outputs 'direct update' (now reflects the original signal)\n *\n * // Create a debounced signal with undefined initial value and default delay\n * const anotherSignal = debounced();\n * ```\n * @typeParam T - The type of the signal's value.\n * @param initial The initial value of the signal. Optional; defaults to `undefined`.\n * @param opt Configuration options for the signal, including the debounce delay (`ms`).\n * @returns A `DebouncedSignal` instance.\n */\nexport function debounced<T>(): DebouncedSignal<T | undefined>;\n/**\n * Creates a debounced signal with a defined initial value.\n *\n * @typeParam T - The type of the signal's value.\n * @param initial The initial value of the signal.\n * @param opt Configuration options for the signal, including the debounce delay (`ms`).\n * @returns A `DebouncedSignal` instance.\n */\nexport function debounced<T>(\n initial: T,\n opt?: CreateDebouncedOptions<T>,\n): DebouncedSignal<T>;\nexport function debounced<T>(\n value?: T,\n opt?: CreateDebouncedOptions<T | undefined>,\n): DebouncedSignal<T | undefined> {\n const sig = signal<T | undefined>(value, opt) as DebouncedSignal<\n T | undefined\n >;\n const ms = opt?.ms ?? 300;\n\n let timeout: ReturnType<typeof setTimeout> | undefined;\n\n const originalSet = sig.set;\n const originalUpdate = sig.update;\n\n const trigger = signal(false);\n\n // Set on the original signal, then trigger the debounced update\n const set = (value: T | undefined) => {\n originalSet(value); // Update the *original* signal immediately\n\n if (timeout) clearTimeout(timeout);\n timeout = setTimeout(() => {\n trigger.update((cur) => !cur); // Trigger the computed signal\n }, ms);\n };\n\n // Update on the original signal, then trigger the debounced update\n const update = (fn: (prev: T | undefined) => T | undefined) => {\n originalUpdate(fn); // Update the *original* signal immediately\n\n if (timeout) clearTimeout(timeout);\n timeout = setTimeout(() => {\n trigger.update((cur) => !cur); // Trigger the computed signal\n }, ms);\n };\n\n // Create a computed signal that depends on the trigger.\n // This computed signal is what provides the debounced behavior.\n const writable = toWritable(\n computed(() => {\n trigger();\n return untracked(sig);\n }),\n set,\n update,\n ) as DebouncedSignal<T | undefined>;\n\n writable.original = sig;\n\n return writable;\n}\n","import {\n computed,\n CreateSignalOptions,\n signal,\n untracked,\n type WritableSignal,\n} from '@angular/core';\nimport type { UnknownObject } from '@mmstack/object';\nimport { toWritable } from './to-writable';\n\n/**\n * Options for creating a derived signal using the full `derived` function signature.\n * @typeParam T - The type of the source signal's value (parent).\n * @typeParam U - The type of the derived signal's value (child).\n */\ntype CreateDerivedOptions<T, U> = CreateSignalOptions<U> & {\n /**\n * A function that extracts the derived value (`U`) from the source signal's value (`T`).\n */\n from: (v: T) => U;\n /**\n * A function that updates the source signal's value (`T`) when the derived signal's value (`U`) changes.\n * This establishes the two-way binding.\n */\n onChange: (newValue: U) => void;\n};\n\n/**\n * A `WritableSignal` that derives its value from another `WritableSignal` (the \"source\" signal).\n * It provides two-way binding: changes to the source signal update the derived signal, and\n * changes to the derived signal update the source signal.\n *\n * @typeParam T - The type of the source signal's value (parent).\n * @typeParam U - The type of the derived signal's value (child).\n */\nexport type DerivedSignal<T, U> = WritableSignal<U> & {\n /**\n * The function used to derive the derived signal's value from the source signal's value.\n * This is primarily for internal use and introspection.\n */\n from: (v: T) => U;\n};\n\n/**\n * Creates a `DerivedSignal` that derives its value from another `WritableSignal` (the source signal).\n * This overload provides the most flexibility, allowing you to specify custom `from` and `onChange` functions.\n *\n * @typeParam T - The type of the source signal's value.\n * @typeParam U - The type of the derived signal's value.\n * @param source - The source `WritableSignal`.\n * @param options - An object containing the `from` and `onChange` functions, and optional signal options.\n * @returns A `DerivedSignal` instance.\n *\n * @example\n * const user = signal({ name: 'John', age: 30 });\n * const name = derived(user, {\n * from: (u) => u.name,\n * onChange: (newName) => user.update((u) => ({ ...u, name: newName })),\n * });\n */\nexport function derived<T, U>(\n source: WritableSignal<T>,\n opt: CreateDerivedOptions<T, U>,\n): DerivedSignal<T, U>;\n\n/**\n * Creates a `DerivedSignal` that derives a property from an object held by the source signal.\n * This overload simplifies creating derived signals for object properties.\n *\n * @typeParam T - The type of the source signal's value (must be an object).\n * @typeParam TKey - The key of the property to derive.\n * @param source - The source `WritableSignal` (holding an object).\n * @param key - The key of the property to derive.\n * @param options - Optional signal options for the derived signal.\n * @returns A `DerivedSignal` instance.\n *\n * @example\n * const user = signal({ name: 'John', age: 30 });\n * const name = derived(user, 'name');\n */\nexport function derived<T extends UnknownObject, TKey extends keyof T>(\n source: WritableSignal<T>,\n key: TKey,\n opt?: CreateSignalOptions<T[TKey]>,\n): DerivedSignal<T, T[TKey]>;\n\n/**\n * Creates a `DerivedSignal` from an array, and derives an element by index.\n *\n * @typeParam T - The type of the source signal's value (must be an array).\n * @param source - The source `WritableSignal` (holding an array).\n * @param index - The index of the element to derive.\n * @param options - Optional signal options for the derived signal.\n * @returns A `DerivedSignal` instance.\n *\n * @example\n * const numbers = signal([1, 2, 3]);\n * const secondNumber = derived(numbers, 1); // secondNumber() === 2\n * secondNumber.set(5); // numbers() === [1, 5, 3]\n */\nexport function derived<T extends any[]>(\n source: WritableSignal<T>,\n index: number,\n opt?: CreateSignalOptions<T[number]>,\n): DerivedSignal<T, T[number]>;\n\nexport function derived<T, U>(\n source: WritableSignal<T>,\n optOrKey: CreateDerivedOptions<T, U> | keyof T,\n opt?: CreateSignalOptions<U>,\n): DerivedSignal<T, U> {\n const isArray =\n Array.isArray(untracked(source)) && typeof optOrKey === 'number';\n\n const from =\n typeof optOrKey === 'object' ? optOrKey.from : (v: T) => v[optOrKey] as U;\n const onChange =\n typeof optOrKey === 'object'\n ? optOrKey.onChange\n : isArray\n ? (next: U) => {\n source.update(\n (cur) =>\n (cur as unknown as any[]).map((v, i) =>\n i === optOrKey ? next : v,\n ) as T,\n );\n }\n : (next: U) => {\n source.update((cur) => ({ ...cur, [optOrKey]: next }));\n };\n\n const rest = typeof optOrKey === 'object' ? optOrKey : opt;\n\n const sig = toWritable<U>(\n computed(() => from(source()), rest),\n (newVal) => onChange(newVal),\n ) as DerivedSignal<T, U>;\n\n sig.from = from;\n\n return sig;\n}\n\n/**\n * Creates a \"fake\" `DerivedSignal` from a simple value. This is useful for creating\n * `FormControlSignal` instances that are not directly derived from another signal.\n * The returned signal's `from` function will always return the initial value.\n *\n * @typeParam T - This type parameter is not used in the implementation but is kept for type compatibility with `DerivedSignal`.\n * @typeParam U - The type of the signal's value.\n * @param initial - The initial value of the signal.\n * @returns A `DerivedSignal` instance.\n * @internal\n */\nexport function toFakeDerivation<T, U>(initial: U): DerivedSignal<T, U> {\n const sig = signal(initial) as DerivedSignal<T, U>;\n sig.from = () => initial;\n\n return sig;\n}\n\n/**\n * Creates a \"fake\" `DerivedSignal` from an existing `WritableSignal`. This is useful\n * for treating a regular `WritableSignal` as a `DerivedSignal` without changing its behavior.\n * The returned signal's `from` function returns the current value of signal, using `untracked`.\n *\n * @typeParam T - This type parameter is not used in the implementation but is kept for type compatibility with `DerivedSignal`.\n * @typeParam U - The type of the signal's value.\n * @param initial - The existing `WritableSignal`.\n * @returns A `DerivedSignal` instance.\n * @internal\n */\nexport function toFakeSignalDerivation<T, U>(\n initial: WritableSignal<U>,\n): DerivedSignal<T, U> {\n const sig = initial as DerivedSignal<T, U>;\n sig.from = () => untracked(initial);\n return sig;\n}\n\n/**\n * Type guard function to check if a given `WritableSignal` is a `DerivedSignal`.\n *\n * @typeParam T - The type of the source signal's value (optional, defaults to `any`).\n * @typeParam U - The type of the derived signal's value (optional, defaults to `any`).\n * @param sig - The `WritableSignal` to check.\n * @returns `true` if the signal is a `DerivedSignal`, `false` otherwise.\n */\nexport function isDerivation<T, U>(\n sig: WritableSignal<U>,\n): sig is DerivedSignal<T, U> {\n return 'from' in sig;\n}\n","import {\n computed,\n type CreateSignalOptions,\n isSignal,\n linkedSignal,\n type Signal,\n} from '@angular/core';\n\n/**\n * Options for the mapArray function.\n * @template T The type of elements in the array.\n * @extends CreateSignalOptions<T> Inherits options for creating individual signals.\n */\nexport type MapArrayOptions<T> = CreateSignalOptions<T> & {\n /**\n * An optional function to transform each element from the source array.\n * If not provided, the original element is used.\n * @param source The current value of the source array signal.\n * @param index The index of the element being mapped.\n * @returns The transformed element for the corresponding signal.\n */\n map?: (source: T[], index: number) => T;\n};\n\nfunction createReconciler<T>(source: Signal<T[]>, opt?: MapArrayOptions<T>) {\n const map = opt?.map ?? ((source, index) => source[index]);\n\n return (\n length: number,\n prev?: {\n value: Signal<T>[];\n source: number;\n },\n ): Signal<T>[] => {\n if (!prev)\n return Array.from({ length }, (_, i) =>\n computed(() => map(source(), i), opt),\n );\n\n if (length === prev.source) return prev.value;\n\n if (length < prev.source) {\n return prev.value.slice(0, length);\n } else {\n const next = [...prev.value];\n for (let i = prev.source; i < length; i++) {\n next.push(computed(() => map(source(), i), opt));\n }\n\n return next;\n }\n };\n}\n\n/**\n * Creates a reactive array of signals from a source array signal (or a function returning one),\n * applying an optional mapping function to each element.\n *\n * This is useful for scenarios like rendering lists where each item\n * needs its own reactive state derived from the source array. It efficiently\n * handles changes in the source array's length by reusing existing signals\n * for elements that remain, adding signals for new elements, and removing signals\n * for deleted elements.\n *\n * @template T The type of elements in the source array.\n * @param source A function that returns the source array (or readonly array).\n * This function will be tracked for changes.\n * @param opt Optional configuration including a `map` function to transform elements\n * and options (`CreateSignalOptions`) for the created signals.\n * @returns A signal (`Signal<Signal<T | undefined>[]>`) where the outer signal updates\n * when the array length changes, and the inner array contains signals\n * representing each element (potentially mapped).\n */\nexport function mapArray<T>(\n source: () => T[],\n opt?: MapArrayOptions<T | undefined>,\n): Signal<Signal<T | undefined>[]>;\n\n/**\n * Creates a reactive array of signals from a source readonly array (or a function returning one),\n * applying an optional mapping function to each element.\n *\n * This is useful for scenarios like rendering lists where each item\n * needs its own reactive state derived from the source array. It efficiently\n * handles changes in the source array's length by reusing existing signals\n * for elements that remain, adding signals for new elements, and removing signals\n * for deleted elements.\n *\n * @template T The type of elements in the source array.\n * @param source A function that returns the source readonly array.\n * This function will be tracked for changes.\n * @param opt Optional configuration including a `map` function to transform elements\n * and options (`CreateSignalOptions`) for the created signals.\n * @returns A signal (`Signal<Signal<T>[]>`) where the outer signal updates\n * when the array length changes, and the inner array contains signals\n * representing each element (potentially mapped).\n */\nexport function mapArray<T>(\n source: () => readonly T[],\n opt?: MapArrayOptions<T>,\n): Signal<Signal<T>[]>;\n\nexport function mapArray<T>(\n source: (() => T[]) | (() => readonly T[]),\n opt?: MapArrayOptions<T>,\n): Signal<Signal<T | undefined>[]> {\n const data = isSignal(source) ? source : computed(source);\n const length = computed(() => data().length);\n\n const reconciler = createReconciler<T>(data as Signal<T[]>, opt);\n\n return linkedSignal<number, Signal<T>[]>({\n source: () => length(),\n computation: (len, prev) => reconciler(len, prev),\n });\n}\n","import {\r\n signal,\r\n type CreateSignalOptions,\r\n type ValueEqualityFn,\r\n type WritableSignal,\r\n} from '@angular/core';\r\n\r\nconst { is } = Object;\r\n\r\n/**\r\n * A `MutableSignal` is a special type of `WritableSignal` that allows for in-place mutation of its value.\r\n * In addition to the standard `set` and `update` methods, it provides a `mutate` method. This is useful\r\n * for performance optimization when dealing with complex objects or arrays, as it avoids unnecessary\r\n * object copying.\r\n *\r\n * @typeParam T - The type of value held by the signal.\r\n */\r\nexport type MutableSignal<T> = WritableSignal<T> & {\r\n /**\r\n * Mutates the signal's value in-place. This is similar to `update`, but it's optimized for\r\n * scenarios where you want to modify the existing object directly rather than creating a new one.\r\n *\r\n * @param updater - A function that takes the current value as input and modifies it directly.\r\n *\r\n * @example\r\n * const myArray = mutable([1, 2, 3]);\r\n * myArray.mutate((arr) => {\r\n * arr.push(4);\r\n * return arr;\r\n * }); // myArray() now returns [1, 2, 3, 4]\r\n */\r\n mutate: WritableSignal<T>['update'];\r\n\r\n /**\r\n * Mutates the signal's value in-place, similar to `mutate`, but with a void-returning value in updater\r\n * function. This further emphasizes that the mutation is happening inline, improving readability\r\n * in some cases.\r\n * @param updater - Function to change to the current value\r\n * @example\r\n * const myObject = mutable({ a: 1, b: 2 });\r\n * myObject.inline((obj) => (obj.a = 3)); // myObject() now returns { a: 3, b: 2 }\r\n */\r\n inline: (updater: Parameters<WritableSignal<T>['update']>[0]) => void;\r\n};\r\n\r\n/**\r\n * Creates a `MutableSignal`. This function overloads the standard `signal` function to provide\r\n * the additional `mutate` and `inline` methods.\r\n *\r\n * @typeParam T - The type of value held by the signal.\r\n *\r\n * @param initial - The initial value of the signal. If no initial value is provided, it defaults to `undefined`.\r\n * @param options - Optional. An object containing signal options, including a custom equality function (`equal`).\r\n *\r\n * @returns A `MutableSignal` instance.\r\n *\r\n * @example\r\n * // Create a mutable signal with an initial value:\r\n * const mySignal = mutable({ count: 0 }) // MutableSignal<{ count: number }>;\r\n *\r\n * // Create a mutable signal with no initial value (starts as undefined):\r\n * const = mutable<number>(); // MutableSignal<number | undefined>\r\n *\r\n * // Create a mutable signal with a custom equality function:\r\n * const myCustomSignal = mutable({ a: 1 }, { equal: (a, b) => a.a === b.a });\r\n */\r\nexport function mutable<T>(): MutableSignal<T | undefined>;\r\nexport function mutable<T>(initial: T): MutableSignal<T>;\r\nexport function mutable<T>(\r\n initial: T,\r\n opt?: CreateSignalOptions<T>,\r\n): MutableSignal<T>;\r\n\r\nexport function mutable<T>(\r\n initial?: T,\r\n opt?: CreateSignalOptions<T>,\r\n): MutableSignal<T> {\r\n const baseEqual = opt?.equal ?? is;\r\n let trigger = false;\r\n\r\n const equal: ValueEqualityFn<T | undefined> = (a, b) => {\r\n if (trigger) return false;\r\n return baseEqual(a, b);\r\n };\r\n\r\n const sig = signal<T | undefined>(initial, {\r\n ...opt,\r\n equal,\r\n }) as MutableSignal<T>;\r\n\r\n const internalUpdate = sig.update;\r\n\r\n sig.mutate = (updater) => {\r\n trigger = true;\r\n internalUpdate(updater);\r\n trigger = false;\r\n };\r\n\r\n sig.inline = (updater) => {\r\n sig.mutate((prev) => {\r\n updater(prev);\r\n return prev;\r\n });\r\n };\r\n\r\n return sig;\r\n}\r\n\r\n/**\r\n * Type guard function to check if a given `WritableSignal` is a `MutableSignal`. This is useful\r\n * for situations where you need to conditionally use the `mutate` or `inline` methods.\r\n *\r\n * @typeParam T - The type of the signal's value (optional, defaults to `any`).\r\n * @param value - The `WritableSignal` to check.\r\n * @returns `true` if the signal is a `MutableSignal`, `false` otherwise.\r\n *\r\n * @example\r\n * const mySignal = signal(0);\r\n * const myMutableSignal = mutable(0);\r\n *\r\n * if (isMutable(mySignal)) {\r\n * mySignal.mutate(x => x + 1); // This would cause a type error, as mySignal is not a MutableSignal.\r\n * }\r\n *\r\n * if (isMutable(myMutableSignal)) {\r\n * myMutableSignal.mutate(x => x + 1); // This is safe.\r\n * }\r\n */\r\nexport function isMutable<T = any>(\r\n value: WritableSignal<T>,\r\n): value is MutableSignal<T> {\r\n return 'mutate' in value && typeof value.mutate === 'function';\r\n}\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mmstack-primitives.mjs","sources":["../../../../packages/primitives/src/lib/to-writable.ts","../../../../packages/primitives/src/lib/debounced.ts","../../../../packages/primitives/src/lib/derived.ts","../../../../packages/primitives/src/lib/map-array.ts","../../../../packages/primitives/src/lib/mutable.ts","../../../../packages/primitives/src/lib/stored.ts","../../../../packages/primitives/src/mmstack-primitives.ts"],"sourcesContent":["import { Signal, untracked, WritableSignal } from '@angular/core';\n\n/**\n * Converts a read-only `Signal` into a `WritableSignal` by providing custom `set` and, optionally, `update` functions.\n * This can be useful for creating controlled write access to a signal that is otherwise read-only.\n *\n * @typeParam T - The type of value held by the signal.\n *\n * @param signal - The read-only `Signal` to be made writable.\n * @param set - A function that will be used to set the signal's value. This function *must* handle\n * the actual update mechanism (e.g., updating a backing store, emitting an event, etc.).\n * @param update - (Optional) A function that will be used to update the signal's value based on its\n * previous value. If not provided, a default `update` implementation is used that\n * calls the provided `set` function with the result of the updater function. The\n * default implementation uses `untracked` to avoid creating unnecessary dependencies\n * within the updater function.\n *\n * @returns A `WritableSignal` that uses the provided `set` and `update` functions. The `asReadonly`\n * method of the returned signal will still return the original read-only signal.\n *\n * @example\n * // Basic usage: Making a read-only signal writable with a custom set function.\n * const originalValue = signal({a: 0});\n * const readOnlySignal = computed(() => originalValue().a);\n * const writableSignal = toWritable(readOnlySignal, (newValue) => {\n * originalValue.update((prev) => { ...prev, a: newValue });\n * });\n *\n * writableSignal.set(5); // sets value of originalValue.a to 5 & triggers all signals\n */\nexport function toWritable<T>(\n signal: Signal<T>,\n set: (value: T) => void,\n update?: (updater: (value: T) => T) => void,\n): WritableSignal<T> {\n const internal = signal as WritableSignal<T>;\n internal.asReadonly = () => signal;\n internal.set = set;\n internal.update = update ?? ((updater) => set(updater(untracked(internal))));\n\n return internal;\n}\n","import {\n computed,\n type CreateSignalOptions,\n type Signal,\n signal,\n type WritableSignal,\n} from '@angular/core';\nimport { toWritable } from './to-writable';\n\n/**\n * Options for creating a debounced writable signal.\n * Extends Angular's `CreateSignalOptions` with a debounce time setting.\n *\n * @template T The type of value held by the signal.\n */\nexport type CreateDebouncedOptions<T> = CreateSignalOptions<T> & {\n /**\n * The debounce delay in milliseconds. Specifies how long to wait after the\n * last `set` or `update` call before the debounced signal reflects the new value.\n */\n ms?: number;\n};\n\n/**\n * A specialized `WritableSignal` whose publicly readable value updates are debounced.\n *\n * It provides access to the underlying, non-debounced signal via the `original` property.\n *\n * @template T The type of value held by the signal.\n */\nexport type DebouncedSignal<T> = WritableSignal<T> & {\n /**\n * A reference to the original, inner `WritableSignal`.\n * This signal's value is updated *immediately* upon calls to `set` or `update`\n * on the parent `DebouncedSignal`. Useful for accessing the latest value\n * without the debounce delay.\n */\n original: Signal<T>;\n};\n\n/**\n * Creates a `WritableSignal` whose publicly readable value is updated only after\n * a specified debounce period (`ms`) has passed since the last call to its\n * `.set()` or `.update()` method.\n *\n * This implementation avoids using `effect` by leveraging intermediate `computed`\n * signals and a custom `equal` function to delay value propagation based on a timer.\n *\n * @template T The type of value the signal holds.\n * @param initial The initial value of the signal.\n * @param opt Options for signal creation, including:\n * - `ms`: The debounce time in milliseconds. Defaults to 0 if omitted (no debounce).\n * - Other `CreateSignalOptions` (like `equal`) are passed to underlying signals.\n * @returns A `DebouncedSignal<T>` instance. Its readable value updates are debounced,\n * and it includes an `.original` property providing immediate access to the latest set value.\n *\n * @example\n * ```ts\n * import { effect } from '@angular/core';\n *\n * // Create a debounced signal with a 500ms delay\n * const query = debounced('', { ms: 500 });\n *\n * effect(() => {\n * // This effect runs 500ms after the last change to 'query'\n * console.log('Debounced Query:', query());\n * });\n *\n * effect(() => {\n * // This effect runs immediately when 'query.original' changes\n * console.log('Original Query:', query.original());\n * });\n *\n * console.log('Setting query to \"a\"');\n * query.set('a');\n * // Output: Original Query: a\n *\n * setTimeout(() => {\n * console.log('Setting query to \"ab\"');\n * query.set('ab');\n * // Output: Original Query: ab\n * }, 200); // Before debounce timeout\n *\n * setTimeout(() => {\n * console.log('Setting query to \"abc\"');\n * query.set('abc');\n * // Output: Original Query: abc\n * }, 400); // Before debounce timeout\n *\n * // ~500ms after the *last* set (at 400ms), the debounced effect runs:\n * // Output (at ~900ms): Debounced Query: abc\n * ```\n */\nexport function debounced<T>(\n initial: T,\n opt: CreateDebouncedOptions<T>,\n): DebouncedSignal<T> {\n const internal = signal(initial, opt);\n const ms = opt.ms ?? 0;\n\n const trigger = signal(false);\n\n let timeout: ReturnType<typeof setTimeout> | undefined;\n\n const set = (value: T) => {\n if (timeout) clearTimeout(timeout);\n internal.set(value);\n\n timeout = setTimeout(() => {\n trigger.update((c) => !c);\n }, ms);\n };\n\n const update = (fn: (prev: T) => T) => {\n if (timeout) clearTimeout(timeout);\n internal.update(fn);\n\n timeout = setTimeout(() => {\n trigger.update((c) => !c);\n }, ms);\n };\n\n const stable = computed(\n () => ({\n trigger: trigger(),\n value: internal(),\n }),\n {\n equal: (a, b) => a.trigger === b.trigger,\n },\n );\n\n const writable = toWritable(\n computed(() => stable().value, opt),\n set,\n update,\n ) as DebouncedSignal<T>;\n writable.original = internal;\n\n return writable;\n}\n","import {\n computed,\n CreateSignalOptions,\n signal,\n untracked,\n type WritableSignal,\n} from '@angular/core';\nimport type { UnknownObject } from '@mmstack/object';\nimport { toWritable } from './to-writable';\n\n/**\n * Options for creating a derived signal using the full `derived` function signature.\n * @typeParam T - The type of the source signal's value (parent).\n * @typeParam U - The type of the derived signal's value (child).\n */\ntype CreateDerivedOptions<T, U> = CreateSignalOptions<U> & {\n /**\n * A function that extracts the derived value (`U`) from the source signal's value (`T`).\n */\n from: (v: T) => U;\n /**\n * A function that updates the source signal's value (`T`) when the derived signal's value (`U`) changes.\n * This establishes the two-way binding.\n */\n onChange: (newValue: U) => void;\n};\n\n/**\n * A `WritableSignal` that derives its value from another `WritableSignal` (the \"source\" signal).\n * It provides two-way binding: changes to the source signal update the derived signal, and\n * changes to the derived signal update the source signal.\n *\n * @typeParam T - The type of the source signal's value (parent).\n * @typeParam U - The type of the derived signal's value (child).\n */\nexport type DerivedSignal<T, U> = WritableSignal<U> & {\n /**\n * The function used to derive the derived signal's value from the source signal's value.\n * This is primarily for internal use and introspection.\n */\n from: (v: T) => U;\n};\n\n/**\n * Creates a `DerivedSignal` that derives its value from another `WritableSignal` (the source signal).\n * This overload provides the most flexibility, allowing you to specify custom `from` and `onChange` functions.\n *\n * @typeParam T - The type of the source signal's value.\n * @typeParam U - The type of the derived signal's value.\n * @param source - The source `WritableSignal`.\n * @param options - An object containing the `from` and `onChange` functions, and optional signal options.\n * @returns A `DerivedSignal` instance.\n *\n * @example\n * const user = signal({ name: 'John', age: 30 });\n * const name = derived(user, {\n * from: (u) => u.name,\n * onChange: (newName) => user.update((u) => ({ ...u, name: newName })),\n * });\n */\nexport function derived<T, U>(\n source: WritableSignal<T>,\n opt: CreateDerivedOptions<T, U>,\n): DerivedSignal<T, U>;\n\n/**\n * Creates a `DerivedSignal` that derives a property from an object held by the source signal.\n * This overload simplifies creating derived signals for object properties.\n *\n * @typeParam T - The type of the source signal's value (must be an object).\n * @typeParam TKey - The key of the property to derive.\n * @param source - The source `WritableSignal` (holding an object).\n * @param key - The key of the property to derive.\n * @param options - Optional signal options for the derived signal.\n * @returns A `DerivedSignal` instance.\n *\n * @example\n * const user = signal({ name: 'John', age: 30 });\n * const name = derived(user, 'name');\n */\nexport function derived<T extends UnknownObject, TKey extends keyof T>(\n source: WritableSignal<T>,\n key: TKey,\n opt?: CreateSignalOptions<T[TKey]>,\n): DerivedSignal<T, T[TKey]>;\n\n/**\n * Creates a `DerivedSignal` from an array, and derives an element by index.\n *\n * @typeParam T - The type of the source signal's value (must be an array).\n * @param source - The source `WritableSignal` (holding an array).\n * @param index - The index of the element to derive.\n * @param options - Optional signal options for the derived signal.\n * @returns A `DerivedSignal` instance.\n *\n * @example\n * const numbers = signal([1, 2, 3]);\n * const secondNumber = derived(numbers, 1); // secondNumber() === 2\n * secondNumber.set(5); // numbers() === [1, 5, 3]\n */\nexport function derived<T extends any[]>(\n source: WritableSignal<T>,\n index: number,\n opt?: CreateSignalOptions<T[number]>,\n): DerivedSignal<T, T[number]>;\n\nexport function derived<T, U>(\n source: WritableSignal<T>,\n optOrKey: CreateDerivedOptions<T, U> | keyof T,\n opt?: CreateSignalOptions<U>,\n): DerivedSignal<T, U> {\n const isArray =\n Array.isArray(untracked(source)) && typeof optOrKey === 'number';\n\n const from =\n typeof optOrKey === 'object' ? optOrKey.from : (v: T) => v[optOrKey] as U;\n const onChange =\n typeof optOrKey === 'object'\n ? optOrKey.onChange\n : isArray\n ? (next: U) => {\n source.update(\n (cur) =>\n (cur as unknown as any[]).map((v, i) =>\n i === optOrKey ? next : v,\n ) as T,\n );\n }\n : (next: U) => {\n source.update((cur) => ({ ...cur, [optOrKey]: next }));\n };\n\n const rest = typeof optOrKey === 'object' ? optOrKey : opt;\n\n const sig = toWritable<U>(\n computed(() => from(source()), rest),\n (newVal) => onChange(newVal),\n ) as DerivedSignal<T, U>;\n\n sig.from = from;\n\n return sig;\n}\n\n/**\n * Creates a \"fake\" `DerivedSignal` from a simple value. This is useful for creating\n * `FormControlSignal` instances that are not directly derived from another signal.\n * The returned signal's `from` function will always return the initial value.\n *\n * @typeParam T - This type parameter is not used in the implementation but is kept for type compatibility with `DerivedSignal`.\n * @typeParam U - The type of the signal's value.\n * @param initial - The initial value of the signal.\n * @returns A `DerivedSignal` instance.\n * @internal\n */\nexport function toFakeDerivation<T, U>(initial: U): DerivedSignal<T, U> {\n const sig = signal(initial) as DerivedSignal<T, U>;\n sig.from = () => initial;\n\n return sig;\n}\n\n/**\n * Creates a \"fake\" `DerivedSignal` from an existing `WritableSignal`. This is useful\n * for treating a regular `WritableSignal` as a `DerivedSignal` without changing its behavior.\n * The returned signal's `from` function returns the current value of signal, using `untracked`.\n *\n * @typeParam T - This type parameter is not used in the implementation but is kept for type compatibility with `DerivedSignal`.\n * @typeParam U - The type of the signal's value.\n * @param initial - The existing `WritableSignal`.\n * @returns A `DerivedSignal` instance.\n * @internal\n */\nexport function toFakeSignalDerivation<T, U>(\n initial: WritableSignal<U>,\n): DerivedSignal<T, U> {\n const sig = initial as DerivedSignal<T, U>;\n sig.from = () => untracked(initial);\n return sig;\n}\n\n/**\n * Type guard function to check if a given `WritableSignal` is a `DerivedSignal`.\n *\n * @typeParam T - The type of the source signal's value (optional, defaults to `any`).\n * @typeParam U - The type of the derived signal's value (optional, defaults to `any`).\n * @param sig - The `WritableSignal` to check.\n * @returns `true` if the signal is a `DerivedSignal`, `false` otherwise.\n */\nexport function isDerivation<T, U>(\n sig: WritableSignal<U>,\n): sig is DerivedSignal<T, U> {\n return 'from' in sig;\n}\n","import {\n computed,\n type CreateSignalOptions,\n isSignal,\n linkedSignal,\n type Signal,\n} from '@angular/core';\n\n/**\n * Reactively maps items from a source array (or signal of an array) using a provided mapping function.\n *\n * This function serves a similar purpose to SolidJS's `mapArray` by providing stability\n * for mapped items. It receives a source function returning an array (or a Signal<T[]>)\n * and a mapping function.\n *\n * For each item in the source array, it creates a stable `computed` signal representing\n * that item's value at its current index. This stable signal (`Signal<T>`) is passed\n * to the mapping function. This ensures that downstream computations or components\n * depending on the mapped result only re-render or re-calculate for the specific items\n * that have changed, or when items are added/removed, rather than re-evaluating everything\n * when the source array reference changes but items remain the same.\n *\n * It efficiently handles changes in the source array's length by reusing existing mapped\n * results when possible, slicing when the array shrinks, and appending new mapped items\n * when it grows.\n *\n * @template T The type of items in the source array.\n * @template U The type of items in the resulting mapped array.\n *\n * @param source A function returning the source array `T[]`, or a `Signal<T[]>` itself.\n * The `mapArray` function will reactively update based on changes to this source.\n * @param map The mapping function. It is called for each item in the source array.\n * It receives:\n * - `value`: A stable `Signal<T>` representing the item at the current index.\n * Use this signal within your mapping logic if you need reactivity\n * tied to the specific item's value changes.\n * - `index`: The number index of the item in the array.\n * It should return the mapped value `U`.\n * @param [opt] Optional `CreateSignalOptions<T>`. These options are passed directly\n * to the `computed` signal created for each individual item (`Signal<T>`).\n * This allows specifying options like a custom `equal` function for item comparison.\n *\n * @returns A `Signal<U[]>` containing the mapped array. This signal updates whenever\n * the source array changes (either length or the values of its items).\n *\n * @example\n * ```ts\n * const sourceItems = signal([\n * { id: 1, name: 'Apple' },\n * { id: 2, name: 'Banana' }\n * ]);\n *\n * const mappedItems = mapArray(\n * sourceItems,\n * (itemSignal, index) => {\n * // itemSignal is stable for a given item based on its index.\n * // We create a computed here to react to changes in the item's name.\n * return computed(() => `${index}: ${itemSignal().name.toUpperCase()}`);\n * },\n * // Example optional options (e.g., custom equality for item signals)\n * { equal: (a, b) => a.id === b.id && a.name === b.name }\n * );\n * ```\n */\nexport function mapArray<T, U>(\n source: () => T[],\n map: (value: Signal<T>, index: number) => U,\n opt?: CreateSignalOptions<T>,\n): Signal<U[]> {\n const data = isSignal(source) ? source : computed(source);\n const len = computed(() => data().length);\n\n return linkedSignal<number, U[]>({\n source: () => len(),\n computation: (len, prev) => {\n if (!prev)\n return Array.from({ length: len }, (_, i) =>\n map(\n computed(() => source()[i], opt),\n i,\n ),\n );\n\n if (len === prev.value.length) return prev.value;\n\n if (len < prev.value.length) {\n return prev.value.slice(0, len);\n } else {\n const next = [...prev.value];\n for (let i = prev.value.length; i < len; i++) {\n next[i] = map(\n computed(() => source()[i], opt),\n i,\n );\n }\n return next;\n }\n },\n equal: (a, b) => a.length === b.length,\n });\n}\n","import {\r\n signal,\r\n type CreateSignalOptions,\r\n type ValueEqualityFn,\r\n type WritableSignal,\r\n} from '@angular/core';\r\n\r\nconst { is } = Object;\r\n\r\n/**\r\n * A `MutableSignal` is a special type of `WritableSignal` that allows for in-place mutation of its value.\r\n * In addition to the standard `set` and `update` methods, it provides a `mutate` method. This is useful\r\n * for performance optimization when dealing with complex objects or arrays, as it avoids unnecessary\r\n * object copying.\r\n *\r\n * @typeParam T - The type of value held by the signal.\r\n */\r\nexport type MutableSignal<T> = WritableSignal<T> & {\r\n /**\r\n * Mutates the signal's value in-place. This is similar to `update`, but it's optimized for\r\n * scenarios where you want to modify the existing object directly rather than creating a new one.\r\n *\r\n * @param updater - A function that takes the current value as input and modifies it directly.\r\n *\r\n * @example\r\n * const myArray = mutable([1, 2, 3]);\r\n * myArray.mutate((arr) => {\r\n * arr.push(4);\r\n * return arr;\r\n * }); // myArray() now returns [1, 2, 3, 4]\r\n */\r\n mutate: WritableSignal<T>['update'];\r\n\r\n /**\r\n * Mutates the signal's value in-place, similar to `mutate`, but with a void-returning value in updater\r\n * function. This further emphasizes that the mutation is happening inline, improving readability\r\n * in some cases.\r\n * @param updater - Function to change to the current value\r\n * @example\r\n * const myObject = mutable({ a: 1, b: 2 });\r\n * myObject.inline((obj) => (obj.a = 3)); // myObject() now returns { a: 3, b: 2 }\r\n */\r\n inline: (updater: (value: T) => void) => void;\r\n};\r\n\r\n/**\r\n * Creates a `MutableSignal`. This function overloads the standard `signal` function to provide\r\n * the additional `mutate` and `inline` methods.\r\n *\r\n * @typeParam T - The type of value held by the signal.\r\n *\r\n * @param initial - The initial value of the signal. If no initial value is provided, it defaults to `undefined`.\r\n * @param options - Optional. An object containing signal options, including a custom equality function (`equal`).\r\n *\r\n * @returns A `MutableSignal` instance.\r\n *\r\n * @example\r\n * // Create a mutable signal with an initial value:\r\n * const mySignal = mutable({ count: 0 }) // MutableSignal<{ count: number }>;\r\n *\r\n * // Create a mutable signal with no initial value (starts as undefined):\r\n * const = mutable<number>(); // MutableSignal<number | undefined>\r\n *\r\n * // Create a mutable signal with a custom equality function:\r\n * const myCustomSignal = mutable({ a: 1 }, { equal: (a, b) => a.a === b.a });\r\n */\r\nexport function mutable<T>(): MutableSignal<T | undefined>;\r\nexport function mutable<T>(initial: T): MutableSignal<T>;\r\nexport function mutable<T>(\r\n initial: T,\r\n opt?: CreateSignalOptions<T>,\r\n): MutableSignal<T>;\r\n\r\nexport function mutable<T>(\r\n initial?: T,\r\n opt?: CreateSignalOptions<T>,\r\n): MutableSignal<T> {\r\n const baseEqual = opt?.equal ?? is;\r\n let trigger = false;\r\n\r\n const equal: ValueEqualityFn<T | undefined> = (a, b) => {\r\n if (trigger) return false;\r\n return baseEqual(a, b);\r\n };\r\n\r\n const sig = signal<T | undefined>(initial, {\r\n ...opt,\r\n equal,\r\n }) as MutableSignal<T>;\r\n\r\n const internalUpdate = sig.update;\r\n\r\n sig.mutate = (updater) => {\r\n trigger = true;\r\n internalUpdate(updater);\r\n trigger = false;\r\n };\r\n\r\n sig.inline = (updater) => {\r\n sig.mutate((prev) => {\r\n updater(prev);\r\n return prev;\r\n });\r\n };\r\n\r\n return sig;\r\n}\r\n\r\n/**\r\n * Type guard function to check if a given `WritableSignal` is a `MutableSignal`. This is useful\r\n * for situations where you need to conditionally use the `mutate` or `inline` methods.\r\n *\r\n * @typeParam T - The type of the signal's value (optional, defaults to `any`).\r\n * @param value - The `WritableSignal` to check.\r\n * @returns `true` if the signal is a `MutableSignal`, `false` otherwise.\r\n *\r\n * @example\r\n * const mySignal = signal(0);\r\n * const myMutableSignal = mutable(0);\r\n *\r\n * if (isMutable(mySignal)) {\r\n * mySignal.mutate(x => x + 1); // This would cause a type error, as mySignal is not a MutableSignal.\r\n * }\r\n *\r\n * if (isMutable(myMutableSignal)) {\r\n * myMutableSignal.mutate(x => x + 1); // This is safe.\r\n * }\r\n */\r\nexport function isMutable<T = any>(\r\n value: WritableSignal<T>,\r\n): value is MutableSignal<T> {\r\n return 'mutate' in value && typeof value.mutate === 'function';\r\n}\r\n","import { isPlatformServer } from '@angular/common';\nimport {\n computed,\n DestroyRef,\n effect,\n inject,\n isDevMode,\n isSignal,\n PLATFORM_ID,\n Signal,\n signal,\n untracked,\n type CreateSignalOptions,\n type WritableSignal,\n} from '@angular/core';\nimport { toWritable } from './to-writable';\n\n/**\n * Interface for storage mechanisms compatible with the `stored` signal.\n * Matches the essential parts of the `Storage` interface (`localStorage`, `sessionStorage`).\n */\ntype Store = {\n /** Retrieves an item from storage for a given key. */\n getItem: (key: string) => string | null;\n /** Sets an item in storage for a given key. */\n setItem: (key: string, value: string) => void;\n /** Removes an item from storage for a given key. */\n removeItem: (key: string) => void;\n};\n\n// Internal dummy store for server-side rendering\nconst noopStore: Store = {\n getItem: () => null,\n setItem: () => {\n /* noop */\n },\n removeItem: () => {\n /* noop */\n },\n};\n\n/**\n * Options for creating a signal synchronized with persistent storage using `stored()`.\n * Extends Angular's `CreateSignalOptions`.\n *\n * @template T The type of value held by the signal.\n */\nexport type CreateStoredOptions<T> = CreateSignalOptions<T> & {\n /**\n * The key used to identify the item in storage.\n * Can be a static string or a function/signal returning a string for dynamic keys\n * (e.g., based on user ID or other application state).\n */\n key: string | (() => string);\n /**\n * Optional custom storage implementation (e.g., `sessionStorage` or a custom adapter).\n * Must conform to the `Store` interface (`getItem`, `setItem`, `removeItem`).\n * Defaults to `localStorage` in browser environments and a no-op store on the server.\n */\n store?: Store;\n /**\n * Optional function to serialize the value (type `T`) into a string before storing.\n * Defaults to `JSON.stringify`.\n * @param {T} value The value to serialize.\n * @returns {string} The serialized string representation.\n */\n serialize?: (value: T) => string;\n /**\n * Optional function to deserialize the string retrieved from storage back into the value (type `T`).\n * Defaults to `JSON.parse`.\n * @param {string} value The string retrieved from storage.\n * @returns {T} The deserialized value.\n */\n deserialize?: (value: string) => T;\n /**\n * If `true`, the signal will attempt to synchronize its state across multiple browser tabs\n * using the `storage` event. Changes made in one tab (set, update, clear) will be\n * reflected in other tabs using the same storage key.\n * Requires a browser environment. Defaults to `false`.\n */\n syncTabs?: boolean;\n};\n\n/**\n * A specialized `WritableSignal` returned by the `stored()` function.\n * It synchronizes its value with persistent storage and provides additional methods.\n *\n * @template T The type of value held by the signal (matches the fallback type).\n */\nexport type StoredSignal<T> = WritableSignal<T> & {\n /**\n * Removes the item associated with the signal's key from the configured storage.\n * After clearing, reading the signal will return the fallback value until it's set again.\n */\n clear: () => void;\n /**\n * A `Signal<string>` containing the current storage key being used by this stored signal.\n * This is particularly useful if the key was configured dynamically. You can read or react\n * to this signal to know the active key.\n */\n key: Signal<string>;\n};\n\n/**\n * Creates a `WritableSignal` whose state is automatically synchronized with persistent storage\n * (like `localStorage` or `sessionStorage`).\n *\n * It handles Server-Side Rendering (SSR) gracefully, allows dynamic storage keys,\n * custom serialization/deserialization, custom storage providers, and optional\n * synchronization across browser tabs.\n *\n * @template T The type of value held by the signal and stored (after serialization).\n * @param fallback The default value of type `T` to use when no value is found in storage\n * or when deserialization fails. The signal's value will never be `null` or `undefined`\n * publicly, it will always revert to this fallback.\n * @param options Configuration options (`CreateStoredOptions<T>`). Requires at least the `key`.\n * @returns A `StoredSignal<T>` instance. This signal behaves like a standard `WritableSignal<T>`,\n * but its value is persisted. It includes a `.clear()` method to remove the item from storage\n * and a `.key` signal providing the current storage key.\n *\n * @remarks\n * - **Persistence:** The signal automatically saves its value to storage whenever the signal's\n * value or its configured `key` changes. This is managed internally using `effect`.\n * - **SSR Safety:** Detects server environments and uses a no-op storage, preventing errors.\n * - **Error Handling:** Catches and logs errors during serialization/deserialization in dev mode.\n * - **Tab Sync:** If `syncTabs` is true, listens to `storage` events to keep the signal value\n * consistent across browser tabs using the same key. Cleanup is handled automatically\n * using `DestroyRef`.\n * - **Removal:** Use the `.clear()` method on the returned signal to remove the item from storage.\n * Setting the signal to the fallback value will store the fallback value, not remove the item.\n *\n * @example\n * ```ts\n * import { Component, effect, signal } from '@angular/core';\n * import { stored } from '@mmstack/primitives'; // Adjust import path\n *\n * @Component({\n * selector: 'app-settings',\n * standalone: true,\n * template: `\n * Theme:\n * <select [ngModel]=\"theme()\" (ngModelChange)=\"theme.set($event)\">\n * <option value=\"light\">Light</option>\n * <option value=\"dark\">Dark</option>\n * </select>\n * <button (click)=\"theme.clear()\">Clear Theme Setting</button>\n * <p>Storage Key Used: {{ theme.key() }}</p>\n * ` // Requires FormsModule for ngModel\n * })\n * export class SettingsComponent {\n * theme = stored<'light' | 'dark'>('light', { key: 'app-theme', syncTabs: true });\n * }\n * ```\n */\nexport function stored<T>(\n fallback: T,\n {\n key,\n store: providedStore,\n serialize = JSON.stringify,\n deserialize = JSON.parse,\n syncTabs = false,\n equal = Object.is,\n ...rest\n }: CreateStoredOptions<T>,\n): StoredSignal<T> {\n const isServer = isPlatformServer(inject(PLATFORM_ID));\n\n const fallbackStore = isServer ? noopStore : localStorage;\n const store = providedStore ?? fallbackStore;\n\n const keySig =\n typeof key === 'string'\n ? computed(() => key)\n : isSignal(key)\n ? key\n : computed(key);\n\n const getValue = (key: string): T | null => {\n const found = store.getItem(key);\n if (found === null) return null;\n try {\n return deserialize(found);\n } catch (err) {\n if (isDevMode())\n console.error(`Failed to parse stored value for key \"${key}\":`, err);\n return null;\n }\n };\n\n const storeValue = (key: string, value: T | null) => {\n try {\n if (value === null) return store.removeItem(key);\n const serialized = serialize(value);\n store.setItem(key, serialized);\n } catch (err) {\n if (isDevMode())\n console.error(`Failed to store value for key \"${key}\":`, err);\n }\n };\n\n const opt = {\n ...rest,\n equal,\n };\n\n const internal = signal(getValue(untracked(keySig)), {\n ...opt,\n equal: (a, b) => {\n if (a === null && b === null) return true;\n if (a === null || b === null) return false;\n return equal(a, b);\n },\n });\n\n effect(() => storeValue(keySig(), internal()));\n\n if (syncTabs && !isServer) {\n const destroyRef = inject(DestroyRef);\n const sync = (e: StorageEvent) => {\n if (e.key !== untracked(keySig)) return;\n\n if (e.newValue === null) internal.set(null);\n else internal.set(getValue(e.key));\n };\n\n window.addEventListener('storage', sync);\n\n destroyRef.onDestroy(() => window.removeEventListener('storage', sync));\n }\n\n const writable = toWritable<T>(\n computed(() => internal() ?? fallback, opt),\n internal.set,\n ) as StoredSignal<T>;\n\n writable.clear = () => {\n internal.set(null);\n };\n writable.key = keySig;\n return writable;\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
package/index.d.ts CHANGED
@@ -2,4 +2,5 @@ export * from './lib/debounced';
2
2
  export * from './lib/derived';
3
3
  export * from './lib/map-array';
4
4
  export * from './lib/mutable';
5
+ export * from './lib/stored';
5
6
  export * from './lib/to-writable';
@@ -1,76 +1,84 @@
1
- import { type CreateSignalOptions, type WritableSignal } from '@angular/core';
1
+ import { type CreateSignalOptions, type Signal, type WritableSignal } from '@angular/core';
2
2
  /**
3
- * A `DebouncedSignal` is a special type of `WritableSignal` that delays updates
4
- * to its value. This is useful for scenarios where you want to avoid
5
- * frequent updates, such as responding to user input in a search field.
6
- * It keeps a reference to the original `WritableSignal` via the `original` property.
3
+ * Options for creating a debounced writable signal.
4
+ * Extends Angular's `CreateSignalOptions` with a debounce time setting.
7
5
  *
8
- * @typeParam T - The type of value held by the signal.
6
+ * @template T The type of value held by the signal.
9
7
  */
10
- export type DebouncedSignal<T> = WritableSignal<T> & {
8
+ export type CreateDebouncedOptions<T> = CreateSignalOptions<T> & {
11
9
  /**
12
- * A reference to the original, un-debounced `WritableSignal`. This allows
13
- * you to access the immediate value (without the debounce delay) if needed,
14
- * and also ensures that any direct modifications to the original signal
15
- * are reflected in the debounced signal after the debounce period.
10
+ * The debounce delay in milliseconds. Specifies how long to wait after the
11
+ * last `set` or `update` call before the debounced signal reflects the new value.
16
12
  */
17
- original: WritableSignal<T>;
13
+ ms?: number;
18
14
  };
19
15
  /**
20
- * Options for creating a debounced signal.
16
+ * A specialized `WritableSignal` whose publicly readable value updates are debounced.
17
+ *
18
+ * It provides access to the underlying, non-debounced signal via the `original` property.
21
19
  *
22
- * @typeParam T - The type of value held by the signal.
20
+ * @template T The type of value held by the signal.
23
21
  */
24
- export type CreateDebouncedOptions<T> = CreateSignalOptions<T> & {
22
+ export type DebouncedSignal<T> = WritableSignal<T> & {
25
23
  /**
26
- * The debounce delay in milliseconds. Defaults to 300.
24
+ * A reference to the original, inner `WritableSignal`.
25
+ * This signal's value is updated *immediately* upon calls to `set` or `update`
26
+ * on the parent `DebouncedSignal`. Useful for accessing the latest value
27
+ * without the debounce delay.
27
28
  */
28
- ms?: number;
29
+ original: Signal<T>;
29
30
  };
30
31
  /**
31
- * Creates a debounced signal. The signal's value will only be propagated to
32
- * subscribers after a specified delay (debounce time) has passed since the last
33
- * time it was set or updated. Crucially, updates to the *original* signal
34
- * are also debounced.
32
+ * Creates a `WritableSignal` whose publicly readable value is updated only after
33
+ * a specified debounce period (`ms`) has passed since the last call to its
34
+ * `.set()` or `.update()` method.
35
+ *
36
+ * This implementation avoids using `effect` by leveraging intermediate `computed`
37
+ * signals and a custom `equal` function to delay value propagation based on a timer.
35
38
  *
36
- * @see {@link DebouncedSignal}
37
- * @see {@link CreateDebouncedOptions}
39
+ * @template T The type of value the signal holds.
40
+ * @param initial The initial value of the signal.
41
+ * @param opt Options for signal creation, including:
42
+ * - `ms`: The debounce time in milliseconds. Defaults to 0 if omitted (no debounce).
43
+ * - Other `CreateSignalOptions` (like `equal`) are passed to underlying signals.
44
+ * @returns A `DebouncedSignal<T>` instance. Its readable value updates are debounced,
45
+ * and it includes an `.original` property providing immediate access to the latest set value.
38
46
  *
39
47
  * @example
40
- * ```typescript
41
- * // Create a debounced signal with an initial value and a custom delay.
42
- * const searchTerm = debounced('initial value', { ms: 500 });
48
+ * ```ts
49
+ * import { effect } from '@angular/core';
43
50
  *
44
- * // Update the debounced signal. The actual update will be delayed by 500ms.
45
- * searchTerm.set('new value');
51
+ * // Create a debounced signal with a 500ms delay
52
+ * const query = debounced('', { ms: 500 });
46
53
  *
47
- * // Access the original, un-debounced signal.
48
- * console.log(searchTerm.original()); // Outputs 'new value' (immediately)
49
- * // ... after 500ms ...
50
- * console.log(searchTerm()); // Outputs 'new value' (debounced)
54
+ * effect(() => {
55
+ * // This effect runs 500ms after the last change to 'query'
56
+ * console.log('Debounced Query:', query());
57
+ * });
51
58
  *
52
- * // Directly update the *original* signal.
53
- * searchTerm.original.set('direct update');
54
- * console.log(searchTerm.original()); // Outputs 'direct update' (immediately)
55
- * console.log(searchTerm()); // Outputs 'new value' (still debounced from the previous set)
56
- * // ... after 500ms ...
57
- * console.log(searchTerm()); // Outputs 'direct update' (now reflects the original signal)
59
+ * effect(() => {
60
+ * // This effect runs immediately when 'query.original' changes
61
+ * console.log('Original Query:', query.original());
62
+ * });
58
63
  *
59
- * // Create a debounced signal with undefined initial value and default delay
60
- * const anotherSignal = debounced();
61
- * ```
62
- * @typeParam T - The type of the signal's value.
63
- * @param initial The initial value of the signal. Optional; defaults to `undefined`.
64
- * @param opt Configuration options for the signal, including the debounce delay (`ms`).
65
- * @returns A `DebouncedSignal` instance.
66
- */
67
- export declare function debounced<T>(): DebouncedSignal<T | undefined>;
68
- /**
69
- * Creates a debounced signal with a defined initial value.
64
+ * console.log('Setting query to "a"');
65
+ * query.set('a');
66
+ * // Output: Original Query: a
70
67
  *
71
- * @typeParam T - The type of the signal's value.
72
- * @param initial The initial value of the signal.
73
- * @param opt Configuration options for the signal, including the debounce delay (`ms`).
74
- * @returns A `DebouncedSignal` instance.
68
+ * setTimeout(() => {
69
+ * console.log('Setting query to "ab"');
70
+ * query.set('ab');
71
+ * // Output: Original Query: ab
72
+ * }, 200); // Before debounce timeout
73
+ *
74
+ * setTimeout(() => {
75
+ * console.log('Setting query to "abc"');
76
+ * query.set('abc');
77
+ * // Output: Original Query: abc
78
+ * }, 400); // Before debounce timeout
79
+ *
80
+ * // ~500ms after the *last* set (at 400ms), the debounced effect runs:
81
+ * // Output (at ~900ms): Debounced Query: abc
82
+ * ```
75
83
  */
76
- export declare function debounced<T>(initial: T, opt?: CreateDebouncedOptions<T>): DebouncedSignal<T>;
84
+ export declare function debounced<T>(initial: T, opt: CreateDebouncedOptions<T>): DebouncedSignal<T>;
@@ -1,56 +1,58 @@
1
1
  import { type CreateSignalOptions, type Signal } from '@angular/core';
2
2
  /**
3
- * Options for the mapArray function.
4
- * @template T The type of elements in the array.
5
- * @extends CreateSignalOptions<T> Inherits options for creating individual signals.
6
- */
7
- export type MapArrayOptions<T> = CreateSignalOptions<T> & {
8
- /**
9
- * An optional function to transform each element from the source array.
10
- * If not provided, the original element is used.
11
- * @param source The current value of the source array signal.
12
- * @param index The index of the element being mapped.
13
- * @returns The transformed element for the corresponding signal.
14
- */
15
- map?: (source: T[], index: number) => T;
16
- };
17
- /**
18
- * Creates a reactive array of signals from a source array signal (or a function returning one),
19
- * applying an optional mapping function to each element.
20
- *
21
- * This is useful for scenarios like rendering lists where each item
22
- * needs its own reactive state derived from the source array. It efficiently
23
- * handles changes in the source array's length by reusing existing signals
24
- * for elements that remain, adding signals for new elements, and removing signals
25
- * for deleted elements.
26
- *
27
- * @template T The type of elements in the source array.
28
- * @param source A function that returns the source array (or readonly array).
29
- * This function will be tracked for changes.
30
- * @param opt Optional configuration including a `map` function to transform elements
31
- * and options (`CreateSignalOptions`) for the created signals.
32
- * @returns A signal (`Signal<Signal<T | undefined>[]>`) where the outer signal updates
33
- * when the array length changes, and the inner array contains signals
34
- * representing each element (potentially mapped).
35
- */
36
- export declare function mapArray<T>(source: () => T[], opt?: MapArrayOptions<T | undefined>): Signal<Signal<T | undefined>[]>;
37
- /**
38
- * Creates a reactive array of signals from a source readonly array (or a function returning one),
39
- * applying an optional mapping function to each element.
40
- *
41
- * This is useful for scenarios like rendering lists where each item
42
- * needs its own reactive state derived from the source array. It efficiently
43
- * handles changes in the source array's length by reusing existing signals
44
- * for elements that remain, adding signals for new elements, and removing signals
45
- * for deleted elements.
46
- *
47
- * @template T The type of elements in the source array.
48
- * @param source A function that returns the source readonly array.
49
- * This function will be tracked for changes.
50
- * @param opt Optional configuration including a `map` function to transform elements
51
- * and options (`CreateSignalOptions`) for the created signals.
52
- * @returns A signal (`Signal<Signal<T>[]>`) where the outer signal updates
53
- * when the array length changes, and the inner array contains signals
54
- * representing each element (potentially mapped).
3
+ * Reactively maps items from a source array (or signal of an array) using a provided mapping function.
4
+ *
5
+ * This function serves a similar purpose to SolidJS's `mapArray` by providing stability
6
+ * for mapped items. It receives a source function returning an array (or a Signal<T[]>)
7
+ * and a mapping function.
8
+ *
9
+ * For each item in the source array, it creates a stable `computed` signal representing
10
+ * that item's value at its current index. This stable signal (`Signal<T>`) is passed
11
+ * to the mapping function. This ensures that downstream computations or components
12
+ * depending on the mapped result only re-render or re-calculate for the specific items
13
+ * that have changed, or when items are added/removed, rather than re-evaluating everything
14
+ * when the source array reference changes but items remain the same.
15
+ *
16
+ * It efficiently handles changes in the source array's length by reusing existing mapped
17
+ * results when possible, slicing when the array shrinks, and appending new mapped items
18
+ * when it grows.
19
+ *
20
+ * @template T The type of items in the source array.
21
+ * @template U The type of items in the resulting mapped array.
22
+ *
23
+ * @param source A function returning the source array `T[]`, or a `Signal<T[]>` itself.
24
+ * The `mapArray` function will reactively update based on changes to this source.
25
+ * @param map The mapping function. It is called for each item in the source array.
26
+ * It receives:
27
+ * - `value`: A stable `Signal<T>` representing the item at the current index.
28
+ * Use this signal within your mapping logic if you need reactivity
29
+ * tied to the specific item's value changes.
30
+ * - `index`: The number index of the item in the array.
31
+ * It should return the mapped value `U`.
32
+ * @param [opt] Optional `CreateSignalOptions<T>`. These options are passed directly
33
+ * to the `computed` signal created for each individual item (`Signal<T>`).
34
+ * This allows specifying options like a custom `equal` function for item comparison.
35
+ *
36
+ * @returns A `Signal<U[]>` containing the mapped array. This signal updates whenever
37
+ * the source array changes (either length or the values of its items).
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * const sourceItems = signal([
42
+ * { id: 1, name: 'Apple' },
43
+ * { id: 2, name: 'Banana' }
44
+ * ]);
45
+ *
46
+ * const mappedItems = mapArray(
47
+ * sourceItems,
48
+ * (itemSignal, index) => {
49
+ * // itemSignal is stable for a given item based on its index.
50
+ * // We create a computed here to react to changes in the item's name.
51
+ * return computed(() => `${index}: ${itemSignal().name.toUpperCase()}`);
52
+ * },
53
+ * // Example optional options (e.g., custom equality for item signals)
54
+ * { equal: (a, b) => a.id === b.id && a.name === b.name }
55
+ * );
56
+ * ```
55
57
  */
56
- export declare function mapArray<T>(source: () => readonly T[], opt?: MapArrayOptions<T>): Signal<Signal<T>[]>;
58
+ export declare function mapArray<T, U>(source: () => T[], map: (value: Signal<T>, index: number) => U, opt?: CreateSignalOptions<T>): Signal<U[]>;
package/lib/mutable.d.ts CHANGED
@@ -31,7 +31,7 @@ export type MutableSignal<T> = WritableSignal<T> & {
31
31
  * const myObject = mutable({ a: 1, b: 2 });
32
32
  * myObject.inline((obj) => (obj.a = 3)); // myObject() now returns { a: 3, b: 2 }
33
33
  */
34
- inline: (updater: Parameters<WritableSignal<T>['update']>[0]) => void;
34
+ inline: (updater: (value: T) => void) => void;
35
35
  };
36
36
  /**
37
37
  * Creates a `MutableSignal`. This function overloads the standard `signal` function to provide
@@ -0,0 +1,126 @@
1
+ import { Signal, type CreateSignalOptions, type WritableSignal } from '@angular/core';
2
+ /**
3
+ * Interface for storage mechanisms compatible with the `stored` signal.
4
+ * Matches the essential parts of the `Storage` interface (`localStorage`, `sessionStorage`).
5
+ */
6
+ type Store = {
7
+ /** Retrieves an item from storage for a given key. */
8
+ getItem: (key: string) => string | null;
9
+ /** Sets an item in storage for a given key. */
10
+ setItem: (key: string, value: string) => void;
11
+ /** Removes an item from storage for a given key. */
12
+ removeItem: (key: string) => void;
13
+ };
14
+ /**
15
+ * Options for creating a signal synchronized with persistent storage using `stored()`.
16
+ * Extends Angular's `CreateSignalOptions`.
17
+ *
18
+ * @template T The type of value held by the signal.
19
+ */
20
+ export type CreateStoredOptions<T> = CreateSignalOptions<T> & {
21
+ /**
22
+ * The key used to identify the item in storage.
23
+ * Can be a static string or a function/signal returning a string for dynamic keys
24
+ * (e.g., based on user ID or other application state).
25
+ */
26
+ key: string | (() => string);
27
+ /**
28
+ * Optional custom storage implementation (e.g., `sessionStorage` or a custom adapter).
29
+ * Must conform to the `Store` interface (`getItem`, `setItem`, `removeItem`).
30
+ * Defaults to `localStorage` in browser environments and a no-op store on the server.
31
+ */
32
+ store?: Store;
33
+ /**
34
+ * Optional function to serialize the value (type `T`) into a string before storing.
35
+ * Defaults to `JSON.stringify`.
36
+ * @param {T} value The value to serialize.
37
+ * @returns {string} The serialized string representation.
38
+ */
39
+ serialize?: (value: T) => string;
40
+ /**
41
+ * Optional function to deserialize the string retrieved from storage back into the value (type `T`).
42
+ * Defaults to `JSON.parse`.
43
+ * @param {string} value The string retrieved from storage.
44
+ * @returns {T} The deserialized value.
45
+ */
46
+ deserialize?: (value: string) => T;
47
+ /**
48
+ * If `true`, the signal will attempt to synchronize its state across multiple browser tabs
49
+ * using the `storage` event. Changes made in one tab (set, update, clear) will be
50
+ * reflected in other tabs using the same storage key.
51
+ * Requires a browser environment. Defaults to `false`.
52
+ */
53
+ syncTabs?: boolean;
54
+ };
55
+ /**
56
+ * A specialized `WritableSignal` returned by the `stored()` function.
57
+ * It synchronizes its value with persistent storage and provides additional methods.
58
+ *
59
+ * @template T The type of value held by the signal (matches the fallback type).
60
+ */
61
+ export type StoredSignal<T> = WritableSignal<T> & {
62
+ /**
63
+ * Removes the item associated with the signal's key from the configured storage.
64
+ * After clearing, reading the signal will return the fallback value until it's set again.
65
+ */
66
+ clear: () => void;
67
+ /**
68
+ * A `Signal<string>` containing the current storage key being used by this stored signal.
69
+ * This is particularly useful if the key was configured dynamically. You can read or react
70
+ * to this signal to know the active key.
71
+ */
72
+ key: Signal<string>;
73
+ };
74
+ /**
75
+ * Creates a `WritableSignal` whose state is automatically synchronized with persistent storage
76
+ * (like `localStorage` or `sessionStorage`).
77
+ *
78
+ * It handles Server-Side Rendering (SSR) gracefully, allows dynamic storage keys,
79
+ * custom serialization/deserialization, custom storage providers, and optional
80
+ * synchronization across browser tabs.
81
+ *
82
+ * @template T The type of value held by the signal and stored (after serialization).
83
+ * @param fallback The default value of type `T` to use when no value is found in storage
84
+ * or when deserialization fails. The signal's value will never be `null` or `undefined`
85
+ * publicly, it will always revert to this fallback.
86
+ * @param options Configuration options (`CreateStoredOptions<T>`). Requires at least the `key`.
87
+ * @returns A `StoredSignal<T>` instance. This signal behaves like a standard `WritableSignal<T>`,
88
+ * but its value is persisted. It includes a `.clear()` method to remove the item from storage
89
+ * and a `.key` signal providing the current storage key.
90
+ *
91
+ * @remarks
92
+ * - **Persistence:** The signal automatically saves its value to storage whenever the signal's
93
+ * value or its configured `key` changes. This is managed internally using `effect`.
94
+ * - **SSR Safety:** Detects server environments and uses a no-op storage, preventing errors.
95
+ * - **Error Handling:** Catches and logs errors during serialization/deserialization in dev mode.
96
+ * - **Tab Sync:** If `syncTabs` is true, listens to `storage` events to keep the signal value
97
+ * consistent across browser tabs using the same key. Cleanup is handled automatically
98
+ * using `DestroyRef`.
99
+ * - **Removal:** Use the `.clear()` method on the returned signal to remove the item from storage.
100
+ * Setting the signal to the fallback value will store the fallback value, not remove the item.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * import { Component, effect, signal } from '@angular/core';
105
+ * import { stored } from '@mmstack/primitives'; // Adjust import path
106
+ *
107
+ * @Component({
108
+ * selector: 'app-settings',
109
+ * standalone: true,
110
+ * template: `
111
+ * Theme:
112
+ * <select [ngModel]="theme()" (ngModelChange)="theme.set($event)">
113
+ * <option value="light">Light</option>
114
+ * <option value="dark">Dark</option>
115
+ * </select>
116
+ * <button (click)="theme.clear()">Clear Theme Setting</button>
117
+ * <p>Storage Key Used: {{ theme.key() }}</p>
118
+ * ` // Requires FormsModule for ngModel
119
+ * })
120
+ * export class SettingsComponent {
121
+ * theme = stored<'light' | 'dark'>('light', { key: 'app-theme', syncTabs: true });
122
+ * }
123
+ * ```
124
+ */
125
+ export declare function stored<T>(fallback: T, { key, store: providedStore, serialize, deserialize, syncTabs, equal, ...rest }: CreateStoredOptions<T>): StoredSignal<T>;
126
+ export {};
package/package.json CHANGED
@@ -1,8 +1,22 @@
1
1
  {
2
2
  "name": "@mmstack/primitives",
3
- "version": "19.0.4",
3
+ "version": "19.0.5",
4
+ "keywords": [
5
+ "angular",
6
+ "signals",
7
+ "debounce",
8
+ "mapArray",
9
+ "primitives"
10
+ ],
11
+ "license": "MIT",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/mihajm/mmstack"
15
+ },
16
+ "homepage": "https://github.com/mihajm/mmstack/blob/master/packages/primitives",
4
17
  "peerDependencies": {
5
18
  "@angular/core": "~19.2.3",
19
+ "@angular/common": "~19.2.3",
6
20
  "@mmstack/object": "~19.0.0"
7
21
  },
8
22
  "sideEffects": false,