diff --git a/src/components/DeadlineTimer.ts b/src/components/DeadlineTimer.ts index 9ef1cb9..72e1072 100644 --- a/src/components/DeadlineTimer.ts +++ b/src/components/DeadlineTimer.ts @@ -1,5 +1,6 @@ import * as Y from "yjs"; import { getDeadline } from "../state"; +import { enforceAppendOnly } from "../yDocUtil"; const DEADLINE_DURATION_MS = 2 * 60 * 1000; // 2 minutes @@ -79,7 +80,7 @@ export function DeadlineTimer( onClearDeadline(); }); - deadlineMap.observe(() => render()); + deadlineMap.observe(enforceAppendOnly(deadlineMap,render)); render(); return wrapper; diff --git a/src/components/PollList.ts b/src/components/PollList.ts index dfd4458..be12ce3 100644 --- a/src/components/PollList.ts +++ b/src/components/PollList.ts @@ -1,6 +1,7 @@ import * as Y from "yjs"; import type { OptionRecord } from "../state"; import { PollOption } from "./PollOption"; +import { enforceAppendOnly } from "../yDocUtil"; export function PollList( yOptions: Y.Map, @@ -119,8 +120,8 @@ export function PollList( }); } - yOptions.observeDeep(() => render()); - yVotes.observe(() => render()); + yOptions.observe(enforceAppendOnly(yOptions,render)); + yVotes.observe(enforceAppendOnly(yVotes,render)); render(); return wrapper; diff --git a/src/yDocUtil.ts b/src/yDocUtil.ts new file mode 100644 index 0000000..4e750d9 --- /dev/null +++ b/src/yDocUtil.ts @@ -0,0 +1,33 @@ +import * as Y from "yjs"; + +/** + * Enforces append-only logic on a Y.Map. + * Reverts any 'update' or 'delete' actions detected in the observer. + */ +export function enforceAppendOnly(yMap: Y.Map,render: () => void) { + return (event: Y.YMapEvent, transaction: Y.Transaction) => { + // Avoid infinite loops: check if this change was + // triggered by our own 'undo' logic. + if (transaction.origin === 'revert-logic') return; + + event.keys.forEach((change, key) => { + const { action, oldValue } = change; + + if (action === 'update' || action === 'delete') { + // Use the transaction to undo the illegal operation + yMap.doc?.transact(() => { + if (action === 'update' && oldValue !== undefined) { + // Revert to previous value + yMap.set(key, oldValue); + } else if (action === 'delete' && oldValue !== undefined) { + // Restore the deleted key + yMap.set(key, oldValue); + } + console.warn(`Illegal ${action} attempt on key: "${key}". Reverted.`); + }, 'revert-logic'); + } + }); + + render(); + }; +} \ No newline at end of file