mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 18:55:22 +00:00
141 lines
4.2 KiB
TypeScript
141 lines
4.2 KiB
TypeScript
import { createHash, randomUUID } from "node:crypto";
|
|
import { chmod, mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
import { join, relative, resolve, sep } from "node:path";
|
|
|
|
type SnapshotObject = Record<string, unknown>;
|
|
|
|
export type SnapshotTarget = {
|
|
targetId?: string;
|
|
urlIncludes?: string;
|
|
titleIncludes?: string;
|
|
};
|
|
|
|
export type SnapshotCaptureMetadata = {
|
|
version: 1;
|
|
captureId: string;
|
|
capturedAt: string;
|
|
path: string;
|
|
bytes: number;
|
|
targetKey: string;
|
|
targetId?: string | number;
|
|
mode?: string;
|
|
elementCount: number;
|
|
matchCount: number;
|
|
staleAfterInteraction: true;
|
|
};
|
|
|
|
export type StoredSnapshotCapture = {
|
|
metadata: SnapshotCaptureMetadata;
|
|
absolutePath: string;
|
|
};
|
|
|
|
export type SnapshotCaptureFile = {
|
|
version: 1;
|
|
captureId: string;
|
|
capturedAt: string;
|
|
targetKey: string;
|
|
snapshot: unknown;
|
|
};
|
|
|
|
function hashSegment(value: string): string {
|
|
return createHash("sha256").update(value).digest("hex").slice(0, 20);
|
|
}
|
|
|
|
function normalizeRelativePath(path: string): string {
|
|
return path.split(sep).join("/");
|
|
}
|
|
|
|
function snapshotObject(snapshot: unknown): SnapshotObject | undefined {
|
|
return snapshot && typeof snapshot === "object" && !Array.isArray(snapshot)
|
|
? snapshot as SnapshotObject
|
|
: undefined;
|
|
}
|
|
|
|
export function snapshotTargetKey(target: SnapshotTarget = {}): string {
|
|
if (target.targetId) return `target:${target.targetId}`;
|
|
if (target.urlIncludes) return `url:${target.urlIncludes}`;
|
|
if (target.titleIncludes) return `title:${target.titleIncludes}`;
|
|
return "default";
|
|
}
|
|
|
|
export class SessionSnapshotStore {
|
|
readonly workspaceRoot: string;
|
|
readonly sessionRoot: string;
|
|
readonly sessionHash: string;
|
|
|
|
constructor(workspaceRoot: string, sessionId: string) {
|
|
this.workspaceRoot = resolve(workspaceRoot);
|
|
this.sessionHash = hashSegment(sessionId || "unknown-session");
|
|
this.sessionRoot = join(this.workspaceRoot, ".pi", "chrome-context", this.sessionHash);
|
|
}
|
|
|
|
async save(snapshot: unknown, target: SnapshotTarget = {}): Promise<StoredSnapshotCapture> {
|
|
const targetKey = snapshotTargetKey(target);
|
|
const targetDirectory = join(this.sessionRoot, hashSegment(targetKey));
|
|
const absolutePath = join(targetDirectory, "snapshot.json");
|
|
const temporaryPath = join(targetDirectory, `.snapshot.${process.pid}.${randomUUID()}.tmp`);
|
|
const captureId = randomUUID();
|
|
const capturedAt = new Date().toISOString();
|
|
const capture: SnapshotCaptureFile = {
|
|
version: 1,
|
|
captureId,
|
|
capturedAt,
|
|
targetKey,
|
|
snapshot,
|
|
};
|
|
const serialized = `${JSON.stringify(capture, null, 2)}\n`;
|
|
|
|
await mkdir(targetDirectory, { recursive: true, mode: 0o700 });
|
|
await chmod(this.sessionRoot, 0o700).catch(() => undefined);
|
|
await chmod(targetDirectory, 0o700).catch(() => undefined);
|
|
try {
|
|
await writeFile(temporaryPath, serialized, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
await chmod(temporaryPath, 0o600).catch(() => undefined);
|
|
await rename(temporaryPath, absolutePath);
|
|
await chmod(absolutePath, 0o600).catch(() => undefined);
|
|
} catch (error) {
|
|
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
throw error;
|
|
}
|
|
|
|
const object = snapshotObject(snapshot);
|
|
const elements = Array.isArray(object?.elements) ? object.elements : [];
|
|
const matches = Array.isArray(object?.matches) ? object.matches : [];
|
|
const targetId = typeof object?.targetId === "string" || typeof object?.targetId === "number"
|
|
? object.targetId
|
|
: undefined;
|
|
const mode = typeof object?.mode === "string" ? object.mode : undefined;
|
|
const path = normalizeRelativePath(relative(this.workspaceRoot, absolutePath));
|
|
|
|
return {
|
|
absolutePath,
|
|
metadata: {
|
|
version: 1,
|
|
captureId,
|
|
capturedAt,
|
|
path,
|
|
bytes: Buffer.byteLength(serialized),
|
|
targetKey,
|
|
targetId,
|
|
mode,
|
|
elementCount: elements.length,
|
|
matchCount: matches.length,
|
|
staleAfterInteraction: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
async invalidate(target?: SnapshotTarget): Promise<void> {
|
|
if (!target) {
|
|
await this.clear();
|
|
return;
|
|
}
|
|
const targetDirectory = join(this.sessionRoot, hashSegment(snapshotTargetKey(target)));
|
|
await rm(targetDirectory, { recursive: true, force: true });
|
|
}
|
|
|
|
async clear(): Promise<void> {
|
|
await rm(this.sessionRoot, { recursive: true, force: true });
|
|
}
|
|
}
|