mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
305 lines
11 KiB
TypeScript
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;
|