(function (root, factory) { "use strict"; const comparison = typeof module === "object" && module.exports ? require("./comparison-workflow.js") : root.MbnComparisonWorkflow; const api = Object.freeze(factory(comparison)); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.MbnNamedComparisonRestoreWorkflow = api; })(typeof globalThis === "object" ? globalThis : this, function (comparison) { "use strict"; if (!comparison) throw new Error("Comparison workflow is unavailable."); const requestIdPattern = /^[A-Za-z0-9_-]{1,128}$/; const isoTimestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,7})?(?:Z|[+-]\d{2}:\d{2})$/; const unsafeTextPattern = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}\uFFFD]/u; const pendingValues = new WeakSet(); const failureCodes = new Set([ "INVALID_ROW", "MISSING_MARKET_IDENTITY", "IDENTITY_NOT_FOUND", "IDENTITY_AMBIGUOUS", "UNSUPPORTED_ACTION", "DATABASE_DATA_INVALID", "RESTORE_FAILED" ]); const actionIds = new Set(comparison.actions.map(action => action.id)); const returnActionBySubtype = Object.freeze({ "5일": "return-5d", "1개월": "return-1m", "3개월": "return-3m", "6개월": "return-6m", "12개월": "return-12m" }); function isPlainObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } function hasExactKeys(value, keys) { if (!isPlainObject(value)) return false; const actual = Object.keys(value).sort(); const expected = [...keys].sort(); return actual.length === expected.length && actual.every((key, index) => key === expected[index]); } function safeText(value, maximum, allowEmpty = false) { return typeof value === "string" && value.length <= maximum && (allowEmpty || value.length > 0) && value === value.trim() && !value.includes("^") && !unsafeTextPattern.test(value); } function actionForRaw(raw) { if (raw.graphicType === "2열판") { if (raw.subtype === "") return "two-column-current"; if (raw.subtype === "예상체결") return "two-column-expected"; return null; } if (raw.graphicType === "종목별 비교분석_캔들 그래프" && raw.subtype === "") { return "comparison-candle"; } if (raw.graphicType === "종목별 비교분석_라인 그래프" && raw.subtype === "") { return "comparison-line"; } return raw.graphicType === "종목별 수익률 비교" ? returnActionBySubtype[raw.subtype] || null : null; } function classify(raw, options = {}) { if (!isPlainObject(raw) || typeof raw.enabled !== "boolean" || !Number.isInteger(raw.itemIndex) || raw.itemIndex < 0 || raw.itemIndex >= 1000 || !safeText(raw.groupCode, 256, true) || !safeText(raw.subject, 4000) || !safeText(raw.graphicType, 256) || !safeText(raw.subtype, 256, true) || raw.pageText !== "1/1" || raw.dataCode !== "" || !safeText(raw.pageText, 9) || !safeText(raw.dataCode, 1024, true) || !requestIdPattern.test(options.id) || !Number.isInteger(options.fadeDuration) || options.fadeDuration < 0 || options.fadeDuration > 60) return null; const names = raw.subject.split(","); const actionId = actionForRaw(raw); if (!actionId || names.length !== 2 || names.some(name => !safeText(name, 200))) return null; const pending = Object.freeze({ itemIndex: raw.itemIndex, enabled: raw.enabled, actionId, entry: Object.freeze({ id: options.id }), fadeDuration: options.fadeDuration, rawSelection: Object.freeze({ groupCode: raw.groupCode, subject: raw.subject, graphicType: raw.graphicType, subtype: raw.subtype, dataCode: raw.dataCode }) }); pendingValues.add(pending); return pending; } function normalizeWireTarget(value) { if (!isPlainObject(value)) return null; let candidate; switch (value.kind) { case "market-target": if (!hasExactKeys(value, ["kind", "target"])) return null; candidate = value; break; case "krx-stock": case "nxt-stock": if (!hasExactKeys(value, ["kind", "market", "stockName", "stockCode"])) return null; candidate = value; break; case "world-stock": if (!hasExactKeys(value, ["kind", "inputName", "symbol", "nation"])) return null; candidate = { ...value, displayName: value.inputName }; break; default: return null; } return comparison.normalizeTarget(candidate); } function normalizeOutcome(value, pending) { if (!pendingValues.has(pending) || !isPlainObject(value) || value.itemIndex !== pending.itemIndex) return null; if (value.status === "failed") { if (!hasExactKeys(value, ["itemIndex", "status", "failure"]) || !failureCodes.has(value.failure)) return null; return Object.freeze({ itemIndex: value.itemIndex, status: value.status, failure: value.failure }); } if (value.status !== "resolved" || !hasExactKeys(value, ["itemIndex", "status", "actionId", "first", "second"]) || value.actionId !== pending.actionId || !actionIds.has(value.actionId)) return null; const first = normalizeWireTarget(value.first); const second = normalizeWireTarget(value.second); if (!first || !second || !comparison.isActionAllowed(value.actionId, [first, second])) return null; return Object.freeze({ itemIndex: value.itemIndex, status: value.status, actionId: value.actionId, first, second }); } function normalizeResponse(value, expectedRequestId, pendingRows) { if (!requestIdPattern.test(expectedRequestId) || !Array.isArray(pendingRows) || pendingRows.length < 1 || pendingRows.length > 1000 || pendingRows.some(pending => !pendingValues.has(pending)) || !hasExactKeys(value, ["requestId", "retrievedAt", "totalRowCount", "rows"]) || value.requestId !== expectedRequestId || typeof value.retrievedAt !== "string" || !isoTimestampPattern.test(value.retrievedAt) || !Number.isFinite(Date.parse(value.retrievedAt)) || value.totalRowCount !== pendingRows.length || !Array.isArray(value.rows) || value.rows.length !== pendingRows.length) return null; const rows = []; for (let index = 0; index < pendingRows.length; index += 1) { const outcome = normalizeOutcome(value.rows[index], pendingRows[index]); if (!outcome) return null; rows.push(outcome); } return Object.freeze({ requestId: value.requestId, retrievedAt: value.retrievedAt, rows: Object.freeze(rows) }); } function normalizeError(value, expectedRequestId) { return requestIdPattern.test(expectedRequestId) && hasExactKeys(value, ["requestId", "code", "message", "retryable"]) && value.requestId === expectedRequestId && safeText(value.code, 64) && safeText(value.message, 1024) && value.retryable === false ? Object.freeze({ ...value }) : null; } function materialize(pending, outcome) { if (!pendingValues.has(pending) || !outcome || outcome.status !== "resolved" || outcome.itemIndex !== pending.itemIndex || outcome.actionId !== pending.actionId) return null; try { const entry = comparison.createComparisonPlaylistEntry( outcome.actionId, [outcome.first, outcome.second], { id: pending.entry.id, fadeDuration: pending.fadeDuration }); return comparison.restoreComparisonPlaylistEntry({ ...entry, enabled: pending.enabled }); } catch { return null; } } function isPending(value) { return pendingValues.has(value); } return { classify, normalizeResponse, normalizeError, materialize, isPending }; });