mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
88 lines
3.0 KiB
TypeScript
88 lines
3.0 KiB
TypeScript
/**
|
|||
|
|
* Server-side feature-install queue (in-memory, FIFO).
|
||
|
|
*
|
||
|
|
* The queue lives on the server so a POST is always durable: the client POSTs
|
||
|
|
* immediately and the server serializes installs behind the existing file lock.
|
||
|
|
* A queued-but-not-yet-started install therefore has a real server footprint
|
||
|
|
* (this module), instead of only living in a browser tab that can close and
|
||
|
|
* silently drop it.
|
||
|
|
*
|
||
|
|
* This is a LEAF module: it imports NOTHING from feature-status.ts. The
|
||
|
|
* dependency runs the other way (feature-status.ts imports getQueuedBundleIds
|
||
|
|
* from here to surface "queued" status), so importing back would form a cycle.
|
||
|
|
*
|
||
|
|
* In-memory is deliberate, not a shortcut. The active install is a child of the
|
||
|
|
* API process and does not survive a server restart (recoverInterruptedInstalls
|
||
|
|
* clears the lock on boot because any previous install is dead). This queue
|
||
|
|
* matches that exact contract: it survives a browser tab close (the bug it
|
||
|
|
* fixes) but not a server restart.
|
||
|
|
*/
|
||
|
|
|
||
|
|
export interface QueuedInstall {
|
||
|
|
bundleId: string;
|
||
|
|
jobId: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
let activeInstall: QueuedInstall | null = null;
|
||
|
|
const queue: QueuedInstall[] = [];
|
||
|
|
|
||
|
|
/** The bundle currently installing (holding the file lock), or null. */
|
||
|
|
export function getActiveBundleId(): string | null {
|
||
|
|
return activeInstall?.bundleId ?? null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Bundle ids waiting in the queue, in FIFO order (excludes the active one). */
|
||
|
|
export function getQueuedBundleIds(): string[] {
|
||
|
|
return queue.map((entry) => entry.bundleId);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** True when the bundle is currently installing or already waiting in the queue. */
|
||
|
|
export function isQueuedOrActive(bundleId: string): boolean {
|
||
|
|
return activeInstall?.bundleId === bundleId || queue.some((entry) => entry.bundleId === bundleId);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add a bundle to the FIFO queue and return the effective jobId to track.
|
||
|
|
*
|
||
|
|
* Server-side dedup: if the bundle is already the active install or already in
|
||
|
|
* the queue, no second entry is added and the existing job's id is returned so
|
||
|
|
* the caller can attach to the in-flight progress stream.
|
||
|
|
*/
|
||
|
|
export function enqueue(entry: QueuedInstall): string {
|
||
|
|
if (activeInstall?.bundleId === entry.bundleId) {
|
||
|
|
return activeInstall.jobId;
|
||
|
|
}
|
||
|
|
const existing = queue.find((q) => q.bundleId === entry.bundleId);
|
||
|
|
if (existing) {
|
||
|
|
return existing.jobId;
|
||
|
|
}
|
||
|
|
queue.push(entry);
|
||
|
|
return entry.jobId;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Peek the head of the queue without removing it. */
|
||
|
|
export function peekQueue(): QueuedInstall | null {
|
||
|
|
return queue[0] ?? null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Remove and return the head of the queue. */
|
||
|
|
export function dequeue(): QueuedInstall | null {
|
||
|
|
return queue.shift() ?? null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Mark a bundle as the active install (called by the pump when it starts one). */
|
||
|
|
export function setActive(entry: QueuedInstall | null): void {
|
||
|
|
activeInstall = entry;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Clear the active install (called when the installer child exits). */
|
||
|
|
export function clearActive(): void {
|
||
|
|
activeInstall = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Test helper: wipe all queue state. */
|
||
|
|
export function resetQueueState(): void {
|
||
|
|
activeInstall = null;
|
||
|
|
queue.length = 0;
|
||
|
|
}
|