@pierre/diffs
Version:
923 lines (922 loc) • 35 kB
JavaScript
import { DEFAULT_THEMES } from "../constants.js";
import { areLanguagesAttached } from "../highlighter/languages/areLanguagesAttached.js";
import { getThemes } from "../utils/getThemes.js";
import { attachResolvedThemes } from "../highlighter/themes/attachResolvedThemes.js";
import { getSharedHighlighter } from "../highlighter/shared_highlighter.js";
import { areThemesEqual } from "../utils/areThemesEqual.js";
import { hasResolvedThemes } from "../highlighter/themes/hasResolvedThemes.js";
import { areFileRenderOptionsEqual } from "../utils/areFileRenderOptionsEqual.js";
import { getCustomExtensionsMap, getCustomExtensionsVersion, getFiletypeFromFileName } from "../utils/getFiletypeFromFileName.js";
import { isFilePlainText } from "../utils/isFilePlainText.js";
import { renderFileWithHighlighter } from "../utils/renderFileWithHighlighter.js";
import { areDiffTargetsEqual } from "../utils/areDiffTargetsEqual.js";
import { areFilesEqual } from "../utils/areFilesEqual.js";
import { areDiffRenderOptionsEqual } from "../utils/areDiffRenderOptionsEqual.js";
import { isDiffPlainText } from "../utils/isDiffPlainText.js";
import { renderDiffWithHighlighter } from "../utils/renderDiffWithHighlighter.js";
import { getResolvedLanguages } from "../highlighter/languages/getResolvedLanguages.js";
import { hasResolvedLanguages } from "../highlighter/languages/hasResolvedLanguages.js";
import { resolveLanguages } from "../highlighter/languages/resolveLanguages.js";
import { getResolvedThemes } from "../highlighter/themes/getResolvedThemes.js";
import { resolveThemes } from "../highlighter/themes/resolveThemes.js";
import LRUMapPkg from "lru_map";
//#region src/worker/WorkerPoolManager.ts
const IGNORE_RESPONSE = Symbol("IGNORE_RESPONSE");
const DEFAULT_WORKER_INITIALIZATION_TIMEOUT = 1e4;
var WorkerPoolTerminatedError = class extends Error {
constructor() {
super("WorkerPoolManager: operation canceled because the pool terminated");
}
};
var WorkerPoolTaskCanceledError = class extends Error {
constructor() {
super("WorkerPoolManager: operation canceled before the task completed");
}
};
var WorkerPoolManager = class {
options;
highlighter;
preferredHighlighter;
renderOptions;
renderOptionsRequestVersion = 0;
renderOptionsVersion = 0;
initialized = false;
workers = [];
queuedTasks = [];
queuedTaskByInstance = /* @__PURE__ */ new Map();
taskByHighlightKey = /* @__PURE__ */ new Map();
activeTaskById = /* @__PURE__ */ new Map();
activeRequestByInstance = /* @__PURE__ */ new Map();
nextRequestId = 0;
themeSubscribers = /* @__PURE__ */ new Set();
workersFailed = false;
statSubscribers = /* @__PURE__ */ new Set();
fileCache;
diffCache;
_queuedBroadcast;
lifecycleGeneration = 0;
constructor(options, { langs, theme = DEFAULT_THEMES, useTokenTransformer = false, lineDiffType = "word-alt", maxLineDiffLength = 1e3, tokenizeMaxLineLength = 1e3, preferredHighlighter = "shiki-js" }) {
this.options = options;
this.preferredHighlighter = preferredHighlighter;
this.renderOptions = {
theme,
useTokenTransformer,
lineDiffType,
maxLineDiffLength,
tokenizeMaxLineLength
};
this.fileCache = new LRUMapPkg.LRUMap(options.totalASTLRUCacheSize ?? 100);
this.diffCache = new LRUMapPkg.LRUMap(options.totalASTLRUCacheSize ?? 100);
this.queueInitialization(langs);
}
isWorkingPool() {
return !this.workersFailed;
}
getFileResultCache(file) {
return file.cacheKey != null ? this.fileCache.get(file.cacheKey) : void 0;
}
getDiffResultCache(diff) {
return diff.cacheKey != null ? this.diffCache.get(diff.cacheKey) : void 0;
}
inspectCaches() {
const { fileCache, diffCache } = this;
return {
fileCache,
diffCache
};
}
evictFileFromCache(cacheKey) {
try {
return this.fileCache.delete(cacheKey) !== void 0;
} finally {
this.queueBroadcastStateChanges();
}
}
evictDiffFromCache(cacheKey) {
try {
return this.diffCache.delete(cacheKey) !== void 0;
} finally {
this.queueBroadcastStateChanges();
}
}
async setRenderOptions({ theme = this.renderOptions.theme ?? DEFAULT_THEMES, useTokenTransformer = this.renderOptions.useTokenTransformer ?? false, lineDiffType = this.renderOptions.lineDiffType ?? "word-alt", maxLineDiffLength = this.renderOptions.maxLineDiffLength ?? 1e3, tokenizeMaxLineLength = this.renderOptions.tokenizeMaxLineLength ?? 1e3 }) {
const { lifecycleGeneration } = this;
const renderOptionsRequestVersion = ++this.renderOptionsRequestVersion;
const isCurrentRequest = () => this.isCurrentLifecycle(lifecycleGeneration) && this.renderOptionsRequestVersion === renderOptionsRequestVersion;
try {
const newRenderOptions = {
theme,
useTokenTransformer,
lineDiffType,
maxLineDiffLength,
tokenizeMaxLineLength
};
if (!this.isInitialized()) await this.initialize();
if (!isCurrentRequest() || areDiffRenderOptionsEqual(newRenderOptions, this.renderOptions)) return;
const themeNames = getThemes(theme);
let resolvedThemes = [];
if (!areThemesEqual(newRenderOptions.theme, this.renderOptions.theme)) if (hasResolvedThemes(themeNames)) resolvedThemes = getResolvedThemes(themeNames);
else resolvedThemes = await resolveThemes(themeNames);
if (!isCurrentRequest()) return;
if (this.highlighter != null) attachResolvedThemes(resolvedThemes, this.highlighter);
else {
const highlighter = await getSharedHighlighter({
themes: themeNames,
langs: ["text"],
preferredHighlighter: this.preferredHighlighter
});
if (!isCurrentRequest()) return;
this.highlighter = highlighter;
}
const workerSetup = this.setRenderOptionsOnWorkers(newRenderOptions, resolvedThemes);
this.renderOptions = newRenderOptions;
this.renderOptionsVersion++;
this.diffCache.clear();
this.fileCache.clear();
this.invalidateRenderTasks();
for (const instance of this.themeSubscribers) instance.onThemeChange();
await workerSetup;
} catch (error) {
if (error instanceof WorkerPoolTerminatedError || !isCurrentRequest()) return;
throw error;
}
}
getPreferredHighlighter() {
return this.preferredHighlighter;
}
getFileRenderOptions() {
const { tokenizeMaxLineLength, theme, useTokenTransformer } = this.renderOptions;
return {
theme,
useTokenTransformer,
tokenizeMaxLineLength
};
}
getDiffRenderOptions() {
return { ...this.renderOptions };
}
async setRenderOptionsOnWorkers(renderOptions, resolvedThemes) {
if (this.workersFailed) return;
if (!this.isInitialized()) await this.initialize();
const taskPromises = [];
for (const managedWorker of this.workers) {
if (!managedWorker.initialized) {
console.log({ managedWorker });
throw new Error("setRenderOptionsOnWorkers: Somehow we have an uninitialized worker");
}
taskPromises.push(new Promise((resolve, reject) => {
const id = this.generateRequestId();
const task = {
type: "set-render-options",
id,
request: {
type: "set-render-options",
id,
renderOptions,
resolvedThemes
},
resolve,
reject,
requestStart: Date.now()
};
this.activeTaskById.set(id, task);
managedWorker.pendingSetupRequestId = id;
managedWorker.worker.postMessage(task.request);
}));
}
await Promise.all(taskPromises);
}
subscribeToThemeChanges(instance) {
this.themeSubscribers.add(instance);
this.queueBroadcastStateChanges();
return () => {
this.unsubscribeToThemeChanges(instance);
this.queueBroadcastStateChanges();
};
}
unsubscribeToThemeChanges(instance) {
this.themeSubscribers.delete(instance);
this.queueBroadcastStateChanges();
}
subscribeToStatChanges(callback) {
this.statSubscribers.add(callback);
callback(this.getStats());
return () => {
this.statSubscribers.delete(callback);
};
}
queueBroadcastStateChanges() {
if (this._queuedBroadcast != null) return;
this._queuedBroadcast = requestAnimationFrame(this._broadcastStateChanges);
}
_broadcastStateChanges = () => {
if (this._queuedBroadcast != null) {
cancelAnimationFrame(this._queuedBroadcast);
this._queuedBroadcast = void 0;
}
const stats = this.getStats();
for (const callback of this.statSubscribers) callback(stats);
};
cleanUpTasks(instance) {
this.detachInstanceFromQueuedTasks(instance);
const requestId = this.activeRequestByInstance.get(instance);
if (requestId != null) {
const task = this.activeTaskById.get(requestId);
if (isRenderTask(task)) {
this.detachInstanceFromRenderTask(task, instance);
if (!task.primeCache && task.instances.size === 0) this.removeActiveTask(task);
} else this.activeTaskById.delete(requestId);
}
this.activeRequestByInstance.delete(instance);
this.queueBroadcastStateChanges();
}
isInitialized() {
return this.initialized === true;
}
async initialize(languages = []) {
if (this.initialized === true) return;
else if (this.initialized === false) {
const { lifecycleGeneration } = this;
this.initialized = new Promise((resolve, reject) => {
(async () => {
try {
const themes = getThemes(this.renderOptions.theme);
let resolvedThemes = [];
if (hasResolvedThemes(themes)) resolvedThemes = getResolvedThemes(themes);
else resolvedThemes = await resolveThemes(themes);
if (!this.isCurrentLifecycle(lifecycleGeneration)) {
resolve();
return;
}
let resolvedLanguages = [];
if (hasResolvedLanguages(languages)) resolvedLanguages = getResolvedLanguages(languages);
else resolvedLanguages = await resolveLanguages(languages);
if (!this.isCurrentLifecycle(lifecycleGeneration)) {
resolve();
return;
}
const [highlighter] = await Promise.all([getSharedHighlighter({
themes,
langs: ["text", ...languages],
preferredHighlighter: this.preferredHighlighter
}), this.initializeWorkers(resolvedThemes, resolvedLanguages)]);
if (!this.isCurrentLifecycle(lifecycleGeneration)) {
this.terminateWorkers();
resolve();
return;
}
this.highlighter = highlighter;
this.initialized = true;
this.diffCache.clear();
this.fileCache.clear();
this.drainQueue();
this.queueBroadcastStateChanges();
resolve();
} catch (e) {
if (e instanceof WorkerPoolTerminatedError || !this.isCurrentLifecycle(lifecycleGeneration)) {
resolve();
return;
}
this.initialized = false;
this.workersFailed = true;
this.cancelActiveWorkerTasks();
this.terminateWorkers();
this.activeTaskById.clear();
for (const task of this.queuedTasks) this.rejectRenderTaskCallbacks(task, normalizeWorkerError(e));
this.queueBroadcastStateChanges();
reject(e);
}
})();
});
this.queueBroadcastStateChanges();
} else return this.initialized;
}
async initializeWorkers(resolvedThemes, resolvedLanguages) {
this.workersFailed = false;
const initPromises = [];
const customExtensionVersion = getCustomExtensionsVersion();
const customExtensionMap = customExtensionVersion > 0 ? getCustomExtensionsMap() : void 0;
if (this.workers.length > 0) this.terminateWorkers();
for (let i = 0; i < (this.options.poolSize ?? 8); i++) {
const worker = this.options.workerFactory();
const managedWorker = {
worker,
requestId: void 0,
pendingSetupRequestId: void 0,
initialized: false,
langs: /* @__PURE__ */ new Set(["text", ...resolvedLanguages.map(({ name }) => name)]),
customExtensionsVersion: 0
};
worker.addEventListener("message", (event) => {
this.handleWorkerMessage(managedWorker, event.data);
});
worker.addEventListener("error", (error) => this.handleWorkerError(managedWorker, error));
this.workers.push(managedWorker);
initPromises.push(new Promise((resolve, reject) => {
const id = this.generateRequestId();
const task = {
type: "initialize",
id,
request: {
type: "initialize",
id,
renderOptions: this.renderOptions,
preferredHighlighter: this.preferredHighlighter,
resolvedThemes,
resolvedLanguages,
customExtensionsVersion: customExtensionMap != null ? customExtensionVersion : void 0,
customExtensionMap
},
resolve() {
managedWorker.initialized = true;
resolve();
},
reject,
requestStart: Date.now()
};
managedWorker.pendingSetupRequestId = id;
this.activeTaskById.set(id, task);
this.executeTask(managedWorker, task);
}));
}
const timeoutMs = this.options.workerInitializationTimeout ?? DEFAULT_WORKER_INITIALIZATION_TIMEOUT;
let timeoutId;
try {
await Promise.race([Promise.all(initPromises), new Promise((_, reject) => {
timeoutId = setTimeout(() => {
reject(/* @__PURE__ */ new Error(`WorkerPoolManager: worker initialization timed out after ${timeoutMs}ms`));
}, timeoutMs);
})]);
} finally {
if (timeoutId != null) clearTimeout(timeoutId);
}
}
handleWorkerError(managedWorker, error) {
console.error("Worker error:", error, managedWorker);
const { pendingSetupRequestId } = managedWorker;
if (pendingSetupRequestId == null) return;
const task = this.activeTaskById.get(pendingSetupRequestId);
if (task?.type !== "initialize") return;
task.reject(normalizeWorkerError(error));
this.cleanWorkerAndTask(managedWorker, task);
}
drainQueue = () => {
this._queuedDrain = void 0;
if (this.initialized !== true || this.queuedTasks.length === 0) return;
for (let i = 0; i < this.queuedTasks.length;) {
const task = this.queuedTasks[i];
if (this.hasActiveRequest(task)) {
i++;
continue;
}
const langs = getLangsFromTask(task);
const availableWorker = this.getAvailableWorker(langs);
if (availableWorker == null) break;
this.queuedTasks.splice(i, 1);
this.assignWorkerToTask(task, availableWorker);
this.resolveLanguagesAndExecuteTask(availableWorker, task, langs);
}
this.queueBroadcastStateChanges();
};
highlightFileAST(instance, file) {
const cachedResult = this.getFileResultCache(file);
if (isFilePlainText(file) || cachedResult != null && areFileRenderOptionsEqual(cachedResult.options, this.getFileRenderOptions())) return;
if (!this.hasMatchingFileInstanceTask(instance, file)) this.submitTask(instance, {
type: "file",
file
});
}
primeFileHighlightCache(file) {
if (!this.isWorkingPool()) return Promise.reject(/* @__PURE__ */ new Error("WorkerPoolManager.primeFileHighlightCache: worker pool is not working"));
const task = this.getOrCreateFileHighlightCacheTask(file);
return task != null ? this.createRenderTaskCallbacks(task) : Promise.resolve();
}
getOrCreateFileHighlightCacheTask(file) {
if (file.cacheKey == null) {
console.warn(`WorkerPoolManager.primeFileHighlightCache: priming highlight cache requires file.cacheKey; skipping "${file.name}".`);
return;
}
const cachedResult = this.getFileResultCache(file);
const highlightKey = this.getFileHighlightKey(file);
if (highlightKey == null || isFilePlainText(file) || cachedResult != null && areFileRenderOptionsEqual(cachedResult.options, this.getFileRenderOptions())) return;
const existingTask = this.getTaskByHighlightKey(highlightKey);
if (existingTask != null) {
existingTask.primeCache = true;
return existingTask.type === "file" ? existingTask : void 0;
} else {
const task = this.submitCacheTask({
type: "file",
file
}, highlightKey);
return task.type === "file" ? task : void 0;
}
}
getPlainFileAST(file, startingLine, totalLines, lines) {
if (this.highlighter == null) {
this.queueInitialization();
return;
}
return renderFileWithHighlighter(file, this.highlighter, this.renderOptions, {
forcePlainText: true,
startingLine,
totalLines,
lines
});
}
highlightDiffAST(instance, diff) {
const cachedResult = this.getDiffResultCache(diff);
if (isDiffPlainText(diff) || cachedResult != null && areDiffRenderOptionsEqual(cachedResult.options, this.getDiffRenderOptions())) return;
if (!this.hasMatchingDiffInstanceTask(instance, diff)) this.submitTask(instance, {
type: "diff",
diff
});
}
primeDiffHighlightCache(diff) {
if (!this.isWorkingPool()) return Promise.reject(/* @__PURE__ */ new Error("WorkerPoolManager.primeDiffHighlightCache: worker pool is not working"));
const task = this.getOrCreateDiffHighlightCacheTask(diff);
return task != null ? this.createRenderTaskCallbacks(task) : Promise.resolve();
}
getOrCreateDiffHighlightCacheTask(diff) {
if (diff.cacheKey == null) {
console.warn(`WorkerPoolManager.primeDiffHighlightCache: priming highlight cache requires diff.cacheKey; skipping "${diff.prevName ?? diff.name}" -> "${diff.name}".`);
return;
}
const cachedResult = this.getDiffResultCache(diff);
const highlightKey = this.getDiffHighlightKey(diff);
if (highlightKey == null || isDiffPlainText(diff) || cachedResult != null && areDiffRenderOptionsEqual(cachedResult.options, this.getDiffRenderOptions())) return;
const existingTask = this.getTaskByHighlightKey(highlightKey);
if (existingTask != null) {
existingTask.primeCache = true;
return existingTask.type === "diff" ? existingTask : void 0;
} else {
const task = this.submitCacheTask({
type: "diff",
diff
}, highlightKey);
return task.type === "diff" ? task : void 0;
}
}
getPlainDiffAST(diff, startingLine, totalLines, expandedHunks, collapsedContextThreshold) {
return this.highlighter != null ? renderDiffWithHighlighter(diff, this.highlighter, this.renderOptions, {
forcePlainText: true,
startingLine,
totalLines,
expandedHunks,
collapsedContextThreshold
}) : void 0;
}
terminate() {
this.lifecycleGeneration++;
this.cancelActiveWorkerTasks();
this.terminateWorkers();
const error = new WorkerPoolTerminatedError();
for (const task of this.queuedTasks) this.rejectRenderTaskCallbacks(task, error);
this.fileCache.clear();
this.diffCache.clear();
this.activeRequestByInstance.clear();
this.queuedTasks.length = 0;
this.queuedTaskByInstance.clear();
this.taskByHighlightKey.clear();
this.activeTaskById.clear();
this.highlighter = void 0;
this.initialized = false;
this.workersFailed = false;
this.queueBroadcastStateChanges();
}
isCurrentLifecycle(lifecycleGeneration) {
return this.lifecycleGeneration === lifecycleGeneration;
}
queueInitialization(languages) {
this.initialize(languages).catch((error) => {
console.error(error);
});
}
cancelActiveWorkerTasks() {
const error = new WorkerPoolTerminatedError();
for (const task of this.activeTaskById.values()) if ("reject" in task) task.reject(error);
else if (isRenderTask(task)) this.rejectRenderTaskCallbacks(task, error);
}
terminateWorkers() {
for (const managedWorker of this.workers) managedWorker.worker.terminate();
this.workers.length = 0;
}
getStats() {
return {
managerState: (() => {
if (this.initialized === false) return "waiting";
if (this.initialized !== true) return "initializing";
return "initialized";
})(),
totalWorkers: this.workers.length,
workersFailed: this.workersFailed,
busyWorkers: this.workers.filter((w) => w.requestId != null || w.pendingSetupRequestId != null).length,
queuedTasks: this.queuedTasks.length,
activeTasks: this.activeTaskById.size,
themeSubscribers: this.themeSubscribers.size,
fileCacheSize: this.fileCache.size,
diffCacheSize: this.diffCache.size
};
}
submitTask(instance, request) {
if (this.initialized === false) this.queueInitialization();
const highlightKey = this.getHighlightKeyForRequest(request);
const existingTask = highlightKey != null ? this.getTaskByHighlightKey(highlightKey) : void 0;
if (existingTask != null) {
this.detachInstanceFromQueuedTasks(instance, existingTask);
this.addInstanceToTask(existingTask, instance);
this.queueBroadcastStateChanges();
return;
}
this.detachInstanceFromQueuedTasks(instance);
const id = this.generateRequestId();
const requestStart = Date.now();
const { renderOptionsVersion } = this;
const task = (() => {
switch (request.type) {
case "file": return {
type: "file",
id,
request: {
...request,
id
},
instances: /* @__PURE__ */ new Set([instance]),
primeCache: false,
highlightKey,
callbacks: /* @__PURE__ */ new Set(),
renderOptionsVersion,
requestStart
};
case "diff": return {
type: "diff",
id,
request: {
...request,
id
},
instances: /* @__PURE__ */ new Set([instance]),
primeCache: false,
highlightKey,
callbacks: /* @__PURE__ */ new Set(),
renderOptionsVersion,
requestStart
};
}
})();
this.enqueueRenderTask(task, instance);
}
submitCacheTask(request, highlightKey) {
if (this.initialized === false) this.queueInitialization();
const id = this.generateRequestId();
const requestStart = Date.now();
const { renderOptionsVersion } = this;
const task = (() => {
switch (request.type) {
case "file": return {
type: "file",
id,
request: {
...request,
id
},
instances: /* @__PURE__ */ new Set(),
primeCache: true,
highlightKey,
callbacks: /* @__PURE__ */ new Set(),
renderOptionsVersion,
requestStart
};
case "diff": return {
type: "diff",
id,
request: {
...request,
id
},
instances: /* @__PURE__ */ new Set(),
primeCache: true,
highlightKey,
callbacks: /* @__PURE__ */ new Set(),
renderOptionsVersion,
requestStart
};
}
})();
this.enqueueRenderTask(task);
return task;
}
enqueueRenderTask(task, instance) {
this.queuedTasks.push(task);
if (instance != null) this.queuedTaskByInstance.set(instance, task);
if (task.highlightKey != null) this.taskByHighlightKey.set(task.highlightKey, task);
this.queueDrain();
}
async resolveLanguagesAndExecuteTask(availableWorker, task, langs) {
try {
const mainThreadLangs = langs.filter((lang) => !areLanguagesAttached(lang));
if (mainThreadLangs.length > 0) getSharedHighlighter({
themes: getThemes(this.renderOptions.theme),
langs: ["text", ...mainThreadLangs],
preferredHighlighter: this.preferredHighlighter
}).catch((error) => {
console.error(error);
});
const workerMissingLangs = langs.filter((lang) => !availableWorker.langs.has(lang));
if (workerMissingLangs.length > 0) if (hasResolvedLanguages(workerMissingLangs)) task.request.resolvedLanguages = getResolvedLanguages(workerMissingLangs);
else task.request.resolvedLanguages = await resolveLanguages(workerMissingLangs);
if (!this.activeTaskById.has(task.id)) {
if (availableWorker.requestId === task.id) {
this.cleanWorkerAndTask(availableWorker, task);
this.queueBroadcastStateChanges();
if (this.queuedTasks.length > 0) this.queueDrain();
}
return;
}
this.executeTask(availableWorker, task);
} catch (error) {
this.rejectRenderTaskCallbacks(task, normalizeWorkerError(error));
this.cleanWorkerAndTask(availableWorker, task);
this.queueBroadcastStateChanges();
if (this.queuedTasks.length > 0) this.queueDrain();
}
}
handleWorkerMessage(managedWorker, response) {
const task = this.activeTaskById.get(response.id);
try {
if (task == null) throw IGNORE_RESPONSE;
else if (response.type === "error") {
const error = new Error(response.error);
if (response.stack) error.stack = response.stack;
if ("reject" in task) task.reject(error);
else if (isRenderTask(task)) {
this.notifyHighlightError(task, error);
this.rejectRenderTaskCallbacks(task, error);
} else throw new Error("handleWorkerMessage: unknown task type");
throw error;
} else switch (response.requestType) {
case "initialize":
if (task.type !== "initialize") throw new Error("handleWorkerMessage: task/response dont match");
this.syncCustomExtensionVersion(managedWorker, task.request);
task.resolve();
break;
case "set-render-options":
if (task.type !== "set-render-options") throw new Error("handleWorkerMessage: task/response dont match");
task.resolve();
break;
case "file": {
if (task.type !== "file") throw new Error("handleWorkerMessage: task/response dont match");
const { result, options } = response;
if (!this.isCurrentRenderTask(task) || !areFileRenderOptionsEqual(options, this.getFileRenderOptions())) throw IGNORE_RESPONSE;
const { request } = task;
this.syncCustomExtensionVersion(managedWorker, request);
if (task.cacheKeyAtDispatch != null) this.fileCache.set(task.cacheKeyAtDispatch, {
result,
options
});
this.resolveRenderTaskCallbacks(task);
this.notifyFileInstances(task, result, options);
break;
}
case "diff": {
if (task.type !== "diff") throw new Error("handleWorkerMessage: task/response dont match");
const { result, options } = response;
if (!this.isCurrentRenderTask(task) || !areDiffRenderOptionsEqual(options, this.getDiffRenderOptions())) throw IGNORE_RESPONSE;
const { request } = task;
this.syncCustomExtensionVersion(managedWorker, request);
if (task.cacheKeyAtDispatch != null) this.diffCache.set(task.cacheKeyAtDispatch, {
result,
options
});
this.resolveRenderTaskCallbacks(task);
this.notifyDiffInstances(task, result, options);
break;
}
}
} catch (error) {
if (error !== IGNORE_RESPONSE) console.error(error, task, response);
}
this.cleanWorkerAndTask(managedWorker, task, response.id);
this.queueBroadcastStateChanges();
if (this.queuedTasks.length > 0) this.queueDrain();
}
_queuedDrain;
queueDrain() {
if (this._queuedDrain != null) return;
this._queuedDrain = Promise.resolve().then(this.drainQueue);
this.queueBroadcastStateChanges();
}
assignWorkerToTask(task, managedWorker) {
managedWorker.requestId = task.id;
if (isRenderTask(task)) {
this.clearQueuedInstanceRequests(task);
this.trackInstanceRequests(task);
}
this.activeTaskById.set(task.id, task);
}
cleanWorkerAndTask(managedWorker, task, requestId = task?.id) {
if (managedWorker.requestId === requestId) managedWorker.requestId = void 0;
if (managedWorker.pendingSetupRequestId === requestId) managedWorker.pendingSetupRequestId = void 0;
if (task != null) {
if (isRenderTask(task)) {
this.rejectRenderTaskCallbacks(task, new WorkerPoolTaskCanceledError());
this.clearInstanceRequests(task);
this.clearHighlightKey(task);
}
this.activeTaskById.delete(task.id);
}
}
executeTask(managedWorker, task) {
if (shouldSyncCustomExtensions(task.request)) this.maybeAttachCustomExtensions(managedWorker, task.request);
if (!this.activeTaskById.has(task.id)) this.assignWorkerToTask(task, managedWorker);
for (const lang of getLangsFromTask(task)) managedWorker.langs.add(lang);
try {
if (task.type === "file") task.cacheKeyAtDispatch = task.request.file.cacheKey;
else if (task.type === "diff") task.cacheKeyAtDispatch = task.request.diff.cacheKey;
managedWorker.worker.postMessage(task.request);
} catch (error) {
console.error("Failed to post message to worker:", error);
if (isRenderTask(task)) {
this.notifyHighlightError(task, error);
this.rejectRenderTaskCallbacks(task, normalizeWorkerError(error));
} else if ("reject" in task) task.reject(error);
this.cleanWorkerAndTask(managedWorker, task);
if (this.queuedTasks.length > 0) this.queueDrain();
}
this.queueBroadcastStateChanges();
}
maybeAttachCustomExtensions(managedWorker, request) {
if (request.customExtensionsVersion != null) return;
const version = getCustomExtensionsVersion();
if (managedWorker.customExtensionsVersion >= version) return;
request.customExtensionsVersion = version;
request.customExtensionMap = getCustomExtensionsMap();
}
syncCustomExtensionVersion(managedWorker, request) {
if (request.customExtensionsVersion == null) return;
managedWorker.customExtensionsVersion = request.customExtensionsVersion;
}
getAvailableWorker(langs) {
let worker;
for (const managedWorker of this.workers) {
if (managedWorker.requestId != null || managedWorker.pendingSetupRequestId != null || !managedWorker.initialized) continue;
worker = managedWorker;
if (langs.length === 0) break;
let hasEveryLang = true;
for (const lang of langs) if (!managedWorker.langs.has(lang)) {
hasEveryLang = false;
break;
}
if (hasEveryLang) break;
}
return worker;
}
getFileHighlightKey(file) {
if (file.cacheKey == null) return;
return `file:${file.cacheKey}:${this.renderOptionsVersion}`;
}
getDiffHighlightKey(diff) {
if (diff.cacheKey == null) return;
return `diff:${diff.cacheKey}:${this.renderOptionsVersion}`;
}
getHighlightKeyForRequest(request) {
switch (request.type) {
case "file": return this.getFileHighlightKey(request.file);
case "diff": return this.getDiffHighlightKey(request.diff);
}
}
hasActiveRequest(task) {
for (const instance of getInstances(task)) if (this.activeRequestByInstance.has(instance)) return true;
return false;
}
addInstanceToTask(task, instance) {
if (task.type === "file") task.instances.add(instance);
else task.instances.add(instance);
if (this.activeTaskById.has(task.id)) this.activeRequestByInstance.set(instance, task.id);
else this.queuedTaskByInstance.set(instance, task);
}
detachInstanceFromQueuedTasks(instance, exceptTask) {
const task = this.queuedTaskByInstance.get(instance);
if (task == null || task === exceptTask) return;
this.queuedTaskByInstance.delete(instance);
this.detachInstanceFromRenderTask(task, instance);
if (!task.primeCache && task.instances.size === 0) this.removeQueuedTask(task);
}
detachInstanceFromRenderTask(task, instance) {
if (task.type === "file") task.instances.delete(instance);
else task.instances.delete(instance);
}
removeQueuedTask(task) {
const index = this.queuedTasks.indexOf(task);
if (index !== -1) this.queuedTasks.splice(index, 1);
this.rejectRenderTaskCallbacks(task, new WorkerPoolTaskCanceledError());
this.clearQueuedInstanceRequests(task);
this.clearHighlightKey(task);
}
removeActiveTask(task) {
this.rejectRenderTaskCallbacks(task, new WorkerPoolTaskCanceledError());
this.clearInstanceRequests(task);
this.clearHighlightKey(task);
this.activeTaskById.delete(task.id);
}
createRenderTaskCallbacks(task) {
return new Promise((resolve, reject) => {
task.callbacks.add({
resolve,
reject
});
});
}
resolveRenderTaskCallbacks(task) {
for (const callbacks of task.callbacks) callbacks.resolve();
task.callbacks.clear();
}
rejectRenderTaskCallbacks(task, error) {
for (const callbacks of task.callbacks) callbacks.reject(error);
task.callbacks.clear();
}
invalidateRenderTasks() {
for (let index = this.queuedTasks.length - 1; index >= 0; index--) {
const task = this.queuedTasks[index];
if (task.renderOptionsVersion !== this.renderOptionsVersion) this.removeQueuedTask(task);
}
for (const task of Array.from(this.activeTaskById.values())) if (isRenderTask(task) && task.renderOptionsVersion !== this.renderOptionsVersion) this.removeActiveTask(task);
}
clearQueuedInstanceRequests(task) {
for (const instance of getInstances(task)) if (this.queuedTaskByInstance.get(instance) === task) this.queuedTaskByInstance.delete(instance);
}
clearHighlightKey(task) {
if (task.highlightKey != null && this.taskByHighlightKey.get(task.highlightKey) === task) this.taskByHighlightKey.delete(task.highlightKey);
}
trackInstanceRequests(task) {
for (const instance of getInstances(task)) this.activeRequestByInstance.set(instance, task.id);
}
clearInstanceRequests(task) {
for (const instance of getInstances(task)) if (this.activeRequestByInstance.get(instance) === task.id) this.activeRequestByInstance.delete(instance);
}
notifyFileInstances(task, result, options) {
for (const instance of task.instances) if (this.activeRequestByInstance.get(instance) === task.id) instance.onHighlightSuccess(task.request.file, result, options);
}
notifyDiffInstances(task, result, options) {
for (const instance of task.instances) if (this.activeRequestByInstance.get(instance) === task.id) instance.onHighlightSuccess(task.request.diff, result, options);
}
notifyHighlightError(task, error) {
for (const instance of getInstances(task)) if (this.activeRequestByInstance.get(instance) === task.id) instance.onHighlightError(error);
}
hasMatchingFileInstanceTask(instance, file) {
for (const task of this.iterateRenderTasks()) if (task.type === "file" && this.isCurrentRenderTask(task) && task.instances.has(instance) && areFilesEqual(file, task.request.file)) return true;
return false;
}
hasMatchingDiffInstanceTask(instance, diff) {
for (const task of this.iterateRenderTasks()) if (task.type === "diff" && this.isCurrentRenderTask(task) && task.instances.has(instance) && areDiffTargetsEqual(task.request.diff, diff)) return true;
return false;
}
getTaskByHighlightKey(highlightKey) {
const task = this.taskByHighlightKey.get(highlightKey);
return task != null && this.isCurrentRenderTask(task) ? task : void 0;
}
isCurrentRenderTask(task) {
return task.renderOptionsVersion === this.renderOptionsVersion;
}
*iterateRenderTasks() {
for (const task of this.queuedTasks) yield task;
for (const task of this.activeTaskById.values()) if (isRenderTask(task)) yield task;
}
generateRequestId() {
return `req_${++this.nextRequestId}`;
}
};
function shouldSyncCustomExtensions(request) {
return request.type === "initialize" || request.type === "file" || request.type === "diff";
}
function getLangsFromTask(task) {
const langs = /* @__PURE__ */ new Set();
if (task.type === "initialize" || task.type === "set-render-options") return [];
switch (task.type) {
case "file":
langs.add(task.request.file.lang ?? getFiletypeFromFileName(task.request.file.name));
break;
case "diff":
langs.add(task.request.diff.lang ?? getFiletypeFromFileName(task.request.diff.name));
langs.add(task.request.diff.lang ?? getFiletypeFromFileName(task.request.diff.prevName ?? "-"));
break;
}
langs.delete("text");
return Array.from(langs);
}
function isRenderTask(task) {
return task?.type === "file" || task?.type === "diff";
}
function getInstances(task) {
return task.instances;
}
function normalizeWorkerError(error) {
if (error instanceof Error) return error;
if (error != null && typeof error === "object") {
if ("error" in error && error.error instanceof Error) return error.error;
if ("message" in error && typeof error.message === "string") return new Error(error.message);
}
return new Error(String(error));
}
//#endregion
export { WorkerPoolManager };
//# sourceMappingURL=WorkerPoolManager.js.map