@ng-forge/dynamic-forms 0.9.0 → 0.10.0-next.1

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.
Files changed (21) hide show
  1. package/fesm2022/{ng-forge-dynamic-forms-array-field.component-Tw49K5kz.mjs → ng-forge-dynamic-forms-array-field.component-GiaVHO9N.mjs} +3 -3
  2. package/fesm2022/{ng-forge-dynamic-forms-array-field.component-Tw49K5kz.mjs.map → ng-forge-dynamic-forms-array-field.component-GiaVHO9N.mjs.map} +1 -1
  3. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-CrOIZEL_.mjs → ng-forge-dynamic-forms-container-field.component-C554hOxN.mjs} +3 -3
  4. package/fesm2022/{ng-forge-dynamic-forms-container-field.component-CrOIZEL_.mjs.map → ng-forge-dynamic-forms-container-field.component-C554hOxN.mjs.map} +1 -1
  5. package/fesm2022/{ng-forge-dynamic-forms-container-utils-Bx1A5tg-.mjs → ng-forge-dynamic-forms-container-utils-BS4d0cCk.mjs} +2 -2
  6. package/fesm2022/{ng-forge-dynamic-forms-container-utils-Bx1A5tg-.mjs.map → ng-forge-dynamic-forms-container-utils-BS4d0cCk.mjs.map} +1 -1
  7. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-Q2g56KUM.mjs → ng-forge-dynamic-forms-css-wrapper.component-CnJgFWLB.mjs} +2 -2
  8. package/fesm2022/{ng-forge-dynamic-forms-css-wrapper.component-Q2g56KUM.mjs.map → ng-forge-dynamic-forms-css-wrapper.component-CnJgFWLB.mjs.map} +1 -1
  9. package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-9l5gPNQc.mjs → ng-forge-dynamic-forms-emit-initialization-3PyXVwsn.mjs} +2 -2
  10. package/fesm2022/{ng-forge-dynamic-forms-emit-initialization-9l5gPNQc.mjs.map → ng-forge-dynamic-forms-emit-initialization-3PyXVwsn.mjs.map} +1 -1
  11. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-DqnhddRH.mjs → ng-forge-dynamic-forms-group-field.component-BJ8j7Sh0.mjs} +3 -3
  12. package/fesm2022/{ng-forge-dynamic-forms-group-field.component-DqnhddRH.mjs.map → ng-forge-dynamic-forms-group-field.component-BJ8j7Sh0.mjs.map} +1 -1
  13. package/fesm2022/{ng-forge-dynamic-forms-ng-forge-dynamic-forms-Be1Lk0qR.mjs → ng-forge-dynamic-forms-ng-forge-dynamic-forms--PuL0AKc.mjs} +795 -193
  14. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms--PuL0AKc.mjs.map +1 -0
  15. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs → ng-forge-dynamic-forms-page-field.component-Bz2RpleR.mjs} +3 -3
  16. package/fesm2022/{ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs.map → ng-forge-dynamic-forms-page-field.component-Bz2RpleR.mjs.map} +1 -1
  17. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-C14mlROS.mjs → ng-forge-dynamic-forms-text-field.component-BGw6T-4h.mjs} +2 -2
  18. package/fesm2022/{ng-forge-dynamic-forms-text-field.component-C14mlROS.mjs.map → ng-forge-dynamic-forms-text-field.component-BGw6T-4h.mjs.map} +1 -1
  19. package/fesm2022/ng-forge-dynamic-forms.mjs +1 -1
  20. package/package.json +2 -2
  21. package/fesm2022/ng-forge-dynamic-forms-ng-forge-dynamic-forms-Be1Lk0qR.mjs.map +0 -1
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
2
2
  import { InjectionToken, inject, reflectComponentType, Injector, EnvironmentInjector, DestroyRef, computed, signal, input, ViewContainerRef, Directive, Injectable, isSignal, untracked, linkedSignal, ChangeDetectionStrategy, Component, resourceFromSnapshots, runInInjectionContext, afterNextRender, isWritableSignal, TemplateRef, model, contentChildren, makeEnvironmentProviders, Pipe } from '@angular/core';
3
3
  import { explicitEffect } from 'ngxtension/explicit-effect';
4
4
  import { takeUntilDestroyed, toObservable, toSignal, rxResource, outputFromObservable } from '@angular/core/rxjs-interop';
5
- import { of, forkJoin, from, catchError, map, switchMap, isObservable, firstValueFrom, exhaustMap, EMPTY, Subject, filter, TimeoutError, throwError, defer, combineLatest, pipe, debounceTime, distinctUntilChanged, tap, startWith, scan as scan$1, Observable, concatMap, take as take$1, pairwise, takeUntil, combineLatestWith, auditTime, scheduled, queueScheduler, merge, timer } from 'rxjs';
5
+ import { of, forkJoin, from, catchError, map, switchMap, isObservable, firstValueFrom, exhaustMap, EMPTY, Subject, filter, TimeoutError, throwError, defer, combineLatest, pipe, debounceTime, distinctUntilChanged, tap, startWith, scan as scan$1, Observable, concatMap, take as take$1, merge, pairwise, combineLatestWith, auditTime, scheduled, queueScheduler, timer } from 'rxjs';
6
6
  import { submit, email, min, max, minLength, maxLength, pattern, required, validate, validateAsync, validateHttp, disabled, readonly, hidden, applyEach, applyWhenValue, applyWhen, apply, schema, validateStandardSchema, validateTree, form, provideSignalFormsConfig } from '@angular/forms/signals';
7
7
  import { scan, map as map$1, filter as filter$1, take, switchMap as switchMap$1, timeout, catchError as catchError$1, shareReplay } from 'rxjs/operators';
8
8
  import { HttpClient } from '@angular/common/http';
@@ -2010,10 +2010,16 @@ var TokenType;
2010
2010
  // Punctuation
2011
2011
  TokenType["DOT"] = "DOT";
2012
2012
  TokenType["COMMA"] = "COMMA";
2013
+ TokenType["COLON"] = "COLON";
2013
2014
  TokenType["LPAREN"] = "LPAREN";
2014
2015
  TokenType["RPAREN"] = "RPAREN";
2015
2016
  TokenType["LBRACKET"] = "LBRACKET";
2016
2017
  TokenType["RBRACKET"] = "RBRACKET";
2018
+ TokenType["LBRACE"] = "LBRACE";
2019
+ TokenType["RBRACE"] = "RBRACE";
2020
+ TokenType["ARROW"] = "ARROW";
2021
+ TokenType["QUESTION"] = "QUESTION";
2022
+ TokenType["OPTIONAL_DOT"] = "OPTIONAL_DOT";
2017
2023
  // Special
2018
2024
  TokenType["EOF"] = "EOF";
2019
2025
  })(TokenType || (TokenType = {}));
@@ -2193,6 +2199,12 @@ class Tokenizer {
2193
2199
  case '||':
2194
2200
  this.position += 2;
2195
2201
  return { type: TokenType.OR, value: '||', position: start };
2202
+ case '=>':
2203
+ this.position += 2;
2204
+ return { type: TokenType.ARROW, value: '=>', position: start };
2205
+ case '?.':
2206
+ this.position += 2;
2207
+ return { type: TokenType.OPTIONAL_DOT, value: '?.', position: start };
2196
2208
  }
2197
2209
  // Single-character operators and punctuation
2198
2210
  this.position++;
@@ -2217,6 +2229,8 @@ class Tokenizer {
2217
2229
  return { type: TokenType.DOT, value: '.', position: start };
2218
2230
  case ',':
2219
2231
  return { type: TokenType.COMMA, value: ',', position: start };
2232
+ case ':':
2233
+ return { type: TokenType.COLON, value: ':', position: start };
2220
2234
  case '(':
2221
2235
  return { type: TokenType.LPAREN, value: '(', position: start };
2222
2236
  case ')':
@@ -2225,6 +2239,12 @@ class Tokenizer {
2225
2239
  return { type: TokenType.LBRACKET, value: '[', position: start };
2226
2240
  case ']':
2227
2241
  return { type: TokenType.RBRACKET, value: ']', position: start };
2242
+ case '{':
2243
+ return { type: TokenType.LBRACE, value: '{', position: start };
2244
+ case '}':
2245
+ return { type: TokenType.RBRACE, value: '}', position: start };
2246
+ case '?':
2247
+ return { type: TokenType.QUESTION, value: '?', position: start };
2228
2248
  default:
2229
2249
  throw new ExpressionParserError(`Unexpected character: ${char}`, start, this.expression);
2230
2250
  }
@@ -2260,7 +2280,88 @@ class Parser {
2260
2280
  return result;
2261
2281
  }
2262
2282
  parseExpression() {
2263
- return this.parseLogicalOr();
2283
+ // Try arrow function first — it has the lowest precedence and would otherwise
2284
+ // be ambiguous with grouped expressions / bare identifiers.
2285
+ const arrow = this.tryParseArrowFunction();
2286
+ if (arrow)
2287
+ return arrow;
2288
+ return this.parseConditional();
2289
+ }
2290
+ /**
2291
+ * Ternary `test ? consequent : alternate` — right-associative, sits between
2292
+ * the arrow-function check and logical-or in precedence.
2293
+ */
2294
+ parseConditional() {
2295
+ const test = this.parseLogicalOr();
2296
+ if (!this.match(TokenType.QUESTION))
2297
+ return test;
2298
+ const consequent = this.parseExpression();
2299
+ this.consume(TokenType.COLON, 'Expected ":" between ternary branches');
2300
+ const alternate = this.parseExpression();
2301
+ return { type: 'Conditional', test, consequent, alternate };
2302
+ }
2303
+ /**
2304
+ * Attempts to parse an arrow function. Returns null and leaves position
2305
+ * unchanged if the next tokens don't form an arrow function head.
2306
+ *
2307
+ * Supported shapes:
2308
+ * x => body
2309
+ * () => body
2310
+ * (x) => body
2311
+ * (x, y, ...) => body
2312
+ *
2313
+ * Body returning an object literal must be wrapped in parens: `d => ({...})`.
2314
+ */
2315
+ tryParseArrowFunction() {
2316
+ const savedPos = this.current;
2317
+ // Single unparenthesized param: IDENTIFIER =>
2318
+ if (this.check(TokenType.IDENTIFIER) && this.checkAt(this.current + 1, TokenType.ARROW)) {
2319
+ const param = this.advance().value;
2320
+ this.advance(); // consume =>
2321
+ const body = this.parseExpression();
2322
+ return { type: 'ArrowFunction', params: [param], body };
2323
+ }
2324
+ // Parenthesized params: ( ) => or ( id (, id)* ) =>
2325
+ if (this.check(TokenType.LPAREN)) {
2326
+ this.advance(); // tentatively consume (
2327
+ const params = [];
2328
+ if (this.check(TokenType.RPAREN)) {
2329
+ this.advance(); // )
2330
+ }
2331
+ else if (this.check(TokenType.IDENTIFIER)) {
2332
+ params.push(this.advance().value);
2333
+ while (this.check(TokenType.COMMA)) {
2334
+ this.advance(); // ,
2335
+ if (!this.check(TokenType.IDENTIFIER)) {
2336
+ this.current = savedPos;
2337
+ return null;
2338
+ }
2339
+ params.push(this.advance().value);
2340
+ }
2341
+ if (!this.check(TokenType.RPAREN)) {
2342
+ this.current = savedPos;
2343
+ return null;
2344
+ }
2345
+ this.advance(); // )
2346
+ }
2347
+ else {
2348
+ this.current = savedPos;
2349
+ return null;
2350
+ }
2351
+ if (!this.check(TokenType.ARROW)) {
2352
+ this.current = savedPos;
2353
+ return null;
2354
+ }
2355
+ this.advance(); // =>
2356
+ const body = this.parseExpression();
2357
+ return { type: 'ArrowFunction', params, body };
2358
+ }
2359
+ return null;
2360
+ }
2361
+ checkAt(index, type) {
2362
+ if (index < 0 || index >= this.tokens.length)
2363
+ return false;
2364
+ return this.tokens[index].type === type;
2264
2365
  }
2265
2366
  parseLogicalOr() {
2266
2367
  let node = this.parseLogicalAnd();
@@ -2335,6 +2436,32 @@ class Parser {
2335
2436
  const property = this.advance().value;
2336
2437
  node = { type: 'MemberAccess', object: node, property };
2337
2438
  }
2439
+ else if (this.match(TokenType.OPTIONAL_DOT)) {
2440
+ // Optional chain — supports `?.prop`, `?.[expr]`, and `?.(args)`.
2441
+ if (this.match(TokenType.LBRACKET)) {
2442
+ const property = this.parseExpression();
2443
+ this.consume(TokenType.RBRACKET, 'Expected "]" after computed property');
2444
+ node = { type: 'ComputedMemberAccess', object: node, property, optional: true };
2445
+ }
2446
+ else if (this.match(TokenType.LPAREN)) {
2447
+ const args = this.parseArgumentList();
2448
+ this.consume(TokenType.RPAREN, 'Expected ")" after arguments');
2449
+ node = { type: 'CallExpression', callee: node, arguments: args, optional: true };
2450
+ }
2451
+ else if (this.check(TokenType.IDENTIFIER)) {
2452
+ const property = this.advance().value;
2453
+ node = { type: 'MemberAccess', object: node, property, optional: true };
2454
+ }
2455
+ else {
2456
+ throw new ExpressionParserError('Expected property name, "[", or "(" after "?."', this.peek().position, this.expression);
2457
+ }
2458
+ }
2459
+ else if (this.match(TokenType.LBRACKET)) {
2460
+ // Computed member access: obj[expr]
2461
+ const property = this.parseExpression();
2462
+ this.consume(TokenType.RBRACKET, 'Expected "]" after computed property');
2463
+ node = { type: 'ComputedMemberAccess', object: node, property };
2464
+ }
2338
2465
  else if (this.match(TokenType.LPAREN)) {
2339
2466
  // Function call: func()
2340
2467
  const args = this.parseArgumentList();
@@ -2373,6 +2500,12 @@ class Parser {
2373
2500
  this.consume(TokenType.RBRACKET, 'Expected "]" after array elements');
2374
2501
  return { type: 'ArrayLiteral', elements };
2375
2502
  }
2503
+ // Object literal
2504
+ if (this.match(TokenType.LBRACE)) {
2505
+ const properties = this.parseObjectProperties();
2506
+ this.consume(TokenType.RBRACE, 'Expected "}" after object properties');
2507
+ return { type: 'ObjectLiteral', properties };
2508
+ }
2376
2509
  // Identifier
2377
2510
  if (this.match(TokenType.IDENTIFIER)) {
2378
2511
  return { type: 'Identifier', name: this.previous().value };
@@ -2403,6 +2536,28 @@ class Parser {
2403
2536
  }
2404
2537
  return elements;
2405
2538
  }
2539
+ parseObjectProperties() {
2540
+ const properties = [];
2541
+ if (this.check(TokenType.RBRACE)) {
2542
+ return properties;
2543
+ }
2544
+ do {
2545
+ // Trailing comma support: `{ a: 1, }`
2546
+ if (this.check(TokenType.RBRACE))
2547
+ break;
2548
+ let key;
2549
+ if (this.check(TokenType.IDENTIFIER) || this.check(TokenType.STRING)) {
2550
+ key = this.advance().value;
2551
+ }
2552
+ else {
2553
+ throw new ExpressionParserError(`Expected property name (identifier or string) in object literal, got '${this.peek().value}'`, this.peek().position, this.expression);
2554
+ }
2555
+ this.consume(TokenType.COLON, 'Expected ":" after property name in object literal');
2556
+ const value = this.parseExpression();
2557
+ properties.push({ key, value });
2558
+ } while (this.match(TokenType.COMMA));
2559
+ return properties;
2560
+ }
2406
2561
  match(...types) {
2407
2562
  for (const type of types) {
2408
2563
  if (this.check(type)) {
@@ -2577,14 +2732,22 @@ class Evaluator {
2577
2732
  return this.evaluateIdentifier(node.name);
2578
2733
  case 'MemberAccess':
2579
2734
  return this.evaluateMemberAccess(node);
2735
+ case 'ComputedMemberAccess':
2736
+ return this.evaluateComputedMemberAccess(node);
2580
2737
  case 'BinaryOp':
2581
2738
  return this.evaluateBinaryOp(node);
2582
2739
  case 'UnaryOp':
2583
2740
  return this.evaluateUnaryOp(node);
2584
2741
  case 'CallExpression':
2585
2742
  return this.evaluateCallExpression(node);
2743
+ case 'Conditional':
2744
+ return this.evaluate(node.test) ? this.evaluate(node.consequent) : this.evaluate(node.alternate);
2586
2745
  case 'ArrayLiteral':
2587
2746
  return this.evaluateArrayLiteral(node);
2747
+ case 'ObjectLiteral':
2748
+ return this.evaluateObjectLiteral(node);
2749
+ case 'ArrowFunction':
2750
+ return this.evaluateArrowFunction(node);
2588
2751
  default:
2589
2752
  throw new ExpressionParserError(`Unknown node type: ${node.type}`, 0, this.expression);
2590
2753
  }
@@ -2610,6 +2773,27 @@ class Evaluator {
2610
2773
  }
2611
2774
  return undefined;
2612
2775
  }
2776
+ evaluateComputedMemberAccess(node) {
2777
+ const obj = this.evaluate(node.object);
2778
+ if (obj === null || obj === undefined) {
2779
+ // Both `obj?.[expr]` and `obj[expr]` short-circuit on null/undefined —
2780
+ // matches the safe-access behavior of dotted member access above.
2781
+ return undefined;
2782
+ }
2783
+ const rawKey = this.evaluate(node.property);
2784
+ if (rawKey === null || rawKey === undefined)
2785
+ return undefined;
2786
+ const key = String(rawKey);
2787
+ // Same blocklist as dotted access — `obj['__proto__']` must be rejected
2788
+ // even though the property name was computed.
2789
+ if (BLOCKED_PROPERTIES.has(key)) {
2790
+ throw new ExpressionParserError(`Property "${key}" is not accessible for security reasons`, 0, this.expression);
2791
+ }
2792
+ if (typeof obj === 'object' || typeof obj === 'string' || typeof obj === 'number') {
2793
+ return obj[key];
2794
+ }
2795
+ return undefined;
2796
+ }
2613
2797
  evaluateBinaryOp(node) {
2614
2798
  // Handle short-circuit evaluation for logical operators
2615
2799
  if (node.operator === '&&') {
@@ -2674,11 +2858,21 @@ class Evaluator {
2674
2858
  }
2675
2859
  }
2676
2860
  evaluateCallExpression(node) {
2677
- // Only allow method calls (member access), not arbitrary function calls
2861
+ // Only allow method calls (member access). Computed access (`obj['method']()`)
2862
+ // is also rejected — the security model whitelists method names statically.
2678
2863
  if (node.callee.type !== 'MemberAccess') {
2679
2864
  throw new ExpressionParserError('Only method calls are allowed, not arbitrary function calls', 0, this.expression);
2680
2865
  }
2681
2866
  const obj = this.evaluate(node.callee.object);
2867
+ // Calling a method on a nullish receiver silently returns undefined —
2868
+ // consistent with how dotted member access (`obj.prop`) treats nullish
2869
+ // receivers above. This means `a?.b()`, `a?.b.c()`, and `nullable.method()`
2870
+ // all short-circuit to undefined rather than tripping the whitelist check
2871
+ // with a misleading "method not allowed" error. The whitelist still
2872
+ // enforces strictly on non-nullish receivers.
2873
+ if (obj === null || obj === undefined) {
2874
+ return undefined;
2875
+ }
2682
2876
  const methodName = node.callee.property;
2683
2877
  // Block access to dangerous properties (must check before method call)
2684
2878
  if (BLOCKED_PROPERTIES.has(methodName)) {
@@ -2701,6 +2895,28 @@ class Evaluator {
2701
2895
  evaluateArrayLiteral(node) {
2702
2896
  return node.elements.map((element) => this.evaluate(element));
2703
2897
  }
2898
+ evaluateObjectLiteral(node) {
2899
+ const result = {};
2900
+ for (const { key, value } of node.properties) {
2901
+ if (BLOCKED_PROPERTIES.has(key)) {
2902
+ throw new ExpressionParserError(`Property "${key}" is not allowed in object literals for security reasons`, 0, this.expression);
2903
+ }
2904
+ result[key] = this.evaluate(value);
2905
+ }
2906
+ return result;
2907
+ }
2908
+ evaluateArrowFunction(node) {
2909
+ const parentScope = this.scope;
2910
+ const expression = this.expression;
2911
+ return (...args) => {
2912
+ const childScope = { ...parentScope };
2913
+ for (let i = 0; i < node.params.length; i++) {
2914
+ childScope[node.params[i]] = args[i];
2915
+ }
2916
+ const childEvaluator = new Evaluator(childScope, expression);
2917
+ return childEvaluator.evaluate(node.body);
2918
+ };
2919
+ }
2704
2920
  isMethodSafe(obj, methodName) {
2705
2921
  if (obj === null || obj === undefined) {
2706
2922
  return false;
@@ -4241,9 +4457,9 @@ class PageOrchestratorComponent {
4241
4457
  }
4242
4458
  }
4243
4459
  }
4244
- `, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs').then(m => m.default)]] });
4460
+ `, isInline: true, styles: [":host{display:block;position:relative;width:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [import('./ng-forge-dynamic-forms-page-field.component-Bz2RpleR.mjs').then(m => m.default)], () => [import('./ng-forge-dynamic-forms-page-field.component-Bz2RpleR.mjs').then(m => m.default)]] });
4245
4461
  }
4246
- i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.14", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
4462
+ i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.14", ngImport: i0, type: PageOrchestratorComponent, resolveDeferredDeps: () => [import('./ng-forge-dynamic-forms-page-field.component-Bz2RpleR.mjs').then(m => m.default)], resolveMetadata: PageFieldComponent => ({ decorators: [{
4247
4463
  type: Component,
4248
4464
  args: [{ selector: 'div[page-orchestrator]', imports: [PageFieldComponent], template: `
4249
4465
  @for (pageField of pageFields(); track pageField.key; let i = $index) {
@@ -13413,21 +13629,67 @@ function validateNoCycles(collection, logger) {
13413
13629
  }
13414
13630
 
13415
13631
  /**
13416
- * Creates a staleness guard for HTTP/async derivation streams.
13632
+ * Compares two entry lists as multisets of content signatures. Order-insensitive
13633
+ * — entries reordered with the same content compare equal, so topological-sort
13634
+ * reorderings triggered by unrelated entries don't churn downstream stream
13635
+ * lifecycles.
13636
+ *
13637
+ * Multiset (not set) semantics: two distinct entries with identical signatures
13638
+ * are counted separately, so `[A, B]` does NOT equal `[A, A]` even though both
13639
+ * have length 2 and both contain `A` as a signature.
13417
13640
  *
13418
- * - Call `invalidate()` when tearing down the current generation of streams.
13419
- * - Pipe HTTP requests through `takeUntil(guard$)` to automatically discard
13420
- * in-flight responses that arrive after the generation has been invalidated.
13641
+ * Used by the value-derivation and property-derivation orchestrators to gate
13642
+ * `distinctUntilChanged` over HTTP- and async-function-entry sets.
13643
+ *
13644
+ * @internal
13421
13645
  */
13422
- function createStreamGuard() {
13423
- const subject = new Subject();
13424
- return {
13425
- invalidate: () => subject.next(),
13426
- guard$: subject.asObservable().pipe(take$1(1)),
13427
- };
13646
+ function entrySetsEqual(prev, next, sig) {
13647
+ if (prev.length !== next.length)
13648
+ return false;
13649
+ const counts = new Map();
13650
+ for (const item of prev) {
13651
+ const key = sig(item);
13652
+ counts.set(key, (counts.get(key) ?? 0) + 1);
13653
+ }
13654
+ for (const item of next) {
13655
+ const key = sig(item);
13656
+ const remaining = counts.get(key);
13657
+ if (!remaining)
13658
+ return false;
13659
+ if (remaining === 1)
13660
+ counts.delete(key);
13661
+ else
13662
+ counts.set(key, remaining - 1);
13663
+ }
13664
+ return counts.size === 0;
13428
13665
  }
13429
- const LOG_PREFIX$1 = 'HTTP Derivation -';
13430
- const DEFAULT_HTTP_DEBOUNCE_MS = 300;
13666
+ /**
13667
+ * Declarative pipeline shared by both derivation orchestrators:
13668
+ *
13669
+ * 1. Project the collection down to a typed entry list.
13670
+ * 2. Skip rebuilds when the entry set hasn't changed (multiset comparison).
13671
+ * 3. `switchMap` cancels the previous merged inner streams and creates fresh
13672
+ * ones from the new entries; in-flight HTTP / async work is cancelled via
13673
+ * ordinary RxJS unsubscribe semantics.
13674
+ *
13675
+ * No mutable subscription bookkeeping; the caller pipes through
13676
+ * `takeUntilDestroyed` for unmount cleanup.
13677
+ *
13678
+ * @internal
13679
+ */
13680
+ function buildEntryStreamPipeline(config) {
13681
+ return config.collection$.pipe(map((collection) => config.selectEntries(collection)), distinctUntilChanged((prev, next) => entrySetsEqual(prev, next, config.entrySignature)), switchMap((entries) => {
13682
+ if (entries.length === 0)
13683
+ return EMPTY;
13684
+ const streams = entries.map((entry) => config.createStream(entry)).filter((stream) => stream !== null);
13685
+ if (streams.length === 0)
13686
+ return EMPTY;
13687
+ return merge(...streams);
13688
+ }));
13689
+ }
13690
+
13691
+ const LOG_PREFIX$3 = 'HTTP Derivation -';
13692
+ const DEFAULT_HTTP_DEBOUNCE_MS$1 = 300;
13431
13693
  /**
13432
13694
  * Creates an RxJS Observable stream that processes an HTTP derivation entry.
13433
13695
  *
@@ -13451,7 +13713,7 @@ const DEFAULT_HTTP_DEBOUNCE_MS = 300;
13451
13713
  function createHttpDerivationStream(entry, formValue$, context) {
13452
13714
  // Guard: no array support for HTTP derivations
13453
13715
  if (isArrayPlaceholderPath(entry.fieldKey)) {
13454
- context.logger.warn(`${LOG_PREFIX$1} HTTP derivation for array field '${entry.fieldKey}' is not supported. ` +
13716
+ context.logger.warn(`${LOG_PREFIX$3} HTTP derivation for array field '${entry.fieldKey}' is not supported. ` +
13455
13717
  `Array HTTP derivations will be supported in a future release.`);
13456
13718
  return EMPTY;
13457
13719
  }
@@ -13460,7 +13722,7 @@ function createHttpDerivationStream(entry, formValue$, context) {
13460
13722
  }
13461
13723
  // Capture after the guard — TS narrows this to `string` here
13462
13724
  const responseExpression = entry.responseExpression;
13463
- const debounceMs = entry.debounceMs ?? DEFAULT_HTTP_DEBOUNCE_MS;
13725
+ const debounceMs = entry.debounceMs ?? DEFAULT_HTTP_DEBOUNCE_MS$1;
13464
13726
  return formValue$.pipe(startWith(null), pairwise(), map(([previous, current]) => ({
13465
13727
  current: current,
13466
13728
  changedFields: getChangedKeys(previous, current),
@@ -13554,15 +13816,16 @@ function createHttpDerivationStream(entry, formValue$, context) {
13554
13816
  subscriber.complete();
13555
13817
  return;
13556
13818
  }
13557
- // Make the HTTP request. takeUntil(guard$) automatically discards responses
13558
- // that arrive after the config has changed and the guard has been invalidated.
13819
+ // Cancellation is handled declaratively by the orchestrator's outer
13820
+ // switchMap: when the entry set changes, the outer stream unsubscribes
13821
+ // from this inner observable, the cleanup callback below runs, and
13822
+ // the in-flight HTTP request is cancelled.
13559
13823
  const method = (resolvedRequest.method ?? 'GET').toUpperCase();
13560
13824
  const httpSub = context.httpClient
13561
13825
  .request(method, resolvedRequest.url, {
13562
13826
  body: resolvedRequest.body,
13563
13827
  headers: resolvedRequest.headers,
13564
13828
  })
13565
- .pipe(takeUntil(context.guard$))
13566
13829
  .subscribe({
13567
13830
  next: (response) => {
13568
13831
  try {
@@ -13597,13 +13860,13 @@ function createHttpDerivationStream(entry, formValue$, context) {
13597
13860
  }
13598
13861
  catch (error) {
13599
13862
  const message = error instanceof Error ? error.message : String(error);
13600
- context.logger.warn(`${LOG_PREFIX$1} Failed to process response for '${entry.fieldKey}': ${message}`);
13863
+ context.logger.warn(`${LOG_PREFIX$3} Failed to process response for '${entry.fieldKey}': ${message}`);
13601
13864
  subscriber.complete();
13602
13865
  }
13603
13866
  },
13604
13867
  error: (error) => {
13605
13868
  const message = error instanceof Error ? error.message : String(error);
13606
- context.logger.warn(`${LOG_PREFIX$1} HTTP request failed for '${entry.fieldKey}': ${message}`);
13869
+ context.logger.warn(`${LOG_PREFIX$3} HTTP request failed for '${entry.fieldKey}': ${message}`);
13607
13870
  subscriber.complete();
13608
13871
  },
13609
13872
  });
@@ -13617,13 +13880,13 @@ function createHttpDerivationStream(entry, formValue$, context) {
13617
13880
  }), catchError$1((error) => {
13618
13881
  // Safety net — should not normally be reached since inner errors are caught
13619
13882
  const message = error instanceof Error ? error.message : String(error);
13620
- context.logger.warn(`${LOG_PREFIX$1} Unexpected stream error for '${entry.fieldKey}': ${message}`);
13883
+ context.logger.warn(`${LOG_PREFIX$3} Unexpected stream error for '${entry.fieldKey}': ${message}`);
13621
13884
  return EMPTY;
13622
13885
  }));
13623
13886
  }
13624
13887
 
13625
- const LOG_PREFIX = 'Async Derivation -';
13626
- const DEFAULT_ASYNC_DEBOUNCE_MS = 300;
13888
+ const LOG_PREFIX$2 = 'Async Derivation -';
13889
+ const DEFAULT_ASYNC_DEBOUNCE_MS$1 = 300;
13627
13890
  /**
13628
13891
  * Creates an RxJS Observable stream that processes an async derivation entry.
13629
13892
  *
@@ -13642,22 +13905,22 @@ const DEFAULT_ASYNC_DEBOUNCE_MS = 300;
13642
13905
  function createAsyncDerivationStream(entry, formValue$, context) {
13643
13906
  // Guard: no array support for async derivations
13644
13907
  if (isArrayPlaceholderPath(entry.fieldKey)) {
13645
- context.logger.warn(`${LOG_PREFIX} Async derivation for array field '${entry.fieldKey}' is not supported. ` +
13908
+ context.logger.warn(`${LOG_PREFIX$2} Async derivation for array field '${entry.fieldKey}' is not supported. ` +
13646
13909
  `Array async derivations will be supported in a future release.`);
13647
13910
  return EMPTY;
13648
13911
  }
13649
13912
  if (!entry.asyncFunctionName && !entry.asyncFn) {
13650
- context.logger.warn(`${LOG_PREFIX} Derivation for '${entry.fieldKey}' has neither "asyncFunctionName" nor "asyncFn". ` +
13913
+ context.logger.warn(`${LOG_PREFIX$2} Derivation for '${entry.fieldKey}' has neither "asyncFunctionName" nor "asyncFn". ` +
13651
13914
  `One of the two is required for source: 'asyncFunction'.`);
13652
13915
  return EMPTY;
13653
13916
  }
13654
13917
  if (entry.asyncFn && entry.asyncFunctionName) {
13655
- context.logger.warn(`${LOG_PREFIX} Both "asyncFn" and "asyncFunctionName" are set on derivation for '${entry.fieldKey}'. ` +
13918
+ context.logger.warn(`${LOG_PREFIX$2} Both "asyncFn" and "asyncFunctionName" are set on derivation for '${entry.fieldKey}'. ` +
13656
13919
  `Inline "asyncFn" takes precedence; "asyncFunctionName" is ignored.`);
13657
13920
  }
13658
13921
  const inlineAsyncFn = entry.asyncFn;
13659
13922
  const asyncFunctionName = entry.asyncFunctionName;
13660
- const debounceMs = entry.debounceMs ?? DEFAULT_ASYNC_DEBOUNCE_MS;
13923
+ const debounceMs = entry.debounceMs ?? DEFAULT_ASYNC_DEBOUNCE_MS$1;
13661
13924
  // startWith(null) → pairwise() causes all fields to appear "changed" on the
13662
13925
  // first emission (null vs actual form value), so every dependsOn field matches.
13663
13926
  // This intentionally fires the async derivation on initial form load — same
@@ -13704,7 +13967,7 @@ function createAsyncDerivationStream(entry, formValue$, context) {
13704
13967
  // Inline `asyncFn` wins; otherwise look up the registered function.
13705
13968
  const asyncFn = inlineAsyncFn ?? (asyncFunctionName ? asyncDerivationFunctions?.[asyncFunctionName] : undefined);
13706
13969
  if (!asyncFn) {
13707
- context.logger.warn(`${LOG_PREFIX} Async derivation function '${asyncFunctionName}' not found for field '${entry.fieldKey}'. ` +
13970
+ context.logger.warn(`${LOG_PREFIX$2} Async derivation function '${asyncFunctionName}' not found for field '${entry.fieldKey}'. ` +
13708
13971
  `Register it in customFnConfig.asyncDerivations.`);
13709
13972
  subscriber.complete();
13710
13973
  return;
@@ -13772,13 +14035,13 @@ function createAsyncDerivationStream(entry, formValue$, context) {
13772
14035
  }
13773
14036
  catch (error) {
13774
14037
  const message = error instanceof Error ? error.message : String(error);
13775
- context.logger.warn(`${LOG_PREFIX} Failed to process result for '${entry.fieldKey}': ${message}`);
14038
+ context.logger.warn(`${LOG_PREFIX$2} Failed to process result for '${entry.fieldKey}': ${message}`);
13776
14039
  subscriber.complete();
13777
14040
  }
13778
14041
  },
13779
14042
  error: (error) => {
13780
14043
  const message = error instanceof Error ? error.message : String(error);
13781
- context.logger.warn(`${LOG_PREFIX} Async function failed for '${entry.fieldKey}': ${message}`);
14044
+ context.logger.warn(`${LOG_PREFIX$2} Async function failed for '${entry.fieldKey}': ${message}`);
13782
14045
  subscriber.complete();
13783
14046
  },
13784
14047
  complete: () => {
@@ -13795,7 +14058,7 @@ function createAsyncDerivationStream(entry, formValue$, context) {
13795
14058
  }), catchError$1((error) => {
13796
14059
  // Safety net — should not normally be reached since inner errors are caught
13797
14060
  const message = error instanceof Error ? error.message : String(error);
13798
- context.logger.warn(`${LOG_PREFIX} Unexpected stream error for '${entry.fieldKey}': ${message}`);
14061
+ context.logger.warn(`${LOG_PREFIX$2} Unexpected stream error for '${entry.fieldKey}': ${message}`);
13799
14062
  return EMPTY;
13800
14063
  }));
13801
14064
  }
@@ -13873,20 +14136,6 @@ class DerivationOrchestrator {
13873
14136
  functionRegistry = inject(FunctionRegistryService);
13874
14137
  formOptions = inject(FORM_OPTIONS);
13875
14138
  httpClient = inject(HttpClient, { optional: true });
13876
- /** Active HTTP derivation stream subscriptions */
13877
- httpSubscriptions = [];
13878
- /** Identity keys of current HTTP entries for smart teardown comparison */
13879
- lastHttpEntryKeys = null;
13880
- /** Active async function derivation stream subscriptions */
13881
- asyncFunctionSubscriptions = [];
13882
- /** Identity keys of current async entries for smart teardown comparison */
13883
- lastAsyncEntryKeys = null;
13884
- /**
13885
- * Guard for detecting stale HTTP responses. Call `invalidate()` on teardown to cancel
13886
- * in-flight requests from the previous generation. Each HTTP stream subscribes via
13887
- * `takeUntil(guard$)` — when `invalidate()` fires, all in-flight requests are discarded.
13888
- */
13889
- streamGuard = createStreamGuard();
13890
14139
  /**
13891
14140
  * Computed signal containing the collected and validated derivations.
13892
14141
  * Returns null if no derivations are defined.
@@ -14042,36 +14291,33 @@ class DerivationOrchestrator {
14042
14291
  }
14043
14292
  }
14044
14293
  /**
14045
- * Sets up reactive HTTP derivation streams that react to collection changes.
14294
+ * Builds a declarative stream that reacts to changes in the set of HTTP
14295
+ * derivation entries and emits per-entry streams merged together.
14046
14296
  *
14047
- * Subscribes to derivationCollection changes and creates per-entry HTTP streams.
14048
- * Uses smart teardown: only recreates streams when HTTP entries actually change.
14297
+ * - `distinctUntilChanged` over the set of entry signatures skips rebuilds
14298
+ * when the HTTP entries haven't actually changed. The comparison is
14299
+ * order-insensitive — `topologicalSort` in the collector can reorder
14300
+ * existing entries when unrelated entries are added/removed, and we
14301
+ * don't want to tear down healthy streams in that case.
14302
+ * - `switchMap` cancels the previous merged stream when entries change,
14303
+ * which propagates into each `createHttpDerivationStream` and cancels
14304
+ * any in-flight HTTP requests via ordinary RxJS unsubscribe semantics.
14305
+ *
14306
+ * No mutable subscription bookkeeping or stream guard — `takeUntilDestroyed`
14307
+ * on the outer subscription handles unmount.
14049
14308
  */
14050
14309
  setupHttpStreams() {
14051
- toObservable(this.derivationCollection, { injector: this.injector })
14052
- .pipe(takeUntilDestroyed(this.destroyRef))
14053
- .subscribe((collection) => {
14054
- const httpEntries = collection?.entries.filter((entry) => entry.http) ?? [];
14055
- if (httpEntries.length === 0) {
14056
- this.teardownHttpStreams();
14057
- this.lastHttpEntryKeys = null;
14058
- return;
14059
- }
14060
- // Smart teardown: compare entry identity keys to avoid redundant stream recreation
14061
- const newKeys = this.computeHttpEntryKeys(httpEntries);
14062
- if (this.lastHttpEntryKeys && this.setsEqual(this.lastHttpEntryKeys, newKeys)) {
14063
- return; // No change in HTTP entries — keep existing streams
14064
- }
14065
- // Validate HttpClient availability
14066
- if (!this.httpClient) {
14067
- this.logger.error('HTTP Derivation: HttpClient is not available. ' + 'Ensure provideHttpClient() is included in your application providers.');
14068
- return;
14069
- }
14070
- // Tear down previous streams and create new ones
14071
- this.teardownHttpStreams();
14072
- this.lastHttpEntryKeys = newKeys;
14073
- const formValue$ = toObservable(this.config.formValue, { injector: this.injector });
14074
- for (const entry of httpEntries) {
14310
+ const formValue$ = toObservable(this.config.formValue, { injector: this.injector });
14311
+ const collection$ = toObservable(this.derivationCollection, { injector: this.injector });
14312
+ buildEntryStreamPipeline({
14313
+ collection$,
14314
+ selectEntries: (collection) => collection?.entries.filter((entry) => entry.http) ?? [],
14315
+ entrySignature: httpEntrySignature$1,
14316
+ createStream: (entry) => {
14317
+ if (!this.httpClient) {
14318
+ this.logger.error('HTTP Derivation: HttpClient is not available. Ensure provideHttpClient() is included in your application providers.');
14319
+ return null;
14320
+ }
14075
14321
  const context = {
14076
14322
  formValue: this.config.formValue,
14077
14323
  form: this.config.form,
@@ -14081,119 +14327,77 @@ class DerivationOrchestrator {
14081
14327
  customFunctions: () => this.functionRegistry.getCustomFunctions(),
14082
14328
  externalData: () => resolveExternalData(this.config.externalData),
14083
14329
  warningTracker: this.warningTracker,
14084
- guard$: this.streamGuard.guard$,
14085
14330
  };
14086
- const stream = createHttpDerivationStream(entry, formValue$, context).pipe(takeUntilDestroyed(this.destroyRef));
14087
- this.httpSubscriptions.push(stream.subscribe({
14088
- error: (err) => this.logger.error(`HTTP Derivation: Stream error for '${entry.fieldKey}'`, err),
14089
- }));
14090
- }
14091
- });
14092
- }
14093
- /**
14094
- * Tears down all active HTTP derivation streams. Invalidating the stream guard
14095
- * cancels any in-flight HTTP responses from the old generation via `takeUntil`.
14096
- */
14097
- teardownHttpStreams() {
14098
- this.streamGuard.invalidate();
14099
- for (const sub of this.httpSubscriptions) {
14100
- sub.unsubscribe();
14101
- }
14102
- this.httpSubscriptions = [];
14103
- }
14104
- /**
14105
- * Computes a set of identity keys for HTTP entries.
14106
- * Used for smart teardown comparison.
14107
- */
14108
- computeHttpEntryKeys(entries) {
14109
- return new Set(entries.map((entry) => `${entry.fieldKey}:${JSON.stringify(entry.http, Object.keys(entry.http ?? {}).sort())}`));
14110
- }
14111
- /**
14112
- * Compares two sets for equality.
14113
- */
14114
- setsEqual(a, b) {
14115
- if (a.size !== b.size)
14116
- return false;
14117
- for (const item of a) {
14118
- if (!b.has(item))
14119
- return false;
14120
- }
14121
- return true;
14331
+ return createHttpDerivationStream(entry, formValue$, context);
14332
+ },
14333
+ })
14334
+ .pipe(catchError((err) => {
14335
+ this.logger.error('HTTP Derivation: outer stream error', err);
14336
+ return EMPTY;
14337
+ }), takeUntilDestroyed(this.destroyRef))
14338
+ .subscribe();
14122
14339
  }
14123
- /**
14124
- * Sets up reactive async function derivation streams that react to collection changes.
14125
- *
14126
- * Subscribes to derivationCollection changes and creates per-entry async streams.
14127
- * Uses smart teardown: only recreates streams when async entries actually change.
14128
- */
14129
14340
  setupAsyncFunctionStreams() {
14130
- toObservable(this.derivationCollection, { injector: this.injector })
14131
- .pipe(takeUntilDestroyed(this.destroyRef))
14132
- .subscribe((collection) => {
14133
- const asyncEntries = collection?.entries.filter((entry) => entry.asyncFunctionName || entry.asyncFn) ?? [];
14134
- if (asyncEntries.length === 0) {
14135
- this.teardownAsyncFunctionStreams();
14136
- this.lastAsyncEntryKeys = null;
14137
- return;
14138
- }
14139
- // Smart teardown: compare entry identity keys to avoid redundant stream recreation
14140
- const newKeys = this.computeAsyncEntryKeys(asyncEntries);
14141
- if (this.lastAsyncEntryKeys && this.setsEqual(this.lastAsyncEntryKeys, newKeys)) {
14142
- return; // No change in async entries — keep existing streams
14143
- }
14144
- // Tear down previous streams and create new ones
14145
- this.teardownAsyncFunctionStreams();
14146
- this.lastAsyncEntryKeys = newKeys;
14147
- const formValue$ = toObservable(this.config.formValue, { injector: this.injector });
14148
- for (const entry of asyncEntries) {
14149
- const context = {
14150
- formValue: this.config.formValue,
14151
- form: this.config.form,
14152
- logger: this.logger,
14153
- derivationLogger: this.config.derivationLogger,
14154
- customFunctions: () => this.functionRegistry.getCustomFunctions(),
14155
- asyncDerivationFunctions: () => this.functionRegistry.getAsyncDerivationFunctions(),
14156
- externalData: () => resolveExternalData(this.config.externalData),
14157
- warningTracker: this.warningTracker,
14158
- };
14159
- const stream = createAsyncDerivationStream(entry, formValue$, context).pipe(takeUntilDestroyed(this.destroyRef));
14160
- this.asyncFunctionSubscriptions.push(stream.subscribe({
14161
- error: (err) => this.logger.error(`Async Derivation: Stream error for '${entry.fieldKey}'`, err),
14162
- }));
14163
- }
14164
- });
14165
- }
14166
- /**
14167
- * Tears down all active async function derivation streams.
14168
- */
14169
- teardownAsyncFunctionStreams() {
14170
- for (const sub of this.asyncFunctionSubscriptions) {
14171
- sub.unsubscribe();
14172
- }
14173
- this.asyncFunctionSubscriptions = [];
14174
- }
14175
- /**
14176
- * Computes a set of identity keys for async entries.
14177
- * Used for smart teardown comparison.
14178
- */
14179
- computeAsyncEntryKeys(entries) {
14180
- // For inline `asyncFn` entries we tag the key with the field path (the real
14181
- // identity, since derivations are self-targeting) rather than the array
14182
- // index. Index-based tagging churned identities whenever topologicalSort
14183
- // reordered entries, causing unnecessary teardown + re-subscribe.
14184
- return new Set(entries.map((entry) => {
14185
- const config = {
14186
- asyncFunctionName: entry.asyncFunctionName,
14187
- asyncFnId: entry.asyncFn ? `inline:${entry.fieldKey}` : undefined,
14188
- dependsOn: entry.dependsOn,
14189
- debounceMs: entry.debounceMs,
14190
- stopOnUserOverride: entry.stopOnUserOverride,
14191
- reEngageOnDependencyChange: entry.reEngageOnDependencyChange,
14192
- };
14193
- return `${entry.fieldKey}:${JSON.stringify(config, Object.keys(config).sort())}`;
14194
- }));
14341
+ const formValue$ = toObservable(this.config.formValue, { injector: this.injector });
14342
+ const collection$ = toObservable(this.derivationCollection, { injector: this.injector });
14343
+ buildEntryStreamPipeline({
14344
+ collection$,
14345
+ selectEntries: (collection) => collection?.entries.filter((entry) => entry.asyncFunctionName || entry.asyncFn) ?? [],
14346
+ entrySignature: asyncEntrySignature$1,
14347
+ createStream: (entry) => createAsyncDerivationStream(entry, formValue$, {
14348
+ formValue: this.config.formValue,
14349
+ form: this.config.form,
14350
+ logger: this.logger,
14351
+ derivationLogger: this.config.derivationLogger,
14352
+ customFunctions: () => this.functionRegistry.getCustomFunctions(),
14353
+ asyncDerivationFunctions: () => this.functionRegistry.getAsyncDerivationFunctions(),
14354
+ externalData: () => resolveExternalData(this.config.externalData),
14355
+ warningTracker: this.warningTracker,
14356
+ }),
14357
+ })
14358
+ .pipe(catchError((err) => {
14359
+ this.logger.error('Async Derivation: outer stream error', err);
14360
+ return EMPTY;
14361
+ }), takeUntilDestroyed(this.destroyRef))
14362
+ .subscribe();
14195
14363
  }
14196
14364
  }
14365
+ /**
14366
+ * Identity signature for an HTTP derivation entry. Includes every field that
14367
+ * drives the inner stream's behavior — changing any of them must tear down
14368
+ * and rebuild the stream. Stable across topological reorderings because the
14369
+ * consumer ({@link entrySetsEqual}) compares as a multiset.
14370
+ */
14371
+ function httpEntrySignature$1(entry) {
14372
+ const config = {
14373
+ http: entry.http,
14374
+ responseExpression: entry.responseExpression,
14375
+ dependsOn: entry.dependsOn,
14376
+ debounceMs: entry.debounceMs,
14377
+ trigger: entry.trigger,
14378
+ condition: entry.condition,
14379
+ stopOnUserOverride: entry.stopOnUserOverride,
14380
+ reEngageOnDependencyChange: entry.reEngageOnDependencyChange,
14381
+ };
14382
+ return `${entry.fieldKey}:${JSON.stringify(config)}`;
14383
+ }
14384
+ function asyncEntrySignature$1(entry) {
14385
+ // For inline `asyncFn` entries we tag the signature with the field path (the
14386
+ // real identity, since derivations are self-targeting) rather than the array
14387
+ // index. Index-based tagging churned identities whenever topologicalSort
14388
+ // reordered entries, causing unnecessary teardown + re-subscribe.
14389
+ const config = {
14390
+ asyncFunctionName: entry.asyncFunctionName,
14391
+ asyncFnId: entry.asyncFn ? `inline:${entry.fieldKey}` : undefined,
14392
+ dependsOn: entry.dependsOn,
14393
+ debounceMs: entry.debounceMs,
14394
+ trigger: entry.trigger,
14395
+ condition: entry.condition,
14396
+ stopOnUserOverride: entry.stopOnUserOverride,
14397
+ reEngageOnDependencyChange: entry.reEngageOnDependencyChange,
14398
+ };
14399
+ return `${entry.fieldKey}:${JSON.stringify(config)}`;
14400
+ }
14197
14401
  /**
14198
14402
  * Creates a DerivationOrchestrator for a dynamic form.
14199
14403
  * Must be called within an injection context.
@@ -14501,6 +14705,27 @@ function withLoggerConfig(config = true) {
14501
14705
  return createFeature('logger', providers);
14502
14706
  }
14503
14707
 
14708
+ /**
14709
+ * Creates an empty property derivation collection.
14710
+ *
14711
+ * @returns Empty collection
14712
+ *
14713
+ * @internal
14714
+ */
14715
+ function createEmptyPropertyDerivationCollection() {
14716
+ return { entries: [] };
14717
+ }
14718
+ /**
14719
+ * Returns true when the entry's value source is async (HTTP or async function)
14720
+ * and therefore must be processed by a dedicated stream subscription rather
14721
+ * than the synchronous applicator pipeline.
14722
+ *
14723
+ * @internal
14724
+ */
14725
+ function isAsyncPropertyDerivationEntry(entry) {
14726
+ return Boolean(entry.http) || Boolean(entry.asyncFunctionName) || Boolean(entry.asyncFn);
14727
+ }
14728
+
14504
14729
  /** @internal */
14505
14730
  const ERROR_PREFIX = '[PropertyDerivation]';
14506
14731
  /**
@@ -14521,7 +14746,10 @@ function applyPropertyDerivations(collection, context, changedFields) {
14521
14746
  let appliedCount = 0;
14522
14747
  let skippedCount = 0;
14523
14748
  let errorCount = 0;
14524
- const entriesToProcess = changedFields ? getEntriesForChangedFields(collection.entries, changedFields) : collection.entries;
14749
+ const baseEntries = changedFields ? getEntriesForChangedFields(collection.entries, changedFields) : collection.entries;
14750
+ // Async entries (HTTP / asyncFn / asyncFunctionName) are processed by dedicated
14751
+ // stream subscriptions in the orchestrator, not by this synchronous applicator.
14752
+ const entriesToProcess = baseEntries.filter((entry) => !isAsyncPropertyDerivationEntry(entry));
14525
14753
  for (const entry of entriesToProcess) {
14526
14754
  try {
14527
14755
  const applied = tryApplyPropertyDerivation(entry, context);
@@ -14766,6 +14994,78 @@ function collectFromField(field, entries, context) {
14766
14994
  */
14767
14995
  function createPropertyDerivationEntryFromDerivation(fieldKey, config, context) {
14768
14996
  const effectiveFieldKey = buildPropertyOverrideKey(context.arrayPath, context.arrayPath ? PLACEHOLDER_INDEX : undefined, fieldKey, context.groupPath);
14997
+ // HTTP and async function property derivations require explicit, non-wildcard
14998
+ // dependencies — mirrors the same guards on value-derivation HTTP/async sources.
14999
+ //
15000
+ // Type-level discriminants require these fields, but JSON-loaded configs can
15001
+ // bypass that — so we defensively guard against missing arrays and missing
15002
+ // HTTP fields here to fail fast with `DynamicFormError` instead of throwing
15003
+ // an opaque `TypeError` later.
15004
+ //
15005
+ // `targetProperty` is captured up front and `untypedConfig` is used inside
15006
+ // the source-narrowed blocks because TypeScript narrows `config` to `never`
15007
+ // once we negate a discriminant-required field (e.g. `!config.http` when
15008
+ // `source: 'http'` requires `http`).
15009
+ const errorLocation = `'${effectiveFieldKey}.${config.targetProperty}'`;
15010
+ const untypedConfig = config;
15011
+ // Reject configs that mix value-source fields with an explicit async source.
15012
+ // TypeScript's discriminated union already enforces this XOR for inline
15013
+ // configs; the runtime check guards JSON-loaded configs (API/OpenAPI/MCP)
15014
+ // where the wrong combination would silently degrade — async entries are
15015
+ // routed to the stream pipeline while sync fields would be ignored.
15016
+ if (untypedConfig.source === 'http' || untypedConfig.source === 'asyncFunction') {
15017
+ const conflictingSyncSources = [];
15018
+ if (untypedConfig.value !== undefined)
15019
+ conflictingSyncSources.push('value');
15020
+ if (untypedConfig.expression !== undefined)
15021
+ conflictingSyncSources.push('expression');
15022
+ if (untypedConfig.functionName !== undefined)
15023
+ conflictingSyncSources.push('functionName');
15024
+ if (untypedConfig.fn !== undefined)
15025
+ conflictingSyncSources.push('fn');
15026
+ if (conflictingSyncSources.length > 0) {
15027
+ throw new DynamicFormError(`Property derivation for ${errorLocation} sets 'source: ${JSON.stringify(untypedConfig.source)}' alongside ` +
15028
+ `sync value source(s) [${conflictingSyncSources.join(', ')}]. These are mutually exclusive — remove either ` +
15029
+ `the sync fields or the async source.`);
15030
+ }
15031
+ }
15032
+ // Reject HTTP + asyncFunction-field combinations as well (TS already
15033
+ // enforces this; runtime guard for JSON-loaded configs).
15034
+ if (untypedConfig.source === 'http' && (untypedConfig.asyncFunctionName !== undefined || untypedConfig.asyncFn !== undefined)) {
15035
+ throw new DynamicFormError(`Property derivation for ${errorLocation} sets 'source: "http"' alongside an async function — these are mutually exclusive.`);
15036
+ }
15037
+ if (untypedConfig.source === 'asyncFunction' && (untypedConfig.http !== undefined || untypedConfig.responseExpression !== undefined)) {
15038
+ throw new DynamicFormError(`Property derivation for ${errorLocation} sets 'source: "asyncFunction"' alongside HTTP fields — these are mutually exclusive.`);
15039
+ }
15040
+ if (untypedConfig.source === 'http') {
15041
+ if (!untypedConfig.http) {
15042
+ throw new DynamicFormError(`HTTP property derivation for ${errorLocation} requires an 'http' config object.`);
15043
+ }
15044
+ if (typeof untypedConfig.responseExpression !== 'string' || untypedConfig.responseExpression.trim() === '') {
15045
+ throw new DynamicFormError(`HTTP property derivation for ${errorLocation} requires a non-empty 'responseExpression'.`);
15046
+ }
15047
+ if (!Array.isArray(untypedConfig.dependsOn) || untypedConfig.dependsOn.length === 0) {
15048
+ throw new DynamicFormError(`HTTP property derivation for ${errorLocation} requires explicit 'dependsOn'. ` +
15049
+ `Wildcard dependencies would trigger HTTP requests on every form change.`);
15050
+ }
15051
+ if (untypedConfig.dependsOn.includes('*')) {
15052
+ throw new DynamicFormError(`HTTP property derivation for ${errorLocation} cannot use wildcard ('*') in 'dependsOn'. ` +
15053
+ `Wildcards would trigger HTTP requests on every form change. Specify explicit field dependencies instead.`);
15054
+ }
15055
+ }
15056
+ if (untypedConfig.source === 'asyncFunction') {
15057
+ if (!untypedConfig.asyncFunctionName && !untypedConfig.asyncFn) {
15058
+ throw new DynamicFormError(`Async property derivation for ${errorLocation} requires either 'asyncFunctionName' or 'asyncFn'.`);
15059
+ }
15060
+ if (!Array.isArray(untypedConfig.dependsOn) || untypedConfig.dependsOn.length === 0) {
15061
+ throw new DynamicFormError(`Async property derivation for ${errorLocation} requires explicit 'dependsOn'. ` +
15062
+ `Wildcard dependencies would trigger async functions on every form change.`);
15063
+ }
15064
+ if (untypedConfig.dependsOn.includes('*')) {
15065
+ throw new DynamicFormError(`Async property derivation for ${errorLocation} cannot use wildcard ('*') in 'dependsOn'. ` +
15066
+ `Wildcards would trigger async functions on every form change. Specify explicit field dependencies instead.`);
15067
+ }
15068
+ }
14769
15069
  const dependsOn = extractDependenciesFromConfig(config);
14770
15070
  const condition = config.condition ?? true;
14771
15071
  const trigger = config.trigger ?? 'onChange';
@@ -14779,6 +15079,10 @@ function createPropertyDerivationEntryFromDerivation(fieldKey, config, context)
14779
15079
  expression: config.expression,
14780
15080
  functionName: config.functionName,
14781
15081
  fn: extractInlineFn(config),
15082
+ http: config.http,
15083
+ responseExpression: config.responseExpression,
15084
+ asyncFunctionName: config.asyncFunctionName,
15085
+ asyncFn: extractInlineAsyncFn(config),
14782
15086
  trigger,
14783
15087
  debounceMs,
14784
15088
  debugName: config.debugName,
@@ -14786,6 +15090,204 @@ function createPropertyDerivationEntryFromDerivation(fieldKey, config, context)
14786
15090
  };
14787
15091
  }
14788
15092
 
15093
+ const LOG_PREFIX$1 = 'HTTP Property Derivation -';
15094
+ const DEFAULT_HTTP_DEBOUNCE_MS = 300;
15095
+ /**
15096
+ * Creates an RxJS Observable stream that processes an HTTP property-derivation entry.
15097
+ *
15098
+ * Mirrors `createHttpDerivationStream` (value pipeline) but writes the result via
15099
+ * `store.setOverride(fieldKey, targetProperty, value)` instead of patching the form.
15100
+ *
15101
+ * The stream uses `startWith(null) → pairwise()` so the first emission fires a
15102
+ * request for all `dependsOn` fields, populating the derived property on initial
15103
+ * form load. Subsequent emissions only fire when a `dependsOn` field changes.
15104
+ *
15105
+ * @internal
15106
+ */
15107
+ function createHttpPropertyDerivationStream(entry, formValue$, context) {
15108
+ // Guard: no array support yet for HTTP property derivations (mirrors value pipeline).
15109
+ if (isArrayPlaceholderPath(entry.fieldKey)) {
15110
+ context.logger.warn(`${LOG_PREFIX$1} HTTP property derivation for array field '${entry.fieldKey}' is not supported. ` +
15111
+ `Array HTTP property derivations will be supported in a future release.`);
15112
+ return EMPTY;
15113
+ }
15114
+ if (!entry.http || !entry.responseExpression) {
15115
+ return EMPTY;
15116
+ }
15117
+ const responseExpression = entry.responseExpression;
15118
+ const debounceMs = entry.debounceMs ?? DEFAULT_HTTP_DEBOUNCE_MS;
15119
+ return formValue$.pipe(startWith(null), pairwise(), map(([previous, current]) => ({
15120
+ current: current,
15121
+ changedFields: getChangedKeys(previous, current),
15122
+ })), filter(({ changedFields }) => {
15123
+ if (changedFields.size === 0)
15124
+ return false;
15125
+ return entry.dependsOn.some((dep) => changedFields.has(dep));
15126
+ }), debounceTime(debounceMs), switchMap(({ current }) => {
15127
+ return new Observable((subscriber) => {
15128
+ const customFunctions = context.customFunctions?.();
15129
+ const externalData = context.externalData?.();
15130
+ const fieldValue = getNestedValue(current, entry.fieldKey);
15131
+ const evalContext = {
15132
+ fieldValue,
15133
+ formValue: current,
15134
+ fieldPath: entry.fieldKey,
15135
+ customFunctions,
15136
+ externalData,
15137
+ logger: context.logger,
15138
+ };
15139
+ // Evaluate condition — skip if false
15140
+ if (entry.condition !== true) {
15141
+ const conditionMet = typeof entry.condition === 'boolean' ? entry.condition : evaluateCondition(entry.condition, evalContext);
15142
+ if (!conditionMet) {
15143
+ subscriber.complete();
15144
+ return;
15145
+ }
15146
+ }
15147
+ // Returns null when a path param is undefined — suppress the request
15148
+ const resolvedRequest = resolveHttpRequest(entry.http, evalContext);
15149
+ if (!resolvedRequest) {
15150
+ subscriber.complete();
15151
+ return;
15152
+ }
15153
+ const method = (resolvedRequest.method ?? 'GET').toUpperCase();
15154
+ // Inner cancellation is handled declaratively by the orchestrator's
15155
+ // outer switchMap: when the entry set changes, the outer stream
15156
+ // unsubscribes from this inner observable, which triggers the
15157
+ // cleanup callback below and cancels the in-flight HTTP request.
15158
+ const httpSub = context.httpClient
15159
+ .request(method, resolvedRequest.url, {
15160
+ body: resolvedRequest.body,
15161
+ headers: resolvedRequest.headers,
15162
+ })
15163
+ .subscribe({
15164
+ next: (response) => {
15165
+ try {
15166
+ const newValue = ExpressionParser.evaluate(responseExpression, { response });
15167
+ // `setOverride` performs its own deep-equality check, so we don't need to
15168
+ // compare with the current store value here.
15169
+ untracked(() => context.store.setOverride(entry.fieldKey, entry.targetProperty, newValue));
15170
+ subscriber.complete();
15171
+ }
15172
+ catch (error) {
15173
+ const message = error instanceof Error ? error.message : String(error);
15174
+ context.logger.warn(`${LOG_PREFIX$1} Failed to process response for '${entry.fieldKey}.${entry.targetProperty}': ${message}`);
15175
+ subscriber.complete();
15176
+ }
15177
+ },
15178
+ error: (error) => {
15179
+ const message = error instanceof Error ? error.message : String(error);
15180
+ context.logger.warn(`${LOG_PREFIX$1} HTTP request failed for '${entry.fieldKey}.${entry.targetProperty}': ${message}`);
15181
+ subscriber.complete();
15182
+ },
15183
+ });
15184
+ return () => {
15185
+ httpSub.unsubscribe();
15186
+ };
15187
+ });
15188
+ }), catchError$1((error) => {
15189
+ const message = error instanceof Error ? error.message : String(error);
15190
+ context.logger.warn(`${LOG_PREFIX$1} Unexpected stream error for '${entry.fieldKey}.${entry.targetProperty}': ${message}`);
15191
+ return EMPTY;
15192
+ }));
15193
+ }
15194
+
15195
+ const LOG_PREFIX = 'Async Property Derivation -';
15196
+ const DEFAULT_ASYNC_DEBOUNCE_MS = 300;
15197
+ /**
15198
+ * Creates an RxJS Observable stream that processes an async-function property-derivation entry.
15199
+ *
15200
+ * Mirrors `createAsyncDerivationStream` (value pipeline) but writes the result via
15201
+ * `store.setOverride(fieldKey, targetProperty, value)` instead of patching the form.
15202
+ *
15203
+ * @internal
15204
+ */
15205
+ function createAsyncPropertyDerivationStream(entry, formValue$, context) {
15206
+ if (isArrayPlaceholderPath(entry.fieldKey)) {
15207
+ context.logger.warn(`${LOG_PREFIX} Async property derivation for array field '${entry.fieldKey}' is not supported. ` +
15208
+ `Array async property derivations will be supported in a future release.`);
15209
+ return EMPTY;
15210
+ }
15211
+ if (!entry.asyncFunctionName && !entry.asyncFn) {
15212
+ context.logger.warn(`${LOG_PREFIX} Property derivation for '${entry.fieldKey}.${entry.targetProperty}' has neither "asyncFunctionName" nor "asyncFn". ` +
15213
+ `One of the two is required for source: 'asyncFunction'.`);
15214
+ return EMPTY;
15215
+ }
15216
+ if (entry.asyncFn && entry.asyncFunctionName) {
15217
+ context.logger.warn(`${LOG_PREFIX} Both "asyncFn" and "asyncFunctionName" are set on property derivation for '${entry.fieldKey}.${entry.targetProperty}'. ` +
15218
+ `Inline "asyncFn" takes precedence; "asyncFunctionName" is ignored.`);
15219
+ }
15220
+ const inlineAsyncFn = entry.asyncFn;
15221
+ const asyncFunctionName = entry.asyncFunctionName;
15222
+ const debounceMs = entry.debounceMs ?? DEFAULT_ASYNC_DEBOUNCE_MS;
15223
+ return formValue$.pipe(startWith(null), pairwise(), map(([previous, current]) => ({
15224
+ current: current,
15225
+ changedFields: getChangedKeys(previous, current),
15226
+ })), filter(({ changedFields }) => {
15227
+ if (changedFields.size === 0)
15228
+ return false;
15229
+ return entry.dependsOn.some((dep) => changedFields.has(dep));
15230
+ }), debounceTime(debounceMs), switchMap(({ current }) => {
15231
+ return new Observable((subscriber) => {
15232
+ const customFunctions = context.customFunctions?.();
15233
+ const asyncDerivationFunctions = context.asyncDerivationFunctions?.();
15234
+ const externalData = context.externalData?.();
15235
+ const asyncFn = inlineAsyncFn ?? (asyncFunctionName ? asyncDerivationFunctions?.[asyncFunctionName] : undefined);
15236
+ if (!asyncFn) {
15237
+ context.logger.warn(`${LOG_PREFIX} Async derivation function '${asyncFunctionName}' not found for '${entry.fieldKey}.${entry.targetProperty}'. ` +
15238
+ `Register it in customFnConfig.asyncDerivations.`);
15239
+ subscriber.complete();
15240
+ return;
15241
+ }
15242
+ const fieldValue = getNestedValue(current, entry.fieldKey);
15243
+ const evalContext = {
15244
+ fieldValue,
15245
+ formValue: current,
15246
+ fieldPath: entry.fieldKey,
15247
+ customFunctions,
15248
+ externalData,
15249
+ logger: context.logger,
15250
+ };
15251
+ if (entry.condition !== true) {
15252
+ const conditionMet = typeof entry.condition === 'boolean' ? entry.condition : evaluateCondition(entry.condition, evalContext);
15253
+ if (!conditionMet) {
15254
+ subscriber.complete();
15255
+ return;
15256
+ }
15257
+ }
15258
+ const result$ = from(asyncFn(evalContext));
15259
+ const asyncSub = result$.subscribe({
15260
+ next: (newValue) => {
15261
+ try {
15262
+ untracked(() => context.store.setOverride(entry.fieldKey, entry.targetProperty, newValue));
15263
+ subscriber.complete();
15264
+ }
15265
+ catch (error) {
15266
+ const message = error instanceof Error ? error.message : String(error);
15267
+ context.logger.warn(`${LOG_PREFIX} Failed to process result for '${entry.fieldKey}.${entry.targetProperty}': ${message}`);
15268
+ subscriber.complete();
15269
+ }
15270
+ },
15271
+ error: (error) => {
15272
+ const message = error instanceof Error ? error.message : String(error);
15273
+ context.logger.warn(`${LOG_PREFIX} Async function failed for '${entry.fieldKey}.${entry.targetProperty}': ${message}`);
15274
+ subscriber.complete();
15275
+ },
15276
+ complete: () => {
15277
+ subscriber.complete();
15278
+ },
15279
+ });
15280
+ return () => {
15281
+ asyncSub.unsubscribe();
15282
+ };
15283
+ });
15284
+ }), catchError$1((error) => {
15285
+ const message = error instanceof Error ? error.message : String(error);
15286
+ context.logger.warn(`${LOG_PREFIX} Unexpected stream error for '${entry.fieldKey}.${entry.targetProperty}': ${message}`);
15287
+ return EMPTY;
15288
+ }));
15289
+ }
15290
+
14789
15291
  /**
14790
15292
  * Orchestrates property derivation processing for a dynamic form.
14791
15293
  *
@@ -14805,6 +15307,7 @@ class PropertyDerivationOrchestrator {
14805
15307
  deprecationTracker = inject(DEPRECATION_WARNING_TRACKER);
14806
15308
  functionRegistry = inject(FunctionRegistryService);
14807
15309
  formOptions = inject(FORM_OPTIONS);
15310
+ httpClient = inject(HttpClient, { optional: true });
14808
15311
  /**
14809
15312
  * Computed signal containing the collected property derivations.
14810
15313
  * Returns null if no property derivations are defined.
@@ -14840,6 +15343,8 @@ class PropertyDerivationOrchestrator {
14840
15343
  });
14841
15344
  this.setupOnChangeStream();
14842
15345
  this.setupDebouncedStream();
15346
+ this.setupHttpStreams();
15347
+ this.setupAsyncFunctionStreams();
14843
15348
  }
14844
15349
  setupOnChangeStream() {
14845
15350
  const collection$ = toObservable(this.propertyDerivationCollection, { injector: this.injector });
@@ -14909,6 +15414,103 @@ class PropertyDerivationOrchestrator {
14909
15414
  };
14910
15415
  applyPropertyDerivationsForTrigger(filteredCollection, 'debounced', applicatorContext, changedFields);
14911
15416
  }
15417
+ /**
15418
+ * Declarative pipeline for HTTP property-derivation entries. Delegates the
15419
+ * collection-watching / dedup / cancellation plumbing to
15420
+ * {@link buildEntryStreamPipeline} so both orchestrators share one
15421
+ * implementation.
15422
+ *
15423
+ * Errors inside `createStream` invocations are isolated via the inner
15424
+ * stream's own `catchError`; we additionally wrap the outer pipeline so a
15425
+ * synchronous throw during stream construction (e.g., evaluation of a
15426
+ * `condition`) doesn't tear the whole pipeline down for the lifetime of
15427
+ * the form.
15428
+ */
15429
+ setupHttpStreams() {
15430
+ const formValue$ = toObservable(this.config.formValue, { injector: this.injector });
15431
+ const collection$ = toObservable(this.propertyDerivationCollection, { injector: this.injector });
15432
+ buildEntryStreamPipeline({
15433
+ collection$,
15434
+ selectEntries: (collection) => collection?.entries.filter((entry) => entry.http) ?? [],
15435
+ entrySignature: httpEntrySignature,
15436
+ createStream: (entry) => {
15437
+ if (!this.httpClient) {
15438
+ this.logger.error('HTTP Property Derivation: HttpClient is not available. Ensure provideHttpClient() is included in your application providers.');
15439
+ return null;
15440
+ }
15441
+ return createHttpPropertyDerivationStream(entry, formValue$, {
15442
+ formValue: this.config.formValue,
15443
+ store: this.config.store,
15444
+ httpClient: this.httpClient,
15445
+ logger: this.logger,
15446
+ customFunctions: () => this.functionRegistry.getCustomFunctions(),
15447
+ externalData: () => resolveExternalData(this.config.externalData),
15448
+ });
15449
+ },
15450
+ })
15451
+ .pipe(catchError((err) => {
15452
+ this.logger.error('HTTP Property Derivation: outer stream error', err);
15453
+ return EMPTY;
15454
+ }), takeUntilDestroyed(this.destroyRef))
15455
+ .subscribe();
15456
+ }
15457
+ setupAsyncFunctionStreams() {
15458
+ const formValue$ = toObservable(this.config.formValue, { injector: this.injector });
15459
+ const collection$ = toObservable(this.propertyDerivationCollection, { injector: this.injector });
15460
+ buildEntryStreamPipeline({
15461
+ collection$,
15462
+ selectEntries: (collection) => collection?.entries.filter((entry) => entry.asyncFunctionName || entry.asyncFn) ?? [],
15463
+ entrySignature: asyncEntrySignature,
15464
+ createStream: (entry) => createAsyncPropertyDerivationStream(entry, formValue$, {
15465
+ formValue: this.config.formValue,
15466
+ store: this.config.store,
15467
+ logger: this.logger,
15468
+ customFunctions: () => this.functionRegistry.getCustomFunctions(),
15469
+ asyncDerivationFunctions: () => this.functionRegistry.getAsyncDerivationFunctions(),
15470
+ externalData: () => resolveExternalData(this.config.externalData),
15471
+ }),
15472
+ })
15473
+ .pipe(catchError((err) => {
15474
+ this.logger.error('Async Property Derivation: outer stream error', err);
15475
+ return EMPTY;
15476
+ }), takeUntilDestroyed(this.destroyRef))
15477
+ .subscribe();
15478
+ }
15479
+ }
15480
+ /**
15481
+ * Identity signature for an HTTP property-derivation entry. Includes every
15482
+ * field that drives the inner stream's behavior — changing any of them must
15483
+ * tear down and rebuild the stream. Stable across topological reorderings
15484
+ * because the consumer ({@link entrySetsEqual}) compares as a multiset.
15485
+ */
15486
+ function httpEntrySignature(entry) {
15487
+ const config = {
15488
+ http: entry.http,
15489
+ responseExpression: entry.responseExpression,
15490
+ dependsOn: entry.dependsOn,
15491
+ debounceMs: entry.debounceMs,
15492
+ trigger: entry.trigger,
15493
+ condition: entry.condition,
15494
+ };
15495
+ return `${entry.fieldKey}.${entry.targetProperty}:${JSON.stringify(config)}`;
15496
+ }
15497
+ /**
15498
+ * Identity signature for an async-function property-derivation entry. Same
15499
+ * principle as {@link httpEntrySignature}: include every field the inner
15500
+ * stream closes over so changes force a rebuild. Inline `asyncFn`s use the
15501
+ * `fieldKey.targetProperty` path as a stable identifier — function identity
15502
+ * isn't JSON-serializable.
15503
+ */
15504
+ function asyncEntrySignature(entry) {
15505
+ const config = {
15506
+ asyncFunctionName: entry.asyncFunctionName,
15507
+ asyncFnId: entry.asyncFn ? `inline:${entry.fieldKey}.${entry.targetProperty}` : undefined,
15508
+ dependsOn: entry.dependsOn,
15509
+ debounceMs: entry.debounceMs,
15510
+ trigger: entry.trigger,
15511
+ condition: entry.condition,
15512
+ };
15513
+ return `${entry.fieldKey}.${entry.targetProperty}:${JSON.stringify(config)}`;
14912
15514
  }
14913
15515
  /**
14914
15516
  * Creates a PropertyDerivationOrchestrator for a dynamic form.
@@ -16326,35 +16928,35 @@ const BUILT_IN_FIELDS = [
16326
16928
  // `row` is a virtual field type: it maps to ContainerFieldComponent, and
16327
16929
  // the rowFieldMapper injects a `{ type: 'row' }` wrapper so the container
16328
16930
  // renders the flex/grid layout via RowWrapperComponent.
16329
- loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-CrOIZEL_.mjs'),
16931
+ loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-C554hOxN.mjs'),
16330
16932
  mapper: rowFieldMapper,
16331
16933
  valueHandling: 'flatten',
16332
16934
  ...CONTAINER_FIELD_TYPES_BASE,
16333
16935
  },
16334
16936
  {
16335
16937
  name: 'group',
16336
- loadComponent: () => import('./ng-forge-dynamic-forms-group-field.component-DqnhddRH.mjs'),
16938
+ loadComponent: () => import('./ng-forge-dynamic-forms-group-field.component-BJ8j7Sh0.mjs'),
16337
16939
  mapper: groupFieldMapper,
16338
16940
  valueHandling: 'include',
16339
16941
  ...CONTAINER_FIELD_TYPES_BASE,
16340
16942
  },
16341
16943
  {
16342
16944
  name: 'array',
16343
- loadComponent: () => import('./ng-forge-dynamic-forms-array-field.component-Tw49K5kz.mjs'),
16945
+ loadComponent: () => import('./ng-forge-dynamic-forms-array-field.component-GiaVHO9N.mjs'),
16344
16946
  mapper: arrayFieldMapper,
16345
16947
  valueHandling: 'include',
16346
16948
  ...CONTAINER_FIELD_TYPES_BASE,
16347
16949
  },
16348
16950
  {
16349
16951
  name: 'page',
16350
- loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-n4KTMOTm.mjs'),
16952
+ loadComponent: () => import('./ng-forge-dynamic-forms-page-field.component-Bz2RpleR.mjs'),
16351
16953
  mapper: pageFieldMapper,
16352
16954
  valueHandling: 'flatten',
16353
16955
  ...CONTAINER_FIELD_TYPES_BASE,
16354
16956
  },
16355
16957
  {
16356
16958
  name: 'text',
16357
- loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-C14mlROS.mjs'),
16959
+ loadComponent: () => import('./ng-forge-dynamic-forms-text-field.component-BGw6T-4h.mjs'),
16358
16960
  mapper: textFieldMapper,
16359
16961
  valueHandling: 'exclude',
16360
16962
  ...DISPLAY_FIELD_TYPES_BASE,
@@ -16366,7 +16968,7 @@ const BUILT_IN_FIELDS = [
16366
16968
  },
16367
16969
  {
16368
16970
  name: 'container',
16369
- loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-CrOIZEL_.mjs'),
16971
+ loadComponent: () => import('./ng-forge-dynamic-forms-container-field.component-C554hOxN.mjs'),
16370
16972
  mapper: containerFieldMapper,
16371
16973
  valueHandling: 'flatten',
16372
16974
  ...CONTAINER_FIELD_TYPES_BASE,
@@ -16375,7 +16977,7 @@ const BUILT_IN_FIELDS = [
16375
16977
  const BUILT_IN_WRAPPERS = [
16376
16978
  {
16377
16979
  wrapperName: 'css',
16378
- loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-Q2g56KUM.mjs'),
16980
+ loadComponent: () => import('./ng-forge-dynamic-forms-css-wrapper.component-CnJgFWLB.mjs'),
16379
16981
  },
16380
16982
  {
16381
16983
  wrapperName: 'row',
@@ -18168,4 +18770,4 @@ function extractErrorParams(error) {
18168
18770
  */
18169
18771
 
18170
18772
  export { arrayFieldMapper as $, ADDON_ACTION_HANDLERS as A, BUILT_IN_FIELDS as B, CONTAINER_FIELD_PROCESSORS as C, DEFAULT_PROPS as D, EventBus as E, FIELD_REGISTRY as F, GROUP_CONTEXT as G, NoopLogger as H, INITIALIZATION_TIMEOUT_MS as I, PopArrayItemEvent as J, PrependArrayItemEvent as K, PreviousPageEvent as L, MoveArrayItemEvent as M, NextPageEvent as N, RootFormRegistryService as O, PageChangeEvent as P, SubmitEvent as Q, RemoveAtIndexEvent as R, ShiftArrayItemEvent as S, TemplateAddonComponent as T, TextAddonComponent as U, WRAPPER_REGISTRY as V, WRAPPER_AUTO_ASSOCIATIONS as W, applyMetaToElement as X, applyValidator as Y, applyValidators as Z, arrayEvent as _, ADDON_ACTION_REGISTRY as a, textFieldMapper as a$, baseFieldMapper as a0, buildBaseInputs as a1, containerFieldMapper as a2, createField as a3, createFieldResolutionPipe as a4, createHiddenSignal as a5, createRenderReadySignal as a6, createSchemaFromFields as a7, createWrapperChainController as a8, createWrappers as a9, isGroupField as aA, isLeafField as aB, isPageField as aC, isRowField as aD, isSameWrapperChain as aE, isSimplifiedArrayField as aF, isValueBearingField as aG, isValueField as aH, isWrapperTypeDefinition as aI, isWrappersBundle as aJ, logAddonWarnings as aK, mapFieldToInputs as aL, omit as aM, pageFieldMapper as aN, provideAddonActions as aO, provideDynamicForm as aP, resolveDynamicValue as aQ, resolveNextButtonDisabled as aR, resolveNonFieldDisabled as aS, resolveNonFieldHidden as aT, resolveSubmitButtonDisabled as aU, resolveTokens as aV, resolveWrappers as aW, rowFieldMapper as aX, runPresetAction as aY, sanitizeFormConfig as aZ, sanitizeFormConfigPure as a_, derivedFromDeferred as aa, dynamicTextToObservable as ab, evaluateNonFieldDisabled as ac, evaluateNonFieldHidden as ad, field as ae, formConfig as af, formatAddonWarning as ag, getArrayLength as ah, getFieldDefaultValue as ai, getFieldValueHandling as aj, getNormalizedArrayMetadata as ak, groupFieldMapper as al, hasFormValue as am, injectAddonKindRegistry as an, injectFieldRegistry as ao, injectFieldSignalContext as ap, injectFieldsSupportingAddons as aq, interpolateParams as ar, isArrayField as as, isCheckedField as at, isContainerField as au, isContainerTypedField as av, isDisplayOnlyField as aw, isEqual as ax, isFieldBoundContext as ay, isFormStateCondition as az, ADDON_KIND_COMPONENT_CACHE as b, toReadonlyFieldTree as b0, validateFieldAddons as b1, validatePageNesting as b2, walkAndValidateAddons as b3, withCustomAddon as b4, withEventFormValue as b5, withLegacyStatusClasses as b6, withLoggerConfig as b7, withPreviousValue as b8, withValidationExecutionDefaults as b9, withValueExclusionDefaults as ba, wrapperProps as bb, writeToFieldValue as bc, ADDON_KIND_DEFINITIONS as c, ADDON_KIND_REGISTRY as d, ARRAY_CONTEXT as e, AppendArrayItemEvent as f, ArrayItemRegistryService as g, ComponentAddonComponent as h, ComponentInitializedEvent as i, ConsoleLogger as j, DEFAULT_VALIDATION_MESSAGES as k, DEFAULT_WRAPPERS as l, DF_FIELD_TEMPLATES as m, DfAddonSlot as n, DfFieldOutlet as o, DfTemplate as p, DynamicForm as q, DynamicFormError as r, DynamicFormLogger as s, DynamicTextPipe as t, EventDispatcher as u, FIELD_SIGNAL_CONTEXT as v, FORM_OPTIONS as w, FormClearEvent as x, FormResetEvent as y, InsertArrayItemEvent as z };
18171
- //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms-Be1Lk0qR.mjs.map
18773
+ //# sourceMappingURL=ng-forge-dynamic-forms-ng-forge-dynamic-forms--PuL0AKc.mjs.map