(function (root, factory) { "use strict"; const api = Object.freeze(factory()); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.MbnLegacyForeignIndexCandleWorkflow = api; })(typeof globalThis === "object" ? globalThis : this, function () { "use strict"; const SCHEMA_VERSION = 1; 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 resolvedValues = new WeakSet(); const materializedEntries = new WeakSet(); const failureCodes = new Set([ "INVALID_ROW", "IDENTITY_NOT_FOUND", "IDENTITY_AMBIGUOUS", "DATABASE_DATA_INVALID", "RESTORE_FAILED" ]); const targetProfiles = Object.freeze([ Object.freeze({ key: "Dow", inputName: "다우존스", symbol: "DJI@DJI", nationCode: "US", foreignDomesticCode: "0" }), Object.freeze({ key: "Nasdaq", inputName: "나스닥", symbol: "NAS@IXIC", nationCode: "US", foreignDomesticCode: "0" }), Object.freeze({ key: "Sp500", inputName: "S&P500", symbol: "SPI@SPX", nationCode: "US", foreignDomesticCode: "0" }) ]); const targetsByInputName = new Map(targetProfiles.map(value => [value.inputName, value])); const targetsByKey = new Map(targetProfiles.map(value => [value.key, value])); const periodProfiles = Object.freeze([ Object.freeze({ legacy: "5일", days: 5, cutCode: "8061" }), Object.freeze({ legacy: "20일", days: 20, cutCode: "8040" }), Object.freeze({ legacy: "60일", days: 60, cutCode: "8046" }), Object.freeze({ legacy: "120일", days: 120, cutCode: "8051" }), Object.freeze({ legacy: "240일", days: 240, cutCode: "8056" }) ]); const periodsByLegacy = new Map(periodProfiles.map(value => [value.legacy, value])); const periodsByDays = new Map(periodProfiles.map(value => [value.days, value])); const bridgeContract = Object.freeze({ resultType: "named-foreign-index-candle-restore-results", errorType: "named-foreign-index-candle-restore-error" }); 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, maximumLength, allowEmpty = false) { return typeof value === "string" && value.length <= maximumLength && (allowEmpty || value.length > 0) && value === value.trim() && !value.includes("^") && !unsafeTextPattern.test(value); } function requireOptions(options) { if (!hasExactKeys(options, ["id", "fadeDuration"]) || !requestIdPattern.test(options.id) || !Number.isInteger(options.fadeDuration) || options.fadeDuration < 0 || options.fadeDuration > 60) { return null; } return Object.freeze({ id: options.id, fadeDuration: options.fadeDuration }); } function rawSelection(raw) { return Object.freeze({ groupCode: raw.groupCode, subject: raw.subject, graphicType: raw.graphicType, subtype: raw.subtype, dataCode: raw.dataCode }); } function classify(raw, options) { const normalizedOptions = requireOptions(options); if (!normalizedOptions || !isPlainObject(raw) || !Number.isInteger(raw.itemIndex) || raw.itemIndex < 0 || raw.itemIndex >= 1000 || typeof raw.enabled !== "boolean" || raw.groupCode !== "해외지수" || raw.graphicType !== "캔들그래프" || raw.pageText !== "1/1" || raw.dataCode !== "" || !safeText(raw.subject, 200) || !safeText(raw.subtype, 256) || !safeText(raw.pageText, 9) || !safeText(raw.dataCode, 1024, true)) return null; const target = targetsByInputName.get(raw.subject); const period = periodsByLegacy.get(raw.subtype); if (!target || !period) return null; const pending = Object.freeze({ itemIndex: raw.itemIndex, enabled: raw.enabled, targetKey: target.key, inputName: target.inputName, symbol: target.symbol, nationCode: target.nationCode, foreignDomesticCode: target.foreignDomesticCode, periodDays: period.days, cutCode: period.cutCode, entry: Object.freeze({ id: normalizedOptions.id }), fadeDuration: normalizedOptions.fadeDuration, rawSelection: rawSelection(raw) }); pendingValues.add(pending); return pending; } function normalizeOutcome(value, pending, verifiedAt) { 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: "failed", failure: value.failure }); } if (value.status !== "resolved" || !hasExactKeys(value, [ "itemIndex", "status", "targetKey", "inputName", "symbol", "nationCode", "foreignDomesticCode", "sceneCode", "valueType", "periodDays", "cutCode" ])) return null; if (value.targetKey !== pending.targetKey || value.inputName !== pending.inputName || value.symbol !== pending.symbol || value.nationCode !== pending.nationCode || value.foreignDomesticCode !== pending.foreignDomesticCode || value.sceneCode !== "s8010" || value.valueType !== "PRICE" || value.periodDays !== pending.periodDays || value.cutCode !== pending.cutCode) return null; const resolved = Object.freeze({ itemIndex: value.itemIndex, status: "resolved", targetKey: value.targetKey, inputName: value.inputName, symbol: value.symbol, nationCode: value.nationCode, foreignDomesticCode: value.foreignDomesticCode, sceneCode: value.sceneCode, valueType: value.valueType, periodDays: value.periodDays, cutCode: value.cutCode, verifiedAt }); resolvedValues.add(resolved); return resolved; } 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], value.retrievedAt); if (!outcome) return null; rows.push(outcome); } return Object.freeze({ requestId: value.requestId, retrievedAt: value.retrievedAt, rows: Object.freeze(rows) }); } function normalizeError(value, expectedRequestId) { if (!requestIdPattern.test(expectedRequestId) || !hasExactKeys(value, ["requestId", "code", "message", "retryable"]) || value.requestId !== expectedRequestId || !["PLAYOUT_PRIORITY", "DATABASE_UNAVAILABLE", "RESTORE_FAILED"].includes(value.code) || !safeText(value.message, 1024) || value.retryable !== false) return null; return Object.freeze({ ...value }); } function buildEntry(pending, outcome) { const code = outcome.cutCode; const selection = Object.freeze({ groupCode: "FOREIGN_INDEX", subject: outcome.inputName, graphicType: "", subtype: "PRICE", dataCode: `${outcome.symbol}|${outcome.nationCode}|${outcome.foreignDomesticCode}` }); return Object.freeze({ id: pending.entry.id, builderKey: "s8010", code, aliases: Object.freeze([code]), title: `${outcome.inputName} 캔들 ${outcome.periodDays}일`, detail: `해외지수 · ${outcome.inputName} · ${outcome.periodDays}일`, category: "chart", market: "overseas", selection, enabled: pending.enabled, fadeDuration: pending.fadeDuration, graphicType: selection.graphicType, subtype: selection.subtype, operator: Object.freeze({ source: "legacy-foreign-index-candle-workflow", schemaVersion: SCHEMA_VERSION, targetKey: outcome.targetKey, inputName: outcome.inputName, symbol: outcome.symbol, nationCode: outcome.nationCode, foreignDomesticCode: outcome.foreignDomesticCode, periodDays: outcome.periodDays, cutCode: outcome.cutCode, verifiedAt: outcome.verifiedAt }) }); } function materialize(pending, outcome) { if (!pendingValues.has(pending) || !resolvedValues.has(outcome) || outcome.itemIndex !== pending.itemIndex || outcome.targetKey !== pending.targetKey || outcome.periodDays !== pending.periodDays || outcome.cutCode !== pending.cutCode) return null; const entry = buildEntry(pending, outcome); materializedEntries.add(entry); return entry; } function restorePlaylistEntry(value) { if (!isPlainObject(value) || !hasExactKeys(value, [ "id", "builderKey", "code", "aliases", "title", "detail", "category", "market", "selection", "enabled", "fadeDuration", "graphicType", "subtype", "operator" ]) || !requestIdPattern.test(value.id) || value.builderKey !== "s8010" || !Array.isArray(value.aliases) || value.aliases.length !== 1 || value.aliases[0] !== value.code || typeof value.enabled !== "boolean" || !Number.isInteger(value.fadeDuration) || value.fadeDuration < 0 || value.fadeDuration > 60 || !isPlainObject(value.operator) || !hasExactKeys(value.operator, [ "source", "schemaVersion", "targetKey", "inputName", "symbol", "nationCode", "foreignDomesticCode", "periodDays", "cutCode", "verifiedAt" ]) || value.operator.source !== "legacy-foreign-index-candle-workflow" || value.operator.schemaVersion !== SCHEMA_VERSION || typeof value.operator.verifiedAt !== "string" || !isoTimestampPattern.test(value.operator.verifiedAt) || !Number.isFinite(Date.parse(value.operator.verifiedAt))) return null; const target = targetsByKey.get(value.operator.targetKey); const period = periodsByDays.get(value.operator.periodDays); if (!target || !period || value.operator.inputName !== target.inputName || value.operator.symbol !== target.symbol || value.operator.nationCode !== target.nationCode || value.operator.foreignDomesticCode !== target.foreignDomesticCode || value.operator.cutCode !== period.cutCode || value.code !== period.cutCode) return null; const pending = { entry: { id: value.id }, enabled: value.enabled, fadeDuration: value.fadeDuration }; const outcome = { cutCode: period.cutCode, inputName: target.inputName, symbol: target.symbol, nationCode: target.nationCode, foreignDomesticCode: target.foreignDomesticCode, periodDays: period.days, targetKey: target.key, verifiedAt: value.operator.verifiedAt }; const rebuilt = buildEntry(pending, outcome); const scalarKeys = [ "id", "builderKey", "code", "title", "detail", "category", "market", "enabled", "fadeDuration", "graphicType", "subtype" ]; if (!scalarKeys.every(key => value[key] === rebuilt[key]) || !hasExactKeys(value.selection, ["groupCode", "subject", "graphicType", "subtype", "dataCode"]) || !Object.keys(rebuilt.selection).every(key => value.selection[key] === rebuilt.selection[key])) return null; return rebuilt; } function legacySelectionForEntry(entry) { const restored = restorePlaylistEntry(entry); if (!restored) return null; const period = periodsByDays.get(restored.operator.periodDays); return period ? Object.freeze({ groupCode: "해외지수", subject: restored.operator.inputName, graphicType: "캔들그래프", subtype: period.legacy, dataCode: "" }) : null; } function withEnabled(entry, enabled) { if (!materializedEntries.has(entry) || typeof enabled !== "boolean") return null; if (entry.enabled === enabled) return entry; const replacement = Object.freeze({ ...entry, enabled }); materializedEntries.add(replacement); return replacement; } function matchesRaw(entry, raw) { const signature = legacySelectionForEntry(entry); return Boolean(signature && raw && typeof raw.enabled === "boolean" && entry.enabled === raw.enabled && raw.pageText === "1/1" && ["groupCode", "subject", "graphicType", "subtype", "dataCode"] .every(key => signature[key] === raw[key])); } function isPending(value) { return pendingValues.has(value); } function isMaterializedEntry(value) { return materializedEntries.has(value); } return { bridgeContract, targetProfiles, periodProfiles, classify, normalizeResponse, normalizeError, materialize, restorePlaylistEntry, withEnabled, legacySelectionForEntry, matchesRaw, isPending, isMaterializedEntry }; });