@craft-ng/core 0.1.1 → 0.1.2

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,92 +20,67 @@ 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;
26
+
31
27
  /**
32
- * Creates a source for event-driven communication with lazy emission semantics.
28
+ * Creates a derived readonly source that transforms source emissions through a callback function.
33
29
  *
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
30
+ * This function binds queries, mutations, and async methods to sources for automatic execution by:
31
+ * - Listening to source emissions and computing new values
32
+ * - Providing a readonly source suitable for method binding
33
+ * - Maintaining reactivity through Angular's effect system
34
+ * - Enabling source-based triggering patterns
40
35
  *
41
36
  * @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
37
+ * **Primary Use Case:**
38
+ * Bind queries/mutations/async methods to sources for automatic execution:
39
+ * ```ts
40
+ * method: afterRecomputation(mySource, (data) => data)
41
+ * ```
42
+ * This pattern makes queries/mutations execute automatically when the source emits.
53
43
  *
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
44
+ * **Execution Flow:**
45
+ * 1. Source emits a value via `source.set(value)`
46
+ * 2. afterRecomputation callback transforms the value
47
+ * 3. Resulting readonly source emits the transformed value
48
+ * 4. Bound query/mutation/async method executes with the new value
61
49
  *
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)
50
+ * **Difference from computedSource:**
51
+ * - `afterRecomputation`: Designed for binding to method parameters
52
+ * - `computedSource`: General-purpose source transformation
53
+ * - Both transform source values, but afterRecomputation is optimized for method binding
66
54
  *
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
55
+ * **Common Patterns:**
56
+ * - **Identity transformation**: `afterRecomputation(source, (x) => x)` - pass value through
57
+ * - **Field extraction**: `afterRecomputation(source, (data) => data.id)` - extract specific field
58
+ * - **Validation**: `afterRecomputation(source, (data) => validate(data))` - transform and validate
59
+ * - **Mapping**: `afterRecomputation(source, (data) => mapToDto(data))` - convert to different type
71
60
  *
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.
61
+ * @template State - The type of values produced by the callback
62
+ * @template SourceType - The type of values emitted by the origin source
90
63
  *
91
- * @template T - The type of values emitted by the source
64
+ * @param _source - The source to listen to.
65
+ * When this source emits, the callback is invoked.
92
66
  *
93
- * @param options - Optional configuration:
94
- * - `equal`: Custom equality function for change detection (prevents duplicate emissions)
95
- * - `debugName`: Name for debugging purposes
67
+ * @param callback - Function that transforms source values.
68
+ * Receives the emitted value and returns the transformed result.
96
69
  *
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
70
+ * @returns A readonly source that emits transformed values.
71
+ * Can be used as the `method` parameter in queries, mutations, and async methods.
101
72
  *
102
73
  * @example
103
- * Basic source for user actions
74
+ * Binding a query to a source for automatic execution
104
75
  * ```ts
105
76
  * const { injectCraft } = craft(
106
77
  * { name: '', providedIn: 'root' },
107
78
  * craftSources({
108
- * loadUser: source<string>(),
79
+ * userIdChange: source<string>(),
109
80
  * }),
110
- * craftQuery('user', ({ loadUser }) =>
81
+ * craftQuery('user', ({ userIdChange }) =>
111
82
  * query({
112
- * method: afterRecomputation(loadUser, (userId) => userId),
83
+ * method: afterRecomputation(userIdChange, (userId) => userId),
113
84
  * loader: async ({ params }) => {
114
85
  * const response = await fetch(`/api/users/${params}`);
115
86
  * return response.json();
@@ -121,27 +92,24 @@ type SignalSource<T> = Signal<T | undefined> & {
121
92
  * const store = injectCraft();
122
93
  *
123
94
  * // 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'
95
+ * store.setUserIdChange('user-123');
96
+ * // -> query loader executes with params 'user-123'
127
97
  *
128
- * store.setLoadUser('user-456');
129
- * // -> user query executes again with params 'user-456'
98
+ * store.setUserIdChange('user-456');
99
+ * // -> query loader executes again with params 'user-456'
130
100
  * ```
131
101
  *
132
102
  * @example
133
- * Source for form submission
103
+ * Binding a mutation to a source
134
104
  * ```ts
135
- * type FormData = { name: string; email: string };
136
- *
137
105
  * const { injectCraft } = craft(
138
106
  * { name: '', providedIn: 'root' },
139
107
  * craftSources({
140
- * submitForm: source<FormData>(),
108
+ * submitForm: source<{ name: string; email: string }>(),
141
109
  * }),
142
110
  * craftMutations(({ submitForm }) => ({
143
111
  * submit: mutation({
144
- * method: afterRecomputation(submitForm, (data) => data),
112
+ * method: afterRecomputation(submitForm, (formData) => formData),
145
113
  * loader: async ({ params }) => {
146
114
  * const response = await fetch('/api/submit', {
147
115
  * method: 'POST',
@@ -155,86 +123,60 @@ type SignalSource<T> = Signal<T | undefined> & {
155
123
  *
156
124
  * const store = injectCraft();
157
125
  *
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
- * }
126
+ * // Mutation executes automatically when source emits
127
+ * store.setSubmitForm({ name: 'John', email: 'john@example.com' });
128
+ * // -> mutation loader executes with form data
129
+ * // Note: No store.mutateSubmit method exposed (source-based)
170
130
  * ```
171
131
  *
172
132
  * @example
173
- * Source for reload/refresh actions
133
+ * Binding async method to a source
174
134
  * ```ts
175
135
  * const { injectCraft } = craft(
176
136
  * { name: '', providedIn: 'root' },
177
137
  * craftSources({
178
- * reload: source<void>(),
138
+ * searchInput: source<string>(),
179
139
  * }),
180
- * craftQuery('data', ({ reload }) =>
181
- * query(
182
- * {
183
- * params: () => ({}),
184
- * loader: async () => {
185
- * const response = await fetch('/api/data');
186
- * return response.json();
187
- * },
140
+ * craftAsyncProcesses(({ searchInput }) => ({
141
+ * search: asyncProcess({
142
+ * method: afterRecomputation(searchInput, (term) => term),
143
+ * loader: async ({ params }) => {
144
+ * // Debounce at source level before setting
145
+ * const response = await fetch(`/api/search?q=${params}`);
146
+ * return response.json();
188
147
  * },
189
- * insertReloadOnSource(reload)
190
- * )
191
- * )
148
+ * }),
149
+ * }))
192
150
  * );
193
151
  *
194
152
  * const store = injectCraft();
195
153
  *
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>
154
+ * // Async method executes automatically
155
+ * store.setSearchInput('query');
156
+ * // -> search loader executes
203
157
  * ```
204
158
  *
205
159
  * @example
206
- * Multiple sources for different actions
160
+ * Extracting specific field from complex data
207
161
  * ```ts
162
+ * type FormData = {
163
+ * user: { id: string; name: string };
164
+ * address: { city: string };
165
+ * };
166
+ *
208
167
  * const { injectCraft } = craft(
209
168
  * { name: '', providedIn: 'root' },
210
169
  * craftSources({
211
- * addTodo: source<{ text: string }>(),
212
- * deleteTodo: source<string>(),
213
- * toggleTodo: source<string>(),
170
+ * formSubmit: source<FormData>(),
214
171
  * }),
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),
172
+ * craftMutations(({ formSubmit }) => ({
173
+ * updateUser: mutation({
174
+ * // Extract only user data
175
+ * method: afterRecomputation(formSubmit, (data) => data.user),
235
176
  * loader: async ({ params }) => {
236
- * const response = await fetch(`/api/todos/${params}/toggle`, {
177
+ * const response = await fetch(`/api/users/${params.id}`, {
237
178
  * method: 'PATCH',
179
+ * body: JSON.stringify(params),
238
180
  * });
239
181
  * return response.json();
240
182
  * },
@@ -244,267 +186,136 @@ type SignalSource<T> = Signal<T | undefined> & {
244
186
  *
245
187
  * const store = injectCraft();
246
188
  *
247
- * // Different actions trigger different mutations
248
- * store.setAddTodo({ text: 'Buy milk' });
249
- * store.setToggleTodo('todo-123');
250
- * store.setDeleteTodo('todo-456');
189
+ * // Only user data is passed to mutation
190
+ * store.setFormSubmit({
191
+ * user: { id: 'user-1', name: 'John' },
192
+ * address: { city: 'NYC' },
193
+ * });
194
+ * // -> mutation receives only { id: 'user-1', name: 'John' }
251
195
  * ```
252
196
  *
253
197
  * @example
254
- * Late listener with preserveLastValue
198
+ * Transforming data before execution
255
199
  * ```ts
256
- * const mySource = source<string>();
257
- *
258
- * // Early listener
259
- * const listener1 = computed(() => mySource());
260
- * console.log(listener1()); // undefined
200
+ * const { injectCraft } = craft(
201
+ * { name: '', providedIn: 'root' },
202
+ * craftSources({
203
+ * searchParams: source<{ query: string; filters: string[] }>(),
204
+ * }),
205
+ * craftQuery('results', ({ searchParams }) =>
206
+ * query({
207
+ * method: afterRecomputation(searchParams, (params) => ({
208
+ * q: params.query.trim().toLowerCase(),
209
+ * f: params.filters.join(','),
210
+ * })),
211
+ * loader: async ({ params }) => {
212
+ * const query = new URLSearchParams(params);
213
+ * const response = await fetch(`/api/search?${query}`);
214
+ * return response.json();
215
+ * },
216
+ * })
217
+ * )
218
+ * );
261
219
  *
262
- * // Emit value
263
- * mySource.set('Hello');
264
- * console.log(listener1()); // 'Hello'
220
+ * const store = injectCraft();
265
221
  *
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
222
+ * // Data is transformed before query execution
223
+ * store.setSearchParams({
224
+ * query: ' Angular ',
225
+ * filters: ['tutorial', 'advanced'],
286
226
  * });
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
- * }
227
+ * // -> query receives { q: 'angular', f: 'tutorial,advanced' }
342
228
  * ```
343
229
  *
344
230
  * @example
345
- * Source with complex payload
231
+ * Validation and type narrowing
346
232
  * ```ts
347
- * type SearchParams = {
348
- * query: string;
349
- * filters: string[];
350
- * page: number;
351
- * };
352
- *
353
233
  * const { injectCraft } = craft(
354
234
  * { name: '', providedIn: 'root' },
355
235
  * craftSources({
356
- * search: source<SearchParams>(),
236
+ * inputChange: source<string>(),
357
237
  * }),
358
- * craftQuery('results', ({ search }) =>
359
- * query({
360
- * method: afterRecomputation(search, (params) => params),
238
+ * craftAsyncProcesses(({ inputChange }) => ({
239
+ * validate: asyncProcess({
240
+ * method: afterRecomputation(inputChange, (input) => {
241
+ * // Only proceed if input is valid
242
+ * const trimmed = input.trim();
243
+ * if (trimmed.length < 3) {
244
+ * throw new Error('Input too short');
245
+ * }
246
+ * return trimmed;
247
+ * }),
361
248
  * loader: async ({ params }) => {
362
- * const queryString = new URLSearchParams({
363
- * q: params.query,
364
- * filters: params.filters.join(','),
365
- * page: String(params.page),
249
+ * const response = await fetch('/api/validate', {
250
+ * method: 'POST',
251
+ * body: JSON.stringify({ input: params }),
366
252
  * });
367
- * const response = await fetch(`/api/search?${queryString}`);
368
253
  * return response.json();
369
254
  * },
370
- * })
371
- * )
255
+ * }),
256
+ * }))
372
257
  * );
373
258
  *
374
259
  * const store = injectCraft();
375
260
  *
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
-
389
- /**
390
- * Creates a derived readonly source that transforms source emissions through a callback function.
391
- *
392
- * This function binds queries, mutations, and async methods to sources for automatic execution by:
393
- * - Listening to source emissions and computing new values
394
- * - Providing a readonly source suitable for method binding
395
- * - Maintaining reactivity through Angular's effect system
396
- * - Enabling source-based triggering patterns
261
+ * // Invalid input throws error in callback
262
+ * store.setInputChange('ab'); // Error: Input too short
397
263
  *
398
- * @remarks
399
- * **Primary Use Case:**
400
- * Bind queries/mutations/async methods to sources for automatic execution:
401
- * ```ts
402
- * method: afterRecomputation(mySource, (data) => data)
264
+ * // Valid input proceeds
265
+ * store.setInputChange('valid input'); // Validation executes
403
266
  * ```
404
- * This pattern makes queries/mutations execute automatically when the source emits.
405
- *
406
- * **Execution Flow:**
407
- * 1. Source emits a value via `source.set(value)`
408
- * 2. afterRecomputation callback transforms the value
409
- * 3. Resulting readonly source emits the transformed value
410
- * 4. Bound query/mutation/async method executes with the new value
411
- *
412
- * **Difference from computedSource:**
413
- * - `afterRecomputation`: Designed for binding to method parameters
414
- * - `computedSource`: General-purpose source transformation
415
- * - Both transform source values, but afterRecomputation is optimized for method binding
416
- *
417
- * **Common Patterns:**
418
- * - **Identity transformation**: `afterRecomputation(source, (x) => x)` - pass value through
419
- * - **Field extraction**: `afterRecomputation(source, (data) => data.id)` - extract specific field
420
- * - **Validation**: `afterRecomputation(source, (data) => validate(data))` - transform and validate
421
- * - **Mapping**: `afterRecomputation(source, (data) => mapToDto(data))` - convert to different type
422
- *
423
- * @template State - The type of values produced by the callback
424
- * @template SourceType - The type of values emitted by the origin source
425
- *
426
- * @param _source - The source to listen to.
427
- * When this source emits, the callback is invoked.
428
- *
429
- * @param callback - Function that transforms source values.
430
- * Receives the emitted value and returns the transformed result.
431
- *
432
- * @returns A readonly source that emits transformed values.
433
- * Can be used as the `method` parameter in queries, mutations, and async methods.
434
267
  *
435
268
  * @example
436
- * Binding a query to a source for automatic execution
269
+ * Multiple sources with different transformations
437
270
  * ```ts
438
271
  * const { injectCraft } = craft(
439
272
  * { name: '', providedIn: 'root' },
440
273
  * craftSources({
441
- * userIdChange: source<string>(),
274
+ * quickSearch: source<string>(),
275
+ * advancedSearch: source<{ query: string; options: unknown }>(),
442
276
  * }),
443
- * craftQuery('user', ({ userIdChange }) =>
277
+ * craftQuery('searchResults', ({ quickSearch, advancedSearch }) =>
444
278
  * query({
445
- * method: afterRecomputation(userIdChange, (userId) => userId),
446
- * loader: async ({ params }) => {
447
- * const response = await fetch(`/api/users/${params}`);
448
- * return response.json();
449
- * },
450
- * })
451
- * )
452
- * );
453
- *
454
- * const store = injectCraft();
455
- *
456
- * // Query executes automatically when source emits
457
- * store.setUserIdChange('user-123');
458
- * // -> query loader executes with params 'user-123'
459
- *
460
- * store.setUserIdChange('user-456');
461
- * // -> query loader executes again with params 'user-456'
462
- * ```
463
- *
464
- * @example
465
- * Binding a mutation to a source
466
- * ```ts
467
- * const { injectCraft } = craft(
468
- * { name: '', providedIn: 'root' },
469
- * craftSources({
470
- * submitForm: source<{ name: string; email: string }>(),
471
- * }),
472
- * craftMutations(({ submitForm }) => ({
473
- * submit: mutation({
474
- * method: afterRecomputation(submitForm, (formData) => formData),
279
+ * method: afterRecomputation(
280
+ * // Can combine sources at higher level
281
+ * quickSearch, // For this example, using one source
282
+ * (term) => ({ query: term, mode: 'quick' })
283
+ * ),
475
284
  * loader: async ({ params }) => {
476
- * const response = await fetch('/api/submit', {
285
+ * const response = await fetch('/api/search', {
477
286
  * method: 'POST',
478
287
  * body: JSON.stringify(params),
479
288
  * });
480
289
  * return response.json();
481
290
  * },
482
- * }),
483
- * }))
291
+ * })
292
+ * )
484
293
  * );
485
294
  *
486
295
  * const store = injectCraft();
487
296
  *
488
- * // Mutation executes automatically when source emits
489
- * store.setSubmitForm({ name: 'John', email: 'john@example.com' });
490
- * // -> mutation loader executes with form data
491
- * // Note: No store.mutateSubmit method exposed (source-based)
297
+ * // Quick search with simple string
298
+ * store.setQuickSearch('angular');
299
+ * // -> query receives { query: 'angular', mode: 'quick' }
492
300
  * ```
493
301
  *
494
302
  * @example
495
- * Binding async method to a source
303
+ * Identity transformation (pass-through)
496
304
  * ```ts
497
305
  * const { injectCraft } = craft(
498
306
  * { name: '', providedIn: 'root' },
499
307
  * craftSources({
500
- * searchInput: source<string>(),
308
+ * dataUpdate: source<{ id: string; payload: unknown }>(),
501
309
  * }),
502
- * craftAsyncProcesses(({ searchInput }) => ({
503
- * search: asyncProcess({
504
- * method: afterRecomputation(searchInput, (term) => term),
310
+ * craftMutations(({ dataUpdate }) => ({
311
+ * update: mutation({
312
+ * // Pass data through unchanged
313
+ * method: afterRecomputation(dataUpdate, (data) => data),
505
314
  * loader: async ({ params }) => {
506
- * // Debounce at source level before setting
507
- * const response = await fetch(`/api/search?q=${params}`);
315
+ * const response = await fetch(`/api/data/${params.id}`, {
316
+ * method: 'PUT',
317
+ * body: JSON.stringify(params.payload),
318
+ * });
508
319
  * return response.json();
509
320
  * },
510
321
  * }),
@@ -513,185 +324,12 @@ declare function signalSource<T>(options?: {
513
324
  *
514
325
  * const store = injectCraft();
515
326
  *
516
- * // Async method executes automatically
517
- * store.setSearchInput('query');
518
- * // -> search loader executes
519
- * ```
520
- *
521
- * @example
522
- * Extracting specific field from complex data
523
- * ```ts
524
- * type FormData = {
525
- * user: { id: string; name: string };
526
- * address: { city: string };
527
- * };
528
- *
529
- * const { injectCraft } = craft(
530
- * { name: '', providedIn: 'root' },
531
- * craftSources({
532
- * formSubmit: source<FormData>(),
533
- * }),
534
- * craftMutations(({ formSubmit }) => ({
535
- * updateUser: mutation({
536
- * // Extract only user data
537
- * method: afterRecomputation(formSubmit, (data) => data.user),
538
- * loader: async ({ params }) => {
539
- * const response = await fetch(`/api/users/${params.id}`, {
540
- * method: 'PATCH',
541
- * body: JSON.stringify(params),
542
- * });
543
- * return response.json();
544
- * },
545
- * }),
546
- * }))
547
- * );
548
- *
549
- * const store = injectCraft();
550
- *
551
- * // Only user data is passed to mutation
552
- * store.setFormSubmit({
553
- * user: { id: 'user-1', name: 'John' },
554
- * address: { city: 'NYC' },
555
- * });
556
- * // -> mutation receives only { id: 'user-1', name: 'John' }
557
- * ```
558
- *
559
- * @example
560
- * Transforming data before execution
561
- * ```ts
562
- * const { injectCraft } = craft(
563
- * { name: '', providedIn: 'root' },
564
- * craftSources({
565
- * searchParams: source<{ query: string; filters: string[] }>(),
566
- * }),
567
- * craftQuery('results', ({ searchParams }) =>
568
- * query({
569
- * method: afterRecomputation(searchParams, (params) => ({
570
- * q: params.query.trim().toLowerCase(),
571
- * f: params.filters.join(','),
572
- * })),
573
- * loader: async ({ params }) => {
574
- * const query = new URLSearchParams(params);
575
- * const response = await fetch(`/api/search?${query}`);
576
- * return response.json();
577
- * },
578
- * })
579
- * )
580
- * );
581
- *
582
- * const store = injectCraft();
583
- *
584
- * // Data is transformed before query execution
585
- * store.setSearchParams({
586
- * query: ' Angular ',
587
- * filters: ['tutorial', 'advanced'],
588
- * });
589
- * // -> query receives { q: 'angular', f: 'tutorial,advanced' }
590
- * ```
591
- *
592
- * @example
593
- * Validation and type narrowing
594
- * ```ts
595
- * const { injectCraft } = craft(
596
- * { name: '', providedIn: 'root' },
597
- * craftSources({
598
- * inputChange: source<string>(),
599
- * }),
600
- * craftAsyncProcesses(({ inputChange }) => ({
601
- * validate: asyncProcess({
602
- * method: afterRecomputation(inputChange, (input) => {
603
- * // Only proceed if input is valid
604
- * const trimmed = input.trim();
605
- * if (trimmed.length < 3) {
606
- * throw new Error('Input too short');
607
- * }
608
- * return trimmed;
609
- * }),
610
- * loader: async ({ params }) => {
611
- * const response = await fetch('/api/validate', {
612
- * method: 'POST',
613
- * body: JSON.stringify({ input: params }),
614
- * });
615
- * return response.json();
616
- * },
617
- * }),
618
- * }))
619
- * );
620
- *
621
- * const store = injectCraft();
622
- *
623
- * // Invalid input throws error in callback
624
- * store.setInputChange('ab'); // Error: Input too short
625
- *
626
- * // Valid input proceeds
627
- * store.setInputChange('valid input'); // Validation executes
628
- * ```
629
- *
630
- * @example
631
- * Multiple sources with different transformations
632
- * ```ts
633
- * const { injectCraft } = craft(
634
- * { name: '', providedIn: 'root' },
635
- * craftSources({
636
- * quickSearch: source<string>(),
637
- * advancedSearch: source<{ query: string; options: unknown }>(),
638
- * }),
639
- * craftQuery('searchResults', ({ quickSearch, advancedSearch }) =>
640
- * query({
641
- * method: afterRecomputation(
642
- * // Can combine sources at higher level
643
- * quickSearch, // For this example, using one source
644
- * (term) => ({ query: term, mode: 'quick' })
645
- * ),
646
- * loader: async ({ params }) => {
647
- * const response = await fetch('/api/search', {
648
- * method: 'POST',
649
- * body: JSON.stringify(params),
650
- * });
651
- * return response.json();
652
- * },
653
- * })
654
- * )
655
- * );
656
- *
657
- * const store = injectCraft();
658
- *
659
- * // Quick search with simple string
660
- * store.setQuickSearch('angular');
661
- * // -> query receives { query: 'angular', mode: 'quick' }
662
- * ```
663
- *
664
- * @example
665
- * Identity transformation (pass-through)
666
- * ```ts
667
- * const { injectCraft } = craft(
668
- * { name: '', providedIn: 'root' },
669
- * craftSources({
670
- * dataUpdate: source<{ id: string; payload: unknown }>(),
671
- * }),
672
- * craftMutations(({ dataUpdate }) => ({
673
- * update: mutation({
674
- * // Pass data through unchanged
675
- * method: afterRecomputation(dataUpdate, (data) => data),
676
- * loader: async ({ params }) => {
677
- * const response = await fetch(`/api/data/${params.id}`, {
678
- * method: 'PUT',
679
- * body: JSON.stringify(params.payload),
680
- * });
681
- * return response.json();
682
- * },
683
- * }),
684
- * }))
685
- * );
686
- *
687
- * const store = injectCraft();
688
- *
689
- * // Data passed through unchanged
690
- * store.setDataUpdate({ id: 'item-1', payload: { value: 123 } });
691
- * // -> mutation receives exact same object
327
+ * // Data passed through unchanged
328
+ * store.setDataUpdate({ id: 'item-1', payload: { value: 123 } });
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>;