(function (root, factory) { "use strict"; const themes = typeof module === "object" && module.exports ? require("./theme-workflow.js") : root.MbnThemeWorkflow; const api = Object.freeze(factory(themes)); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.MbnNamedNxtThemeRestoreWorkflow = api; })(typeof globalThis === "object" ? globalThis : this, function (themes) { "use strict"; if (!themes) throw new Error("Theme workflow is unavailable."); const SCHEMA_VERSION = 1; const requestIdPattern = /^[A-Za-z0-9_-]{1,128}$/; const themeCodePattern = /^[0-9]{3,8}$/; 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 materializedValues = new WeakSet(); const failureCodes = new Set([ "INVALID_ROW", "UNSUPPORTED_ACTION", "IDENTITY_NOT_FOUND", "IDENTITY_AMBIGUOUS", "PREVIEW_EMPTY", "LIVE_ITEMS_EMPTY", "DATABASE_DATA_INVALID", "RESTORE_FAILED" ]); const profiles = Object.freeze({ "five-row-current": Object.freeze({ builderKey: "s5074", cutCode: "5074", pageSize: 5 }), "six-row-current": Object.freeze({ builderKey: "s5077", cutCode: "5077", pageSize: 6 }), "twelve-row-current": Object.freeze({ builderKey: "s5088", cutCode: "5088", pageSize: 12 }) }); const bridgeContract = Object.freeze({ resultType: "named-nxt-theme-restore-results", errorType: "named-nxt-theme-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 safeRaw(raw) { return isPlainObject(raw) && typeof raw.enabled === "boolean" && Number.isInteger(raw.itemIndex) && raw.itemIndex >= 0 && raw.itemIndex < 1000 && safeText(raw.groupCode, 256) && safeText(raw.subject, 256, true) && safeText(raw.graphicType, 256, true) && safeText(raw.subtype, 256, true) && safeText(raw.pageText, 9, true) && safeText(raw.dataCode, 256, true); } // Route every exact group row. Malformed or unsupported grammar is still a // correlated pending row so the native verifier can return a closed reason. function classify(raw, options = {}) { if (!safeRaw(raw) || raw.groupCode !== "테마_NXT" || !requestIdPattern.test(options.id) || !Number.isInteger(options.fadeDuration) || options.fadeDuration < 0 || options.fadeDuration > 60) return null; const pending = Object.freeze({ itemIndex: raw.itemIndex, enabled: raw.enabled, fadeDuration: options.fadeDuration, entry: Object.freeze({ id: options.id }), rawSelection: Object.freeze({ groupCode: raw.groupCode, subject: raw.subject, graphicType: raw.graphicType, subtype: raw.subtype, dataCode: raw.dataCode }) }); pendingValues.add(pending); return pending; } 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({ ...value }); } const keys = [ "itemIndex", "status", "actionId", "builderKey", "cutCode", "pageSize", "sort", "session", "themeTitle", "storedThemeCode", "currentThemeCode", "previewItemCount", "liveItemCount", "previewIsTruncated", "codeWasRemapped" ]; if (value.status !== "resolved" || !hasExactKeys(value, keys)) return null; const profile = profiles[value.actionId]; if (!profile || value.builderKey !== profile.builderKey || value.cutCode !== profile.cutCode || value.pageSize !== profile.pageSize || !["INPUT_ORDER", "GAIN_DESC", "GAIN_ASC"].includes(value.sort) || !["PRE_MARKET", "AFTER_MARKET"].includes(value.session) || !safeText(value.themeTitle, 128) || value.themeTitle.includes("(NXT)") || pending.rawSelection.subject !== `${value.themeTitle}(NXT)` || !themeCodePattern.test(value.storedThemeCode) || value.storedThemeCode !== pending.rawSelection.dataCode || !themeCodePattern.test(value.currentThemeCode) || value.codeWasRemapped !== (value.storedThemeCode !== value.currentThemeCode) || !Number.isInteger(value.previewItemCount) || value.previewItemCount < 1 || value.previewItemCount > 1000000 || !Number.isInteger(value.liveItemCount) || value.liveItemCount < 1 || value.liveItemCount > value.previewItemCount || typeof value.previewIsTruncated !== "boolean" || value.previewIsTruncated !== (value.previewItemCount > 240)) return null; return Object.freeze({ ...value }); } 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?.status !== "resolved" || outcome.itemIndex !== pending.itemIndex) return null; try { const created = themes.createThemePlaylistEntry( outcome.actionId, { market: "nxt", themeCode: outcome.currentThemeCode, title: outcome.themeTitle, session: outcome.session, itemCount: outcome.liveItemCount }, outcome.sort, { id: pending.entry.id, fadeDuration: pending.fadeDuration }); const entry = themes.restoreThemePlaylistEntry({ ...created, enabled: pending.enabled, operator: Object.freeze({ ...created.operator, namedNxtRestore: Object.freeze({ schemaVersion: SCHEMA_VERSION, rawSelection: pending.rawSelection, currentThemeCode: outcome.currentThemeCode, previewItemCount: outcome.previewItemCount, liveItemCount: outcome.liveItemCount, codeWasRemapped: outcome.codeWasRemapped }) }) }); if (!entry) return null; // restoreThemePlaylistEntry recreates its operator, so retain the native // proof only after the closed theme entry itself has round-tripped. const materialized = Object.freeze({ ...entry, operator: Object.freeze({ ...entry.operator, namedNxtRestore: Object.freeze({ schemaVersion: SCHEMA_VERSION, rawSelection: pending.rawSelection, currentThemeCode: outcome.currentThemeCode, previewItemCount: outcome.previewItemCount, liveItemCount: outcome.liveItemCount, codeWasRemapped: outcome.codeWasRemapped }) }) }); materializedValues.add(materialized); return materialized; } catch { return null; } } function refreshDynamicSession(entry, now = new Date()) { if (!materializedValues.has(entry) || !(now instanceof Date) || !Number.isFinite(now.getTime())) return null; const proof = entry.operator.namedNxtRestore; const session = now.getHours() <= 12 ? "PRE_MARKET" : "AFTER_MARKET"; const recreated = themes.createThemePlaylistEntry( entry.operator.actionId, { ...entry.operator.theme, session }, entry.operator.sort, { id: entry.id, fadeDuration: entry.fadeDuration }); const refreshed = Object.freeze({ ...recreated, enabled: entry.enabled, pageText: entry.pageText, operator: Object.freeze({ ...recreated.operator, namedNxtRestore: proof }) }); materializedValues.add(refreshed); return refreshed; } function matchesRaw(entry, raw) { if (!materializedValues.has(entry) || !safeRaw(raw)) return false; const saved = entry.operator?.namedNxtRestore?.rawSelection; return saved && raw.enabled === entry.enabled && ["groupCode", "subject", "graphicType", "subtype", "dataCode"] .every(key => saved[key] === raw[key]); } function legacySelectionForEntry(entry) { if (!materializedValues.has(entry)) return null; return entry.operator.namedNxtRestore.rawSelection; } function withEnabled(entry, enabled) { if (!materializedValues.has(entry) || typeof enabled !== "boolean") return null; if (entry.enabled === enabled) return entry; const replacement = Object.freeze({ ...entry, enabled }); materializedValues.add(replacement); return replacement; } function isPending(value) { return pendingValues.has(value); } function isMaterializedEntry(value) { return materializedValues.has(value); } return { bridgeContract, classify, normalizeResponse, normalizeError, materialize, refreshDynamicSession, withEnabled, matchesRaw, legacySelectionForEntry, isPending, isMaterializedEntry }; });