noteloom 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +344 -53
- package/bin/noteloom.mjs +139 -0
- package/dist/canvas.cjs +2 -0
- package/dist/canvas.cjs.map +1 -0
- package/dist/canvas.d.ts +2 -0
- package/dist/canvas.js +5 -0
- package/dist/canvas.js.map +1 -0
- package/dist/collab.cjs +2 -0
- package/dist/collab.cjs.map +1 -0
- package/dist/collab.d.ts +19 -0
- package/dist/collab.js +379 -0
- package/dist/collab.js.map +1 -0
- package/dist/comments.cjs +2 -0
- package/dist/comments.cjs.map +1 -0
- package/dist/comments.d.ts +17 -0
- package/dist/comments.js +19 -0
- package/dist/comments.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +392 -32
- package/dist/index.js +3983 -0
- package/dist/index.js.map +1 -0
- package/dist/persistence.cjs +2 -0
- package/dist/persistence.cjs.map +1 -0
- package/dist/persistence.d.ts +11 -0
- package/dist/persistence.js +12 -0
- package/dist/persistence.js.map +1 -0
- package/dist/shared/CommentAvatar-TKqsM8vG.js +53 -0
- package/dist/shared/CommentAvatar-TKqsM8vG.js.map +1 -0
- package/dist/shared/CommentAvatar-V457IgoQ.cjs +2 -0
- package/dist/shared/CommentAvatar-V457IgoQ.cjs.map +1 -0
- package/dist/shared/CommentThreadCard-CAJ3oPdr.cjs +2 -0
- package/dist/shared/CommentThreadCard-CAJ3oPdr.cjs.map +1 -0
- package/dist/shared/CommentThreadCard-Cxf8GwHJ.js +213 -0
- package/dist/shared/CommentThreadCard-Cxf8GwHJ.js.map +1 -0
- package/dist/shared/CommentsPanel-BwVVyMOC.js +76 -0
- package/dist/shared/CommentsPanel-BwVVyMOC.js.map +1 -0
- package/dist/shared/CommentsPanel-CES4uzye.cjs +2 -0
- package/dist/shared/CommentsPanel-CES4uzye.cjs.map +1 -0
- package/dist/shared/VersionHistory-Bg6fufP5.js +1120 -0
- package/dist/shared/VersionHistory-Bg6fufP5.js.map +1 -0
- package/dist/shared/VersionHistory-DELa2Wbq.cjs +12 -0
- package/dist/shared/VersionHistory-DELa2Wbq.cjs.map +1 -0
- package/dist/shared/VoiceListeningIndicator-BOfVmLK0.js +338 -0
- package/dist/shared/VoiceListeningIndicator-BOfVmLK0.js.map +1 -0
- package/dist/shared/VoiceListeningIndicator-DNX8JlQM.cjs +2 -0
- package/dist/shared/VoiceListeningIndicator-DNX8JlQM.cjs.map +1 -0
- package/dist/shared/fieldRegistry-BoYyW3GH.cjs +2 -0
- package/dist/shared/fieldRegistry-BoYyW3GH.cjs.map +1 -0
- package/dist/shared/fieldRegistry-PjJWXFoB.js +223 -0
- package/dist/shared/fieldRegistry-PjJWXFoB.js.map +1 -0
- package/dist/shared/icons-C1_s0jKK.js +4269 -0
- package/dist/shared/icons-C1_s0jKK.js.map +1 -0
- package/dist/shared/icons-DtCsXuU6.cjs +3531 -0
- package/dist/shared/icons-DtCsXuU6.cjs.map +1 -0
- package/dist/shared/index-B9jyyL8u.js +2145 -0
- package/dist/shared/index-B9jyyL8u.js.map +1 -0
- package/dist/shared/index-D9WEXpYM.cjs +2 -0
- package/dist/shared/index-D9WEXpYM.cjs.map +1 -0
- package/dist/shared/indexedDbPersistence-BKlGPFTP.js +112 -0
- package/dist/shared/indexedDbPersistence-BKlGPFTP.js.map +1 -0
- package/dist/shared/indexedDbPersistence-BWJ1h5Vp.cjs +2 -0
- package/dist/shared/indexedDbPersistence-BWJ1h5Vp.cjs.map +1 -0
- package/dist/shared/leafBlockFactory-A9eUhSe-.js +335 -0
- package/dist/shared/leafBlockFactory-A9eUhSe-.js.map +1 -0
- package/dist/shared/leafBlockFactory-Dp382eP8.cjs +2 -0
- package/dist/shared/leafBlockFactory-Dp382eP8.cjs.map +1 -0
- package/dist/shared/serialize-DWEfk0vU.cjs +3 -0
- package/dist/shared/serialize-DWEfk0vU.cjs.map +1 -0
- package/dist/shared/serialize-T9Odwx24.js +86 -0
- package/dist/shared/serialize-T9Odwx24.js.map +1 -0
- package/dist/shared/starter-kit-CY5wzYDf.js +4980 -0
- package/dist/shared/starter-kit-CY5wzYDf.js.map +1 -0
- package/dist/shared/starter-kit-lPN-NpJ5.cjs +27 -0
- package/dist/shared/starter-kit-lPN-NpJ5.cjs.map +1 -0
- package/dist/shared/usePopoverEdgeClamp-CDG26jBM.cjs +2 -0
- package/dist/shared/usePopoverEdgeClamp-CDG26jBM.cjs.map +1 -0
- package/dist/shared/usePopoverEdgeClamp-gmFm0CnI.js +109 -0
- package/dist/shared/usePopoverEdgeClamp-gmFm0CnI.js.map +1 -0
- package/dist/shared/useServiceWorkerUpdate-BduabXv6.js +94 -0
- package/dist/shared/useServiceWorkerUpdate-BduabXv6.js.map +1 -0
- package/dist/shared/useServiceWorkerUpdate-DQboGesi.cjs +2 -0
- package/dist/shared/useServiceWorkerUpdate-DQboGesi.cjs.map +1 -0
- package/dist/starter-kit.cjs +2 -0
- package/dist/starter-kit.cjs.map +1 -0
- package/dist/starter-kit.d.ts +9 -0
- package/dist/starter-kit.js +10 -0
- package/dist/starter-kit.js.map +1 -0
- package/dist/style.css +3529 -3200
- package/dist/versions.cjs +2 -0
- package/dist/versions.cjs.map +1 -0
- package/dist/versions.d.ts +11 -0
- package/dist/versions.js +13 -0
- package/dist/versions.js.map +1 -0
- package/dist/voice.cjs +2 -0
- package/dist/voice.cjs.map +1 -0
- package/dist/voice.d.ts +7 -0
- package/dist/voice.js +8 -0
- package/dist/voice.js.map +1 -0
- package/package.json +159 -92
- package/dist/noteloom.cjs +0 -3209
- package/dist/noteloom.cjs.map +0 -1
- package/dist/noteloom.es.js +0 -14622
- package/dist/noteloom.es.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoiceListeningIndicator-BOfVmLK0.js","sources":["../../src/react/restoreHistorySelection.js","../../src/voice/voiceCommands.js","../../src/voice/applyVoiceAction.js","../../src/react/useVoiceTyping.js","../../src/react/VoicePermissionModal.jsx","../../src/react/useCaretRect.js","../../src/react/VoiceListeningIndicator.jsx"],"sourcesContent":["import { focusRunAtOffset } from './focusRun.js';\nimport { focusBlockStart } from '../blocks/shared/navigationCommands.js';\n\n/**\n * Moves the caret to wherever the undo/redo that was just performed should\n * leave it — call this right after a truthy store.undo()/store.redo().\n * Prefers the precise {runId, offset} History derives for a plain text-run\n * edit; falls back to the start of the first still-existing block the\n * entry touched for structural edits (insert/remove/move block, etc.),\n * which don't reduce to one offset but still shouldn't leave focus\n * dangling on whatever had it before (usually a toolbar button, or a DOM\n * node a delete/undo just replaced outright).\n */\nexport function restoreSelectionAfterHistoryChange(store) {\n const selection = store.getPendingSelection?.();\n if (selection) {\n focusRunAtOffset(selection.runId, selection.offset);\n return;\n }\n\n const affectedIds = store.getPendingAffectedBlockIds?.() ?? [];\n const targetBlockId = affectedIds.find((id) => store.getBlock(id));\n if (targetBlockId) focusBlockStart(store, targetBlockId);\n}\n","/**\n * The spoken-phrase -> editor-action table driving voice typing (see\n * useVoiceTyping.js). Deliberately data-driven and pure (no store/DOM\n * access here at all) so the \"does this phrase count as a command\"\n * decision is directly unit-testable without a real SpeechRecognition\n * engine — matchVoiceCommand is exercised with plain strings.\n *\n * Only a *complete, isolated* recognized utterance is ever checked against\n * this table (see useVoiceTyping's isFinal handling) — a command phrase\n * spoken as part of a longer sentence while dictating never matches,\n * since the caller only calls this with one finalized speech segment at a\n * time, normalized as its own whole utterance.\n *\n * Each entry's `description` is shown to end users (see listVoiceCommands)\n * — not just a code comment — so keep it short and phrased as what saying\n * it does, not how it's implemented.\n */\nconst COMMANDS = [\n {\n phrases: ['new paragraph', 'next paragraph'],\n description: 'Start a new paragraph',\n action: { type: 'insertParagraph' },\n },\n {\n phrases: ['heading one', 'heading 1'],\n description: 'Convert the current block to a heading 1',\n action: { type: 'convertBlock', blockType: 'heading', props: { level: 1 } },\n },\n {\n phrases: ['heading two', 'heading 2'],\n description: 'Convert the current block to a heading 2',\n action: { type: 'convertBlock', blockType: 'heading', props: { level: 2 } },\n },\n {\n phrases: ['heading three', 'heading 3'],\n description: 'Convert the current block to a heading 3',\n action: { type: 'convertBlock', blockType: 'heading', props: { level: 3 } },\n },\n {\n phrases: ['bulleted list', 'bullet point', 'bullet list'],\n description: 'Convert the current block to a bulleted list item',\n action: {\n type: 'convertBlock',\n blockType: 'listItem',\n props: { ordered: false, titleRunIds: [] },\n },\n },\n {\n phrases: ['numbered list', 'ordered list'],\n description: 'Convert the current block to a numbered list item',\n action: {\n type: 'convertBlock',\n blockType: 'listItem',\n props: { ordered: true, titleRunIds: [] },\n },\n },\n {\n phrases: ['checklist', 'to-do', 'to do', 'to-do list', 'to do list'],\n description: 'Convert the current block to a to-do item',\n action: {\n type: 'convertBlock',\n blockType: 'listItem',\n props: { ordered: false, checked: false, titleRunIds: [] },\n },\n },\n {\n phrases: ['quote'],\n description: 'Convert the current block to a quote',\n action: { type: 'convertBlock', blockType: 'blockquote', props: {} },\n },\n {\n phrases: ['code block', 'code'],\n description: 'Convert the current block to a code block',\n action: { type: 'convertBlock', blockType: 'code', props: { language: 'plaintext' } },\n },\n { phrases: ['undo'], description: 'Undo the last edit', action: { type: 'undo' } },\n { phrases: ['redo'], description: 'Redo the last undone edit', action: { type: 'redo' } },\n {\n phrases: ['stop dictation', 'stop listening', 'stop'],\n description: 'Stop dictation',\n action: { type: 'stopDictation' },\n },\n];\n\nconst PHRASE_TO_ACTION = new Map();\nfor (const { phrases, action } of COMMANDS) {\n for (const phrase of phrases) PHRASE_TO_ACTION.set(phrase, action);\n}\n\n/**\n * Lowercases, trims, collapses internal whitespace, and strips trailing\n * sentence punctuation a speech engine commonly appends to a finalized\n * utterance (\".\", \",\", \"!\", \"?\") — none of that punctuation is meaningful\n * for matching a short command phrase.\n */\nexport function normalizeUtterance(text) {\n return (text ?? '')\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, ' ')\n .replace(/[.,!?]+$/, '');\n}\n\n/**\n * Returns the matching command action for a normalized, complete utterance,\n * or null if it's plain dictation (no command matched). Exact-match only —\n * deliberately not a substring/contains check, so a sentence that merely\n * *mentions* a command phrase while dictating prose is never misread as a\n * command (matches only when the whole finalized utterance IS the phrase).\n */\nexport function matchVoiceCommand(text) {\n return PHRASE_TO_ACTION.get(normalizeUtterance(text)) ?? null;\n}\n\n/**\n * Read-only `[{ phrases, description }]` list for a host to render as an\n * in-app \"what can I say\" help list — pure data, no rendering opinion (see\n * examples/basic's own \"Voice commands\" button for one way to display it).\n */\nexport function listVoiceCommands() {\n return COMMANDS.map(({ phrases, description }) => ({ phrases: [...phrases], description }));\n}\n","import { convertBlockType } from '../blocks/shared/convertBlockType.js';\nimport { insertSiblingAfterAndFocus } from '../blocks/shared/blockCommands.js';\nimport { createTextLeafBlock } from '../blocks/shared/leafBlockFactory.js';\nimport { focusRunStart } from '../react/focusRun.js';\nimport { restoreSelectionAfterHistoryChange } from '../react/restoreHistorySelection.js';\n\nfunction applyOps(store, ops) {\n if (typeof store.performBatch === 'function') store.performBatch(ops);\n else for (const op of ops) store.applyOperation(op);\n}\n\nfunction getOwnRunIds(block) {\n return block?.props?.titleRunIds ?? block?.contentIds ?? [];\n}\n\n/**\n * Executes one matched voice command (see voiceCommands.js) against the\n * store. `blockId` is wherever the caret currently is — resolved by the\n * caller (useVoiceTyping) via resolveCollapsedCaret, the same way every\n * other command in this codebase locates \"the current block\".\n *\n * 'convertBlock' reuses convertBlockType directly (not the slash-command\n * factory path) specifically so any text already dictated into the\n * current block survives the conversion — the same reason\n * markdownShortcuts.js uses it, and doubly important here since a live\n * dictation session is far more likely to have real typed content in the\n * block at the moment a structural command is spoken.\n */\nexport function applyVoiceAction(store, blockId, action) {\n if (action.type === 'insertParagraph') {\n insertSiblingAfterAndFocus(store, blockId, createTextLeafBlock('paragraph'));\n return;\n }\n\n if (action.type === 'convertBlock') {\n const block = store.getBlock(blockId);\n if (!block) return;\n const runIds = getOwnRunIds(block);\n const { ops } = convertBlockType(store, blockId, action.blockType, action.props, runIds);\n applyOps(store, ops);\n if (runIds[0]) focusRunStart(runIds[0]);\n return;\n }\n\n if (action.type === 'undo') {\n if (store.undo?.()) restoreSelectionAfterHistoryChange(store);\n return;\n }\n\n if (action.type === 'redo') {\n if (store.redo?.()) restoreSelectionAfterHistoryChange(store);\n }\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport { flushSync } from 'react-dom';\nimport { useEditorStore } from './EditorProvider.jsx';\nimport { resolveCollapsedCaret } from './selectionResolve.js';\nimport { setCaretSync } from './focusRun.js';\nimport { updateRun } from '../store/operations.js';\nimport { matchVoiceCommand } from '../voice/voiceCommands.js';\nimport { applyVoiceAction } from '../voice/applyVoiceAction.js';\n\nfunction getSpeechRecognitionCtor() {\n if (typeof window === 'undefined') return null;\n return window.SpeechRecognition ?? window.webkitSpeechRecognition ?? null;\n}\n\nconst PERMISSION_DENIED_MESSAGE =\n \"Microphone access is blocked. Enable it in your browser's site settings, then try again.\";\n\n/**\n * Native-browser (Web Speech API) voice typing: live, continuous dictation\n * into the current caret position (text appears and self-corrects as you\n * speak, not just after each pause), plus spoken structural commands\n * (\"heading one\", \"new paragraph\", \"undo\", \"stop dictation\", ...)\n * recognized via voiceCommands.js — see that file for the full command\n * table and matching rules (also exported there as listVoiceCommands() for\n * an in-app \"what can I say\" help list).\n *\n * Deliberately built on the browser's own SpeechRecognition rather than a\n * bundled speech-to-text SDK: real speech recognition needs either a\n * network round-trip to a vendor's servers or a genuinely heavy on-device\n * model, either of which conflicts with this package staying zero-runtime-\n * dependency. `isSupported` is false (start()/stop() are no-ops) wherever\n * the API doesn't exist (Firefox, most notably) — a host app decides how\n * to surface that (hide the mic button, show a note, etc.), this hook just\n * degrades safely rather than throwing.\n *\n * ---- Never trust SpeechRecognition's resultIndex for phrase identity —\n * this is the part two earlier versions of this hook got wrong ----\n *\n * `sessionAnchorRef` (`{ runId, blockId, offset }`) is this hook's own,\n * single source of truth for \"where does the next chunk of dictated text\n * go\" — resolved from the real caret when `start()` is called, and after\n * that updated only by this hook's own writes OR by the `selectionchange`\n * listener below noticing the user manually moved the caret somewhere else\n * (click, arrow keys, Tab into another block) — it is never blindly\n * re-read from `window.getSelection()` on some other trigger (e.g. every\n * `onresult` event), which is what the two broken attempts below actually\n * ran into.\n *\n * `liveEntryRef` is the *one* mutable \"currently being revised, not yet\n * final\" text buffer — on every `onresult` event, every non-final result\n * in that event is concatenated into one string and used to *fully\n * replace* this single buffer's contents, regardless of which\n * `resultIndex`(es) the browser happened to report them under.\n * `committedCountRef` is a separate, monotonically-increasing \"how many\n * result indices have I already permanently committed\" counter, advanced\n * only when *this hook* finalizes something — used purely as a \"don't\n * reprocess an index I already committed\" guard, never for matching a new\n * event's index back to an existing in-progress phrase.\n *\n * This design replaced two earlier, both-broken attempts at using\n * `resultIndex` as a phrase identifier:\n * 1. Tracking a `Map` of every \"in-flight\" phrase keyed by resultIndex,\n * re-resolving the live caret each time a new index appeared. Real\n * Chrome doesn't keep resultIndex behaving cleanly enough for that —\n * a later index could get matched against a stale map entry, and\n * text landed out of order (\"11234567 89 10 11 ...\" for numbers\n * spoken in order).\n * 2. Comparing consecutive events' resultIndex to decide \"is this a\n * revision of the same phrase, or a new one\" (one phrase at a time,\n * no Map). Real Chrome can advance resultIndex on what is still\n * conceptually the same in-progress phrase, which made this treat\n * every revision as a brand-new phrase, anchored after the\n * previous (wrongly-separate) one's text — producing runaway,\n * compounding duplication (\"test for the\" -> \"test for the life\" ->\n * \"test for the life desc\" -> ... each revision re-typing the whole\n * growing sentence from scratch instead of replacing the last guess).\n *\n * Concatenate-all-non-final-into-one-buffer is the standard pattern real\n * speech-to-text integrations use (e.g. how the `react-speech-recognition`\n * library and most Web Speech API tutorials build up \"interim transcript\"\n * + \"final transcript\" separately) — it needs no assumption whatsoever\n * about how the browser indexes results across events.\n *\n * Every write still goes through `flushSync` (react-dom) before\n * `setCaretSync` (the non-rAF variant of focusRunAtOffset, see\n * focusRun.js) moves the *visible* caret to match — `TextRunSpan`\n * (EditableBlockContent.jsx) fully destroys and recreates the run's DOM\n * text node on every value change, which resets the browser's native\n * Selection, and a plain `store.applyOperation(...)` call is not\n * guaranteed to have committed that DOM change by the time it returns\n * (React may batch it) — `flushSync` forces the commit first. This part\n * only affects what the user visually sees the caret doing; the actual\n * text-insertion math never depends on it, since `sessionAnchorRef` is the\n * authority, not the DOM.\n *\n * Text is inserted/updated plain, with no \"tentative\" styling while\n * interim — it just grows and occasionally self-corrects as the engine\n * hears more context, the same way Google's own voice typing behaves.\n *\n * Command detection vs. plain dictation: only a *finalized* segment is\n * ever checked against the command table (matchVoiceCommand) — natural\n * pauses in speech are what the Speech API itself uses to decide a segment\n * is \"final\", so a command spoken as its own isolated utterance is\n * detected reliably, while the same words embedded in a longer dictated\n * sentence never match (the whole segment has to equal a known phrase,\n * not just contain one). If a phrase that was already live-inserted as\n * interim text finalizes as a command, the interim text is reverted\n * (removed) before the command runs — so e.g. saying \"heading one\" never\n * leaves \"heading one\" sitting in the paragraph before converting it, even\n * though \"heading\" alone may have briefly appeared while still mid-\n * utterance.\n *\n * `status` (`'idle' | 'listening' | 'processing'`) is driven by the Speech\n * API's own `onspeechstart`/`onspeechend` events, not a fabricated timer:\n * `onspeechend` fires when the engine detects you've stopped talking and is\n * about to finalize what it heard ('processing'), and the next committed\n * final result (or a fresh `onspeechstart`) returns to 'listening'. Neither\n * event is guaranteed on every engine — if they never fire, status simply\n * stays 'listening' for the whole session, a safe degrade, not a crash.\n * `VoiceListeningIndicator` (also exported) renders a small badge that\n * follows the live caret off this state.\n *\n * Mic permission: `recognition.onerror` reporting 'not-allowed' or\n * 'service-not-allowed' is the one cross-browser-reliable signal that\n * permission was denied (the Permissions API's 'microphone' descriptor\n * isn't supported in Firefox at all, and is inconsistent in Safari) — that\n * error sets `permissionState: 'denied'`. Where the Permissions API *is*\n * available, it's also queried opportunistically on mount (and its own\n * `change` event subscribed to) purely so a previously-denied permission\n * shows up in `permissionState` before the user even presses the mic\n * button, rather than only ever finding out after a failed start().\n * `VoicePermissionModal` (also exported) renders itself automatically off\n * this same state.\n *\n * `options.shortcut` (default true) wires one native, document-level\n * Ctrl/Cmd+Shift+M keydown listener that toggles start()/stop() — pass\n * `{ shortcut: false }` to opt out if a host wants its own binding.\n *\n * `options.engine` lets a host app supply its own recognition engine\n * instead of relying on `window.SpeechRecognition`/`webkitSpeechRecognition`\n * — needed because that API is commonly absent or non-functional in\n * embedded webviews (e.g. Tauri's WebView2/WKWebView), which have no Web\n * Speech API implementation, or a Chromium-derived one that's missing the\n * private Google API key real Chrome ships with (constructor exists, but\n * every session fails immediately with a `'network'` error). `engine` is a\n * zero-arg factory, `() => SpeechRecognitionLike`, called once per `start()`\n * to produce a fresh instance implementing the same surface this hook\n * already drives on the native constructor:\n * - properties: `continuous`, `interimResults` (set by this hook before\n * starting; the engine should honor them)\n * - event handlers this hook assigns: `onresult`, `onstart`,\n * `onspeechstart`, `onspeechend`, `onerror`, `onend`\n * - methods: `start()`, `stop()`\n * - `onresult`'s event shape: `{ resultIndex, results }` where `results`\n * is array-like of `results[i][0].transcript` (string) and\n * `results[i].isFinal` (boolean) — exactly the native SpeechRecognition\n * result shape, since `handleRecognitionResult` above reads it that way\n * regardless of what produced it (native browser engine, a native/OS\n * bridge, or a cloud STT service streamed back into this shape).\n * - `onerror`'s event shape: `{ error: string }` — pass `'not-allowed'` or\n * `'service-not-allowed'` for a permission denial (drives\n * `permissionState`/`VoicePermissionModal`), anything else is surfaced\n * as-is via `error`.\n * When `engine` is provided, `isSupported` is unconditionally `true` (the\n * host has already decided its engine is usable) — the browser-detection\n * path (`getSpeechRecognitionCtor`) is only consulted when `engine` is\n * omitted, so existing callers are unaffected.\n */\nexport function useVoiceTyping({ shortcut = true, engine } = {}) {\n const store = useEditorStore();\n const [isListening, setIsListening] = useState(false);\n const [status, setStatus] = useState('idle');\n const [permissionState, setPermissionState] = useState('unknown');\n const [error, setError] = useState(null);\n const recognitionRef = useRef(null);\n // { runId, blockId, offset } | null — this hook's single, internally-\n // owned source of truth for \"where does dictation write next.\" See the\n // class comment for why this is never re-read from the live DOM\n // mid-session EXCEPT via the selectionchange listener below, which only\n // reacts to a caret move this hook didn't itself just cause.\n const sessionAnchorRef = useRef(null);\n // { runId, offset } | null — wherever this hook itself most recently\n // placed the caret (via setCaretSync in writeEntryText/revertEntryText).\n // The selectionchange listener compares against this to tell \"the user\n // just clicked/arrow-keyed somewhere else\" apart from \"the selection\n // changed because WE just rewrote this run's text and moved the caret to\n // match\" — without it, every dictated word would look indistinguishable\n // from a manual caret move and constantly re-anchor to nowhere useful.\n const expectedCaretRef = useRef(null);\n // The one, single \"still being revised\" text buffer — replaced wholesale\n // on every onresult event, never matched against a prior event by index.\n // { runId, blockId, startOffset, insertedLength, needsLeadingSpace } | null\n const liveEntryRef = useRef(null);\n // How many result indices this hook has already permanently committed —\n // only ever advanced by this hook itself; used solely to avoid\n // reprocessing an index already committed, never to identify a phrase.\n const committedCountRef = useRef(0);\n const isSupported = engine != null || getSpeechRecognitionCtor() !== null;\n\n useEffect(() => {\n if (!navigator.permissions?.query) return undefined;\n let permissionStatus;\n let cancelled = false;\n navigator.permissions\n .query({ name: 'microphone' })\n .then((result) => {\n if (cancelled) return;\n permissionStatus = result;\n setPermissionState(result.state);\n permissionStatus.onchange = () => setPermissionState(permissionStatus.state);\n })\n .catch(() => {}); // unsupported descriptor name (e.g. Safari) — fall back to onerror-only detection\n return () => {\n cancelled = true;\n if (permissionStatus) permissionStatus.onchange = null;\n };\n }, []);\n\n const createEntryAtAnchor = useCallback(() => {\n if (!sessionAnchorRef.current) {\n const caret = resolveCollapsedCaret();\n if (!caret) return null;\n sessionAnchorRef.current = caret;\n }\n return {\n runId: sessionAnchorRef.current.runId,\n blockId: sessionAnchorRef.current.blockId,\n startOffset: sessionAnchorRef.current.offset,\n insertedLength: 0,\n };\n }, []);\n\n const writeEntryText = useCallback(\n (entry, text) => {\n const run = store.getRun(entry.runId);\n if (!run || run.type !== 'text') return;\n const value = run.value ?? '';\n const before = value.slice(0, entry.startOffset);\n // The leading-space decision is made once, against whatever sat\n // right before the anchor when this phrase began, and reused for\n // every revision of this same phrase — recomputing it against the\n // phrase's *own* still-growing text on each revision would flip it\n // back and forth as that text changes shape.\n if (entry.needsLeadingSpace === undefined) {\n const beforeChar = before[before.length - 1];\n entry.needsLeadingSpace =\n entry.startOffset > 0 && beforeChar !== undefined && !/\\s/.test(beforeChar);\n }\n const insertedText = (entry.needsLeadingSpace ? ' ' : '') + text;\n const after = value.slice(entry.startOffset + entry.insertedLength);\n // flushSync forces React to synchronously commit this store write\n // (including TextRunSpan's DOM-rewriting layout effect) before it\n // returns, so setCaretSync right after is guaranteed to see the\n // freshly-rewritten text node rather than a stale one.\n flushSync(() =>\n store.applyOperation(updateRun(entry.runId, { value: before + insertedText + after })),\n );\n entry.insertedLength = insertedText.length;\n const nextOffset = entry.startOffset + insertedText.length;\n setCaretSync(entry.runId, nextOffset);\n sessionAnchorRef.current = { runId: entry.runId, blockId: entry.blockId, offset: nextOffset };\n expectedCaretRef.current = { runId: entry.runId, offset: nextOffset };\n },\n [store],\n );\n\n const revertEntryText = useCallback(\n (entry) => {\n const run = store.getRun(entry.runId);\n if (!run || run.type !== 'text') return;\n const value = run.value ?? '';\n const restored =\n value.slice(0, entry.startOffset) + value.slice(entry.startOffset + entry.insertedLength);\n flushSync(() => store.applyOperation(updateRun(entry.runId, { value: restored })));\n setCaretSync(entry.runId, entry.startOffset);\n sessionAnchorRef.current = {\n runId: entry.runId,\n blockId: entry.blockId,\n offset: entry.startOffset,\n };\n expectedCaretRef.current = { runId: entry.runId, offset: entry.startOffset };\n },\n [store],\n );\n\n /**\n * Same text removal as `revertEntryText`, but WITHOUT touching the real\n * caret/selection — used when the user has just manually moved focus\n * away from `entry`'s own block (see the selectionchange listener below),\n * where `setCaretSync`ing back to it would fight the move the user just\n * made. Leaving the interim guess in place instead (an earlier version of\n * this fix did exactly that) caused a duplication bug: the NEXT interim\n * update for the same still-in-progress utterance writes the full\n * accumulated transcript again at the new anchor, since SpeechRecognition\n * interim results are always the whole phrase-so-far, not just the new\n * part — so the old block was left with a stale partial guess and the new\n * block got the same words all over again.\n */\n const removeEntryTextSilently = useCallback(\n (entry) => {\n const run = store.getRun(entry.runId);\n if (!run || run.type !== 'text') return;\n const value = run.value ?? '';\n const restored =\n value.slice(0, entry.startOffset) + value.slice(entry.startOffset + entry.insertedLength);\n store.applyOperation(updateRun(entry.runId, { value: restored }));\n },\n [store],\n );\n\n const handleRecognitionResult = useCallback(\n (event) => {\n let interimText = '';\n let hasInterim = false;\n const start = Math.max(event.resultIndex, committedCountRef.current);\n\n for (let i = start; i < event.results.length; i += 1) {\n const result = event.results[i];\n const transcript = result[0]?.transcript ?? '';\n\n if (!result.isFinal) {\n interimText += (interimText ? ' ' : '') + transcript;\n hasInterim = true;\n continue;\n }\n\n // Finalizing: reuse the live buffer if one exists (the common\n // case — this text was almost certainly interim a moment ago),\n // otherwise create a fresh one (a short utterance can finalize\n // with no prior interim event at all).\n const entry = liveEntryRef.current ?? createEntryAtAnchor();\n liveEntryRef.current = null;\n committedCountRef.current = i + 1;\n if (!entry) continue; // nowhere to anchor — drop this one\n\n const command = matchVoiceCommand(transcript);\n if (command) {\n revertEntryText(entry); // the interim guess was never real content\n if (command.type === 'stopDictation') {\n recognitionRef.current?.stop();\n } else {\n applyVoiceAction(store, entry.blockId, command);\n // The command may have moved focus into a different block/run\n // (e.g. a new paragraph) via this codebase's usual rAF-deferred\n // focus helpers — force the next phrase to re-resolve the live\n // caret rather than trusting a position that's now stale.\n // expectedCaretRef is cleared too, so the selectionchange\n // listener below doesn't mistake the command's own (rAF-\n // deferred, so not yet reflected here) focus move for a manual\n // one and race it.\n sessionAnchorRef.current = null;\n expectedCaretRef.current = null;\n setStatus('listening');\n }\n } else {\n writeEntryText(entry, transcript);\n setStatus('listening');\n }\n }\n\n if (hasInterim) {\n const entry = liveEntryRef.current ?? createEntryAtAnchor();\n if (entry) {\n liveEntryRef.current = entry;\n writeEntryText(entry, interimText);\n }\n }\n },\n [store, createEntryAtAnchor, writeEntryText, revertEntryText],\n );\n\n // Detects the user manually moving the caret (click, arrow keys, Tab into\n // another block, ...) WHILE dictation is listening, and redirects the\n // next dictated phrase there instead of silently continuing to write at\n // the stale session anchor — this is the fix for a real bug: without it,\n // clicking into a different block mid-dictation had no effect on where\n // the next phrase landed, and `setCaretSync` would keep snapping the\n // visible caret back to the old spot on every subsequent word, making it\n // look like focus was being fought over.\n //\n // Only reacts when the resolved caret differs from `expectedCaretRef` —\n // this hook's own record of wherever IT most recently placed the caret —\n // so it never mistakes its own writes (which also move the real DOM\n // selection) for a manual move. `expectedCaretRef` being null (right\n // after a voice command moved focus, before the next phrase has\n // re-resolved yet) means there's no baseline to compare against yet, so\n // the listener stays passive rather than racing that in-flight move.\n useEffect(() => {\n if (!isListening) return undefined;\n const handleSelectionChange = () => {\n const expected = expectedCaretRef.current;\n if (!expected) return;\n const caret = resolveCollapsedCaret();\n if (!caret) return; // selection isn't a collapsed caret inside a run right now — nothing to redirect to\n if (caret.runId === expected.runId && caret.offset === expected.offset) return; // our own write\n if (liveEntryRef.current) {\n // Discard the not-yet-finalized guess left behind in the old block —\n // see removeEntryTextSilently's own doc comment for why leaving it\n // in place duplicated text instead.\n removeEntryTextSilently(liveEntryRef.current);\n liveEntryRef.current = null;\n }\n sessionAnchorRef.current = caret;\n expectedCaretRef.current = { runId: caret.runId, offset: caret.offset };\n };\n document.addEventListener('selectionchange', handleSelectionChange);\n return () => document.removeEventListener('selectionchange', handleSelectionChange);\n }, [isListening, removeEntryTextSilently]);\n\n const start = useCallback(() => {\n if (!isSupported || recognitionRef.current) return;\n setError(null);\n const initialCaret = resolveCollapsedCaret();\n sessionAnchorRef.current = initialCaret;\n expectedCaretRef.current = initialCaret\n ? { runId: initialCaret.runId, offset: initialCaret.offset }\n : null;\n liveEntryRef.current = null;\n committedCountRef.current = 0;\n let recognition;\n if (engine) {\n recognition = engine();\n } else {\n const Ctor = getSpeechRecognitionCtor();\n recognition = new Ctor();\n }\n recognition.continuous = true;\n recognition.interimResults = true;\n recognition.onresult = handleRecognitionResult;\n recognition.onstart = () => setStatus('listening');\n recognition.onspeechstart = () => setStatus('listening');\n recognition.onspeechend = () => setStatus('processing');\n recognition.onerror = (event) => {\n if (event.error === 'not-allowed' || event.error === 'service-not-allowed') {\n setPermissionState('denied');\n setError(PERMISSION_DENIED_MESSAGE);\n } else {\n setError(event.error ?? 'Voice typing stopped unexpectedly.');\n }\n };\n recognition.onend = () => {\n recognitionRef.current = null;\n sessionAnchorRef.current = null;\n expectedCaretRef.current = null;\n liveEntryRef.current = null;\n committedCountRef.current = 0;\n setIsListening(false);\n setStatus('idle');\n };\n recognitionRef.current = recognition;\n recognition.start();\n setIsListening(true);\n }, [isSupported, handleRecognitionResult, engine]);\n\n const stop = useCallback(() => {\n recognitionRef.current?.stop();\n }, []);\n\n useEffect(() => () => recognitionRef.current?.stop(), []);\n\n useEffect(() => {\n if (!shortcut || !isSupported) return undefined;\n const handleKeyDown = (event) => {\n const mod = event.metaKey || event.ctrlKey;\n if (!mod || !event.shiftKey || event.key.toLowerCase() !== 'm') return;\n event.preventDefault();\n if (recognitionRef.current) stop();\n else start();\n };\n document.addEventListener('keydown', handleKeyDown);\n return () => document.removeEventListener('keydown', handleKeyDown);\n }, [shortcut, isSupported, start, stop]);\n\n return { isSupported, isListening, status, permissionState, error, start, stop };\n}\n","import { useEffect, useState } from 'react';\nimport { Modal } from './Modal.jsx';\n\n/**\n * Ready-made \"microphone access is blocked\" dialog for useVoiceTyping —\n * mount it once anywhere alongside a mic button: `<VoicePermissionModal\n * voice={voice} />`. Driven by `voice.permissionState`, so there's no\n * separate isOpen prop to wire — it opens itself the instant permission is\n * detected as denied (whether that's from a failed start() attempt or the\n * opportunistic Permissions API check useVoiceTyping does on mount).\n *\n * Manually dismissible (Escape/backdrop click, via Modal's own built-in\n * handling) rather than forced to stay open until permission is fixed —\n * `dismissed` tracks that locally and resets the instant a *new* denial\n * comes in (voice.error changing is what \"a new denial just happened\"\n * means here, since start() always clears error first) so the dialog\n * reappears on the next real attempt even after being dismissed once,\n * rather than staying silently suppressed forever.\n */\nexport function VoicePermissionModal({ voice }) {\n const [dismissed, setDismissed] = useState(false);\n\n useEffect(() => {\n setDismissed(false);\n }, [voice?.error]);\n\n const isOpen = voice?.permissionState === 'denied' && !dismissed;\n\n return (\n <Modal isOpen={isOpen} onClose={() => setDismissed(true)} title=\"Microphone access blocked\">\n <p>\n {voice?.error ||\n \"Microphone access is blocked. Enable it in your browser's site settings, then try again.\"}\n </p>\n <div className=\"be-modal-actions\">\n <button type=\"button\" className=\"be-modal-cancel\" onClick={() => setDismissed(true)}>\n Dismiss\n </button>\n <button type=\"button\" className=\"be-modal-save\" onClick={() => voice?.start?.()}>\n Try again\n </button>\n </div>\n </Modal>\n );\n}\n","import { useEffect, useState } from 'react';\n\n/**\n * Tracks the live collapsed caret's bounding rect, recomputed via the same\n * event set `useFloatingToolbarTrigger` uses for its own (non-collapsed)\n * selection tracking — `document`'s `selectionchange`, plus `window`\n * scroll (capture phase)/resize, since a `position: fixed` box anchored to\n * a rect needs to stay correct across page scroll. `selectionchange` fires\n * even for *programmatic* `Selection.addRange()` calls, which is what\n * `setCaretSync` (focusRun.js) already does on every voice-dictation text\n * insertion — so this reliably follows the caret while dictating without\n * useVoiceTyping needing to know anything about this hook at all.\n *\n * Only active while `isActive` is true (pass `voice.isListening`) — a\n * no-op with zero listeners attached otherwise, so this costs nothing when\n * not dictating. Returns `null` whenever there's no collapsed caret inside\n * an editable run (`[data-run-id]`), rather than a stale/wrong position.\n */\nexport function useCaretRect(isActive) {\n const [rect, setRect] = useState(null);\n\n useEffect(() => {\n if (!isActive) {\n setRect(null);\n return undefined;\n }\n\n const recompute = () => {\n const selection = window.getSelection?.();\n if (!selection || selection.rangeCount === 0 || !selection.isCollapsed) {\n setRect(null);\n return;\n }\n const node = selection.anchorNode;\n const el = node?.nodeType === 1 ? node : node?.parentElement;\n if (!el?.closest?.('[data-run-id]')) {\n setRect(null);\n return;\n }\n\n // jsdom (this package's own test suite) implements\n // Element.prototype.getBoundingClientRect but not\n // Range.prototype.getBoundingClientRect — falling back to a zero\n // rect keeps this testable without a real layout engine, same\n // convention as useFloatingToolbarTrigger.\n const nextRect = selection.getRangeAt(0).getBoundingClientRect?.() ?? {\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n width: 0,\n height: 0,\n };\n setRect(nextRect);\n };\n\n recompute();\n document.addEventListener('selectionchange', recompute);\n window.addEventListener('scroll', recompute, true);\n window.addEventListener('resize', recompute);\n return () => {\n document.removeEventListener('selectionchange', recompute);\n window.removeEventListener('scroll', recompute, true);\n window.removeEventListener('resize', recompute);\n };\n }, [isActive]);\n\n return rect;\n}\n","import { useRef } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useCaretRect } from './useCaretRect.js';\nimport { useAutoAdjustedCenteredLeft } from './usePopoverEdgeClamp.js';\nimport { MicIcon } from './icons.jsx';\n\n/**\n * Ready-made \"voice typing is actively working\" indicator for\n * useVoiceTyping — mount it once anywhere: `<VoiceListeningIndicator\n * voice={voice} />`. A toolbar-only status label is easy to miss since\n * attention is on the text being dictated, not the toolbar, so this\n * renders a small pulsing badge anchored right above the live caret\n * instead — the same `getBoundingClientRect()`-off-a-Range positioning\n * `FloatingToolbar`/`LinkHoverCard` already use elsewhere in this\n * codebase (see useCaretRect.js), so it visibly follows the caret as\n * dictated text streams in and moves it.\n *\n * Renders nothing when not listening, or when there's currently no\n * resolvable caret position (useCaretRect returns null) — never shows a\n * stale/wrong-position badge.\n */\nexport function VoiceListeningIndicator({ voice }) {\n const isActive = Boolean(voice?.isListening);\n const rect = useCaretRect(isActive);\n const badgeRef = useRef(null);\n // Only the horizontal axis is clamped — the badge sits right above the\n // caret (`translateY(-100%)`), which stays within the viewport in\n // practice since the caret itself must be visible to type into; the\n // centered horizontal offset is the one that can push it off-screen near\n // the left/right edge of a narrow viewport.\n const centerLeft = useAutoAdjustedCenteredLeft(badgeRef, Boolean(isActive && rect), rect?.left);\n\n if (!isActive || !rect || centerLeft == null) return null;\n\n const isProcessing = voice.status === 'processing';\n\n return createPortal(\n <div\n ref={badgeRef}\n className={`be-voice-indicator${isProcessing ? ' be-voice-indicator-processing' : ''}`}\n contentEditable={false}\n style={{\n position: 'fixed',\n top: rect.top - 8,\n left: centerLeft,\n transform: 'translate(-50%, -100%)',\n }}\n >\n <span className=\"be-voice-indicator-dot\" />\n <MicIcon size={12} />\n <span className=\"be-voice-indicator-label\">\n {isProcessing ? 'Processing…' : 'Listening…'}\n </span>\n </div>,\n document.body,\n );\n}\n"],"names":["restoreSelectionAfterHistoryChange","store","selection","_a","focusRunAtOffset","targetBlockId","_b","id","focusBlockStart","COMMANDS","PHRASE_TO_ACTION","phrases","action","phrase","normalizeUtterance","text","matchVoiceCommand","listVoiceCommands","description","applyOps","ops","op","getOwnRunIds","block","applyVoiceAction","blockId","insertSiblingAfterAndFocus","createTextLeafBlock","runIds","convertBlockType","focusRunStart","getSpeechRecognitionCtor","PERMISSION_DENIED_MESSAGE","useVoiceTyping","shortcut","engine","useEditorStore","isListening","setIsListening","useState","status","setStatus","permissionState","setPermissionState","error","setError","recognitionRef","useRef","sessionAnchorRef","expectedCaretRef","liveEntryRef","committedCountRef","isSupported","useEffect","permissionStatus","cancelled","result","createEntryAtAnchor","useCallback","caret","resolveCollapsedCaret","writeEntryText","entry","run","value","before","beforeChar","insertedText","after","flushSync","updateRun","nextOffset","setCaretSync","revertEntryText","restored","removeEntryTextSilently","handleRecognitionResult","event","interimText","hasInterim","start","i","transcript","command","handleSelectionChange","expected","initialCaret","recognition","Ctor","stop","handleKeyDown","VoicePermissionModal","voice","dismissed","setDismissed","isOpen","jsxs","Modal","jsx","useCaretRect","isActive","rect","setRect","recompute","node","el","nextRect","_d","_c","VoiceListeningIndicator","badgeRef","centerLeft","useAutoAdjustedCenteredLeft","isProcessing","createPortal","MicIcon"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";const c=require("react"),q=require("react-dom"),L=require("./icons-DtCsXuU6.cjs"),g=require("./leafBlockFactory-Dp382eP8.cjs"),V=require("./usePopoverEdgeClamp-CDG26jBM.cjs"),b=require("react/jsx-runtime");function N(e){var u,l;const s=(u=e.getPendingSelection)==null?void 0:u.call(e);if(s){g.focusRunAtOffset(s.runId,s.offset);return}const i=(((l=e.getPendingAffectedBlockIds)==null?void 0:l.call(e))??[]).find(a=>e.getBlock(a));i&&g.focusBlockStart(e,i)}const F=[{phrases:["new paragraph","next paragraph"],description:"Start a new paragraph",action:{type:"insertParagraph"}},{phrases:["heading one","heading 1"],description:"Convert the current block to a heading 1",action:{type:"convertBlock",blockType:"heading",props:{level:1}}},{phrases:["heading two","heading 2"],description:"Convert the current block to a heading 2",action:{type:"convertBlock",blockType:"heading",props:{level:2}}},{phrases:["heading three","heading 3"],description:"Convert the current block to a heading 3",action:{type:"convertBlock",blockType:"heading",props:{level:3}}},{phrases:["bulleted list","bullet point","bullet list"],description:"Convert the current block to a bulleted list item",action:{type:"convertBlock",blockType:"listItem",props:{ordered:!1,titleRunIds:[]}}},{phrases:["numbered list","ordered list"],description:"Convert the current block to a numbered list item",action:{type:"convertBlock",blockType:"listItem",props:{ordered:!0,titleRunIds:[]}}},{phrases:["checklist","to-do","to do","to-do list","to do list"],description:"Convert the current block to a to-do item",action:{type:"convertBlock",blockType:"listItem",props:{ordered:!1,checked:!1,titleRunIds:[]}}},{phrases:["quote"],description:"Convert the current block to a quote",action:{type:"convertBlock",blockType:"blockquote",props:{}}},{phrases:["code block","code"],description:"Convert the current block to a code block",action:{type:"convertBlock",blockType:"code",props:{language:"plaintext"}}},{phrases:["undo"],description:"Undo the last edit",action:{type:"undo"}},{phrases:["redo"],description:"Redo the last undone edit",action:{type:"redo"}},{phrases:["stop dictation","stop listening","stop"],description:"Stop dictation",action:{type:"stopDictation"}}],H=new Map;for(const{phrases:e,action:s}of F)for(const n of e)H.set(n,s);function $(e){return(e??"").toLowerCase().trim().replace(/\s+/g," ").replace(/[.,!?]+$/,"")}function G(e){return H.get($(e))??null}function J(){return F.map(({phrases:e,description:s})=>({phrases:[...e],description:s}))}function Q(e,s){if(typeof e.performBatch=="function")e.performBatch(s);else for(const n of s)e.applyOperation(n)}function W(e){var s;return((s=e==null?void 0:e.props)==null?void 0:s.titleRunIds)??(e==null?void 0:e.contentIds)??[]}function X(e,s,n){var i,u;if(n.type==="insertParagraph"){g.insertSiblingAfterAndFocus(e,s,g.createTextLeafBlock("paragraph"));return}if(n.type==="convertBlock"){const l=e.getBlock(s);if(!l)return;const a=W(l),{ops:E}=V.convertBlockType(e,s,n.blockType,n.props,a);Q(e,E),a[0]&&g.focusRunStart(a[0]);return}if(n.type==="undo"){(i=e.undo)!=null&&i.call(e)&&N(e);return}n.type==="redo"&&(u=e.redo)!=null&&u.call(e)&&N(e)}function _(){return typeof window>"u"?null:window.SpeechRecognition??window.webkitSpeechRecognition??null}const Y="Microphone access is blocked. Enable it in your browser's site settings, then try again.";function Z({shortcut:e=!0,engine:s}={}){const n=L.useEditorStore(),[i,u]=c.useState(!1),[l,a]=c.useState("idle"),[E,k]=c.useState("unknown"),[v,C]=c.useState(null),p=c.useRef(null),d=c.useRef(null),I=c.useRef(null),m=c.useRef(null),O=c.useRef(0),x=s!=null||_()!==null;c.useEffect(()=>{var o;if(!((o=navigator.permissions)!=null&&o.query))return;let t,r=!1;return navigator.permissions.query({name:"microphone"}).then(f=>{r||(t=f,k(f.state),t.onchange=()=>k(t.state))}).catch(()=>{}),()=>{r=!0,t&&(t.onchange=null)}},[]);const T=c.useCallback(()=>{if(!d.current){const t=g.resolveCollapsedCaret();if(!t)return null;d.current=t}return{runId:d.current.runId,blockId:d.current.blockId,startOffset:d.current.offset,insertedLength:0}},[]),A=c.useCallback((t,r)=>{const o=n.getRun(t.runId);if(!o||o.type!=="text")return;const f=o.value??"",y=f.slice(0,t.startOffset);if(t.needsLeadingSpace===void 0){const w=y[y.length-1];t.needsLeadingSpace=t.startOffset>0&&w!==void 0&&!/\s/.test(w)}const R=(t.needsLeadingSpace?" ":"")+r,h=f.slice(t.startOffset+t.insertedLength);q.flushSync(()=>n.applyOperation(L.updateRun(t.runId,{value:y+R+h}))),t.insertedLength=R.length;const S=t.startOffset+R.length;g.setCaretSync(t.runId,S),d.current={runId:t.runId,blockId:t.blockId,offset:S},I.current={runId:t.runId,offset:S}},[n]),D=c.useCallback(t=>{const r=n.getRun(t.runId);if(!r||r.type!=="text")return;const o=r.value??"",f=o.slice(0,t.startOffset)+o.slice(t.startOffset+t.insertedLength);q.flushSync(()=>n.applyOperation(L.updateRun(t.runId,{value:f}))),g.setCaretSync(t.runId,t.startOffset),d.current={runId:t.runId,blockId:t.blockId,offset:t.startOffset},I.current={runId:t.runId,offset:t.startOffset}},[n]),z=c.useCallback(t=>{const r=n.getRun(t.runId);if(!r||r.type!=="text")return;const o=r.value??"",f=o.slice(0,t.startOffset)+o.slice(t.startOffset+t.insertedLength);n.applyOperation(L.updateRun(t.runId,{value:f}))},[n]),K=c.useCallback(t=>{var y,R;let r="",o=!1;const f=Math.max(t.resultIndex,O.current);for(let h=f;h<t.results.length;h+=1){const S=t.results[h],w=((y=S[0])==null?void 0:y.transcript)??"";if(!S.isFinal){r+=(r?" ":"")+w,o=!0;continue}const B=m.current??T();if(m.current=null,O.current=h+1,!B)continue;const j=G(w);j?(D(B),j.type==="stopDictation"?(R=p.current)==null||R.stop():(X(n,B.blockId,j),d.current=null,I.current=null,a("listening"))):(A(B,w),a("listening"))}if(o){const h=m.current??T();h&&(m.current=h,A(h,r))}},[n,T,A,D]);c.useEffect(()=>{if(!i)return;const t=()=>{const r=I.current;if(!r)return;const o=g.resolveCollapsedCaret();o&&(o.runId===r.runId&&o.offset===r.offset||(m.current&&(z(m.current),m.current=null),d.current=o,I.current={runId:o.runId,offset:o.offset}))};return document.addEventListener("selectionchange",t),()=>document.removeEventListener("selectionchange",t)},[i,z]);const M=c.useCallback(()=>{if(!x||p.current)return;C(null);const t=g.resolveCollapsedCaret();d.current=t,I.current=t?{runId:t.runId,offset:t.offset}:null,m.current=null,O.current=0;let r;if(s)r=s();else{const o=_();r=new o}r.continuous=!0,r.interimResults=!0,r.onresult=K,r.onstart=()=>a("listening"),r.onspeechstart=()=>a("listening"),r.onspeechend=()=>a("processing"),r.onerror=o=>{o.error==="not-allowed"||o.error==="service-not-allowed"?(k("denied"),C(Y)):C(o.error??"Voice typing stopped unexpectedly.")},r.onend=()=>{p.current=null,d.current=null,I.current=null,m.current=null,O.current=0,u(!1),a("idle")},p.current=r,r.start(),u(!0)},[x,K,s]),P=c.useCallback(()=>{var t;(t=p.current)==null||t.stop()},[]);return c.useEffect(()=>()=>{var t;return(t=p.current)==null?void 0:t.stop()},[]),c.useEffect(()=>{if(!e||!x)return;const t=r=>{!(r.metaKey||r.ctrlKey)||!r.shiftKey||r.key.toLowerCase()!=="m"||(r.preventDefault(),p.current?P():M())};return document.addEventListener("keydown",t),()=>document.removeEventListener("keydown",t)},[e,x,M,P]),{isSupported:x,isListening:i,status:l,permissionState:E,error:v,start:M,stop:P}}function ee({voice:e}){const[s,n]=c.useState(!1);c.useEffect(()=>{n(!1)},[e==null?void 0:e.error]);const i=(e==null?void 0:e.permissionState)==="denied"&&!s;return b.jsxs(V.Modal,{isOpen:i,onClose:()=>n(!0),title:"Microphone access blocked",children:[b.jsx("p",{children:(e==null?void 0:e.error)||"Microphone access is blocked. Enable it in your browser's site settings, then try again."}),b.jsxs("div",{className:"be-modal-actions",children:[b.jsx("button",{type:"button",className:"be-modal-cancel",onClick:()=>n(!0),children:"Dismiss"}),b.jsx("button",{type:"button",className:"be-modal-save",onClick:()=>{var u;return(u=e==null?void 0:e.start)==null?void 0:u.call(e)},children:"Try again"})]})]})}function U(e){const[s,n]=c.useState(null);return c.useEffect(()=>{if(!e){n(null);return}const i=()=>{var k,v,C,p;const u=(k=window.getSelection)==null?void 0:k.call(window);if(!u||u.rangeCount===0||!u.isCollapsed){n(null);return}const l=u.anchorNode,a=(l==null?void 0:l.nodeType)===1?l:l==null?void 0:l.parentElement;if(!((v=a==null?void 0:a.closest)!=null&&v.call(a,"[data-run-id]"))){n(null);return}const E=((p=(C=u.getRangeAt(0)).getBoundingClientRect)==null?void 0:p.call(C))??{top:0,left:0,right:0,bottom:0,width:0,height:0};n(E)};return i(),document.addEventListener("selectionchange",i),window.addEventListener("scroll",i,!0),window.addEventListener("resize",i),()=>{document.removeEventListener("selectionchange",i),window.removeEventListener("scroll",i,!0),window.removeEventListener("resize",i)}},[e]),s}function te({voice:e}){const s=!!(e!=null&&e.isListening),n=U(s),i=c.useRef(null),u=V.useAutoAdjustedCenteredLeft(i,!!(s&&n),n==null?void 0:n.left);if(!s||!n||u==null)return null;const l=e.status==="processing";return q.createPortal(b.jsxs("div",{ref:i,className:`be-voice-indicator${l?" be-voice-indicator-processing":""}`,contentEditable:!1,style:{position:"fixed",top:n.top-8,left:u,transform:"translate(-50%, -100%)"},children:[b.jsx("span",{className:"be-voice-indicator-dot"}),b.jsx(L.MicIcon,{size:12}),b.jsx("span",{className:"be-voice-indicator-label",children:l?"Processing…":"Listening…"})]}),document.body)}exports.VoiceListeningIndicator=te;exports.VoicePermissionModal=ee;exports.listVoiceCommands=J;exports.restoreSelectionAfterHistoryChange=N;exports.useCaretRect=U;exports.useVoiceTyping=Z;
|
|
2
|
+
//# sourceMappingURL=VoiceListeningIndicator-DNX8JlQM.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoiceListeningIndicator-DNX8JlQM.cjs","sources":["../../src/react/restoreHistorySelection.js","../../src/voice/voiceCommands.js","../../src/voice/applyVoiceAction.js","../../src/react/useVoiceTyping.js","../../src/react/VoicePermissionModal.jsx","../../src/react/useCaretRect.js","../../src/react/VoiceListeningIndicator.jsx"],"sourcesContent":["import { focusRunAtOffset } from './focusRun.js';\nimport { focusBlockStart } from '../blocks/shared/navigationCommands.js';\n\n/**\n * Moves the caret to wherever the undo/redo that was just performed should\n * leave it — call this right after a truthy store.undo()/store.redo().\n * Prefers the precise {runId, offset} History derives for a plain text-run\n * edit; falls back to the start of the first still-existing block the\n * entry touched for structural edits (insert/remove/move block, etc.),\n * which don't reduce to one offset but still shouldn't leave focus\n * dangling on whatever had it before (usually a toolbar button, or a DOM\n * node a delete/undo just replaced outright).\n */\nexport function restoreSelectionAfterHistoryChange(store) {\n const selection = store.getPendingSelection?.();\n if (selection) {\n focusRunAtOffset(selection.runId, selection.offset);\n return;\n }\n\n const affectedIds = store.getPendingAffectedBlockIds?.() ?? [];\n const targetBlockId = affectedIds.find((id) => store.getBlock(id));\n if (targetBlockId) focusBlockStart(store, targetBlockId);\n}\n","/**\n * The spoken-phrase -> editor-action table driving voice typing (see\n * useVoiceTyping.js). Deliberately data-driven and pure (no store/DOM\n * access here at all) so the \"does this phrase count as a command\"\n * decision is directly unit-testable without a real SpeechRecognition\n * engine — matchVoiceCommand is exercised with plain strings.\n *\n * Only a *complete, isolated* recognized utterance is ever checked against\n * this table (see useVoiceTyping's isFinal handling) — a command phrase\n * spoken as part of a longer sentence while dictating never matches,\n * since the caller only calls this with one finalized speech segment at a\n * time, normalized as its own whole utterance.\n *\n * Each entry's `description` is shown to end users (see listVoiceCommands)\n * — not just a code comment — so keep it short and phrased as what saying\n * it does, not how it's implemented.\n */\nconst COMMANDS = [\n {\n phrases: ['new paragraph', 'next paragraph'],\n description: 'Start a new paragraph',\n action: { type: 'insertParagraph' },\n },\n {\n phrases: ['heading one', 'heading 1'],\n description: 'Convert the current block to a heading 1',\n action: { type: 'convertBlock', blockType: 'heading', props: { level: 1 } },\n },\n {\n phrases: ['heading two', 'heading 2'],\n description: 'Convert the current block to a heading 2',\n action: { type: 'convertBlock', blockType: 'heading', props: { level: 2 } },\n },\n {\n phrases: ['heading three', 'heading 3'],\n description: 'Convert the current block to a heading 3',\n action: { type: 'convertBlock', blockType: 'heading', props: { level: 3 } },\n },\n {\n phrases: ['bulleted list', 'bullet point', 'bullet list'],\n description: 'Convert the current block to a bulleted list item',\n action: {\n type: 'convertBlock',\n blockType: 'listItem',\n props: { ordered: false, titleRunIds: [] },\n },\n },\n {\n phrases: ['numbered list', 'ordered list'],\n description: 'Convert the current block to a numbered list item',\n action: {\n type: 'convertBlock',\n blockType: 'listItem',\n props: { ordered: true, titleRunIds: [] },\n },\n },\n {\n phrases: ['checklist', 'to-do', 'to do', 'to-do list', 'to do list'],\n description: 'Convert the current block to a to-do item',\n action: {\n type: 'convertBlock',\n blockType: 'listItem',\n props: { ordered: false, checked: false, titleRunIds: [] },\n },\n },\n {\n phrases: ['quote'],\n description: 'Convert the current block to a quote',\n action: { type: 'convertBlock', blockType: 'blockquote', props: {} },\n },\n {\n phrases: ['code block', 'code'],\n description: 'Convert the current block to a code block',\n action: { type: 'convertBlock', blockType: 'code', props: { language: 'plaintext' } },\n },\n { phrases: ['undo'], description: 'Undo the last edit', action: { type: 'undo' } },\n { phrases: ['redo'], description: 'Redo the last undone edit', action: { type: 'redo' } },\n {\n phrases: ['stop dictation', 'stop listening', 'stop'],\n description: 'Stop dictation',\n action: { type: 'stopDictation' },\n },\n];\n\nconst PHRASE_TO_ACTION = new Map();\nfor (const { phrases, action } of COMMANDS) {\n for (const phrase of phrases) PHRASE_TO_ACTION.set(phrase, action);\n}\n\n/**\n * Lowercases, trims, collapses internal whitespace, and strips trailing\n * sentence punctuation a speech engine commonly appends to a finalized\n * utterance (\".\", \",\", \"!\", \"?\") — none of that punctuation is meaningful\n * for matching a short command phrase.\n */\nexport function normalizeUtterance(text) {\n return (text ?? '')\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, ' ')\n .replace(/[.,!?]+$/, '');\n}\n\n/**\n * Returns the matching command action for a normalized, complete utterance,\n * or null if it's plain dictation (no command matched). Exact-match only —\n * deliberately not a substring/contains check, so a sentence that merely\n * *mentions* a command phrase while dictating prose is never misread as a\n * command (matches only when the whole finalized utterance IS the phrase).\n */\nexport function matchVoiceCommand(text) {\n return PHRASE_TO_ACTION.get(normalizeUtterance(text)) ?? null;\n}\n\n/**\n * Read-only `[{ phrases, description }]` list for a host to render as an\n * in-app \"what can I say\" help list — pure data, no rendering opinion (see\n * examples/basic's own \"Voice commands\" button for one way to display it).\n */\nexport function listVoiceCommands() {\n return COMMANDS.map(({ phrases, description }) => ({ phrases: [...phrases], description }));\n}\n","import { convertBlockType } from '../blocks/shared/convertBlockType.js';\nimport { insertSiblingAfterAndFocus } from '../blocks/shared/blockCommands.js';\nimport { createTextLeafBlock } from '../blocks/shared/leafBlockFactory.js';\nimport { focusRunStart } from '../react/focusRun.js';\nimport { restoreSelectionAfterHistoryChange } from '../react/restoreHistorySelection.js';\n\nfunction applyOps(store, ops) {\n if (typeof store.performBatch === 'function') store.performBatch(ops);\n else for (const op of ops) store.applyOperation(op);\n}\n\nfunction getOwnRunIds(block) {\n return block?.props?.titleRunIds ?? block?.contentIds ?? [];\n}\n\n/**\n * Executes one matched voice command (see voiceCommands.js) against the\n * store. `blockId` is wherever the caret currently is — resolved by the\n * caller (useVoiceTyping) via resolveCollapsedCaret, the same way every\n * other command in this codebase locates \"the current block\".\n *\n * 'convertBlock' reuses convertBlockType directly (not the slash-command\n * factory path) specifically so any text already dictated into the\n * current block survives the conversion — the same reason\n * markdownShortcuts.js uses it, and doubly important here since a live\n * dictation session is far more likely to have real typed content in the\n * block at the moment a structural command is spoken.\n */\nexport function applyVoiceAction(store, blockId, action) {\n if (action.type === 'insertParagraph') {\n insertSiblingAfterAndFocus(store, blockId, createTextLeafBlock('paragraph'));\n return;\n }\n\n if (action.type === 'convertBlock') {\n const block = store.getBlock(blockId);\n if (!block) return;\n const runIds = getOwnRunIds(block);\n const { ops } = convertBlockType(store, blockId, action.blockType, action.props, runIds);\n applyOps(store, ops);\n if (runIds[0]) focusRunStart(runIds[0]);\n return;\n }\n\n if (action.type === 'undo') {\n if (store.undo?.()) restoreSelectionAfterHistoryChange(store);\n return;\n }\n\n if (action.type === 'redo') {\n if (store.redo?.()) restoreSelectionAfterHistoryChange(store);\n }\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport { flushSync } from 'react-dom';\nimport { useEditorStore } from './EditorProvider.jsx';\nimport { resolveCollapsedCaret } from './selectionResolve.js';\nimport { setCaretSync } from './focusRun.js';\nimport { updateRun } from '../store/operations.js';\nimport { matchVoiceCommand } from '../voice/voiceCommands.js';\nimport { applyVoiceAction } from '../voice/applyVoiceAction.js';\n\nfunction getSpeechRecognitionCtor() {\n if (typeof window === 'undefined') return null;\n return window.SpeechRecognition ?? window.webkitSpeechRecognition ?? null;\n}\n\nconst PERMISSION_DENIED_MESSAGE =\n \"Microphone access is blocked. Enable it in your browser's site settings, then try again.\";\n\n/**\n * Native-browser (Web Speech API) voice typing: live, continuous dictation\n * into the current caret position (text appears and self-corrects as you\n * speak, not just after each pause), plus spoken structural commands\n * (\"heading one\", \"new paragraph\", \"undo\", \"stop dictation\", ...)\n * recognized via voiceCommands.js — see that file for the full command\n * table and matching rules (also exported there as listVoiceCommands() for\n * an in-app \"what can I say\" help list).\n *\n * Deliberately built on the browser's own SpeechRecognition rather than a\n * bundled speech-to-text SDK: real speech recognition needs either a\n * network round-trip to a vendor's servers or a genuinely heavy on-device\n * model, either of which conflicts with this package staying zero-runtime-\n * dependency. `isSupported` is false (start()/stop() are no-ops) wherever\n * the API doesn't exist (Firefox, most notably) — a host app decides how\n * to surface that (hide the mic button, show a note, etc.), this hook just\n * degrades safely rather than throwing.\n *\n * ---- Never trust SpeechRecognition's resultIndex for phrase identity —\n * this is the part two earlier versions of this hook got wrong ----\n *\n * `sessionAnchorRef` (`{ runId, blockId, offset }`) is this hook's own,\n * single source of truth for \"where does the next chunk of dictated text\n * go\" — resolved from the real caret when `start()` is called, and after\n * that updated only by this hook's own writes OR by the `selectionchange`\n * listener below noticing the user manually moved the caret somewhere else\n * (click, arrow keys, Tab into another block) — it is never blindly\n * re-read from `window.getSelection()` on some other trigger (e.g. every\n * `onresult` event), which is what the two broken attempts below actually\n * ran into.\n *\n * `liveEntryRef` is the *one* mutable \"currently being revised, not yet\n * final\" text buffer — on every `onresult` event, every non-final result\n * in that event is concatenated into one string and used to *fully\n * replace* this single buffer's contents, regardless of which\n * `resultIndex`(es) the browser happened to report them under.\n * `committedCountRef` is a separate, monotonically-increasing \"how many\n * result indices have I already permanently committed\" counter, advanced\n * only when *this hook* finalizes something — used purely as a \"don't\n * reprocess an index I already committed\" guard, never for matching a new\n * event's index back to an existing in-progress phrase.\n *\n * This design replaced two earlier, both-broken attempts at using\n * `resultIndex` as a phrase identifier:\n * 1. Tracking a `Map` of every \"in-flight\" phrase keyed by resultIndex,\n * re-resolving the live caret each time a new index appeared. Real\n * Chrome doesn't keep resultIndex behaving cleanly enough for that —\n * a later index could get matched against a stale map entry, and\n * text landed out of order (\"11234567 89 10 11 ...\" for numbers\n * spoken in order).\n * 2. Comparing consecutive events' resultIndex to decide \"is this a\n * revision of the same phrase, or a new one\" (one phrase at a time,\n * no Map). Real Chrome can advance resultIndex on what is still\n * conceptually the same in-progress phrase, which made this treat\n * every revision as a brand-new phrase, anchored after the\n * previous (wrongly-separate) one's text — producing runaway,\n * compounding duplication (\"test for the\" -> \"test for the life\" ->\n * \"test for the life desc\" -> ... each revision re-typing the whole\n * growing sentence from scratch instead of replacing the last guess).\n *\n * Concatenate-all-non-final-into-one-buffer is the standard pattern real\n * speech-to-text integrations use (e.g. how the `react-speech-recognition`\n * library and most Web Speech API tutorials build up \"interim transcript\"\n * + \"final transcript\" separately) — it needs no assumption whatsoever\n * about how the browser indexes results across events.\n *\n * Every write still goes through `flushSync` (react-dom) before\n * `setCaretSync` (the non-rAF variant of focusRunAtOffset, see\n * focusRun.js) moves the *visible* caret to match — `TextRunSpan`\n * (EditableBlockContent.jsx) fully destroys and recreates the run's DOM\n * text node on every value change, which resets the browser's native\n * Selection, and a plain `store.applyOperation(...)` call is not\n * guaranteed to have committed that DOM change by the time it returns\n * (React may batch it) — `flushSync` forces the commit first. This part\n * only affects what the user visually sees the caret doing; the actual\n * text-insertion math never depends on it, since `sessionAnchorRef` is the\n * authority, not the DOM.\n *\n * Text is inserted/updated plain, with no \"tentative\" styling while\n * interim — it just grows and occasionally self-corrects as the engine\n * hears more context, the same way Google's own voice typing behaves.\n *\n * Command detection vs. plain dictation: only a *finalized* segment is\n * ever checked against the command table (matchVoiceCommand) — natural\n * pauses in speech are what the Speech API itself uses to decide a segment\n * is \"final\", so a command spoken as its own isolated utterance is\n * detected reliably, while the same words embedded in a longer dictated\n * sentence never match (the whole segment has to equal a known phrase,\n * not just contain one). If a phrase that was already live-inserted as\n * interim text finalizes as a command, the interim text is reverted\n * (removed) before the command runs — so e.g. saying \"heading one\" never\n * leaves \"heading one\" sitting in the paragraph before converting it, even\n * though \"heading\" alone may have briefly appeared while still mid-\n * utterance.\n *\n * `status` (`'idle' | 'listening' | 'processing'`) is driven by the Speech\n * API's own `onspeechstart`/`onspeechend` events, not a fabricated timer:\n * `onspeechend` fires when the engine detects you've stopped talking and is\n * about to finalize what it heard ('processing'), and the next committed\n * final result (or a fresh `onspeechstart`) returns to 'listening'. Neither\n * event is guaranteed on every engine — if they never fire, status simply\n * stays 'listening' for the whole session, a safe degrade, not a crash.\n * `VoiceListeningIndicator` (also exported) renders a small badge that\n * follows the live caret off this state.\n *\n * Mic permission: `recognition.onerror` reporting 'not-allowed' or\n * 'service-not-allowed' is the one cross-browser-reliable signal that\n * permission was denied (the Permissions API's 'microphone' descriptor\n * isn't supported in Firefox at all, and is inconsistent in Safari) — that\n * error sets `permissionState: 'denied'`. Where the Permissions API *is*\n * available, it's also queried opportunistically on mount (and its own\n * `change` event subscribed to) purely so a previously-denied permission\n * shows up in `permissionState` before the user even presses the mic\n * button, rather than only ever finding out after a failed start().\n * `VoicePermissionModal` (also exported) renders itself automatically off\n * this same state.\n *\n * `options.shortcut` (default true) wires one native, document-level\n * Ctrl/Cmd+Shift+M keydown listener that toggles start()/stop() — pass\n * `{ shortcut: false }` to opt out if a host wants its own binding.\n *\n * `options.engine` lets a host app supply its own recognition engine\n * instead of relying on `window.SpeechRecognition`/`webkitSpeechRecognition`\n * — needed because that API is commonly absent or non-functional in\n * embedded webviews (e.g. Tauri's WebView2/WKWebView), which have no Web\n * Speech API implementation, or a Chromium-derived one that's missing the\n * private Google API key real Chrome ships with (constructor exists, but\n * every session fails immediately with a `'network'` error). `engine` is a\n * zero-arg factory, `() => SpeechRecognitionLike`, called once per `start()`\n * to produce a fresh instance implementing the same surface this hook\n * already drives on the native constructor:\n * - properties: `continuous`, `interimResults` (set by this hook before\n * starting; the engine should honor them)\n * - event handlers this hook assigns: `onresult`, `onstart`,\n * `onspeechstart`, `onspeechend`, `onerror`, `onend`\n * - methods: `start()`, `stop()`\n * - `onresult`'s event shape: `{ resultIndex, results }` where `results`\n * is array-like of `results[i][0].transcript` (string) and\n * `results[i].isFinal` (boolean) — exactly the native SpeechRecognition\n * result shape, since `handleRecognitionResult` above reads it that way\n * regardless of what produced it (native browser engine, a native/OS\n * bridge, or a cloud STT service streamed back into this shape).\n * - `onerror`'s event shape: `{ error: string }` — pass `'not-allowed'` or\n * `'service-not-allowed'` for a permission denial (drives\n * `permissionState`/`VoicePermissionModal`), anything else is surfaced\n * as-is via `error`.\n * When `engine` is provided, `isSupported` is unconditionally `true` (the\n * host has already decided its engine is usable) — the browser-detection\n * path (`getSpeechRecognitionCtor`) is only consulted when `engine` is\n * omitted, so existing callers are unaffected.\n */\nexport function useVoiceTyping({ shortcut = true, engine } = {}) {\n const store = useEditorStore();\n const [isListening, setIsListening] = useState(false);\n const [status, setStatus] = useState('idle');\n const [permissionState, setPermissionState] = useState('unknown');\n const [error, setError] = useState(null);\n const recognitionRef = useRef(null);\n // { runId, blockId, offset } | null — this hook's single, internally-\n // owned source of truth for \"where does dictation write next.\" See the\n // class comment for why this is never re-read from the live DOM\n // mid-session EXCEPT via the selectionchange listener below, which only\n // reacts to a caret move this hook didn't itself just cause.\n const sessionAnchorRef = useRef(null);\n // { runId, offset } | null — wherever this hook itself most recently\n // placed the caret (via setCaretSync in writeEntryText/revertEntryText).\n // The selectionchange listener compares against this to tell \"the user\n // just clicked/arrow-keyed somewhere else\" apart from \"the selection\n // changed because WE just rewrote this run's text and moved the caret to\n // match\" — without it, every dictated word would look indistinguishable\n // from a manual caret move and constantly re-anchor to nowhere useful.\n const expectedCaretRef = useRef(null);\n // The one, single \"still being revised\" text buffer — replaced wholesale\n // on every onresult event, never matched against a prior event by index.\n // { runId, blockId, startOffset, insertedLength, needsLeadingSpace } | null\n const liveEntryRef = useRef(null);\n // How many result indices this hook has already permanently committed —\n // only ever advanced by this hook itself; used solely to avoid\n // reprocessing an index already committed, never to identify a phrase.\n const committedCountRef = useRef(0);\n const isSupported = engine != null || getSpeechRecognitionCtor() !== null;\n\n useEffect(() => {\n if (!navigator.permissions?.query) return undefined;\n let permissionStatus;\n let cancelled = false;\n navigator.permissions\n .query({ name: 'microphone' })\n .then((result) => {\n if (cancelled) return;\n permissionStatus = result;\n setPermissionState(result.state);\n permissionStatus.onchange = () => setPermissionState(permissionStatus.state);\n })\n .catch(() => {}); // unsupported descriptor name (e.g. Safari) — fall back to onerror-only detection\n return () => {\n cancelled = true;\n if (permissionStatus) permissionStatus.onchange = null;\n };\n }, []);\n\n const createEntryAtAnchor = useCallback(() => {\n if (!sessionAnchorRef.current) {\n const caret = resolveCollapsedCaret();\n if (!caret) return null;\n sessionAnchorRef.current = caret;\n }\n return {\n runId: sessionAnchorRef.current.runId,\n blockId: sessionAnchorRef.current.blockId,\n startOffset: sessionAnchorRef.current.offset,\n insertedLength: 0,\n };\n }, []);\n\n const writeEntryText = useCallback(\n (entry, text) => {\n const run = store.getRun(entry.runId);\n if (!run || run.type !== 'text') return;\n const value = run.value ?? '';\n const before = value.slice(0, entry.startOffset);\n // The leading-space decision is made once, against whatever sat\n // right before the anchor when this phrase began, and reused for\n // every revision of this same phrase — recomputing it against the\n // phrase's *own* still-growing text on each revision would flip it\n // back and forth as that text changes shape.\n if (entry.needsLeadingSpace === undefined) {\n const beforeChar = before[before.length - 1];\n entry.needsLeadingSpace =\n entry.startOffset > 0 && beforeChar !== undefined && !/\\s/.test(beforeChar);\n }\n const insertedText = (entry.needsLeadingSpace ? ' ' : '') + text;\n const after = value.slice(entry.startOffset + entry.insertedLength);\n // flushSync forces React to synchronously commit this store write\n // (including TextRunSpan's DOM-rewriting layout effect) before it\n // returns, so setCaretSync right after is guaranteed to see the\n // freshly-rewritten text node rather than a stale one.\n flushSync(() =>\n store.applyOperation(updateRun(entry.runId, { value: before + insertedText + after })),\n );\n entry.insertedLength = insertedText.length;\n const nextOffset = entry.startOffset + insertedText.length;\n setCaretSync(entry.runId, nextOffset);\n sessionAnchorRef.current = { runId: entry.runId, blockId: entry.blockId, offset: nextOffset };\n expectedCaretRef.current = { runId: entry.runId, offset: nextOffset };\n },\n [store],\n );\n\n const revertEntryText = useCallback(\n (entry) => {\n const run = store.getRun(entry.runId);\n if (!run || run.type !== 'text') return;\n const value = run.value ?? '';\n const restored =\n value.slice(0, entry.startOffset) + value.slice(entry.startOffset + entry.insertedLength);\n flushSync(() => store.applyOperation(updateRun(entry.runId, { value: restored })));\n setCaretSync(entry.runId, entry.startOffset);\n sessionAnchorRef.current = {\n runId: entry.runId,\n blockId: entry.blockId,\n offset: entry.startOffset,\n };\n expectedCaretRef.current = { runId: entry.runId, offset: entry.startOffset };\n },\n [store],\n );\n\n /**\n * Same text removal as `revertEntryText`, but WITHOUT touching the real\n * caret/selection — used when the user has just manually moved focus\n * away from `entry`'s own block (see the selectionchange listener below),\n * where `setCaretSync`ing back to it would fight the move the user just\n * made. Leaving the interim guess in place instead (an earlier version of\n * this fix did exactly that) caused a duplication bug: the NEXT interim\n * update for the same still-in-progress utterance writes the full\n * accumulated transcript again at the new anchor, since SpeechRecognition\n * interim results are always the whole phrase-so-far, not just the new\n * part — so the old block was left with a stale partial guess and the new\n * block got the same words all over again.\n */\n const removeEntryTextSilently = useCallback(\n (entry) => {\n const run = store.getRun(entry.runId);\n if (!run || run.type !== 'text') return;\n const value = run.value ?? '';\n const restored =\n value.slice(0, entry.startOffset) + value.slice(entry.startOffset + entry.insertedLength);\n store.applyOperation(updateRun(entry.runId, { value: restored }));\n },\n [store],\n );\n\n const handleRecognitionResult = useCallback(\n (event) => {\n let interimText = '';\n let hasInterim = false;\n const start = Math.max(event.resultIndex, committedCountRef.current);\n\n for (let i = start; i < event.results.length; i += 1) {\n const result = event.results[i];\n const transcript = result[0]?.transcript ?? '';\n\n if (!result.isFinal) {\n interimText += (interimText ? ' ' : '') + transcript;\n hasInterim = true;\n continue;\n }\n\n // Finalizing: reuse the live buffer if one exists (the common\n // case — this text was almost certainly interim a moment ago),\n // otherwise create a fresh one (a short utterance can finalize\n // with no prior interim event at all).\n const entry = liveEntryRef.current ?? createEntryAtAnchor();\n liveEntryRef.current = null;\n committedCountRef.current = i + 1;\n if (!entry) continue; // nowhere to anchor — drop this one\n\n const command = matchVoiceCommand(transcript);\n if (command) {\n revertEntryText(entry); // the interim guess was never real content\n if (command.type === 'stopDictation') {\n recognitionRef.current?.stop();\n } else {\n applyVoiceAction(store, entry.blockId, command);\n // The command may have moved focus into a different block/run\n // (e.g. a new paragraph) via this codebase's usual rAF-deferred\n // focus helpers — force the next phrase to re-resolve the live\n // caret rather than trusting a position that's now stale.\n // expectedCaretRef is cleared too, so the selectionchange\n // listener below doesn't mistake the command's own (rAF-\n // deferred, so not yet reflected here) focus move for a manual\n // one and race it.\n sessionAnchorRef.current = null;\n expectedCaretRef.current = null;\n setStatus('listening');\n }\n } else {\n writeEntryText(entry, transcript);\n setStatus('listening');\n }\n }\n\n if (hasInterim) {\n const entry = liveEntryRef.current ?? createEntryAtAnchor();\n if (entry) {\n liveEntryRef.current = entry;\n writeEntryText(entry, interimText);\n }\n }\n },\n [store, createEntryAtAnchor, writeEntryText, revertEntryText],\n );\n\n // Detects the user manually moving the caret (click, arrow keys, Tab into\n // another block, ...) WHILE dictation is listening, and redirects the\n // next dictated phrase there instead of silently continuing to write at\n // the stale session anchor — this is the fix for a real bug: without it,\n // clicking into a different block mid-dictation had no effect on where\n // the next phrase landed, and `setCaretSync` would keep snapping the\n // visible caret back to the old spot on every subsequent word, making it\n // look like focus was being fought over.\n //\n // Only reacts when the resolved caret differs from `expectedCaretRef` —\n // this hook's own record of wherever IT most recently placed the caret —\n // so it never mistakes its own writes (which also move the real DOM\n // selection) for a manual move. `expectedCaretRef` being null (right\n // after a voice command moved focus, before the next phrase has\n // re-resolved yet) means there's no baseline to compare against yet, so\n // the listener stays passive rather than racing that in-flight move.\n useEffect(() => {\n if (!isListening) return undefined;\n const handleSelectionChange = () => {\n const expected = expectedCaretRef.current;\n if (!expected) return;\n const caret = resolveCollapsedCaret();\n if (!caret) return; // selection isn't a collapsed caret inside a run right now — nothing to redirect to\n if (caret.runId === expected.runId && caret.offset === expected.offset) return; // our own write\n if (liveEntryRef.current) {\n // Discard the not-yet-finalized guess left behind in the old block —\n // see removeEntryTextSilently's own doc comment for why leaving it\n // in place duplicated text instead.\n removeEntryTextSilently(liveEntryRef.current);\n liveEntryRef.current = null;\n }\n sessionAnchorRef.current = caret;\n expectedCaretRef.current = { runId: caret.runId, offset: caret.offset };\n };\n document.addEventListener('selectionchange', handleSelectionChange);\n return () => document.removeEventListener('selectionchange', handleSelectionChange);\n }, [isListening, removeEntryTextSilently]);\n\n const start = useCallback(() => {\n if (!isSupported || recognitionRef.current) return;\n setError(null);\n const initialCaret = resolveCollapsedCaret();\n sessionAnchorRef.current = initialCaret;\n expectedCaretRef.current = initialCaret\n ? { runId: initialCaret.runId, offset: initialCaret.offset }\n : null;\n liveEntryRef.current = null;\n committedCountRef.current = 0;\n let recognition;\n if (engine) {\n recognition = engine();\n } else {\n const Ctor = getSpeechRecognitionCtor();\n recognition = new Ctor();\n }\n recognition.continuous = true;\n recognition.interimResults = true;\n recognition.onresult = handleRecognitionResult;\n recognition.onstart = () => setStatus('listening');\n recognition.onspeechstart = () => setStatus('listening');\n recognition.onspeechend = () => setStatus('processing');\n recognition.onerror = (event) => {\n if (event.error === 'not-allowed' || event.error === 'service-not-allowed') {\n setPermissionState('denied');\n setError(PERMISSION_DENIED_MESSAGE);\n } else {\n setError(event.error ?? 'Voice typing stopped unexpectedly.');\n }\n };\n recognition.onend = () => {\n recognitionRef.current = null;\n sessionAnchorRef.current = null;\n expectedCaretRef.current = null;\n liveEntryRef.current = null;\n committedCountRef.current = 0;\n setIsListening(false);\n setStatus('idle');\n };\n recognitionRef.current = recognition;\n recognition.start();\n setIsListening(true);\n }, [isSupported, handleRecognitionResult, engine]);\n\n const stop = useCallback(() => {\n recognitionRef.current?.stop();\n }, []);\n\n useEffect(() => () => recognitionRef.current?.stop(), []);\n\n useEffect(() => {\n if (!shortcut || !isSupported) return undefined;\n const handleKeyDown = (event) => {\n const mod = event.metaKey || event.ctrlKey;\n if (!mod || !event.shiftKey || event.key.toLowerCase() !== 'm') return;\n event.preventDefault();\n if (recognitionRef.current) stop();\n else start();\n };\n document.addEventListener('keydown', handleKeyDown);\n return () => document.removeEventListener('keydown', handleKeyDown);\n }, [shortcut, isSupported, start, stop]);\n\n return { isSupported, isListening, status, permissionState, error, start, stop };\n}\n","import { useEffect, useState } from 'react';\nimport { Modal } from './Modal.jsx';\n\n/**\n * Ready-made \"microphone access is blocked\" dialog for useVoiceTyping —\n * mount it once anywhere alongside a mic button: `<VoicePermissionModal\n * voice={voice} />`. Driven by `voice.permissionState`, so there's no\n * separate isOpen prop to wire — it opens itself the instant permission is\n * detected as denied (whether that's from a failed start() attempt or the\n * opportunistic Permissions API check useVoiceTyping does on mount).\n *\n * Manually dismissible (Escape/backdrop click, via Modal's own built-in\n * handling) rather than forced to stay open until permission is fixed —\n * `dismissed` tracks that locally and resets the instant a *new* denial\n * comes in (voice.error changing is what \"a new denial just happened\"\n * means here, since start() always clears error first) so the dialog\n * reappears on the next real attempt even after being dismissed once,\n * rather than staying silently suppressed forever.\n */\nexport function VoicePermissionModal({ voice }) {\n const [dismissed, setDismissed] = useState(false);\n\n useEffect(() => {\n setDismissed(false);\n }, [voice?.error]);\n\n const isOpen = voice?.permissionState === 'denied' && !dismissed;\n\n return (\n <Modal isOpen={isOpen} onClose={() => setDismissed(true)} title=\"Microphone access blocked\">\n <p>\n {voice?.error ||\n \"Microphone access is blocked. Enable it in your browser's site settings, then try again.\"}\n </p>\n <div className=\"be-modal-actions\">\n <button type=\"button\" className=\"be-modal-cancel\" onClick={() => setDismissed(true)}>\n Dismiss\n </button>\n <button type=\"button\" className=\"be-modal-save\" onClick={() => voice?.start?.()}>\n Try again\n </button>\n </div>\n </Modal>\n );\n}\n","import { useEffect, useState } from 'react';\n\n/**\n * Tracks the live collapsed caret's bounding rect, recomputed via the same\n * event set `useFloatingToolbarTrigger` uses for its own (non-collapsed)\n * selection tracking — `document`'s `selectionchange`, plus `window`\n * scroll (capture phase)/resize, since a `position: fixed` box anchored to\n * a rect needs to stay correct across page scroll. `selectionchange` fires\n * even for *programmatic* `Selection.addRange()` calls, which is what\n * `setCaretSync` (focusRun.js) already does on every voice-dictation text\n * insertion — so this reliably follows the caret while dictating without\n * useVoiceTyping needing to know anything about this hook at all.\n *\n * Only active while `isActive` is true (pass `voice.isListening`) — a\n * no-op with zero listeners attached otherwise, so this costs nothing when\n * not dictating. Returns `null` whenever there's no collapsed caret inside\n * an editable run (`[data-run-id]`), rather than a stale/wrong position.\n */\nexport function useCaretRect(isActive) {\n const [rect, setRect] = useState(null);\n\n useEffect(() => {\n if (!isActive) {\n setRect(null);\n return undefined;\n }\n\n const recompute = () => {\n const selection = window.getSelection?.();\n if (!selection || selection.rangeCount === 0 || !selection.isCollapsed) {\n setRect(null);\n return;\n }\n const node = selection.anchorNode;\n const el = node?.nodeType === 1 ? node : node?.parentElement;\n if (!el?.closest?.('[data-run-id]')) {\n setRect(null);\n return;\n }\n\n // jsdom (this package's own test suite) implements\n // Element.prototype.getBoundingClientRect but not\n // Range.prototype.getBoundingClientRect — falling back to a zero\n // rect keeps this testable without a real layout engine, same\n // convention as useFloatingToolbarTrigger.\n const nextRect = selection.getRangeAt(0).getBoundingClientRect?.() ?? {\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n width: 0,\n height: 0,\n };\n setRect(nextRect);\n };\n\n recompute();\n document.addEventListener('selectionchange', recompute);\n window.addEventListener('scroll', recompute, true);\n window.addEventListener('resize', recompute);\n return () => {\n document.removeEventListener('selectionchange', recompute);\n window.removeEventListener('scroll', recompute, true);\n window.removeEventListener('resize', recompute);\n };\n }, [isActive]);\n\n return rect;\n}\n","import { useRef } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useCaretRect } from './useCaretRect.js';\nimport { useAutoAdjustedCenteredLeft } from './usePopoverEdgeClamp.js';\nimport { MicIcon } from './icons.jsx';\n\n/**\n * Ready-made \"voice typing is actively working\" indicator for\n * useVoiceTyping — mount it once anywhere: `<VoiceListeningIndicator\n * voice={voice} />`. A toolbar-only status label is easy to miss since\n * attention is on the text being dictated, not the toolbar, so this\n * renders a small pulsing badge anchored right above the live caret\n * instead — the same `getBoundingClientRect()`-off-a-Range positioning\n * `FloatingToolbar`/`LinkHoverCard` already use elsewhere in this\n * codebase (see useCaretRect.js), so it visibly follows the caret as\n * dictated text streams in and moves it.\n *\n * Renders nothing when not listening, or when there's currently no\n * resolvable caret position (useCaretRect returns null) — never shows a\n * stale/wrong-position badge.\n */\nexport function VoiceListeningIndicator({ voice }) {\n const isActive = Boolean(voice?.isListening);\n const rect = useCaretRect(isActive);\n const badgeRef = useRef(null);\n // Only the horizontal axis is clamped — the badge sits right above the\n // caret (`translateY(-100%)`), which stays within the viewport in\n // practice since the caret itself must be visible to type into; the\n // centered horizontal offset is the one that can push it off-screen near\n // the left/right edge of a narrow viewport.\n const centerLeft = useAutoAdjustedCenteredLeft(badgeRef, Boolean(isActive && rect), rect?.left);\n\n if (!isActive || !rect || centerLeft == null) return null;\n\n const isProcessing = voice.status === 'processing';\n\n return createPortal(\n <div\n ref={badgeRef}\n className={`be-voice-indicator${isProcessing ? ' be-voice-indicator-processing' : ''}`}\n contentEditable={false}\n style={{\n position: 'fixed',\n top: rect.top - 8,\n left: centerLeft,\n transform: 'translate(-50%, -100%)',\n }}\n >\n <span className=\"be-voice-indicator-dot\" />\n <MicIcon size={12} />\n <span className=\"be-voice-indicator-label\">\n {isProcessing ? 'Processing…' : 'Listening…'}\n </span>\n </div>,\n document.body,\n );\n}\n"],"names":["restoreSelectionAfterHistoryChange","store","selection","_a","focusRunAtOffset","targetBlockId","_b","id","focusBlockStart","COMMANDS","PHRASE_TO_ACTION","phrases","action","phrase","normalizeUtterance","text","matchVoiceCommand","listVoiceCommands","description","applyOps","ops","op","getOwnRunIds","block","applyVoiceAction","blockId","insertSiblingAfterAndFocus","createTextLeafBlock","runIds","convertBlockType","focusRunStart","getSpeechRecognitionCtor","PERMISSION_DENIED_MESSAGE","useVoiceTyping","shortcut","engine","useEditorStore","isListening","setIsListening","useState","status","setStatus","permissionState","setPermissionState","error","setError","recognitionRef","useRef","sessionAnchorRef","expectedCaretRef","liveEntryRef","committedCountRef","isSupported","useEffect","permissionStatus","cancelled","result","createEntryAtAnchor","useCallback","caret","resolveCollapsedCaret","writeEntryText","entry","run","value","before","beforeChar","insertedText","after","flushSync","updateRun","nextOffset","setCaretSync","revertEntryText","restored","removeEntryTextSilently","handleRecognitionResult","event","interimText","hasInterim","start","i","transcript","command","handleSelectionChange","expected","initialCaret","recognition","Ctor","stop","handleKeyDown","VoicePermissionModal","voice","dismissed","setDismissed","isOpen","jsxs","Modal","jsx","useCaretRect","isActive","rect","setRect","recompute","node","el","nextRect","_d","_c","VoiceListeningIndicator","badgeRef","centerLeft","useAutoAdjustedCenteredLeft","isProcessing","createPortal","MicIcon"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";class n{constructor(e){this.peerId=e,this.wallTime=0,this.counter=0}tick(){const e=Date.now();return e>this.wallTime?(this.wallTime=e,this.counter=0):this.counter+=1,{wallTime:this.wallTime,counter:this.counter,peerId:this.peerId}}receive(e){const t=Date.now(),s=Math.max(t,this.wallTime,e.wallTime);return s===this.wallTime&&s===e.wallTime?this.counter=Math.max(this.counter,e.counter)+1:s===this.wallTime?this.counter+=1:s===e.wallTime?this.counter=e.counter+1:this.counter=0,this.wallTime=s,{wallTime:this.wallTime,counter:this.counter,peerId:this.peerId}}static compare(e,t){return e.wallTime!==t.wallTime?e.wallTime-t.wallTime:e.counter!==t.counter?e.counter-t.counter:e.peerId===t.peerId?0:e.peerId<t.peerId?-1:1}}function d(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():`peer-${Date.now()}-${Math.random().toString(36).slice(2)}`}class c{constructor(){this.slots=new Map}static fromArray(e,{peerId:t="legacy"}={}){const s=new c;let o=null;for(const r of e)s.slots.set(r,{id:r,originId:o,peerId:t,clock:{wallTime:0,counter:0,peerId:t},deleted:!1}),o=r;return s}has(e){return this.slots.has(e)}getSlot(e){return this.slots.get(e)}isDeleted(e){const t=this.slots.get(e);return!t||t.deleted}insert(e,t,s,o){this.slots.has(e)||this.slots.set(e,{id:e,originId:t,peerId:o,clock:s,deleted:!1})}delete(e,t){const s=this.slots.get(e);s&&(s.deleted=!0,t&&(s.deletedClock=t))}restore(e){const t=this.slots.get(e);t&&(t.deleted=!1)}move(e,t,s,o){if(t===e)return;const r=this.slots.get(e);if(!r)return;const i=r.originId;for(const u of this.slots.values())u.originId===e&&(u.originId=i);r.originId=t,r.peerId=o,r.clock=s,r.deleted=!1}merge(e){for(const t of e){const s=this.slots.get(t.id);s?t.deleted&&!s.deleted&&(s.deleted=!0):this.slots.set(t.id,{...t})}}toSlotArray(){return Array.from(this.slots.values())}tombstoneCount(){let e=0;for(const t of this.slots.values())t.deleted&&(e+=1);return e}pruneTombstones(e){let t=0;for(const[s,o]of this.slots){if(!o.deleted)continue;const r=o.deletedClock??o.clock;if(!(n.compare(r,e)>=0)){for(const i of this.slots.values())i.originId===s&&(i.originId=o.originId);this.slots.delete(s),t+=1}}return t}toArray(){const e=new Map;for(const o of this.slots.values()){const r=e.get(o.originId);r?r.push(o):e.set(o.originId,[o])}for(const o of e.values())o.sort((r,i)=>n.compare(i.clock,r.clock));const t=[],s=[...e.get(null)??[]].reverse();for(;s.length>0;){const o=s.pop();o.deleted||t.push(o.id);const r=e.get(o.id);if(r)for(let i=r.length-1;i>=0;i-=1)s.push(r[i])}return t}}class l{constructor(){this.clocks=new Map}static compositeKey(e,t){return`${e}:${t}`}get(e,t){return this.clocks.get(l.compositeKey(e,t))??null}recordLocal(e,t,s){this.clocks.set(l.compositeKey(e,t),s)}shouldApplyRemote(e,t,s){const o=this.get(e,t);return o?n.compare(s,o)>0:!0}recordRemote(e,t,s){this.clocks.set(l.compositeKey(e,t),s)}}exports.FieldClockRegistry=l;exports.HLC=n;exports.ListCrdtState=c;exports.genPeerId=d;
|
|
2
|
+
//# sourceMappingURL=fieldRegistry-BoYyW3GH.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fieldRegistry-BoYyW3GH.cjs","sources":["../../src/crdt/clock.js","../../src/crdt/listCrdt.js","../../src/crdt/fieldRegistry.js"],"sourcesContent":["/**\n * Hybrid Logical Clock — a (wallTime, counter, peerId) timestamp used to\n * totally order concurrent edits across peers without a central server.\n * wallTime keeps timestamps meaningful as real time (e.g. \"edited 2 min\n * ago\"); counter breaks ties when multiple local events share a wallTime;\n * peerId breaks ties deterministically when both wallTime and counter\n * match across peers.\n */\nexport class HLC {\n constructor(peerId) {\n this.peerId = peerId;\n this.wallTime = 0;\n this.counter = 0;\n }\n\n /** Local event (a user's own edit). Returns the stamped timestamp. */\n tick() {\n const now = Date.now();\n if (now > this.wallTime) {\n this.wallTime = now;\n this.counter = 0;\n } else {\n this.counter += 1;\n }\n return { wallTime: this.wallTime, counter: this.counter, peerId: this.peerId };\n }\n\n /**\n * Merges in a timestamp observed from a remote peer, advancing this\n * clock so subsequent local ticks are guaranteed to sort after\n * anything just received.\n */\n receive(remote) {\n const now = Date.now();\n const maxWall = Math.max(now, this.wallTime, remote.wallTime);\n if (maxWall === this.wallTime && maxWall === remote.wallTime) {\n this.counter = Math.max(this.counter, remote.counter) + 1;\n } else if (maxWall === this.wallTime) {\n this.counter += 1;\n } else if (maxWall === remote.wallTime) {\n this.counter = remote.counter + 1;\n } else {\n this.counter = 0;\n }\n this.wallTime = maxWall;\n return { wallTime: this.wallTime, counter: this.counter, peerId: this.peerId };\n }\n\n /** Total order: wallTime, then counter, then peerId. */\n static compare(a, b) {\n if (a.wallTime !== b.wallTime) return a.wallTime - b.wallTime;\n if (a.counter !== b.counter) return a.counter - b.counter;\n if (a.peerId === b.peerId) return 0;\n return a.peerId < b.peerId ? -1 : 1;\n }\n}\n\nexport function genPeerId() {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n return `peer-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n","import { HLC } from './clock.js';\n\n/**\n * Replicated ordered list, keyed by externally-owned stable ids (Noteloom's\n * existing block/run ids — this module never generates ids, only orders\n * and tombstones the ones it's given). Each item's position is defined\n * relative to a neighbor id (\"insert after X\"), not an array index, so\n * concurrent inserts/deletes at different local positions merge\n * deterministically regardless of what order the operations are applied\n * in on a given peer.\n *\n * Concurrent inserts anchored to the exact same origin are ordered\n * newest-first (by clock, then peerId) so a live edit lands as close to\n * its anchor as possible. Sequential inserts from one peer/batch (each\n * anchored to the id it just inserted, not re-anchored to the original\n * origin — the natural way typing and multi-block paste already work)\n * are never split apart by a concurrent peer's insert at the same\n * origin. A concurrent *non-chained* multi-item insert at the same\n * anchor by two peers can still interleave with this simpler\n * (RGA-style) scheme; convergence still holds, only interleave-avoidance\n * is partial rather than the full Fugue tree guarantee.\n */\nexport class ListCrdtState {\n constructor() {\n this.slots = new Map();\n }\n\n /** Builds CRDT state from an existing plain-array order (e.g. loading a v1 document with no CRDT history). */\n static fromArray(ids, { peerId = 'legacy' } = {}) {\n const state = new ListCrdtState();\n let prev = null;\n for (const id of ids) {\n state.slots.set(id, {\n id,\n originId: prev,\n peerId,\n clock: { wallTime: 0, counter: 0, peerId },\n deleted: false,\n });\n prev = id;\n }\n return state;\n }\n\n has(id) {\n return this.slots.has(id);\n }\n\n /** The raw slot for `id` ({id, originId, peerId, clock, deleted}), or undefined — used to build a wire envelope for a change that was just made. */\n getSlot(id) {\n return this.slots.get(id);\n }\n\n isDeleted(id) {\n const slot = this.slots.get(id);\n return !slot || slot.deleted;\n }\n\n /**\n * Inserts `id` immediately after `afterId` (null = at the very start).\n * Idempotent: re-inserting an id that already has a slot is a no-op,\n * which guards against duplicate delivery over an unreliable transport.\n */\n insert(id, afterId, clock, peerId) {\n if (this.slots.has(id)) return;\n this.slots.set(id, { id, originId: afterId, peerId, clock, deleted: false });\n }\n\n /**\n * `clock` records WHEN the delete happened (used by pruneTombstones to\n * judge tombstone age) — deliberately a separate value from whatever\n * clock the slot already had from insertion/move, which recorded a\n * different thing (when its POSITION was last established). Without\n * this, an old-but-just-deleted slot would look ancient from the\n * moment it's deleted (inheriting its original insertion time, however\n * long ago that was) and become immediately GC-eligible.\n */\n delete(id, clock) {\n const slot = this.slots.get(id);\n if (!slot) return;\n slot.deleted = true;\n if (clock) slot.deletedClock = clock;\n }\n\n /** Undoes a local delete in place, preserving the slot's original position. */\n restore(id) {\n const slot = this.slots.get(id);\n if (!slot) return;\n slot.deleted = false;\n }\n\n /**\n * Repositions an existing slot to a new anchor in place, rather than\n * tombstoning it and creating a new one — there is only ever one slot\n * per id in a given list, so a same-list reorder (or moving a block\n * back to a parent it was in before) must reuse it. Local-only for now:\n * concurrent moves of the same id by two peers have no defined merge\n * rule yet (last local move simply wins), since there's no remote path\n * exercising this until the transport layer exists.\n *\n * Splices the slot out of the chain BEFORE re-anchoring it: anything\n * currently anchored directly after `id` gets reparented to `id`'s old\n * origin first. Without this, moving a node to an anchor that's\n * currently one of its own (transitive) successors creates a cycle —\n * e.g. a->b->c, \"move a to after c\" naively sets a.originId='c', but\n * c.originId is (transitively) 'a', so the walk from the list start can\n * no longer reach any of them. Splicing `id` out first means whatever\n * anchor is chosen can never depend on `id` still being where it was.\n */\n move(id, afterId, clock, peerId) {\n if (afterId === id) return;\n const slot = this.slots.get(id);\n if (!slot) return;\n const oldOriginId = slot.originId;\n for (const other of this.slots.values()) {\n if (other.originId === id) other.originId = oldOriginId;\n }\n slot.originId = afterId;\n slot.peerId = peerId;\n slot.clock = clock;\n slot.deleted = false;\n }\n\n /**\n * Merges another peer's slot set into this one. Safe to call with\n * slots in any order, any number of times — idempotent, commutative,\n * and associative — which is what makes convergence hold regardless\n * of network delivery order.\n */\n merge(remoteSlots) {\n for (const remote of remoteSlots) {\n const local = this.slots.get(remote.id);\n if (!local) {\n this.slots.set(remote.id, { ...remote });\n } else if (remote.deleted && !local.deleted) {\n local.deleted = true;\n }\n // originId/peerId/clock never change after a slot is created —\n // a slot's structural identity is fixed at insert time; `deleted`\n // is the only field that can change, and only monotonically\n // (existence-wins: once deleted, always deleted).\n }\n }\n\n /** Every slot as a plain array, for persistence/transport. */\n toSlotArray() {\n return Array.from(this.slots.values());\n }\n\n /** How many tombstoned (deleted but still retained) slots this list is currently carrying — for deciding whether GC is worth running, or just observing growth. */\n tombstoneCount() {\n let count = 0;\n for (const slot of this.slots.values()) {\n if (slot.deleted) count += 1;\n }\n return count;\n }\n\n /**\n * Permanently removes tombstoned slots whose delete happened before\n * `beforeClock` — the actual fix for the \"deleted content is never\n * garbage-collected\" limitation. Splices each removed slot out of the\n * chain first (children reparented to its own origin, same technique\n * as `move()`) so removal never orphans a slot anchored to it. Returns\n * the number of slots actually removed.\n *\n * Safety of this is specifically tied to how this package's transport\n * layer reconnects: `CollabSession` resyncs a reconnecting peer via a\n * full document snapshot (see syncResponse in CollabSession.js), never\n * by replaying a log of historical ops — so a peer that was offline\n * longer than any reasonable GC threshold never needs an old tombstone\n * to resolve a stale anchor; it just adopts the current state directly.\n * The one theoretical residual risk is a single already-open connection\n * somehow stalling for exactly as long as the GC threshold and then\n * delivering an old queued message afterward — vanishingly unlikely for\n * a live, ordered, reliable data channel (which disconnects long before\n * that under any real network interruption), but not mathematically\n * impossible, which is why this is opt-in rather than automatic.\n */\n pruneTombstones(beforeClock) {\n let removed = 0;\n for (const [id, slot] of this.slots) {\n if (!slot.deleted) continue;\n // deletedClock (when it was actually deleted) is what age means\n // here -- falling back to the slot's own clock (when its position\n // was established) only for a slot deleted before this field\n // existed, which otherwise has no recorded deletion time at all.\n const age = slot.deletedClock ?? slot.clock;\n if (HLC.compare(age, beforeClock) >= 0) continue; // not old enough yet\n for (const other of this.slots.values()) {\n if (other.originId === id) other.originId = slot.originId;\n }\n this.slots.delete(id);\n removed += 1;\n }\n return removed;\n }\n\n /**\n * Materializes the currently-visible order as a plain array of ids\n * (tombstoned ids excluded) — this is what gets projected onto\n * Block.contentIds after every structural change.\n */\n toArray() {\n const childrenByOrigin = new Map();\n for (const slot of this.slots.values()) {\n const bucket = childrenByOrigin.get(slot.originId);\n if (bucket) bucket.push(slot);\n else childrenByOrigin.set(slot.originId, [slot]);\n }\n for (const bucket of childrenByOrigin.values()) {\n bucket.sort((a, b) => HLC.compare(b.clock, a.clock));\n }\n\n const result = [];\n // Explicit-stack depth-first walk — a naive recursive walk's depth\n // equals the list length on a long simple chain (the common case for\n // typed/sequential content), which risks a stack overflow on large\n // documents.\n const stack = [...(childrenByOrigin.get(null) ?? [])].reverse();\n while (stack.length > 0) {\n const slot = stack.pop();\n if (!slot.deleted) result.push(slot.id);\n const children = childrenByOrigin.get(slot.id);\n if (children) {\n for (let i = children.length - 1; i >= 0; i -= 1) stack.push(children[i]);\n }\n }\n return result;\n }\n}\n","import { HLC } from './clock.js';\n\n/**\n * Tracks the logical-clock high-water mark for individual mutable fields\n * (a block's type, one prop key, a run's value, ...) so that when a\n * remote peer's write arrives it's only applied if it's actually newer\n * than whatever's already there — last-write-wins, scoped per field\n * rather than per whole block/run, so unrelated concurrent edits (peer A\n * changes the type, peer B changes a prop) both survive.\n */\nexport class FieldClockRegistry {\n constructor() {\n this.clocks = new Map();\n }\n\n static compositeKey(id, field) {\n return `${id}:${field}`;\n }\n\n get(id, field) {\n return this.clocks.get(FieldClockRegistry.compositeKey(id, field)) ?? null;\n }\n\n /** Local write: always wins outright — it just happened, nothing to compare against. */\n recordLocal(id, field, clock) {\n this.clocks.set(FieldClockRegistry.compositeKey(id, field), clock);\n }\n\n /**\n * Whether a remote write to this field should be applied. Callers must\n * check this before mutating so a stale remote write can be discarded\n * without ever touching local state.\n */\n shouldApplyRemote(id, field, remoteClock) {\n const current = this.get(id, field);\n if (!current) return true;\n return HLC.compare(remoteClock, current) > 0;\n }\n\n recordRemote(id, field, remoteClock) {\n this.clocks.set(FieldClockRegistry.compositeKey(id, field), remoteClock);\n }\n}\n"],"names":["HLC","peerId","now","remote","maxWall","a","b","genPeerId","ListCrdtState","ids","state","prev","id","slot","afterId","clock","oldOriginId","other","remoteSlots","local","count","beforeClock","removed","age","childrenByOrigin","bucket","result","stack","children","FieldClockRegistry","field","remoteClock","current"],"mappings":"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"}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
class n {
|
|
2
|
+
constructor(e) {
|
|
3
|
+
this.peerId = e, this.wallTime = 0, this.counter = 0;
|
|
4
|
+
}
|
|
5
|
+
/** Local event (a user's own edit). Returns the stamped timestamp. */
|
|
6
|
+
tick() {
|
|
7
|
+
const e = Date.now();
|
|
8
|
+
return e > this.wallTime ? (this.wallTime = e, this.counter = 0) : this.counter += 1, { wallTime: this.wallTime, counter: this.counter, peerId: this.peerId };
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Merges in a timestamp observed from a remote peer, advancing this
|
|
12
|
+
* clock so subsequent local ticks are guaranteed to sort after
|
|
13
|
+
* anything just received.
|
|
14
|
+
*/
|
|
15
|
+
receive(e) {
|
|
16
|
+
const t = Date.now(), s = Math.max(t, this.wallTime, e.wallTime);
|
|
17
|
+
return s === this.wallTime && s === e.wallTime ? this.counter = Math.max(this.counter, e.counter) + 1 : s === this.wallTime ? this.counter += 1 : s === e.wallTime ? this.counter = e.counter + 1 : this.counter = 0, this.wallTime = s, { wallTime: this.wallTime, counter: this.counter, peerId: this.peerId };
|
|
18
|
+
}
|
|
19
|
+
/** Total order: wallTime, then counter, then peerId. */
|
|
20
|
+
static compare(e, t) {
|
|
21
|
+
return e.wallTime !== t.wallTime ? e.wallTime - t.wallTime : e.counter !== t.counter ? e.counter - t.counter : e.peerId === t.peerId ? 0 : e.peerId < t.peerId ? -1 : 1;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function d() {
|
|
25
|
+
return typeof crypto < "u" && typeof crypto.randomUUID == "function" ? crypto.randomUUID() : `peer-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
26
|
+
}
|
|
27
|
+
class u {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.slots = /* @__PURE__ */ new Map();
|
|
30
|
+
}
|
|
31
|
+
/** Builds CRDT state from an existing plain-array order (e.g. loading a v1 document with no CRDT history). */
|
|
32
|
+
static fromArray(e, { peerId: t = "legacy" } = {}) {
|
|
33
|
+
const s = new u();
|
|
34
|
+
let o = null;
|
|
35
|
+
for (const r of e)
|
|
36
|
+
s.slots.set(r, {
|
|
37
|
+
id: r,
|
|
38
|
+
originId: o,
|
|
39
|
+
peerId: t,
|
|
40
|
+
clock: { wallTime: 0, counter: 0, peerId: t },
|
|
41
|
+
deleted: !1
|
|
42
|
+
}), o = r;
|
|
43
|
+
return s;
|
|
44
|
+
}
|
|
45
|
+
has(e) {
|
|
46
|
+
return this.slots.has(e);
|
|
47
|
+
}
|
|
48
|
+
/** The raw slot for `id` ({id, originId, peerId, clock, deleted}), or undefined — used to build a wire envelope for a change that was just made. */
|
|
49
|
+
getSlot(e) {
|
|
50
|
+
return this.slots.get(e);
|
|
51
|
+
}
|
|
52
|
+
isDeleted(e) {
|
|
53
|
+
const t = this.slots.get(e);
|
|
54
|
+
return !t || t.deleted;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Inserts `id` immediately after `afterId` (null = at the very start).
|
|
58
|
+
* Idempotent: re-inserting an id that already has a slot is a no-op,
|
|
59
|
+
* which guards against duplicate delivery over an unreliable transport.
|
|
60
|
+
*/
|
|
61
|
+
insert(e, t, s, o) {
|
|
62
|
+
this.slots.has(e) || this.slots.set(e, { id: e, originId: t, peerId: o, clock: s, deleted: !1 });
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* `clock` records WHEN the delete happened (used by pruneTombstones to
|
|
66
|
+
* judge tombstone age) — deliberately a separate value from whatever
|
|
67
|
+
* clock the slot already had from insertion/move, which recorded a
|
|
68
|
+
* different thing (when its POSITION was last established). Without
|
|
69
|
+
* this, an old-but-just-deleted slot would look ancient from the
|
|
70
|
+
* moment it's deleted (inheriting its original insertion time, however
|
|
71
|
+
* long ago that was) and become immediately GC-eligible.
|
|
72
|
+
*/
|
|
73
|
+
delete(e, t) {
|
|
74
|
+
const s = this.slots.get(e);
|
|
75
|
+
s && (s.deleted = !0, t && (s.deletedClock = t));
|
|
76
|
+
}
|
|
77
|
+
/** Undoes a local delete in place, preserving the slot's original position. */
|
|
78
|
+
restore(e) {
|
|
79
|
+
const t = this.slots.get(e);
|
|
80
|
+
t && (t.deleted = !1);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Repositions an existing slot to a new anchor in place, rather than
|
|
84
|
+
* tombstoning it and creating a new one — there is only ever one slot
|
|
85
|
+
* per id in a given list, so a same-list reorder (or moving a block
|
|
86
|
+
* back to a parent it was in before) must reuse it. Local-only for now:
|
|
87
|
+
* concurrent moves of the same id by two peers have no defined merge
|
|
88
|
+
* rule yet (last local move simply wins), since there's no remote path
|
|
89
|
+
* exercising this until the transport layer exists.
|
|
90
|
+
*
|
|
91
|
+
* Splices the slot out of the chain BEFORE re-anchoring it: anything
|
|
92
|
+
* currently anchored directly after `id` gets reparented to `id`'s old
|
|
93
|
+
* origin first. Without this, moving a node to an anchor that's
|
|
94
|
+
* currently one of its own (transitive) successors creates a cycle —
|
|
95
|
+
* e.g. a->b->c, "move a to after c" naively sets a.originId='c', but
|
|
96
|
+
* c.originId is (transitively) 'a', so the walk from the list start can
|
|
97
|
+
* no longer reach any of them. Splicing `id` out first means whatever
|
|
98
|
+
* anchor is chosen can never depend on `id` still being where it was.
|
|
99
|
+
*/
|
|
100
|
+
move(e, t, s, o) {
|
|
101
|
+
if (t === e) return;
|
|
102
|
+
const r = this.slots.get(e);
|
|
103
|
+
if (!r) return;
|
|
104
|
+
const i = r.originId;
|
|
105
|
+
for (const c of this.slots.values())
|
|
106
|
+
c.originId === e && (c.originId = i);
|
|
107
|
+
r.originId = t, r.peerId = o, r.clock = s, r.deleted = !1;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Merges another peer's slot set into this one. Safe to call with
|
|
111
|
+
* slots in any order, any number of times — idempotent, commutative,
|
|
112
|
+
* and associative — which is what makes convergence hold regardless
|
|
113
|
+
* of network delivery order.
|
|
114
|
+
*/
|
|
115
|
+
merge(e) {
|
|
116
|
+
for (const t of e) {
|
|
117
|
+
const s = this.slots.get(t.id);
|
|
118
|
+
s ? t.deleted && !s.deleted && (s.deleted = !0) : this.slots.set(t.id, { ...t });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Every slot as a plain array, for persistence/transport. */
|
|
122
|
+
toSlotArray() {
|
|
123
|
+
return Array.from(this.slots.values());
|
|
124
|
+
}
|
|
125
|
+
/** How many tombstoned (deleted but still retained) slots this list is currently carrying — for deciding whether GC is worth running, or just observing growth. */
|
|
126
|
+
tombstoneCount() {
|
|
127
|
+
let e = 0;
|
|
128
|
+
for (const t of this.slots.values())
|
|
129
|
+
t.deleted && (e += 1);
|
|
130
|
+
return e;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Permanently removes tombstoned slots whose delete happened before
|
|
134
|
+
* `beforeClock` — the actual fix for the "deleted content is never
|
|
135
|
+
* garbage-collected" limitation. Splices each removed slot out of the
|
|
136
|
+
* chain first (children reparented to its own origin, same technique
|
|
137
|
+
* as `move()`) so removal never orphans a slot anchored to it. Returns
|
|
138
|
+
* the number of slots actually removed.
|
|
139
|
+
*
|
|
140
|
+
* Safety of this is specifically tied to how this package's transport
|
|
141
|
+
* layer reconnects: `CollabSession` resyncs a reconnecting peer via a
|
|
142
|
+
* full document snapshot (see syncResponse in CollabSession.js), never
|
|
143
|
+
* by replaying a log of historical ops — so a peer that was offline
|
|
144
|
+
* longer than any reasonable GC threshold never needs an old tombstone
|
|
145
|
+
* to resolve a stale anchor; it just adopts the current state directly.
|
|
146
|
+
* The one theoretical residual risk is a single already-open connection
|
|
147
|
+
* somehow stalling for exactly as long as the GC threshold and then
|
|
148
|
+
* delivering an old queued message afterward — vanishingly unlikely for
|
|
149
|
+
* a live, ordered, reliable data channel (which disconnects long before
|
|
150
|
+
* that under any real network interruption), but not mathematically
|
|
151
|
+
* impossible, which is why this is opt-in rather than automatic.
|
|
152
|
+
*/
|
|
153
|
+
pruneTombstones(e) {
|
|
154
|
+
let t = 0;
|
|
155
|
+
for (const [s, o] of this.slots) {
|
|
156
|
+
if (!o.deleted) continue;
|
|
157
|
+
const r = o.deletedClock ?? o.clock;
|
|
158
|
+
if (!(n.compare(r, e) >= 0)) {
|
|
159
|
+
for (const i of this.slots.values())
|
|
160
|
+
i.originId === s && (i.originId = o.originId);
|
|
161
|
+
this.slots.delete(s), t += 1;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return t;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Materializes the currently-visible order as a plain array of ids
|
|
168
|
+
* (tombstoned ids excluded) — this is what gets projected onto
|
|
169
|
+
* Block.contentIds after every structural change.
|
|
170
|
+
*/
|
|
171
|
+
toArray() {
|
|
172
|
+
const e = /* @__PURE__ */ new Map();
|
|
173
|
+
for (const o of this.slots.values()) {
|
|
174
|
+
const r = e.get(o.originId);
|
|
175
|
+
r ? r.push(o) : e.set(o.originId, [o]);
|
|
176
|
+
}
|
|
177
|
+
for (const o of e.values())
|
|
178
|
+
o.sort((r, i) => n.compare(i.clock, r.clock));
|
|
179
|
+
const t = [], s = [...e.get(null) ?? []].reverse();
|
|
180
|
+
for (; s.length > 0; ) {
|
|
181
|
+
const o = s.pop();
|
|
182
|
+
o.deleted || t.push(o.id);
|
|
183
|
+
const r = e.get(o.id);
|
|
184
|
+
if (r)
|
|
185
|
+
for (let i = r.length - 1; i >= 0; i -= 1) s.push(r[i]);
|
|
186
|
+
}
|
|
187
|
+
return t;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
class l {
|
|
191
|
+
constructor() {
|
|
192
|
+
this.clocks = /* @__PURE__ */ new Map();
|
|
193
|
+
}
|
|
194
|
+
static compositeKey(e, t) {
|
|
195
|
+
return `${e}:${t}`;
|
|
196
|
+
}
|
|
197
|
+
get(e, t) {
|
|
198
|
+
return this.clocks.get(l.compositeKey(e, t)) ?? null;
|
|
199
|
+
}
|
|
200
|
+
/** Local write: always wins outright — it just happened, nothing to compare against. */
|
|
201
|
+
recordLocal(e, t, s) {
|
|
202
|
+
this.clocks.set(l.compositeKey(e, t), s);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Whether a remote write to this field should be applied. Callers must
|
|
206
|
+
* check this before mutating so a stale remote write can be discarded
|
|
207
|
+
* without ever touching local state.
|
|
208
|
+
*/
|
|
209
|
+
shouldApplyRemote(e, t, s) {
|
|
210
|
+
const o = this.get(e, t);
|
|
211
|
+
return o ? n.compare(s, o) > 0 : !0;
|
|
212
|
+
}
|
|
213
|
+
recordRemote(e, t, s) {
|
|
214
|
+
this.clocks.set(l.compositeKey(e, t), s);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
export {
|
|
218
|
+
l as F,
|
|
219
|
+
n as H,
|
|
220
|
+
u as L,
|
|
221
|
+
d as g
|
|
222
|
+
};
|
|
223
|
+
//# sourceMappingURL=fieldRegistry-PjJWXFoB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fieldRegistry-PjJWXFoB.js","sources":["../../src/crdt/clock.js","../../src/crdt/listCrdt.js","../../src/crdt/fieldRegistry.js"],"sourcesContent":["/**\n * Hybrid Logical Clock — a (wallTime, counter, peerId) timestamp used to\n * totally order concurrent edits across peers without a central server.\n * wallTime keeps timestamps meaningful as real time (e.g. \"edited 2 min\n * ago\"); counter breaks ties when multiple local events share a wallTime;\n * peerId breaks ties deterministically when both wallTime and counter\n * match across peers.\n */\nexport class HLC {\n constructor(peerId) {\n this.peerId = peerId;\n this.wallTime = 0;\n this.counter = 0;\n }\n\n /** Local event (a user's own edit). Returns the stamped timestamp. */\n tick() {\n const now = Date.now();\n if (now > this.wallTime) {\n this.wallTime = now;\n this.counter = 0;\n } else {\n this.counter += 1;\n }\n return { wallTime: this.wallTime, counter: this.counter, peerId: this.peerId };\n }\n\n /**\n * Merges in a timestamp observed from a remote peer, advancing this\n * clock so subsequent local ticks are guaranteed to sort after\n * anything just received.\n */\n receive(remote) {\n const now = Date.now();\n const maxWall = Math.max(now, this.wallTime, remote.wallTime);\n if (maxWall === this.wallTime && maxWall === remote.wallTime) {\n this.counter = Math.max(this.counter, remote.counter) + 1;\n } else if (maxWall === this.wallTime) {\n this.counter += 1;\n } else if (maxWall === remote.wallTime) {\n this.counter = remote.counter + 1;\n } else {\n this.counter = 0;\n }\n this.wallTime = maxWall;\n return { wallTime: this.wallTime, counter: this.counter, peerId: this.peerId };\n }\n\n /** Total order: wallTime, then counter, then peerId. */\n static compare(a, b) {\n if (a.wallTime !== b.wallTime) return a.wallTime - b.wallTime;\n if (a.counter !== b.counter) return a.counter - b.counter;\n if (a.peerId === b.peerId) return 0;\n return a.peerId < b.peerId ? -1 : 1;\n }\n}\n\nexport function genPeerId() {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n return `peer-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n","import { HLC } from './clock.js';\n\n/**\n * Replicated ordered list, keyed by externally-owned stable ids (Noteloom's\n * existing block/run ids — this module never generates ids, only orders\n * and tombstones the ones it's given). Each item's position is defined\n * relative to a neighbor id (\"insert after X\"), not an array index, so\n * concurrent inserts/deletes at different local positions merge\n * deterministically regardless of what order the operations are applied\n * in on a given peer.\n *\n * Concurrent inserts anchored to the exact same origin are ordered\n * newest-first (by clock, then peerId) so a live edit lands as close to\n * its anchor as possible. Sequential inserts from one peer/batch (each\n * anchored to the id it just inserted, not re-anchored to the original\n * origin — the natural way typing and multi-block paste already work)\n * are never split apart by a concurrent peer's insert at the same\n * origin. A concurrent *non-chained* multi-item insert at the same\n * anchor by two peers can still interleave with this simpler\n * (RGA-style) scheme; convergence still holds, only interleave-avoidance\n * is partial rather than the full Fugue tree guarantee.\n */\nexport class ListCrdtState {\n constructor() {\n this.slots = new Map();\n }\n\n /** Builds CRDT state from an existing plain-array order (e.g. loading a v1 document with no CRDT history). */\n static fromArray(ids, { peerId = 'legacy' } = {}) {\n const state = new ListCrdtState();\n let prev = null;\n for (const id of ids) {\n state.slots.set(id, {\n id,\n originId: prev,\n peerId,\n clock: { wallTime: 0, counter: 0, peerId },\n deleted: false,\n });\n prev = id;\n }\n return state;\n }\n\n has(id) {\n return this.slots.has(id);\n }\n\n /** The raw slot for `id` ({id, originId, peerId, clock, deleted}), or undefined — used to build a wire envelope for a change that was just made. */\n getSlot(id) {\n return this.slots.get(id);\n }\n\n isDeleted(id) {\n const slot = this.slots.get(id);\n return !slot || slot.deleted;\n }\n\n /**\n * Inserts `id` immediately after `afterId` (null = at the very start).\n * Idempotent: re-inserting an id that already has a slot is a no-op,\n * which guards against duplicate delivery over an unreliable transport.\n */\n insert(id, afterId, clock, peerId) {\n if (this.slots.has(id)) return;\n this.slots.set(id, { id, originId: afterId, peerId, clock, deleted: false });\n }\n\n /**\n * `clock` records WHEN the delete happened (used by pruneTombstones to\n * judge tombstone age) — deliberately a separate value from whatever\n * clock the slot already had from insertion/move, which recorded a\n * different thing (when its POSITION was last established). Without\n * this, an old-but-just-deleted slot would look ancient from the\n * moment it's deleted (inheriting its original insertion time, however\n * long ago that was) and become immediately GC-eligible.\n */\n delete(id, clock) {\n const slot = this.slots.get(id);\n if (!slot) return;\n slot.deleted = true;\n if (clock) slot.deletedClock = clock;\n }\n\n /** Undoes a local delete in place, preserving the slot's original position. */\n restore(id) {\n const slot = this.slots.get(id);\n if (!slot) return;\n slot.deleted = false;\n }\n\n /**\n * Repositions an existing slot to a new anchor in place, rather than\n * tombstoning it and creating a new one — there is only ever one slot\n * per id in a given list, so a same-list reorder (or moving a block\n * back to a parent it was in before) must reuse it. Local-only for now:\n * concurrent moves of the same id by two peers have no defined merge\n * rule yet (last local move simply wins), since there's no remote path\n * exercising this until the transport layer exists.\n *\n * Splices the slot out of the chain BEFORE re-anchoring it: anything\n * currently anchored directly after `id` gets reparented to `id`'s old\n * origin first. Without this, moving a node to an anchor that's\n * currently one of its own (transitive) successors creates a cycle —\n * e.g. a->b->c, \"move a to after c\" naively sets a.originId='c', but\n * c.originId is (transitively) 'a', so the walk from the list start can\n * no longer reach any of them. Splicing `id` out first means whatever\n * anchor is chosen can never depend on `id` still being where it was.\n */\n move(id, afterId, clock, peerId) {\n if (afterId === id) return;\n const slot = this.slots.get(id);\n if (!slot) return;\n const oldOriginId = slot.originId;\n for (const other of this.slots.values()) {\n if (other.originId === id) other.originId = oldOriginId;\n }\n slot.originId = afterId;\n slot.peerId = peerId;\n slot.clock = clock;\n slot.deleted = false;\n }\n\n /**\n * Merges another peer's slot set into this one. Safe to call with\n * slots in any order, any number of times — idempotent, commutative,\n * and associative — which is what makes convergence hold regardless\n * of network delivery order.\n */\n merge(remoteSlots) {\n for (const remote of remoteSlots) {\n const local = this.slots.get(remote.id);\n if (!local) {\n this.slots.set(remote.id, { ...remote });\n } else if (remote.deleted && !local.deleted) {\n local.deleted = true;\n }\n // originId/peerId/clock never change after a slot is created —\n // a slot's structural identity is fixed at insert time; `deleted`\n // is the only field that can change, and only monotonically\n // (existence-wins: once deleted, always deleted).\n }\n }\n\n /** Every slot as a plain array, for persistence/transport. */\n toSlotArray() {\n return Array.from(this.slots.values());\n }\n\n /** How many tombstoned (deleted but still retained) slots this list is currently carrying — for deciding whether GC is worth running, or just observing growth. */\n tombstoneCount() {\n let count = 0;\n for (const slot of this.slots.values()) {\n if (slot.deleted) count += 1;\n }\n return count;\n }\n\n /**\n * Permanently removes tombstoned slots whose delete happened before\n * `beforeClock` — the actual fix for the \"deleted content is never\n * garbage-collected\" limitation. Splices each removed slot out of the\n * chain first (children reparented to its own origin, same technique\n * as `move()`) so removal never orphans a slot anchored to it. Returns\n * the number of slots actually removed.\n *\n * Safety of this is specifically tied to how this package's transport\n * layer reconnects: `CollabSession` resyncs a reconnecting peer via a\n * full document snapshot (see syncResponse in CollabSession.js), never\n * by replaying a log of historical ops — so a peer that was offline\n * longer than any reasonable GC threshold never needs an old tombstone\n * to resolve a stale anchor; it just adopts the current state directly.\n * The one theoretical residual risk is a single already-open connection\n * somehow stalling for exactly as long as the GC threshold and then\n * delivering an old queued message afterward — vanishingly unlikely for\n * a live, ordered, reliable data channel (which disconnects long before\n * that under any real network interruption), but not mathematically\n * impossible, which is why this is opt-in rather than automatic.\n */\n pruneTombstones(beforeClock) {\n let removed = 0;\n for (const [id, slot] of this.slots) {\n if (!slot.deleted) continue;\n // deletedClock (when it was actually deleted) is what age means\n // here -- falling back to the slot's own clock (when its position\n // was established) only for a slot deleted before this field\n // existed, which otherwise has no recorded deletion time at all.\n const age = slot.deletedClock ?? slot.clock;\n if (HLC.compare(age, beforeClock) >= 0) continue; // not old enough yet\n for (const other of this.slots.values()) {\n if (other.originId === id) other.originId = slot.originId;\n }\n this.slots.delete(id);\n removed += 1;\n }\n return removed;\n }\n\n /**\n * Materializes the currently-visible order as a plain array of ids\n * (tombstoned ids excluded) — this is what gets projected onto\n * Block.contentIds after every structural change.\n */\n toArray() {\n const childrenByOrigin = new Map();\n for (const slot of this.slots.values()) {\n const bucket = childrenByOrigin.get(slot.originId);\n if (bucket) bucket.push(slot);\n else childrenByOrigin.set(slot.originId, [slot]);\n }\n for (const bucket of childrenByOrigin.values()) {\n bucket.sort((a, b) => HLC.compare(b.clock, a.clock));\n }\n\n const result = [];\n // Explicit-stack depth-first walk — a naive recursive walk's depth\n // equals the list length on a long simple chain (the common case for\n // typed/sequential content), which risks a stack overflow on large\n // documents.\n const stack = [...(childrenByOrigin.get(null) ?? [])].reverse();\n while (stack.length > 0) {\n const slot = stack.pop();\n if (!slot.deleted) result.push(slot.id);\n const children = childrenByOrigin.get(slot.id);\n if (children) {\n for (let i = children.length - 1; i >= 0; i -= 1) stack.push(children[i]);\n }\n }\n return result;\n }\n}\n","import { HLC } from './clock.js';\n\n/**\n * Tracks the logical-clock high-water mark for individual mutable fields\n * (a block's type, one prop key, a run's value, ...) so that when a\n * remote peer's write arrives it's only applied if it's actually newer\n * than whatever's already there — last-write-wins, scoped per field\n * rather than per whole block/run, so unrelated concurrent edits (peer A\n * changes the type, peer B changes a prop) both survive.\n */\nexport class FieldClockRegistry {\n constructor() {\n this.clocks = new Map();\n }\n\n static compositeKey(id, field) {\n return `${id}:${field}`;\n }\n\n get(id, field) {\n return this.clocks.get(FieldClockRegistry.compositeKey(id, field)) ?? null;\n }\n\n /** Local write: always wins outright — it just happened, nothing to compare against. */\n recordLocal(id, field, clock) {\n this.clocks.set(FieldClockRegistry.compositeKey(id, field), clock);\n }\n\n /**\n * Whether a remote write to this field should be applied. Callers must\n * check this before mutating so a stale remote write can be discarded\n * without ever touching local state.\n */\n shouldApplyRemote(id, field, remoteClock) {\n const current = this.get(id, field);\n if (!current) return true;\n return HLC.compare(remoteClock, current) > 0;\n }\n\n recordRemote(id, field, remoteClock) {\n this.clocks.set(FieldClockRegistry.compositeKey(id, field), remoteClock);\n }\n}\n"],"names":["HLC","peerId","now","remote","maxWall","a","b","genPeerId","ListCrdtState","ids","state","prev","id","slot","afterId","clock","oldOriginId","other","remoteSlots","local","count","beforeClock","removed","age","childrenByOrigin","bucket","result","stack","children","FieldClockRegistry","field","remoteClock","current"],"mappings":"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"}
|