(function (root, factory) { "use strict"; const api = Object.freeze(factory()); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.MbnOverseasWorkflow = api; })(typeof globalThis === "object" ? globalThis : this, function () { "use strict"; const DEFAULT_FADE_DURATION = 6; const DEFAULT_MAXIMUM_RESULTS = 100; const MAXIMUM_RESULTS = 500; const MAXIMUM_QUERY_LENGTH = 64; const SCHEMA_VERSION = 1; const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/; const symbolPattern = /^[A-Za-z0-9@._$:-]{1,64}$/; const unsafeTextPattern = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}\uFFFD]/u; const bridgeContracts = Object.freeze({ industry: Object.freeze({ requestType: "search-overseas-industries", resultType: "overseas-industry-results", errorType: "overseas-industry-error", defaultMaximumResults: DEFAULT_MAXIMUM_RESULTS, maximumResults: MAXIMUM_RESULTS, maximumQueryLength: MAXIMUM_QUERY_LENGTH }), stock: Object.freeze({ requestType: "search-world-stocks", resultType: "world-stock-search-results", errorType: "world-stock-search-error", defaultMaximumResults: DEFAULT_MAXIMUM_RESULTS, maximumResults: MAXIMUM_RESULTS, maximumQueryLength: MAXIMUM_QUERY_LENGTH }) }); const sourceContract = Object.freeze({ originalControl: "Control/UC5.cs", industry: Object.freeze({ fdtc: "0", nationCodes: Object.freeze(["US"]), lookupField: "F_KNAM", currentCutCode: "5001" }), stock: Object.freeze({ fdtc: "1", nationCodes: Object.freeze(["US", "TW"]), lookupField: "F_INPUT_NAME", currentCutCode: "5001", candleCutCodes: Object.freeze(["8061", "8040", "8046", "8051", "8056"]) }) }); function action(id, label, builderKey, code, kind, periodDays = null) { return Object.freeze({ id, label, builderKey, code, aliases: Object.freeze([code]), kind, periodDays }); } const industryActions = Object.freeze([ action("industry-current", "\uBBF8\uAD6D \uD574\uC678\uC5C5\uC885 \uD604\uC7AC\uAC00 1\uC5F4\uD310", "s5001", "5001", "current") ]); const stockActions = Object.freeze([ action("stock-current", "\uD574\uC678\uC885\uBAA9 \uD604\uC7AC\uAC00 1\uC5F4\uD310", "s5001", "5001", "current"), action("stock-candle-5d", "\uD574\uC678\uC885\uBAA9 \uCE94\uB4E4 5\uC77C", "s8010", "8061", "candle", 5), action("stock-candle-20d", "\uD574\uC678\uC885\uBAA9 \uCE94\uB4E4 20\uC77C", "s8010", "8040", "candle", 20), action("stock-candle-60d", "\uD574\uC678\uC885\uBAA9 \uCE94\uB4E4 60\uC77C", "s8010", "8046", "candle", 60), action("stock-candle-120d", "\uD574\uC678\uC885\uBAA9 \uCE94\uB4E4 120\uC77C", "s8010", "8051", "candle", 120), action("stock-candle-240d", "\uD574\uC678\uC885\uBAA9 \uCE94\uB4E4 240\uC77C", "s8010", "8056", "candle", 240) ]); const actionsById = new Map( [...industryActions, ...stockActions].map(value => [value.id, value])); function hasExactKeys(value, expected) { if (!value || typeof value !== "object" || Array.isArray(value)) return false; const actual = Object.keys(value).sort(); const required = [...expected].sort(); return actual.length === required.length && actual.every((key, index) => key === required[index]); } function isSafeCanonicalText(value, maximumLength, allowEmpty = false) { return typeof value === "string" && value.length <= maximumLength && (allowEmpty || value.length > 0) && value === value.trim() && !unsafeTextPattern.test(value); } function normalizeQuery(value) { if (typeof value !== "string" || unsafeTextPattern.test(value)) return null; const query = value.trim(); return query.length <= MAXIMUM_QUERY_LENGTH && !unsafeTextPattern.test(query) ? query : null; } function normalizeSymbol(value) { return isSafeCanonicalText(value, 64) && symbolPattern.test(value) ? value : null; } function normalizeIndustry(value) { if (!hasExactKeys(value, [ "koreanName", "inputName", "symbol", "nationCode", "fdtc" ])) return null; if (!isSafeCanonicalText(value.koreanName, 200) || value.koreanName.includes("|") || !isSafeCanonicalText(value.inputName, 200) || value.inputName.includes("|") || !normalizeSymbol(value.symbol) || value.nationCode !== "US" || value.fdtc !== "0") { return null; } return Object.freeze({ koreanName: value.koreanName, inputName: value.inputName, symbol: value.symbol, nationCode: "US", fdtc: "0" }); } function normalizeStock(value) { if (!hasExactKeys(value, ["inputName", "symbol", "nationCode", "fdtc"])) return null; if (!isSafeCanonicalText(value.inputName, 200) || value.inputName.includes("|") || !normalizeSymbol(value.symbol) || !["US", "TW"].includes(value.nationCode) || value.fdtc !== "1") { return null; } return Object.freeze({ inputName: value.inputName, symbol: value.symbol, nationCode: value.nationCode, fdtc: "1" }); } function createSearchRequest(requestId, query, maximumResults) { if (!isSafeCanonicalText(requestId, 128) || !identifierPattern.test(requestId)) { throw new TypeError("A safe overseas search request id is required."); } const normalizedQuery = normalizeQuery(query ?? ""); if (normalizedQuery === null) { throw new TypeError(`An overseas search query must contain at most ${MAXIMUM_QUERY_LENGTH} safe characters.`); } if (maximumResults !== undefined && (!Number.isInteger(maximumResults) || maximumResults < 1 || maximumResults > MAXIMUM_RESULTS)) { throw new RangeError(`Overseas results must be limited from 1 through ${MAXIMUM_RESULTS}.`); } const result = { requestId, query: normalizedQuery }; if (maximumResults !== undefined) result.maximumResults = maximumResults; return Object.freeze(result); } function createIndustrySearchRequest(requestId, query = "", maximumResults) { return createSearchRequest(requestId, query, maximumResults); } function createStockSearchRequest(requestId, query = "", maximumResults) { return createSearchRequest(requestId, query, maximumResults); } function compareUpper(left, right) { const upperLeft = left.toLocaleUpperCase("en-US"); const upperRight = right.toLocaleUpperCase("en-US"); if (upperLeft < upperRight) return -1; if (upperLeft > upperRight) return 1; return 0; } function compareOrdinal(left, right) { if (left < right) return -1; if (left > right) return 1; return 0; } function compareIndustries(left, right) { return compareUpper(left.koreanName, right.koreanName) || compareOrdinal(left.inputName, right.inputName) || compareOrdinal(left.symbol, right.symbol); } function compareStocks(left, right) { return compareUpper(left.inputName, right.inputName) || compareOrdinal(left.symbol, right.symbol) || compareOrdinal(left.nationCode, right.nationCode); } function normalizeSearchResponse(value, expectedRequest, normalizeItem, compare, duplicateKeys) { if (!hasExactKeys(value, [ "requestId", "query", "retrievedAt", "totalRowCount", "truncated", "results" ]) || !isSafeCanonicalText(value.requestId, 128) || !identifierPattern.test(value.requestId) || normalizeQuery(value.query) !== value.query || !isSafeCanonicalText(value.retrievedAt, 64) || !Number.isFinite(Date.parse(value.retrievedAt)) || !Number.isInteger(value.totalRowCount) || value.totalRowCount < 0 || value.totalRowCount > MAXIMUM_RESULTS || typeof value.truncated !== "boolean" || !Array.isArray(value.results) || value.results.length !== value.totalRowCount) { return null; } if (expectedRequest) { let expected; try { expected = createSearchRequest( expectedRequest.requestId, expectedRequest.query, expectedRequest.maximumResults); } catch { return null; } const expectedLimit = expected.maximumResults ?? DEFAULT_MAXIMUM_RESULTS; if (value.requestId !== expected.requestId || value.query !== expected.query || value.results.length > expectedLimit) { return null; } } const results = []; const keySets = duplicateKeys.map(() => new Set()); for (const raw of value.results) { const item = normalizeItem(raw); if (!item) return null; for (let index = 0; index < duplicateKeys.length; index += 1) { const key = duplicateKeys[index](item); if (keySets[index].has(key)) return null; keySets[index].add(key); } if (results.length && compare(results[results.length - 1], item) > 0) return null; results.push(item); } return Object.freeze({ requestId: value.requestId, query: value.query, retrievedAt: value.retrievedAt, totalRowCount: value.totalRowCount, truncated: value.truncated, results: Object.freeze(results) }); } function normalizeIndustrySearchResponse(value, expectedRequest) { return normalizeSearchResponse( value, expectedRequest, normalizeIndustry, compareIndustries, [item => item.koreanName, item => item.inputName, item => item.symbol]); } function normalizeStockSearchResponse(value, expectedRequest) { return normalizeSearchResponse( value, expectedRequest, normalizeStock, compareStocks, [item => item.inputName, item => `${item.inputName}\u0000${item.symbol}\u0000${item.nationCode}`]); } function normalizeSearchError(value, expectedRequest) { if (!hasExactKeys(value, ["requestId", "query", "message"]) || !isSafeCanonicalText(value.requestId, 128) || !identifierPattern.test(value.requestId) || normalizeQuery(value.query) !== value.query || !isSafeCanonicalText(value.message, 512)) { return null; } if (expectedRequest && (value.requestId !== expectedRequest.requestId || value.query !== normalizeQuery(expectedRequest.query))) return null; return Object.freeze({ requestId: value.requestId, query: value.query, message: value.message }); } function normalizeIndustrySearchError(value, expectedRequest) { return normalizeSearchError(value, expectedRequest); } function normalizeStockSearchError(value, expectedRequest) { return normalizeSearchError(value, expectedRequest); } function requireOptions(options) { if (!options || typeof options !== "object" || Array.isArray(options) || !isSafeCanonicalText(options.id, 128) || !identifierPattern.test(options.id)) { throw new TypeError("Safe overseas playlist options are required."); } const fadeDuration = options.fadeDuration === undefined ? DEFAULT_FADE_DURATION : options.fadeDuration; const ma5 = options.ma5 === undefined ? false : options.ma5; const ma20 = options.ma20 === undefined ? false : options.ma20; if (!Number.isInteger(fadeDuration) || fadeDuration < 0 || fadeDuration > 60) { throw new RangeError("Fade duration must be an integer from 0 through 60."); } if (typeof ma5 !== "boolean" || typeof ma20 !== "boolean") { throw new TypeError("Moving-average flags must be boolean values."); } return Object.freeze({ id: options.id, fadeDuration, ma5, ma20 }); } function encodeIndustryIdentity(value) { return `${value.inputName}|${value.symbol}|${value.nationCode}|${value.fdtc}`; } function encodeStockIdentity(value) { return `${value.symbol}|${value.nationCode}|${value.fdtc}`; } function movingAverageFlags(actionValue, options) { const enabled = actionValue.kind === "candle"; const movingAverages = Object.freeze({ ma5: enabled && options.ma5, ma20: enabled && options.ma20 }); const flags = []; if (movingAverages.ma5) flags.push("MA5"); if (movingAverages.ma20) flags.push("MA20"); return Object.freeze({ movingAverages, graphicType: flags.join(",") }); } function buildOverseasIndustrySelection(industryValue) { const industry = normalizeIndustry(industryValue); if (!industry) throw new TypeError("A valid US overseas-industry identity is required."); return Object.freeze({ builderKey: "s5001", code: "5001", aliases: Object.freeze(["5001"]), selection: Object.freeze({ groupCode: "FOREIGN_INDUSTRY", subject: industry.koreanName, graphicType: "\u0031\uC5F4\uD310\uAE30\uBCF8", subtype: "CURRENT", dataCode: encodeIndustryIdentity(industry) }), movingAverages: Object.freeze({ ma5: false, ma20: false }) }); } function buildOverseasStockSelection(stockValue, actionId, options = {}) { const stock = normalizeStock(stockValue); const selectedAction = actionsById.get(actionId); if (!stock) throw new TypeError("A valid US/TW overseas-stock identity is required."); if (!selectedAction || !stockActions.includes(selectedAction)) { throw new RangeError("The overseas-stock action is unknown."); } const ma5 = options.ma5 === undefined ? false : options.ma5; const ma20 = options.ma20 === undefined ? false : options.ma20; if (typeof ma5 !== "boolean" || typeof ma20 !== "boolean") { throw new TypeError("Moving-average flags must be boolean values."); } const flags = movingAverageFlags(selectedAction, { ma5, ma20 }); return Object.freeze({ builderKey: selectedAction.builderKey, code: selectedAction.code, aliases: selectedAction.aliases, selection: Object.freeze({ groupCode: "FOREIGN_STOCK", subject: stock.inputName, graphicType: selectedAction.kind === "current" ? "\u0031\uC5F4\uD310\uAE30\uBCF8" : flags.graphicType, subtype: selectedAction.kind === "current" ? "CURRENT" : "PRICE", dataCode: encodeStockIdentity(stock) }), movingAverages: flags.movingAverages }); } function createOverseasIndustryPlaylistEntry(industryValue, options) { const industry = normalizeIndustry(industryValue); if (!industry) throw new TypeError("A valid US overseas-industry identity is required."); const normalizedOptions = requireOptions(options); const mapping = buildOverseasIndustrySelection(industry); const actionValue = industryActions[0]; return { id: normalizedOptions.id, builderKey: mapping.builderKey, code: mapping.code, aliases: [...mapping.aliases], title: industry.koreanName, detail: actionValue.label, category: "plate", market: "overseas", symbol: industry.symbol, nationCode: industry.nationCode, fdtc: industry.fdtc, selection: mapping.selection, enabled: true, fadeDuration: normalizedOptions.fadeDuration, graphicType: mapping.selection.graphicType, subtype: mapping.selection.subtype, operator: Object.freeze({ source: "legacy-overseas-workflow", schemaVersion: SCHEMA_VERSION, kind: "industry", actionId: actionValue.id, identity: industry, movingAverages: mapping.movingAverages }) }; } function createOverseasStockPlaylistEntry(stockValue, actionId, options) { const stock = normalizeStock(stockValue); if (!stock) throw new TypeError("A valid US/TW overseas-stock identity is required."); const selectedAction = actionsById.get(actionId); if (!selectedAction || !stockActions.includes(selectedAction)) { throw new RangeError("The overseas-stock action is unknown."); } const normalizedOptions = requireOptions(options); const mapping = buildOverseasStockSelection(stock, actionId, normalizedOptions); return { id: normalizedOptions.id, builderKey: mapping.builderKey, code: mapping.code, aliases: [...mapping.aliases], title: stock.inputName, detail: selectedAction.label, category: selectedAction.kind === "candle" ? "chart" : "stock", market: "overseas", symbol: stock.symbol, nationCode: stock.nationCode, fdtc: stock.fdtc, selection: mapping.selection, enabled: true, fadeDuration: normalizedOptions.fadeDuration, graphicType: mapping.selection.graphicType, subtype: mapping.selection.subtype, operator: Object.freeze({ source: "legacy-overseas-workflow", schemaVersion: SCHEMA_VERSION, kind: "stock", actionId, identity: stock, movingAverages: mapping.movingAverages }) }; } 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 sameAliases(value, recreated) { return Array.isArray(value.aliases) && value.aliases.length === 1 && value.aliases[0] === recreated.code; } function restoreOverseasPlaylistEntry(value) { if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.enabled !== "boolean") return null; const operator = value.operator; if (!hasExactKeys(operator, [ "source", "schemaVersion", "kind", "actionId", "identity", "movingAverages" ]) || operator.source !== "legacy-overseas-workflow" || operator.schemaVersion !== SCHEMA_VERSION || !hasExactKeys(operator.movingAverages, ["ma5", "ma20"]) || typeof operator.movingAverages.ma5 !== "boolean" || typeof operator.movingAverages.ma20 !== "boolean") return null; let recreated; try { recreated = operator.kind === "industry" ? createOverseasIndustryPlaylistEntry(operator.identity, { id: value.id, fadeDuration: value.fadeDuration, ma5: operator.movingAverages.ma5, ma20: operator.movingAverages.ma20 }) : operator.kind === "stock" ? createOverseasStockPlaylistEntry(operator.identity, operator.actionId, { id: value.id, fadeDuration: value.fadeDuration, ma5: operator.movingAverages.ma5, ma20: operator.movingAverages.ma20 }) : null; } catch { return null; } if (!recreated || operator.actionId !== recreated.operator.actionId) return null; const scalars = [ "id", "builderKey", "code", "title", "detail", "category", "market", "symbol", "nationCode", "fdtc", "fadeDuration", "graphicType", "subtype" ]; if (!scalars.every(key => value[key] === recreated[key]) || !sameAliases(value, recreated) || !sameSelection(value.selection, recreated.selection) || JSON.stringify(operator) !== JSON.stringify(recreated.operator)) return null; return { ...recreated, enabled: value.enabled }; } function isActionAllowed(kind, actionId, identity) { if (kind === "industry") { return actionId === "industry-current" && Boolean(normalizeIndustry(identity)); } return kind === "stock" && Boolean(normalizeStock(identity)) && stockActions.some(value => value.id === actionId); } return { bridgeContracts, sourceContract, industryActions, stockActions, normalizeIndustry, normalizeStock, createIndustrySearchRequest, createStockSearchRequest, normalizeIndustrySearchResponse, normalizeStockSearchResponse, normalizeIndustrySearchError, normalizeStockSearchError, isActionAllowed, buildOverseasIndustrySelection, buildOverseasStockSelection, createOverseasIndustryPlaylistEntry, createOverseasStockPlaylistEntry, restoreOverseasPlaylistEntry, refreshOverseasPlaylistEntry: restoreOverseasPlaylistEntry }; });