import { fromGeneratedExtEnvelope, type LiveExtEnvelope, } from "./ext.js"; import type * as protocol from "./generated-protocol.js"; import { ownershipMatchesSelector, ownershipSelectorKey, type LiveOwnershipScope, } from "./ownership.js"; import { fromGeneratedStreamChannel, fromGeneratedVmLifecycleState, } from "./protocol-maps.js"; export const ANY_BUFFERED_EVENT_KEY = "*"; export type { LiveOwnershipScope } from "./ownership.js"; export type LiveSidecarEventPayload = | { type: "vm_lifecycle"; state: "creating" | "ready" | "disposing" | "disposed" | "failed"; } | { type: "process_output"; process_id: string; channel: "stdout" | "stderr"; chunk: Uint8Array; } | { type: "process_exited"; process_id: string; exit_code: number; } | { type: "structured"; name: string; detail: Record; } | { type: "ext"; envelope: LiveExtEnvelope; }; export interface LiveSidecarEventFrame { ownership: LiveOwnershipScope; payload: LiveSidecarEventPayload; } export type LiveSidecarEventSelector = | { any: true; } | { type: "vm_lifecycle"; ownership?: LiveOwnershipScope; state?: Extract< LiveSidecarEventPayload, { type: "vm_lifecycle" } >["state"]; } | { type: "process_output"; ownership?: LiveOwnershipScope; processId?: string; channel?: Extract< LiveSidecarEventPayload, { type: "process_output" } >["channel"]; } | { type: "process_exited"; ownership?: LiveOwnershipScope; processId?: string; } | { type: "structured"; ownership?: LiveOwnershipScope; name?: string; detail?: Record; }; export type LiveSidecarBufferedEventRecord< TEvent extends LiveSidecarEventFrame, > = { event: TEvent; keys: readonly string[]; }; export type LiveSidecarEventWaitMatcher = { matches: (event: TEvent) => boolean; bufferKey: string | null; }; export class SidecarEventBufferOverflow extends Error { readonly capacity: number; readonly bufferedEvents: number; readonly eventType: LiveSidecarEventPayload["type"]; constructor(options: { capacity: number; bufferedEvents: number; eventType: LiveSidecarEventPayload["type"]; }) { super( `sidecar event buffer overflow after ${options.bufferedEvents} queued events (capacity ${options.capacity}) while buffering ${options.eventType}`, ); this.name = "SidecarEventBufferOverflow"; this.capacity = options.capacity; this.bufferedEvents = options.bufferedEvents; this.eventType = options.eventType; } } export class SidecarEventBuffer { private readonly bufferedEvents = new Map< number, LiveSidecarBufferedEventRecord >(); private readonly bufferedEventQueues = new Map>(); private nextBufferedEventId = 1; constructor(private readonly capacity: number) {} get size(): number { return this.bufferedEvents.size; } buffer(event: TEvent): SidecarEventBufferOverflow | null { if (this.bufferedEvents.size >= this.capacity) { return new SidecarEventBufferOverflow({ capacity: this.capacity, bufferedEvents: this.bufferedEvents.size, eventType: event.payload.type, }); } const eventId = this.nextBufferedEventId++; const keys = sidecarEventBufferKeys(event); this.bufferedEvents.set(eventId, { event, keys, }); for (const key of keys) { const queue = this.bufferedEventQueues.get(key); if (queue) { queue.add(eventId); continue; } this.bufferedEventQueues.set(key, new Set([eventId])); } return null; } take(matcher: LiveSidecarEventWaitMatcher): TEvent | null { if (matcher.bufferKey !== null) { return this.takeFromKey(matcher.bufferKey); } const queue = this.bufferedEventQueues.get(ANY_BUFFERED_EVENT_KEY); if (!queue) { return null; } for (const eventId of queue) { const record = this.bufferedEvents.get(eventId); if (!record) { continue; } if (!matcher.matches(record.event)) { continue; } return this.remove(eventId); } return null; } private takeFromKey(key: string): TEvent | null { const queue = this.bufferedEventQueues.get(key); if (!queue) { return null; } for (const eventId of queue) { const record = this.bufferedEvents.get(eventId); if (!record) { queue.delete(eventId); continue; } return this.remove(eventId); } return null; } private remove(eventId: number): TEvent | null { const record = this.bufferedEvents.get(eventId); if (!record) { return null; } this.bufferedEvents.delete(eventId); for (const key of record.keys) { const queue = this.bufferedEventQueues.get(key); if (!queue) { continue; } queue.delete(eventId); if (queue.size === 0) { this.bufferedEventQueues.delete(key); } } return record.event; } } function buildBufferKey( type: LiveSidecarEventPayload["type"], options?: { ownership?: LiveOwnershipScope; state?: string; processId?: string; channel?: string; name?: string; }, ): string { const parts = [`type:${type}`]; if (options?.ownership) { parts.push(`ownership:${ownershipSelectorKey(options.ownership)}`); } if (options?.state) { parts.push(`state:${options.state}`); } if (options?.processId) { parts.push(`process:${options.processId}`); } if (options?.channel) { parts.push(`channel:${options.channel}`); } if (options?.name) { parts.push(`name:${options.name}`); } return parts.join("|"); } export function sidecarSelectorMatchesEvent( selector: LiveSidecarEventSelector, event: TEvent, ): boolean { if ("any" in selector) { return true; } if (event.payload.type !== selector.type) { return false; } if (!ownershipMatchesSelector(selector.ownership, event.ownership)) { return false; } switch (selector.type) { case "vm_lifecycle": { const payload = event.payload as Extract< LiveSidecarEventPayload, { type: "vm_lifecycle" } >; return selector.state === undefined || payload.state === selector.state; } case "process_output": { const payload = event.payload as Extract< LiveSidecarEventPayload, { type: "process_output" } >; return ( (selector.processId === undefined || payload.process_id === selector.processId) && (selector.channel === undefined || payload.channel === selector.channel) ); } case "process_exited": { const payload = event.payload as Extract< LiveSidecarEventPayload, { type: "process_exited" } >; return ( selector.processId === undefined || payload.process_id === selector.processId ); } case "structured": { const payload = event.payload as Extract< LiveSidecarEventPayload, { type: "structured" } >; if (selector.name !== undefined && payload.name !== selector.name) { return false; } if (!selector.detail) { return true; } for (const [key, value] of Object.entries(selector.detail)) { if (payload.detail[key] !== value) { return false; } } return true; } } } export function sidecarSelectorBufferKey( selector: LiveSidecarEventSelector, ): string | null { if ("any" in selector) { return ANY_BUFFERED_EVENT_KEY; } switch (selector.type) { case "vm_lifecycle": return buildBufferKey(selector.type, { ownership: selector.ownership, state: selector.state, }); case "process_output": return buildBufferKey(selector.type, { ownership: selector.ownership, processId: selector.processId, channel: selector.channel, }); case "process_exited": return buildBufferKey(selector.type, { ownership: selector.ownership, processId: selector.processId, }); case "structured": if (selector.detail) { return null; } return buildBufferKey(selector.type, { ownership: selector.ownership, name: selector.name, }); } } export function normalizeSidecarEventMatcher< TEvent extends LiveSidecarEventFrame, >( selector: LiveSidecarEventSelector | ((event: TEvent) => boolean), ): LiveSidecarEventWaitMatcher { if (typeof selector === "function") { return { matches: selector, bufferKey: null, }; } return { matches: (event) => sidecarSelectorMatchesEvent(selector, event), bufferKey: sidecarSelectorBufferKey(selector), }; } export function fromGeneratedEventPayload( payload: protocol.EventPayload, ): LiveSidecarEventPayload { switch (payload.tag) { case "VmLifecycleEvent": return { type: "vm_lifecycle", state: fromGeneratedVmLifecycleState(payload.val.state), }; case "ProcessOutputEvent": return { type: "process_output", process_id: payload.val.processId, channel: fromGeneratedStreamChannel(payload.val.channel), chunk: Buffer.from(payload.val.chunk), }; case "ProcessExitedEvent": return { type: "process_exited", process_id: payload.val.processId, exit_code: payload.val.exitCode, }; case "StructuredEvent": return { type: "structured", name: payload.val.name, detail: Object.fromEntries(payload.val.detail), }; case "ExtEnvelope": return { type: "ext", envelope: fromGeneratedExtEnvelope(payload.val), }; } } export function sidecarEventWaitAbortError(reason: unknown): Error { return reason instanceof Error ? reason : new Error(reason ? String(reason) : "sidecar event wait aborted"); } export function sidecarEventBufferKeys( event: TEvent, ): string[] { const owner = event.ownership; const keys = new Set([ ANY_BUFFERED_EVENT_KEY, buildBufferKey(event.payload.type), buildBufferKey(event.payload.type, { ownership: owner }), ]); switch (event.payload.type) { case "vm_lifecycle": keys.add( buildBufferKey(event.payload.type, { state: event.payload.state, }), ); keys.add( buildBufferKey(event.payload.type, { ownership: owner, state: event.payload.state, }), ); break; case "process_output": keys.add( buildBufferKey(event.payload.type, { processId: event.payload.process_id, }), ); keys.add( buildBufferKey(event.payload.type, { channel: event.payload.channel, }), ); keys.add( buildBufferKey(event.payload.type, { processId: event.payload.process_id, channel: event.payload.channel, }), ); keys.add( buildBufferKey(event.payload.type, { ownership: owner, processId: event.payload.process_id, }), ); keys.add( buildBufferKey(event.payload.type, { ownership: owner, channel: event.payload.channel, }), ); keys.add( buildBufferKey(event.payload.type, { ownership: owner, processId: event.payload.process_id, channel: event.payload.channel, }), ); break; case "process_exited": keys.add( buildBufferKey(event.payload.type, { processId: event.payload.process_id, }), ); keys.add( buildBufferKey(event.payload.type, { ownership: owner, processId: event.payload.process_id, }), ); break; case "structured": keys.add( buildBufferKey(event.payload.type, { name: event.payload.name, }), ); keys.add( buildBufferKey(event.payload.type, { ownership: owner, name: event.payload.name, }), ); break; case "ext": break; } return [...keys]; }