(function (root, factory) { "use strict"; const overseas = typeof module === "object" && module.exports ? require("./overseas-workflow.js") : root.MbnOverseasWorkflow; const api = Object.freeze(factory(overseas)); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.MbnNamedForeignStockRestoreWorkflow = api; })(typeof globalThis === "object" ? globalThis : this, function (overseas) { "use strict"; if (!overseas) throw new Error("Overseas workflow is unavailable."); const SCHEMA_VERSION = 1; const SOURCE = "legacy-named-foreign-stock-workflow"; 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 actionProfiles = Object.freeze([ Object.freeze({ actionId: "stock-current", graphicType: "1열판기본", subtype: "", builderKey: "s5001", cutCode: "5001", valueType: "CURRENT", periodDays: null }), Object.freeze({ actionId: "stock-candle-5d", graphicType: "캔들그래프", subtype: "5일", builderKey: "s8010", cutCode: "8061", valueType: "PRICE", periodDays: 5 }), Object.freeze({ actionId: "stock-candle-20d", graphicType: "캔들그래프", subtype: "20일", builderKey: "s8010", cutCode: "8040", valueType: "PRICE", periodDays: 20 }), Object.freeze({ actionId: "stock-candle-60d", graphicType: "캔들그래프", subtype: "60일", builderKey: "s8010", cutCode: "8046", valueType: "PRICE", periodDays: 60 }), Object.freeze({ actionId: "stock-candle-120d", graphicType: "캔들그래프", subtype: "120일", builderKey: "s8010", cutCode: "8051", valueType: "PRICE", periodDays: 120 }), Object.freeze({ actionId: "stock-candle-240d", graphicType: "캔들그래프", subtype: "240일", builderKey: "s8010", cutCode: "8056", valueType: "PRICE", periodDays: 240 }) ]); const profilesByAction = new Map(actionProfiles.map(value => [value.actionId, value])); const profilesByRaw = new Map(actionProfiles.map(value => [ `${value.graphicType}\u0000${value.subtype}`, value ])); const bridgeContract = Object.freeze({ resultType: "named-foreign-stock-restore-results", errorType: "named-foreign-stock-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", "ma5", "ma20"]) || !requestIdPattern.test(options.id) || !Number.isInteger(options.fadeDuration) || options.fadeDuration < 0 || options.fadeDuration > 60 || typeof options.ma5 !== "boolean" || typeof options.ma20 !== "boolean") return null; return Object.freeze({ ...options }); } function rawSelection(raw) { return Object.freeze({ groupCode: raw.groupCode, subject: raw.subject, graphicType: raw.graphicType, subtype: raw.subtype, dataCode: raw.dataCode }); } function profileForRaw(raw) { return profilesByRaw.get(`${raw?.graphicType}\u0000${raw?.subtype}`) || null; } function safeRaw(raw) { return isPlainObject(raw) && Number.isInteger(raw.itemIndex) && raw.itemIndex >= 0 && raw.itemIndex < 1000 && typeof raw.enabled === "boolean" && raw.groupCode === "해외종목" && safeText(raw.subject, 200) && raw.pageText === "1/1" && raw.dataCode === "" && safeText(raw.graphicType, 256) && safeText(raw.subtype, 256, true) && safeText(raw.pageText, 9) && safeText(raw.dataCode, 1024, true) && Boolean(profileForRaw(raw)); } function classify(raw, options) { const normalizedOptions = requireOptions(options); const profile = profileForRaw(raw); if (!normalizedOptions || !safeRaw(raw) || !profile) return null; const movingAverages = Object.freeze(profile.builderKey === "s8010" ? { ma5: normalizedOptions.ma5, ma20: normalizedOptions.ma20 } : { ma5: false, ma20: false }); const pending = Object.freeze({ itemIndex: raw.itemIndex, enabled: raw.enabled, actionId: profile.actionId, builderKey: profile.builderKey, cutCode: profile.cutCode, valueType: profile.valueType, periodDays: profile.periodDays, inputName: raw.subject, entry: Object.freeze({ id: normalizedOptions.id }), fadeDuration: normalizedOptions.fadeDuration, movingAverages, 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({ ...value }); } if (value.status !== "resolved" || !hasExactKeys(value, [ "itemIndex", "status", "actionId", "inputName", "symbol", "nationCode", "foreignDomesticCode", "builderKey", "cutCode", "valueType", "periodDays" ]) || value.actionId !== pending.actionId || value.inputName !== pending.inputName || value.foreignDomesticCode !== "1" || value.builderKey !== pending.builderKey || value.cutCode !== pending.cutCode || value.valueType !== pending.valueType || value.periodDays !== pending.periodDays) return null; const identity = overseas.normalizeStock({ inputName: value.inputName, symbol: value.symbol, nationCode: value.nationCode, fdtc: value.foreignDomesticCode }); if (!identity) return null; const resolved = Object.freeze({ itemIndex: value.itemIndex, status: "resolved", actionId: value.actionId, inputName: identity.inputName, symbol: identity.symbol, nationCode: identity.nationCode, foreignDomesticCode: identity.fdtc, builderKey: value.builderKey, cutCode: value.cutCode, valueType: value.valueType, periodDays: value.periodDays, 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) { return 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 ? Object.freeze({ ...value }) : null; } function customOperator(canonical, raw, verifiedAt) { return Object.freeze({ source: SOURCE, schemaVersion: SCHEMA_VERSION, kind: "stock", actionId: canonical.operator.actionId, identity: canonical.operator.identity, movingAverages: canonical.operator.movingAverages, rawSelection: raw, verifiedAt }); } function buildEntry(pending, outcome) { const canonical = overseas.createOverseasStockPlaylistEntry({ inputName: outcome.inputName, symbol: outcome.symbol, nationCode: outcome.nationCode, fdtc: outcome.foreignDomesticCode }, outcome.actionId, { id: pending.entry.id, fadeDuration: pending.fadeDuration, ma5: pending.movingAverages.ma5, ma20: pending.movingAverages.ma20 }); if (canonical.builderKey !== outcome.builderKey || canonical.code !== outcome.cutCode) return null; return Object.freeze({ ...canonical, enabled: pending.enabled, operator: customOperator(canonical, pending.rawSelection, outcome.verifiedAt) }); } function materialize(pending, outcome) { if (!pendingValues.has(pending) || !resolvedValues.has(outcome) || outcome.itemIndex !== pending.itemIndex || outcome.actionId !== pending.actionId) return null; let entry; try { entry = buildEntry(pending, outcome); } catch { return null; } if (!entry) return null; materializedEntries.add(entry); return entry; } function sameSelection(left, right) { return hasExactKeys(left, ["groupCode", "subject", "graphicType", "subtype", "dataCode"]) && hasExactKeys(right, ["groupCode", "subject", "graphicType", "subtype", "dataCode"]) && ["groupCode", "subject", "graphicType", "subtype", "dataCode"] .every(key => left[key] === right[key]); } function restorePlaylistEntry(value) { if (!hasExactKeys(value, [ "id", "builderKey", "code", "aliases", "title", "detail", "category", "market", "symbol", "nationCode", "fdtc", "selection", "enabled", "fadeDuration", "graphicType", "subtype", "operator" ]) || typeof value.enabled !== "boolean" || !isPlainObject(value.operator) || !hasExactKeys(value.operator, [ "source", "schemaVersion", "kind", "actionId", "identity", "movingAverages", "rawSelection", "verifiedAt" ]) || value.operator.source !== SOURCE || value.operator.schemaVersion !== SCHEMA_VERSION || value.operator.kind !== "stock" || !profilesByAction.has(value.operator.actionId) || !hasExactKeys(value.operator.identity, ["inputName", "symbol", "nationCode", "fdtc"]) || !hasExactKeys(value.operator.movingAverages, ["ma5", "ma20"]) || typeof value.operator.movingAverages.ma5 !== "boolean" || typeof value.operator.movingAverages.ma20 !== "boolean" || typeof value.operator.verifiedAt !== "string" || !isoTimestampPattern.test(value.operator.verifiedAt) || !Number.isFinite(Date.parse(value.operator.verifiedAt))) return null; const identity = overseas.normalizeStock(value.operator.identity); const raw = value.operator.rawSelection; const profile = profileForRaw(raw); if (!identity || !profile || profile.actionId !== value.operator.actionId || !hasExactKeys(raw, ["groupCode", "subject", "graphicType", "subtype", "dataCode"]) || raw.groupCode !== "해외종목" || raw.subject !== identity.inputName || raw.dataCode !== "" || !safeText(raw.subject, 200) || (profile.builderKey !== "s8010" && (value.operator.movingAverages.ma5 || value.operator.movingAverages.ma20))) return null; let canonical; try { canonical = overseas.createOverseasStockPlaylistEntry( identity, profile.actionId, { id: value.id, fadeDuration: value.fadeDuration, ma5: value.operator.movingAverages.ma5, ma20: value.operator.movingAverages.ma20 }); } catch { return null; } const rebuilt = Object.freeze({ ...canonical, enabled: value.enabled, operator: customOperator(canonical, raw, value.operator.verifiedAt) }); const scalarKeys = [ "id", "builderKey", "code", "title", "detail", "category", "market", "symbol", "nationCode", "fdtc", "enabled", "fadeDuration", "graphicType", "subtype" ]; if (!scalarKeys.every(key => value[key] === rebuilt[key]) || !Array.isArray(value.aliases) || value.aliases.length !== 1 || value.aliases[0] !== rebuilt.code || !sameSelection(value.selection, rebuilt.selection) || JSON.stringify(value.operator) !== JSON.stringify(rebuilt.operator)) return null; materializedEntries.add(rebuilt); return rebuilt; } function legacySelectionForEntry(entry) { const restored = restorePlaylistEntry(entry); return restored ? restored.operator.rawSelection : 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 && safeRaw(raw) && entry.enabled === raw.enabled && ["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 { source: SOURCE, bridgeContract, actionProfiles, classify, normalizeResponse, normalizeError, materialize, restorePlaylistEntry, withEnabled, legacySelectionForEntry, matchesRaw, isPending, isMaterializedEntry }; });