Files
trade-message-center/apps/chrome-extension/src/service-worker-entry.ts
T

305 lines
11 KiB
TypeScript

// 启动页面桥并管理可配置的 OneTalk 同步运行时
import {
composeOneTalkDiagnosticLog,
isPlainRecord,
type OneTalkDiagnosticLogEvent,
} from "@trade-message-center/onetalk-contract";
import {
clearOneTalkExtensionConfig,
isMaskedOneTalkBinding,
isOneTalkConfigMessage,
isOneTalkPopupErrorCode,
loadOrCreateOneTalkDeviceId,
loadOneTalkExtensionConfig,
ONE_TALK_EXTENSION_CONFIG_KEY,
OneTalkExtensionConfigError,
type OneTalkConfigStatusEvent,
saveOneTalkExtensionConfig,
type OneTalkConfigMessageResponse,
type OneTalkExtensionConfig,
type OneTalkExtensionStorageArea,
type OneTalkPopupErrorCode,
} from "./onetalk/config.ts";
import {
createOneTalkServiceWorkerSyncController,
type OneTalkServiceWorkerSyncController,
type OneTalkServiceWorkerSyncSnapshot,
} from "./onetalk/service-worker/sync-runtime.ts";
import { logOneTalkError } from "./onetalk/service-worker/error-diagnostics.ts";
import type { OneTalkServiceWorkerPort } from "./onetalk/service-worker/runtime.ts";
import { BRIGHT_WEBSOCKET_URL, ENABLE_ONE_TALK_DIAGNOSTICS } from "./onetalk/build-config.ts";
import { OneTalkBrightClientError } from "./onetalk/service-worker/transport/bright-client.ts";
declare const chrome: {
runtime: {
id?: string;
onConnect: {
addListener(listener: (port: OneTalkServiceWorkerPort) => void): void;
};
onMessage: {
addListener(
listener: (
message: unknown,
sender: unknown,
) => Promise<OneTalkConfigMessageResponse>,
): void;
};
sendMessage: (message: OneTalkConfigStatusEvent) => Promise<unknown>;
};
storage: {
local: OneTalkExtensionStorageArea;
onChanged: {
addListener(
listener: (changes: Record<string, unknown>, areaName: string) => void,
): void;
};
};
};
const logOneTalkDiagnostic = <T extends OneTalkDiagnosticLogEvent>(event: T): void => {
if (!ENABLE_ONE_TALK_DIAGNOSTICS) return;
const diagnostic = composeOneTalkDiagnosticLog("extension", event);
console[diagnostic.level](diagnostic.headline, diagnostic.payload);
};
const isNoPopupReceiverError = (error: unknown): boolean => {
const message =
error instanceof Error ? error.message : typeof error === "string" ? error : undefined;
if (message === undefined) return false;
return [
"Could not establish connection. Receiving end does not exist",
"The message port closed before a response was received",
].some((prefix) => message.trim().replace(/[.]$/u, "") === prefix);
};
const reportStatusBroadcastError = (error: unknown): void => {
if (isNoPopupReceiverError(error)) return;
logOneTalkError(error, "runtime_error", ENABLE_ONE_TALK_DIAGNOSTICS, console.error);
};
export const oneTalkEntryDiagnosticErrorCode = (error: unknown): OneTalkPopupErrorCode => {
if (
(error instanceof OneTalkExtensionConfigError ||
error instanceof OneTalkBrightClientError) &&
isOneTalkPopupErrorCode(error.code)
) {
return error.code;
}
return "runtime_error";
};
let lastBroadcastStatus: OneTalkConfigStatusEvent | undefined;
const broadcastStatus = (snapshot: OneTalkServiceWorkerSyncSnapshot): void => {
const event: OneTalkConfigStatusEvent = {
type: "onetalk.config.status",
connectionStatus: snapshot.connectionStatus,
...(snapshot.error === undefined ? {} : { error: snapshot.error }),
};
if (
lastBroadcastStatus?.connectionStatus === event.connectionStatus &&
lastBroadcastStatus?.error === event.error
)
return;
lastBroadcastStatus = event;
try {
void chrome.runtime.sendMessage(event).catch(reportStatusBroadcastError);
} catch (error: unknown) {
reportStatusBroadcastError(error);
}
};
const controller: OneTalkServiceWorkerSyncController = createOneTalkServiceWorkerSyncController({
onBrightDiagnostic: logOneTalkDiagnostic,
onPageDiagnostic: logOneTalkDiagnostic,
onEngineDiagnostic: logOneTalkDiagnostic,
onProfileDiagnostic: logOneTalkDiagnostic,
onStatusChange: (snapshot) => {
broadcastStatus(snapshot);
logOneTalkDiagnostic({
event: "state_snapshot",
online: snapshot.engineStatus?.online ?? false,
brightStatus: snapshot.connectionStatus,
anchorSnapshotReceived: snapshot.engineStatus?.anchorSnapshotReceived ?? false,
pageReady: snapshot.engineStatus?.pageReady ?? false,
bootstrapStatus: snapshot.engineStatus?.bootstrapStatus ?? "idle",
lastError: snapshot.engineStatus?.lastError ?? snapshot.error,
});
},
onError: (error: unknown) => {
logOneTalkError(
error,
oneTalkEntryDiagnosticErrorCode(error),
ENABLE_ONE_TALK_DIAGNOSTICS,
console.error,
);
},
});
const storage = chrome.storage.local;
let configurationQueue = Promise.resolve();
let appliedConfiguration: OneTalkExtensionConfig | null | undefined;
let deviceId: string | undefined;
const enqueueConfiguration = (operation: () => Promise<void>): Promise<void> => {
const next = configurationQueue.then(operation, operation);
configurationQueue = next.then(
() => undefined,
() => undefined,
);
return next;
};
const sameConfiguration = (
left: OneTalkExtensionConfig | null | undefined,
right: OneTalkExtensionConfig | null,
): boolean => {
if (left === undefined || left === null || right === null) return left === right;
return (
left.brightWebSocketUrl === right.brightWebSocketUrl &&
left.channelAccountId === right.channelAccountId &&
left.deviceId === right.deviceId &&
left.binding === right.binding
);
};
const applyConfiguration = async (config: OneTalkExtensionConfig | null): Promise<void> => {
if (sameConfiguration(appliedConfiguration, config)) return;
await controller.configure(config);
appliedConfiguration = config;
};
const applyStoredConfiguration = async (): Promise<void> => {
try {
const config = await loadOneTalkExtensionConfig(storage, BRIGHT_WEBSOCKET_URL);
await applyConfiguration(config);
} catch (error: unknown) {
if (error instanceof OneTalkExtensionConfigError) {
await applyConfiguration(null);
controller.setConfigurationError(error.code);
return;
}
await applyConfiguration(null);
controller.setConfigurationError("storage_unavailable");
}
};
const responseFor = (): OneTalkConfigMessageResponse => {
const snapshot = controller.getSnapshot();
return {
ok: true,
config: snapshot.config,
deviceId: deviceId ?? snapshot.config?.deviceId ?? "",
connectionStatus: snapshot.connectionStatus,
permissions: [...snapshot.permissions],
...(snapshot.authorizationVersion === undefined
? {}
: { authorizationVersion: snapshot.authorizationVersion }),
...(snapshot.error === undefined ? {} : { error: snapshot.error }),
};
};
const handleConfigurationMessage = async (
message: unknown,
sender: unknown,
): Promise<OneTalkConfigMessageResponse> => {
if (!isAllowedPopupSender(sender)) return { ok: false, code: "sender_not_allowed" };
if (!isOneTalkConfigMessage(message)) {
return { ok: false, code: "invalid_configuration" };
}
try {
if (message.type === "onetalk.config.get") {
await initialization;
return responseFor();
}
if (message.type === "onetalk.config.clear") {
await enqueueConfiguration(async () => {
await clearOneTalkExtensionConfig(storage);
await applyConfiguration(null);
});
return responseFor();
}
await enqueueConfiguration(async () => {
const current = await loadOneTalkExtensionConfig(storage, BRIGHT_WEBSOCKET_URL).catch(
(error: unknown) => {
if (
error instanceof OneTalkExtensionConfigError &&
error.code !== "storage_unavailable"
) {
return null;
}
throw error;
},
);
const configPatch = isPlainRecord(message.config) ? message.config : {};
const {
binding,
brightWebSocketUrl: providedBrightWebSocketUrl,
deviceId: providedDeviceId,
...configWithoutDeviceId
} = configPatch;
void providedBrightWebSocketUrl;
void providedDeviceId;
const candidate = {
...(current ?? {}),
...configWithoutDeviceId,
...(isMaskedOneTalkBinding(binding) ? {} : { binding }),
};
const config = await saveOneTalkExtensionConfig(
candidate,
storage,
BRIGHT_WEBSOCKET_URL,
);
await applyConfiguration(config);
});
return responseFor();
} catch (error: unknown) {
if (error instanceof OneTalkExtensionConfigError) {
return { ok: false, code: error.code };
}
return { ok: false, code: "storage_unavailable" };
}
};
const isAllowedPopupSender = (sender: unknown): boolean => {
if (!isPlainRecord(sender) || typeof sender.url !== "string" || !chrome.runtime.id)
return false;
try {
const url = new URL(sender.url);
return (
url.protocol === "chrome-extension:" &&
url.hostname === chrome.runtime.id &&
url.pathname === "/popup/popup.html"
);
} catch {
return false;
}
};
const installServiceWorkerListeners = (): void => {
chrome.runtime.onConnect.addListener(controller.runtime.handleConnect);
chrome.runtime.onMessage.addListener(handleConfigurationMessage);
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== "local" || !(ONE_TALK_EXTENSION_CONFIG_KEY in changes)) return;
void enqueueConfiguration(applyStoredConfiguration).catch((error: unknown) => {
controller.setConfigurationError(
error instanceof OneTalkExtensionConfigError ? error.code : "storage_unavailable",
);
});
});
};
const initialization = enqueueConfiguration(async () => {
deviceId = await loadOrCreateOneTalkDeviceId(storage);
await applyStoredConfiguration();
});
/** 在 Service Worker 顶层安装页面和配置监听。 */
installServiceWorkerListeners();
export const oneTalkServiceWorkerRuntime = controller.runtime;
export const oneTalkServiceWorkerController = controller;