From f7f660676ddb021ec2dd7bd6609e89d031e98b1e Mon Sep 17 00:00:00 2001 From: otariidae Date: Thu, 22 Jun 2023 22:37:34 +0900 Subject: [PATCH] feat(script-compiler): add error responses (#75) * feat(script-compiler): add error responses * refactor: add missing error handling * feat: unite error types * refactor: use AbortController to remove event listeners * wip: introduce TextlintError * feat: add error response as only error from Textlint worker * refactor: simplify message handler by using promise chain * refactor: handle errors in wrapper functions * refactor: simplify redundant Promise handling --- .../src/CodeGenerator/worker-codegen.ts | 41 ++++++++++- .../@textlint/script-compiler/src/index.ts | 3 +- packages/textchecker-element/index.ts | 70 ++++++++++++++----- .../src/attach-to-text-area.ts | 20 ++++-- .../app/scripts/background/textlint.ts | 50 ++++++++----- 5 files changed, 142 insertions(+), 42 deletions(-) diff --git a/packages/@textlint/script-compiler/src/CodeGenerator/worker-codegen.ts b/packages/@textlint/script-compiler/src/CodeGenerator/worker-codegen.ts index 82ded81..1229868 100644 --- a/packages/@textlint/script-compiler/src/CodeGenerator/worker-codegen.ts +++ b/packages/@textlint/script-compiler/src/CodeGenerator/worker-codegen.ts @@ -40,10 +40,16 @@ export type TextlintWorkerCommandResponseFix = { command: "fix:result"; result: TextlintFixResult; }; +export type TextlintWorkerCommandResponseError = { + id?: MessageId | undefined; + command: "error"; + error: Error; +}; export type TextlintWorkerCommandResponse = | TextlintWorkerCommandResponseInit | TextlintWorkerCommandResponseLint - | TextlintWorkerCommandResponseFix; + | TextlintWorkerCommandResponseFix + | TextlintWorkerCommandResponseError; export const generateCode = async (config: TextlintConfigDescriptor) => { // macro replacement @@ -118,6 +124,24 @@ const assignConfig = (textlintrc) => { }); } }; +self.addEventListener('error', (event) => { + self.postMessage({ + command: "error", + // wrapping any type error with Error + error: new Error("unexpected error", { + cause: event.error + }) + }) +}); +self.addEventListener('unhandledrejection', (event) => { + self.postMessage({ + command: "error", + // wrapping any type error with Error + error: new Error("unexpected unhandled promise rejection", { + cause: event.reason + }) + }) +}); self.addEventListener('message', (event) => { const data = event.data; const rules = data.ruleId === undefined @@ -139,6 +163,15 @@ self.addEventListener('message', (event) => { command: "lint:result", result }); + }).catch(error => { + return self.postMessage({ + id: data.id, + command: "error", + // wrapping any type error with Error + error: new Error("failed to lint text", { + cause: error + }) + }) }); case "fix": return kernel.fixText(data.text, { @@ -153,6 +186,12 @@ self.addEventListener('message', (event) => { command: "fix:result", result }); + }).catch(error => { + return self.postMessage({ + id: data.id, + command: "error", + error + }) }); default: console.log("Unknown command: " + data.command); diff --git a/packages/@textlint/script-compiler/src/index.ts b/packages/@textlint/script-compiler/src/index.ts index 988ea54..27edbb6 100644 --- a/packages/@textlint/script-compiler/src/index.ts +++ b/packages/@textlint/script-compiler/src/index.ts @@ -7,5 +7,6 @@ export type { TextlintWorkerCommandLint, TextlintWorkerCommandResponseFix, TextlintWorkerCommandResponseInit, - TextlintWorkerCommandResponseLint + TextlintWorkerCommandResponseLint, + TextlintWorkerCommandResponseError } from "./CodeGenerator/worker-codegen"; diff --git a/packages/textchecker-element/index.ts b/packages/textchecker-element/index.ts index 2c3bbb9..9346877 100644 --- a/packages/textchecker-element/index.ts +++ b/packages/textchecker-element/index.ts @@ -41,16 +41,22 @@ const generateMessageId = () => crypto.randomUUID(); const createTextlint = ({ worker, ext }: { worker: Worker; ext: string }) => { const lintText: LintEngineAPI["lintText"] = async ({ text }: { text: string }): Promise => { updateStatus("linting..."); - return new Promise((resolve, _reject) => { + const controller = new AbortController(); + const lintPromise = new Promise((resolve, reject) => { const id = generateMessageId(); - worker.addEventListener("message", function handler(event) { - const data: TextlintWorkerCommandResponse = event.data; - if (data.command === "lint:result" && data.id === id) { - resolve([data.result]); - worker.removeEventListener("message", handler); - } - updateStatus("linted"); - }); + worker.addEventListener( + "message", + (event: MessageEvent) => { + const data = event.data; + // global error or ID-specified error + if (data.command === "error" && (!("id" in data) || data.id === id)) { + reject(data.error); + } else if (data.command === "lint:result" && data.id === id) { + resolve([data.result]); + } + }, + { signal: controller.signal } + ); return worker.postMessage({ id, command: "lint", @@ -58,6 +64,17 @@ const createTextlint = ({ worker, ext }: { worker: Worker; ext: string }) => { ext: ext } as TextlintWorkerCommandLint); }); + lintPromise + .then(() => { + updateStatus("linted"); + }) + .catch(() => { + updateStatus("failed to lint"); + }) + .finally(() => { + controller.abort(); + }); + return lintPromise; }; const fixText = async ({ text, @@ -67,16 +84,22 @@ const createTextlint = ({ worker, ext }: { worker: Worker; ext: string }) => { message?: TextlintMessage; }): Promise => { updateStatus("fixing..."); - return new Promise((resolve, _reject) => { + const controller = new AbortController(); + const fixPromise = new Promise((resolve, reject) => { const id = generateMessageId(); - worker.addEventListener("message", function handler(event) { - const data: TextlintWorkerCommandResponse = event.data; - if (data.command === "fix:result" && data.id === id) { - resolve(data.result); - worker.removeEventListener("message", handler); - } - updateStatus("fixed"); - }); + worker.addEventListener( + "message", + (event: MessageEvent) => { + const data = event.data; + // global error or ID-specified error + if (data.command === "error" && (!("id" in data) || data.id === id)) { + reject(data.error); + } else if (data.command === "fix:result" && data.id === id) { + resolve(data.result); + } + }, + { signal: controller.signal } + ); return worker.postMessage({ id, command: "fix", @@ -85,6 +108,17 @@ const createTextlint = ({ worker, ext }: { worker: Worker; ext: string }) => { ext: ext } as TextlintWorkerCommandFix); }); + fixPromise + .then(() => { + updateStatus("fixed"); + }) + .catch(() => { + updateStatus("failed to fix"); + }) + .finally(() => { + controller.abort(); + }); + return fixPromise; }; return { lintText, diff --git a/packages/textchecker-element/src/attach-to-text-area.ts b/packages/textchecker-element/src/attach-to-text-area.ts index 0b74ccd..2ed725e 100644 --- a/packages/textchecker-element/src/attach-to-text-area.ts +++ b/packages/textchecker-element/src/attach-to-text-area.ts @@ -163,7 +163,7 @@ export const attachToTextArea = ({ const currentText = textAreaElement.value; if (currentText === text && currentText !== newText) { textAreaElement.value = newText; - await update(); + await updateOrClearAnnotationsIfFailed(); textCheckerPopup.dismissCard(card); } }; @@ -228,7 +228,7 @@ export const attachToTextArea = ({ text, message }); - await update(); + await updateOrClearAnnotationsIfFailed(); }, onSeeDocument() { const id = message.ruleId.includes("/") @@ -267,24 +267,32 @@ export const attachToTextArea = ({ debug("annotations", annotations); textChecker.updateAnnotations(annotations); }, lintingDebounceMs); + const updateOrClearAnnotationsIfFailed = async () => { + try { + await update(); + } catch (error) { + debug("update error", error); + textChecker.updateAnnotations([]); + } + }; // Events // when resize element, update annotation const resizeObserver = new ResizeObserver(() => { debug("ResizeObserver do update"); textCheckerPopup.dismissCards(); textChecker.resetAnnotations(); - update(); + updateOrClearAnnotationsIfFailed(); }); resizeObserver.observe(textAreaElement); // when scroll window, update annotation const onScroll = () => { textCheckerPopup.dismissCards(); textChecker.resetAnnotations(); - update(); + updateOrClearAnnotationsIfFailed(); }; const onFocus = () => { textCheckerPopup.dismissCards(); - update(); + updateOrClearAnnotationsIfFailed(); }; const onBlur = (event: FocusEvent) => { // does not dismiss on click popup items(require tabindex) @@ -302,7 +310,7 @@ export const attachToTextArea = ({ window.addEventListener("scroll", onScroll); // when scroll the element, update annotation textAreaElement.addEventListener("scroll", onScroll); - update(); + updateOrClearAnnotationsIfFailed(); return () => { window.removeEventListener("scroll", onScroll); textAreaElement.removeEventListener("scroll", onScroll); diff --git a/packages/webextension/app/scripts/background/textlint.ts b/packages/webextension/app/scripts/background/textlint.ts index e4458a4..df9741f 100644 --- a/packages/webextension/app/scripts/background/textlint.ts +++ b/packages/webextension/app/scripts/background/textlint.ts @@ -56,21 +56,30 @@ export const createTextlintWorker = (script: Script) => { const defaultWorker = new Worker(workerUrl); const workerRef = createWorkerRef(defaultWorker); const lintText = async ({ text, ext }: { text: string; ext: string }): Promise => { - return new Promise((resolve, _reject) => { + const controller = new AbortController(); + return new Promise((resolve, reject) => { const id = generateMessageId(); - workerRef.current.addEventListener("message", function handler(event) { - const data: TextlintWorkerCommandResponse = event.data; - if (data.command === "lint:result" && data.id === id) { - resolve([data.result]); - workerRef.current.removeEventListener("message", handler); - } - }); + workerRef.current.addEventListener( + "message", + (event: MessageEvent) => { + const data = event.data; + // global error or ID-specified error + if (data.command === "error" && (!("id" in data) || data.id === id)) { + reject(data.error); + } else if (data.command === "lint:result" && data.id === id) { + resolve([data.result]); + } + }, + { signal: controller.signal } + ); return workerRef.current.postMessage({ id, command: "lint", text, ext } as TextlintWorkerCommandLint); + }).finally(() => { + controller.abort(); }); }; // Note: currently does not use background implementation. @@ -85,15 +94,22 @@ export const createTextlintWorker = (script: Script) => { ext: string; message?: TextlintMessage; }): Promise => { - return new Promise((resolve, _reject) => { + const controller = new AbortController(); + return new Promise((resolve, reject) => { const id = generateMessageId(); - workerRef.current.addEventListener("message", function handler(event) { - const data: TextlintWorkerCommandResponse = event.data; - if (data.command === "fix:result" && data.id === id) { - resolve(data.result); - workerRef.current.removeEventListener("message", handler); - } - }); + workerRef.current.addEventListener( + "message", + (event: MessageEvent) => { + const data = event.data; + // global error or ID-specified error + if (data.command === "error" && (!("id" in data) || data.id === id)) { + reject(data.error); + } else if (data.command === "fix:result" && data.id === id) { + resolve(data.result); + } + }, + { signal: controller.signal } + ); return workerRef.current.postMessage({ id, command: "fix", @@ -101,6 +117,8 @@ export const createTextlintWorker = (script: Script) => { ruleId: message?.ruleId, ext: ext } as TextlintWorkerCommandFix); + }).finally(() => { + controller.abort(); }); }; const mergeConfig = async ({ textlintrc }: { textlintrc: TextlintRcConfig }): Promise => {