UNPKG

zephyr-agent

Version:
175 lines (174 loc) • 8.94 kB
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