Files
paseo/packages/app/src/terminal/runtime/terminal-output-session.ts
T

188 lines
4.8 KiB
TypeScript

import {
TerminalOutputPump,
type TerminalOutputChunk,
} from "./terminal-output-pump";
import {
TerminalOutputDeliveryQueue,
type TerminalOutputDeliveryChunk,
} from "./terminal-output-delivery-queue";
type TerminalOutputState = {
selectedTerminalId: string | null;
snapshotText: string;
snapshotSequence: number;
chunkText: string;
chunkSequence: number;
chunkReplay: boolean;
};
const EMPTY_STATE: TerminalOutputState = {
selectedTerminalId: null,
snapshotText: "",
snapshotSequence: 0,
chunkText: "",
chunkSequence: 0,
chunkReplay: false,
};
export type TerminalOutputSessionAppendInput = {
terminalId: string;
text: string;
replay: boolean;
};
export type TerminalOutputSessionSetSelectedInput = {
terminalId: string | null;
};
export type TerminalOutputSessionPruneInput = {
terminalIds: string[];
};
export type TerminalOutputSessionConsumeInput = {
sequence: number;
};
export type TerminalOutputSessionReadSnapshotInput = {
terminalId: string | null;
};
export class TerminalOutputSession {
private readonly listeners = new Set<() => void>();
private readonly outputPump: TerminalOutputPump;
private readonly deliveryQueue: TerminalOutputDeliveryQueue;
private state: TerminalOutputState = EMPTY_STATE;
constructor(input: { maxOutputChars: number }) {
this.outputPump = new TerminalOutputPump({
maxOutputChars: input.maxOutputChars,
onSelectedOutputChunk: (chunk: TerminalOutputChunk) => {
this.deliveryQueue.enqueue(chunk);
},
});
this.deliveryQueue = new TerminalOutputDeliveryQueue({
onDeliver: (chunk: TerminalOutputDeliveryChunk) => {
const snapshotText = this.outputPump.readSnapshot({
terminalId: this.state.selectedTerminalId,
});
this.state = {
...this.state,
snapshotText,
snapshotSequence: chunk.sequence,
chunkText: chunk.text,
chunkSequence: chunk.sequence,
chunkReplay: chunk.replay,
};
this.emit();
},
});
}
subscribe(listener: () => void): () => void {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
getState(): TerminalOutputState {
return this.state;
}
setSelectedTerminal(input: TerminalOutputSessionSetSelectedInput): void {
if (this.state.selectedTerminalId === input.terminalId) {
return;
}
this.deliveryQueue.reset();
this.outputPump.setSelectedTerminal({ terminalId: input.terminalId });
const snapshotText = this.outputPump.readSnapshot({
terminalId: input.terminalId,
});
this.state = {
selectedTerminalId: input.terminalId,
snapshotText,
snapshotSequence: this.state.snapshotSequence,
chunkText: "",
chunkSequence: 0,
chunkReplay: false,
};
this.emit();
}
append(input: TerminalOutputSessionAppendInput): void {
this.outputPump.append(input);
}
readSnapshot(input: TerminalOutputSessionReadSnapshotInput): string {
return this.outputPump.readSnapshot(input);
}
clearTerminal(input: { terminalId: string }): void {
this.outputPump.clearTerminal(input);
if (this.state.selectedTerminalId !== input.terminalId) {
return;
}
this.deliveryQueue.reset();
this.state = {
...this.state,
snapshotText: "",
chunkText: "",
chunkSequence: 0,
chunkReplay: false,
};
this.emit();
}
prune(input: TerminalOutputSessionPruneInput): void {
this.outputPump.prune(input);
}
consume(input: TerminalOutputSessionConsumeInput): void {
this.deliveryQueue.consume(input);
}
private emit(): void {
for (const listener of this.listeners) {
listener();
}
}
}
const sessionsByScopeKey = new Map<string, TerminalOutputSession>();
const sessionRefCountByScopeKey = new Map<string, number>();
export function getTerminalOutputSession(input: {
scopeKey: string;
maxOutputChars: number;
}): TerminalOutputSession {
const existing = sessionsByScopeKey.get(input.scopeKey);
if (existing) {
return existing;
}
const session = new TerminalOutputSession({
maxOutputChars: input.maxOutputChars,
});
sessionsByScopeKey.set(input.scopeKey, session);
return session;
}
export function retainTerminalOutputSession(input: { scopeKey: string }): void {
const current = sessionRefCountByScopeKey.get(input.scopeKey) ?? 0;
sessionRefCountByScopeKey.set(input.scopeKey, current + 1);
}
export function releaseTerminalOutputSession(input: { scopeKey: string }): void {
const current = sessionRefCountByScopeKey.get(input.scopeKey) ?? 0;
if (current <= 1) {
sessionRefCountByScopeKey.delete(input.scopeKey);
sessionsByScopeKey.delete(input.scopeKey);
return;
}
sessionRefCountByScopeKey.set(input.scopeKey, current - 1);
}