@xplane/core 0.9.2 → 0.10.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 ADDED
@@ -0,0 +1,709 @@
1
+ import { Construct, Construct as Construct$1 } from "constructs";
2
+ //#region src/tracking/dependency-graph.ts
3
+ /**
4
+ * Directed acyclic graph of resource dependencies.
5
+ * Supports topological sorting to determine resource creation order
6
+ * and cycle detection to surface configuration errors.
7
+ */
8
+ 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 */
12
+ _resources = /* @__PURE__ */ new Map();
13
+ /** raw edges for introspection */
14
+ _edges = [];
15
+ /** Register a resource node in the graph. */
16
+ addResource(ref) {
17
+ this._resources.set(ref.id, ref);
18
+ if (!this._deps.has(ref.id)) this._deps.set(ref.id, /* @__PURE__ */ new Set());
19
+ }
20
+ /** Add dependency edges from the collector. */
21
+ 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
+ }
29
+ }
30
+ /** Add an explicit dependency: `dependent` depends on `dependency`. */
31
+ addExplicitDependency(dependent, dependency) {
32
+ this.addResource(dependent);
33
+ this.addResource(dependency);
34
+ const deps = this._deps.get(dependent.id);
35
+ if (deps) deps.add(dependency.id);
36
+ }
37
+ /** Get all resource IDs that `resourceId` directly depends on. */
38
+ getDependencies(resourceId) {
39
+ return this._deps.get(resourceId) ?? /* @__PURE__ */ new Set();
40
+ }
41
+ /** Get all registered resource IDs. */
42
+ get resourceIds() {
43
+ return [...this._resources.keys()];
44
+ }
45
+ /** Get all raw edges. */
46
+ get edges() {
47
+ return this._edges;
48
+ }
49
+ /**
50
+ * Returns resource IDs in topological order (dependencies first).
51
+ * Throws if the graph contains cycles.
52
+ */
53
+ topologicalSort() {
54
+ const visited = /* @__PURE__ */ new Set();
55
+ const visiting = /* @__PURE__ */ new Set();
56
+ const sorted = [];
57
+ 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
+ }
63
+ visiting.add(id);
64
+ const deps = this._deps.get(id);
65
+ if (deps) for (const depId of deps) visit(depId);
66
+ visiting.delete(id);
67
+ visited.add(id);
68
+ sorted.push(id);
69
+ };
70
+ for (const id of this._resources.keys()) visit(id);
71
+ return sorted;
72
+ }
73
+ };
74
+ //#endregion
75
+ //#region src/tracking/types.ts
76
+ /**
77
+ * Symbols used to access tracking metadata on proxy-wrapped values.
78
+ * These are not enumerable and won't leak into serialized output.
79
+ */
80
+ const TRACKING_META = Symbol.for("xplane.tracking.meta");
81
+ const IS_TRACKED = Symbol.for("xplane.tracking.isTracked");
82
+ //#endregion
83
+ //#region src/tracking/proxy.ts
84
+ /**
85
+ * Registry that collects dependency edges discovered during proxy access.
86
+ * Shared across all tracked values within a single composition run.
87
+ */
88
+ var DependencyCollector = class {
89
+ _edges = [];
90
+ addEdge(edge) {
91
+ 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
+ }
93
+ get edges() {
94
+ return this._edges;
95
+ }
96
+ clear() {
97
+ this._edges.length = 0;
98
+ }
99
+ };
100
+ /**
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.
119
+ *
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
+ };
130
+ return new Proxy(target, {
131
+ 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
+ if (typeof prop === "symbol") return Reflect.get(obj, prop, receiver);
141
+ 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
+ });
151
+ Reflect.set(obj, prop, wrapped);
152
+ return wrapped;
153
+ }
154
+ if (existing !== void 0 || prop in obj) return existing;
155
+ if (opts.observed) return createTrackedProxy({}, {
156
+ owner: opts.owner,
157
+ path: opts.path ? `${opts.path}.${prop}` : String(prop),
158
+ observed: true,
159
+ collector: opts.collector
160
+ });
161
+ const wrapped = createTrackedProxy({}, {
162
+ owner: opts.owner,
163
+ path: opts.path ? `${opts.path}.${prop}` : String(prop),
164
+ observed: false,
165
+ collector: opts.collector
166
+ });
167
+ Reflect.set(obj, prop, wrapped);
168
+ return wrapped;
169
+ },
170
+ set(obj, prop, value) {
171
+ if (typeof prop === "symbol") return Reflect.set(obj, prop, value);
172
+ const targetPath = opts.path ? `${opts.path}.${prop}` : String(prop);
173
+ if (isTracked(value)) {
174
+ const sourceMeta = getTrackingMeta(value);
175
+ if (sourceMeta && sourceMeta.owner.id === "__xr__") {
176
+ const concrete = resolveTrackedValue(value);
177
+ return Reflect.set(obj, prop, concrete === UNRESOLVED ? void 0 : concrete);
178
+ }
179
+ if (sourceMeta && sourceMeta.owner.id !== opts.owner.id) opts.collector.addEdge({
180
+ from: sourceMeta.owner,
181
+ fromPath: sourceMeta.path,
182
+ to: opts.owner,
183
+ toPath: targetPath
184
+ });
185
+ const concrete = resolveTrackedValue(value);
186
+ return Reflect.set(obj, prop, concrete);
187
+ }
188
+ if (typeof value === "object" && value !== null && !isTracked(value)) {
189
+ const wrapped = createTrackedProxy(value, {
190
+ owner: opts.owner,
191
+ path: targetPath,
192
+ observed: opts.observed,
193
+ collector: opts.collector
194
+ });
195
+ return Reflect.set(obj, prop, wrapped);
196
+ }
197
+ return Reflect.set(obj, prop, value);
198
+ },
199
+ ownKeys(obj) {
200
+ return Reflect.ownKeys(obj).filter((k) => typeof k === "string");
201
+ },
202
+ getOwnPropertyDescriptor(obj, prop) {
203
+ const desc = Reflect.getOwnPropertyDescriptor(obj, prop);
204
+ if (desc) return {
205
+ ...desc,
206
+ configurable: true,
207
+ enumerable: true
208
+ };
209
+ if (opts.observed && typeof prop === "string") return {
210
+ configurable: true,
211
+ enumerable: true,
212
+ writable: true,
213
+ value: void 0
214
+ };
215
+ },
216
+ has(obj, prop) {
217
+ if (prop === IS_TRACKED || prop === TRACKING_META) return true;
218
+ return Reflect.has(obj, prop);
219
+ }
220
+ });
221
+ }
222
+ /**
223
+ * Sentinel value used when a tracked reference cannot be resolved yet
224
+ * (the source resource hasn't been observed).
225
+ */
226
+ const UNRESOLVED = Symbol.for("xplane.unresolved");
227
+ /**
228
+ * Attempts to extract a concrete (non-proxy) value from a tracked value.
229
+ * Returns UNRESOLVED if the tracked value points to an empty observed path.
230
+ */
231
+ function resolveTrackedValue(tracked) {
232
+ if (!isTracked(tracked)) return tracked;
233
+ const obj = unwrapProxy(tracked);
234
+ const keys = Object.keys(obj);
235
+ const meta = getTrackingMeta(tracked);
236
+ if (keys.length === 0 && meta?.observed) return UNRESOLVED;
237
+ return obj;
238
+ }
239
+ /**
240
+ * Strips the proxy layer and returns the raw underlying object.
241
+ */
242
+ function unwrapProxy(tracked) {
243
+ const result = {};
244
+ for (const key of Object.keys(tracked)) result[key] = tracked[key];
245
+ return result;
246
+ }
247
+ //#endregion
248
+ //#region src/core/construct.ts
249
+ /**
250
+ * Context keys used to propagate tracking infrastructure through the construct tree.
251
+ * Set by Composition (root), read by Resource and other constructs.
252
+ * @internal
253
+ */
254
+ const CONTEXT_COLLECTOR = "xplane:collector";
255
+ const CONTEXT_GRAPH = "xplane:graph";
256
+ /** Raw XR name and namespace stored at composition root for use by uniqueName. */
257
+ const CONTEXT_XR_META = "xplane:xr-meta";
258
+ //#endregion
259
+ //#region src/core/composition.ts
260
+ /**
261
+ * A Composition is the root Construct for a Crossplane composition function.
262
+ * Like CDK's `App` or cdk8s's `Chart`, it is the root of the construct tree.
263
+ * Resources and constructs are created in the constructor.
264
+ *
265
+ * Usage:
266
+ * ```ts
267
+ * class MyComposition extends Composition {
268
+ * constructor() {
269
+ * super();
270
+ * const vpc = new aws.ec2.VPC(this, 'vpc', { ... });
271
+ * const subnet = new aws.ec2.Subnet(this, 'subnet', {
272
+ * spec: { forProvider: { vpcId: vpc.status.atProvider.vpcId } }
273
+ * });
274
+ * }
275
+ * }
276
+ * ```
277
+ */
278
+ var Composition = class Composition extends Construct$1 {
279
+ /**
280
+ * Pending XR data, set by the framework before instantiation.
281
+ * @internal
282
+ */
283
+ static _pendingXR;
284
+ /**
285
+ * Pending environment data, set by the framework before instantiation.
286
+ * Populated from the Crossplane context key `apiextensions.crossplane.io/environment`.
287
+ * @internal
288
+ */
289
+ static _pendingEnvironment;
290
+ /** The composite resource (XR) — proxy-wrapped for tracking. */
291
+ xr;
292
+ /** Environment data from function-environment-configs or other pipeline steps. */
293
+ environment;
294
+ /** Raw name from the XR metadata (not proxy-tracked). */
295
+ xrName;
296
+ /** Raw namespace from the XR metadata (not proxy-tracked). */
297
+ xrNamespace;
298
+ /** Dependency collector shared across all resources. */
299
+ collector;
300
+ /** Dependency graph built during compose(). */
301
+ graph;
302
+ /** Registered status output function. @internal */
303
+ _statusFn;
304
+ constructor() {
305
+ super(void 0, "");
306
+ this.collector = new DependencyCollector();
307
+ this.graph = new DependencyGraph();
308
+ this.node.setContext(CONTEXT_COLLECTOR, this.collector);
309
+ this.node.setContext(CONTEXT_GRAPH, this.graph);
310
+ const xrData = Composition._pendingXR ?? {};
311
+ Composition._pendingXR = void 0;
312
+ const envData = Composition._pendingEnvironment ?? {};
313
+ Composition._pendingEnvironment = void 0;
314
+ const xrMeta = xrData.metadata ?? {};
315
+ this.xrName = typeof xrMeta.name === "string" ? xrMeta.name : void 0;
316
+ this.xrNamespace = typeof xrMeta.namespace === "string" ? xrMeta.namespace : void 0;
317
+ this.node.setContext(CONTEXT_XR_META, {
318
+ name: this.xrName,
319
+ namespace: this.xrNamespace
320
+ });
321
+ this.xr = createTrackedProxy(xrData, {
322
+ owner: { id: "__xr__" },
323
+ path: "",
324
+ observed: true,
325
+ collector: this.collector
326
+ });
327
+ this.environment = envData;
328
+ }
329
+ /**
330
+ * Register a function that computes the desired XR status output.
331
+ *
332
+ * The function is called by the framework **after** observed state has been
333
+ * fed into all resources, so `resource.observed` contains real data.
334
+ *
335
+ * @example
336
+ * ```ts
337
+ * this.setStatusOutput(() => ({
338
+ * config: {
339
+ * projectHostedZoneId: hostedZone.observed?.status?.atProvider?.id,
340
+ * },
341
+ * }));
342
+ * ```
343
+ */
344
+ setStatusOutput(fn) {
345
+ this._statusFn = fn;
346
+ }
347
+ /**
348
+ * Compute and return the desired status output.
349
+ * Returns an empty object if no status function was registered.
350
+ * @internal
351
+ */
352
+ computeStatusOutput() {
353
+ return this._statusFn?.() ?? {};
354
+ }
355
+ /**
356
+ * Walk up the construct tree and return the root Composition.
357
+ * Throws if the scope is not within a Composition.
358
+ */
359
+ static of(scope) {
360
+ let current = scope;
361
+ while (current !== void 0) {
362
+ if (current instanceof Composition) return current;
363
+ current = current.node.scope;
364
+ }
365
+ throw new Error("No Composition found in the scope chain. Ensure constructs are created within a Composition.");
366
+ }
367
+ /** Get all registered resources keyed by construct path. */
368
+ get resources() {
369
+ const map = /* @__PURE__ */ new Map();
370
+ for (const construct of this.node.findAll()) if (isResource(construct)) map.set(construct.node.path, construct);
371
+ return map;
372
+ }
373
+ };
374
+ /**
375
+ * Type guard for Resource — avoids circular import by checking for
376
+ * characteristic properties rather than instanceof.
377
+ */
378
+ function isResource(construct) {
379
+ return construct !== null && typeof construct === "object" && "apiVersion" in construct && "kind" in construct && "resourceRef" in construct;
380
+ }
381
+ //#endregion
382
+ //#region src/core/resource.ts
383
+ /**
384
+ * A Construct that represents a single Crossplane managed/composed resource.
385
+ *
386
+ * The `spec` and `status` properties are proxy-wrapped for automatic
387
+ * dependency tracking. Assigning a value from another resource's status
388
+ * to this resource's spec automatically records a dependency edge.
389
+ */
390
+ var Resource = class extends Construct$1 {
391
+ apiVersion;
392
+ kind;
393
+ resourceRef;
394
+ /** Proxy-wrapped desired spec — writes are tracked. */
395
+ spec;
396
+ /** Proxy-wrapped observed status — reads create dependency tracking. */
397
+ status;
398
+ /** Proxy-wrapped desired metadata. */
399
+ metadata;
400
+ /** Whether auto-ready is enabled for this resource. */
401
+ autoReady;
402
+ /** Extra top-level fields (e.g. data/stringData for Secret). Not proxy-tracked. */
403
+ _extra;
404
+ /** Observed state populated by the bridge before construction. */
405
+ _observed;
406
+ /** Backing object for the status proxy — populated by setObserved(). */
407
+ _statusTarget;
408
+ /** Backing object for the metadata proxy — populated by setObserved(). */
409
+ _metaTarget;
410
+ /** Keys the user explicitly declared in constructor metadata props. */
411
+ _desiredMetaKeys;
412
+ /** Explicit dependency refs. */
413
+ _explicitDeps = [];
414
+ /** @internal */
415
+ _graph;
416
+ constructor(scope, id, props, options) {
417
+ super(scope, id);
418
+ this.apiVersion = props.apiVersion;
419
+ this.kind = props.kind;
420
+ this.autoReady = options?.autoReady ?? true;
421
+ const collector = this.node.tryGetContext(CONTEXT_COLLECTOR);
422
+ const graph = this.node.tryGetContext(CONTEXT_GRAPH);
423
+ if (!collector || !graph) throw new Error("Resource must be created within a Composition tree");
424
+ this.resourceRef = { id: this.node.path };
425
+ graph.addResource(this.resourceRef);
426
+ const KNOWN_KEYS = new Set([
427
+ "apiVersion",
428
+ "kind",
429
+ "metadata",
430
+ "spec"
431
+ ]);
432
+ this._extra = {};
433
+ for (const [k, v] of Object.entries(props)) if (!KNOWN_KEYS.has(k)) this._extra[k] = v;
434
+ const specTarget = props.spec ?? {};
435
+ resolveTrackedRefs(specTarget, this.resourceRef, "spec", collector);
436
+ this.spec = createTrackedProxy(specTarget, {
437
+ owner: this.resourceRef,
438
+ path: "spec",
439
+ observed: false,
440
+ collector
441
+ });
442
+ this._metaTarget = props.metadata ?? {};
443
+ this._desiredMetaKeys = new Set(Object.keys(this._metaTarget));
444
+ this.metadata = createTrackedProxy(this._metaTarget, {
445
+ owner: this.resourceRef,
446
+ path: "metadata",
447
+ observed: true,
448
+ collector
449
+ });
450
+ this._statusTarget = {};
451
+ this.status = createTrackedProxy(this._statusTarget, {
452
+ owner: this.resourceRef,
453
+ path: "status",
454
+ observed: true,
455
+ collector
456
+ });
457
+ this._graph = graph;
458
+ }
459
+ /** Fully qualified path in the construct tree. */
460
+ get path() {
461
+ return this.node.path;
462
+ }
463
+ /** Add an explicit dependency on another resource. */
464
+ addDependency(other) {
465
+ this._explicitDeps.push(other.resourceRef);
466
+ this._graph.addExplicitDependency(this.resourceRef, other.resourceRef);
467
+ }
468
+ /** Get explicit dependency refs. */
469
+ get explicitDependencies() {
470
+ return this._explicitDeps;
471
+ }
472
+ /** Set observed state (called by the bridge before compose). */
473
+ setObserved(observed) {
474
+ this._observed = observed;
475
+ for (const key of Object.keys(this._metaTarget)) this._desiredMetaKeys.add(key);
476
+ if (observed.metadata && typeof observed.metadata === "object") Object.assign(this._metaTarget, observed.metadata);
477
+ if (observed.status && typeof observed.status === "object") Object.assign(this._statusTarget, observed.status);
478
+ }
479
+ /** Get observed state. */
480
+ get observed() {
481
+ return this._observed;
482
+ }
483
+ /**
484
+ * Compute a unique name for a resource based on its construct node path,
485
+ * similar to `cdk.Names.uniqueResourceName`.
486
+ *
487
+ * The name is structured as:
488
+ * `[namespace-]claimName-PathSegments[-extra]-hash8`
489
+ *
490
+ * - XR namespace (if present) and XR name are always prepended.
491
+ * - Path segments (construct tree, root skipped) are appended next.
492
+ * - An optional `extra` string is appended after the path.
493
+ * - An 8-char hash of the full untruncated string is always appended for uniqueness.
494
+ * - Whitespace in each segment is stripped (CDK convention).
495
+ * - Disallowed characters are replaced by the separator; consecutive separators are collapsed.
496
+ * - The result is truncated to `maxLength` while keeping the hash suffix.
497
+ *
498
+ * @param scope - The construct whose node path is used.
499
+ * @param options - Optional tuning.
500
+ */
501
+ static uniqueName(scope, options = {}) {
502
+ const maxLength = options.maxLength ?? 63;
503
+ const separator = options.separator ?? "-";
504
+ const allowedPattern = options.allowedPattern ?? /[^a-zA-Z0-9]/g;
505
+ const escapedSep = separator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
506
+ const collapseRe = new RegExp(`${escapedSep}+`, "g");
507
+ const clean = (s) => s.replace(/\s+/g, "").replace(allowedPattern, separator).replace(collapseRe, separator).replace(new RegExp(`^${escapedSep}|${escapedSep}$`, "g"), "");
508
+ const xrMeta = scope.node.tryGetContext(CONTEXT_XR_META);
509
+ const xrName = xrMeta?.name;
510
+ const xrNamespace = xrMeta?.namespace;
511
+ const parts = [];
512
+ if (xrNamespace) parts.push(clean(xrNamespace));
513
+ if (xrName) parts.push(clean(xrName));
514
+ for (const s of scope.node.scopes.slice(1)) {
515
+ const c = clean(s.node.id);
516
+ if (c) parts.push(c);
517
+ }
518
+ if (options.extra) {
519
+ const c = clean(options.extra);
520
+ if (c) parts.push(c);
521
+ }
522
+ const full = parts.join(separator);
523
+ const hash = shortHash(full);
524
+ const withHash = `${full}${separator}${hash}`;
525
+ if (withHash.length <= maxLength) return withHash;
526
+ return `${full.slice(0, maxLength - hash.length - separator.length)}${separator}${hash}`;
527
+ }
528
+ /**
529
+ * Serialize to a plain Kubernetes resource object for the desired state.
530
+ * Strips proxy wrappers, UNRESOLVED sentinels, and server-managed metadata
531
+ * fields (uid, resourceVersion, etc.) that must not appear in desired state.
532
+ */
533
+ toDesired() {
534
+ const fullMeta = JSON.parse(JSON.stringify(this.metadata));
535
+ const desiredMeta = {};
536
+ for (const key of this._desiredMetaKeys) if (key in fullMeta) desiredMeta[key] = fullMeta[key];
537
+ const cleanMeta = stripUnresolved(desiredMeta);
538
+ const desired = {
539
+ ...this._extra,
540
+ apiVersion: this.apiVersion,
541
+ kind: this.kind,
542
+ metadata: cleanMeta,
543
+ spec: stripUnresolved(JSON.parse(JSON.stringify(this.spec)))
544
+ };
545
+ if (desired.spec && typeof desired.spec === "object" && Object.keys(desired.spec).length === 0) delete desired.spec;
546
+ return desired;
547
+ }
548
+ };
549
+ /**
550
+ * Produce an 8-character hex hash of a string using a simple djb2-style
551
+ * algorithm — no crypto dependency required.
552
+ */
553
+ function shortHash(s) {
554
+ let h = 5381;
555
+ for (let i = 0; i < s.length; i++) {
556
+ h = (h << 5) + h ^ s.charCodeAt(i);
557
+ h = h >>> 0;
558
+ }
559
+ return h.toString(16).padStart(8, "0");
560
+ }
561
+ /** Recursively remove UNRESOLVED sentinel values from an object. */
562
+ function stripUnresolved(obj) {
563
+ if (obj === null || obj === void 0) return obj;
564
+ if (typeof obj === "symbol" && obj === UNRESOLVED) return void 0;
565
+ if (Array.isArray(obj)) return obj.map(stripUnresolved);
566
+ if (typeof obj === "object") {
567
+ const result = {};
568
+ for (const [key, value] of Object.entries(obj)) {
569
+ const cleaned = stripUnresolved(value);
570
+ if (cleaned !== void 0) result[key] = cleaned;
571
+ }
572
+ return result;
573
+ }
574
+ return obj;
575
+ }
576
+ /**
577
+ * Recursively scan an object for tracked proxy values from other resources.
578
+ * For each one found, record a dependency edge and replace the value with
579
+ * the UNRESOLVED sentinel. This handles values passed via object literals
580
+ * in constructor props, which bypass the proxy's set trap.
581
+ */
582
+ function resolveTrackedRefs(obj, owner, basePath, collector) {
583
+ for (const [key, value] of Object.entries(obj)) {
584
+ const path = basePath ? `${basePath}.${key}` : key;
585
+ if (isTracked(value)) {
586
+ const sourceMeta = getTrackingMeta(value);
587
+ if (sourceMeta && sourceMeta.owner.id !== owner.id) {
588
+ collector.addEdge({
589
+ from: sourceMeta.owner,
590
+ fromPath: sourceMeta.path,
591
+ to: owner,
592
+ toPath: path
593
+ });
594
+ obj[key] = UNRESOLVED;
595
+ }
596
+ continue;
597
+ }
598
+ if (Array.isArray(value)) {
599
+ for (let i = 0; i < value.length; i++) {
600
+ const item = value[i];
601
+ if (isTracked(item)) {
602
+ const sourceMeta = getTrackingMeta(item);
603
+ if (sourceMeta && sourceMeta.owner.id !== owner.id) {
604
+ collector.addEdge({
605
+ from: sourceMeta.owner,
606
+ fromPath: sourceMeta.path,
607
+ to: owner,
608
+ toPath: `${path}[${i}]`
609
+ });
610
+ value[i] = UNRESOLVED;
611
+ }
612
+ } else if (typeof item === "object" && item !== null) resolveTrackedRefs(item, owner, `${path}[${i}]`, collector);
613
+ }
614
+ continue;
615
+ }
616
+ if (typeof value === "object" && value !== null) resolveTrackedRefs(value, owner, path, collector);
617
+ }
618
+ }
619
+ //#endregion
620
+ //#region src/ready/auto-ready.ts
621
+ /**
622
+ * Determines if a Crossplane managed resource is ready based on its
623
+ * observed status conditions.
624
+ *
625
+ * - If the resource has a `Ready: True` condition → ready.
626
+ * - If the resource has a `Ready: False` condition → not ready.
627
+ * - If the resource exists but has no `Ready` condition at all (e.g. Namespace,
628
+ * ProviderConfig) → considered ready (the resource exists and is functional).
629
+ * - If not yet observed → not ready.
630
+ */
631
+ function isResourceReady(observed) {
632
+ if (!observed) return false;
633
+ const conditions = observed.status?.conditions;
634
+ if (!Array.isArray(conditions) || conditions.length === 0) return true;
635
+ const readyCondition = conditions.find((c) => c.type === "Ready");
636
+ if (!readyCondition) return true;
637
+ return readyCondition.status === "True";
638
+ }
639
+ /**
640
+ * Gets the Ready condition from a resource, if present.
641
+ */
642
+ function getReadyCondition(observed) {
643
+ if (!observed?.status) return void 0;
644
+ const conditions = observed.status.conditions;
645
+ if (!Array.isArray(conditions)) return void 0;
646
+ return conditions.find((c) => c.type === "Ready");
647
+ }
648
+ //#endregion
649
+ //#region src/sequencing/resolver.ts
650
+ /**
651
+ * Resolves resource dependencies and determines which resources can be
652
+ * emitted in the current pass.
653
+ *
654
+ * Algorithm:
655
+ * 1. Topologically sort resources using the dependency graph.
656
+ * 2. For each resource (in order), check if upstream dependencies have
657
+ * resolved values in observed state.
658
+ * 3. If all deps resolved → emit. If any dep unresolved → block.
659
+ */
660
+ function resolveSequencing(resources, graph, observedResources) {
661
+ const order = graph.topologicalSort();
662
+ const emit = [];
663
+ const blocked = [];
664
+ for (const resourceId of order) {
665
+ const resource = findResourceByRef(resources, resourceId);
666
+ if (!resource) continue;
667
+ const deps = graph.getDependencies(resourceId);
668
+ let allDepsReady = true;
669
+ for (const depId of deps) {
670
+ const depResource = findResourceByRef(resources, depId);
671
+ if (!depResource) {
672
+ allDepsReady = false;
673
+ continue;
674
+ }
675
+ if (!observedResources.get(depResource.path)) allDepsReady = false;
676
+ }
677
+ if (allDepsReady && hasUnresolvedFields(resource)) allDepsReady = false;
678
+ if (allDepsReady) emit.push(resource);
679
+ else blocked.push(resource);
680
+ }
681
+ return {
682
+ emit,
683
+ blocked,
684
+ order
685
+ };
686
+ }
687
+ /**
688
+ * Check if a resource's desired state contains any UNRESOLVED sentinels.
689
+ * Uses the raw spec/metadata before stripping, so UNRESOLVED symbols are visible.
690
+ */
691
+ function hasUnresolvedFields(resource) {
692
+ return containsUnresolved(resource.spec) || containsUnresolved(resource.metadata);
693
+ }
694
+ /** Recursively check if an object contains UNRESOLVED sentinels. */
695
+ function containsUnresolved(obj) {
696
+ if (obj === UNRESOLVED) return true;
697
+ if (obj === null || obj === void 0) return false;
698
+ if (Array.isArray(obj)) return obj.some(containsUnresolved);
699
+ if (typeof obj === "object") return Object.values(obj).some(containsUnresolved);
700
+ return false;
701
+ }
702
+ /** Find a resource by its ref ID (which is the path). */
703
+ function findResourceByRef(resources, refId) {
704
+ return resources.get(refId);
705
+ }
706
+ //#endregion
707
+ export { Composition, Construct, DependencyCollector, DependencyGraph, IS_TRACKED, Resource, TRACKING_META, UNRESOLVED, createTrackedProxy, getReadyCondition, getTrackingMeta, isResourceReady, isTracked, resolveSequencing };
708
+
709
+ //# sourceMappingURL=index.mjs.map