@xplane/core 1.0.1 → 1.2.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.d.mts +124 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +329 -39
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -46,6 +46,30 @@ var Composition = class extends Construct$1 {
|
|
|
46
46
|
graph;
|
|
47
47
|
/** The edge collector accumulating dependency edges. */
|
|
48
48
|
collector;
|
|
49
|
+
/**
|
|
50
|
+
* When `true`, the runtime injects a structured `status.xplane` payload on
|
|
51
|
+
* the XR containing `emittedResources` and `blockedResources`. Defaults to
|
|
52
|
+
* `false` because writing this field requires the XRD's `openAPIV3Schema`
|
|
53
|
+
* to declare `status.xplane` (or `status` to allow unknown fields) — typed
|
|
54
|
+
* patches will otherwise be rejected by Crossplane.
|
|
55
|
+
*
|
|
56
|
+
* To enable, set in your composition's constructor:
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* this.emitXplaneStatus = true;
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* and add the following to your XRD's status schema:
|
|
63
|
+
*
|
|
64
|
+
* ```yaml
|
|
65
|
+
* status:
|
|
66
|
+
* properties:
|
|
67
|
+
* xplane:
|
|
68
|
+
* type: object
|
|
69
|
+
* x-kubernetes-preserve-unknown-fields: true
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
emitXplaneStatus = false;
|
|
49
73
|
constructor() {
|
|
50
74
|
super(void 0, "Composition");
|
|
51
75
|
const ctx = getCompositionContext();
|
|
@@ -219,6 +243,114 @@ var DependencyGraph = class {
|
|
|
219
243
|
}
|
|
220
244
|
};
|
|
221
245
|
//#endregion
|
|
246
|
+
//#region src/tracking/types.ts
|
|
247
|
+
/**
|
|
248
|
+
* A Pending marker stored in a desired document when a ReadProxy value
|
|
249
|
+
* is assigned. Carries full source info so the resolve phase knows
|
|
250
|
+
* where to look for the concrete value.
|
|
251
|
+
*/
|
|
252
|
+
const PENDING_TAG = Symbol.for("xplane.pending");
|
|
253
|
+
var Pending = class {
|
|
254
|
+
source;
|
|
255
|
+
path;
|
|
256
|
+
static TAG = PENDING_TAG;
|
|
257
|
+
[PENDING_TAG] = true;
|
|
258
|
+
constructor(source, path) {
|
|
259
|
+
this.source = source;
|
|
260
|
+
this.path = path;
|
|
261
|
+
}
|
|
262
|
+
static is(value) {
|
|
263
|
+
return typeof value === "object" && value !== null && value[PENDING_TAG] === true;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* A Pending-like marker for strings produced by template literals that
|
|
268
|
+
* interpolate one or more unresolved ReadProxy values.
|
|
269
|
+
*
|
|
270
|
+
* Holds the template structure so the resolve phase can reconstruct the
|
|
271
|
+
* final string once all dependency slots are available.
|
|
272
|
+
*
|
|
273
|
+
* Invariant: parts.length === slots.length + 1
|
|
274
|
+
* result = parts[0] + resolved[0] + parts[1] + … + resolved[n-1] + parts[n]
|
|
275
|
+
*/
|
|
276
|
+
const PENDING_TEMPLATE_TAG = Symbol.for("xplane.pendingTemplate");
|
|
277
|
+
var PendingTemplate = class {
|
|
278
|
+
parts;
|
|
279
|
+
slots;
|
|
280
|
+
static TAG = PENDING_TEMPLATE_TAG;
|
|
281
|
+
[PENDING_TEMPLATE_TAG] = true;
|
|
282
|
+
constructor(parts, slots) {
|
|
283
|
+
this.parts = parts;
|
|
284
|
+
this.slots = slots;
|
|
285
|
+
}
|
|
286
|
+
static is(value) {
|
|
287
|
+
return typeof value === "object" && value !== null && value[PENDING_TEMPLATE_TAG] === true;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
//#endregion
|
|
291
|
+
//#region src/tracking/token-registry.ts
|
|
292
|
+
/**
|
|
293
|
+
* Per-run AsyncLocalStorage for the template-literal token registry.
|
|
294
|
+
* Kept separate from compositionStorage (core/) to avoid circular deps.
|
|
295
|
+
*/
|
|
296
|
+
const tokenRegistryStorage = new AsyncLocalStorage();
|
|
297
|
+
function createTokenRegistry() {
|
|
298
|
+
return {
|
|
299
|
+
byToken: /* @__PURE__ */ new Map(),
|
|
300
|
+
byKey: /* @__PURE__ */ new Map(),
|
|
301
|
+
counter: 0
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Get or create a stable token for a given (owner, path) pair.
|
|
306
|
+
* Returns null when no registry is active (outside of a composition run).
|
|
307
|
+
*/
|
|
308
|
+
function getOrCreateToken(owner, path) {
|
|
309
|
+
const registry = tokenRegistryStorage.getStore();
|
|
310
|
+
if (!registry) return null;
|
|
311
|
+
const key = `${owner.id}\0${path}`;
|
|
312
|
+
const existing = registry.byKey.get(key);
|
|
313
|
+
if (existing !== void 0) return existing;
|
|
314
|
+
const token = `__pending__tpl_${registry.counter++}__`;
|
|
315
|
+
registry.byToken.set(token, {
|
|
316
|
+
owner,
|
|
317
|
+
path
|
|
318
|
+
});
|
|
319
|
+
registry.byKey.set(key, token);
|
|
320
|
+
return token;
|
|
321
|
+
}
|
|
322
|
+
function lookupToken(token) {
|
|
323
|
+
return tokenRegistryStorage.getStore()?.byToken.get(token);
|
|
324
|
+
}
|
|
325
|
+
const TEMPLATE_TOKEN_RE = /__pending__tpl_\d+__/g;
|
|
326
|
+
/**
|
|
327
|
+
* Scan a string for pending template tokens. If any are found, calls
|
|
328
|
+
* `onSlot` for each registered token and returns a PendingTemplate.
|
|
329
|
+
* Returns the original string if no tokens are found or none are registered.
|
|
330
|
+
*/
|
|
331
|
+
function processStringValue(value, onSlot) {
|
|
332
|
+
const parts = [];
|
|
333
|
+
const slots = [];
|
|
334
|
+
let lastIndex = 0;
|
|
335
|
+
let hasSlots = false;
|
|
336
|
+
TEMPLATE_TOKEN_RE.lastIndex = 0;
|
|
337
|
+
for (const match of value.matchAll(TEMPLATE_TOKEN_RE)) {
|
|
338
|
+
const meta = lookupToken(match[0]);
|
|
339
|
+
if (!meta) continue;
|
|
340
|
+
hasSlots = true;
|
|
341
|
+
parts.push(value.slice(lastIndex, match.index));
|
|
342
|
+
slots.push({
|
|
343
|
+
source: meta.owner,
|
|
344
|
+
path: meta.path
|
|
345
|
+
});
|
|
346
|
+
onSlot(meta);
|
|
347
|
+
lastIndex = match.index + match[0].length;
|
|
348
|
+
}
|
|
349
|
+
if (!hasSlots) return value;
|
|
350
|
+
parts.push(value.slice(lastIndex));
|
|
351
|
+
return new PendingTemplate(parts, slots);
|
|
352
|
+
}
|
|
353
|
+
//#endregion
|
|
222
354
|
//#region src/tracking/read-proxy.ts
|
|
223
355
|
/**
|
|
224
356
|
* WeakMap storing metadata for ReadProxy instances.
|
|
@@ -277,13 +409,15 @@ function createReadProxy(target, owner, basePath) {
|
|
|
277
409
|
* coerced to a primitive.
|
|
278
410
|
*/
|
|
279
411
|
function createLeafReadProxy(owner, path) {
|
|
280
|
-
const
|
|
412
|
+
const target = Object.create(null);
|
|
413
|
+
const getToken = () => getOrCreateToken(owner, path) ?? `__pending__${owner.id}__${path}`;
|
|
414
|
+
const proxy = new Proxy(target, {
|
|
281
415
|
get(_obj, prop) {
|
|
282
416
|
if (prop === READ_PROXY_TAG) return true;
|
|
283
|
-
if (prop === Symbol.toPrimitive) return () =>
|
|
417
|
+
if (prop === Symbol.toPrimitive) return () => getToken();
|
|
284
418
|
if (typeof prop === "symbol") return void 0;
|
|
285
419
|
if (prop === "toJSON") return () => void 0;
|
|
286
|
-
if (prop === "toString") return () =>
|
|
420
|
+
if (prop === "toString") return () => getToken();
|
|
287
421
|
if (prop === "valueOf") return () => proxy;
|
|
288
422
|
return createLeafReadProxy(owner, `${path}.${String(prop)}`);
|
|
289
423
|
},
|
|
@@ -326,27 +460,6 @@ function createPrimitiveReadProxy(value, owner, path) {
|
|
|
326
460
|
return proxy;
|
|
327
461
|
}
|
|
328
462
|
//#endregion
|
|
329
|
-
//#region src/tracking/types.ts
|
|
330
|
-
/**
|
|
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.
|
|
334
|
-
*/
|
|
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
|
-
};
|
|
349
|
-
//#endregion
|
|
350
463
|
//#region src/tracking/write-proxy.ts
|
|
351
464
|
/**
|
|
352
465
|
* Collector that accumulates dependency edges discovered during
|
|
@@ -422,6 +535,17 @@ function createWriteProxy(target, opts) {
|
|
|
422
535
|
return Reflect.set(obj, prop, new Pending(meta.owner, meta.path));
|
|
423
536
|
}
|
|
424
537
|
}
|
|
538
|
+
if (typeof value === "string") {
|
|
539
|
+
const processed = processStringValue(value, (meta) => {
|
|
540
|
+
collector.add({
|
|
541
|
+
from: meta.owner,
|
|
542
|
+
fromPath: meta.path,
|
|
543
|
+
to: owner,
|
|
544
|
+
toPath: targetPath
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
return Reflect.set(obj, prop, processed);
|
|
548
|
+
}
|
|
425
549
|
if (typeof value === "object" && value !== null && !Pending.is(value)) {
|
|
426
550
|
const processed = deepProcessValue(value, owner, targetPath, collector);
|
|
427
551
|
return Reflect.set(obj, prop, processed);
|
|
@@ -453,6 +577,14 @@ function tryExtractPrimitive$2(proxy) {
|
|
|
453
577
|
*/
|
|
454
578
|
function deepProcessValue(value, owner, basePath, collector) {
|
|
455
579
|
if (value === null || value === void 0) return value;
|
|
580
|
+
if (typeof value === "string") return processStringValue(value, (meta) => {
|
|
581
|
+
collector.add({
|
|
582
|
+
from: meta.owner,
|
|
583
|
+
fromPath: meta.path,
|
|
584
|
+
to: owner,
|
|
585
|
+
toPath: basePath
|
|
586
|
+
});
|
|
587
|
+
});
|
|
456
588
|
if (typeof value !== "object") return value;
|
|
457
589
|
if (isReadProxy(value)) {
|
|
458
590
|
const meta = getReadProxyMeta(value);
|
|
@@ -517,8 +649,14 @@ var Resource = class Resource extends Construct$1 {
|
|
|
517
649
|
collector
|
|
518
650
|
};
|
|
519
651
|
internals.set(this, internal);
|
|
652
|
+
const ctx = compositionStorage.getStore();
|
|
653
|
+
if (ctx) {
|
|
654
|
+
const observed = ctx.observedComposed.get(this.node.path);
|
|
655
|
+
if (observed) Object.assign(internal.observed, observed);
|
|
656
|
+
}
|
|
520
657
|
const proxy = createResourceProxy(this, internal);
|
|
521
658
|
scope.node._children[this.node.id] = proxy;
|
|
659
|
+
this.node.host = proxy;
|
|
522
660
|
return proxy;
|
|
523
661
|
}
|
|
524
662
|
/**
|
|
@@ -581,6 +719,33 @@ var Resource = class Resource extends Construct$1 {
|
|
|
581
719
|
if (withHash.length <= maxLength) return withHash;
|
|
582
720
|
return `${full.slice(0, maxLength - hash.length - separator.length)}${separator}${hash}`;
|
|
583
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* Like {@link uniqueName} but produces names compliant with RFC 1123 DNS labels:
|
|
724
|
+
* lowercase alphanumeric characters and hyphens only, starting and ending with
|
|
725
|
+
* an alphanumeric character. Suitable for use as Kubernetes resource names.
|
|
726
|
+
*/
|
|
727
|
+
static uniqueNameRfc1123(scope, options = {}) {
|
|
728
|
+
const maxLength = options.maxLength ?? 63;
|
|
729
|
+
const clean = (s) => s.toLowerCase().replace(/\s+/g, "").replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
730
|
+
const xrMeta = scope.node.tryGetContext("xplane:xr-meta");
|
|
731
|
+
const parts = [];
|
|
732
|
+
if (xrMeta?.namespace) parts.push(clean(xrMeta.namespace));
|
|
733
|
+
if (xrMeta?.name) parts.push(clean(xrMeta.name));
|
|
734
|
+
for (const s of scope.node.scopes.slice(1)) {
|
|
735
|
+
const c = clean(s.node.id);
|
|
736
|
+
if (c) parts.push(c);
|
|
737
|
+
}
|
|
738
|
+
if (options.extra) {
|
|
739
|
+
const c = clean(options.extra);
|
|
740
|
+
if (c) parts.push(c);
|
|
741
|
+
}
|
|
742
|
+
const full = parts.join("-");
|
|
743
|
+
const hash = shortHash(full);
|
|
744
|
+
const withHash = `${full}-${hash}`;
|
|
745
|
+
if (withHash.length <= maxLength) return withHash;
|
|
746
|
+
const trimmedPrefix = full.slice(0, maxLength - hash.length - 1).replace(/-+$/, "");
|
|
747
|
+
return trimmedPrefix ? `${trimmedPrefix}-${hash}` : hash;
|
|
748
|
+
}
|
|
584
749
|
};
|
|
585
750
|
function getResourceInternals(resource) {
|
|
586
751
|
const internal = internals.get(resource);
|
|
@@ -642,6 +807,14 @@ function processDesiredProps(props, owner, collector) {
|
|
|
642
807
|
}
|
|
643
808
|
function processValue(value, owner, path, collector) {
|
|
644
809
|
if (value === null || value === void 0) return value;
|
|
810
|
+
if (typeof value === "string") return processStringValue(value, (meta) => {
|
|
811
|
+
collector.add({
|
|
812
|
+
from: meta.owner,
|
|
813
|
+
fromPath: meta.path,
|
|
814
|
+
to: owner,
|
|
815
|
+
toPath: path
|
|
816
|
+
});
|
|
817
|
+
});
|
|
645
818
|
if (typeof value !== "object") return value;
|
|
646
819
|
if (isReadProxy(value)) {
|
|
647
820
|
const meta = getReadProxyMeta(value);
|
|
@@ -805,12 +978,12 @@ function diagnose(state) {
|
|
|
805
978
|
if (!ref) continue;
|
|
806
979
|
const observed = getObservedDocument(resource);
|
|
807
980
|
if (Object.keys(observed).length > 0) continue;
|
|
808
|
-
|
|
981
|
+
if (typeof ref.name !== "string" || ref.name.startsWith("__pending__")) continue;
|
|
809
982
|
const nsDisplay = ref.namespace ? ` in namespace '${ref.namespace}'` : "";
|
|
810
983
|
diagnostics.push({
|
|
811
984
|
resource: getResourceRef(resource).id,
|
|
812
985
|
reason: "not-found",
|
|
813
|
-
detail: `External resource ${ref.apiVersion}/${ref.kind} '${
|
|
986
|
+
detail: `External resource ${ref.apiVersion}/${ref.kind} '${ref.name}'${nsDisplay} was required but not found by Crossplane`
|
|
814
987
|
});
|
|
815
988
|
}
|
|
816
989
|
const pendingDiagnostics = [];
|
|
@@ -874,22 +1047,39 @@ function findPendingPaths(obj, basePath) {
|
|
|
874
1047
|
* Kubernetes resource document ready for Crossplane.
|
|
875
1048
|
*
|
|
876
1049
|
* Also extracts the XR desired status from this.xr.status assignments.
|
|
1050
|
+
*
|
|
1051
|
+
* For resources classified as 'blocked' that have previously-observed state,
|
|
1052
|
+
* emits the observed document as-is (marked `preserved: true`) so that
|
|
1053
|
+
* Crossplane does not delete them while their dependencies are still resolving.
|
|
877
1054
|
*/
|
|
878
1055
|
function emit(state) {
|
|
879
1056
|
const emitted = [];
|
|
880
1057
|
for (const resource of state.resources) {
|
|
881
1058
|
if (isExternal(resource)) continue;
|
|
882
1059
|
const ref = getResourceRef(resource);
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1060
|
+
const classification = state.classification.get(ref.id);
|
|
1061
|
+
if (classification === "emit") {
|
|
1062
|
+
const internal = getResourceInternals(resource);
|
|
1063
|
+
const desired = getDesiredDocument(resource);
|
|
1064
|
+
const name = ref.id.startsWith("Composition/") ? ref.id.slice(12) : ref.id;
|
|
1065
|
+
emitted.push({
|
|
1066
|
+
name,
|
|
1067
|
+
document: deepClean(desired),
|
|
1068
|
+
autoReady: internal.config.autoReady,
|
|
1069
|
+
readyChecks: getReadyChecks(resource)
|
|
1070
|
+
});
|
|
1071
|
+
} else if (classification === "blocked") {
|
|
1072
|
+
const observed = getObservedDocument(resource);
|
|
1073
|
+
if (Object.keys(observed).length === 0) continue;
|
|
1074
|
+
const name = ref.id.startsWith("Composition/") ? ref.id.slice(12) : ref.id;
|
|
1075
|
+
emitted.push({
|
|
1076
|
+
name,
|
|
1077
|
+
document: stripServerFields(deepClean(observed)),
|
|
1078
|
+
autoReady: false,
|
|
1079
|
+
readyChecks: [],
|
|
1080
|
+
preserved: true
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
893
1083
|
}
|
|
894
1084
|
const resourceById = new Map(state.resources.map((r) => [getResourceRef(r).id, r]));
|
|
895
1085
|
const xrStatusPatches = resolveXrStatus(getXrDesiredStatus(state.composition), resourceById);
|
|
@@ -911,12 +1101,43 @@ function deepClean(obj) {
|
|
|
911
1101
|
function cleanValue(value) {
|
|
912
1102
|
if (value === null || value === void 0) return value;
|
|
913
1103
|
if (typeof value !== "object") return value;
|
|
1104
|
+
if (PendingTemplate.is(value)) throw new Error(`PendingTemplate reached emit phase — resource should have been classified as blocked. Parts: ${JSON.stringify(value.parts)}`);
|
|
914
1105
|
if (Array.isArray(value)) return value.map(cleanValue);
|
|
915
1106
|
const result = {};
|
|
916
1107
|
for (const [key, val] of Object.entries(value)) result[key] = cleanValue(val);
|
|
917
1108
|
return result;
|
|
918
1109
|
}
|
|
919
1110
|
/**
|
|
1111
|
+
* Strip server-managed Kubernetes fields from an observed document before
|
|
1112
|
+
* emitting it as a preserved desired resource.
|
|
1113
|
+
*
|
|
1114
|
+
* These fields are set by the API server / controllers and must not appear
|
|
1115
|
+
* in the desired state — including them causes Crossplane to try propagating
|
|
1116
|
+
* them (e.g. `uid`) into XR resourceRefs fields that the XRD schema may not
|
|
1117
|
+
* declare, resulting in a typed-patch validation failure.
|
|
1118
|
+
*/
|
|
1119
|
+
const SERVER_MANAGED_METADATA = new Set([
|
|
1120
|
+
"uid",
|
|
1121
|
+
"resourceVersion",
|
|
1122
|
+
"generation",
|
|
1123
|
+
"creationTimestamp",
|
|
1124
|
+
"deletionTimestamp",
|
|
1125
|
+
"deletionGracePeriodSeconds",
|
|
1126
|
+
"managedFields",
|
|
1127
|
+
"ownerReferences",
|
|
1128
|
+
"selfLink"
|
|
1129
|
+
]);
|
|
1130
|
+
function stripServerFields(doc) {
|
|
1131
|
+
const result = { ...doc };
|
|
1132
|
+
delete result.status;
|
|
1133
|
+
if (result.metadata && typeof result.metadata === "object") {
|
|
1134
|
+
const meta = { ...result.metadata };
|
|
1135
|
+
for (const field of SERVER_MANAGED_METADATA) delete meta[field];
|
|
1136
|
+
result.metadata = meta;
|
|
1137
|
+
}
|
|
1138
|
+
return result;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
920
1141
|
* Resolve ReadProxy values in XR status using observed resource data.
|
|
921
1142
|
* This is needed because XR status is written at construction time (before hydration),
|
|
922
1143
|
* so read proxy references need to be resolved post-hydration.
|
|
@@ -1017,6 +1238,9 @@ function resolvePending(obj, resourceById) {
|
|
|
1017
1238
|
const resolved = getNestedValue(getObservedDocument(sourceResource), value.path);
|
|
1018
1239
|
if (resolved !== void 0) obj[key] = resolved;
|
|
1019
1240
|
}
|
|
1241
|
+
} else if (PendingTemplate.is(value)) {
|
|
1242
|
+
const resolved = resolvePendingTemplate(value, resourceById);
|
|
1243
|
+
if (resolved !== void 0) obj[key] = resolved;
|
|
1020
1244
|
} else if (Array.isArray(value)) resolveArray(value, resourceById);
|
|
1021
1245
|
else if (value !== null && typeof value === "object") resolvePending(value, resourceById);
|
|
1022
1246
|
}
|
|
@@ -1029,6 +1253,9 @@ function resolveArray(arr, resourceById) {
|
|
|
1029
1253
|
const resolved = getNestedValue(getObservedDocument(sourceResource), value.path);
|
|
1030
1254
|
if (resolved !== void 0) arr[i] = resolved;
|
|
1031
1255
|
}
|
|
1256
|
+
} else if (PendingTemplate.is(value)) {
|
|
1257
|
+
const resolved = resolvePendingTemplate(value, resourceById);
|
|
1258
|
+
if (resolved !== void 0) arr[i] = resolved;
|
|
1032
1259
|
} else if (Array.isArray(value)) resolveArray(value, resourceById);
|
|
1033
1260
|
else if (value !== null && typeof value === "object") resolvePending(value, resourceById);
|
|
1034
1261
|
}
|
|
@@ -1046,6 +1273,23 @@ function getNestedValue(obj, path) {
|
|
|
1046
1273
|
}
|
|
1047
1274
|
return current;
|
|
1048
1275
|
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Attempt to resolve all slots of a PendingTemplate.
|
|
1278
|
+
* Returns the reconstructed string if all slots resolve, undefined otherwise.
|
|
1279
|
+
*/
|
|
1280
|
+
function resolvePendingTemplate(template, resourceById) {
|
|
1281
|
+
const values = [];
|
|
1282
|
+
for (const slot of template.slots) {
|
|
1283
|
+
const sourceResource = resourceById.get(slot.source.id);
|
|
1284
|
+
if (!sourceResource) return void 0;
|
|
1285
|
+
const resolved = getNestedValue(getObservedDocument(sourceResource), slot.path);
|
|
1286
|
+
if (resolved === void 0 || resolved === null) return void 0;
|
|
1287
|
+
values.push(String(resolved));
|
|
1288
|
+
}
|
|
1289
|
+
let result = template.parts[0];
|
|
1290
|
+
for (let i = 0; i < values.length; i++) result += values[i] + template.parts[i + 1];
|
|
1291
|
+
return result;
|
|
1292
|
+
}
|
|
1049
1293
|
//#endregion
|
|
1050
1294
|
//#region src/pipeline/sequence.ts
|
|
1051
1295
|
/**
|
|
@@ -1083,6 +1327,7 @@ function sequence(state) {
|
|
|
1083
1327
|
function containsPending(obj) {
|
|
1084
1328
|
if (obj === null || obj === void 0) return false;
|
|
1085
1329
|
if (Pending.is(obj)) return true;
|
|
1330
|
+
if (PendingTemplate.is(obj)) return true;
|
|
1086
1331
|
if (typeof obj !== "object") return false;
|
|
1087
1332
|
if (Array.isArray(obj)) return obj.some(containsPending);
|
|
1088
1333
|
for (const value of Object.values(obj)) if (containsPending(value)) return true;
|
|
@@ -1267,15 +1512,23 @@ function runComposition(CompositionClass, input) {
|
|
|
1267
1512
|
xr: input.xr,
|
|
1268
1513
|
pipelineContext,
|
|
1269
1514
|
requiredResources: observedRequired,
|
|
1515
|
+
observedComposed,
|
|
1270
1516
|
graph,
|
|
1271
1517
|
collector
|
|
1272
1518
|
};
|
|
1519
|
+
const composition = compositionStorage.run(ctx, () => tokenRegistryStorage.run(createTokenRegistry(), () => new CompositionClass()));
|
|
1273
1520
|
const state = runPipeline({
|
|
1274
|
-
composition
|
|
1521
|
+
composition,
|
|
1275
1522
|
observedComposed,
|
|
1276
1523
|
observedRequired
|
|
1277
1524
|
});
|
|
1278
1525
|
const resources = state.emitted.map((emitted) => {
|
|
1526
|
+
if (emitted.preserved) return {
|
|
1527
|
+
name: emitted.name,
|
|
1528
|
+
document: emitted.document,
|
|
1529
|
+
ready: false,
|
|
1530
|
+
preserved: true
|
|
1531
|
+
};
|
|
1279
1532
|
const allChecks = [...emitted.readyChecks, ...DEFAULT_CHECKS];
|
|
1280
1533
|
const observed = observedComposed.get(`Composition/${emitted.name}`);
|
|
1281
1534
|
const ready = emitted.autoReady ? evaluateReadiness(allChecks, observed) : true;
|
|
@@ -1299,14 +1552,51 @@ function runComposition(CompositionClass, input) {
|
|
|
1299
1552
|
...ref.namespace ? { namespace: ref.namespace } : {}
|
|
1300
1553
|
});
|
|
1301
1554
|
}
|
|
1555
|
+
const blockedResources = [];
|
|
1556
|
+
for (const resource of state.resources) {
|
|
1557
|
+
if (isExternal(resource)) continue;
|
|
1558
|
+
const ref = getResourceRef(resource);
|
|
1559
|
+
if (state.classification.get(ref.id) !== "blocked") continue;
|
|
1560
|
+
const name = ref.id.startsWith("Composition/") ? ref.id.slice(12) : ref.id;
|
|
1561
|
+
const desired = getDesiredDocument(resource);
|
|
1562
|
+
const apiVersion = typeof desired.apiVersion === "string" ? desired.apiVersion : "";
|
|
1563
|
+
const kind = typeof desired.kind === "string" ? desired.kind : "";
|
|
1564
|
+
const metadata = desired.metadata && typeof desired.metadata === "object" ? desired.metadata : void 0;
|
|
1565
|
+
const resourceName = metadata && typeof metadata.name === "string" ? metadata.name : void 0;
|
|
1566
|
+
const waitingFor = describeWaitingFor(name, state.diagnostics);
|
|
1567
|
+
blockedResources.push({
|
|
1568
|
+
name,
|
|
1569
|
+
apiVersion,
|
|
1570
|
+
kind,
|
|
1571
|
+
...resourceName ? { resourceName } : {},
|
|
1572
|
+
...waitingFor && waitingFor.length > 0 ? { waitingFor } : {}
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1302
1575
|
return {
|
|
1303
1576
|
resources,
|
|
1577
|
+
blockedResources,
|
|
1304
1578
|
externalResources,
|
|
1305
1579
|
xrStatus: state.xrStatusPatches,
|
|
1306
|
-
diagnostics: state.diagnostics
|
|
1580
|
+
diagnostics: state.diagnostics,
|
|
1581
|
+
emitXplaneStatus: composition.emitXplaneStatus === true
|
|
1307
1582
|
};
|
|
1308
1583
|
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Build a human-readable `waitingFor` list for a blocked resource from the
|
|
1586
|
+
* matching diagnostic. Each entry describes one thing the resource is waiting
|
|
1587
|
+
* on (one entry per pending path, or a single entry for cycle/not-found).
|
|
1588
|
+
*/
|
|
1589
|
+
function describeWaitingFor(name, diagnostics) {
|
|
1590
|
+
const id = `Composition/${name}`;
|
|
1591
|
+
const diag = diagnostics.find((d) => d.resource === id || d.resource === name);
|
|
1592
|
+
if (!diag) return void 0;
|
|
1593
|
+
if (diag.reason === "cycle") return [`circular dependency: ${(diag.cycle ?? []).join(" → ")}`];
|
|
1594
|
+
if (diag.reason === "not-found") return [diag.detail ?? "external resource not found"];
|
|
1595
|
+
if (diag.pendingPaths && diag.pendingPaths.length > 0) return diag.pendingPaths.map((p) => {
|
|
1596
|
+
return `${p.waitingOn.resource.startsWith("Composition/") ? p.waitingOn.resource.slice(12) : p.waitingOn.resource}.${p.waitingOn.path}`;
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1309
1599
|
//#endregion
|
|
1310
|
-
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 };
|
|
1600
|
+
export { Composition, Construct, DEFAULT_CHECKS, DependencyGraph, EdgeCollector, Pending, PendingTemplate, Resource, compositionStorage, createPrimitiveReadProxy, createReadProxy, createTokenRegistry, createWriteProxy, diagnose, emit, evaluateReadiness, getCompositionContext, getDesiredDocument, getExternalRef, getLogger, getObservedDocument, getReadProxyMeta, getReadyChecks, getResourceInternals, getResourceRef, getXrDesiredStatus, hydrate, hydrateObserved, isExternal, isReadProxy, resolve, runComposition, runPipeline, sequence, tokenRegistryStorage, withLogger };
|
|
1311
1601
|
|
|
1312
1602
|
//# sourceMappingURL=index.mjs.map
|