zephyr-agent
Version:
Zephyr plugin agent
175 lines (174 loc) • 8.94 kB
JavaScript
import "node:module";
import { BuildSession, BuildSessionRollbackError, BuildSessionStateError } from "./build-session.mjs";
function buildDefinitionKey(options) {
const participants = options.participants.map((participant)=>({
name: participant.name,
role: participant.role ?? participant.name,
required: participant.required ?? true
})).sort((left, right)=>left.name.localeCompare(right.name));
const postprocessors = [
...options.postprocessors ?? []
].sort();
return JSON.stringify({
participants,
postprocessors
});
}
class ApplicationContext {
applicationUid;
options;
latestGeneration = -1;
activeSession = null;
activeDefinitionKey = null;
constructor(options){
if (!options.applicationUid.trim()) throw new BuildSessionStateError('Application UID must not be empty');
this.applicationUid = options.applicationUid;
this.options = options;
}
get currentBuild() {
return this.activeSession;
}
beginBuild(options) {
const definitionKey = buildDefinitionKey(options);
const generation = options.generation ?? this.latestGeneration + 1;
if (!Number.isSafeInteger(generation) || generation < 0) throw new BuildSessionStateError(`Build generation must be a non-negative safe integer, received ${generation}`);
const invocationId = options.invocationId ?? `invocation-${generation}`;
if (!invocationId.trim()) throw new BuildSessionStateError('Build invocation ID must not be empty');
const active = this.activeSession;
if (active?.failure instanceof BuildSessionRollbackError) throw active.failure;
if (active && active.identity.generation === generation && active.identity.invocationId === invocationId) {
if (definitionKey !== this.activeDefinitionKey) throw new BuildSessionStateError('Build participants or postprocessors differ for the same invocation generation');
return active;
}
if (generation <= this.latestGeneration) throw new BuildSessionStateError(`Build generation ${generation} is stale; latest generation is ${this.latestGeneration}`);
if (active && !active.isTerminal) active.abort(`Superseded by build generation ${generation}`);
const session = new BuildSession({
applicationUid: this.applicationUid,
invocationId,
generation
}, options.participants, options.postprocessors ?? [], {
prepare: this.options.prepare,
publish: this.options.publish,
finish: this.options.finish,
onFailure: this.options.onFailure
});
this.latestGeneration = generation;
this.activeSession = session;
this.activeDefinitionKey = definitionKey;
return session;
}
abortCurrent(reason) {
const active = this.activeSession;
if (active?.failure instanceof BuildSessionRollbackError) throw active.failure;
active?.abort(reason);
}
}
class ApplicationContextRegistry {
entries = new Map();
pending = new Map();
disposedPending = new WeakSet();
getOrCreate(locator, create) {
const aliases = this.createAliases(locator);
const existingEntries = new Set(aliases.map((alias)=>this.entries.get(alias)).filter((entry)=>!!entry));
if (existingEntries.size > 1) throw new BuildSessionStateError('Application context aliases resolve to different registered contexts');
let entry = existingEntries.values().next().value;
if (!entry) {
const context = create();
entry = [
...new Set(this.entries.values())
].find((candidate)=>candidate.context === context) ?? {
context,
aliases: new Set()
};
}
if (locator.applicationUid && entry.context.applicationUid !== locator.applicationUid) throw new BuildSessionStateError(`Application context UID mismatch: expected ${locator.applicationUid}, received ${entry.context.applicationUid}`);
for (const alias of aliases){
const owner = this.entries.get(alias);
if (owner && owner !== entry) throw new BuildSessionStateError(`Application context alias "${alias}" is already registered`);
entry.aliases.add(alias);
this.entries.set(alias, entry);
}
return entry.context;
}
async getOrCreateAsync(locator, create) {
const aliases = this.createAliases(locator);
const existing = this.get(locator);
if (existing) return existing;
const pendingContexts = new Set(aliases.map((alias)=>this.pending.get(alias)).filter((context)=>!!context));
if (pendingContexts.size > 1) throw new BuildSessionStateError('Application context aliases have different pending initializations');
let pendingContext = pendingContexts.values().next().value;
if (!pendingContext) pendingContext = Promise.resolve().then(create);
for (const alias of aliases){
const owner = this.pending.get(alias);
if (owner && owner !== pendingContext) throw new BuildSessionStateError(`Application context alias "${alias}" is already initializing`);
this.pending.set(alias, pendingContext);
}
try {
const context = await pendingContext;
if (this.disposedPending.has(pendingContext)) {
context.abortCurrent('Context disposed during initialization');
throw new BuildSessionStateError('Application context was disposed during initialization');
}
this.clearPendingAliases(pendingContext);
return this.getOrCreate(locator, ()=>context);
} catch (error) {
this.clearPendingAliases(pendingContext);
throw error;
}
}
get(locator) {
const entries = new Set(this.createAliases(locator).map((alias)=>this.entries.get(alias)).filter((entry)=>!!entry));
if (entries.size > 1) throw new BuildSessionStateError('Application context aliases resolve to different registered contexts');
return entries.values().next().value?.context ?? null;
}
associate(existing, aliases) {
const context = this.get(existing);
if (!context) throw new BuildSessionStateError('Cannot associate an unknown application context');
const aliasedContext = this.get(aliases);
if (aliasedContext && aliasedContext !== context) throw new BuildSessionStateError('Cannot associate aliases owned by a different application context');
return this.getOrCreate(aliases, ()=>context);
}
dispose(locator, reason = 'Context disposed') {
const aliases = this.createAliases(locator);
const entry = aliases.map((alias)=>this.entries.get(alias)).find((candidate)=>!!candidate);
if (!entry) {
const pendingContext = aliases.map((alias)=>this.pending.get(alias)).find((context)=>!!context);
if (!pendingContext) return false;
this.disposedPending.add(pendingContext);
this.clearPendingAliases(pendingContext);
return true;
}
if (entry.context.currentBuild?.status !== 'publishing') entry.context.abortCurrent(reason);
for (const alias of entry.aliases)this.entries.delete(alias);
entry.aliases.clear();
return true;
}
clear(reason = 'Registry cleared') {
for (const pendingContext of new Set(this.pending.values()))this.disposedPending.add(pendingContext);
this.pending.clear();
const uniqueEntries = new Set(this.entries.values());
for (const entry of uniqueEntries){
if (entry.context.currentBuild?.status !== 'publishing') entry.context.abortCurrent(reason);
entry.aliases.clear();
}
this.entries.clear();
}
clearPendingAliases(pendingContext) {
for (const [alias, owner] of this.pending)if (owner === pendingContext) this.pending.delete(alias);
}
createAliases(locator) {
const aliases = [];
if (locator.contextKey?.trim()) aliases.push(`context:${locator.contextKey.trim()}`);
if (locator.applicationUid || locator.invocationId) {
if (!locator.applicationUid?.trim() || !locator.invocationId?.trim()) throw new BuildSessionStateError('applicationUid and invocationId must be supplied together');
aliases.push(`identity:${JSON.stringify([
locator.applicationUid,
locator.invocationId
])}`);
}
if (0 === aliases.length) throw new BuildSessionStateError('An application/invocation identity or contextKey is required');
return aliases;
}
}
export { ApplicationContext, ApplicationContextRegistry };
//# sourceMappingURL=application-context.mjs.map