(function (root, factory) { "use strict"; const api = Object.freeze(factory()); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.MbnManualFinancialWorkflow = api; })(typeof globalThis === "object" ? globalThis : this, function () { "use strict"; const DEFAULT_MAXIMUM_RESULTS = 200; const MAXIMUM_RESULTS = 1000; const MAXIMUM_QUERY_LENGTH = 64; const DEFAULT_FADE_DURATION = 6; const OPERATOR_SCHEMA_VERSION = 1; const MAXIMUM_FLOAT = 1_000_000_000_000; const int32Minimum = -2147483648; const int32Maximum = 2147483647; const requestIdPattern = /^[A-Za-z0-9_-]{1,128}$/; const stockCodePattern = /^[A-Za-z0-9]{1,32}$/; const rowVersionPattern = /^[0-9A-F]{64}$/; const numberTextPattern = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/; const unsafeTextPattern = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}\uFFFD]/u; const bridgeContract = Object.freeze({ requestErrorType: "manual-financial-request-error", listRequestType: "request-manual-financial-list", listResultType: "manual-financial-list-results", listErrorType: "manual-financial-list-error", loadRequestType: "request-manual-financial-load", loadResultType: "manual-financial-load-results", loadErrorType: "manual-financial-load-error", selectionRequestType: "request-manual-financial-selection", selectionResultType: "manual-financial-selection-results", selectionErrorType: "manual-financial-selection-error", createRequestType: "create-manual-financial-record", updateRequestType: "update-manual-financial-record", deleteRequestType: "delete-manual-financial-record", deleteAllRequestType: "delete-all-manual-financial-records", mutationResultType: "manual-financial-mutation-result", mutationErrorType: "manual-financial-mutation-error", defaultMaximumResults: DEFAULT_MAXIMUM_RESULTS, maximumResults: MAXIMUM_RESULTS, maximumQueryLength: MAXIMUM_QUERY_LENGTH }); const screenDefinitions = Object.freeze({ "revenue-composition": Object.freeze({ screen: "revenue-composition", table: "INPUT_PIE", label: "주요매출 구성", builderKey: "s5076", code: "5076", aliases: Object.freeze(["5076"]), graphicType: "REVENUE_COMPOSITION", storageColumns: Object.freeze([ "STOCK_NAME", "BASE_DATE", "GUSUNG_1", "GUSUNG_2", "GUSUNG_3", "GUSUNG_4", "GUSUNG_5" ]), storageShape: "BASE_DATE + five label_percentage columns", deleteAllConfirmation: "DELETE_ALL_INPUT_PIE" }), "growth-metrics": Object.freeze({ screen: "growth-metrics", table: "INPUT_GROW", label: "성장성 지표", builderKey: "s5079", code: "5079", aliases: Object.freeze(["5079"]), graphicType: "GROWTH_METRICS", storageColumns: Object.freeze([ "STOCK_NAME", "GUSUNG_1", "GUSUNG_2", "GUSUNG_3", "GUSUNG_4", "BASE_DATE" ]), storageShape: "four value_value_value_value series + BASE_DATE periods", deleteAllConfirmation: "DELETE_ALL_INPUT_GROW" }), sales: Object.freeze({ screen: "sales", table: "INPUT_SELL", label: "매출액", builderKey: "s5080", code: "5080", aliases: Object.freeze(["5080"]), graphicType: "SALES", storageColumns: Object.freeze([ "STOCK_NAME", "GUSUNG_1", "GUSUNG_2", "GUSUNG_3", "GUSUNG_4", "GUSUNG_5", "GUSUNG_6" ]), storageShape: "six quarter_integer columns", deleteAllConfirmation: "DELETE_ALL_INPUT_SELL" }), "operating-profit": Object.freeze({ screen: "operating-profit", table: "INPUT_PROFIT", label: "영업이익", builderKey: "s5081", code: "5081", aliases: Object.freeze(["5081"]), graphicType: "OPERATING_PROFIT", storageColumns: Object.freeze([ "STOCK_NAME", "GUSUNG_1", "GUSUNG_2", "GUSUNG_3", "GUSUNG_4", "GUSUNG_5", "GUSUNG_6" ]), storageShape: "six quarter_integer columns", deleteAllConfirmation: "DELETE_ALL_INPUT_PROFIT" }) }); const sourceContract = Object.freeze({ originalForm: "Form/GraphE.cs", provider: "oracle", operatorSchemaVersion: OPERATOR_SCHEMA_VERSION, restoreRequiresNativeLoad: true, rawNamedPlaylistRowPlayable: false, screens: Object.freeze(Object.values(screenDefinitions)), writeSafety: Object.freeze({ valuesBound: true, oneTransactionPerWrite: true, exclusiveTableIdentityCheck: true, optimisticRowVersion: "SHA-256", outcomeUnknownAutomaticRetry: false }) }); const actions = Object.freeze(Object.values(screenDefinitions).map(definition => Object.freeze({ id: definition.screen, label: definition.label, screen: definition.screen, builderKey: definition.builderKey, code: definition.code, aliases: definition.aliases, pageSize: 1 }))); const trustedSnapshots = new WeakSet(); const trustedStockSearchResponses = new WeakSet(); const verifiedStocks = new WeakSet(); const trustedSelectionResponses = new WeakSet(); const trustedPlaylistEntries = new WeakSet(); const pendingRestoreEntries = new WeakSet(); function hasExactKeys(value, keys) { if (!value || typeof value !== "object" || Array.isArray(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 isSafeText(value, maximumLength, options = {}) { const { allowEmpty = false, allowUnderscore = true } = options; return typeof value === "string" && value.length <= maximumLength && (allowEmpty || value.length > 0) && value === value.trim() && !unsafeTextPattern.test(value) && !value.includes("^") && (allowUnderscore || !value.includes("_")); } function isRequestId(value) { return typeof value === "string" && requestIdPattern.test(value); } function definition(screen) { return typeof screen === "string" && Object.prototype.hasOwnProperty.call(screenDefinitions, screen) ? screenDefinitions[screen] : null; } 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 normalizeFinite(value) { return typeof value === "number" && Number.isFinite(value) && Math.abs(value) <= MAXIMUM_FLOAT ? value : null; } function normalizeRevenueSlice(value) { if (value === null) return null; if (!hasExactKeys(value, ["label", "percentageText", "percentage"]) || !isSafeText(value.label, 200, { allowUnderscore: false }) || !isSafeText(value.percentageText, 64, { allowUnderscore: false }) || normalizeFinite(value.percentage) === null) return undefined; const parsed = value.percentageText === "-" ? 0 : numberTextPattern.test(value.percentageText) ? Number(value.percentageText) : Number.NaN; if (!Number.isFinite(parsed) || Math.abs(parsed) > MAXIMUM_FLOAT || !Object.is(parsed, value.percentage) && parsed !== value.percentage) return undefined; return Object.freeze({ label: value.label, percentageText: value.percentageText, percentage: parsed }); } function normalizeGrowthSeries(value) { if (!Array.isArray(value) || value.length !== 4) return null; const result = []; for (const item of value) { if (item === null) { result.push(null); } else { const normalized = normalizeFinite(item); if (normalized === null) return null; result.push(normalized); } } return Object.freeze(result); } function normalizePeriods(value) { if (!Array.isArray(value) || value.length !== 4) return null; const periods = []; for (const period of value) { if (!isSafeText(period, 200, { allowEmpty: true, allowUnderscore: false })) return null; periods.push(period); } return Object.freeze(periods); } function normalizeQuarters(value) { if (!Array.isArray(value) || value.length !== 6) return null; const quarters = []; for (const item of value) { if (!hasExactKeys(item, ["quarter", "value"]) || !isSafeText(item.quarter, 200, { allowUnderscore: false }) || !Number.isInteger(item.value) || item.value < int32Minimum || item.value > int32Maximum) { return null; } quarters.push(Object.freeze({ quarter: item.quarter, value: item.value })); } return Object.freeze(quarters); } function normalizeRecord(screen, value) { const profile = definition(screen); if (!profile || !value || typeof value !== "object" || Array.isArray(value) || value.kind !== screen || !isSafeText(value.stockName, 200)) return null; if (screen === "revenue-composition") { if (!hasExactKeys(value, ["kind", "stockName", "baseDate", "slices"]) || !isSafeText(value.baseDate, 200, { allowEmpty: true }) || !Array.isArray(value.slices) || value.slices.length !== 5) return null; const slices = []; for (const raw of value.slices) { const slice = normalizeRevenueSlice(raw); if (slice === undefined) return null; slices.push(slice); } return Object.freeze({ kind: screen, stockName: value.stockName, baseDate: value.baseDate, slices: Object.freeze(slices) }); } if (screen === "growth-metrics") { if (!hasExactKeys(value, [ "kind", "stockName", "salesGrowth", "operatingProfitGrowth", "netAssetGrowth", "netIncomeGrowth", "periods" ])) return null; const salesGrowth = normalizeGrowthSeries(value.salesGrowth); const operatingProfitGrowth = normalizeGrowthSeries(value.operatingProfitGrowth); const netAssetGrowth = normalizeGrowthSeries(value.netAssetGrowth); const netIncomeGrowth = normalizeGrowthSeries(value.netIncomeGrowth); const periods = normalizePeriods(value.periods); if (!salesGrowth || !operatingProfitGrowth || !netAssetGrowth || !netIncomeGrowth || !periods) return null; return Object.freeze({ kind: screen, stockName: value.stockName, salesGrowth, operatingProfitGrowth, netAssetGrowth, netIncomeGrowth, periods }); } if (screen === "sales" || screen === "operating-profit") { if (!hasExactKeys(value, ["kind", "stockName", "quarters"])) return null; const quarters = normalizeQuarters(value.quarters); return quarters && Object.freeze({ kind: screen, stockName: value.stockName, quarters }); } return null; } function serializeRecordForStorage(screen, value) { const record = normalizeRecord(screen, value); const profile = definition(screen); if (!record || !profile) return null; let values; if (screen === "revenue-composition") { values = [record.stockName, record.baseDate, ...record.slices.map(slice => slice ? `${slice.label}_${slice.percentageText}` : "")]; } else if (screen === "growth-metrics") { const series = [record.salesGrowth, record.operatingProfitGrowth, record.netAssetGrowth, record.netIncomeGrowth]; values = [record.stockName, ...series.map(items => items.map(item => item === null ? "" : String(item)).join("_")), record.periods.join("_")]; } else { values = [record.stockName, ...record.quarters.map(item => `${item.quarter}_${item.value}`)]; } return Object.freeze({ table: profile.table, columns: profile.storageColumns, values: Object.freeze(values) }); } function createListRequest(requestId, screen, query = "", maximumResults) { if (!isRequestId(requestId)) throw new TypeError("A safe list request id is required."); if (!definition(screen)) throw new TypeError("A closed manual-financial screen is required."); const normalizedQuery = normalizeQuery(query); if (normalizedQuery === null) throw new TypeError("The manual-financial query is invalid."); if (maximumResults !== undefined && (!Number.isInteger(maximumResults) || maximumResults < 1 || maximumResults > MAXIMUM_RESULTS)) { throw new RangeError(`Manual-financial results must be limited from 1 through ${MAXIMUM_RESULTS}.`); } const request = { requestId, screen, query: normalizedQuery }; if (maximumResults !== undefined) request.maximumResults = maximumResults; return Object.freeze(request); } function normalizeRequestError(value, expectedOperation) { if (!hasExactKeys(value, ["requestId", "operation", "code", "message"]) || value.requestId !== "" && !isRequestId(value.requestId) || ![ "list", "load", "selection", "create", "update", "delete-one", "delete-all" ].includes(value.operation) || value.code !== "INVALID_REQUEST" || !isSafeText(value.message, 512) || expectedOperation !== undefined && value.operation !== expectedOperation) return null; return Object.freeze({ ...value }); } function compareNames(left, right) { const foldedLeft = left.toUpperCase(); const foldedRight = right.toUpperCase(); if (foldedLeft < foldedRight) return -1; if (foldedLeft > foldedRight) return 1; if (left < right) return -1; if (left > right) return 1; return 0; } function normalizeSnapshot(screen, value) { if (!hasExactKeys(value, ["stockName", "rowVersion", "record"]) || !rowVersionPattern.test(value.rowVersion)) return null; const record = normalizeRecord(screen, value.record); if (!record || value.stockName !== record.stockName) return null; const snapshot = Object.freeze({ screen, stockName: record.stockName, rowVersion: value.rowVersion, record }); trustedSnapshots.add(snapshot); return snapshot; } function normalizeListResponse(value, expectedRequest) { if (!hasExactKeys(value, [ "requestId", "screen", "query", "retrievedAt", "totalRowCount", "truncated", "items" ]) || !isRequestId(value.requestId) || !definition(value.screen) || normalizeQuery(value.query) !== value.query || !isSafeText(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.items) || value.items.length !== value.totalRowCount) return null; if (expectedRequest !== undefined) { let expected; try { expected = createListRequest( expectedRequest?.requestId, expectedRequest?.screen, expectedRequest?.query, expectedRequest?.maximumResults); } catch { return null; } const bound = expected.maximumResults ?? DEFAULT_MAXIMUM_RESULTS; if (value.requestId !== expected.requestId || value.screen !== expected.screen || value.query !== expected.query || value.items.length > bound || value.truncated && value.items.length !== bound) return null; } const items = []; const names = new Set(); for (const raw of value.items) { const snapshot = normalizeSnapshot(value.screen, raw); const foldedName = snapshot?.stockName.toUpperCase(); if (!snapshot || names.has(foldedName) || items.length && compareNames(items[items.length - 1].stockName, snapshot.stockName) > 0) { return null; } names.add(foldedName); items.push(snapshot); } return Object.freeze({ requestId: value.requestId, screen: value.screen, query: value.query, retrievedAt: value.retrievedAt, totalRowCount: value.totalRowCount, truncated: value.truncated, items: Object.freeze(items) }); } function normalizeListError(value, expectedRequest) { if (!hasExactKeys(value, ["requestId", "screen", "query", "message"]) || !isRequestId(value.requestId) || !definition(value.screen) || normalizeQuery(value.query) !== value.query || !isSafeText(value.message, 512)) return null; if (expectedRequest) { let expected; try { expected = createListRequest( expectedRequest.requestId, expectedRequest.screen, expectedRequest.query, expectedRequest.maximumResults); } catch { return null; } if (value.requestId !== expected.requestId || value.screen !== expected.screen || value.query !== expected.query) return null; } return Object.freeze({ requestId: value.requestId, screen: value.screen, query: value.query, message: value.message }); } function createLoadRequest(requestId, screen, stockName) { if (!isRequestId(requestId)) throw new TypeError("A safe load request id is required."); if (!definition(screen) || !isSafeText(stockName, 200)) { throw new TypeError("A closed screen and canonical stock name are required."); } return Object.freeze({ requestId, screen, stockName }); } function normalizeLoadResponse(value, expectedRequest) { if (!hasExactKeys(value, ["requestId", "screen", "retrievedAt", "snapshot"]) || !isRequestId(value.requestId) || !definition(value.screen) || !isSafeText(value.retrievedAt, 64) || !Number.isFinite(Date.parse(value.retrievedAt))) return null; let expected; if (expectedRequest !== undefined) { try { expected = createLoadRequest( expectedRequest?.requestId, expectedRequest?.screen, expectedRequest?.stockName); } catch { return null; } if (value.requestId !== expected.requestId || value.screen !== expected.screen) return null; } const snapshot = normalizeSnapshot(value.screen, value.snapshot); if (!snapshot || expected && snapshot.stockName !== expected.stockName) return null; return Object.freeze({ requestId: value.requestId, screen: value.screen, retrievedAt: value.retrievedAt, snapshot }); } function normalizeLoadError(value, expectedRequest) { if (!hasExactKeys(value, [ "requestId", "screen", "stockName", "code", "message", "retryable" ]) || !isRequestId(value.requestId) || !definition(value.screen) || !isSafeText(value.stockName, 200) || !isSafeText(value.code, 64) || !isSafeText(value.message, 512) || typeof value.retryable !== "boolean") return null; if (expectedRequest) { let expected; try { expected = createLoadRequest( expectedRequest.requestId, expectedRequest.screen, expectedRequest.stockName); } catch { return null; } if (value.requestId !== expected.requestId || value.screen !== expected.screen || value.stockName !== expected.stockName) return null; } return Object.freeze({ ...value }); } function normalizeStockSearchResponse(value) { if (!hasExactKeys(value, [ "requestId", "query", "retrievedAt", "totalRowCount", "truncated", "results" ]) || !isRequestId(value.requestId) || normalizeQuery(value.query) !== value.query || !isSafeText(value.retrievedAt, 64) || !Number.isFinite(Date.parse(value.retrievedAt)) || !Number.isInteger(value.totalRowCount) || value.totalRowCount < 0 || typeof value.truncated !== "boolean" || !Array.isArray(value.results) || value.results.length !== value.totalRowCount) return null; const results = []; const identities = new Set(); for (const item of value.results) { if (!hasExactKeys(item, ["market", "source", "name", "code"]) || !["kospi", "kosdaq", "nxt-kospi", "nxt-kosdaq"].includes(item.market) || !["oracle", "mariaDb"].includes(item.source) || (item.market.startsWith("nxt-") ? item.source !== "mariaDb" : item.source !== "oracle") || !isSafeText(item.name, 200) || !stockCodePattern.test(item.code)) return null; const key = `${item.market}\u001f${item.source}\u001f${item.code}`; if (identities.has(key)) return null; identities.add(key); results.push(Object.freeze({ market: item.market, source: item.source, name: item.name, code: item.code })); } const response = Object.freeze({ requestId: value.requestId, query: value.query.trim(), retrievedAt: value.retrievedAt, totalRowCount: value.totalRowCount, truncated: value.truncated, results: Object.freeze(results) }); trustedStockSearchResponses.add(response); return response; } function verifyStockForRecord(recordValue, stockResponse, selectedMarket, selectedCode) { const screen = recordValue?.kind; const record = normalizeRecord(screen, recordValue); if (!record || !stockResponse || !trustedStockSearchResponses.has(stockResponse) || typeof selectedMarket !== "string" || !stockCodePattern.test(selectedCode)) return null; const matches = stockResponse.results.filter(item => item.name === record.stockName); if (matches.length !== 1 || matches[0].market !== selectedMarket || matches[0].code !== selectedCode) return null; const verified = Object.freeze({ market: matches[0].market, source: matches[0].source, name: matches[0].name, code: matches[0].code }); verifiedStocks.add(verified); return verified; } function normalizeVerifiedStockValue(value) { if (!hasExactKeys(value, ["market", "source", "name", "code"]) || !["kospi", "kosdaq", "nxt-kospi", "nxt-kosdaq"].includes(value.market) || !["oracle", "mariaDb"].includes(value.source) || (value.market.startsWith("nxt-") ? value.source !== "mariaDb" : value.source !== "oracle") || !isSafeText(value.name, 200) || !stockCodePattern.test(value.code)) return null; return Object.freeze({ market: value.market, source: value.source, name: value.name, code: value.code }); } function createSelectionRequest(requestId, snapshot, verifiedStock) { if (!isRequestId(requestId)) throw new TypeError("A safe selection request id is required."); if (!snapshot || !trustedSnapshots.has(snapshot) || !verifiedStock || !verifiedStocks.has(verifiedStock) || snapshot.stockName !== verifiedStock.name) { throw new TypeError("Selection requires a native snapshot and freshly verified stock."); } return Object.freeze({ requestId, screen: snapshot.screen, stockName: snapshot.stockName, rowVersion: snapshot.rowVersion, stock: Object.freeze({ market: verifiedStock.market, source: verifiedStock.source, name: verifiedStock.name, code: verifiedStock.code }) }); } function normalizeSelectionResponse(value, expectedRequest) { if (!hasExactKeys(value, [ "requestId", "screen", "stockName", "rowVersion", "builderKey", "code", "aliases", "selection", "currentPage", "totalPages" ]) || !isRequestId(value.requestId) || !definition(value.screen) || !isSafeText(value.stockName, 200) || !rowVersionPattern.test(value.rowVersion) || !isSafeText(value.builderKey, 32) || !/^[0-9A-Z]{4,6}$/.test(value.code) || !Array.isArray(value.aliases) || value.aliases.length !== 1 || value.aliases[0] !== value.code || !hasExactKeys(value.selection, [ "groupCode", "subject", "graphicType", "subtype", "dataCode" ]) || value.currentPage !== 1 || value.totalPages !== 1) return null; const expected = expectedRequest; if (!expected || !hasExactKeys(expected, [ "requestId", "screen", "stockName", "rowVersion", "stock" ]) || value.requestId !== expected.requestId || value.screen !== expected.screen || value.stockName !== expected.stockName || value.rowVersion !== expected.rowVersion || !normalizeVerifiedStockValue(expected.stock) || expected.stock.name !== expected.stockName) return null; const profile = definition(value.screen); const groupCode = { kospi: "KOSPI", kosdaq: "KOSDAQ", "nxt-kospi": "NXT_KOSPI", "nxt-kosdaq": "NXT_KOSDAQ" }[expected.stock?.market]; if (!profile || !groupCode || value.builderKey !== profile.builderKey || value.code !== profile.code || value.selection.groupCode !== groupCode || value.selection.subject !== value.stockName || value.selection.graphicType !== profile.graphicType || value.selection.subtype !== "" || value.selection.dataCode !== "") return null; const normalized = Object.freeze({ requestId: value.requestId, screen: value.screen, stockName: value.stockName, rowVersion: value.rowVersion, builderKey: value.builderKey, code: value.code, aliases: Object.freeze([...value.aliases]), selection: Object.freeze({ ...value.selection }), currentPage: 1, totalPages: 1 }); trustedSelectionResponses.add(normalized); return normalized; } function normalizeSelectionError(value, expectedRequest) { if (!hasExactKeys(value, [ "requestId", "screen", "stockName", "code", "message", "retryable" ]) || !isRequestId(value.requestId) || !definition(value.screen) || !isSafeText(value.stockName, 200) || !isSafeText(value.code, 64) || !isSafeText(value.message, 512) || typeof value.retryable !== "boolean") return null; if (expectedRequest && (value.requestId !== expectedRequest.requestId || value.screen !== expectedRequest.screen || value.stockName !== expectedRequest.stockName)) return null; return Object.freeze({ ...value }); } function createCreateRequest(requestId, recordValue, verifiedStock) { const screen = recordValue?.kind; const record = normalizeRecord(screen, recordValue); if (!isRequestId(requestId)) throw new TypeError("A safe create request id is required."); if (!record || !verifiedStock || !verifiedStocks.has(verifiedStock) || verifiedStock.name !== record.stockName) { throw new TypeError("Create requires one verified stock and a valid closed record."); } return Object.freeze({ requestId, screen, stock: Object.freeze({ market: verifiedStock.market, source: verifiedStock.source, name: verifiedStock.name, code: verifiedStock.code }), record }); } function createUpdateRequest(requestId, snapshot, replacementValue) { if (!isRequestId(requestId)) throw new TypeError("A safe update request id is required."); if (!snapshot || !trustedSnapshots.has(snapshot)) { throw new TypeError("Update requires a native-normalized snapshot."); } const replacement = normalizeRecord(snapshot.screen, replacementValue); if (!replacement || replacement.stockName !== snapshot.stockName) { throw new TypeError("A GraphE update cannot rename a stock or change its screen."); } return Object.freeze({ requestId, screen: snapshot.screen, stockName: snapshot.stockName, expectedRowVersion: snapshot.rowVersion, record: replacement }); } function createDeleteRequest(requestId, snapshot) { if (!isRequestId(requestId)) throw new TypeError("A safe delete request id is required."); if (!snapshot || !trustedSnapshots.has(snapshot)) { throw new TypeError("Delete requires a native-normalized snapshot."); } return Object.freeze({ requestId, screen: snapshot.screen, stockName: snapshot.stockName, expectedRowVersion: snapshot.rowVersion }); } function createDeleteAllRequest(requestId, screen, confirmationToken) { const profile = definition(screen); if (!isRequestId(requestId)) throw new TypeError("A safe delete-all request id is required."); if (!profile || confirmationToken !== profile.deleteAllConfirmation) { throw new TypeError("Delete-all requires the exact screen confirmation token."); } return Object.freeze({ requestId, screen, confirmationToken }); } function normalizeMutationResult(value, expectedRequest) { if (!hasExactKeys(value, [ "requestId", "operationId", "operation", "screen", "stockName", "committedAt", "affectedRows" ]) || !isRequestId(value.requestId) || !isRequestId(value.operationId) || !["create", "update", "delete-one", "delete-all"].includes(value.operation) || !definition(value.screen) || typeof value.stockName !== "string" || (value.operation === "delete-all" ? value.stockName !== "" : !isSafeText(value.stockName, 200)) || !isSafeText(value.committedAt, 64) || !Number.isFinite(Date.parse(value.committedAt)) || !Number.isInteger(value.affectedRows) || value.affectedRows < 0 || value.operation !== "delete-all" && value.affectedRows !== 1) return null; if (expectedRequest && (value.requestId !== expectedRequest.requestId || value.screen !== expectedRequest.screen || value.operation !== operationForRequest(expectedRequest) || value.stockName !== (expectedRequest.stockName ?? expectedRequest.record?.stockName ?? ""))) return null; return Object.freeze({ ...value }); } function normalizeMutationError(value, expectedRequest) { if (!hasExactKeys(value, [ "requestId", "operation", "screen", "stockName", "code", "message", "retryable", "outcomeUnknown" ]) || !isRequestId(value.requestId) || !["create", "update", "delete-one", "delete-all"].includes(value.operation) || !definition(value.screen) || typeof value.stockName !== "string" || (value.operation === "delete-all" ? value.stockName !== "" : !isSafeText(value.stockName, 200)) || !isSafeText(value.code, 64) || !isSafeText(value.message, 512) || value.retryable !== false || typeof value.outcomeUnknown !== "boolean") return null; if (expectedRequest && (value.requestId !== expectedRequest.requestId || value.screen !== expectedRequest.screen || value.operation !== operationForRequest(expectedRequest) || value.stockName !== (expectedRequest.stockName ?? expectedRequest.record?.stockName ?? ""))) return null; return Object.freeze({ ...value }); } function operationForRequest(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return null; if (Object.prototype.hasOwnProperty.call(value, "confirmationToken")) return "delete-all"; if (Object.prototype.hasOwnProperty.call(value, "expectedRowVersion")) { return Object.prototype.hasOwnProperty.call(value, "record") ? "update" : "delete-one"; } return Object.prototype.hasOwnProperty.call(value, "record") ? "create" : null; } function selectionForValues(screen, stockName, stock) { const profile = definition(screen); const groupCode = { kospi: "KOSPI", kosdaq: "KOSDAQ", "nxt-kospi": "NXT_KOSPI", "nxt-kosdaq": "NXT_KOSDAQ" }[stock?.market]; if (!profile || !groupCode) return null; return Object.freeze({ builderKey: profile.builderKey, code: profile.code, aliases: profile.aliases, selection: Object.freeze({ groupCode, subject: stockName, graphicType: profile.graphicType, subtype: "", dataCode: "" }), currentPage: 1, pageCount: 1 }); } function buildSelection(snapshot, verifiedStock) { if (!snapshot || !trustedSnapshots.has(snapshot) || !verifiedStock || !verifiedStocks.has(verifiedStock) || snapshot.stockName !== verifiedStock.name) return null; return selectionForValues(snapshot.screen, snapshot.stockName, verifiedStock); } function normalizePlaylistOptions(options, profile, stock) { if (!options || typeof options !== "object" || Array.isArray(options) || !Object.keys(options).every(key => ["id", "fadeDuration", "enabled"].includes(key))) { throw new TypeError("Manual-financial playlist options are invalid."); } const id = options.id ?? `manual-financial-${profile.code}-${stock.code}`; const fadeDuration = options.fadeDuration ?? DEFAULT_FADE_DURATION; const enabled = options.enabled ?? true; if (!isRequestId(id) || !Number.isInteger(fadeDuration) || fadeDuration < 0 || fadeDuration > 60 || typeof enabled !== "boolean") { throw new TypeError("Manual-financial playlist options are invalid."); } return Object.freeze({ id, fadeDuration, enabled }); } function createEntryObject(snapshot, stock, selection, options) { const profile = definition(snapshot.screen); const normalizedOptions = normalizePlaylistOptions(options, profile, stock); const snapshotMetadata = Object.freeze({ screen: snapshot.screen, stockName: snapshot.stockName, rowVersion: snapshot.rowVersion, record: snapshot.record }); const stockMetadata = Object.freeze({ market: stock.market, source: stock.source, name: stock.name, code: stock.code }); return Object.freeze({ id: normalizedOptions.id, builderKey: selection.builderKey, code: selection.code, aliases: Object.freeze([...selection.aliases]), title: snapshot.stockName, detail: profile.label, category: "manual-financial", market: stock.market, stockName: snapshot.stockName, stockCode: stock.code, isNxt: stock.market.startsWith("nxt-"), selection: Object.freeze({ ...selection.selection }), enabled: normalizedOptions.enabled, fadeDuration: normalizedOptions.fadeDuration, graphicType: selection.selection.graphicType, subtype: selection.selection.subtype, currentPage: 1, pageCount: 1, operator: Object.freeze({ source: "manual-financial-workflow", schemaVersion: OPERATOR_SCHEMA_VERSION, screen: snapshot.screen, snapshot: snapshotMetadata, verifiedStock: stockMetadata }) }); } function createPlaylistEntry(snapshot, verifiedStock, selectionResponse, options = {}) { const selection = buildSelection(snapshot, verifiedStock); if (!selection || !selectionResponse || !trustedSelectionResponses.has(selectionResponse) || selectionResponse.screen !== snapshot.screen || selectionResponse.stockName !== snapshot.stockName || selectionResponse.rowVersion !== snapshot.rowVersion || selectionResponse.builderKey !== selection.builderKey || selectionResponse.code !== selection.code || !deepEqualValue(selectionResponse.aliases, selection.aliases) || !deepEqualValue(selectionResponse.selection, selection.selection) || selectionResponse.currentPage !== 1 || selectionResponse.totalPages !== 1) { throw new TypeError( "A native-confirmed selection, trusted snapshot and verified stock are required."); } const entry = createEntryObject(snapshot, verifiedStock, selection, options); trustedPlaylistEntries.add(entry); return entry; } function deepEqualValue(left, right) { if (Object.is(left, right)) return true; if (Array.isArray(left) || Array.isArray(right)) { return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => deepEqualValue(value, right[index])); } if (!left || typeof left !== "object" || !right || typeof right !== "object") return false; const leftKeys = Object.keys(left).sort(); const rightKeys = Object.keys(right).sort(); return leftKeys.length === rightKeys.length && leftKeys.every((key, index) => key === rightKeys[index] && deepEqualValue(left[key], right[key])); } function restorePlaylistEntry(value) { if (!hasExactKeys(value, [ "id", "builderKey", "code", "aliases", "title", "detail", "category", "market", "stockName", "stockCode", "isNxt", "selection", "enabled", "fadeDuration", "graphicType", "subtype", "currentPage", "pageCount", "operator" ]) || !value.operator || !hasExactKeys(value.operator, [ "source", "schemaVersion", "screen", "snapshot", "verifiedStock" ]) || value.operator.source !== "manual-financial-workflow" || value.operator.schemaVersion !== OPERATOR_SCHEMA_VERSION || !definition(value.operator.screen) || !hasExactKeys(value.operator.snapshot, ["screen", "stockName", "rowVersion", "record"]) || value.operator.snapshot.screen !== value.operator.screen || !rowVersionPattern.test(value.operator.snapshot.rowVersion)) return null; const record = normalizeRecord(value.operator.screen, value.operator.snapshot.record); const stock = normalizeVerifiedStockValue(value.operator.verifiedStock); if (!record || !stock || record.stockName !== value.operator.snapshot.stockName || stock.name !== record.stockName) return null; const snapshot = Object.freeze({ screen: value.operator.screen, stockName: record.stockName, rowVersion: value.operator.snapshot.rowVersion, record }); const selection = selectionForValues(snapshot.screen, snapshot.stockName, stock); if (!selection) return null; let canonical; try { canonical = createEntryObject(snapshot, stock, selection, { id: value.id, fadeDuration: value.fadeDuration, enabled: value.enabled }); } catch { return null; } if (!deepEqualValue(value, canonical)) return null; const pending = Object.freeze({ status: "refresh-required", playable: false, entry: canonical, identity: Object.freeze({ screen: snapshot.screen, stockName: snapshot.stockName }), rowVersion: snapshot.rowVersion, verifiedStock: stock }); pendingRestoreEntries.add(pending); return pending; } function createRestoreLoadRequest(requestId, pending) { if (!pending || !pendingRestoreEntries.has(pending)) { throw new TypeError("A validated pending restore is required."); } return createLoadRequest(requestId, pending.identity.screen, pending.identity.stockName); } function sameStoredSnapshot(pending, snapshot) { const stored = pending?.entry?.operator?.snapshot; return !!stored && snapshot.screen === stored.screen && snapshot.stockName === stored.stockName && snapshot.rowVersion === stored.rowVersion && deepEqualValue(snapshot.record, stored.record); } function createRestoreSelectionRequest(requestId, pending, loadResponse, verifiedStock) { if (!pending || !pendingRestoreEntries.has(pending) || !loadResponse || !loadResponse.snapshot || !trustedSnapshots.has(loadResponse.snapshot) || !verifiedStock || !verifiedStocks.has(verifiedStock) || !sameStoredSnapshot(pending, loadResponse.snapshot) || !deepEqualValue(pending.verifiedStock, verifiedStock)) { throw new TypeError("Restore refresh did not reproduce the stored identities."); } return createSelectionRequest(requestId, loadResponse.snapshot, verifiedStock); } function materializeRestoredPlaylistEntry( pending, loadResponse, verifiedStock, selectionResponse) { if (!pending || !pendingRestoreEntries.has(pending) || !loadResponse || !loadResponse.snapshot || !trustedSnapshots.has(loadResponse.snapshot) || !verifiedStock || !verifiedStocks.has(verifiedStock) || !selectionResponse || !trustedSelectionResponses.has(selectionResponse) || !sameStoredSnapshot(pending, loadResponse.snapshot) || !deepEqualValue(pending.verifiedStock, verifiedStock)) return null; let recreated; try { recreated = createPlaylistEntry( loadResponse.snapshot, verifiedStock, selectionResponse, { id: pending.entry.id, fadeDuration: pending.entry.fadeDuration, enabled: pending.entry.enabled }); } catch { return null; } return deepEqualValue(recreated, pending.entry) ? recreated : null; } function isPlaylistEntryPlayable(value) { return !!value && trustedPlaylistEntries.has(value); } function withEnabled(entry, enabled) { if (!trustedPlaylistEntries.has(entry) || typeof enabled !== "boolean") return null; if (entry.enabled === enabled) return entry; const replacement = Object.freeze({ ...entry, enabled }); trustedPlaylistEntries.add(replacement); return replacement; } return { bridgeContract, sourceContract, actions, getScreenDefinition: definition, normalizeRecord, serializeRecordForStorage, normalizeRequestError, createListRequest, normalizeListResponse, normalizeListError, createLoadRequest, normalizeLoadResponse, normalizeLoadError, normalizeStockSearchResponse, verifyStockForRecord, createSelectionRequest, normalizeSelectionResponse, normalizeSelectionError, createCreateRequest, createUpdateRequest, createDeleteRequest, createDeleteAllRequest, normalizeMutationResult, normalizeMutationError, buildSelection, createPlaylistEntry, restorePlaylistEntry, createRestoreLoadRequest, createRestoreSelectionRequest, materializeRestoredPlaylistEntry, withEnabled, isPlaylistEntryPlayable }; });