(function (root, factory) { "use strict"; const api = Object.freeze(factory()); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.MbnNamedPlaylistWorkflow = api; })(typeof globalThis === "object" ? globalThis : this, function () { "use strict"; const SCHEMA_VERSION = 1; const MAXIMUM_DEFINITIONS = 1000; const MAXIMUM_ITEMS = 1000; const MAXIMUM_PAGE_COUNT = 20; const MAXIMUM_STORED_PAGE_COUNT = 9999; const DEFAULT_MAXIMUM_DEFINITIONS = 200; const MAXIMUM_SELECTION_FIELD_LENGTH = 256; const MAXIMUM_SUBJECT_FIELD_LENGTH = 4000; const MAXIMUM_LIST_TEXT_LENGTH = 4000; const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/; const programCodePattern = /^[0-9]{8}$/; const cutCodePattern = /^[0-9]{1,8}$/; const builderKeyPattern = /^s[0-9]+$/; const controlCharacterPattern = /[\u0000-\u001f\u007f]/; const pagedBuilderByCode = Object.freeze({ "5074": Object.freeze({ builderKey: "s5074", pageSize: 5 }), "5077": Object.freeze({ builderKey: "s5077", pageSize: 6 }), "5088": Object.freeze({ builderKey: "s5088", pageSize: 12 }) }); const bridgeContract = Object.freeze({ requests: Object.freeze({ list: "request-named-playlist-list", load: "request-named-playlist-load", create: "create-named-playlist", replace: "replace-named-playlist", delete: "delete-named-playlist" }), responses: Object.freeze({ list: "named-playlist-list-results", load: "named-playlist-load-results", mutation: "named-playlist-mutation-results", error: "named-playlist-error" }), schemaVersion: SCHEMA_VERSION, maximumDefinitions: MAXIMUM_DEFINITIONS, maximumItems: MAXIMUM_ITEMS, maximumPageCount: MAXIMUM_PAGE_COUNT, maximumStoredPageCount: MAXIMUM_STORED_PAGE_COUNT }); const pagePlanBridgeContract = Object.freeze({ request: "request-playlist-page-plans", results: "playlist-page-plans-results", error: "playlist-page-plans-error", pagedBuilderByCode }); const operations = Object.freeze(["list", "load", "create", "replace", "delete"]); const operationSet = new Set(operations); function isPlainObject(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return false; const prototype = Object.getPrototypeOf(value); return prototype === Object.prototype || prototype === null; } function hasExactKeys(value, expected) { if (!isPlainObject(value)) return false; const actual = Object.keys(value).sort(); const sortedExpected = [...expected].sort(); return actual.length === sortedExpected.length && actual.every((key, index) => key === sortedExpected[index]); } function exactText(value, maximumLength, allowEmpty = false, allowCaret = false) { if (typeof value !== "string" || value.length > maximumLength || (!allowEmpty && value.length === 0) || value !== value.trim() || controlCharacterPattern.test(value) || (!allowCaret && value.includes("^"))) return null; return value; } function requestId(value) { const text = exactText(value, 128); return text && identifierPattern.test(text) ? text : null; } function programCode(value) { const text = exactText(value, 8); return text && programCodePattern.test(text) ? text : null; } function isIsoTimestamp(value) { return typeof value === "string" && value.length >= 20 && value.length <= 64 && !Number.isNaN(Date.parse(value)); } function createListRequest(requestIdValue, maximumResults = DEFAULT_MAXIMUM_DEFINITIONS) { const id = requestId(requestIdValue); if (!id) throw new TypeError("A safe named-playlist request id is required."); if (!Number.isInteger(maximumResults) || maximumResults < 1 || maximumResults > MAXIMUM_DEFINITIONS) { throw new RangeError(`The named-playlist definition limit must be 1 through ${MAXIMUM_DEFINITIONS}.`); } return Object.freeze({ requestId: id, maximumResults }); } function createLoadRequest(requestIdValue, programCodeValue) { const id = requestId(requestIdValue); const code = programCode(programCodeValue); if (!id || !code) throw new TypeError("A safe request id and eight-digit program code are required."); return Object.freeze({ requestId: id, programCode: code }); } function createDefinitionRequest(requestIdValue, programCodeValue, titleValue) { const request = createLoadRequest(requestIdValue, programCodeValue); const title = exactText(titleValue, 128, false, true); if (!title) throw new TypeError("A canonical named-playlist title is required."); return Object.freeze({ ...request, title }); } function createDeleteRequest(requestIdValue, programCodeValue) { return createLoadRequest(requestIdValue, programCodeValue); } function parseStoredPageText(value) { if (value === "") return null; if (typeof value !== "string" || !/^[1-9][0-9]{0,3}\/(?:0|[1-9][0-9]{0,3})$/.test(value)) { return null; } const [currentText, totalText] = value.split("/"); const currentPage = Number(currentText); const totalPages = Number(totalText); if (totalPages > MAXIMUM_STORED_PAGE_COUNT || (totalPages === 0 ? currentPage !== 1 : currentPage > totalPages)) return null; const isWithinPlayoutBounds = totalPages >= 1 && totalPages <= MAXIMUM_PAGE_COUNT && currentPage <= totalPages; return Object.freeze({ pageText: value, currentPage, totalPages, isWithinPlayoutBounds, historicalPageOutOfBounds: !isWithinPlayoutBounds, pageRecalculationRequired: true }); } function normalizeSelection(value) { if (!hasExactKeys(value, ["groupCode", "subject", "graphicType", "subtype", "dataCode"])) { return null; } const groupCode = exactText(value.groupCode, MAXIMUM_SELECTION_FIELD_LENGTH, true); const subject = exactText(value.subject, MAXIMUM_SUBJECT_FIELD_LENGTH, true); const graphicType = exactText(value.graphicType, MAXIMUM_SELECTION_FIELD_LENGTH, true); const subtype = exactText(value.subtype, MAXIMUM_SELECTION_FIELD_LENGTH, true); const dataCode = exactText(value.dataCode, MAXIMUM_SELECTION_FIELD_LENGTH, true); if ([groupCode, subject, graphicType, subtype, dataCode].some(field => field === null)) return null; return Object.freeze({ groupCode, subject, graphicType, subtype, dataCode }); } function normalizePageTextForSave(value) { if (value === "") return ""; const page = parseStoredPageText(value); if (!page) throw new RangeError("The stored named-playlist page text is invalid."); return page.pageText; } function toLegacySevenFields(entry, pageTextValue = "1/1") { if (!isPlainObject(entry) || typeof entry.enabled !== "boolean") { throw new TypeError("A typed playlist entry with an enabled flag is required."); } const selection = normalizeSelection(entry.selection); if (!selection) throw new TypeError("The playlist selection cannot be stored safely."); const pageText = normalizePageTextForSave(pageTextValue); return Object.freeze([ entry.enabled ? "1" : "0", selection.groupCode, selection.subject, selection.graphicType, selection.subtype, pageText, selection.dataCode ]); } function legacyListText(fieldsValue) { if (!Array.isArray(fieldsValue) || fieldsValue.length !== 7 || fieldsValue.some(field => typeof field !== "string" || field.includes("^") || controlCharacterPattern.test(field)) || !["0", "1"].includes(fieldsValue[0]) || exactText(fieldsValue[1], MAXIMUM_SELECTION_FIELD_LENGTH, true) === null || exactText(fieldsValue[2], MAXIMUM_SUBJECT_FIELD_LENGTH, true) === null || exactText(fieldsValue[3], MAXIMUM_SELECTION_FIELD_LENGTH, true) === null || exactText(fieldsValue[4], MAXIMUM_SELECTION_FIELD_LENGTH, true) === null || exactText(fieldsValue[6], MAXIMUM_SELECTION_FIELD_LENGTH, true) === null || (fieldsValue[5] !== "" && !parseStoredPageText(fieldsValue[5]))) { throw new TypeError("Exactly seven safe legacy playlist fields are required."); } const value = fieldsValue.join("^"); if (value.length > MAXIMUM_LIST_TEXT_LENGTH) { throw new RangeError("The legacy LIST_TEXT value is too long."); } return value; } function createReplaceRequest( requestIdValue, programCodeValue, playlistEntries, options = {}) { const request = createLoadRequest(requestIdValue, programCodeValue); if (!Array.isArray(playlistEntries) || playlistEntries.length > MAXIMUM_ITEMS) { throw new RangeError(`A named playlist can contain at most ${MAXIMUM_ITEMS} entries.`); } if (!isPlainObject(options) || typeof options.isTrustedEntry !== "function") { throw new TypeError("A trusted-entry verifier is required before database storage."); } if (options.pageTextForEntry !== undefined && typeof options.pageTextForEntry !== "function") { throw new TypeError("pageTextForEntry must be a function when supplied."); } if (options.fieldsForEntry !== undefined && typeof options.fieldsForEntry !== "function") { throw new TypeError("fieldsForEntry must be a function when supplied."); } const items = playlistEntries.map((entry, itemIndex) => { if (options.isTrustedEntry(entry, itemIndex) !== true) { throw new TypeError(`Playlist entry ${itemIndex} did not pass trusted restoration.`); } const pageText = options.pageTextForEntry ? options.pageTextForEntry(entry, itemIndex) : "1/1"; const projected = options.fieldsForEntry ? options.fieldsForEntry(entry, itemIndex, pageText) : undefined; const fields = options.fieldsForEntry ? projected : toLegacySevenFields(entry, pageText); if (!Array.isArray(fields) || fields.length !== 7 || fields[0] !== (entry.enabled ? "1" : "0") || fields[5] !== pageText) { throw new TypeError(`Playlist entry ${itemIndex} has no exact legacy seven-field projection.`); } legacyListText(fields); return Object.freeze({ itemIndex, enabled: fields[0] === "1", groupCode: fields[1], subject: fields[2], graphicType: fields[3], subtype: fields[4], pageText: fields[5], dataCode: fields[6] }); }); return Object.freeze({ ...request, items: Object.freeze(items) }); } function normalizeDefinition(value) { if (!hasExactKeys(value, ["programCode", "title"])) return null; const code = programCode(value.programCode); const title = exactText(value.title, 128, false, true); return code && title ? Object.freeze({ programCode: code, title }) : null; } function normalizeListResponse(value) { const keys = [ "requestId", "retrievedAt", "totalRowCount", "truncated", "suggestedProgramCode", "canMutate", "writeQuarantined", "definitions" ]; if (!hasExactKeys(value, keys) || !requestId(value.requestId) || !isIsoTimestamp(value.retrievedAt) || !Number.isInteger(value.totalRowCount) || value.totalRowCount < 0 || typeof value.truncated !== "boolean" || !programCode(value.suggestedProgramCode) || typeof value.canMutate !== "boolean" || typeof value.writeQuarantined !== "boolean" || !Array.isArray(value.definitions) || value.definitions.length !== value.totalRowCount || value.definitions.length > MAXIMUM_DEFINITIONS || (value.writeQuarantined && value.canMutate)) return null; const definitions = []; const codes = new Set(); for (const raw of value.definitions) { const definition = normalizeDefinition(raw); if (!definition || codes.has(definition.programCode)) return null; codes.add(definition.programCode); definitions.push(definition); } return Object.freeze({ requestId: value.requestId, retrievedAt: value.retrievedAt, totalRowCount: value.totalRowCount, truncated: value.truncated, suggestedProgramCode: value.suggestedProgramCode, canMutate: value.canMutate, writeQuarantined: value.writeQuarantined, definitions: Object.freeze(definitions) }); } function normalizeRawRow(value, expectedIndex) { const keys = [ "itemIndex", "enabled", "groupCode", "subject", "graphicType", "subtype", "pageText", "dataCode" ]; if (!hasExactKeys(value, keys) || value.itemIndex !== expectedIndex || typeof value.enabled !== "boolean") return null; const selection = normalizeSelection({ groupCode: value.groupCode, subject: value.subject, graphicType: value.graphicType, subtype: value.subtype, dataCode: value.dataCode }); if (!selection || typeof value.pageText !== "string" || value.pageText.length > 9) return null; const pageState = value.pageText === "" ? null : parseStoredPageText(value.pageText); if (value.pageText !== "" && !pageState) return null; const legacyFields = Object.freeze([ value.enabled ? "1" : "0", selection.groupCode, selection.subject, selection.graphicType, selection.subtype, value.pageText, selection.dataCode ]); const listText = legacyListText(legacyFields); return Object.freeze({ itemIndex: value.itemIndex, enabled: value.enabled, groupCode: selection.groupCode, subject: selection.subject, graphicType: selection.graphicType, subtype: selection.subtype, pageText: value.pageText, dataCode: selection.dataCode, legacyFields, listText, pageState, requiresTrustedRestore: true, pageRecalculationRequired: pageState !== null, historicalPageOutOfBounds: pageState?.historicalPageOutOfBounds === true }); } function normalizeLoadResponse(value) { const keys = [ "requestId", "definition", "retrievedAt", "totalRowCount", "canMutate", "writeQuarantined", "rows" ]; if (!hasExactKeys(value, keys) || !requestId(value.requestId) || !isIsoTimestamp(value.retrievedAt) || !Number.isInteger(value.totalRowCount) || value.totalRowCount < 0 || value.totalRowCount > MAXIMUM_ITEMS || typeof value.canMutate !== "boolean" || typeof value.writeQuarantined !== "boolean" || (value.writeQuarantined && value.canMutate) || !Array.isArray(value.rows) || value.rows.length !== value.totalRowCount) return null; const definition = normalizeDefinition(value.definition); if (!definition) return null; const rawRows = []; for (let index = 0; index < value.rows.length; index += 1) { const row = normalizeRawRow(value.rows[index], index); if (!row) return null; rawRows.push(row); } return Object.freeze({ kind: "named-playlist-load", schemaVersion: SCHEMA_VERSION, requestId: value.requestId, definition, retrievedAt: value.retrievedAt, totalRowCount: value.totalRowCount, canMutate: value.canMutate, writeQuarantined: value.writeQuarantined, rawRows: Object.freeze(rawRows) }); } function sameSelection(entry, rawRow) { const selection = normalizeSelection(entry?.selection); return selection !== null && selection.groupCode === rawRow.groupCode && selection.subject === rawRow.subject && selection.graphicType === rawRow.graphicType && selection.subtype === rawRow.subtype && selection.dataCode === rawRow.dataCode; } function freezeTrustedEntry(entry) { const copy = { ...entry, selection: Object.freeze({ ...entry.selection }) }; if (Array.isArray(entry.aliases)) copy.aliases = Object.freeze([...entry.aliases]); if (isPlainObject(entry.operator)) copy.operator = Object.freeze({ ...entry.operator }); return Object.freeze(copy); } function compatibleSelection(entry, rawRow, compatibilityVerifier) { if (sameSelection(entry, rawRow)) return true; if (typeof compatibilityVerifier !== "function") return false; try { return compatibilityVerifier(entry, rawRow) === true; } catch { return false; } } function trustedEntryMatchesRaw(entry, rawRow, compatibilityVerifier) { return isPlainObject(entry) && requestId(entry.id) && typeof entry.builderKey === "string" && builderKeyPattern.test(entry.builderKey) && typeof entry.code === "string" && cutCodePattern.test(entry.code) && entry.enabled === rawRow.enabled && compatibleSelection(entry, rawRow, compatibilityVerifier); } function restoreRawRows(loadValue, trustedResolver, compatibilityVerifier) { const load = loadValue?.kind === "named-playlist-load" ? loadValue : normalizeLoadResponse(loadValue); if (!load || typeof trustedResolver !== "function" || (compatibilityVerifier !== undefined && typeof compatibilityVerifier !== "function")) { throw new TypeError("A normalized load response and trusted resolver are required."); } const rows = load.rawRows.map(rawRow => { let candidate = null; try { candidate = trustedResolver(rawRow, rawRow.itemIndex); } catch { candidate = null; } const trusted = trustedEntryMatchesRaw(candidate, rawRow, compatibilityVerifier); return Object.freeze({ rawRow, entry: trusted ? freezeTrustedEntry(candidate) : null, trusted, pageRecalculationRequired: rawRow.pageRecalculationRequired, pageRecalculated: !rawRow.pageRecalculationRequired, pagePreflightCompleted: false, pageUnavailableReason: null, recalculatedPagePlan: null }); }); return freezeRestoration(load.definition, rows); } function preparePagePreflight(restoration, isPagedEntry) { if (!restoration || restoration.kind !== "named-playlist-restoration" || typeof isPagedEntry !== "function") { throw new TypeError("A valid restoration and paged-entry classifier are required."); } const rows = restoration.rows.map(row => { const paged = row.trusted && isPagedEntry(row.entry, row.rawRow.itemIndex) === true; return Object.freeze({ ...row, pageRecalculationRequired: paged, pageRecalculated: !paged, pagePreflightCompleted: false, pageUnavailableReason: null, recalculatedPagePlan: null }); }); return freezeRestoration(restoration.definition, rows); } function pagePlanEntry(entry) { const definition = pagedBuilderByCode[entry?.code]; const selection = normalizeSelection(entry?.selection); const fadeDuration = Number(entry?.fadeDuration); if (!definition || definition.builderKey !== entry?.builderKey || !requestId(entry?.id) || typeof entry?.enabled !== "boolean" || !selection || !Number.isInteger(fadeDuration) || fadeDuration < 0 || fadeDuration > 60) { return null; } return Object.freeze({ id: entry.id, code: entry.code, enabled: entry.enabled, fadeDuration, selection }); } function createPagePlanRequest(requestIdValue, restoration) { const id = requestId(requestIdValue); if (!id || !restoration || restoration.kind !== "named-playlist-restoration") { throw new TypeError("A safe request id and trusted restoration are required."); } const entries = []; const identifiers = new Set(); for (const row of restoration.rows) { if (!row.trusted || !row.pageRecalculationRequired) continue; const entry = pagePlanEntry(row.entry); if (!entry || identifiers.has(entry.id)) { throw new TypeError("A paged restoration entry is not safe for page preflight."); } identifiers.add(entry.id); entries.push(entry); } if (!entries.length) return null; return Object.freeze({ requestId: id, entries: Object.freeze(entries) }); } function normalizePagePlanResult(value, expectedEntry) { const keys = [ "entryId", "itemCount", "pageSize", "pageCount", "accessibleItemCount", "isTruncated", "builderKey" ]; const definition = pagedBuilderByCode[expectedEntry?.code]; if (!hasExactKeys(value, keys) || !definition || value.entryId !== expectedEntry.id || value.builderKey !== definition.builderKey || value.pageSize !== definition.pageSize || !Number.isSafeInteger(value.itemCount) || value.itemCount < 0 || !Number.isInteger(value.pageCount) || value.pageCount < 0 || value.pageCount > MAXIMUM_PAGE_COUNT || !Number.isSafeInteger(value.accessibleItemCount) || value.accessibleItemCount < 0 || typeof value.isTruncated !== "boolean") return null; const expectedPageCount = value.itemCount === 0 ? 0 : Math.min(MAXIMUM_PAGE_COUNT, Math.ceil(value.itemCount / value.pageSize)); const expectedAccessible = Math.min( value.itemCount, expectedPageCount * value.pageSize); if (value.pageCount !== expectedPageCount || value.accessibleItemCount !== expectedAccessible || value.isTruncated !== (value.itemCount > expectedAccessible)) return null; return Object.freeze({ ...value }); } function normalizePagePlanResponse(value, expectedRequest) { if (!expectedRequest || !hasExactKeys(expectedRequest, ["requestId", "entries"]) || !hasExactKeys(value, ["requestId", "calculatedAt", "plans"]) || value.requestId !== expectedRequest.requestId || !isIsoTimestamp(value.calculatedAt) || !Array.isArray(value.plans) || value.plans.length !== expectedRequest.entries.length) { return null; } const plans = []; for (let index = 0; index < expectedRequest.entries.length; index += 1) { const plan = normalizePagePlanResult(value.plans[index], expectedRequest.entries[index]); if (!plan) return null; plans.push(plan); } return Object.freeze({ requestId: value.requestId, calculatedAt: value.calculatedAt, plans: Object.freeze(plans) }); } function normalizePagePlanError(value, expectedRequestId) { if (!hasExactKeys(value, ["requestId", "code", "message", "retryable"]) || value.requestId !== expectedRequestId || !requestId(value.requestId) || !exactText(value.code, 64) || !exactText(value.message, 1024) || typeof value.retryable !== "boolean") return null; return Object.freeze({ ...value }); } function normalizePagePlan(value) { if (!hasExactKeys(value, ["itemCount", "pageSize", "pageCount"]) || !Number.isInteger(value.itemCount) || value.itemCount < 0 || !Number.isInteger(value.pageSize) || value.pageSize < 0 || value.pageSize > 12 || !Number.isInteger(value.pageCount) || value.pageCount < 1 || value.pageCount > MAXIMUM_PAGE_COUNT) return null; return Object.freeze({ itemCount: value.itemCount, pageSize: value.pageSize, pageCount: value.pageCount }); } function markPageRecalculated(restoration, itemIndex, pagePlanValue) { if (!restoration || restoration.kind !== "named-playlist-restoration" || !Number.isInteger(itemIndex) || itemIndex < 0 || itemIndex >= restoration.rows.length) { throw new TypeError("A valid restoration row is required."); } const pagePlan = normalizePagePlan(pagePlanValue); if (!pagePlan) throw new RangeError("The recalculated page plan is invalid or exceeds 20 pages."); const rows = restoration.rows.map((row, index) => index === itemIndex ? Object.freeze({ ...row, pageRecalculated: true, pagePreflightCompleted: true, pageUnavailableReason: null, recalculatedPagePlan: pagePlan }) : row); return freezeRestoration(restoration.definition, rows); } function applyPagePlanResponse(restoration, response) { if (!restoration || restoration.kind !== "named-playlist-restoration" || !response || !Array.isArray(response.plans)) { throw new TypeError("A valid restoration and normalized page-plan response are required."); } let current = restoration; for (const plan of response.plans) { const itemIndex = current.rows.findIndex(row => row.trusted && row.entry?.id === plan.entryId && row.entry?.builderKey === plan.builderKey && row.pageRecalculationRequired); if (itemIndex < 0) throw new TypeError("The page-plan result does not match the restoration."); if (plan.itemCount === 0) { const rows = current.rows.map((row, index) => index === itemIndex ? Object.freeze({ ...row, pageRecalculated: false, pagePreflightCompleted: true, pageUnavailableReason: "empty-result", recalculatedPagePlan: Object.freeze({ ...plan }) }) : row); current = freezeRestoration(current.definition, rows); continue; } current = markPageRecalculated(current, itemIndex, { itemCount: plan.itemCount, pageSize: plan.pageSize, pageCount: plan.pageCount }); const rows = current.rows.map((row, index) => index === itemIndex ? Object.freeze({ ...row, recalculatedPagePlan: Object.freeze({ ...plan }) }) : row); current = freezeRestoration(current.definition, rows); } return current; } function prepareBlockers(restoration) { if (!restoration || restoration.kind !== "named-playlist-restoration") { return Object.freeze([Object.freeze({ itemIndex: -1, reason: "invalid-restoration" })]); } const blockers = []; for (const row of restoration.rows) { if (!row.trusted) blockers.push(Object.freeze({ itemIndex: row.rawRow.itemIndex, reason: "trusted-restore-required" })); if (row.pageRecalculationRequired && !row.pageRecalculated) blockers.push(Object.freeze({ itemIndex: row.rawRow.itemIndex, reason: row.pagePreflightCompleted && row.pageUnavailableReason === "empty-result" ? "page-result-empty" : "page-recalculation-required" })); } return Object.freeze(blockers); } function freezeRestoration(definition, rowsValue) { const rows = Object.freeze([...rowsValue]); const provisional = { kind: "named-playlist-restoration", schemaVersion: SCHEMA_VERSION, definition, rows }; const blockers = prepareBlockers(provisional); return Object.freeze({ ...provisional, blockers, readyForPrepare: blockers.length === 0 }); } function canPrepareRestoredRows(restoration) { return prepareBlockers(restoration).length === 0; } function materializeTrustedPlaylist(restoration) { if (!canPrepareRestoredRows(restoration)) { throw new Error("Trusted restoration and fresh page calculation are required before PREPARE."); } return Object.freeze(restoration.rows.map(row => row.entry)); } function pageStatusLabel(rawRow) { if (!rawRow || rawRow.requiresTrustedRestore !== true) return "Invalid stored row"; if (!rawRow.pageState) return "No stored page; trusted restoration is required"; if (rawRow.historicalPageOutOfBounds) { return `Stored page ${rawRow.pageText}; recalculate before PREPARE (20-page maximum)`; } return `Stored page ${rawRow.pageText}; recalculate before PREPARE`; } function normalizeMutationResponse(value) { const keys = ["requestId", "operation", "programCode", "completedAt", "writeQuarantined"]; if (!hasExactKeys(value, keys) || !requestId(value.requestId) || !["create", "replace", "delete"].includes(value.operation) || !programCode(value.programCode) || !isIsoTimestamp(value.completedAt) || typeof value.writeQuarantined !== "boolean") return null; return Object.freeze({ ...value }); } function normalizeError(value) { const keys = [ "requestId", "operation", "programCode", "code", "message", "retryable", "outcomeUnknown", "writeQuarantined" ]; if (!hasExactKeys(value, keys) || !requestId(value.requestId) || !operationSet.has(value.operation) || (value.programCode !== "" && !programCode(value.programCode)) || !exactText(value.code, 64) || !exactText(value.message, 1024) || typeof value.retryable !== "boolean" || typeof value.outcomeUnknown !== "boolean" || typeof value.writeQuarantined !== "boolean" || (value.outcomeUnknown && (value.retryable || !value.writeQuarantined))) return null; return Object.freeze({ ...value }); } function isWriteBlocked(value) { return value?.writeQuarantined === true; } return { bridgeContract, pagePlanBridgeContract, operations, createListRequest, createLoadRequest, createDefinitionRequest, createReplaceRequest, createDeleteRequest, parseStoredPageText, toLegacySevenFields, legacyListText, normalizeListResponse, normalizeLoadResponse, restoreRawRows, preparePagePreflight, createPagePlanRequest, normalizePagePlanResponse, normalizePagePlanError, applyPagePlanResponse, markPageRecalculated, prepareBlockers, canPrepareRestoredRows, materializeTrustedPlaylist, pageStatusLabel, normalizeMutationResponse, normalizeError, isWriteBlocked }; });