Migrate remaining legacy operator workflows

This commit is contained in:
2026-07-12 05:39:27 +09:00
parent ffb8f43c19
commit a01836a2d7
132 changed files with 20566 additions and 720 deletions

View File

@@ -16,6 +16,16 @@
const rowVersionPattern = /^[a-f0-9]{64}$/;
const unsafeTextPattern = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}\uFFFD]/u;
const audiences = Object.freeze(["INDIVIDUAL", "FOREIGN", "INSTITUTION"]);
const statusCodes = new Set([
"READY",
"IMPORT_INTERRUPTED",
"IMPORTED_STATE_INVALID",
"INITIALIZATION_FAILED",
"UNSAFE_CONFIGURATION"
]);
const trustedNetSellReadResponses = new WeakSet();
const trustedViReadResponses = new WeakSet();
const trustedPlaylistEntries = new WeakMap();
const audienceLabels = Object.freeze({
INDIVIDUAL: "개인",
FOREIGN: "외국인",
@@ -25,6 +35,8 @@
const bridgeContract = Object.freeze({
statusRequestType: "request-manual-operator-data-status",
statusResultType: "manual-operator-data-status",
legacyImportType: "import-legacy-manual-operator-data",
legacyImportResultType: "legacy-manual-operator-import-result",
netSellReadType: "request-manual-net-sell-data",
netSellDataType: "manual-net-sell-data",
netSellSaveType: "save-manual-net-sell-data",
@@ -107,6 +119,10 @@
return request(bridgeContract.statusRequestType, requestId);
}
function createLegacyImportRequest(requestId) {
return request(bridgeContract.legacyImportType, requestId);
}
function createNetSellReadRequest(requestId, audience) {
const normalized = normalizeAudience(audience);
if (!normalized) throw new RangeError("The manual net-sell audience is invalid.");
@@ -139,15 +155,31 @@
}
function normalizeStatusResponse(value, expectedRequestId) {
if (!hasExactKeys(value, ["requestId", "available", "writeQuarantined", "message"]) ||
if (!hasExactKeys(value, ["requestId", "available", "code", "writeQuarantined", "message"]) ||
!isRequestId(value.requestId) ||
typeof value.available !== "boolean" ||
!statusCodes.has(value.code) ||
value.available !== (value.code === "READY") ||
typeof value.writeQuarantined !== "boolean" ||
!(value.message === null || isSafeText(value.message, 512, false)) ||
(expectedRequestId !== undefined && value.requestId !== expectedRequestId)) return null;
return Object.freeze({ ...value });
}
function normalizeLegacyImportResponse(value, expectedRequestId) {
if (!hasExactKeys(value, [
"requestId", "markerVersion", "sourceSha256", "importedAt",
"netSellRowCount", "viItemCount"
]) || !isRequestId(value.requestId) || value.markerVersion !== 1 ||
!isRowVersion(value.sourceSha256) ||
typeof value.importedAt !== "string" || !Number.isFinite(Date.parse(value.importedAt)) ||
value.netSellRowCount !== 15 ||
!Number.isInteger(value.viItemCount) || value.viItemCount < 0 ||
value.viItemCount > MAXIMUM_VI_ITEMS ||
(expectedRequestId !== undefined && value.requestId !== expectedRequestId)) return null;
return Object.freeze({ ...value });
}
function normalizeNetSellDataResponse(value, expectedRequest) {
if (!hasExactKeys(value, ["requestId", "audience", "rows"]) ||
!isRequestId(value.requestId) ||
@@ -155,7 +187,10 @@
(expectedRequest &&
(value.requestId !== expectedRequest.requestId || value.audience !== expectedRequest.audience))) return null;
const rows = normalizeNetSellRows(value.rows);
return rows && Object.freeze({ requestId: value.requestId, audience: value.audience, rows });
if (!rows) return null;
const response = Object.freeze({ requestId: value.requestId, audience: value.audience, rows });
trustedNetSellReadResponses.add(response);
return response;
}
function normalizeNetSellSaveResponse(value, expectedRequest) {
@@ -178,13 +213,15 @@
(expectedRequestId !== undefined && value.requestId !== expectedRequestId)) return null;
const items = normalizeViItems(value.items);
if (!items || items.length !== value.itemCount) return null;
return Object.freeze({
const response = Object.freeze({
requestId: value.requestId,
itemCount: value.itemCount,
pageCount: value.pageCount,
items,
version: value.version
});
trustedViReadResponses.add(response);
return response;
}
function normalizeViSaveResponse(value, expectedRequestId) {
@@ -213,14 +250,23 @@
}
function requireOptions(options) {
if (!hasExactKeys(options, ["id", "fadeDuration"]) || !isRequestId(options.id) ||
!Number.isInteger(options.fadeDuration) || options.fadeDuration < 0 || options.fadeDuration > 60) {
const keys = options && typeof options === "object" ? Object.keys(options).sort() : [];
const exact = keys.length === 2 && keys[0] === "fadeDuration" && keys[1] === "id";
const exactWithEnabled = keys.length === 3 && keys[0] === "enabled" &&
keys[1] === "fadeDuration" && keys[2] === "id";
if ((!exact && !exactWithEnabled) || !isRequestId(options.id) ||
!Number.isInteger(options.fadeDuration) || options.fadeDuration < 0 || options.fadeDuration > 60 ||
(exactWithEnabled && typeof options.enabled !== "boolean")) {
throw new TypeError("Safe manual-list playlist options are required.");
}
return options;
return Object.freeze({
id: options.id,
fadeDuration: options.fadeDuration,
enabled: exactWithEnabled ? options.enabled : true
});
}
function createNetSellPlaylistEntry(audience, options) {
function createNetSellEntryObject(audience, options) {
const normalizedAudience = normalizeAudience(audience);
const normalizedOptions = requireOptions(options);
if (!normalizedAudience) throw new RangeError("The manual net-sell audience is invalid.");
@@ -242,7 +288,7 @@
category: "market",
market: "index",
selection,
enabled: true,
enabled: normalizedOptions.enabled,
fadeDuration: normalizedOptions.fadeDuration,
graphicType: selection.graphicType,
subtype: selection.subtype,
@@ -255,7 +301,7 @@
};
}
function createViPlaylistEntry(items, rowVersion, options) {
function createViEntryObject(items, rowVersion, options) {
const normalizedItems = normalizeViItems(items, false);
const normalizedOptions = requireOptions(options);
if (!normalizedItems || !isRowVersion(rowVersion)) {
@@ -282,7 +328,7 @@
pageSize: VI_PAGE_SIZE,
pageCount,
selection,
enabled: true,
enabled: normalizedOptions.enabled,
fadeDuration: normalizedOptions.fadeDuration,
graphicType: selection.graphicType,
subtype: selection.subtype,
@@ -302,6 +348,43 @@
};
}
function createNetSellPlaylistEntry(audience, options, freshRead) {
const entry = createNetSellEntryObject(audience, options);
if (trustedNetSellReadResponses.has(freshRead) &&
freshRead.audience === entry.operator.audience && freshRead.rows.length === 5) {
trustedPlaylistEntries.set(entry, freshRead);
}
return entry;
}
function createViPlaylistEntry(items, rowVersion, options, freshRead) {
const entry = createViEntryObject(items, rowVersion, options);
if (trustedViReadResponses.has(freshRead) && freshRead.version === entry.operator.rowVersion &&
freshRead.itemCount === entry.itemCount && freshRead.pageCount === entry.pageCount &&
freshRead.items.length === entry.operator.items.length &&
freshRead.items.every((item, index) => item.code === entry.operator.items[index].code &&
item.name === entry.operator.items[index].name)) {
trustedPlaylistEntries.set(entry, freshRead);
}
return entry;
}
function isPlaylistEntryPlayable(value) {
if (!value) return false;
const freshRead = trustedPlaylistEntries.get(value);
const restored = restorePlaylistEntry(value);
if (!freshRead || !restored) return false;
if (restored.operator.kind === "net-sell") {
return trustedNetSellReadResponses.has(freshRead) && freshRead.rows.length === 5 &&
freshRead.audience === restored.operator.audience;
}
return trustedViReadResponses.has(freshRead) && freshRead.version === restored.operator.rowVersion &&
freshRead.itemCount === restored.itemCount && freshRead.pageCount === restored.pageCount &&
freshRead.items.length === restored.operator.items.length &&
freshRead.items.every((item, index) => item.code === restored.operator.items[index].code &&
item.name === restored.operator.items[index].name);
}
function sameSelection(left, right) {
const keys = ["groupCode", "subject", "graphicType", "subtype", "dataCode"];
return hasExactKeys(left, keys) && hasExactKeys(right, keys) && keys
@@ -334,10 +417,10 @@
}
let recreated;
try {
const options = { id: value.id, fadeDuration: value.fadeDuration };
const options = { id: value.id, fadeDuration: value.fadeDuration, enabled: value.enabled };
recreated = operator.kind === "net-sell"
? createNetSellPlaylistEntry(operator.audience, options)
: createViPlaylistEntry(operator.items, operator.rowVersion, options);
? createNetSellEntryObject(operator.audience, options)
: createViEntryObject(operator.items, operator.rowVersion, options);
} catch {
return null;
}
@@ -355,7 +438,7 @@
operator.pagePreview.pageSize !== recreated.operator.pagePreview.pageSize ||
operator.pagePreview.pageCount !== recreated.operator.pagePreview.pageCount ||
operator.pagePreview.maximumPageCount !== recreated.operator.pagePreview.maximumPageCount)) return null;
return { ...recreated, enabled: value.enabled };
return recreated;
}
return {
@@ -372,11 +455,13 @@
normalizeViItem,
normalizeViItems,
createStatusRequest,
createLegacyImportRequest,
createNetSellReadRequest,
createNetSellSaveRequest,
createViReadRequest,
createViSaveRequest,
normalizeStatusResponse,
normalizeLegacyImportResponse,
normalizeNetSellDataResponse,
normalizeNetSellSaveResponse,
normalizeViDataResponse,
@@ -384,6 +469,7 @@
normalizeError,
createNetSellPlaylistEntry,
createViPlaylistEntry,
isPlaylistEntryPlayable,
restorePlaylistEntry,
refreshPlaylistEntry: restorePlaylistEntry
};