@xplane/core 0.15.2 → 1.0.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.
package/dist/index.mjs CHANGED
@@ -1,540 +1,547 @@
1
1
  import { Construct, Construct as Construct$1 } from "constructs";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ //#region src/core/context.ts
4
+ /**
5
+ * AsyncLocalStorage instance that carries the CompositionContext.
6
+ * The handler sets it before constructing the user's Composition,
7
+ * and the Composition constructor reads from it.
8
+ */
9
+ const compositionStorage = new AsyncLocalStorage();
10
+ /**
11
+ * Get the current composition context from AsyncLocalStorage.
12
+ * Throws if called outside of a composition construction scope.
13
+ */
14
+ function getCompositionContext() {
15
+ const ctx = compositionStorage.getStore();
16
+ if (ctx) return ctx;
17
+ throw new Error("No composition context found. Ensure the Composition is constructed within compositionStorage.run().");
18
+ }
19
+ //#endregion
20
+ //#region src/core/composition.ts
21
+ /**
22
+ * Base class for user-authored Crossplane compositions.
23
+ *
24
+ * Users extend this class and define resources in the constructor:
25
+ *
26
+ * ```ts
27
+ * class MyComposition extends Composition<MySpec, MyStatus> {
28
+ * constructor() {
29
+ * super();
30
+ * const vpc = new Vpc(this, 'vpc', { spec: { ... } });
31
+ * this.xr.status.vpcId = vpc.status.atProvider.vpcId;
32
+ * }
33
+ * }
34
+ * ```
35
+ *
36
+ * `this.xr` is a "desired-first, fallback-to-observed" proxy over the XR.
37
+ * `this.pipelineContext` provides typed read-only access to Crossplane function context.
38
+ */
39
+ var Composition = class extends Construct$1 {
40
+ /**
41
+ * The XR proxy — reads from desired first (status writes), falls through to observed.
42
+ * Writing to `this.xr.status.*` sets the composite status output.
43
+ */
44
+ xr;
45
+ /** The dependency graph tracking resource relationships. */
46
+ graph;
47
+ /** The edge collector accumulating dependency edges. */
48
+ collector;
49
+ constructor() {
50
+ super(void 0, "Composition");
51
+ const ctx = getCompositionContext();
52
+ this.graph = ctx.graph;
53
+ this.collector = ctx.collector;
54
+ this.node.setContext("xplane:graph", ctx.graph);
55
+ this.node.setContext("xplane:collector", ctx.collector);
56
+ const xrMeta = ctx.xr.metadata;
57
+ if (xrMeta) this.node.setContext("xplane:xr-meta", xrMeta);
58
+ this.xr = createXrProxy(ctx);
59
+ }
60
+ /**
61
+ * Read-only accessor for Crossplane function pipeline context.
62
+ * Keys are the context keys set by Crossplane or prior functions in the pipeline.
63
+ */
64
+ get pipelineContext() {
65
+ const ctx = getCompositionContext();
66
+ return {
67
+ get(key) {
68
+ return ctx.pipelineContext.get(key);
69
+ },
70
+ has(key) {
71
+ return ctx.pipelineContext.has(key);
72
+ },
73
+ keys() {
74
+ return ctx.pipelineContext.keys();
75
+ }
76
+ };
77
+ }
78
+ };
79
+ /**
80
+ * Creates the "desired-first, fallback-to-observed" proxy for the XR.
81
+ *
82
+ * - Reading `xr.spec.*` reads from observed XR spec (creates ReadProxy for tracking)
83
+ * - Writing `xr.status.*` writes to a desired-status store (emitted as composite status)
84
+ * - Other reads fall through to observed
85
+ */
86
+ function createXrProxy(ctx) {
87
+ const xrObserved = ctx.xr;
88
+ const xrDesiredStatus = {};
89
+ ctx.graph.addResource({ id: "__xr__" });
90
+ const statusProxy = new Proxy(xrDesiredStatus, {
91
+ get(_target, prop) {
92
+ if (typeof prop === "symbol") return void 0;
93
+ const key = String(prop);
94
+ if (key in xrDesiredStatus) return xrDesiredStatus[key];
95
+ const observedStatus = xrObserved.status;
96
+ if (observedStatus && key in observedStatus) return observedStatus[key];
97
+ },
98
+ set(_target, prop, value) {
99
+ if (typeof prop === "symbol") return false;
100
+ xrDesiredStatus[String(prop)] = value;
101
+ return true;
102
+ },
103
+ has(_target, prop) {
104
+ if (typeof prop === "symbol") return false;
105
+ const key = String(prop);
106
+ if (key in xrDesiredStatus) return true;
107
+ const observedStatus = xrObserved.status;
108
+ return observedStatus ? key in observedStatus : false;
109
+ }
110
+ });
111
+ return new Proxy({}, {
112
+ get(_target, prop) {
113
+ if (typeof prop === "symbol") return void 0;
114
+ const key = String(prop);
115
+ if (key === "status") return statusProxy;
116
+ if (key in xrObserved) return xrObserved[key];
117
+ },
118
+ set(_target, prop, value) {
119
+ if (typeof prop === "symbol") return false;
120
+ const key = String(prop);
121
+ if (key === "status") {
122
+ Object.assign(xrDesiredStatus, value);
123
+ return true;
124
+ }
125
+ xrObserved[key] = value;
126
+ return true;
127
+ },
128
+ has(_target, prop) {
129
+ if (typeof prop === "symbol") return false;
130
+ return String(prop) in xrObserved || String(prop) === "status";
131
+ }
132
+ });
133
+ }
134
+ /**
135
+ * Extract the desired XR status from a Composition instance.
136
+ * Used by the emit pipeline phase to produce composite status output.
137
+ */
138
+ function getXrDesiredStatus(composition) {
139
+ const statusProxy = composition.xr.status;
140
+ const result = {};
141
+ if (statusProxy && typeof statusProxy === "object") for (const key of Object.keys(statusProxy)) {
142
+ const value = statusProxy[key];
143
+ if (value != null) result[key] = value;
144
+ }
145
+ return result;
146
+ }
147
+ //#endregion
2
148
  //#region src/tracking/dependency-graph.ts
3
149
  /**
4
- * Directed acyclic graph of resource dependencies.
5
- * Supports topological sorting to determine resource creation order
6
- * and cycle detection to surface configuration errors.
150
+ * Tracks dependency relationships between resources as a directed acyclic graph.
151
+ * Edges are added as resources reference each other's observed state.
7
152
  */
8
153
  var DependencyGraph = class {
9
- /** adjacency list: resource id set of resource ids it depends on */
10
- _deps = /* @__PURE__ */ new Map();
11
- /** all registered resource refs by id */
154
+ _adjacency = /* @__PURE__ */ new Map();
12
155
  _resources = /* @__PURE__ */ new Map();
13
- /** raw edges for introspection */
14
156
  _edges = [];
15
- /** Register a resource node in the graph. */
16
157
  addResource(ref) {
17
158
  this._resources.set(ref.id, ref);
18
- if (!this._deps.has(ref.id)) this._deps.set(ref.id, /* @__PURE__ */ new Set());
159
+ if (!this._adjacency.has(ref.id)) this._adjacency.set(ref.id, /* @__PURE__ */ new Set());
160
+ }
161
+ addEdge(edge) {
162
+ this.addResource(edge.from);
163
+ this.addResource(edge.to);
164
+ this._adjacency.get(edge.to.id).add(edge.from.id);
165
+ this._edges.push(edge);
19
166
  }
20
- /** Add dependency edges from the collector. */
21
167
  addEdges(edges) {
22
- for (const edge of edges) {
23
- this.addResource(edge.from);
24
- this.addResource(edge.to);
25
- const deps = this._deps.get(edge.to.id);
26
- if (deps) deps.add(edge.from.id);
27
- this._edges.push(edge);
28
- }
168
+ for (const edge of edges) this.addEdge(edge);
29
169
  }
30
- /** Add an explicit dependency: `dependent` depends on `dependency`. */
31
170
  addExplicitDependency(dependent, dependency) {
32
171
  this.addResource(dependent);
33
172
  this.addResource(dependency);
34
- const deps = this._deps.get(dependent.id);
35
- if (deps) deps.add(dependency.id);
173
+ this._adjacency.get(dependent.id).add(dependency.id);
36
174
  }
37
- /** Get all resource IDs that `resourceId` directly depends on. */
175
+ /** Get the set of resource IDs that `resourceId` depends on. */
38
176
  getDependencies(resourceId) {
39
- return this._deps.get(resourceId) ?? /* @__PURE__ */ new Set();
177
+ return this._adjacency.get(resourceId) ?? /* @__PURE__ */ new Set();
40
178
  }
41
- /** Get all registered resource IDs. */
42
179
  get resourceIds() {
43
180
  return [...this._resources.keys()];
44
181
  }
45
- /** Get all raw edges. */
46
182
  get edges() {
47
183
  return this._edges;
48
184
  }
49
185
  /**
50
- * Returns resource IDs in topological order (dependencies first).
51
- * Throws if the graph contains cycles.
186
+ * Returns a topological ordering of resources.
187
+ * If a cycle is detected, returns { order: null, cycle: string[] }.
52
188
  */
53
189
  topologicalSort() {
54
190
  const visited = /* @__PURE__ */ new Set();
55
191
  const visiting = /* @__PURE__ */ new Set();
56
192
  const sorted = [];
57
193
  const visit = (id) => {
58
- if (visited.has(id)) return;
59
- if (visiting.has(id)) {
60
- const cycle = [...visiting, id].join(" → ");
61
- throw new Error(`Dependency cycle detected: ${cycle}`);
62
- }
194
+ if (visited.has(id)) return null;
195
+ if (visiting.has(id)) return [...visiting, id];
63
196
  visiting.add(id);
64
- const deps = this._deps.get(id);
65
- if (deps) for (const depId of deps) visit(depId);
197
+ const deps = this._adjacency.get(id);
198
+ if (deps) for (const depId of deps) {
199
+ const cycle = visit(depId);
200
+ if (cycle) return cycle;
201
+ }
66
202
  visiting.delete(id);
67
203
  visited.add(id);
68
204
  sorted.push(id);
205
+ return null;
69
206
  };
70
- for (const id of this._resources.keys()) visit(id);
71
- return sorted;
207
+ for (const id of this._resources.keys()) {
208
+ const cycle = visit(id);
209
+ if (cycle) {
210
+ const start = cycle[cycle.length - 1];
211
+ const startIdx = cycle.indexOf(start);
212
+ return {
213
+ order: null,
214
+ cycle: cycle.slice(startIdx)
215
+ };
216
+ }
217
+ }
218
+ return { order: sorted };
72
219
  }
73
220
  };
74
221
  //#endregion
222
+ //#region src/tracking/read-proxy.ts
223
+ /**
224
+ * WeakMap storing metadata for ReadProxy instances.
225
+ * This avoids polluting proxy objects with symbols.
226
+ */
227
+ const proxyMeta = /* @__PURE__ */ new WeakMap();
228
+ /** Sentinel symbol to identify ReadProxy instances. */
229
+ const READ_PROXY_TAG = Symbol.for("xplane.readProxy");
230
+ /**
231
+ * Check if a value is a ReadProxy.
232
+ */
233
+ function isReadProxy(value) {
234
+ return typeof value === "object" && value !== null && value[READ_PROXY_TAG] === true;
235
+ }
236
+ /**
237
+ * Get the metadata for a ReadProxy value.
238
+ */
239
+ function getReadProxyMeta(value) {
240
+ if (!isReadProxy(value)) return void 0;
241
+ return proxyMeta.get(value);
242
+ }
243
+ /**
244
+ * Creates a ReadProxy that wraps observed data.
245
+ *
246
+ * - Property access navigates into the data, building up the path.
247
+ * - Missing paths return `undefined` (no placeholder proxies).
248
+ * - The proxy carries owner + path metadata so that when it's assigned
249
+ * to a WriteProxy, the dependency edge can be recorded.
250
+ */
251
+ function createReadProxy(target, owner, basePath) {
252
+ const proxy = new Proxy(target, {
253
+ get(obj, prop, receiver) {
254
+ if (prop === READ_PROXY_TAG) return true;
255
+ if (typeof prop === "symbol") return Reflect.get(obj, prop, receiver);
256
+ if (prop === "toJSON") return () => obj;
257
+ const childPath = basePath ? `${basePath}.${String(prop)}` : String(prop);
258
+ const value = Reflect.get(obj, prop, receiver);
259
+ if (value === void 0 || value === null) return createLeafReadProxy(owner, childPath);
260
+ if (typeof value === "object") return createReadProxy(value, owner, childPath);
261
+ return createPrimitiveReadProxy(value, owner, childPath);
262
+ },
263
+ has(obj, prop) {
264
+ if (prop === READ_PROXY_TAG) return true;
265
+ return Reflect.has(obj, prop);
266
+ }
267
+ });
268
+ proxyMeta.set(proxy, {
269
+ owner,
270
+ path: basePath
271
+ });
272
+ return proxy;
273
+ }
274
+ /**
275
+ * A "leaf" ReadProxy for paths that don't exist in observed data yet.
276
+ * Carries metadata for edge creation. Resolves to `undefined` when
277
+ * coerced to a primitive.
278
+ */
279
+ function createLeafReadProxy(owner, path) {
280
+ const proxy = new Proxy(Object.create(null), {
281
+ get(_obj, prop) {
282
+ if (prop === READ_PROXY_TAG) return true;
283
+ if (prop === Symbol.toPrimitive) return () => `__pending__${owner.id}__${path}`;
284
+ if (typeof prop === "symbol") return void 0;
285
+ if (prop === "toJSON") return () => void 0;
286
+ if (prop === "toString") return () => `__pending__${owner.id}__${path}`;
287
+ if (prop === "valueOf") return () => proxy;
288
+ return createLeafReadProxy(owner, `${path}.${String(prop)}`);
289
+ },
290
+ has(_obj, prop) {
291
+ if (prop === READ_PROXY_TAG) return true;
292
+ return false;
293
+ }
294
+ });
295
+ proxyMeta.set(proxy, {
296
+ owner,
297
+ path
298
+ });
299
+ return proxy;
300
+ }
301
+ /**
302
+ * Wraps a concrete primitive value so it carries ReadProxy metadata.
303
+ * This allows the WriteProxy to detect it during assignment and
304
+ * record the dependency edge, while the value itself resolves correctly.
305
+ */
306
+ function createPrimitiveReadProxy(value, owner, path) {
307
+ const proxy = new Proxy(Object.create(null), {
308
+ get(_obj, prop) {
309
+ if (prop === READ_PROXY_TAG) return true;
310
+ if (prop === Symbol.toPrimitive) return () => value;
311
+ if (prop === "valueOf") return () => value;
312
+ if (prop === "toString") return () => String(value);
313
+ if (prop === "toJSON") return () => value;
314
+ if (typeof prop === "symbol") return void 0;
315
+ return createLeafReadProxy(owner, `${path}.${String(prop)}`);
316
+ },
317
+ has(_obj, prop) {
318
+ if (prop === READ_PROXY_TAG) return true;
319
+ return false;
320
+ }
321
+ });
322
+ proxyMeta.set(proxy, {
323
+ owner,
324
+ path
325
+ });
326
+ return proxy;
327
+ }
328
+ //#endregion
75
329
  //#region src/tracking/types.ts
76
330
  /**
77
- * Symbols used to access tracking metadata on proxy-wrapped values.
78
- * These are not enumerable and won't leak into serialized output.
331
+ * A Pending marker stored in a desired document when a ReadProxy value
332
+ * is assigned. Carries full source info so the resolve phase knows
333
+ * where to look for the concrete value.
79
334
  */
80
- const TRACKING_META = Symbol.for("xplane.tracking.meta");
81
- const IS_TRACKED = Symbol.for("xplane.tracking.isTracked");
335
+ const PENDING_TAG = Symbol.for("xplane.pending");
336
+ var Pending = class {
337
+ source;
338
+ path;
339
+ static TAG = PENDING_TAG;
340
+ [PENDING_TAG] = true;
341
+ constructor(source, path) {
342
+ this.source = source;
343
+ this.path = path;
344
+ }
345
+ static is(value) {
346
+ return typeof value === "object" && value !== null && value[PENDING_TAG] === true;
347
+ }
348
+ };
82
349
  //#endregion
83
- //#region src/tracking/proxy.ts
350
+ //#region src/tracking/write-proxy.ts
84
351
  /**
85
- * Registry that collects dependency edges discovered during proxy access.
86
- * Shared across all tracked values within a single composition run.
352
+ * Collector that accumulates dependency edges discovered during
353
+ * WriteProxy assignments.
87
354
  */
88
- var DependencyCollector = class {
355
+ var EdgeCollector = class {
89
356
  _edges = [];
90
- addEdge(edge) {
357
+ add(edge) {
91
358
  if (!this._edges.some((e) => e.from.id === edge.from.id && e.fromPath === edge.fromPath && e.to.id === edge.to.id && e.toPath === edge.toPath)) this._edges.push(edge);
92
359
  }
93
360
  get edges() {
94
361
  return this._edges;
95
362
  }
96
- clear() {
97
- this._edges.length = 0;
98
- }
99
363
  };
100
364
  /**
101
- * Returns true if `value` is a tracked proxy created by `createTrackedProxy`.
102
- */
103
- function isTracked(value) {
104
- return typeof value === "object" && value !== null && value[IS_TRACKED] === true;
105
- }
106
- /**
107
- * Retrieves the tracking metadata from a tracked proxy.
108
- * Returns undefined if the value is not tracked.
109
- */
110
- function getTrackingMeta(value) {
111
- if (isTracked(value)) return value[TRACKING_META];
112
- }
113
- /**
114
- * Creates a proxy around `target` that:
115
- * 1. Returns nested proxies for property access (building up dot-paths).
116
- * 2. On set: if the assigned value is itself a tracked proxy from a *different*
117
- * resource, records a DependencyEdge in the collector.
118
- * 3. Stores concrete values normally so the underlying object is populated.
365
+ * Creates a WriteProxy that wraps a desired document.
119
366
  *
120
- * For "observed" proxies, property reads on missing keys return a nested
121
- * tracked proxy (representing an "unknown" value) rather than undefined.
122
- * This lets `vpc.status.atProvider.vpcId` work even before the VPC exists.
123
- */
124
- function createTrackedProxy(target, opts) {
125
- const meta = {
126
- owner: opts.owner,
127
- path: opts.path,
128
- observed: opts.observed
129
- };
367
+ * - Writes store values in the target.
368
+ * - When a ReadProxy value is assigned, it records a dependency edge
369
+ * and stores a Pending marker if the value is not yet concrete.
370
+ * - Reads return the stored value (desired-first).
371
+ */
372
+ function createWriteProxy(target, opts) {
373
+ const { owner, collector, basePath = "" } = opts;
130
374
  return new Proxy(target, {
131
375
  get(obj, prop, receiver) {
132
- if (prop === TRACKING_META) return meta;
133
- if (prop === IS_TRACKED) return true;
134
- if (prop === Symbol.toPrimitive) {
135
- if (opts.observed && Object.keys(obj).length === 0) return () => {
136
- throw new Error(`Cannot coerce XR path '${opts.path}' to a primitive — the field does not exist in the composite resource`);
137
- };
138
- return Reflect.get(obj, prop, receiver);
139
- }
140
376
  if (typeof prop === "symbol") return Reflect.get(obj, prop, receiver);
141
377
  if (prop === "toJSON") return () => obj;
142
- const existing = Reflect.get(obj, prop, receiver);
143
- if (isTracked(existing)) return existing;
144
- if (typeof existing === "object" && existing !== null) {
145
- const wrapped = createTrackedProxy(existing, {
146
- owner: opts.owner,
147
- path: opts.path ? `${opts.path}.${prop}` : String(prop),
148
- observed: opts.observed,
149
- collector: opts.collector,
150
- strict: opts.strict
151
- });
152
- Reflect.set(obj, prop, wrapped);
153
- return wrapped;
154
- }
155
- if (existing !== void 0 || prop in obj) return existing;
156
- if (opts.observed) {
157
- if (opts.strict) return;
158
- return createTrackedProxy({}, {
159
- owner: opts.owner,
160
- path: opts.path ? `${opts.path}.${prop}` : String(prop),
161
- observed: true,
162
- collector: opts.collector
163
- });
164
- }
165
- const wrapped = createTrackedProxy({}, {
166
- owner: opts.owner,
167
- path: opts.path ? `${opts.path}.${prop}` : String(prop),
168
- observed: false,
169
- collector: opts.collector
378
+ const value = Reflect.get(obj, prop, receiver);
379
+ if (typeof value === "object" && value !== null && !Pending.is(value)) return createWriteProxy(value, {
380
+ owner,
381
+ collector,
382
+ basePath: basePath ? `${basePath}.${String(prop)}` : String(prop)
170
383
  });
171
- Reflect.set(obj, prop, wrapped);
172
- return wrapped;
384
+ return value;
173
385
  },
174
386
  set(obj, prop, value) {
175
387
  if (typeof prop === "symbol") return Reflect.set(obj, prop, value);
176
- const targetPath = opts.path ? `${opts.path}.${prop}` : String(prop);
177
- if (isTracked(value)) {
178
- const sourceMeta = getTrackingMeta(value);
179
- if (sourceMeta && sourceMeta.owner.id === "__xr__") {
180
- const concrete = resolveTrackedValue(value);
181
- return Reflect.set(obj, prop, concrete === UNRESOLVED ? void 0 : concrete);
388
+ const targetPath = basePath ? `${basePath}.${String(prop)}` : String(prop);
389
+ if (isReadProxy(value)) {
390
+ const meta = getReadProxyMeta(value);
391
+ if (meta && meta.owner.id !== owner.id) {
392
+ collector.add({
393
+ from: meta.owner,
394
+ fromPath: meta.path,
395
+ to: owner,
396
+ toPath: targetPath
397
+ });
398
+ const primitive = tryExtractPrimitive$2(value);
399
+ if (primitive !== void 0) return Reflect.set(obj, prop, primitive);
400
+ return Reflect.set(obj, prop, new Pending(meta.owner, meta.path));
401
+ }
402
+ const primitive = tryExtractPrimitive$2(value);
403
+ if (primitive !== void 0) return Reflect.set(obj, prop, primitive);
404
+ if (meta) {
405
+ collector.add({
406
+ from: meta.owner,
407
+ fromPath: meta.path,
408
+ to: owner,
409
+ toPath: targetPath
410
+ });
411
+ return Reflect.set(obj, prop, new Pending(meta.owner, meta.path));
182
412
  }
183
- if (sourceMeta && sourceMeta.owner.id !== opts.owner.id) opts.collector.addEdge({
184
- from: sourceMeta.owner,
185
- fromPath: sourceMeta.path,
186
- to: opts.owner,
187
- toPath: targetPath
188
- });
189
- const concrete = resolveTrackedValue(value);
190
- return Reflect.set(obj, prop, concrete);
191
413
  }
192
- if (typeof value === "object" && value !== null && !isTracked(value)) {
193
- const wrapped = createTrackedProxy(value, {
194
- owner: opts.owner,
195
- path: targetPath,
196
- observed: opts.observed,
197
- collector: opts.collector
198
- });
199
- return Reflect.set(obj, prop, wrapped);
414
+ if (typeof value === "object" && value !== null && !Pending.is(value)) {
415
+ const processed = deepProcessValue(value, owner, targetPath, collector);
416
+ return Reflect.set(obj, prop, processed);
200
417
  }
201
418
  return Reflect.set(obj, prop, value);
202
419
  },
203
- ownKeys(obj) {
204
- return Reflect.ownKeys(obj).filter((k) => typeof k === "string");
205
- },
206
- getOwnPropertyDescriptor(obj, prop) {
207
- const desc = Reflect.getOwnPropertyDescriptor(obj, prop);
208
- if (desc) return {
209
- ...desc,
210
- configurable: true,
211
- enumerable: true
212
- };
213
- if (opts.observed && typeof prop === "string") return {
214
- configurable: true,
215
- enumerable: true,
216
- writable: true,
217
- value: void 0
218
- };
219
- },
220
- has(obj, prop) {
221
- if (prop === IS_TRACKED || prop === TRACKING_META) return true;
222
- return Reflect.has(obj, prop);
420
+ deleteProperty(obj, prop) {
421
+ return Reflect.deleteProperty(obj, prop);
223
422
  }
224
423
  });
225
424
  }
226
425
  /**
227
- * Sentinel value used when a tracked reference cannot be resolved yet
228
- * (the source resource hasn't been observed).
426
+ * Try to extract a primitive value from a ReadProxy via Symbol.toPrimitive.
427
+ * Returns `undefined` if the proxy represents a non-existent (leaf) value.
229
428
  */
230
- const UNRESOLVED = Symbol.for("xplane.unresolved");
231
- /**
232
- * Attempts to extract a concrete (non-proxy) value from a tracked value.
233
- * Returns UNRESOLVED if the tracked value points to an empty observed path.
234
- */
235
- function resolveTrackedValue(tracked) {
236
- if (!isTracked(tracked)) return tracked;
237
- const obj = unwrapProxy(tracked);
238
- const keys = Object.keys(obj);
239
- const meta = getTrackingMeta(tracked);
240
- if (keys.length === 0 && meta?.observed) return UNRESOLVED;
241
- return obj;
242
- }
243
- /**
244
- * Strips the proxy layer and returns the raw underlying object.
245
- */
246
- function unwrapProxy(tracked) {
247
- const result = {};
248
- for (const key of Object.keys(tracked)) result[key] = tracked[key];
249
- return result;
429
+ function tryExtractPrimitive$2(proxy) {
430
+ const toPrim = proxy[Symbol.toPrimitive];
431
+ if (typeof toPrim === "function") {
432
+ const result = toPrim();
433
+ if (result !== void 0 && result !== null && typeof result !== "object") {
434
+ if (typeof result === "string" && result.startsWith("__pending__")) return void 0;
435
+ return result;
436
+ }
437
+ }
250
438
  }
251
- //#endregion
252
- //#region src/core/construct.ts
253
- /**
254
- * Context keys used to propagate tracking infrastructure through the construct tree.
255
- * Set by Composition (root), read by Resource and other constructs.
256
- * @internal
257
- */
258
- const CONTEXT_COLLECTOR = "xplane:collector";
259
- const CONTEXT_GRAPH = "xplane:graph";
260
- /** Raw XR name and namespace stored at composition root for use by uniqueName. */
261
- const CONTEXT_XR_META = "xplane:xr-meta";
262
- /** Registry of existing resource references on the composition root. */
263
- const CONTEXT_EXISTING = "xplane:existing";
264
- //#endregion
265
- //#region src/core/composition.ts
266
439
  /**
267
- * A Composition is the root Construct for a Crossplane composition function.
268
- * Like CDK's `App` or cdk8s's `Chart`, it is the root of the construct tree.
269
- * Resources and constructs are created in the constructor.
270
- *
271
- * Usage:
272
- * ```ts
273
- * class MyComposition extends Composition {
274
- * constructor() {
275
- * super();
276
- * const vpc = new aws.ec2.VPC(this, 'vpc', { ... });
277
- * const subnet = new aws.ec2.Subnet(this, 'subnet', {
278
- * spec: { forProvider: { vpcId: vpc.status.atProvider.vpcId } }
279
- * });
280
- * }
281
- * }
282
- * ```
440
+ * Deep-process a plain object/array being assigned to a WriteProxy.
441
+ * Replaces any nested ReadProxy values with Pending markers or concrete values.
283
442
  */
284
- var Composition = class Composition extends Construct$1 {
285
- /**
286
- * Pending XR data, set by the framework before instantiation.
287
- * @internal
288
- */
289
- static _pendingXR;
290
- /**
291
- * Pending environment data, set by the framework before instantiation.
292
- * Populated from the Crossplane context key `apiextensions.crossplane.io/environment`.
293
- * @internal
294
- */
295
- static _pendingEnvironment;
296
- /** The composite resource (XR) — proxy-wrapped for tracking. */
297
- xr;
298
- /** Environment data from function-environment-configs or other pipeline steps. */
299
- environment;
300
- /** Raw name from the XR metadata (not proxy-tracked). */
301
- xrName;
302
- /** Raw namespace from the XR metadata (not proxy-tracked). */
303
- xrNamespace;
304
- /** Dependency collector shared across all resources. */
305
- collector;
306
- /** Dependency graph built during compose(). */
307
- graph;
308
- /** Registry of existing (read-only) resource references keyed by refKey. */
309
- _existingResources = /* @__PURE__ */ new Map();
310
- /** Registered status output function. @internal */
311
- _statusFn;
312
- constructor() {
313
- super(void 0, "");
314
- this.collector = new DependencyCollector();
315
- this.graph = new DependencyGraph();
316
- this.node.setContext(CONTEXT_COLLECTOR, this.collector);
317
- this.node.setContext(CONTEXT_GRAPH, this.graph);
318
- this.node.setContext(CONTEXT_EXISTING, this._existingResources);
319
- const xrData = Composition._pendingXR ?? {};
320
- Composition._pendingXR = void 0;
321
- const envData = Composition._pendingEnvironment ?? {};
322
- Composition._pendingEnvironment = void 0;
323
- const xrMeta = xrData.metadata ?? {};
324
- this.xrName = typeof xrMeta.name === "string" ? xrMeta.name : void 0;
325
- this.xrNamespace = typeof xrMeta.namespace === "string" ? xrMeta.namespace : void 0;
326
- this.node.setContext(CONTEXT_XR_META, {
327
- name: this.xrName,
328
- namespace: this.xrNamespace
443
+ function deepProcessValue(value, owner, basePath, collector) {
444
+ if (value === null || value === void 0) return value;
445
+ if (typeof value !== "object") return value;
446
+ if (isReadProxy(value)) {
447
+ const meta = getReadProxyMeta(value);
448
+ collector.add({
449
+ from: meta.owner,
450
+ fromPath: meta.path,
451
+ to: owner,
452
+ toPath: basePath
329
453
  });
330
- this.xr = createTrackedProxy(xrData, {
331
- owner: { id: "__xr__" },
332
- path: "",
333
- observed: true,
334
- collector: this.collector,
335
- strict: true
336
- });
337
- this.environment = envData;
338
- }
339
- /**
340
- * Register a function that computes the desired XR status output.
341
- *
342
- * The function is called by the framework **after** observed state has been
343
- * fed into all resources, so `resource.observed` contains real data.
344
- *
345
- * @example
346
- * ```ts
347
- * this.setStatusOutput(() => ({
348
- * config: {
349
- * projectHostedZoneId: hostedZone.observed?.status?.atProvider?.id,
350
- * },
351
- * }));
352
- * ```
353
- */
354
- setStatusOutput(fn) {
355
- this._statusFn = fn;
356
- }
357
- /**
358
- * Compute and return the desired status output.
359
- * Returns an empty object if no status function was registered.
360
- * @internal
361
- */
362
- computeStatusOutput() {
363
- return this._statusFn?.() ?? {};
364
- }
365
- /**
366
- * Walk up the construct tree and return the root Composition.
367
- * Throws if the scope is not within a Composition.
368
- */
369
- static of(scope) {
370
- let current = scope;
371
- while (current !== void 0) {
372
- if (current instanceof Composition) return current;
373
- current = current.node.scope;
374
- }
375
- throw new Error("No Composition found in the scope chain. Ensure constructs are created within a Composition.");
454
+ const primitive = tryExtractPrimitive$2(value);
455
+ if (primitive !== void 0) return primitive;
456
+ return new Pending(meta.owner, meta.path);
376
457
  }
377
- /** Get all composed (non-existing) resources keyed by construct path. */
378
- get resources() {
379
- const map = /* @__PURE__ */ new Map();
380
- for (const construct of this.node.findAll()) if (isResource(construct) && !construct.isExisting) map.set(construct.node.path, construct);
381
- return map;
382
- }
383
- /** Get all existing (read-only) resource references keyed by refKey. */
384
- get existingResources() {
385
- return this._existingResources;
386
- }
387
- };
388
- /**
389
- * Type guard for Resource — avoids circular import by checking for
390
- * characteristic properties rather than instanceof.
391
- */
392
- function isResource(construct) {
393
- return construct !== null && typeof construct === "object" && "apiVersion" in construct && "kind" in construct && "resourceRef" in construct && "isExisting" in construct;
458
+ if (Array.isArray(value)) return value.map((item, i) => deepProcessValue(item, owner, `${basePath}[${i}]`, collector));
459
+ const result = {};
460
+ for (const [key, val] of Object.entries(value)) result[key] = deepProcessValue(val, owner, `${basePath}.${key}`, collector);
461
+ return result;
394
462
  }
395
463
  //#endregion
396
464
  //#region src/core/resource.ts
465
+ const internals = /* @__PURE__ */ new WeakMap();
397
466
  /**
398
- * A Construct that represents a single Crossplane managed/composed resource.
467
+ * A Kubernetes resource within a Composition.
468
+ *
469
+ * The Resource instance acts as a "desired-first, fallback-to-observed" proxy:
470
+ * - Reading a path that exists in the desired document returns the desired value.
471
+ * - Reading a path that does NOT exist in desired falls through to a tracked
472
+ * ReadProxy over observed state (creates dependency edges).
473
+ * - Writing always goes to the desired document.
399
474
  *
400
- * The `spec` and `status` properties are proxy-wrapped for automatic
401
- * dependency tracking. Assigning a value from another resource's status
402
- * to this resource's spec automatically records a dependency edge.
475
+ * The only reserved properties are `node` (from Construct) and `resource`
476
+ * (framework config namespace).
403
477
  */
404
478
  var Resource = class Resource extends Construct$1 {
405
- apiVersion;
406
- kind;
407
- resourceRef;
408
- /** Proxy-wrapped desired spec — writes are tracked. */
409
- spec;
410
- /** Proxy-wrapped observed status — reads create dependency tracking. */
411
- status;
412
- /** Proxy-wrapped desired metadata. */
413
- metadata;
414
- /**
415
- * Observed-mode tracked proxy over the entire resource document.
416
- * Use this to access arbitrary top-level fields on existing resources
417
- * (e.g., `secret.root.data.myKey` for a Secret, `configMap.root.data.key` for a ConfigMap).
418
- */
419
- root;
420
- /** Whether auto-ready is enabled for this resource. */
421
- autoReady;
422
- /** Whether this is a read-only reference to an existing cluster resource. */
423
- isExisting;
424
- /** If this is an existing resource, holds the reference metadata for the handler. */
425
- existingRef;
426
- /** Extra top-level fields (e.g. data/stringData for Secret). Not proxy-tracked. */
427
- _extra;
428
- /** Observed state populated by the bridge before construction. */
429
- _observed;
430
- /** Backing object for the status proxy — populated by setObserved(). */
431
- _statusTarget;
432
- /** Backing object for the spec proxy (existing resources only) — populated by setObservedFull(). */
433
- _specTarget;
434
- /** Backing object for the root proxy — populated by setObservedFull(). */
435
- _rootTarget;
436
- /** Backing object for the metadata proxy — populated by setObserved(). */
437
- _metaTarget;
438
- /** Keys the user explicitly declared in constructor metadata props. */
439
- _desiredMetaKeys;
440
- /** Explicit dependency refs. */
441
- _explicitDeps = [];
442
- /** @internal */
443
- _graph;
444
- constructor(scope, id, props, options) {
479
+ resource;
480
+ constructor(scope, id, props) {
445
481
  super(scope, id);
446
- this.apiVersion = props.apiVersion;
447
- this.kind = props.kind;
448
- this.autoReady = options?.autoReady ?? true;
449
- this.isExisting = false;
450
- this.existingRef = void 0;
451
- const collector = this.node.tryGetContext(CONTEXT_COLLECTOR);
452
- const graph = this.node.tryGetContext(CONTEXT_GRAPH);
453
- if (!collector || !graph) throw new Error("Resource must be created within a Composition tree");
454
- this.resourceRef = { id: this.node.path };
455
- graph.addResource(this.resourceRef);
456
- const KNOWN_KEYS = new Set([
457
- "apiVersion",
458
- "kind",
459
- "metadata",
460
- "spec"
461
- ]);
462
- this._extra = {};
463
- for (const [k, v] of Object.entries(props)) if (!KNOWN_KEYS.has(k)) this._extra[k] = v;
464
- const specTarget = deepCloneWithTracked(props.spec ?? {});
465
- resolveTrackedRefs(specTarget, this.resourceRef, "spec", collector);
466
- this.spec = createTrackedProxy(specTarget, {
467
- owner: this.resourceRef,
468
- path: "spec",
469
- observed: false,
470
- collector
471
- });
472
- this._metaTarget = props.metadata ?? {};
473
- this._desiredMetaKeys = new Set(Object.keys(this._metaTarget));
474
- this.metadata = createTrackedProxy(this._metaTarget, {
475
- owner: this.resourceRef,
476
- path: "metadata",
477
- observed: true,
478
- collector
479
- });
480
- this._statusTarget = {};
481
- this.status = createTrackedProxy(this._statusTarget, {
482
- owner: this.resourceRef,
483
- path: "status",
484
- observed: true,
485
- collector
486
- });
487
- this._rootTarget = {};
488
- this.root = createTrackedProxy(this._rootTarget, {
489
- owner: this.resourceRef,
490
- path: "",
491
- observed: true,
482
+ const graph = scope.node.tryGetContext("xplane:graph");
483
+ const collector = scope.node.tryGetContext("xplane:collector");
484
+ if (!graph || !collector) throw new Error("Resource must be created within a Composition tree.");
485
+ const ref = { id: this.node.path };
486
+ graph.addResource(ref);
487
+ const readyChecks = [];
488
+ const config = {
489
+ autoReady: true,
490
+ addReadyCheck(fn, priority = 50) {
491
+ readyChecks.push({
492
+ fn,
493
+ priority
494
+ });
495
+ }
496
+ };
497
+ this.resource = config;
498
+ const internal = {
499
+ ref,
500
+ desired: processDesiredProps(props, ref, collector),
501
+ observed: {},
502
+ external: false,
503
+ config,
504
+ readyChecks,
505
+ graph,
492
506
  collector
493
- });
494
- this._specTarget = void 0;
495
- this._graph = graph;
496
- }
497
- /** Fully qualified path in the construct tree. */
498
- get path() {
499
- return this.node.path;
500
- }
501
- /** Add an explicit dependency on another resource. */
502
- addDependency(other) {
503
- this._explicitDeps.push(other.resourceRef);
504
- this._graph.addExplicitDependency(this.resourceRef, other.resourceRef);
505
- }
506
- /** Get explicit dependency refs. */
507
- get explicitDependencies() {
508
- return this._explicitDeps;
509
- }
510
- /** Set observed state (called by the bridge before compose). */
511
- setObserved(observed) {
512
- this._observed = observed;
513
- for (const key of Object.keys(this._metaTarget)) this._desiredMetaKeys.add(key);
514
- if (observed.metadata && typeof observed.metadata === "object") Object.assign(this._metaTarget, observed.metadata);
515
- if (observed.status && typeof observed.status === "object") Object.assign(this._statusTarget, observed.status);
516
- }
517
- /** Get observed state. */
518
- get observed() {
519
- return this._observed;
507
+ };
508
+ internals.set(this, internal);
509
+ return createResourceProxy(this, internal);
520
510
  }
521
511
  /**
522
- * Compute a unique name for a resource based on its construct node path,
523
- * similar to `cdk.Names.uniqueResourceName`.
524
- *
525
- * The name is structured as:
526
- * `[namespace-]claimName-PathSegments[-extra]-hash8`
512
+ * Look up an existing cluster resource by name.
513
+ * Returns a Resource that only has observed state (no desired output).
527
514
  *
528
- * - XR namespace (if present) and XR name are always prepended.
529
- * - Path segments (construct tree, root skipped) are appended next.
530
- * - An optional `extra` string is appended after the path.
531
- * - An 8-char hash of the full untruncated string is always appended for uniqueness.
532
- * - Whitespace in each segment is stripped (CDK convention).
533
- * - Disallowed characters are replaced by the separator; consecutive separators are collapsed.
534
- * - The result is truncated to `maxLength` while keeping the hash suffix.
535
- *
536
- * @param scope - The construct whose node path is used.
537
- * @param options - Optional tuning.
515
+ * The `name` parameter accepts either a plain string or a PrimitiveReadProxy
516
+ * (returned when reading a tracked property like `ns.metadata.labels['x']`).
517
+ * Proxies are coerced to their underlying string via `Symbol.toPrimitive`.
518
+ */
519
+ static fromExistingByName(scope, apiVersion, kind, name, namespace) {
520
+ const resolvedName = coerceToString(name);
521
+ const refKey = computeRefKey(apiVersion, kind, resolvedName, namespace);
522
+ const instance = new Resource(scope, `__existing__${refKey.replace(/\//g, "_")}`, {
523
+ apiVersion,
524
+ kind
525
+ });
526
+ const internal = internals.get(instance);
527
+ internal.external = true;
528
+ internal.externalRef = {
529
+ apiVersion,
530
+ kind,
531
+ name: resolvedName ?? name,
532
+ namespace,
533
+ refKey
534
+ };
535
+ const ctx = compositionStorage.getStore();
536
+ if (ctx) {
537
+ const observed = ctx.requiredResources.get(refKey);
538
+ if (observed) Object.assign(internal.observed, observed);
539
+ }
540
+ return instance;
541
+ }
542
+ /**
543
+ * Generate a deterministic unique name based on the XR identity and construct path.
544
+ * Useful for resource fields that need unique names (e.g., AWS resource names).
538
545
  */
539
546
  static uniqueName(scope, options = {}) {
540
547
  const maxLength = options.maxLength ?? 63;
@@ -543,12 +550,10 @@ var Resource = class Resource extends Construct$1 {
543
550
  const escapedSep = separator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
544
551
  const collapseRe = new RegExp(`${escapedSep}+`, "g");
545
552
  const clean = (s) => s.replace(/\s+/g, "").replace(allowedPattern, separator).replace(collapseRe, separator).replace(new RegExp(`^${escapedSep}|${escapedSep}$`, "g"), "");
546
- const xrMeta = scope.node.tryGetContext(CONTEXT_XR_META);
547
- const xrName = xrMeta?.name;
548
- const xrNamespace = xrMeta?.namespace;
553
+ const xrMeta = scope.node.tryGetContext("xplane:xr-meta");
549
554
  const parts = [];
550
- if (xrNamespace) parts.push(clean(xrNamespace));
551
- if (xrName) parts.push(clean(xrName));
555
+ if (xrMeta?.namespace) parts.push(clean(xrMeta.namespace));
556
+ if (xrMeta?.name) parts.push(clean(xrMeta.name));
552
557
  for (const s of scope.node.scopes.slice(1)) {
553
558
  const c = clean(s.node.id);
554
559
  if (c) parts.push(c);
@@ -563,91 +568,48 @@ var Resource = class Resource extends Construct$1 {
563
568
  if (withHash.length <= maxLength) return withHash;
564
569
  return `${full.slice(0, maxLength - hash.length - separator.length)}${separator}${hash}`;
565
570
  }
566
- /**
567
- * Serialize to a plain Kubernetes resource object for the desired state.
568
- * Strips proxy wrappers, UNRESOLVED sentinels, and server-managed metadata
569
- * fields (uid, resourceVersion, etc.) that must not appear in desired state.
570
- */
571
- toDesired() {
572
- const fullMeta = JSON.parse(JSON.stringify(this.metadata));
573
- const desiredMeta = {};
574
- for (const key of this._desiredMetaKeys) if (key in fullMeta) desiredMeta[key] = fullMeta[key];
575
- const cleanMeta = stripUnresolved(desiredMeta);
576
- const desired = {
577
- ...this._extra,
578
- apiVersion: this.apiVersion,
579
- kind: this.kind,
580
- metadata: cleanMeta,
581
- spec: stripUnresolved(JSON.parse(JSON.stringify(this.spec)))
582
- };
583
- if (desired.spec && typeof desired.spec === "object" && Object.keys(desired.spec).length === 0) delete desired.spec;
584
- return desired;
585
- }
586
- /**
587
- * Populate the full observed state for an existing resource.
588
- * Feeds data into `root`, `status`, `spec`, and `metadata` backing targets
589
- * so that proxy reads resolve to real values.
590
- */
591
- setObservedFull(resource) {
592
- this._observed = resource;
593
- for (const [key, value] of Object.entries(resource)) if (value !== null && value !== void 0) this._rootTarget[key] = value;
594
- if (resource.status && typeof resource.status === "object") Object.assign(this._statusTarget, resource.status);
595
- if (this._specTarget && resource.spec && typeof resource.spec === "object") Object.assign(this._specTarget, resource.spec);
596
- if (resource.metadata && typeof resource.metadata === "object") Object.assign(this._metaTarget, resource.metadata);
597
- }
598
- /**
599
- * Create a read-only reference to an existing cluster resource.
600
- * The resource will be fetched by Crossplane via the Required Resources mechanism.
601
- * Its `status`, `spec`, and `root` proxies can be read to create dependency edges.
602
- *
603
- * @param scope - Parent construct (typically `this` inside a Composition constructor)
604
- * @param apiVersion - API version of the resource (e.g. "example.io/v1")
605
- * @param kind - Kind of the resource (e.g. "Project")
606
- * @param name - Name of the resource (can be a literal string or a tracked proxy value)
607
- * @param namespace - Optional namespace of the resource
608
- */
609
- static fromExistingByName(scope, apiVersion, kind, name, namespace) {
610
- const refKey = computeRefKey(apiVersion, kind, typeof name === "string" ? name : void 0, namespace);
611
- const resource = new Resource(scope, `__existing__${refKey.replace(/\//g, "_")}`, {
612
- apiVersion,
613
- kind,
614
- spec: {}
615
- });
616
- resource.isExisting = true;
617
- resource.existingRef = {
618
- apiVersion,
619
- kind,
620
- name,
621
- namespace,
622
- refKey
623
- };
624
- const collector = resource.node.tryGetContext(CONTEXT_COLLECTOR);
625
- const specTarget = {};
626
- resource._specTarget = specTarget;
627
- resource.spec = createTrackedProxy(specTarget, {
628
- owner: resource.resourceRef,
629
- path: "spec",
630
- observed: true,
631
- collector
632
- });
633
- const existingMap = resource.node.tryGetContext(CONTEXT_EXISTING);
634
- if (existingMap) existingMap.set(refKey, resource);
635
- return resource;
636
- }
637
571
  };
572
+ function getResourceInternals(resource) {
573
+ const internal = internals.get(resource);
574
+ if (!internal) throw new Error("Resource internals not found");
575
+ return internal;
576
+ }
577
+ function getResourceRef(resource) {
578
+ return getResourceInternals(resource).ref;
579
+ }
580
+ function getDesiredDocument(resource) {
581
+ return getResourceInternals(resource).desired;
582
+ }
583
+ function getObservedDocument(resource) {
584
+ return getResourceInternals(resource).observed;
585
+ }
586
+ function hydrateObserved(resource, data) {
587
+ const internal = getResourceInternals(resource);
588
+ Object.assign(internal.observed, data);
589
+ }
590
+ function isExternal(resource) {
591
+ return getResourceInternals(resource).external;
592
+ }
593
+ function getExternalRef(resource) {
594
+ return getResourceInternals(resource).externalRef;
595
+ }
596
+ function getReadyChecks(resource) {
597
+ return getResourceInternals(resource).readyChecks;
598
+ }
638
599
  /**
639
- * Compute a deterministic reference key for an existing resource.
640
- * Format: "apiVersion/kind/[namespace/]name" or "apiVersion/kind/__unresolved__" if name is not yet known.
600
+ * Coerce a value to a string, handling PrimitiveReadProxy objects that wrap
601
+ * primitive values behind a `Symbol.toPrimitive` method.
602
+ * Returns `undefined` if the value cannot be resolved to a string.
641
603
  */
604
+ function coerceToString(value) {
605
+ if (typeof value === "string") return value;
606
+ if (value != null && typeof value[Symbol.toPrimitive] === "function") return String(value);
607
+ }
642
608
  function computeRefKey(apiVersion, kind, name, namespace) {
643
609
  const namePart = name ?? "__unresolved__";
644
610
  if (namespace) return `${apiVersion}/${kind}/${namespace}/${namePart}`;
645
611
  return `${apiVersion}/${kind}/${namePart}`;
646
612
  }
647
- /**
648
- * Produce an 8-character hex hash of a string using a simple djb2-style
649
- * algorithm — no crypto dependency required.
650
- */
651
613
  function shortHash(s) {
652
614
  let h = 5381;
653
615
  for (let i = 0; i < s.length; i++) {
@@ -656,165 +618,651 @@ function shortHash(s) {
656
618
  }
657
619
  return h.toString(16).padStart(8, "0");
658
620
  }
659
- /** Recursively remove UNRESOLVED sentinel values from an object. */
660
- function stripUnresolved(obj) {
661
- if (obj === null || obj === void 0) return obj;
662
- if (typeof obj === "symbol" && obj === UNRESOLVED) return void 0;
663
- if (Array.isArray(obj)) return obj.map(stripUnresolved);
664
- if (typeof obj === "object") {
665
- const result = {};
666
- for (const [key, value] of Object.entries(obj)) {
667
- const cleaned = stripUnresolved(value);
668
- if (cleaned !== void 0) result[key] = cleaned;
669
- }
670
- return result;
671
- }
672
- return obj;
673
- }
674
621
  /**
675
- * Deep-clone an object while preserving tracked proxy references.
676
- * Plain objects and arrays are cloned; tracked proxies and primitives are kept as-is.
622
+ * Process the desired props — deep-scan for ReadProxy values and replace them
623
+ * with Pending markers (recording edges in the collector).
677
624
  */
678
- function deepCloneWithTracked(obj) {
679
- if (obj === null || obj === void 0) return obj;
680
- if (isTracked(obj)) return obj;
681
- if (typeof obj !== "object") return obj;
682
- if (Array.isArray(obj)) return obj.map(deepCloneWithTracked);
683
- const clone = {};
684
- for (const [key, value] of Object.entries(obj)) clone[key] = deepCloneWithTracked(value);
685
- return clone;
625
+ function processDesiredProps(props, owner, collector) {
626
+ const result = {};
627
+ for (const [key, value] of Object.entries(props)) result[key] = processValue(value, owner, key, collector);
628
+ return result;
629
+ }
630
+ function processValue(value, owner, path, collector) {
631
+ if (value === null || value === void 0) return value;
632
+ if (typeof value !== "object") return value;
633
+ if (isReadProxy(value)) {
634
+ const meta = getReadProxyMeta(value);
635
+ collector.add({
636
+ from: meta.owner,
637
+ fromPath: meta.path,
638
+ to: owner,
639
+ toPath: path
640
+ });
641
+ const prim = tryExtractPrimitive$1(value);
642
+ if (prim !== void 0) return prim;
643
+ return new Pending(meta.owner, meta.path);
644
+ }
645
+ if (Array.isArray(value)) return value.map((item, i) => processValue(item, owner, `${path}[${i}]`, collector));
646
+ const result = {};
647
+ for (const [key, val] of Object.entries(value)) result[key] = processValue(val, owner, `${path}.${key}`, collector);
648
+ return result;
649
+ }
650
+ function tryExtractPrimitive$1(proxy) {
651
+ const toPrim = proxy[Symbol.toPrimitive];
652
+ if (typeof toPrim === "function") {
653
+ const result = toPrim();
654
+ if (result !== void 0 && result !== null && typeof result !== "object") {
655
+ if (typeof result === "string" && result.startsWith("__pending__")) return void 0;
656
+ return result;
657
+ }
658
+ }
686
659
  }
687
660
  /**
688
- * Recursively scan an object for tracked proxy values from other resources.
689
- * For each one found, record a dependency edge and replace the value with
690
- * the UNRESOLVED sentinel. This handles values passed via object literals
691
- * in constructor props, which bypass the proxy's set trap.
661
+ * Creates the "desired-first, fallback-to-observed" proxy around a Resource.
692
662
  */
693
- function resolveTrackedRefs(obj, owner, basePath, collector) {
694
- for (const [key, value] of Object.entries(obj)) {
695
- const path = basePath ? `${basePath}.${key}` : key;
696
- if (isTracked(value)) {
697
- const sourceMeta = getTrackingMeta(value);
698
- if (sourceMeta && sourceMeta.owner.id !== owner.id) {
699
- collector.addEdge({
700
- from: sourceMeta.owner,
701
- fromPath: sourceMeta.path,
702
- to: owner,
703
- toPath: path
663
+ function createResourceProxy(resource, internal) {
664
+ const { ref, desired, collector } = internal;
665
+ const proxy = new Proxy(resource, {
666
+ get(target, prop, receiver) {
667
+ if (prop === "node") return Reflect.get(target, prop, receiver);
668
+ if (prop === "resource") return Reflect.get(target, prop, receiver);
669
+ if (typeof prop === "symbol") return Reflect.get(target, prop, receiver);
670
+ if (prop === "constructor") return Reflect.get(target, prop, receiver);
671
+ if (prop in desired) {
672
+ const value = desired[String(prop)];
673
+ if (typeof value === "object" && value !== null && !Pending.is(value)) return createWriteProxy(value, {
674
+ owner: ref,
675
+ collector,
676
+ basePath: String(prop)
704
677
  });
705
- obj[key] = UNRESOLVED;
678
+ return value;
706
679
  }
707
- continue;
680
+ const observed = internal.observed;
681
+ if (String(prop) in observed) {
682
+ const value = observed[String(prop)];
683
+ if (typeof value === "object" && value !== null) return createReadProxy(value, ref, String(prop));
684
+ return createPrimitiveReadProxyFromResource(value, ref, String(prop));
685
+ }
686
+ return createReadProxy({}, ref, String(prop));
687
+ },
688
+ set(target, prop, value) {
689
+ if (typeof prop === "symbol") return Reflect.set(target, prop, value);
690
+ if (prop === "resource") return Reflect.set(target, prop, value);
691
+ const path = String(prop);
692
+ desired[path] = processValue(value, ref, path, collector);
693
+ return true;
694
+ },
695
+ has(target, prop) {
696
+ if (typeof prop === "symbol") return Reflect.has(target, prop);
697
+ if (prop === "node" || prop === "resource") return true;
698
+ return prop in desired || prop in internal.observed;
708
699
  }
709
- if (Array.isArray(value)) {
710
- for (let i = 0; i < value.length; i++) {
711
- const item = value[i];
712
- if (isTracked(item)) {
713
- const sourceMeta = getTrackingMeta(item);
714
- if (sourceMeta && sourceMeta.owner.id !== owner.id) {
715
- collector.addEdge({
716
- from: sourceMeta.owner,
717
- fromPath: sourceMeta.path,
718
- to: owner,
719
- toPath: `${path}[${i}]`
720
- });
721
- value[i] = UNRESOLVED;
722
- }
723
- } else if (typeof item === "object" && item !== null) resolveTrackedRefs(item, owner, `${path}[${i}]`, collector);
700
+ });
701
+ internals.set(proxy, internal);
702
+ return proxy;
703
+ }
704
+ /**
705
+ * Wrap a primitive observed value so it carries ReadProxy metadata
706
+ * for dependency tracking when assigned elsewhere.
707
+ */
708
+ function createPrimitiveReadProxyFromResource(value, owner, path) {
709
+ if (value === null || value === void 0) return value;
710
+ return createPrimitiveReadProxy(value, owner, path);
711
+ }
712
+ //#endregion
713
+ //#region src/logging/context.ts
714
+ const noopLogger = {
715
+ debug() {},
716
+ info() {},
717
+ warn() {}
718
+ };
719
+ const loggerStorage = new AsyncLocalStorage();
720
+ /**
721
+ * Get the current logger from async context.
722
+ * Returns a no-op logger if none has been set (silent by default).
723
+ */
724
+ function getLogger() {
725
+ return loggerStorage.getStore() ?? noopLogger;
726
+ }
727
+ /**
728
+ * Run a function with a logger available in async context.
729
+ * All code within `fn` (and any nested async calls) can access
730
+ * the logger via `getLogger()`.
731
+ */
732
+ function withLogger(logger, fn) {
733
+ return loggerStorage.run(logger, fn);
734
+ }
735
+ //#endregion
736
+ //#region src/pipeline/diagnose.ts
737
+ /**
738
+ * DIAGNOSE phase: produce structured diagnostics for blocked resources.
739
+ *
740
+ * For each resource classified as 'blocked':
741
+ * - Find all Pending markers in the desired document
742
+ * - Report what they're waiting on (source resource + path)
743
+ *
744
+ * For circular dependencies (detected in sequence phase):
745
+ * - Produce a 'cycle' diagnostic with the full cycle path
746
+ */
747
+ function diagnose(state) {
748
+ const diagnostics = [];
749
+ const sortResult = state.graph.topologicalSort();
750
+ if (sortResult.order === null && sortResult.cycle) {
751
+ const firstCycleMember = sortResult.cycle[0];
752
+ if (firstCycleMember) diagnostics.push({
753
+ resource: firstCycleMember,
754
+ reason: "cycle",
755
+ cycle: sortResult.cycle
756
+ });
757
+ }
758
+ for (const resource of state.resources) {
759
+ if (!isExternal(resource)) continue;
760
+ const ref = getExternalRef(resource);
761
+ if (!ref) continue;
762
+ const observed = getObservedDocument(resource);
763
+ if (Object.keys(observed).length > 0) continue;
764
+ const nameDisplay = typeof ref.name === "string" ? ref.name : ref.name == null ? "(unresolved)" : "(unresolved)";
765
+ const nsDisplay = ref.namespace ? ` in namespace '${ref.namespace}'` : "";
766
+ diagnostics.push({
767
+ resource: getResourceRef(resource).id,
768
+ reason: "not-found",
769
+ detail: `External resource ${ref.apiVersion}/${ref.kind} '${nameDisplay}'${nsDisplay} was required but not found by Crossplane`
770
+ });
771
+ }
772
+ const pendingDiagnostics = [];
773
+ for (const resource of state.resources) {
774
+ if (isExternal(resource)) continue;
775
+ const ref = getResourceRef(resource);
776
+ if (state.classification.get(ref.id) !== "blocked") continue;
777
+ if (sortResult.order === null && sortResult.cycle?.includes(ref.id)) continue;
778
+ const pendingPaths = findPendingPaths(getDesiredDocument(resource), "");
779
+ if (pendingPaths.length > 0) pendingDiagnostics.push({
780
+ resource: ref.id,
781
+ reason: "pending",
782
+ pendingPaths
783
+ });
784
+ }
785
+ const notFoundIds = new Set(diagnostics.filter((d) => d.reason === "not-found").map((d) => d.resource));
786
+ const blockedIds = new Set(pendingDiagnostics.map((d) => d.resource));
787
+ for (const diag of pendingDiagnostics) if (diag.pendingPaths?.some((p) => {
788
+ const dep = p.waitingOn.resource;
789
+ return !blockedIds.has(dep) && !notFoundIds.has(dep);
790
+ })) diagnostics.push(diag);
791
+ return {
792
+ ...state,
793
+ diagnostics
794
+ };
795
+ }
796
+ /**
797
+ * Recursively find all Pending markers in a document and report their source.
798
+ */
799
+ function findPendingPaths(obj, basePath) {
800
+ const results = [];
801
+ if (obj === null || obj === void 0) return results;
802
+ if (Pending.is(obj)) {
803
+ results.push({
804
+ path: basePath,
805
+ waitingOn: {
806
+ resource: obj.source.id,
807
+ path: obj.path
724
808
  }
725
- continue;
809
+ });
810
+ return results;
811
+ }
812
+ if (typeof obj !== "object") return results;
813
+ if (Array.isArray(obj)) {
814
+ for (let i = 0; i < obj.length; i++) {
815
+ const childPath = basePath ? `${basePath}[${i}]` : `[${i}]`;
816
+ results.push(...findPendingPaths(obj[i], childPath));
726
817
  }
727
- if (typeof value === "object" && value !== null) resolveTrackedRefs(value, owner, path, collector);
818
+ return results;
819
+ }
820
+ for (const [key, value] of Object.entries(obj)) {
821
+ const childPath = basePath ? `${basePath}.${key}` : key;
822
+ results.push(...findPendingPaths(value, childPath));
728
823
  }
824
+ return results;
729
825
  }
730
826
  //#endregion
731
- //#region src/ready/auto-ready.ts
827
+ //#region src/pipeline/emit.ts
732
828
  /**
733
- * Determines if a Crossplane managed resource is ready based on its
734
- * observed status conditions.
829
+ * EMIT phase: serialize each resource classified as 'emit' into a plain
830
+ * Kubernetes resource document ready for Crossplane.
735
831
  *
736
- * - If the resource has a `Ready: True` condition → ready.
737
- * - If the resource has a `Ready: False` condition → not ready.
738
- * - If the resource exists but has no `Ready` condition at all (e.g. Namespace,
739
- * ProviderConfig) considered ready (the resource exists and is functional).
740
- * - If not yet observed → not ready.
741
- */
742
- function isResourceReady(observed) {
743
- if (!observed) return false;
744
- const conditions = observed.status?.conditions;
745
- if (!Array.isArray(conditions) || conditions.length === 0) return true;
746
- const readyCondition = conditions.find((c) => c.type === "Ready");
747
- if (!readyCondition) return true;
748
- return readyCondition.status === "True";
832
+ * Also extracts the XR desired status from this.xr.status assignments.
833
+ */
834
+ function emit(state) {
835
+ const emitted = [];
836
+ for (const resource of state.resources) {
837
+ if (isExternal(resource)) continue;
838
+ const ref = getResourceRef(resource);
839
+ if (state.classification.get(ref.id) !== "emit") continue;
840
+ const internal = getResourceInternals(resource);
841
+ const desired = getDesiredDocument(resource);
842
+ const name = ref.id.startsWith("Composition/") ? ref.id.slice(12) : ref.id;
843
+ emitted.push({
844
+ name,
845
+ document: deepClean(desired),
846
+ autoReady: internal.config.autoReady,
847
+ readyChecks: getReadyChecks(resource)
848
+ });
849
+ }
850
+ const resourceById = new Map(state.resources.map((r) => [getResourceRef(r).id, r]));
851
+ const xrStatusPatches = resolveXrStatus(getXrDesiredStatus(state.composition), resourceById);
852
+ return {
853
+ ...state,
854
+ emitted,
855
+ xrStatusPatches
856
+ };
749
857
  }
750
858
  /**
751
- * Gets the Ready condition from a resource, if present.
859
+ * Deep-clone an object, stripping any remaining framework internals.
860
+ * This produces a clean JSON-serializable Kubernetes document.
752
861
  */
753
- function getReadyCondition(observed) {
754
- if (!observed?.status) return void 0;
755
- const conditions = observed.status.conditions;
756
- if (!Array.isArray(conditions)) return void 0;
757
- return conditions.find((c) => c.type === "Ready");
862
+ function deepClean(obj) {
863
+ const result = {};
864
+ for (const [key, value] of Object.entries(obj)) result[key] = cleanValue(value);
865
+ return result;
866
+ }
867
+ function cleanValue(value) {
868
+ if (value === null || value === void 0) return value;
869
+ if (typeof value !== "object") return value;
870
+ if (Array.isArray(value)) return value.map(cleanValue);
871
+ const result = {};
872
+ for (const [key, val] of Object.entries(value)) result[key] = cleanValue(val);
873
+ return result;
874
+ }
875
+ /**
876
+ * Resolve ReadProxy values in XR status using observed resource data.
877
+ * This is needed because XR status is written at construction time (before hydration),
878
+ * so read proxy references need to be resolved post-hydration.
879
+ */
880
+ function resolveXrStatus(status, resourceById) {
881
+ const result = {};
882
+ for (const [key, value] of Object.entries(status)) {
883
+ const resolved = resolveStatusValue(value, resourceById);
884
+ if (resolved !== void 0) result[key] = resolved;
885
+ }
886
+ return result;
887
+ }
888
+ function resolveStatusValue(value, resourceById) {
889
+ if (value === null || value === void 0) return void 0;
890
+ if (isReadProxy(value)) {
891
+ const meta = getReadProxyMeta(value);
892
+ if (!meta) return void 0;
893
+ const prim = tryExtractPrimitive(value);
894
+ if (prim !== void 0) return prim;
895
+ const resource = resourceById.get(meta.owner.id);
896
+ if (!resource) return void 0;
897
+ return getNestedValue$1(getObservedDocument(resource), meta.path);
898
+ }
899
+ if (Array.isArray(value)) return value.map((v) => resolveStatusValue(v, resourceById)).filter((v) => v != null);
900
+ if (typeof value === "object") {
901
+ const out = {};
902
+ for (const [k, v] of Object.entries(value)) {
903
+ const resolved = resolveStatusValue(v, resourceById);
904
+ if (resolved !== void 0) out[k] = resolved;
905
+ }
906
+ return Object.keys(out).length > 0 ? out : void 0;
907
+ }
908
+ return value;
909
+ }
910
+ function tryExtractPrimitive(proxy) {
911
+ const toPrim = proxy[Symbol.toPrimitive];
912
+ if (typeof toPrim === "function") {
913
+ const result = toPrim();
914
+ if (result !== void 0 && result !== null && typeof result !== "object") {
915
+ if (typeof result === "string" && result.startsWith("__pending__")) return void 0;
916
+ return result;
917
+ }
918
+ }
919
+ }
920
+ function getNestedValue$1(obj, path) {
921
+ const parts = path.split(".");
922
+ let current = obj;
923
+ for (const part of parts) {
924
+ if (current === null || current === void 0 || typeof current !== "object") return void 0;
925
+ current = current[part];
926
+ }
927
+ return current;
928
+ }
929
+ //#endregion
930
+ //#region src/pipeline/hydrate.ts
931
+ /**
932
+ * HYDRATE phase: feed observed state from Crossplane into each resource.
933
+ *
934
+ * - Composed resources are matched by their construct path (resource name).
935
+ * - External resources are matched by their refKey.
936
+ */
937
+ function hydrate(state) {
938
+ for (const resource of state.resources) if (isExternal(resource)) {
939
+ const ref = getExternalRef(resource);
940
+ if (ref) {
941
+ const observed = state.observedRequired.get(ref.refKey);
942
+ if (observed) hydrateObserved(resource, observed);
943
+ }
944
+ } else {
945
+ const name = resource.node.path;
946
+ const observed = state.observedComposed.get(name);
947
+ if (observed) hydrateObserved(resource, observed);
948
+ }
949
+ return state;
758
950
  }
759
951
  //#endregion
760
- //#region src/sequencing/resolver.ts
952
+ //#region src/pipeline/resolve.ts
761
953
  /**
762
- * Resolves resource dependencies and determines which resources can be
763
- * emitted in the current pass.
954
+ * RESOLVE phase: walk dependency edges and replace Pending markers
955
+ * with concrete values from observed state where available.
764
956
  *
765
- * Algorithm:
766
- * 1. Topologically sort resources using the dependency graph.
767
- * 2. For each resource (in order), check if upstream dependencies have
768
- * resolved values in observed state.
769
- * 3. If all deps resolved → emit. If any dep unresolved → block.
770
- */
771
- function resolveSequencing(resources, graph, observedResources) {
772
- const order = graph.topologicalSort();
773
- const emit = [];
774
- const blocked = [];
775
- for (const resourceId of order) {
776
- const resource = findResourceByRef(resources, resourceId);
777
- if (!resource) continue;
778
- const deps = graph.getDependencies(resourceId);
779
- let allDepsReady = true;
780
- for (const depId of deps) {
781
- const depResource = findResourceByRef(resources, depId);
782
- if (!depResource) {
783
- if (!observedResources.get(depId)) allDepsReady = false;
784
- continue;
957
+ * For each resource's desired document, recursively find Pending values.
958
+ * Look up the source resource's observed state at the source path.
959
+ * If concrete replace. If not available leave Pending in place.
960
+ */
961
+ function resolve(state) {
962
+ const resourceById = new Map(state.resources.map((r) => [getResourceRef(r).id, r]));
963
+ for (const resource of state.resources) resolvePending(getDesiredDocument(resource), resourceById);
964
+ return state;
965
+ }
966
+ /**
967
+ * Recursively walk an object and resolve any Pending markers.
968
+ */
969
+ function resolvePending(obj, resourceById) {
970
+ for (const [key, value] of Object.entries(obj)) if (Pending.is(value)) {
971
+ const sourceResource = resourceById.get(value.source.id);
972
+ if (sourceResource) {
973
+ const resolved = getNestedValue(getObservedDocument(sourceResource), value.path);
974
+ if (resolved !== void 0) obj[key] = resolved;
975
+ }
976
+ } else if (Array.isArray(value)) resolveArray(value, resourceById);
977
+ else if (value !== null && typeof value === "object") resolvePending(value, resourceById);
978
+ }
979
+ function resolveArray(arr, resourceById) {
980
+ for (let i = 0; i < arr.length; i++) {
981
+ const value = arr[i];
982
+ if (Pending.is(value)) {
983
+ const sourceResource = resourceById.get(value.source.id);
984
+ if (sourceResource) {
985
+ const resolved = getNestedValue(getObservedDocument(sourceResource), value.path);
986
+ if (resolved !== void 0) arr[i] = resolved;
785
987
  }
786
- if (!observedResources.get(depResource.path)) allDepsReady = false;
988
+ } else if (Array.isArray(value)) resolveArray(value, resourceById);
989
+ else if (value !== null && typeof value === "object") resolvePending(value, resourceById);
990
+ }
991
+ }
992
+ /**
993
+ * Get a nested value from an object by dot-separated path.
994
+ */
995
+ function getNestedValue(obj, path) {
996
+ const segments = path.split(".");
997
+ let current = obj;
998
+ for (const segment of segments) {
999
+ if (current === null || current === void 0) return void 0;
1000
+ if (typeof current !== "object") return void 0;
1001
+ current = current[segment];
1002
+ }
1003
+ return current;
1004
+ }
1005
+ //#endregion
1006
+ //#region src/pipeline/sequence.ts
1007
+ /**
1008
+ * SEQUENCE phase: topological sort and classification.
1009
+ *
1010
+ * - Runs topological sort on the dependency graph.
1011
+ * - Classifies each resource as 'emit', 'blocked', or 'external'.
1012
+ * - A resource is 'blocked' if it still has Pending markers in its desired document.
1013
+ * - External resources are classified as 'external' (never emitted).
1014
+ * - Circular dependencies are detected via the graph's topological sort.
1015
+ */
1016
+ function sequence(state) {
1017
+ const classification = /* @__PURE__ */ new Map();
1018
+ const sortResult = state.graph.topologicalSort();
1019
+ for (const resource of state.resources) {
1020
+ const ref = getResourceRef(resource);
1021
+ if (isExternal(resource)) {
1022
+ classification.set(ref.id, "external");
1023
+ continue;
787
1024
  }
788
- if (allDepsReady && hasUnresolvedFields(resource)) allDepsReady = false;
789
- if (allDepsReady) emit.push(resource);
790
- else blocked.push(resource);
1025
+ if (containsPending(getDesiredDocument(resource))) classification.set(ref.id, "blocked");
1026
+ else classification.set(ref.id, "emit");
1027
+ }
1028
+ if (sortResult.order === null && sortResult.cycle) {
1029
+ for (const id of sortResult.cycle) if (classification.get(id) !== "external") classification.set(id, "blocked");
791
1030
  }
792
1031
  return {
793
- emit,
794
- blocked,
795
- order
1032
+ ...state,
1033
+ classification
796
1034
  };
797
1035
  }
798
1036
  /**
799
- * Check if a resource's desired state contains any UNRESOLVED sentinels.
800
- * Uses the raw spec/metadata before stripping, so UNRESOLVED symbols are visible.
1037
+ * Recursively check if an object contains any Pending markers.
801
1038
  */
802
- function hasUnresolvedFields(resource) {
803
- return containsUnresolved(resource.spec) || containsUnresolved(resource.metadata);
804
- }
805
- /** Recursively check if an object contains UNRESOLVED sentinels. */
806
- function containsUnresolved(obj) {
807
- if (obj === UNRESOLVED) return true;
1039
+ function containsPending(obj) {
808
1040
  if (obj === null || obj === void 0) return false;
809
- if (Array.isArray(obj)) return obj.some(containsUnresolved);
810
- if (typeof obj === "object") return Object.values(obj).some(containsUnresolved);
1041
+ if (Pending.is(obj)) return true;
1042
+ if (typeof obj !== "object") return false;
1043
+ if (Array.isArray(obj)) return obj.some(containsPending);
1044
+ for (const value of Object.values(obj)) if (containsPending(value)) return true;
1045
+ return false;
1046
+ }
1047
+ //#endregion
1048
+ //#region src/pipeline/index.ts
1049
+ /**
1050
+ * Run the full rendering pipeline.
1051
+ *
1052
+ * Phases: hydrate → resolve → sequence → diagnose → emit
1053
+ */
1054
+ function runPipeline(input) {
1055
+ const resources = collectResources(input.composition);
1056
+ let state = {
1057
+ composition: input.composition,
1058
+ resources,
1059
+ graph: input.composition.graph,
1060
+ observedComposed: input.observedComposed,
1061
+ observedRequired: input.observedRequired,
1062
+ classification: /* @__PURE__ */ new Map(),
1063
+ diagnostics: [],
1064
+ emitted: [],
1065
+ xrStatusPatches: {}
1066
+ };
1067
+ state = hydrate(state);
1068
+ state = resolve(state);
1069
+ state = sequence(state);
1070
+ state = diagnose(state);
1071
+ state = emit(state);
1072
+ return state;
1073
+ }
1074
+ /**
1075
+ * Collect all Resource instances from the composition's construct tree.
1076
+ */
1077
+ function collectResources(composition) {
1078
+ const resources = [];
1079
+ collectFromNode(composition, resources);
1080
+ return resources;
1081
+ }
1082
+ function collectFromNode(construct, resources) {
1083
+ for (const child of construct.node.children) {
1084
+ if (isResourceInstance(child)) resources.push(child);
1085
+ collectFromNode(child, resources);
1086
+ }
1087
+ }
1088
+ function isResourceInstance(obj) {
1089
+ if (obj === null || typeof obj !== "object") return false;
1090
+ const r = obj;
1091
+ if (!("resource" in r) || r.resource === null || typeof r.resource !== "object") return false;
1092
+ return "autoReady" in r.resource;
1093
+ }
1094
+ //#endregion
1095
+ //#region src/readiness/defaults.ts
1096
+ /**
1097
+ * Checks if the resource has a `Ready` condition with status `True`.
1098
+ */
1099
+ function conditionReady(observed) {
1100
+ const conditions = observed.status?.conditions;
1101
+ if (!conditions || !Array.isArray(conditions)) return void 0;
1102
+ const ready = conditions.find((c) => c.type === "Ready");
1103
+ if (!ready) return void 0;
1104
+ return ready.status === "True";
1105
+ }
1106
+ /**
1107
+ * Checks if the resource has `status.ready === true`.
1108
+ */
1109
+ function statusReady(observed) {
1110
+ const status = observed.status;
1111
+ if (status === void 0) return void 0;
1112
+ if (!("ready" in status)) return void 0;
1113
+ return status.ready === true;
1114
+ }
1115
+ /**
1116
+ * Fallback: resource exists in observed state → ready.
1117
+ * Always returns `true` (only called when observed is defined).
1118
+ */
1119
+ function exists(_observed) {
1120
+ return true;
1121
+ }
1122
+ /**
1123
+ * Built-in default readiness checks, appended at low priority.
1124
+ */
1125
+ const DEFAULT_CHECKS = [
1126
+ {
1127
+ fn: conditionReady,
1128
+ priority: 100,
1129
+ name: "conditionReady"
1130
+ },
1131
+ {
1132
+ fn: statusReady,
1133
+ priority: 200,
1134
+ name: "statusReady"
1135
+ },
1136
+ {
1137
+ fn: exists,
1138
+ priority: 1e3,
1139
+ name: "exists"
1140
+ }
1141
+ ];
1142
+ //#endregion
1143
+ //#region src/readiness/evaluate.ts
1144
+ /**
1145
+ * Evaluate readiness for a resource by running checks grouped by priority.
1146
+ *
1147
+ * - If `observed` is undefined, the resource doesn't exist yet → not ready.
1148
+ * - Checks are grouped by priority (ascending). Within a group, all checks
1149
+ * are AND-ed: if any returns `false`, the resource is not ready. If at least
1150
+ * one returns `true` and none return `false`, the resource is ready.
1151
+ * If all return `undefined`, cascade to the next priority group.
1152
+ * - Final fallback (no group had a definitive answer): not ready.
1153
+ */
1154
+ function evaluateReadiness(checks, observed) {
1155
+ const log = getLogger();
1156
+ if (!observed) {
1157
+ log.debug("readiness: resource not observed, not ready");
1158
+ return false;
1159
+ }
1160
+ const groups = /* @__PURE__ */ new Map();
1161
+ for (const check of checks) {
1162
+ const group = groups.get(check.priority);
1163
+ if (group) group.push(check);
1164
+ else groups.set(check.priority, [check]);
1165
+ }
1166
+ const priorities = [...groups.keys()].sort((a, b) => a - b);
1167
+ for (const priority of priorities) {
1168
+ const group = groups.get(priority);
1169
+ let hasTrue = false;
1170
+ let hasFalse = false;
1171
+ const results = [];
1172
+ for (const check of group) {
1173
+ const result = check.fn(observed);
1174
+ results.push({
1175
+ name: check.name ?? "anonymous",
1176
+ result
1177
+ });
1178
+ if (result === false) {
1179
+ hasFalse = true;
1180
+ break;
1181
+ }
1182
+ if (result === true) hasTrue = true;
1183
+ }
1184
+ log.debug("readiness: evaluated group", {
1185
+ priority,
1186
+ results
1187
+ });
1188
+ if (hasFalse) {
1189
+ log.debug("readiness: group returned false, not ready", { priority });
1190
+ return false;
1191
+ }
1192
+ if (hasTrue) {
1193
+ log.debug("readiness: group returned true, ready", { priority });
1194
+ return true;
1195
+ }
1196
+ }
1197
+ log.debug("readiness: no group had definitive answer, not ready");
811
1198
  return false;
812
1199
  }
813
- /** Find a resource by its ref ID (which is the path). */
814
- function findResourceByRef(resources, refId) {
815
- return resources.get(refId);
1200
+ //#endregion
1201
+ //#region src/run.ts
1202
+ /**
1203
+ * Run a composition class with the given input and return a plain-data result.
1204
+ *
1205
+ * This is the single entry point that bridges the composition author's class
1206
+ * with the runtime. It handles:
1207
+ * 1. Setting up internal context (DependencyGraph, EdgeCollector, ALS)
1208
+ * 2. Instantiating the Composition class
1209
+ * 3. Running the full pipeline (hydrate → resolve → sequence → diagnose → emit)
1210
+ * 4. Evaluating readiness per resource
1211
+ * 5. Returning a fully serializable CompositionResult
1212
+ *
1213
+ * The runtime never needs to access framework internals — everything it needs
1214
+ * is in the returned plain-data structure.
1215
+ */
1216
+ function runComposition(CompositionClass, input) {
1217
+ const observedComposed = new Map(Object.entries(input.observedComposed));
1218
+ const observedRequired = new Map(Object.entries(input.observedRequired));
1219
+ const graph = new DependencyGraph();
1220
+ const collector = new EdgeCollector();
1221
+ const pipelineContext = new Map(Object.entries(input.pipelineContext));
1222
+ const ctx = {
1223
+ xr: input.xr,
1224
+ pipelineContext,
1225
+ requiredResources: observedRequired,
1226
+ graph,
1227
+ collector
1228
+ };
1229
+ const state = runPipeline({
1230
+ composition: compositionStorage.run(ctx, () => new CompositionClass()),
1231
+ observedComposed,
1232
+ observedRequired
1233
+ });
1234
+ const resources = state.emitted.map((emitted) => {
1235
+ const allChecks = [...emitted.readyChecks, ...DEFAULT_CHECKS];
1236
+ const observed = observedComposed.get(`Composition/${emitted.name}`);
1237
+ const ready = emitted.autoReady ? evaluateReadiness(allChecks, observed) : true;
1238
+ return {
1239
+ name: emitted.name,
1240
+ document: emitted.document,
1241
+ ready
1242
+ };
1243
+ });
1244
+ const externalResources = [];
1245
+ for (const resource of state.resources) {
1246
+ if (!isExternal(resource)) continue;
1247
+ const ref = getExternalRef(resource);
1248
+ if (!ref || typeof ref.name !== "string") continue;
1249
+ if (ref.name.startsWith("__pending__")) continue;
1250
+ externalResources.push({
1251
+ refKey: ref.refKey,
1252
+ apiVersion: ref.apiVersion,
1253
+ kind: ref.kind,
1254
+ name: ref.name,
1255
+ ...ref.namespace ? { namespace: ref.namespace } : {}
1256
+ });
1257
+ }
1258
+ return {
1259
+ resources,
1260
+ externalResources,
1261
+ xrStatus: state.xrStatusPatches,
1262
+ diagnostics: state.diagnostics
1263
+ };
816
1264
  }
817
1265
  //#endregion
818
- export { Composition, Construct, DependencyCollector, DependencyGraph, IS_TRACKED, Resource, TRACKING_META, UNRESOLVED, computeRefKey, createTrackedProxy, getReadyCondition, getTrackingMeta, isResourceReady, isTracked, resolveSequencing };
1266
+ export { Composition, Construct, DEFAULT_CHECKS, DependencyGraph, EdgeCollector, Pending, Resource, compositionStorage, createPrimitiveReadProxy, createReadProxy, createWriteProxy, diagnose, emit, evaluateReadiness, getCompositionContext, getDesiredDocument, getExternalRef, getLogger, getObservedDocument, getReadProxyMeta, getReadyChecks, getResourceInternals, getResourceRef, getXrDesiredStatus, hydrate, hydrateObserved, isExternal, isReadProxy, resolve, runComposition, runPipeline, sequence, withLogger };
819
1267
 
820
1268
  //# sourceMappingURL=index.mjs.map