~james/core

core/web/src/lib/components/repo/diffRpc.ts -rw-r--r-- 4.6 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 16 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import type { FileContents, FileDiffMetadata } from "@pierre/diffs";
import DiffWorker from "#lib/components/repo/diffWorker.ts?worker";
import type { DiffWorkerRequest, DiffWorkerResponse } from "#lib/components/repo/diffWorker.ts";

// Strip Svelte 5 reactive proxies; postMessage throws DataCloneError.
let diffWorkerInstance: Worker | undefined;
let nextTaskId = 1;
interface PendingTask {
	resolve: (response: DiffWorkerResponse) => void;
	reject: (err: unknown) => void;
	/** set for streamed patches, which answer many times before they finish */
	partial?: (files: FileDiffMetadata[]) => void;
}
const pendingTasks = new Map<number, PendingTask>();

const DIFF_WORKER_TIMEOUT_MS = 30_000;
const DIFF_WORKER_IDLE_MS = 1000;
let idleTimer: ReturnType<typeof setTimeout> | undefined;

const retireWhenIdle = () => {
	if (pendingTasks.size > 0) return;
	clearTimeout(idleTimer);
	idleTimer = setTimeout(() => {
		if (pendingTasks.size > 0) return;
		diffWorkerInstance?.terminate();
		diffWorkerInstance = undefined;
	}, DIFF_WORKER_IDLE_MS);
};

const failPending = (error: Error) => {
	for (const [id, task] of pendingTasks) {
		pendingTasks.delete(id);
		task.reject(error);
	}
};

const serializeFileContents = (file: FileContents): FileContents => ({
	name: file.name,
	contents: typeof file.contents === "string" ? file.contents : "",
	...(file.cacheKey !== undefined ? { cacheKey: file.cacheKey } : {})
});

const diffWorker = (): Worker => {
	clearTimeout(idleTimer);
	if (diffWorkerInstance) return diffWorkerInstance;
	const worker = new DiffWorker();
	worker.onmessage = (e: MessageEvent<DiffWorkerResponse>) => {
		const task = pendingTasks.get(e.data.id);
		if (!task) return;
		if (task.partial && !e.data.done) {
			if (e.data.files) task.partial(e.data.files);
			return;
		}
		pendingTasks.delete(e.data.id);
		task.resolve(e.data);
		retireWhenIdle();
	};
	worker.onerror = () => {
		failPending(new Error("diff worker failed"));
		diffWorkerInstance?.terminate();
		diffWorkerInstance = undefined;
	};
	diffWorkerInstance = worker;
	return worker;
};

const runInDiffWorker = (build: (id: number) => DiffWorkerRequest): Promise<DiffWorkerResponse> => {
	const worker = diffWorker();
	const id = nextTaskId++;
	return new Promise((resolve, reject) => {
		const timer = setTimeout(() => {
			if (!pendingTasks.delete(id)) return;
			reject(new Error("diff worker timed out"));
			retireWhenIdle();
		}, DIFF_WORKER_TIMEOUT_MS);
		pendingTasks.set(id, {
			resolve: (response) => {
				clearTimeout(timer);
				resolve(response);
			},
			reject: (err) => {
				clearTimeout(timer);
				reject(err);
			}
		});
		worker.postMessage(build(id));
	});
};

export const parseDiffInWorker = async (
	oldFile: FileContents,
	newFile: FileContents
): Promise<FileDiffMetadata> => {
	const response = await runInDiffWorker((id) => ({
		id,
		kind: "file",
		oldFile: serializeFileContents(oldFile),
		newFile: serializeFileContents(newFile)
	}));
	if (response.fileDiff) return response.fileDiff;
	throw new Error(response.error ?? "diff worker returned no diff");
};

export const streamPatchInWorker = async (
	url: string,
	onFiles: (files: FileDiffMetadata[]) => void,
	signal?: AbortSignal
): Promise<void> => {
	// Fetch on main thread to reuse <link rel="preload"> cache (worker has separate cache).
	let body: ReadableStream<Uint8Array> | undefined;
	try {
		const response = await fetch(url, { signal });
		if (!response.ok) throw new Error(`patch ${response.status}`);
		body = response.body ?? undefined;
	} catch (cause) {
		// a failed handover is worth one retry from inside the worker, which has
		// its own fetch and no preload to match
		if (body === undefined && cause instanceof Error && cause.message.startsWith("patch "))
			throw cause;
	}

	const worker = diffWorker();
	const id = nextTaskId++;
	await new Promise<void>((resolve, reject) => {
		pendingTasks.set(id, {
			partial: onFiles,
			resolve: (response) => {
				if (response.error) return reject(new Error(response.error));
				if (response.files?.length) onFiles(response.files);
				resolve();
			},
			reject
		});
		const request: DiffWorkerRequest = body
			? { id, kind: "stream", body }
			: { id, kind: "stream", url };
		worker.postMessage(request, body ? [body] : []);

		// leaving the page stops the parse too: without this the worker keeps
		// chewing through megabytes for a component that no longer exists
		signal?.addEventListener(
			"abort",
			() => {
				if (!pendingTasks.delete(id)) return;
				worker.postMessage({ id, kind: "cancel" } satisfies DiffWorkerRequest);
				resolve();
				retireWhenIdle();
			},
			{ once: true }
		);
	});
};