Complete legacy operator UI and playout migration

This commit is contained in:
2026-07-12 02:05:49 +09:00
parent d2e16bf0c2
commit ffb8f43c19
131 changed files with 48473 additions and 228 deletions

View File

@@ -0,0 +1,691 @@
(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 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, 256, true);
const subject = exactText(value.subject, 256, true);
const graphicType = exactText(value.graphicType, 256, true);
const subtype = exactText(value.subtype, 256, true);
const dataCode = exactText(value.dataCode, 256, 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]) ||
fieldsValue.slice(1, 5).some(field => exactText(field, 256, true) === null) ||
exactText(fieldsValue[6], 256, 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 > 4000) 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.");
}
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 fields = toLegacySevenFields(entry, pageText);
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 trustedEntryMatchesRaw(entry, rawRow) {
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 && sameSelection(entry, rawRow);
}
function restoreRawRows(loadValue, trustedResolver) {
const load = loadValue?.kind === "named-playlist-load"
? loadValue
: normalizeLoadResponse(loadValue);
if (!load || typeof trustedResolver !== "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);
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
};
});