diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b99370f1b0..58a22ccd3ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ ## vNEXT (not yet released) +## v3.22.0 + +### `@liveblocks/react` + +This release adds version history support for Storage: a version now snapshots +both the room's Storage and Yjs documents (previously Yjs only). + +- Creating a version now also snapshots Storage, not just Yjs, see + [docs](https://liveblocks.io/docs/api-reference/rest-api-endpoints#create-version-history-snapshot). +- `useHistoryVersions()` lists the room's versions. Each has a `vh_xxx` id, see + [docs](https://liveblocks.io/docs/api-reference/liveblocks-react#useHistoryVersions). +- `useHistoryVersionStorageData("vh_xxx")` returns that version's Storage as a + read-only `LiveObject` so you can visualize or diff it manually, see + [docs](https://liveblocks.io/docs/api-reference/liveblocks-react#useHistoryVersionStorageData). +- `useRestoreToStorageVersion("vh_xxx")` restores the room's Storage to that + version, as a single undoable change, see + [docs](https://liveblocks.io/docs/api-reference/liveblocks-react#useRestoreToStorageVersion). +- `useDeleteHistoryVersion()` returns `deleteHistoryVersion("vh_xxx")` to + permanently delete a version, see + [docs](https://liveblocks.io/docs/api-reference/liveblocks-react#useDeleteHistoryVersion). + +### `@liveblocks/node` and Python SDK + +- Add methods for version history to list room versions, create a version + snapshot, and delete a version. + ## v3.21.0 ### All packages diff --git a/assets/tutorials/version-history/enable-version-history.png b/assets/tutorials/version-history/enable-version-history.png new file mode 100644 index 00000000000..e46f51aeedd Binary files /dev/null and b/assets/tutorials/version-history/enable-version-history.png differ diff --git a/docs/pages/api-reference/liveblocks-node.mdx b/docs/pages/api-reference/liveblocks-node.mdx index 74628f87f0a..5b16c631f0c 100644 --- a/docs/pages/api-reference/liveblocks-node.mdx +++ b/docs/pages/api-reference/liveblocks-node.mdx @@ -1830,6 +1830,73 @@ const binaryYjsUpdate = await liveblocks.getYjsDocumentAsBinaryUpdate( Read the [Yjs documentation](https://docs.yjs.dev/api/document-updates) to learn more about using binary updates. +### Version History + +#### Liveblocks.getVersionHistory [#get-version-history] + +Returns a room’s version history snapshots, sorted by creation date from newest +to oldest. Throws an error if the room isn’t found. This is a wrapper around the +[Get Version History API](/docs/api-reference/rest-api-endpoints#get-version-history) +and returns the same response. + +```ts +const { data: versions, nextCursor } = await liveblocks.getVersionHistory( + "my-room-id", + { + // Optional, defaults to 20 + limit: 20, + + // Optional, used for pagination + cursor: "eyJjcmVhdGVkQXQi...", + } +); +``` + +#### Liveblocks.createVersionHistorySnapshot [#create-version-history-snapshot] + +Creates a new version history snapshot of a room, capturing both its Storage and +Yjs documents. Throws an error if the room isn’t found. This is a wrapper around +the +[Create Version History Snapshot API](/docs/api-reference/rest-api-endpoints#create-version-history-snapshot) +and returns the same response. + +```ts +const { data } = + await liveblocks.createVersionHistorySnapshot("my-room-id"); + +// { id: "vh_d75sF3..." } +console.log(data); +``` + +#### Liveblocks.getYjsVersion [#get-yjs-version] + +Returns a specific version of a room’s Yjs document encoded as a binary Yjs +update. Throws an error if the room or version isn’t found. This is a wrapper +around the +[Get Yjs Document Version API](/docs/api-reference/rest-api-endpoints#get-yjs-version) +and returns the same response. + +```ts +const binaryYjsUpdate = await liveblocks.getYjsVersion({ + roomId: "my-room-id", + versionId: "vh_d75sF3...", +}); +``` + +#### Liveblocks.deleteVersion [#delete-version] + +Permanently deletes a version from a room’s history. Throws an error if the room +or version isn’t found. This is a wrapper around the +[Delete Version API](/docs/api-reference/rest-api-endpoints#delete-version) and +returns no response. + +```ts +await liveblocks.deleteVersion({ + roomId: "my-room-id", + versionId: "vh_d75sF3...", +}); +``` + ### Attachments #### Liveblocks.getAttachment [#get-rooms-roomId-attachments-attachmentId] diff --git a/docs/pages/api-reference/liveblocks-react-blocknote.mdx b/docs/pages/api-reference/liveblocks-react-blocknote.mdx index 16117d1bff1..74570a1c35c 100644 --- a/docs/pages/api-reference/liveblocks-react-blocknote.mdx +++ b/docs/pages/api-reference/liveblocks-react-blocknote.mdx @@ -739,6 +739,8 @@ The `HistoryVersionPreview` component allows you to display a preview of a specific version of your BlockNote editor’s content. It also contains a button and logic for restoring. To render a list of versions, see [`VersionHistory`](/docs/api-reference/liveblocks-react-ui#Version-History). +Learn how to set this up in our +[version history guide](/docs/guides/how-to-add-version-history-to-your-app). #### Usage [#HistoryVersionPreview-usage] diff --git a/docs/pages/api-reference/liveblocks-react-lexical.mdx b/docs/pages/api-reference/liveblocks-react-lexical.mdx index f1ef805e182..09a1a93b92d 100644 --- a/docs/pages/api-reference/liveblocks-react-lexical.mdx +++ b/docs/pages/api-reference/liveblocks-react-lexical.mdx @@ -1698,6 +1698,8 @@ specific version of your Lexical editor's content. It also contains a button and logic for restoring. It must be used inside the `` context. To render a list of versions, see [`VersionHistory`](/docs/api-reference/liveblocks-react-ui#Version-History). +Learn how to set this up in our +[version history guide](/docs/guides/how-to-add-version-history-to-your-app). #### Usage diff --git a/docs/pages/api-reference/liveblocks-react-tiptap.mdx b/docs/pages/api-reference/liveblocks-react-tiptap.mdx index f94b15a297c..6cd1249dbae 100644 --- a/docs/pages/api-reference/liveblocks-react-tiptap.mdx +++ b/docs/pages/api-reference/liveblocks-react-tiptap.mdx @@ -1646,6 +1646,8 @@ The `HistoryVersionPreview` component allows you to display a preview of a specific version of your Tiptap editor’s content. It also contains a button and logic for restoring. To render a list of versions, see [`VersionHistory`](/docs/api-reference/liveblocks-react-ui#Version-History). +Learn how to set this up in our +[version history guide](/docs/guides/how-to-add-version-history-to-your-app). #### Usage [#HistoryVersionPreview-usage] diff --git a/docs/pages/api-reference/liveblocks-react-ui.mdx b/docs/pages/api-reference/liveblocks-react-ui.mdx index 90a36435a12..8149264e3b4 100644 --- a/docs/pages/api-reference/liveblocks-react-ui.mdx +++ b/docs/pages/api-reference/liveblocks-react-ui.mdx @@ -4117,13 +4117,13 @@ All hooks for Presence are in ## Version History -Version history enables you to track and restore versions of your -[Lexical](https://liveblocks.io/docs/api-reference/liveblocks-react-lexical) or -[Yjs](/docs/api-reference/liveblocks-yjs) document. Versions can be -automatically created when enabled in your project settings, or manually created -using the -[REST API](/docs/api-reference/rest-api-endpoints#create-version-history-snapshot). -These components aid in displaying a list of those versions. +Version history enables you to track and restore versions of your Storage and +Yjs documents. These ready-made components aid in displaying a list of version +information for [Tiptap](/docs/api-reference/liveblocks-react-tiptap), +[BlockNote](/docs/api-reference/liveblocks-react-blocknote), and +[Lexical](/docs/api-reference/liveblocks-react-lexical). Learn how to set this +up in our +[version history guide](/docs/guides/how-to-add-version-history-to-your-app). ### Default components [#Version-History-Components] diff --git a/docs/pages/api-reference/liveblocks-react.mdx b/docs/pages/api-reference/liveblocks-react.mdx index ca067ec5983..340dda8dc4f 100644 --- a/docs/pages/api-reference/liveblocks-react.mdx +++ b/docs/pages/api-reference/liveblocks-react.mdx @@ -6418,10 +6418,14 @@ updateSettings({ ## Version History +Hooks for listing, previewing, restoring, and deleting versions of a room’s +Storage and Yjs documents. Learn how to set this up in our +[version history guide](/docs/guides/how-to-add-version-history-to-your-app). + ### useHistoryVersions [@badge=RoomProvider] -Returns the versions of the room. See -[Version History Components](/docs/api-reference/liveblocks-react-ui#Version-history-components) +Returns the versions of the room, both Storage and Yjs. See +[Version History Components](/docs/api-reference/liveblocks-react-ui#Version-History) for more information on how to display versions. ```tsx @@ -6436,6 +6440,39 @@ const { versions, error, isLoading } = useHistoryVersions(); +### useHistoryVersionStorageData [@badge=RoomProvider] + +Returns the Storage data for a given version of the room, reconstructed as a +read-only [`LiveObject`][]. Because a historical version may not match your +room's current `Storage` type, its shape is typed as the more permissive +`LsonObject`. + +```tsx +import { useHistoryVersionStorageData } from "@liveblocks/react"; + +const { data, error, isLoading } = useHistoryVersionStorageData(versionId); +``` + + + + The ID of the version to retrieve. Obtained from the `id` field of a + `HistoryVersion` returned by [`useHistoryVersions`][]. + + + + + + The Storage data for the version as a read-only `LiveObject`, or `undefined` + while loading. + + + Whether the version data is currently being loaded. + + + Any error that occurred while loading the version data. + + + ### useHistoryVersionYjsData [@badge=RoomProvider] Returns the raw Yjs binary data for a given version of the room, for use with @@ -6466,6 +6503,60 @@ const { data, error, isLoading } = useHistoryVersionYjsData(versionId); +### useRestoreToStorageVersion [@badge=RoomProvider] + +Returns a function that restores the room's Storage to a given version, applied +as a single undoable change. Only the Storage surface is affected; other +surfaces (such as Yjs) are left untouched. + +```tsx +import { useRestoreToStorageVersion } from "@liveblocks/react"; + +const restoreToStorageVersion = useRestoreToStorageVersion(versionId); + +// Later, restore the room's Storage to this version +await restoreToStorageVersion(); +``` + + + + The ID of the version to restore to. Obtained from the `id` field of a + `HistoryVersion` returned by [`useHistoryVersions`][]. + + + + + + A function that restores the room's Storage to the version. The change is + applied locally as a single undoable operation, then synced to the other + clients in the room. + + + +### useDeleteHistoryVersion [@badge=RoomProvider] + +Returns a function that permanently deletes a version from the room's history. + +```tsx +import { useDeleteHistoryVersion } from "@liveblocks/react"; + +const deleteHistoryVersion = useDeleteHistoryVersion(); + +// Later, delete a version by its id +await deleteHistoryVersion(versionId); +``` + + + + A function that deletes the version with the given id. The `versionId` is + obtained from the `id` field of a `HistoryVersion` returned by + [`useHistoryVersions`][]. + + + ## Miscellaneous ### useUser [@badge=Both] @@ -7116,6 +7207,7 @@ And the following timeline: [`useothersconnectionids`]: /docs/api-reference/liveblocks-react#useOthersConnectionIds [`useother`]: /docs/api-reference/liveblocks-react#useOther +[`usehistoryversions`]: /docs/api-reference/liveblocks-react#useHistoryVersions [`uselostconnectionlistener`]: /docs/api-reference/liveblocks-react#useLostConnectionListener [`clientsidesuspense`]: /docs/api-reference/liveblocks-react#ClientSideSuspsnse diff --git a/docs/pages/collaboration-features/multiplayer/sync-engine/liveblocks-storage.mdx b/docs/pages/collaboration-features/multiplayer/sync-engine/liveblocks-storage.mdx index e2a28922af7..7aae128c5ca 100644 --- a/docs/pages/collaboration-features/multiplayer/sync-engine/liveblocks-storage.mdx +++ b/docs/pages/collaboration-features/multiplayer/sync-engine/liveblocks-storage.mdx @@ -148,6 +148,56 @@ function MultiplayerCanvas() { We also provide [`useRedo`](/docs/api-reference/liveblocks-react#useRedo) and [`useCanRedo`](/docs/api-reference/liveblocks-react#useCanRedo) to handle redo. +### Version history + +Create version snapshots of your Storage document, and let users browse, +preview, and restore them. +[`useHistoryVersions`](/docs/api-reference/liveblocks-react#useHistoryVersions) +lists a room’s versions, and +[`useRestoreToStorageVersion`](/docs/api-reference/liveblocks-react#useRestoreToStorageVersion) +restores Storage to a version, applied as a single undoable change. + +```tsx +import { + useHistoryVersions, + useRestoreToStorageVersion, +} from "@liveblocks/react"; + +function VersionHistory() { + // +++ + const { versions } = useHistoryVersions(); + // +++ + + return ( +
+ // +++ + {versions.map((version) => ( +
+ + +
+ ))} + // +++ +
+ ); +} + +function RestoreButton({ versionId }) { + // +++ + const restoreToStorageVersion = useRestoreToStorageVersion(versionId); + // +++ + + return ( + + ); +} +``` + +You can also preview a version’s data with +[`useHistoryVersionStorageData`](/docs/api-reference/liveblocks-react#useHistoryVersionStorageData). +Learn how to set this up in our +[version history guide](/docs/guides/how-to-add-version-history-to-your-app). + ## Broadcast Broadcast realtime events to other clients, helpful for triggering live actions diff --git a/docs/pages/collaboration-features/multiplayer/sync-engine/liveblocks-yjs.mdx b/docs/pages/collaboration-features/multiplayer/sync-engine/liveblocks-yjs.mdx index 358385c357f..95010db968e 100644 --- a/docs/pages/collaboration-features/multiplayer/sync-engine/liveblocks-yjs.mdx +++ b/docs/pages/collaboration-features/multiplayer/sync-engine/liveblocks-yjs.mdx @@ -79,6 +79,59 @@ function App() { } ``` +## Version history + +Create version snapshots of your Yjs document, and let users browse, preview, +and restore them. +[`useHistoryVersions`](/docs/api-reference/liveblocks-react#useHistoryVersions) +lists a room’s versions, and +[`useHistoryVersionYjsData`](/docs/api-reference/liveblocks-react#useHistoryVersionYjsData) +returns a version as a binary Yjs update, which you can apply to a fresh `Y.Doc` +to read its contents. + +```tsx +import { + useHistoryVersions, + useHistoryVersionYjsData, +} from "@liveblocks/react"; +import * as Y from "yjs"; + +function VersionHistory() { + // +++ + const { versions } = useHistoryVersions(); + // +++ + + return ( +
+ // +++ + {versions.map((version) => ( +
+ + +
+ ))} + // +++ +
+} + +function VersionPreview({ versionId }) { + // +++ + const { data } = useHistoryVersionYjsData(versionId); + // +++ + + // Apply the version's binary update to an empty Y.Doc + const yDoc = new Y.Doc(); + Y.applyUpdate(yDoc, data); + + // ... +} +``` + +If you’re using our Tiptap, BlockNote, or Lexical plugins, ready-made +[`HistoryVersionPreview`](/docs/api-reference/liveblocks-react-tiptap#HistoryVersionPreview) +components preview and restore versions for you. Learn how to set this up in our +[version history guide](/docs/guides/how-to-add-version-history-to-your-app). + ## Text and code editor integrations Liveblocks Yjs integrates with popular text and code editors such as Slate, diff --git a/docs/pages/collaboration-features/multiplayer/text-editor/blocknote.mdx b/docs/pages/collaboration-features/multiplayer/text-editor/blocknote.mdx index 8e55c09e8e6..065c1dddc30 100644 --- a/docs/pages/collaboration-features/multiplayer/text-editor/blocknote.mdx +++ b/docs/pages/collaboration-features/multiplayer/text-editor/blocknote.mdx @@ -308,6 +308,9 @@ In this example, `useHistoryVersions` fetches the version history, provides users with an interface to browse through the document's history and restore different versions. +Learn more in our +[version history guide](/docs/guides/how-to-add-version-history-to-your-app). + ## BlockNote API Reference diff --git a/docs/pages/collaboration-features/multiplayer/text-editor/lexical.mdx b/docs/pages/collaboration-features/multiplayer/text-editor/lexical.mdx index 74738a54590..4621550b756 100644 --- a/docs/pages/collaboration-features/multiplayer/text-editor/lexical.mdx +++ b/docs/pages/collaboration-features/multiplayer/text-editor/lexical.mdx @@ -365,6 +365,9 @@ In this example, `useHistoryVersions` fetches the version history, provides users with an interface to browse through the document's history and restore different versions. +Learn more in our +[version history guide](/docs/guides/how-to-add-version-history-to-your-app). + ## Lexical API Reference diff --git a/docs/pages/collaboration-features/multiplayer/text-editor/tiptap.mdx b/docs/pages/collaboration-features/multiplayer/text-editor/tiptap.mdx index 93504dfcc5f..9efb2509af1 100644 --- a/docs/pages/collaboration-features/multiplayer/text-editor/tiptap.mdx +++ b/docs/pages/collaboration-features/multiplayer/text-editor/tiptap.mdx @@ -403,6 +403,9 @@ In this example, `useHistoryVersions` fetches the version history, provides users with an interface to browse through the document's history and restore different versions. +Learn more in our +[version history guide](/docs/guides/how-to-add-version-history-to-your-app). + ## Tiptap API Reference diff --git a/docs/pages/pricing/overview.mdx b/docs/pages/pricing/overview.mdx index b6f1bb1044b..e5f937354ce 100644 --- a/docs/pages/pricing/overview.mdx +++ b/docs/pages/pricing/overview.mdx @@ -42,7 +42,7 @@ first. The main metered units: | [Comments created](/docs/pricing/plans/pro#Metered-usage) | Each comment posted in your app | $0.01 per comment | | [Realtime data storage updates](/docs/pricing/plans/pro#Metered-usage) | Each change written to stored data | $1 per 1M updates | | [Realtime data stored](/docs/pricing/plans/pro#Metered-usage) | Total data kept in storage | $0.15 per GB | -| [Monthly custom notifications](/docs/pricing/plans/pro#Metered-usage) | Notifications you trigger from your own code | $0.01 per event | +| [Monthly custom notifications](/docs/pricing/plans/pro#Metered-usage) | Notifications you trigger from your own code | $0.005 per event | | [File storage](/docs/pricing/plans/pro#Metered-usage) | Total files stored | $0.15 per GB | Rates shown here are indicative. See diff --git a/docs/references/v2.openapi.json b/docs/references/v2.openapi.json index def546850cf..99c99ed43a4 100644 --- a/docs/references/v2.openapi.json +++ b/docs/references/v2.openapi.json @@ -1488,7 +1488,7 @@ "/rooms/{roomId}/versions": { "get": { "summary": "Get Version History", - "description": "This endpoint returns a list of version history snapshots for the room. The versions are returned sorted by creation date, from newest to oldest.", + "description": "This endpoint returns a list of version history snapshots for the room. The versions are returned sorted by creation date, from newest to oldest. Corresponds to [`liveblocks.getVersionHistory`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-version-history).", "tags": ["Version History"], "operationId": "get-version-history", "parameters": [ @@ -1569,7 +1569,7 @@ }, "post": { "summary": "Create version history snapshot", - "description": "This endpoint creates a new version history snapshot of the room, capturing both its Storage and Yjs documents.", + "description": "This endpoint creates a new version history snapshot of the room, capturing both its Storage and Yjs documents. Corresponds to [`liveblocks.createVersionHistorySnapshot`](https://liveblocks.io/docs/api-reference/liveblocks-node#create-version-history-snapshot).", "tags": ["Version History"], "operationId": "create-version-history-snapshot", "parameters": [ @@ -1619,7 +1619,7 @@ "/rooms/{roomId}/versions/{versionId}/yjs": { "get": { "summary": "Get Yjs document version", - "description": "This endpoint returns a specific version of the room's Yjs document encoded as a binary Yjs update.", + "description": "This endpoint returns a specific version of the room's Yjs document encoded as a binary Yjs update. Corresponds to [`liveblocks.getYjsVersion`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-yjs-version).", "tags": ["Version History"], "operationId": "get-yjs-version", "parameters": [ @@ -1671,7 +1671,7 @@ "/rooms/{roomId}/versions/{versionId}": { "delete": { "summary": "Delete a version", - "description": "This endpoint permanently deletes a version from the room's history.", + "description": "This endpoint permanently deletes a version from the room's history. Corresponds to [`liveblocks.deleteVersion`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-version).", "tags": ["Version History"], "operationId": "delete-version", "parameters": [ diff --git a/e2e/next-sandbox/pages/index.tsx b/e2e/next-sandbox/pages/index.tsx index 551c617ef4b..d258e2a35bc 100644 --- a/e2e/next-sandbox/pages/index.tsx +++ b/e2e/next-sandbox/pages/index.tsx @@ -43,6 +43,12 @@ export default function Home() { +
  • + + Version history + +
  • +
  • Offline
  • diff --git a/e2e/next-sandbox/pages/version-history.tsx b/e2e/next-sandbox/pages/version-history.tsx new file mode 100644 index 00000000000..c38a4df2426 --- /dev/null +++ b/e2e/next-sandbox/pages/version-history.tsx @@ -0,0 +1,442 @@ +import type { LsonObject } from "@liveblocks/client"; +import { LiveList, LiveObject } from "@liveblocks/client"; +import { kInternal } from "@liveblocks/core"; +import { createRoomContext } from "@liveblocks/react"; +import { LiveblocksYjsProvider } from "@liveblocks/yjs"; +import { useEffect, useMemo, useRef, useState } from "react"; +import * as Y from "yjs"; + +import { getRoomFromUrl, getUserFromUrl, randomInt } from "../utils"; +import Button from "../utils/Button"; +import { createLiveblocksClient } from "../utils/createClient"; + +const client = createLiveblocksClient({ + // Attribute edits to the ?user= in the URL, so version authors differ per tab. + authEndpoint: async (_roomId) => { + const userId = getUserFromUrl(); + const resp = await fetch( + `/api/auth/access-token?user=${encodeURIComponent(userId)}` + ); + return resp.json(); + }, +}); + +// The Storage root _is_ the document (no wrapper field), so the live document +// and a reconstructed historic version render the exact same shape. +const { + RoomProvider, + useCanRedo, + useCanUndo, + useDeleteHistoryVersion, + useHistoryVersionStorageData, + useHistoryVersionYjsData, + useHistoryVersions, + useMutation, + useRedo, + useRestoreToStorageVersion, + useRoom, + useSelf, + useStorage, + useUndo, +} = createRoomContext(client); + +// A small, fixed key pool so repeated edits overwrite existing keys -- letting +// you flip a key between a number, a nested object, and a list to produce +// interesting version-to-version diffs. +const KEYS = ["a", "b", "c", "d", "e"]; +const randomKey = () => KEYS[randomInt(KEYS.length)]; + +// Decodes a Yjs version's update bytes into the plain text of its "text" field. +function yjsUpdateToText(update: Uint8Array | undefined): string { + if (update === undefined) { + return ""; + } + const doc = new Y.Doc(); + Y.applyUpdate(doc, update); + return doc.getText("text").toString(); +} + +export default function Home() { + const roomId = getRoomFromUrl(); + return ( + + + + ); +} + +function Sandbox() { + const doc = useStorage((root) => root); + const me = useSelf(); + const room = useRoom(); + const undo = useUndo(); + const redo = useRedo(); + const canUndo = useCanUndo(); + const canRedo = useCanRedo(); + const { versions, error: versionsError } = useHistoryVersions(); + const deleteHistoryVersion = useDeleteHistoryVersion(); + const [selectedVersionId, setSelectedVersionId] = useState( + null + ); + const [toast, setToast] = useState(null); + + const showToast = (message: string) => { + setToast(message); + setTimeout(() => setToast(null), 4000); + }; + + // A Yjs document living on the same room, so a version snapshot captures both + // the Storage and the Yjs surfaces (the cross-facet part of the feature). + const ydoc = useMemo(() => new Y.Doc(), []); + const [yjsText, setYjsText] = useState(""); + const [yjsInput, setYjsInput] = useState(""); + const [yjsCanUndo, setYjsCanUndo] = useState(false); + const [yjsCanRedo, setYjsCanRedo] = useState(false); + const yjsUndoManager = useRef(null); + + useEffect(() => { + const provider = new LiveblocksYjsProvider(room, ydoc); + const ytext = ydoc.getText("text"); + // Yjs has its own history (separate from the room's Storage undo/redo). It + // tracks only local edits, not the remote changes the provider applies. + const undoManager = new Y.UndoManager(ytext); + yjsUndoManager.current = undoManager; + const handler = () => { + const text = ytext.toString(); + setYjsText(text); + setYjsInput(text); // keep the editable box in sync with the document + setYjsCanUndo(undoManager.undoStack.length > 0); + setYjsCanRedo(undoManager.redoStack.length > 0); + }; + ydoc.on("update", handler); + handler(); + return () => { + ydoc.off("update", handler); + undoManager.destroy(); + yjsUndoManager.current = null; + provider.destroy(); + }; + }, [room, ydoc]); + + const setNumber = useMutation(({ storage }) => { + storage.set(randomKey(), randomInt(100)); + }, []); + + const addObject = useMutation(({ storage }) => { + storage.set(randomKey(), new LiveObject({ n: randomInt(100) })); + }, []); + + const addList = useMutation(({ storage }) => { + storage.set(randomKey(), new LiveList([randomInt(100), randomInt(100)])); + }, []); + + // Deeply nested edit: push a value into the first list found in the document. + const pushToList = useMutation(({ storage }, value: number) => { + for (const key of storage.keys()) { + const child = storage.get(key); + if (child instanceof LiveList) { + child.push(value); + return; + } + } + }, []); + + const deleteKey = useMutation(({ storage }, key: string) => { + storage.delete(key); + }, []); + + const clearStorage = useMutation(({ storage }) => { + for (const key of [...storage.keys()]) { + storage.delete(key); + } + }, []); + + const setYjs = () => { + const text = ydoc.getText("text"); + text.delete(0, text.length); + text.insert(0, yjsInput); + }; + + const clearYjs = () => { + const text = ydoc.getText("text"); + text.delete(0, text.length); + }; + + const createSnapshot = () => { + void room[kInternal].createVersionHistorySnapshot(); + }; + + // Restore Yjs: replace the live text with the historic version's text. + const restoreYjs = (text: string) => { + const live = ydoc.getText("text"); + live.delete(0, live.length); + live.insert(0, text); + }; + + if (doc === null || me === null) { + return
    Loading…
    ; + } + + const keys = Object.keys(doc); + const hasList = Object.values(doc).some((v) => Array.isArray(v)); + const keyToDelete = keys.length > 0 ? keys[randomInt(keys.length)] : ""; + + return ( +
    + {toast ? ( +
    setToast(null)} + style={{ + position: "fixed", + bottom: 16, + right: 16, + background: "#b00020", + color: "white", + padding: "10px 14px", + borderRadius: 6, + maxWidth: 360, + cursor: "pointer", + zIndex: 1000, + }} + > + {toast} +
    + ) : null} +

    + Home › Version history +

    + +

    + You are {me.id ?? "(anonymous)"}. Open this + page in several tabs with different ?user= values, edit + from each, then snapshot to see multiple authors on a version. A + snapshot captures both the Storage and Yjs surfaces. +

    + +
    + +
    + +
    +
    +

    Live document

    + +
    Storage
    +
    + + + + + + + + +
    +
    +            {JSON.stringify(doc, null, 2)}
    +          
    + +
    Yjs
    +
    +