frontend-app-api: defer conditional bootstrap APIs and root elements
This keeps bootstrap rendering stable while still allowing root elements and API subtrees to activate once session predicates are available, and warns when bootstrap-visible extensions depend on APIs that only appear during finalization. Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -338,12 +338,28 @@ export function createAppNodeInstance(options: {
|
||||
apis: ApiHolder;
|
||||
attachments: ReadonlyMap<string, AppNode[]>;
|
||||
collector: ErrorCollector;
|
||||
onMissingApi?(ctx: { node: AppNode; apiRefId: string }): void;
|
||||
}): AppNodeInstance | undefined {
|
||||
const { node, apis, attachments } = options;
|
||||
const collector = options.collector.child({ node });
|
||||
const { id, extension, config } = node.spec;
|
||||
const extensionData = new Map<string, unknown>();
|
||||
const extensionDataRefs = new Set<ExtensionDataRef<unknown>>();
|
||||
const scopedApis: ApiHolder =
|
||||
options.onMissingApi === undefined
|
||||
? apis
|
||||
: {
|
||||
get(apiRef) {
|
||||
const api = apis.get(apiRef);
|
||||
if (api === undefined) {
|
||||
options.onMissingApi?.({
|
||||
node,
|
||||
apiRefId: apiRef.id,
|
||||
});
|
||||
}
|
||||
return api;
|
||||
},
|
||||
};
|
||||
|
||||
let parsedConfig: { [x: string]: any };
|
||||
try {
|
||||
@@ -368,7 +384,7 @@ export function createAppNodeInstance(options: {
|
||||
if (internalExtension.version === 'v1') {
|
||||
const namedOutputs = internalExtension.factory({
|
||||
node,
|
||||
apis,
|
||||
apis: scopedApis,
|
||||
config: parsedConfig,
|
||||
inputs: resolveV1Inputs(internalExtension.inputs, attachments),
|
||||
});
|
||||
@@ -389,7 +405,7 @@ export function createAppNodeInstance(options: {
|
||||
} else if (internalExtension.version === 'v2') {
|
||||
const context = {
|
||||
node,
|
||||
apis,
|
||||
apis: scopedApis,
|
||||
config: parsedConfig,
|
||||
inputs: resolveV2Inputs(
|
||||
internalExtension.inputs,
|
||||
@@ -513,16 +529,26 @@ export function instantiateAppNodeTree(
|
||||
optionsOrPredicateContext?:
|
||||
| {
|
||||
stopAtAttachment?(ctx: { node: AppNode; input: string }): boolean;
|
||||
skipChild?(ctx: {
|
||||
node: AppNode;
|
||||
input: string;
|
||||
child: AppNode;
|
||||
}): boolean;
|
||||
onMissingApi?(ctx: { node: AppNode; apiRefId: string }): void;
|
||||
predicateContext?: Record<string, unknown>;
|
||||
}
|
||||
| Record<string, unknown>,
|
||||
): boolean {
|
||||
const options: {
|
||||
stopAtAttachment?(ctx: { node: AppNode; input: string }): boolean;
|
||||
skipChild?(ctx: { node: AppNode; input: string; child: AppNode }): boolean;
|
||||
onMissingApi?(ctx: { node: AppNode; apiRefId: string }): void;
|
||||
predicateContext?: Record<string, unknown>;
|
||||
} =
|
||||
optionsOrPredicateContext &&
|
||||
('stopAtAttachment' in optionsOrPredicateContext ||
|
||||
'skipChild' in optionsOrPredicateContext ||
|
||||
'onMissingApi' in optionsOrPredicateContext ||
|
||||
'predicateContext' in optionsOrPredicateContext)
|
||||
? optionsOrPredicateContext
|
||||
: {
|
||||
@@ -551,6 +577,9 @@ export function instantiateAppNodeTree(
|
||||
continue;
|
||||
}
|
||||
const instantiatedChildren = children.flatMap(child => {
|
||||
if (options?.skipChild?.({ node, input, child })) {
|
||||
return [];
|
||||
}
|
||||
const childInstance = createInstance(child);
|
||||
if (!childInstance) {
|
||||
return [];
|
||||
@@ -568,6 +597,7 @@ export function instantiateAppNodeTree(
|
||||
apis,
|
||||
attachments: instantiatedAttachments,
|
||||
collector,
|
||||
onMissingApi: options?.onMissingApi,
|
||||
});
|
||||
|
||||
return node.instance;
|
||||
|
||||
@@ -40,6 +40,16 @@ export class FrontendApiRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
set(factory: AnyApiFactory) {
|
||||
this.factories.set(factory.api.id, factory);
|
||||
}
|
||||
|
||||
setAll(factories: Iterable<AnyApiFactory>) {
|
||||
for (const factory of factories) {
|
||||
this.set(factory);
|
||||
}
|
||||
}
|
||||
|
||||
get<T>(
|
||||
api: ApiRef<T>,
|
||||
): ApiFactory<T, T, { [name: string]: unknown }> | undefined {
|
||||
@@ -80,6 +90,17 @@ export class FrontendApiResolver implements ApiHolder {
|
||||
return this.load(ref);
|
||||
}
|
||||
|
||||
invalidate(apiRefIds?: Iterable<string>) {
|
||||
if (apiRefIds === undefined) {
|
||||
this.apis.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const apiRefId of apiRefIds) {
|
||||
this.apis.delete(apiRefId);
|
||||
}
|
||||
}
|
||||
|
||||
private load<T>(ref: ApiRef<T>, loading: AnyApiRef[] = []): T | undefined {
|
||||
const existing = this.apis.get(ref.id);
|
||||
if (existing) {
|
||||
|
||||
@@ -82,6 +82,12 @@ export type AppErrorTypes = {
|
||||
existingPluginId: string;
|
||||
};
|
||||
};
|
||||
EXTENSION_BOOTSTRAP_PREDICATE_IGNORED: {
|
||||
context: { node: AppNode };
|
||||
};
|
||||
EXTENSION_BOOTSTRAP_API_UNAVAILABLE: {
|
||||
context: { node: AppNode; apiRefId: string };
|
||||
};
|
||||
// routing
|
||||
ROUTE_DUPLICATE: {
|
||||
context: { routeId: string };
|
||||
|
||||
Reference in New Issue
Block a user