Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lexical-y-public-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lexical/y': patch
---

[feature] A setup API with the choices an app must make required: `createYBinding(editor, {doc, schemaVersion, synced, onStatus})` and `yCollab({...})` (YCollabExtension's `collab` output) return a binding with `markSynced()`, `handshake(generation)`, `history`, `freeze()` and a `status` that carries why it is read only; `Y_DEFAULTS`, `parseYGeneration`, `createYHandshake`, and `describeUnsyncedEdits` on `@lexical/y/headless`. The existing `YBinding` and `createYCollabConfig` keep working
Comment thread
rootnode-bot[bot] marked this conversation as resolved.
Resolved
42 changes: 29 additions & 13 deletions packages/lexical-playground/src/collaborationY.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import {ReactExtension} from '@lexical/react/ReactExtension';
import {ReactProviderExtension} from '@lexical/react/ReactProviderExtension';
import {useSignalValue} from '@lexical/react/useExtensionSignalValue';
import {
createYCollabConfig,
createYDoc,
isYWarning,
STORAGE_MISMATCH_WARNING,
Expand All @@ -45,8 +44,11 @@ import {
UNSUPPORTED_LAYOUT_WARNING,
UNSUPPORTED_SCHEMA_WARNING,
UNSUPPORTED_SLOTS_WARNING,
Y_DEFAULTS,
yCollab,
YCollabExtension,
type YDoc,
type YStatus,
type YWarning,
} from '@lexical/y';
import {CONNECTED_COMMAND, TOGGLE_CONNECT_COMMAND} from '@lexical/yjs';
Expand Down Expand Up @@ -119,6 +121,14 @@ function reasonOf(warning: YWarning): string | undefined {
: undefined;
}

/**
* The binding's status, for debugging (`window.lexicalYStatus`): the
* read-only notice reads the binding's warnings, which carry more detail.
*/
function showStatus(status: YStatus): void {
(globalThis as {lexicalYStatus?: YStatus}).lexicalYStatus = status;
}

let session: CollabYSession | null = null;

/**
Expand Down Expand Up @@ -155,19 +165,25 @@ export function getCollabYSession(
ReactProviderExtension,
configExtension(ReactExtension, {contentEditable: null}),
buildApp(),
// The canonical setup (the same in the package README and y.md).
yCollab({
doc,
history: Y_DEFAULTS.history, // the default
onStatus: status => showStatus(status),
root: Y_DEFAULTS.root, // the default
schemaVersion: SCHEMA_VERSION,
synced: false, // markSynced() after the provider's first sync
}),
// The binding's self-check (off by default) reads its writes
// back in development builds: a mismatch is a warning, which the
// collab Y e2e specs fail on. It is keyed on the build mode, as
// Lexical's own `__DEV__` is (scripts/vite/lexicalMonorepoPlugin.ts),
// and not on `import.meta.env.DEV`, which Vite derives from
// NODE_ENV: `vite build --mode development`, the build the e2e CI
// job tests, has NODE_ENV production and DEV false.
createYCollabConfig({
doc,
// The playground's relay does not migrate stored documents:
// one below the floor stays read only.
layoutUpgrade: null,
schemaVersion: SCHEMA_VERSION,
// job tests, has NODE_ENV production and DEV false. The
// playground's relay does not migrate stored documents: one below
// the floor stays read only.
configExtension(YCollabExtension, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: yCollab above declares a server layout upgrade, so a below-floor room here now logs that the app's server upgrades such documents, while the comment above says the playground's relay does not migrate. Passing layoutUpgrade: null here keeps the playground's explicit choice (a later configExtension overrides the value yCollab set) without changing the canonical yCollab({...}) call that CanonicalExample.test.ts compares:

Suggested change
configExtension(YCollabExtension, {
configExtension(YCollabExtension, {
layoutUpgrade: null,

selfCheck: import.meta.env?.MODE !== 'production',
}),
],
Expand Down Expand Up @@ -208,7 +224,7 @@ export function CollabYPlugin({
readOnlyWarning: Signal<YWarning | null>;
}): JSX.Element | null {
const [editor] = useLexicalComposerContext();
const {binding, status} = getExtensionDependencyFromEditor(
const {collab, status} = getExtensionDependencyFromEditor(
editor,
YCollabExtension,
).output;
Expand All @@ -219,7 +235,7 @@ export function CollabYPlugin({
const onSync = (isSynced: boolean) => {
if (isSynced) {
// A no-op after the first time.
binding.initialize();
collab.markSynced();
}
};
const onStatus = ({status: value}: {status: string}) => {
Expand Down Expand Up @@ -249,7 +265,7 @@ export function CollabYPlugin({
provider.off('status', onStatus);
provider.disconnect();
};
}, [binding, editor, provider]);
}, [collab, editor, provider]);

// The binding keeps the editor non-editable until it has read the
// document, and while it is read only (a layout it cannot write). Anything
Expand All @@ -260,11 +276,11 @@ export function CollabYPlugin({
useEffect(
() =>
editor.registerEditableListener(editable => {
if (editable && binding.status !== 'active') {
if (editable && collab.status.kind !== 'active') {
editor.setEditable(false);
}
}),
[binding, editor],
[collab, editor],
);

// The editor is read only until the binding has read the document, so
Expand Down
62 changes: 62 additions & 0 deletions packages/lexical-website/docs/collaboration/y.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,68 @@ package). It is separate from `@lexical/yjs`, the Yjs 13 binding that the
[React guide](./react.md) uses: the two use different Yjs libraries and
different document layouts, and a document uses one or the other.

## Start here

1. `const doc = createYDoc()`: the shared document, for your provider.
2. `yCollab({doc, schemaVersion, synced, onStatus})` in the editor's
`dependencies` (`createYBinding(editor, {...})` for an editor built
without extensions).
3. Connect your provider, sending `collab.handshake(generation)` before any
sync message.
4. `collab.markSynced()` after its first sync.
5. `collab.freeze()` when the server says the room moved on (a stale
generation): store what it returns before any reload.

```ts
import {
buildEditorFromExtensions,
getExtensionDependencyFromEditor,
} from '@lexical/extension';
import {RichTextExtension} from '@lexical/rich-text';
import {
createYDoc,
Y_DEFAULTS,
yCollab,
YCollabExtension,
type YStatus,
} from '@lexical/y';
import {defineExtension} from 'lexical';

// Bump it when you add, remove or change a node class or its fields.
const SCHEMA_VERSION = 1;
const showStatus = (status: YStatus) => {
// Tell the user when the document is read only, and why (status.cause).
};

const doc = createYDoc();
const editor = buildEditorFromExtensions(
defineExtension({
dependencies: [
RichTextExtension,
yCollab({
doc,
history: Y_DEFAULTS.history, // the default
onStatus: status => showStatus(status),
root: Y_DEFAULTS.root, // the default
schemaVersion: SCHEMA_VERSION,
synced: false, // markSynced() after the provider's first sync
}),
],
name: 'app',
}),
);
const {collab} = getExtensionDependencyFromEditor(
editor,
YCollabExtension,
).output;
// Connect `doc` to a Yjs 14 provider, sending collab.handshake(generation)
// (null for a client with no local state) first; then, after its first sync:
collab.markSynced();
```

The sections below describe the binding in full, with the earlier setup
(`createYCollabConfig`, `binding.initialize()`), which keeps working.

## When to use it

Use `@lexical/y` for a **new** collaborative editor that can run Yjs 14 on
Expand Down
104 changes: 62 additions & 42 deletions packages/lexical-y/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,65 @@ release may stop reading an older one, and rooms stored in it then open read
only until their server migrates them (see
[Layout upgrades](https://lexical.dev/docs/collaboration/y#layout-upgrades)).

## Start here

1. `const doc = createYDoc()`: the shared document, for your provider.
2. `yCollab({doc, schemaVersion, synced, onStatus})` in the editor's
`dependencies` (`createYBinding(editor, {...})` for an editor built
without extensions).
3. Connect your provider, sending `collab.handshake(generation)` before any
sync message.
4. `collab.markSynced()` after its first sync.
5. `collab.freeze()` when the server says the room moved on (a stale
generation): store what it returns before any reload.

```ts
import {
buildEditorFromExtensions,
getExtensionDependencyFromEditor,
} from '@lexical/extension';
import {RichTextExtension} from '@lexical/rich-text';
import {
createYDoc,
Y_DEFAULTS,
yCollab,
YCollabExtension,
type YStatus,
} from '@lexical/y';
import {defineExtension} from 'lexical';

// Bump it when you add, remove or change a node class or its fields.
const SCHEMA_VERSION = 1;
const showStatus = (status: YStatus) => {
// Tell the user when the document is read only, and why (status.cause).
};

const doc = createYDoc();
const editor = buildEditorFromExtensions(
defineExtension({
dependencies: [
RichTextExtension,
yCollab({
doc,
history: Y_DEFAULTS.history, // the default
onStatus: status => showStatus(status),
root: Y_DEFAULTS.root, // the default
schemaVersion: SCHEMA_VERSION,
synced: false, // markSynced() after the provider's first sync
}),
],
name: 'app',
}),
);
const {collab} = getExtensionDependencyFromEditor(
editor,
YCollabExtension,
).output;
// Connect `doc` to a Yjs 14 provider, sending collab.handshake(generation)
// (null for a client with no local state) first; then, after its first sync:
collab.markSynced();
```

## Install

```sh
Expand Down Expand Up @@ -47,48 +106,9 @@ as a relay either (see

## Usage

```ts
import {
buildEditorFromExtensions,
getExtensionDependencyFromEditor,
} from '@lexical/extension';
import {RichTextExtension} from '@lexical/rich-text';
import {
createYCollabConfig,
createYDoc,
YCollabExtension,
} from '@lexical/y';
import {defineExtension} from 'lexical';

// Yjs's format cleanup off (createYDoc(stored) for a stored state): the
// binding takes no other document.
const doc = createYDoc();
const editor = buildEditorFromExtensions(
defineExtension({
dependencies: [
RichTextExtension,
// Required: the version of your editor's nodes (bump it when you add,
// remove or change one), and how documents below MIN_LAYOUT are
// upgraded. null: no server migrates this provider's stored
// documents, so one below the floor opens read only. An app whose
// server migrates passes createYServerLayoutUpgrade() and sends its
// handshake.
createYCollabConfig({
doc,
layoutUpgrade: null,
schemaVersion: 1,
}),
],
name: 'app',
}),
);
const {binding} = getExtensionDependencyFromEditor(
editor,
YCollabExtension,
).output;
// Connect `doc` to any Yjs 14 provider, then after its first sync:
binding.initialize();
```
See [Start here](#start-here). The earlier setup (`createYCollabConfig`,
`binding.initialize()`) keeps working; [the guide](https://lexical.dev/docs/collaboration/y)
covers both.

## Documentation

Expand Down
Loading
Loading