@craft-ng/core 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,8 @@
1
1
  import * as _angular_core from '@angular/core';
2
- import { Signal, WritableSignal, ValueEqualityFn, Injector, InjectionToken, Provider, Type, EventEmitter, ResourceRef, ResourceOptions, ResourceStatus, ResourceLoaderParams, ResourceStreamingLoader } from '@angular/core';
2
+ import { WritableSignal, Signal, Injector, InjectionToken, Provider, ValueEqualityFn, Type, EventEmitter, ResourceRef, ResourceOptions, ResourceStatus, ResourceLoaderParams, ResourceStreamingLoader } from '@angular/core';
3
3
  import { CompatFieldState, FieldState, ReadonlyArrayLike, MaybeFieldTree, Subfields, FieldTree, SchemaPathTree, PathKind, SchemaPath, SchemaPathRules, ValidationError } from '@angular/forms/signals';
4
4
  import { AbstractControl } from '@angular/forms';
5
5
 
6
- type ReadonlySource<T> = Signal<T | undefined> & {
7
- preserveLastValue: Signal<T | undefined>;
8
- } & SourceBranded;
9
-
10
6
  declare const SourceBranded: {
11
7
  [SourceBrand]: true;
12
8
  };
@@ -24,367 +20,9 @@ declare function createMethodHandlers<State>(methodsData: Record<string, ((...ar
24
20
  onStateChange?: (newValue: State) => void;
25
21
  }): Record<string, Function>;
26
22
 
27
- type SignalSource<T> = Signal<T | undefined> & {
28
- set: (value: T) => void;
23
+ type ReadonlySource<T> = Signal<T | undefined> & {
29
24
  preserveLastValue: Signal<T | undefined>;
30
25
  } & SourceBranded;
31
- /**
32
- * Creates a source for event-driven communication with lazy emission semantics.
33
- *
34
- * Sources are the foundation of event-driven patterns in ng-craft, enabling:
35
- * - Discrete event emissions (unlike continuous signals)
36
- * - Lazy behavior (undefined until explicitly set)
37
- * - Decoupled communication between components and stores
38
- * - Automatic triggering of queries, mutations, and async methods
39
- * - Multi-listener support with independent subscription timing
40
- *
41
- * @remarks
42
- * **Core Concept:**
43
- * Sources implement event emitter pattern with reactive semantics:
44
- * - Emit only when explicitly set (not on every read like signals)
45
- * - Listeners receive `undefined` on first read (lazy semantics)
46
- * - New listeners don't receive previous emissions by default
47
- * - Use `preserveLastValue` to get the last emitted value immediately
48
- *
49
- * **Difference from Signals:**
50
- * - **Signals**: Always have a value, recompute on access, continuous state
51
- * - **Sources**: Emit on explicit set, lazy by default, discrete events
52
- * - Sources are for events/actions, signals are for state
53
- *
54
- * **Use Cases:**
55
- * - **User actions**: Button clicks, form submissions, custom events
56
- * - **Navigation events**: Route changes, tab switches
57
- * - **Data events**: Reload triggers, refresh requests
58
- * - **Coordination**: Communication between disconnected components
59
- * - **Store inputs**: Triggering queries/mutations from components
60
- * - **Event buses**: Decoupled event communication
61
- *
62
- * **Integration with Queries/Mutations:**
63
- * - Bind to method using `afterRecomputation(source, callback)`
64
- * - Query/mutation executes automatically when source emits
65
- * - No manual method exposed (source-based triggering)
66
- *
67
- * **Listener Semantics:**
68
- * - **Standard listener**: Returns `undefined` until source emits, then returns new values only
69
- * - **preserveLastValue**: Returns last emitted value immediately, then tracks new values
70
- * - Useful for late subscribers that need current state
71
- *
72
- * **Limitations:**
73
- * Sources are signals and behave differently from observables.
74
- * Understanding these three key limitations is important:
75
- * - **Multiple sets in same cycle**: When a source is set multiple times during the same cycle
76
- * (between the first set and the Change Detection that executes all consumer callbacks),
77
- * consumers will only react once during CD and will only see the last set value.
78
- * Intermediate values are discarded.
79
- * - **Multiple sources order**: Within the same cycle, if multiple sources are triggered,
80
- * consumers cannot determine the order in which the sources were set.
81
- * The original emission sequence is not preserved.
82
- * - **Consumer execution order**: When multiple sources are triggered in the same cycle,
83
- * consumer callbacks are invoked in the order they were declared, not in the order
84
- * their source producers were triggered.
85
- * - **No synchronous intermediate value reactions**: Unlike observables, sources cannot react
86
- * to each intermediate value synchronously. A mechanism similar to observables
87
- * (or using native Observable API) without RxJS is being considered to enable
88
- * synchronous reactions to intermediate values, matching the behavior currently
89
- * offered by observables.
90
- *
91
- * @template T - The type of values emitted by the source
92
- *
93
- * @param options - Optional configuration:
94
- * - `equal`: Custom equality function for change detection (prevents duplicate emissions)
95
- * - `debugName`: Name for debugging purposes
96
- *
97
- * @returns A source object with:
98
- * - `()`: Read current value (undefined until first emission)
99
- * - `set(value)`: Emit a value to all listeners
100
- * - `preserveLastValue`: Alternative signal that returns last value immediately
101
- *
102
- * @example
103
- * Basic source for user actions
104
- * ```ts
105
- * const { injectCraft } = craft(
106
- * { name: '', providedIn: 'root' },
107
- * craftSources({
108
- * loadUser: source<string>(),
109
- * }),
110
- * craftQuery('user', ({ loadUser }) =>
111
- * query({
112
- * method: afterRecomputation(loadUser, (userId) => userId),
113
- * loader: async ({ params }) => {
114
- * const response = await fetch(`/api/users/${params}`);
115
- * return response.json();
116
- * },
117
- * })
118
- * )
119
- * );
120
- *
121
- * const store = injectCraft();
122
- *
123
- * // Query executes automatically when source emits
124
- * store.setLoadUser('user-123');
125
- * // -> loadUser source emits 'user-123'
126
- * // -> user query executes with params 'user-123'
127
- *
128
- * store.setLoadUser('user-456');
129
- * // -> user query executes again with params 'user-456'
130
- * ```
131
- *
132
- * @example
133
- * Source for form submission
134
- * ```ts
135
- * type FormData = { name: string; email: string };
136
- *
137
- * const { injectCraft } = craft(
138
- * { name: '', providedIn: 'root' },
139
- * craftSources({
140
- * submitForm: source<FormData>(),
141
- * }),
142
- * craftMutations(({ submitForm }) => ({
143
- * submit: mutation({
144
- * method: afterRecomputation(submitForm, (data) => data),
145
- * loader: async ({ params }) => {
146
- * const response = await fetch('/api/submit', {
147
- * method: 'POST',
148
- * body: JSON.stringify(params),
149
- * });
150
- * return response.json();
151
- * },
152
- * }),
153
- * }))
154
- * );
155
- *
156
- * const store = injectCraft();
157
- *
158
- * // In component template:
159
- * // <form (submit)="onSubmit()">
160
- * // <input name="name" [(ngModel)]="formData.name" />
161
- * // <input name="email" [(ngModel)]="formData.email" />
162
- * // </form>
163
- *
164
- * onSubmit() {
165
- * // Mutation executes automatically
166
- * this.store.setSubmitForm(this.formData);
167
- * // -> submitForm source emits
168
- * // -> submit mutation executes
169
- * }
170
- * ```
171
- *
172
- * @example
173
- * Source for reload/refresh actions
174
- * ```ts
175
- * const { injectCraft } = craft(
176
- * { name: '', providedIn: 'root' },
177
- * craftSources({
178
- * reload: source<void>(),
179
- * }),
180
- * craftQuery('data', ({ reload }) =>
181
- * query(
182
- * {
183
- * params: () => ({}),
184
- * loader: async () => {
185
- * const response = await fetch('/api/data');
186
- * return response.json();
187
- * },
188
- * },
189
- * insertReloadOnSource(reload)
190
- * )
191
- * )
192
- * );
193
- *
194
- * const store = injectCraft();
195
- *
196
- * // Trigger reload from anywhere
197
- * store.setReload();
198
- * // -> reload source emits
199
- * // -> query reloads
200
- *
201
- * // In component:
202
- * // <button (click)="store.setReload()">Refresh</button>
203
- * ```
204
- *
205
- * @example
206
- * Multiple sources for different actions
207
- * ```ts
208
- * const { injectCraft } = craft(
209
- * { name: '', providedIn: 'root' },
210
- * craftSources({
211
- * addTodo: source<{ text: string }>(),
212
- * deleteTodo: source<string>(),
213
- * toggleTodo: source<string>(),
214
- * }),
215
- * craftMutations(({ addTodo, deleteTodo, toggleTodo }) => ({
216
- * create: mutation({
217
- * method: afterRecomputation(addTodo, (data) => data),
218
- * loader: async ({ params }) => {
219
- * const response = await fetch('/api/todos', {
220
- * method: 'POST',
221
- * body: JSON.stringify(params),
222
- * });
223
- * return response.json();
224
- * },
225
- * }),
226
- * delete: mutation({
227
- * method: afterRecomputation(deleteTodo, (id) => id),
228
- * loader: async ({ params }) => {
229
- * await fetch(`/api/todos/${params}`, { method: 'DELETE' });
230
- * return { deleted: true };
231
- * },
232
- * }),
233
- * toggle: mutation({
234
- * method: afterRecomputation(toggleTodo, (id) => id),
235
- * loader: async ({ params }) => {
236
- * const response = await fetch(`/api/todos/${params}/toggle`, {
237
- * method: 'PATCH',
238
- * });
239
- * return response.json();
240
- * },
241
- * }),
242
- * }))
243
- * );
244
- *
245
- * const store = injectCraft();
246
- *
247
- * // Different actions trigger different mutations
248
- * store.setAddTodo({ text: 'Buy milk' });
249
- * store.setToggleTodo('todo-123');
250
- * store.setDeleteTodo('todo-456');
251
- * ```
252
- *
253
- * @example
254
- * Late listener with preserveLastValue
255
- * ```ts
256
- * const mySource = source<string>();
257
- *
258
- * // Early listener
259
- * const listener1 = computed(() => mySource());
260
- * console.log(listener1()); // undefined
261
- *
262
- * // Emit value
263
- * mySource.set('Hello');
264
- * console.log(listener1()); // 'Hello'
265
- *
266
- * // Late listener (after emission)
267
- * const listener2 = computed(() => mySource());
268
- * console.log(listener2()); // undefined (doesn't get previous emission)
269
- *
270
- * mySource.set('World');
271
- * console.log(listener1()); // 'World'
272
- * console.log(listener2()); // 'World'
273
- *
274
- * // Using preserveLastValue for late listeners
275
- * const listener3 = computed(() => mySource.preserveLastValue());
276
- * console.log(listener3()); // 'World' (gets last value immediately)
277
- * ```
278
- *
279
- * @example
280
- * Custom equality to prevent duplicate emissions
281
- * ```ts
282
- * type Params = { id: string; timestamp: number };
283
- *
284
- * const paramsSource = source<Params>({
285
- * equal: (a, b) => a?.id === b?.id, // Compare only by id
286
- * });
287
- *
288
- * const listener = computed(() => paramsSource());
289
- *
290
- * paramsSource.set({ id: 'item-1', timestamp: Date.now() });
291
- * // -> listener receives value
292
- *
293
- * paramsSource.set({ id: 'item-1', timestamp: Date.now() });
294
- * // -> listener does NOT receive value (same id)
295
- *
296
- * paramsSource.set({ id: 'item-2', timestamp: Date.now() });
297
- * // -> listener receives value (different id)
298
- * ```
299
- *
300
- * @example
301
- * Source for coordinating multiple components
302
- * ```ts
303
- * // Global source (outside component)
304
- * const refreshAllSource = source<void>();
305
- *
306
- * // Component A
307
- * @Component({
308
- * selector: 'app-data-view',
309
- * template: '...',
310
- * })
311
- * export class DataViewComponent {
312
- * { injectCraft } = craft(
313
- * { name: '', providedIn: 'root' },
314
- * craftQuery('data', () =>
315
- * query(
316
- * {
317
- * params: () => ({}),
318
- * loader: async () => {
319
- * const response = await fetch('/api/data');
320
- * return response.json();
321
- * },
322
- * },
323
- * insertReloadOnSource(refreshAllSource)
324
- * )
325
- * )
326
- * );
327
- *
328
- * store = this.injectCraft();
329
- * }
330
- *
331
- * // Component B
332
- * @Component({
333
- * selector: 'app-refresh-button',
334
- * template: '<button (click)="refresh()">Refresh All</button>',
335
- * })
336
- * export class RefreshButtonComponent {
337
- * refresh() {
338
- * // Triggers refresh in all components listening to this source
339
- * refreshAllSource.set();
340
- * }
341
- * }
342
- * ```
343
- *
344
- * @example
345
- * Source with complex payload
346
- * ```ts
347
- * type SearchParams = {
348
- * query: string;
349
- * filters: string[];
350
- * page: number;
351
- * };
352
- *
353
- * const { injectCraft } = craft(
354
- * { name: '', providedIn: 'root' },
355
- * craftSources({
356
- * search: source<SearchParams>(),
357
- * }),
358
- * craftQuery('results', ({ search }) =>
359
- * query({
360
- * method: afterRecomputation(search, (params) => params),
361
- * loader: async ({ params }) => {
362
- * const queryString = new URLSearchParams({
363
- * q: params.query,
364
- * filters: params.filters.join(','),
365
- * page: String(params.page),
366
- * });
367
- * const response = await fetch(`/api/search?${queryString}`);
368
- * return response.json();
369
- * },
370
- * })
371
- * )
372
- * );
373
- *
374
- * const store = injectCraft();
375
- *
376
- * // Emit complex search parameters
377
- * store.setSearch({
378
- * query: 'angular',
379
- * filters: ['tutorial', 'advanced'],
380
- * page: 1,
381
- * });
382
- * ```
383
- */
384
- declare function signalSource<T>(options?: {
385
- equal?: ValueEqualityFn<NoInfer<T> | undefined>;
386
- debugName?: string;
387
- }): SignalSource<T>;
388
26
 
389
27
  /**
390
28
  * Creates a derived readonly source that transforms source emissions through a callback function.
@@ -691,7 +329,7 @@ declare function signalSource<T>(options?: {
691
329
  * // -> mutation receives exact same object
692
330
  * ```
693
331
  */
694
- declare function afterRecomputation<State, SourceType>(_source: SignalSource<SourceType>, callback: (source: SourceType) => State): ReadonlySource<State>;
332
+ declare function afterRecomputation<State, SourceType>(_source: ReadonlySource<SourceType>, callback: (source: SourceType) => State): ReadonlySource<State>;
695
333
 
696
334
  type ContextConstraints = {
697
335
  props: {};
@@ -2167,6 +1805,368 @@ declare function craft<outputs1 extends ContextConstraints, standaloneOutputs1 e
2167
1805
  implements?: ToImplementContract;
2168
1806
  }>;
2169
1807
 
1808
+ type SignalSource<T> = Signal<T | undefined> & {
1809
+ set: (value: T) => void;
1810
+ preserveLastValue: Signal<T | undefined>;
1811
+ } & SourceBranded;
1812
+ /**
1813
+ * Creates a source for event-driven communication with lazy emission semantics.
1814
+ *
1815
+ * Sources are the foundation of event-driven patterns in ng-craft, enabling:
1816
+ * - Discrete event emissions (unlike continuous signals)
1817
+ * - Lazy behavior (undefined until explicitly set)
1818
+ * - Decoupled communication between components and stores
1819
+ * - Automatic triggering of queries, mutations, and async methods
1820
+ * - Multi-listener support with independent subscription timing
1821
+ *
1822
+ * @remarks
1823
+ * **Core Concept:**
1824
+ * Sources implement event emitter pattern with reactive semantics:
1825
+ * - Emit only when explicitly set (not on every read like signals)
1826
+ * - Listeners receive `undefined` on first read (lazy semantics)
1827
+ * - New listeners don't receive previous emissions by default
1828
+ * - Use `preserveLastValue` to get the last emitted value immediately
1829
+ *
1830
+ * **Difference from Signals:**
1831
+ * - **Signals**: Always have a value, recompute on access, continuous state
1832
+ * - **Sources**: Emit on explicit set, lazy by default, discrete events
1833
+ * - Sources are for events/actions, signals are for state
1834
+ *
1835
+ * **Use Cases:**
1836
+ * - **User actions**: Button clicks, form submissions, custom events
1837
+ * - **Navigation events**: Route changes, tab switches
1838
+ * - **Data events**: Reload triggers, refresh requests
1839
+ * - **Coordination**: Communication between disconnected components
1840
+ * - **Store inputs**: Triggering queries/mutations from components
1841
+ * - **Event buses**: Decoupled event communication
1842
+ *
1843
+ * **Integration with Queries/Mutations:**
1844
+ * - Bind to method using `afterRecomputation(source, callback)`
1845
+ * - Query/mutation executes automatically when source emits
1846
+ * - No manual method exposed (source-based triggering)
1847
+ *
1848
+ * **Listener Semantics:**
1849
+ * - **Standard listener**: Returns `undefined` until source emits, then returns new values only
1850
+ * - **preserveLastValue**: Returns last emitted value immediately, then tracks new values
1851
+ * - Useful for late subscribers that need current state
1852
+ *
1853
+ * **Limitations:**
1854
+ * Sources are signals and behave differently from observables.
1855
+ * Understanding these three key limitations is important:
1856
+ * - **Multiple sets in same cycle**: When a source is set multiple times during the same cycle
1857
+ * (between the first set and the Change Detection that executes all consumer callbacks),
1858
+ * consumers will only react once during CD and will only see the last set value.
1859
+ * Intermediate values are discarded.
1860
+ * - **Multiple sources order**: Within the same cycle, if multiple sources are triggered,
1861
+ * consumers cannot determine the order in which the sources were set.
1862
+ * The original emission sequence is not preserved.
1863
+ * - **Consumer execution order**: When multiple sources are triggered in the same cycle,
1864
+ * consumer callbacks are invoked in the order they were declared, not in the order
1865
+ * their source producers were triggered.
1866
+ * - **No synchronous intermediate value reactions**: Unlike observables, sources cannot react
1867
+ * to each intermediate value synchronously. A mechanism similar to observables
1868
+ * (or using native Observable API) without RxJS is being considered to enable
1869
+ * synchronous reactions to intermediate values, matching the behavior currently
1870
+ * offered by observables.
1871
+ *
1872
+ * @template T - The type of values emitted by the source
1873
+ *
1874
+ * @param options - Optional configuration:
1875
+ * - `equal`: Custom equality function for change detection (prevents duplicate emissions)
1876
+ * - `debugName`: Name for debugging purposes
1877
+ *
1878
+ * @returns A source object with:
1879
+ * - `()`: Read current value (undefined until first emission)
1880
+ * - `set(value)`: Emit a value to all listeners
1881
+ * - `preserveLastValue`: Alternative signal that returns last value immediately
1882
+ *
1883
+ * @example
1884
+ * Basic source for user actions
1885
+ * ```ts
1886
+ * const { injectCraft } = craft(
1887
+ * { name: '', providedIn: 'root' },
1888
+ * craftSources({
1889
+ * loadUser: source<string>(),
1890
+ * }),
1891
+ * craftQuery('user', ({ loadUser }) =>
1892
+ * query({
1893
+ * method: afterRecomputation(loadUser, (userId) => userId),
1894
+ * loader: async ({ params }) => {
1895
+ * const response = await fetch(`/api/users/${params}`);
1896
+ * return response.json();
1897
+ * },
1898
+ * })
1899
+ * )
1900
+ * );
1901
+ *
1902
+ * const store = injectCraft();
1903
+ *
1904
+ * // Query executes automatically when source emits
1905
+ * store.setLoadUser('user-123');
1906
+ * // -> loadUser source emits 'user-123'
1907
+ * // -> user query executes with params 'user-123'
1908
+ *
1909
+ * store.setLoadUser('user-456');
1910
+ * // -> user query executes again with params 'user-456'
1911
+ * ```
1912
+ *
1913
+ * @example
1914
+ * Source for form submission
1915
+ * ```ts
1916
+ * type FormData = { name: string; email: string };
1917
+ *
1918
+ * const { injectCraft } = craft(
1919
+ * { name: '', providedIn: 'root' },
1920
+ * craftSources({
1921
+ * submitForm: source<FormData>(),
1922
+ * }),
1923
+ * craftMutations(({ submitForm }) => ({
1924
+ * submit: mutation({
1925
+ * method: afterRecomputation(submitForm, (data) => data),
1926
+ * loader: async ({ params }) => {
1927
+ * const response = await fetch('/api/submit', {
1928
+ * method: 'POST',
1929
+ * body: JSON.stringify(params),
1930
+ * });
1931
+ * return response.json();
1932
+ * },
1933
+ * }),
1934
+ * }))
1935
+ * );
1936
+ *
1937
+ * const store = injectCraft();
1938
+ *
1939
+ * // In component template:
1940
+ * // <form (submit)="onSubmit()">
1941
+ * // <input name="name" [(ngModel)]="formData.name" />
1942
+ * // <input name="email" [(ngModel)]="formData.email" />
1943
+ * // </form>
1944
+ *
1945
+ * onSubmit() {
1946
+ * // Mutation executes automatically
1947
+ * this.store.setSubmitForm(this.formData);
1948
+ * // -> submitForm source emits
1949
+ * // -> submit mutation executes
1950
+ * }
1951
+ * ```
1952
+ *
1953
+ * @example
1954
+ * Source for reload/refresh actions
1955
+ * ```ts
1956
+ * const { injectCraft } = craft(
1957
+ * { name: '', providedIn: 'root' },
1958
+ * craftSources({
1959
+ * reload: source<void>(),
1960
+ * }),
1961
+ * craftQuery('data', ({ reload }) =>
1962
+ * query(
1963
+ * {
1964
+ * params: () => ({}),
1965
+ * loader: async () => {
1966
+ * const response = await fetch('/api/data');
1967
+ * return response.json();
1968
+ * },
1969
+ * },
1970
+ * insertReloadOnSource(reload)
1971
+ * )
1972
+ * )
1973
+ * );
1974
+ *
1975
+ * const store = injectCraft();
1976
+ *
1977
+ * // Trigger reload from anywhere
1978
+ * store.setReload();
1979
+ * // -> reload source emits
1980
+ * // -> query reloads
1981
+ *
1982
+ * // In component:
1983
+ * // <button (click)="store.setReload()">Refresh</button>
1984
+ * ```
1985
+ *
1986
+ * @example
1987
+ * Multiple sources for different actions
1988
+ * ```ts
1989
+ * const { injectCraft } = craft(
1990
+ * { name: '', providedIn: 'root' },
1991
+ * craftSources({
1992
+ * addTodo: source<{ text: string }>(),
1993
+ * deleteTodo: source<string>(),
1994
+ * toggleTodo: source<string>(),
1995
+ * }),
1996
+ * craftMutations(({ addTodo, deleteTodo, toggleTodo }) => ({
1997
+ * create: mutation({
1998
+ * method: afterRecomputation(addTodo, (data) => data),
1999
+ * loader: async ({ params }) => {
2000
+ * const response = await fetch('/api/todos', {
2001
+ * method: 'POST',
2002
+ * body: JSON.stringify(params),
2003
+ * });
2004
+ * return response.json();
2005
+ * },
2006
+ * }),
2007
+ * delete: mutation({
2008
+ * method: afterRecomputation(deleteTodo, (id) => id),
2009
+ * loader: async ({ params }) => {
2010
+ * await fetch(`/api/todos/${params}`, { method: 'DELETE' });
2011
+ * return { deleted: true };
2012
+ * },
2013
+ * }),
2014
+ * toggle: mutation({
2015
+ * method: afterRecomputation(toggleTodo, (id) => id),
2016
+ * loader: async ({ params }) => {
2017
+ * const response = await fetch(`/api/todos/${params}/toggle`, {
2018
+ * method: 'PATCH',
2019
+ * });
2020
+ * return response.json();
2021
+ * },
2022
+ * }),
2023
+ * }))
2024
+ * );
2025
+ *
2026
+ * const store = injectCraft();
2027
+ *
2028
+ * // Different actions trigger different mutations
2029
+ * store.setAddTodo({ text: 'Buy milk' });
2030
+ * store.setToggleTodo('todo-123');
2031
+ * store.setDeleteTodo('todo-456');
2032
+ * ```
2033
+ *
2034
+ * @example
2035
+ * Late listener with preserveLastValue
2036
+ * ```ts
2037
+ * const mySource = source<string>();
2038
+ *
2039
+ * // Early listener
2040
+ * const listener1 = computed(() => mySource());
2041
+ * console.log(listener1()); // undefined
2042
+ *
2043
+ * // Emit value
2044
+ * mySource.set('Hello');
2045
+ * console.log(listener1()); // 'Hello'
2046
+ *
2047
+ * // Late listener (after emission)
2048
+ * const listener2 = computed(() => mySource());
2049
+ * console.log(listener2()); // undefined (doesn't get previous emission)
2050
+ *
2051
+ * mySource.set('World');
2052
+ * console.log(listener1()); // 'World'
2053
+ * console.log(listener2()); // 'World'
2054
+ *
2055
+ * // Using preserveLastValue for late listeners
2056
+ * const listener3 = computed(() => mySource.preserveLastValue());
2057
+ * console.log(listener3()); // 'World' (gets last value immediately)
2058
+ * ```
2059
+ *
2060
+ * @example
2061
+ * Custom equality to prevent duplicate emissions
2062
+ * ```ts
2063
+ * type Params = { id: string; timestamp: number };
2064
+ *
2065
+ * const paramsSource = source<Params>({
2066
+ * equal: (a, b) => a?.id === b?.id, // Compare only by id
2067
+ * });
2068
+ *
2069
+ * const listener = computed(() => paramsSource());
2070
+ *
2071
+ * paramsSource.set({ id: 'item-1', timestamp: Date.now() });
2072
+ * // -> listener receives value
2073
+ *
2074
+ * paramsSource.set({ id: 'item-1', timestamp: Date.now() });
2075
+ * // -> listener does NOT receive value (same id)
2076
+ *
2077
+ * paramsSource.set({ id: 'item-2', timestamp: Date.now() });
2078
+ * // -> listener receives value (different id)
2079
+ * ```
2080
+ *
2081
+ * @example
2082
+ * Source for coordinating multiple components
2083
+ * ```ts
2084
+ * // Global source (outside component)
2085
+ * const refreshAllSource = source<void>();
2086
+ *
2087
+ * // Component A
2088
+ * @Component({
2089
+ * selector: 'app-data-view',
2090
+ * template: '...',
2091
+ * })
2092
+ * export class DataViewComponent {
2093
+ * { injectCraft } = craft(
2094
+ * { name: '', providedIn: 'root' },
2095
+ * craftQuery('data', () =>
2096
+ * query(
2097
+ * {
2098
+ * params: () => ({}),
2099
+ * loader: async () => {
2100
+ * const response = await fetch('/api/data');
2101
+ * return response.json();
2102
+ * },
2103
+ * },
2104
+ * insertReloadOnSource(refreshAllSource)
2105
+ * )
2106
+ * )
2107
+ * );
2108
+ *
2109
+ * store = this.injectCraft();
2110
+ * }
2111
+ *
2112
+ * // Component B
2113
+ * @Component({
2114
+ * selector: 'app-refresh-button',
2115
+ * template: '<button (click)="refresh()">Refresh All</button>',
2116
+ * })
2117
+ * export class RefreshButtonComponent {
2118
+ * refresh() {
2119
+ * // Triggers refresh in all components listening to this source
2120
+ * refreshAllSource.set();
2121
+ * }
2122
+ * }
2123
+ * ```
2124
+ *
2125
+ * @example
2126
+ * Source with complex payload
2127
+ * ```ts
2128
+ * type SearchParams = {
2129
+ * query: string;
2130
+ * filters: string[];
2131
+ * page: number;
2132
+ * };
2133
+ *
2134
+ * const { injectCraft } = craft(
2135
+ * { name: '', providedIn: 'root' },
2136
+ * craftSources({
2137
+ * search: source<SearchParams>(),
2138
+ * }),
2139
+ * craftQuery('results', ({ search }) =>
2140
+ * query({
2141
+ * method: afterRecomputation(search, (params) => params),
2142
+ * loader: async ({ params }) => {
2143
+ * const queryString = new URLSearchParams({
2144
+ * q: params.query,
2145
+ * filters: params.filters.join(','),
2146
+ * page: String(params.page),
2147
+ * });
2148
+ * const response = await fetch(`/api/search?${queryString}`);
2149
+ * return response.json();
2150
+ * },
2151
+ * })
2152
+ * )
2153
+ * );
2154
+ *
2155
+ * const store = injectCraft();
2156
+ *
2157
+ * // Emit complex search parameters
2158
+ * store.setSearch({
2159
+ * query: 'angular',
2160
+ * filters: ['tutorial', 'advanced'],
2161
+ * page: 1,
2162
+ * });
2163
+ * ```
2164
+ */
2165
+ declare function signalSource<T>(options?: {
2166
+ equal?: ValueEqualityFn<NoInfer<T> | undefined>;
2167
+ debugName?: string;
2168
+ }): SignalSource<T>;
2169
+
2170
2170
  type ExtractSignalPropsAndMethods<State, StateKeysTuple, Acc extends {
2171
2171
  props: {};
2172
2172
  methods: Record<string, Function>;
@@ -9265,5 +9265,130 @@ declare function insertFormSubmit<FormValue, MutationValue, MutationParams, Muta
9265
9265
  submitExceptions: Signal<ToSubmitExceptions<InsertMetaInCraftExceptionIfExists<MutationExceptions['params'], 'params', MutationIdentifier> | InsertMetaInCraftExceptionIfExists<MutationExceptions['loader'], 'loader', MutationIdentifier>, SuccessExceptions, ErrorExceptions, ExceptionExceptions, FormIdentifier>[]>;
9266
9266
  }>;
9267
9267
 
9268
- export { CRAFT_EXCEPTION_SYMBOL, EXTERNALLY_PROVIDED, EmptyContext, GlobalPersisterHandlerService, STORE_CONFIG_TOKEN, SourceBrand, SourceBranded, VALIDATOR_OUTPUT_SYMBOL, addMany, addOne, afterRecomputation, asyncProcess, cAsyncValidate, cAsyncValidator, cEmail, cMax, cMaxLength, cMin, cMinLength, cPattern, cRequired, cValidate, cValidator, capitalize, computedIds, computedSource, computedTotal, contract, craft, craftAsyncProcesses, craftComputedStates, craftException, craftFactoryEntries, craftInject, craftInputs, craftMutations, craftQuery, craftQueryParam, craftQueryParams, craftSetAllQueriesParamsStandalone, craftSources, craftState, createMethodHandlers, fromEventToSource$, injectService, insertForm, insertFormAttributes, insertFormSubmit, insertLocalStoragePersister, insertNoopTypingAnchor, insertPaginationPlaceholderData, insertReactOnMutation, insertSelect, insertSelectFormTree, isCraftException, isSource, linkedSource, localStoragePersister, map, mapOne, mutation, on$, partialContext, query, queryParam, reactiveWritableSignal, removeAll, removeMany, removeOne, resourceById, serializeQueryParams, serializedQueryParamsObjectToString, setAll, setMany, setOne, signalSource, source$, sourceFromEvent, stackedSource, state, toInject, toSource, toggleMany, toggleOne, updateMany, updateOne, upsertMany, upsertOne, validatedFormValueSymbol };
9268
+ type ArrayObjectDeepPath<State extends object> = ObjectDeepPath<State> extends infer Path ? Path extends string ? AccessTypeObjectPropertyByDottedPath<State, DottedPathPathToTuple<Path>> extends Array<any> ? Path : never : never : never;
9269
+ type DottedPathToCamel<Path extends string> = Path extends `${infer Head}.${infer Tail}` ? `${Head}${Capitalize<DottedPathToCamel<Tail>>}` : Path;
9270
+ type EntitiesUtilsToMap<EntityHelperFns, Entity, K, HasStateIdentifier, StateIdentifier, HasPath, Path, Acc = {}> = EntityHelperFns extends [infer First, ...infer Rest] ? First extends (data: infer Payload) => infer R ? R extends EntitiesUtilBrand<infer Name> ? EntitiesUtilsToMap<Rest, Entity, K, HasStateIdentifier, StateIdentifier, HasPath, Path, Acc & {
9271
+ [key in Name as `${HasPath extends true ? `${DottedPathToCamel<Path & string>}${Capitalize<string & key>}` : key & string}`]: (payload: MergeObject$1<{
9272
+ [key in Exclude<keyof Payload, 'identifier'> as `${key extends 'entities' ? never : key & string}`]: key extends 'entity' ? Entity : key extends 'ids' ? K[] : key extends 'newEntities' ? Entity[] : `Not implemented mapping for ${key & string}`;
9273
+ }, HasStateIdentifier extends true ? {
9274
+ select: StateIdentifier extends (...args: any) => infer R ? R : never;
9275
+ } : {}>) => void;
9276
+ }> : 'No EntitiesBranded Name Detected' : false : Acc;
9277
+ /**
9278
+ * Creates an insertion that adds entity collection management methods to state, query, or queryParam primitives.
9279
+ *
9280
+ * Provides type-safe manipulation of arrays of entities with operations like add, remove, update, and upsert.
9281
+ * Supports nested properties via dot notation paths and custom entity identifiers.
9282
+ *
9283
+ * @template State - The state type (array or object containing arrays)
9284
+ * @template K - The type of entity identifiers (string or number)
9285
+ * @template PreviousInsertionsOutputs - Combined outputs from previous insertions
9286
+ * @template EntityHelperFns - Tuple type of entity utility functions to expose
9287
+ * @template StateIdentifier - Type of identifier function for parallel queries
9288
+ * @template Path - Dot-notation path to nested array (inferred from state structure)
9289
+ *
9290
+ * @param config - Configuration object
9291
+ * @param config.methods - Array of entity utility functions (addOne, removeOne, updateOne, etc.) to expose as methods
9292
+ * @param config.identifier - Optional custom function to extract unique ID from entities.
9293
+ * Defaults to `entity.id` for objects or `entity` for primitives
9294
+ * @param config.path - Optional dot-notation path to nested array property (e.g., 'catalog.products').
9295
+ * Method names are prefixed with camelCase path when provided
9296
+ *
9297
+ * @returns Insertion function that adds entity management methods to the primitive
9298
+ *
9299
+ * @example
9300
+ * // Basic usage with primitives
9301
+ * const tags = state(
9302
+ * [] as string[],
9303
+ * insertEntities({
9304
+ * methods: [addOne, addMany, removeOne],
9305
+ * })
9306
+ * );
9307
+ * tags.addOne({ entity: 'typescript' });
9308
+ * tags.addMany({ newEntities: ['angular', 'signals'] });
9309
+ *
9310
+ * @example
9311
+ * // With objects having default id property
9312
+ * interface Product {
9313
+ * id: string;
9314
+ * name: string;
9315
+ * price: number;
9316
+ * }
9317
+ * const products = state(
9318
+ * [] as Product[],
9319
+ * insertEntities({
9320
+ * methods: [addOne, setOne, removeOne],
9321
+ * })
9322
+ * );
9323
+ * products.addOne({ entity: { id: '1', name: 'Laptop', price: 999 } });
9324
+ *
9325
+ * @example
9326
+ * // With custom identifier
9327
+ * interface User {
9328
+ * uuid: string;
9329
+ * name: string;
9330
+ * }
9331
+ * const users = state(
9332
+ * [] as User[],
9333
+ * insertEntities({
9334
+ * methods: [setOne, removeOne],
9335
+ * identifier: (user) => user.uuid,
9336
+ * })
9337
+ * );
9338
+ *
9339
+ * @example
9340
+ * // With nested path
9341
+ * interface Catalog {
9342
+ * total: number;
9343
+ * products: Array<{ id: string; name: string }>;
9344
+ * }
9345
+ * const catalog = state(
9346
+ * { total: 0, products: [] } as Catalog,
9347
+ * insertEntities({
9348
+ * methods: [addMany, removeOne],
9349
+ * path: 'products',
9350
+ * })
9351
+ * );
9352
+ * catalog.productsAddMany({ newEntities: [{ id: '1', name: 'Item' }] });
9353
+ *
9354
+ * @example
9355
+ * // With parallel queries
9356
+ * const userQuery = query(
9357
+ * {
9358
+ * params: () => 'userId',
9359
+ * identifier: (params) => params,
9360
+ * loader: async ({ params }) => fetchUserPosts(params),
9361
+ * },
9362
+ * insertEntities({
9363
+ * methods: [addOne],
9364
+ * })
9365
+ * );
9366
+ * userQuery.addOne({
9367
+ * select: 'user-123', // Target specific query instance
9368
+ * entity: { id: 'post-1', title: 'New Post' },
9369
+ * });
9370
+ *
9371
+ * @see {@link https://github.com/ng-angular-stack/ng-craft/blob/main/apps/docs/insertions/insert-entities.md | insertEntities Documentation}
9372
+ */
9373
+ declare function insertEntities<State, K extends string | number, PreviousInsertionsOutputs, const EntityHelperFns extends unknown[], StateIdentifier, const Path = State extends Array<infer Entity> ? never : State extends object ? ArrayObjectDeepPath<State> : never, StateType = State extends Array<infer R> ? R : State extends object ? Path extends string ? AccessTypeObjectPropertyByDottedPath<State, DottedPathPathToTuple<Path>> extends Array<infer Entity> ? Entity : never : never : never, HasStateIdentifier = [unknown] extends [StateIdentifier] ? false : StateIdentifier extends (...args: any) => infer R ? [unknown] extends [R] ? false : true : false, IsEntityIdentifierOptional = StateType extends {
9374
+ id: NoInfer<K>;
9375
+ } ? true : StateType extends string | number ? true : false>(config: {
9376
+ methods: EntityHelperFns;
9377
+ } & MergeObject$1<IsEntityIdentifierOptional extends true ? {
9378
+ identifier?: IdSelector<NoInfer<StateType>, NoInfer<K>>;
9379
+ } : {
9380
+ identifier: IdSelector<NoInfer<StateType>, NoInfer<K>>;
9381
+ }, [
9382
+ Path
9383
+ ] extends [never] ? {} : {
9384
+ path: Path;
9385
+ }>): (context: InsertionStateFactoryContext<State, PreviousInsertionsOutputs> & {
9386
+ identifier?: StateIdentifier;
9387
+ }) => Prettify<EntitiesUtilsToMap<EntityHelperFns, StateType, K, HasStateIdentifier, StateIdentifier, "path" extends keyof typeof config ? true : false, Path>> & {
9388
+ testState: State;
9389
+ testPath: Path;
9390
+ testHasPath: "path" extends keyof typeof config ? true : false;
9391
+ };
9392
+
9393
+ export { CRAFT_EXCEPTION_SYMBOL, EXTERNALLY_PROVIDED, EmptyContext, GlobalPersisterHandlerService, STORE_CONFIG_TOKEN, SourceBrand, SourceBranded, VALIDATOR_OUTPUT_SYMBOL, addMany, addOne, afterRecomputation, asyncProcess, cAsyncValidate, cAsyncValidator, cEmail, cMax, cMaxLength, cMin, cMinLength, cPattern, cRequired, cValidate, cValidator, capitalize, computedIds, computedSource, computedTotal, contract, craft, craftAsyncProcesses, craftComputedStates, craftException, craftFactoryEntries, craftInject, craftInputs, craftMutations, craftQuery, craftQueryParam, craftQueryParams, craftSetAllQueriesParamsStandalone, craftSources, craftState, createMethodHandlers, fromEventToSource$, injectService, insertEntities, insertForm, insertFormAttributes, insertFormSubmit, insertLocalStoragePersister, insertNoopTypingAnchor, insertPaginationPlaceholderData, insertReactOnMutation, insertSelect, insertSelectFormTree, isCraftException, isSource, linkedSource, localStoragePersister, map, mapOne, mutation, on$, partialContext, query, queryParam, reactiveWritableSignal, removeAll, removeMany, removeOne, resourceById, serializeQueryParams, serializedQueryParamsObjectToString, setAll, setMany, setOne, signalSource, source$, sourceFromEvent, stackedSource, state, toInject, toSource, toggleMany, toggleOne, updateMany, updateOne, upsertMany, upsertOne, validatedFormValueSymbol };
9269
9394
  export type { AnyCraftException, AsyncProcessExceptionConstraints, AsyncProcessOutput, AsyncProcessRef, CEmailException, CMaxException, CMaxLengthException, CMinException, CMinLengthException, CRequiredException, CloudProxy, CloudProxySource, ContextConstraints, ContextInput, CraftException, CraftExceptionMeta, CraftExceptionResult, CraftFactory, CraftFactoryEntries, CraftFactoryUtility, DeferredExtract, EntitiesUtilBrand, EqualParams, ExcludeByCode, ExcludeCommonKeys, ExposedStateInsertions, ExtractCodeFromCraftResultUnion, ExtractCraftException, ExtractSignalPropsAndMethods, FilterMethodsBoundToSources, FilterPrivateFields, FilterSource, FlatRecord, FormNodeExceptions, FormWithInsertions, FromEventToSource$, HasChild$1 as HasChild, HasKeys, IdSelector, Identifier, InferInjectedType, InjectService2InsertionContext, InjectService2InsertionFactory, InjectService2Insertions, InjectService2Output, InjectService2Public, InsertFormAttributesConfig, InsertFormAttributesContext, InsertMetaInCraftExceptionIfExists, InsertionFormFactoryContext, InsertionsFormFactory, IsAny, IsEmptyObject, IsNever, IsUnknown, MakeOptionalPropertiesRequired$1 as MakeOptionalPropertiesRequired, MergeObject$1 as MergeObject, MergeObjects$1 as MergeObjects, MergeTwoContexts, MutationOutput, MutationRef, Not, OmitStrict, PartialContext, Prettify, QueryOutput, QueryParamConfig, QueryParamExceptions, QueryParamNavigationOptions$1 as QueryParamNavigationOptions, QueryParamOutput, QueryParamsToState, QueryRef, ReadonlySource, ReadonlySource$, RemoveIndexSignature, ReplaceStoreConfigToken, ResourceByIdHandler, ResourceByIdLikeAsyncProcessExceptions, ResourceByIdLikeExceptions, ResourceByIdLikeMutationExceptions, ResourceByIdLikeMutationRef, ResourceByIdLikeQueryRef, ResourceByIdRef, ResourceLikeAsyncProcessExceptions, ResourceLikeExceptions, ResourceLikeMutationExceptions, ResourceLikeMutationRef, ResourceLikeQueryRef, SignalSource, Source$, SourceFromEvent, SourceSetterMethods, SourceSubscribe, SpecificCraftQueryParamOutputs, SpecificCraftQueryParamsOutputs, StackSource, StateOutput, StoreConfigConstraints, StoreConfigToken, StripCraftException, ToConnectableMethodFromInject, ToConnectableSourceFromInject, ToInjectBindings, UnionToIntersection$1 as UnionToIntersection, UnionToTuple$1 as UnionToTuple, Update, ValidatedFormValue, ValidatorBindingContext, ValidatorModel, ValidatorOutput, ValidatorPending, ValidatorSuccess, ValidatorType, ValidatorUtilBrand };