@@ -150,6 +150,8 @@ export class ExecutionTree implements TreeContext {
150150 new Map ( ) ;
151151 /** Promise that resolves when all critical `force` handles have settled. */
152152 private forcedExecution ?: Promise < void > ;
153+ /** Cached spread data for field-by-field GraphQL resolution. */
154+ private spreadCache ?: Record < string , unknown > ;
153155 /** Shared trace collector — present only when tracing is enabled. */
154156 tracer ?: TraceCollector ;
155157 /**
@@ -1280,6 +1282,7 @@ export class ExecutionTree implements TreeContext {
12801282 const result = this . resolveWires ( matches ) ;
12811283 if ( ! array ) return result ;
12821284 const resolved = await result ;
1285+ if ( resolved == null || ! Array . isArray ( resolved ) ) return resolved ;
12831286 const arrayPathKey = path . join ( "." ) ;
12841287 if ( isLoopControlSignal ( resolved ) ) {
12851288 this . recordEmptyArray ( arrayPathKey ) ;
@@ -1719,6 +1722,29 @@ export class ExecutionTree implements TreeContext {
17191722 return this ;
17201723 }
17211724
1725+ // ── Lazy spread resolution ─────────────────────────────────────
1726+ // When ALL matches are spread wires, resolve them eagerly, cache
1727+ // the result, then return `this` so GraphQL sub-field resolvers
1728+ // can pick up both spread properties and explicit wires.
1729+ if (
1730+ ! array &&
1731+ matches . every (
1732+ ( w ) : boolean => "from" in w && "spread" in w && ! ! w . spread ,
1733+ )
1734+ ) {
1735+ const spreadData = await this . resolveWires ( matches ) ;
1736+ if ( spreadData != null && typeof spreadData === "object" ) {
1737+ const prefix = cleanPath . join ( "." ) ;
1738+ this . spreadCache ??= { } ;
1739+ if ( prefix === "" ) {
1740+ Object . assign ( this . spreadCache , spreadData as Record < string , unknown > ) ;
1741+ } else {
1742+ ( this . spreadCache as Record < string , unknown > ) [ prefix ] = spreadData ;
1743+ }
1744+ }
1745+ return this ;
1746+ }
1747+
17221748 const response = this . resolveWires ( matches ) ;
17231749
17241750 if ( ! array ) {
@@ -1727,6 +1753,7 @@ export class ExecutionTree implements TreeContext {
17271753
17281754 // Array: create shadow trees for per-element resolution
17291755 const resolved = await response ;
1756+ if ( resolved == null || ! Array . isArray ( resolved ) ) return resolved ;
17301757 const arrayPathKey = cleanPath . join ( "." ) ;
17311758 if ( isLoopControlSignal ( resolved ) ) {
17321759 this . recordEmptyArray ( arrayPathKey ) ;
@@ -1745,6 +1772,7 @@ export class ExecutionTree implements TreeContext {
17451772 const response = this . resolveWires ( defineFieldWires ) ;
17461773 if ( ! array ) return response ;
17471774 const resolved = await response ;
1775+ if ( resolved == null || ! Array . isArray ( resolved ) ) return resolved ;
17481776 const definePathKey = cleanPath . join ( "." ) ;
17491777 if ( isLoopControlSignal ( resolved ) ) {
17501778 this . recordEmptyArray ( definePathKey ) ;
@@ -1754,6 +1782,29 @@ export class ExecutionTree implements TreeContext {
17541782 }
17551783 }
17561784
1785+ // ── Spread cache fallback ─────────────────────────────────────────
1786+ // If a spread wire was resolved at a parent path, field-by-field GraphQL
1787+ // resolution consults the cached spread data for fields not covered by
1788+ // explicit wires.
1789+ if ( cleanPath . length > 0 && this . spreadCache ) {
1790+ // Check for a parent-level spread: e.g. cleanPath=["author"] with
1791+ // spread cached under "" (root spread), or cleanPath=["info","author"]
1792+ // with spread cached under "info".
1793+ const fieldName = cleanPath [ cleanPath . length - 1 ] ! ;
1794+ const parentPrefix = cleanPath . slice ( 0 , - 1 ) . join ( "." ) ;
1795+ const parentSpread =
1796+ parentPrefix === ""
1797+ ? this . spreadCache
1798+ : ( this . spreadCache [ parentPrefix ] as Record < string , unknown > | undefined ) ;
1799+ if (
1800+ parentSpread != null &&
1801+ typeof parentSpread === "object" &&
1802+ fieldName in parentSpread
1803+ ) {
1804+ return ( parentSpread as Record < string , unknown > ) [ fieldName ] ;
1805+ }
1806+ }
1807+
17571808 // Fallback: if this shadow tree has stored element data, resolve the
17581809 // requested field directly from it. This handles passthrough arrays
17591810 // where the bridge maps an inner array (e.g. `.stops <- j.stops`) but
0 commit comments