@githolon/dsl 0.62.0 → 0.63.0

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/fields.ts +35 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.62.0",
3
+ "version": "0.63.0",
4
4
  "type": "module",
5
5
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
package/src/fields.ts CHANGED
@@ -397,6 +397,12 @@ function makeField<T, K extends FieldKind = FieldKind>(
397
397
  extra: {
398
398
  refWorkspace?: string;
399
399
  refAggregateId?: string;
400
+ /** LAZY target resolution — `t.ref(() => Agg)` / `t.hasMany(() => Agg)`. Breaks a BIDIRECTIONAL
401
+ * declaration cycle (a Parent's `hasMany(Child)` ⇄ a Child's `ref(Parent)`): the thunk is stored and
402
+ * resolved on FIRST read of `refAggregateId` (at manifest build — after all module consts init), so
403
+ * neither side needs the other to already exist at its own `const`. Consumers read `refAggregateId`
404
+ * transparently (a memoized getter). */
405
+ refAggregateIdThunk?: () => string;
400
406
  viaField?: string;
401
407
  isOptional?: boolean;
402
408
  fromBirth?: true;
@@ -496,6 +502,21 @@ function makeField<T, K extends FieldKind = FieldKind>(
496
502
  return makeField<T, K>(kind, zod, driver, { ...extra, facet }) as Field<T, K> & { readonly facet: Fa };
497
503
  },
498
504
  };
505
+ // LAZY REF TARGET: when the target was given as a thunk (`t.ref(() => Agg)` / `t.hasMany(() => Agg)` — to
506
+ // break a bidirectional declaration cycle), expose `refAggregateId` as a MEMOIZED getter that resolves the
507
+ // thunk on first read (build time, after all module consts init). Consumers read `refAggregateId` as usual.
508
+ if (extra.refAggregateIdThunk !== undefined) {
509
+ const thunk = extra.refAggregateIdThunk;
510
+ let resolved: string | undefined;
511
+ Object.defineProperty(f, "refAggregateId", {
512
+ configurable: true,
513
+ enumerable: true,
514
+ get() {
515
+ if (resolved === undefined) resolved = thunk();
516
+ return resolved;
517
+ },
518
+ });
519
+ }
499
520
  return f;
500
521
  }
501
522
 
@@ -740,13 +761,16 @@ export const t = {
740
761
  * (one event on the CHILD, parent untouched), and the read engine serves `parent.children` as the
741
762
  * managed `Child by parent` index. The dev never builds a write-amplifying parent collection.
742
763
  */
743
- hasMany(child: AggregateHandle<string, Record<string, Field>>): {
764
+ hasMany(child: AggregateHandle<string, Record<string, Field>> | (() => AggregateHandle<string, Record<string, Field>>)): {
744
765
  via(backref: string): Field<string[], "hasMany">;
745
766
  } {
767
+ // THUNK form (`t.hasMany(() => Child)`) — the common bidirectional case (a Parent's `hasMany(Child)` ⇄ a
768
+ // Child's `ref(Parent)`): pass a thunk so the Parent needn't wait for the Child's `const` to exist.
769
+ const isThunk = typeof child === "function";
746
770
  return {
747
771
  via(backref: string): Field<string[], "hasMany"> {
748
772
  return makeField<string[], "hasMany">("hasMany", z.array(z.string()), Conflict, {
749
- refAggregateId: child.id,
773
+ ...(isThunk ? { refAggregateIdThunk: () => child().id } : { refAggregateId: child.id }),
750
774
  viaField: backref,
751
775
  });
752
776
  },
@@ -768,8 +792,11 @@ export const t = {
768
792
  },
769
793
  };
770
794
 
795
+ type RefTarget = AggregateHandle<string, Record<string, Field>> | (() => AggregateHandle<string, Record<string, Field>>);
796
+
771
797
  interface RefFn {
772
- (target: AggregateHandle<string, Record<string, Field>>): Field<string, "ref">;
798
+ /** Same-workspace reference. Pass a THUNK (`t.ref(() => Agg)`) to break a bidirectional declaration cycle. */
799
+ (target: RefTarget): Field<string, "ref">;
773
800
  (
774
801
  workspaceId: string,
775
802
  target: AggregateHandle<string, Record<string, Field>>,
@@ -777,7 +804,7 @@ interface RefFn {
777
804
  }
778
805
 
779
806
  function refImpl(
780
- a: string | AggregateHandle<string, Record<string, Field>>,
807
+ a: string | RefTarget,
781
808
  b?: AggregateHandle<string, Record<string, Field>>,
782
809
  ): Field<string, "ref"> {
783
810
  if (typeof a === "string") {
@@ -787,6 +814,10 @@ function refImpl(
787
814
  refAggregateId: target.id,
788
815
  });
789
816
  }
817
+ // THUNK form (`t.ref(() => Agg)`) — resolve lazily to break a bidirectional declaration cycle.
818
+ if (typeof a === "function") {
819
+ return makeField<string, "ref">("ref", z.string(), Conflict, { refAggregateIdThunk: () => a().id });
820
+ }
790
821
  return makeField<string, "ref">("ref", z.string(), Conflict, { refAggregateId: a.id });
791
822
  }
792
823