Skip to content

[lexical-y][lexical-playground][lexical-website] Feature: The binding's setup API from the layout 4 spec (section 11, with 11.8), beside the existing one - #9891

Merged
zurfyx merged 1 commit into
mainfrom
zurfyx/y-public-api
Nov 13, 2026

Conversation

@zurfyx

@zurfyx zurfyx commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Description

The layout 4 spec's public API, added as new exports while the existing YBinding, createYCollabConfig and their options keep working unchanged:

  • createYBinding(editor, options) (an editor built without extensions) and yCollab(options) (YCollabExtension configured for dependencies, whose output gains collab) take YCollabOptions: doc, schemaVersion, and the two choices an app must make, synced and onStatus, required; root and history optional with their defaults in the exported Y_DEFAULTS.
  • Both return a YCollabBinding: status (pending, active, or readonly with a YReadOnlyCause: layout-older, layout-newer, schema-newer, requires-feature, malformed-record, unknown-node-types, storage-mismatch, unsupported-doc, unreadable-content, unstorable-change, frozen), markSynced(), handshake(generation) (fills hasLocalEdits and this release's layouts and features), history (YHistory), getCursor(), freeze() (stops reading and writing and returns YLocalEdits: state, json, text, differs), dispose().
  • A generation token type, YGeneration, read with parseYGeneration, and createYHandshake for servers and tests.
  • describeUnsyncedEdits on @lexical/y/headless (11.6): block-by-block report of a frozen client's edits against their base and the current document.
  • Errors that name the fix: a missing synced, onStatus, doc or schemaVersion, a root or history of the wrong shape, or a string that is not a generation, throws with the call to make.
  • A "start here" JSDoc on both entry points, and one canonical setup example used identically in the package README, y.md (both now open with the same five steps) and the playground; a test keeps the three identical.

Review fixes in this revision: the error messages carry no backticks (the build's error-code transform rejects them); when two read-only causes arrive together, status.cause keeps the first (a newer schema before what its content does); createYBinding handles UNDO_COMMAND and REDO_COMMAND as the extension does; the binding notes this client's writes after each transaction rather than through an update listener, so a document with no provider is not made to encode updates, and the extension starts listening only when it starts the binding. The removal of the earlier entry points (createYCollabConfig, YCollabInput, createYServerLayoutUpgrade from the main entry, YClientSession, ...) is a separate draft pull request, held until apps have moved to this API.

Test plan

Before

$ pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/PublicApi.test.ts   # this PR's test on main
 FAIL  |unit| packages/lexical-y/src/__tests__/unit/PublicApi.test.ts > createYBinding > starts pending, not editable, until markSynced; a second markSynced does nothing
TypeError: createYBinding is not a function
 Test Files  1 failed (1)
      Tests  28 failed (28)

After

$ pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/PublicApi.test.ts
      Tests  29 passed (29)
$ pnpm exec vitest run --project unit packages/lexical-y packages/lexical-playground
      Tests  25031 passed | 375 expected fail | 154 skipped (25560)
$ pnpm exec vitest run --project scripts-unit
      Tests  3676 passed (3676)
$ pnpm run flow
Found 0 errors
$ pnpm exec tsc -p tsconfig.json --noEmit   # 0 errors
$ pnpm run build   # passes (and build-release)

Where the spec is underspecified, and what this does

  • The new binding's name. Spec 11.1 calls the new interface YBinding, which is today's class; here it is YCollabBinding (binding exposes the class underneath).
  • hasLocalEdits: true when the document held state when the binding was made (pending structs included), or this client wrote to it since (a local transaction, or the binding's own writes such as the seed of an empty document).
  • Read-only causes map from the binding's warnings; the first since the binding was last active is kept. A read-only binding with no recorded cause reports unsupported-doc.
  • Pending until markSynced() holds for an empty document. A document that already holds content when the binding starts (a cached state) is read at once and is active before markSynced(), as YBinding does today; spec 11.1 has it pending, which needs a change in the binding itself.
  • freeze() returns fresh copies on every call, reports frozen once Yjs is idle, and takes its text from the draft JSON (falling back to the editor's text where a node class cannot read it back).
  • parseYGeneration checks only a generation's form ('unknown', or g1: and a non-empty rest): generations stay opaque.
  • The new entry points declare a server layout upgrade, so a below-floor room reports layout-older; an app without a migrating server has no null equivalent yet.
  • describeUnsyncedEdits leaves migration of a below-floor input to the caller (spec 11.6 migrates on a scratch copy); a reordering that changes no block is not reported.
  • Not built here: @lexical/y/debug (enableYSelfCheck, validateYState), the closed YWarning union and checkYSchema, and a development-build status report of the effective settings.

@vercel

vercel Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
lexical Ready Ready Preview Nov 13, 2026 11:41am UTC
lexical-playground Ready Ready Preview Nov 13, 2026 11:41am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Nov 12, 2026
@rootnode-bot

rootnode-bot Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 13, 2026, 12:00 PM UTC

@zurfyx
zurfyx force-pushed the zurfyx/y-public-api branch from 06d473e to d9a76d3 Compare November 12, 2026 22:42
@zurfyx
zurfyx force-pushed the zurfyx/y-public-api branch from d9a76d3 to aad17e7 Compare November 12, 2026 22:45
@zurfyx
zurfyx force-pushed the zurfyx/y-public-api branch from aad17e7 to 9c943ed Compare November 12, 2026 22:50
@zurfyx
zurfyx force-pushed the zurfyx/y-public-api branch from 9c943ed to 74c111b Compare November 12, 2026 22:52
@zurfyx
zurfyx force-pushed the zurfyx/y-public-api branch from 74c111b to 8f200d7 Compare November 12, 2026 22:56
@zurfyx
zurfyx force-pushed the zurfyx/y-public-api branch from 8f200d7 to c778052 Compare November 12, 2026 22:57

@rootnode-bot rootnode-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the layout 4 setup API: the new entry points, the canonical example and the tests read well. One problem breaks the package build. The other items below are suggestions.

Blocking

  • pnpm run build fails on packages/lexical-y/src/internal/Collab.ts. Six invariant messages (lines 45, 50, 56, 61, 66 and 75) contain backticks, such as needs `doc` . The build's error-code transform (scripts/error-codes/transform-error-messages.mjs) rewrites each message as a template literal, and Babel rejects a backtick in the raw text. Rollup then stops with Collab.ts: Invalid raw, which is what fails the integrity, e2e canary and publish jobs on this PR. Unit tests and tsc read the sources without that transform, so they pass. To fix it, remove the backticks from these messages (or use quotes) and update the matching regexes in PublicApi.test.ts.

Suggestions

  • The changesets disagree. .changeset/lexical-y-public-api.md says "The existing YBinding and createYCollabConfig keep working", but the second commit removes createYCollabConfig and five related exports, and lexical-y-public-api-breaking.md records the removal. @lexical/y has not been in a stable release yet (its "Add @lexical/y" changeset is still pending). As a result, the pending lexical-y-layout-upgrade.md and lexical-y-collab-input.md would announce createYCollabConfig and YCollabInput as features in the same release that removes them. If the removal ships with this PR, consider editing those pending entries instead of adding a [breaking] entry for APIs no release included (format in .changeset/README.md). Otherwise, split the second commit out as the description plans.
  • Docs in packages/lexical-website/docs/collaboration/y.md: line 422 tells new YBinding(...) users to call collab.markSynced(), but that path has no collab; the call is still binding.initialize(). The full @y/websocket example calls showStatus(status) at line 214 without defining it, so it does not compile when copied.
  • Coverage: the tests call describeUnsyncedEdits only with {state}. The documented call passes freeze()'s result, which takes the json branch, so one case with edits: binding.freeze() would cover it.
  • Nits: CollabBinding.ts:43 still says createYCollabConfig keeps working. yCollab and createYBinding always declare a server layout upgrade (Collab.ts:106, :177), so for an app without a migrating server (such as the playground relay), the below-floor warning says its server should have migrated the room.

Automated review (maintainer link)

Comment thread .changeset/lexical-y-public-api.md Resolved

@rootnode-bot rootnode-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the layout 4 setup API: requiring synced and onStatus, the typed read-only cause and the shared canonical example all make the binding harder to misuse. One problem breaks the package build; the rest are suggestions.

Blocking

  • pnpm run build fails on packages/lexical-y/src/internal/Collab.ts. Six invariant messages (lines 45, 50, 56, 61, 66 and 75) contain backticks, such as needs `doc` . The build's error-code transform (scripts/error-codes/transform-error-messages.mjs, lines 127-129) turns every invariant message into a template literal, and Babel rejects an unescaped backtick in the raw text, so the build stops with Collab.ts: Invalid raw. The same command passes on the base branch. Unit tests, tsc and Flow read the sources without that transform, which is why the test plan is green. To fix it, remove the backticks from these messages (or use quotes) and update the five matching regexes in PublicApi.test.ts (lines 351-362).

Suggestions

  • status.cause reports the second cause when two arrive together (packages/lexical-y/src/internal/Binding.ts:3793-3794). warn() keeps the last read-only warning, but evaluate() warns the newer schema first on purpose (its comment at line 3023 says a newer version's content explains the warnings that follow). When a schemaVersion: 2 peer has also changed how a node is stored, a schemaVersion: 1 client gets unsupported-schema and then storage-mismatch, and collab.status and onStatus report storage-mismatch instead of schema-newer. An app would then tell its user the node class changed rather than "reload to update". Keeping the first cause since the binding was last active, plus a test that combines two causes, would fix it.
  • The changesets disagree. .changeset/lexical-y-public-api.md says "The existing YBinding and createYCollabConfig keep working", but the second commit removes createYCollabConfig and five related exports, and lexical-y-public-api-breaking.md records that removal. @lexical/y has not been in a stable release yet (its "Add @lexical/y" changeset is still pending), so the pending lexical-y-layout-upgrade.md and lexical-y-collab-input.md would announce createYCollabConfig and YCollabInput as features in the same release that removes them. If the removal ships with this PR, consider editing those pending entries instead of adding a [breaking] entry (format in .changeset/README.md); otherwise split the second commit out, as its message plans.
  • Docs in packages/lexical-website/docs/collaboration/y.md: line 421 tells new YBinding(...) users to call collab.markSynced(), but that path has no collab; the call there is still binding.initialize(). The @y/websocket example calls showStatus(status) at line 214 without defining it (only the Start here example defines it), so it does not compile when copied on its own.
  • Coverage: the tests call describeUnsyncedEdits only with {state}. The documented call passes freeze()'s result, which takes the json branch; one case with edits: binding.freeze() would cover it. AGENTS.md also asks the Test plan for ### Before and ### After output.
  • Nits: CollabBinding.ts:43 still says createYCollabConfig keeps working. yCollab and createYBinding always declare a server layout upgrade (Collab.ts:106, :177), so an app without a migrating server (such as the playground relay) has no equivalent of layoutUpgrade: null, and its below-floor warning says the server should have migrated the room. packages/lexical-y/AGENTS.md asks for a DECISIONS.md row when a decision like this changes.

Automated review (maintainer link)

@rootnode-bot rootnode-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the layout 4 setup API. Requiring synced and onStatus, the typed read-only cause and the shared canonical example all make the binding harder to misuse. This revision rebases the same two commits, so the points below carry over; the createYBinding undo point and the stale warning text are new. One problem breaks the package build; the rest are suggestions.

Blocking

  • pnpm run build fails on packages/lexical-y/src/internal/Collab.ts. Six invariant messages (lines 45, 50, 56, 61, 66 and 75) contain backticks, such as needs `doc` . The build's error-code transform (scripts/error-codes/transform-error-messages.mjs, lines 127-129) turns every invariant message into a template literal, and Babel rejects an unescaped backtick in the raw text, so the build stops with Collab.ts: Invalid raw. That is the failure in the integrity, e2e canary and publish jobs. The same command passes on the base branch, and it passes on this head once those backticks are removed. Unit tests, tsc and Flow read the sources without that transform, which is why the test plan is green. packages/lexical-y/AGENTS.md asks for required checks to be green before merging. To fix it, drop the backticks from these messages (or use quotes) and update the five matching regexes in PublicApi.test.ts (lines 351-362).

Suggestions (non-blocking)

  • status.cause reports the second cause when two arrive together (packages/lexical-y/src/internal/Binding.ts:3682-3684). warn() keeps the last read-only warning, but evaluate() checks the schema first on purpose (line 2912: a newer version's content explains the warnings that follow). Suppose a schemaVersion: 2 peer has also changed how a node type is stored. A schemaVersion: 1 client then queues unsupported-schema and then storage-mismatch, and collab.status and onStatus report storage-mismatch instead of schema-newer, so the app tells its user the node class changed rather than "reload to update". A below-floor document opened through configExtension(YCollabExtension, ...) without layoutUpgrade likewise reports unsupported-doc instead of layout-older. Keeping the first cause since the binding was last active, plus a test that combines two causes, would fix both. The playground's own notice already keeps the first, newer-app warning (needsNewerApp).
  • createYBinding advertises undo it does not handle (Collab.ts:130-151). It dispatches CAN_UNDO_COMMAND/CAN_REDO_COMMAND the way YCollabExtension does, but unlike the extension (YExtension.ts:293-310) it registers no UNDO_COMMAND/REDO_COMMAND handler. After an edit, CAN_UNDO is true, but Ctrl/Cmd+Z and an undo button do nothing until the app wires them to binding.history. Registering both commands (removed in dispose), or saying in the JSDoc and in Start here that the app routes them, would close the gap.
  • The changesets disagree. .changeset/lexical-y-public-api.md says "The existing YBinding and createYCollabConfig keep working", but the second commit removes createYCollabConfig and five related exports, and lexical-y-public-api-breaking.md records that removal. @lexical/y has not been in a stable release yet (its "Add @lexical/y" changeset is still pending), so the pending lexical-y-layout-upgrade.md and lexical-y-collab-input.md would announce createYCollabConfig and YCollabInput as features in the same release that removes them. If the removal ships with this PR, consider editing those pending entries instead of adding a [breaking] entry (format in .changeset/README.md); otherwise split the second commit out, as its message plans.
  • Guidance still names the removed export. The 'missing-layout-upgrade' warning at Binding.ts:2988 tells developers to "create the config with createYCollabConfig and pass layoutUpgrade". It still fires for configExtension(YCollabExtension, ...) and new YBinding(...) without layoutUpgrade, yet neither name is importable from @lexical/y any more. y.md line 504 points that reader to Layout upgrades, which no longer describes layoutUpgrade. The JSDoc at Binding.ts:821-828, YExtension.ts:126-128 and CollabBinding.ts:41-43 names createYCollabConfig the same way.
  • Docs in packages/lexical-website/docs/collaboration/y.md: line 421 tells new YBinding(...) users to call collab.markSynced(), but that path has no collab; the call there is still binding.initialize(). The @y/websocket example calls showStatus(status) at line 214 without defining it (only the Start here example defines it), so it does not compile when copied on its own.
  • Coverage: the tests call describeUnsyncedEdits only with {state}. The documented call passes freeze()'s result, which takes the json branch; one case with edits: binding.freeze() would cover it. AGENTS.md also asks the Test plan for ### Before and ### After output.
  • Nits: yCollab and createYBinding always declare a server layout upgrade (Collab.ts:106, :177). An app without a migrating server, such as the playground relay, therefore has no equivalent of layoutUpgrade: null, and its below-floor warning says the server should have migrated the room. packages/lexical-y/AGENTS.md asks for a DECISIONS.md row when a recorded decision like this changes. flow/LexicalYServer.js.flow:263-264 still says those types are "imported and re-exported, never restated", right above the declarations this PR restates.

Automated review (maintainer link)

@pkg-pr-new

pkg-pr-new Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
lexical

npm i https://pkg.pr.new/lexical@9891

@lexical/a11y

npm i https://pkg.pr.new/@lexical/a11y@9891

@lexical/clipboard

npm i https://pkg.pr.new/@lexical/clipboard@9891

@lexical/code

npm i https://pkg.pr.new/@lexical/code@9891

@lexical/code-core

npm i https://pkg.pr.new/@lexical/code-core@9891

@lexical/code-prism

npm i https://pkg.pr.new/@lexical/code-prism@9891

@lexical/code-shiki

npm i https://pkg.pr.new/@lexical/code-shiki@9891

@lexical/compiler

npm i https://pkg.pr.new/@lexical/compiler@9891

@lexical/devtools-core

npm i https://pkg.pr.new/@lexical/devtools-core@9891

@lexical/dragon

npm i https://pkg.pr.new/@lexical/dragon@9891

@lexical/eslint-plugin

npm i https://pkg.pr.new/@lexical/eslint-plugin@9891

@lexical/extension

npm i https://pkg.pr.new/@lexical/extension@9891

@lexical/fast-check

npm i https://pkg.pr.new/@lexical/fast-check@9891

@lexical/file

npm i https://pkg.pr.new/@lexical/file@9891

@lexical/hashtag

npm i https://pkg.pr.new/@lexical/hashtag@9891

@lexical/headless

npm i https://pkg.pr.new/@lexical/headless@9891

@lexical/history

npm i https://pkg.pr.new/@lexical/history@9891

@lexical/html

npm i https://pkg.pr.new/@lexical/html@9891

@lexical/internal

npm i https://pkg.pr.new/@lexical/internal@9891

@lexical/link

npm i https://pkg.pr.new/@lexical/link@9891

@lexical/list

npm i https://pkg.pr.new/@lexical/list@9891

@lexical/mark

npm i https://pkg.pr.new/@lexical/mark@9891

@lexical/markdown

npm i https://pkg.pr.new/@lexical/markdown@9891

@lexical/mdast

npm i https://pkg.pr.new/@lexical/mdast@9891

@lexical/offset

npm i https://pkg.pr.new/@lexical/offset@9891

@lexical/overflow

npm i https://pkg.pr.new/@lexical/overflow@9891

@lexical/plain-text

npm i https://pkg.pr.new/@lexical/plain-text@9891

@lexical/react

npm i https://pkg.pr.new/@lexical/react@9891

@lexical/rich-text

npm i https://pkg.pr.new/@lexical/rich-text@9891

@lexical/selection

npm i https://pkg.pr.new/@lexical/selection@9891

@lexical/table

npm i https://pkg.pr.new/@lexical/table@9891

@lexical/tailwind

npm i https://pkg.pr.new/@lexical/tailwind@9891

@lexical/text

npm i https://pkg.pr.new/@lexical/text@9891

@lexical/utils

npm i https://pkg.pr.new/@lexical/utils@9891

@lexical/y

npm i https://pkg.pr.new/@lexical/y@9891

@lexical/yjs

npm i https://pkg.pr.new/@lexical/yjs@9891

commit: 8920efb

@rootnode-bot rootnode-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks, this looks good. The new yCollab / createYBinding entry points are additive, the existing YBinding and createYCollabConfig keep working, and the earlier points (backticks in the invariant messages, the first read-only cause, undo commands in createYBinding, the changeset wording) are addressed at this head. Everything below is optional and non-blocking.

  1. No way to say "no migrating server". packages/lexical-y/src/internal/Collab.ts:110 and :202 always pass createYServerLayoutUpgrade(). createYCollabConfig made this an explicit choice (null or a handle), which packages/lexical-y/DECISIONS.md D03 asks for (choices are explicit). An app without a migrating server, like the playground (see the comment at packages/lexical-playground/src/collaborationY.tsx:184), now gets an 'older' warning saying its server upgrades such documents. An optional layoutUpgrade on YCollabOptions, defaulting to the server handle, would keep the choice expressible.
  2. hasLocalEdits and a cached state applied after binding. packages/lexical-y/src/internal/CollabBinding.ts:416 counts state present when the binding is made, plus this client's writes. A stored update applied later (for example by an asynchronous persistence provider) is a remote transaction, so handshake() reports material: false, and with a stale generation checkYSession answers bootstrap instead of stale. createYDoc(stored) avoids this. A sentence in the handshake / YCollabOptions.doc JSDoc saying that the cached state must be in the document before binding would make it explicit.
  3. pending until markSynced(). The JSDoc on YStatus, YCollabOptions.synced (CollabBinding.ts:188) and createYBinding says so, but a document that holds content, or that receives the provider's state first, is active before markSynced(), as the description notes. Worth saying in the JSDoc.
  4. collab.dispose() on an editor built from extensions (nit). It disposes the binding, but YCollabExtension's UNDO_COMMAND / REDO_COMMAND handlers stay registered and keep returning true, and collab.status still reads active. Either document that such editors are disposed with editor.dispose(), or have dispose() defer to it.
  5. DECISIONS.md (nit). packages/lexical-y/AGENTS.md asks a pull request that makes new consequential decisions to add their rows. The YCollabBinding name, the hasLocalEdits definition and the built-in server layout upgrade look like candidates.

The collab Y end-to-end specs did not run for this head (the extended matrix was skipped). Since the playground's collab Y setup changed, running pnpm run test-e2e-collab-y-chromium against pnpm run start:y would be a useful extra check.

Automated review (maintainer link)

return configExtension(YCollabExtension, {
captureTimeout: history === false ? null : history.captureTimeout,
doc: options.doc,
layoutUpgrade: createYServerLayoutUpgrade(),

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 (and createYBinding at line 110) always declares a migrating server, and YCollabOptions has no way to say there is none, which createYCollabConfig made an explicit null (packages/lexical-y/DECISIONS.md D03: choices are explicit). A room below the floor is read only either way, but the 'older' warning then says the app's server upgrades such documents. The playground, whose relay does not migrate, now does this (see the comment at packages/lexical-playground/src/collaborationY.tsx:184). An optional layoutUpgrade on YCollabOptions, defaulting to this handle, would keep the choice expressible.

);
return createYHandshake({
generation,
hasLocalEdits: this.hadState || this.wroteLocally,

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: this counts state present at bind plus this client's writes. A stored update applied after the binding is made (for example by an asynchronous persistence provider) is a remote transaction, so it counts as neither, and handshake(null) reports material: false for a document that holds a cached edit. With a stale generation, checkYSession then answers bootstrap (Server.ts:479-488) and the edit is never handed off. createYDoc(stored) avoids this; a sentence in the handshake / YCollabOptions.doc JSDoc saying the cached state must be in the document before binding would make it explicit.

* Whether the document already holds what the server has (headless, tests,
* a document loaded from storage): `markSynced()` runs at bind. With
* `false`, call `binding.markSynced()` after the provider's first sync;
* until then the editor is not editable.

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: true for an empty document only. A document that holds content when bound, or receives the provider's state before markSynced(), is read at once and is active without markSynced() (as the PR description notes). The same sentence is in YStatus (line 147) and createYBinding's JSDoc (Collab.ts:94-95).

if (this.stop !== null) {
this.stop();
} else {
this.binding.dispose();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: for the extension's collab this disposes the binding, but YCollabExtension.register keeps its UNDO_COMMAND/REDO_COMMAND handlers, which keep returning true (and collab.status still reads active). Consider documenting that an editor built from extensions is disposed with editor.dispose(), or deferring to it here.

…'s setup API from the layout 4 spec (section 11, with 11.8), beside the existing one

## Description

The layout 4 spec's public API, added as new exports while the existing `YBinding`, `createYCollabConfig` and their options keep working unchanged:

- `createYBinding(editor, options)` (an editor built without extensions) and `yCollab(options)` (YCollabExtension configured for `dependencies`, whose output gains `collab`) take `YCollabOptions`: `doc`, `schemaVersion`, and the two choices an app must make, **`synced`** and **`onStatus`**, required; `root` and `history` optional with their defaults in the exported **`Y_DEFAULTS`**.
- Both return a `YCollabBinding`: `status` (`pending`, `active`, or `readonly` with a `YReadOnlyCause`: `layout-older`, `layout-newer`, `schema-newer`, `requires-feature`, `malformed-record`, `unknown-node-types`, `storage-mismatch`, `unsupported-doc`, `unreadable-content`, `unstorable-change`, `frozen`), `markSynced()`, **`handshake(generation)`** (fills `hasLocalEdits` and this release's layouts and features), `history` (`YHistory`), `getCursor()`, `freeze()` (stops reading and writing and returns `YLocalEdits`: `state`, `json`, `text`, `differs`), `dispose()`.
- A generation token type, `YGeneration`, read with `parseYGeneration`, and `createYHandshake` for servers and tests.
- `describeUnsyncedEdits` on `@lexical/y/headless` (11.6): block-by-block report of a frozen client's edits against their base and the current document.
- Errors that name the fix: a missing `synced`, `onStatus`, `doc` or `schemaVersion`, a `root` or `history` of the wrong shape, or a string that is not a generation, throws with the call to make.
- A "start here" JSDoc on both entry points, and one canonical setup example used identically in the package README, y.md (both now open with the same five steps) and the playground; a test keeps the three identical.

Review fixes in this revision: the error messages carry no backticks (the build's error-code transform rejects them); when two read-only causes arrive together, `status.cause` keeps the first (a newer schema before what its content does); `createYBinding` handles `UNDO_COMMAND` and `REDO_COMMAND` as the extension does; the binding notes this client's writes after each transaction rather than through an `update` listener, so a document with no provider is not made to encode updates, and the extension starts listening only when it starts the binding. The removal of the earlier entry points (`createYCollabConfig`, `YCollabInput`, `createYServerLayoutUpgrade` from the main entry, `YClientSession`, ...) is a separate draft pull request, held until apps have moved to this API.

## Test plan

### Before

```
$ pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/PublicApi.test.ts   # this PR's test on main
 FAIL  |unit| packages/lexical-y/src/__tests__/unit/PublicApi.test.ts > createYBinding > starts pending, not editable, until markSynced; a second markSynced does nothing
TypeError: createYBinding is not a function
 Test Files  1 failed (1)
      Tests  28 failed (28)
```

### After

```
$ pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/PublicApi.test.ts
      Tests  29 passed (29)
$ pnpm exec vitest run --project unit packages/lexical-y packages/lexical-playground
      Tests  25031 passed | 375 expected fail | 154 skipped (25560)
$ pnpm exec vitest run --project scripts-unit
      Tests  3676 passed (3676)
$ pnpm run flow
Found 0 errors
$ pnpm exec tsc -p tsconfig.json --noEmit   # 0 errors
$ pnpm run build   # passes (and build-release)
```

@rootnode-bot rootnode-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This looks good: yCollab and createYBinding are added beside YBinding and createYCollabConfig without changing them, the types match in TypeScript and Flow, and the new tests cover the setup errors, status causes, handshake, history and freeze(). Thanks for addressing the earlier points (backticks in the invariant messages, the first read-only cause winning, undo/redo in createYBinding, and keeping createYCollabConfig while the removal waits in its own pull request). Everything below is optional and non-blocking.

Optional suggestions

  1. hasLocalEdits and a cache loaded after binding (packages/lexical-y/src/internal/CollabBinding.ts:415). handshake() counts state present at bind plus this client's own writes. A persistence provider that loads its cache asynchronously applies it as a remote transaction after the binding exists, so handshake(...) reports material: false for a document that holds a cached edit. With a stale generation, checkYSession then answers bootstrap rather than stale (Server.ts:479-488), and the cached edit is not handed off, although createYHandshake documents the flag as covering "a cached state the server lacks". A sentence in the handshake and YCollabOptions.doc JSDoc, and in Start here step 1, saying a cache must be loaded with createYDoc(stored) before binding would keep apps on the safe path.
  2. layoutUpgrade: null cannot be expressed (packages/lexical-y/src/internal/Collab.ts:110, :202). Both entry points always declare a server layout upgrade, while packages/lexical-y/DECISIONS.md D03 asks for explicit choices that may be null. A below-floor room is read only either way, but an app on a plain relay gets a warning saying its server upgrades such documents. An optional layoutUpgrade on YCollabOptions, defaulting to the handle, would keep the choice. The playground can keep its own today by adding layoutUpgrade: null to its existing configExtension(YCollabExtension, {selfCheck}) call (inline suggestion), which also makes the comment above it accurate. Start here step 3 could say the handshake is for servers that check sessions with checkYSession, since a plain relay has nowhere to receive it.
  3. Catalog coverage. packages/lexical-y/src/__tests__/catalog/README.md asks that a new public export or option land with its catalog entry and a matrix test in unit/matrix/, and packages/lexical-y/AGENTS.md says to test the input space. Adding createYBinding and yCollab as bind modes in catalog/bind.ts (and to bound in catalog/configs.ts) would run the existing matrix contracts through them.
  4. A DECISIONS.md row. packages/lexical-y/AGENTS.md asks a pull request that makes a new consequential decision to add its row in the same pull request. The description's "Where the spec is underspecified" list (the YCollabBinding name, what counts as hasLocalEdits, documents with content going active before markSynced(), the always-declared layout upgrade) would fit there.

Nits

  • YStatus's and createYBinding's JSDoc say the binding is pending until markSynced(); that holds for an empty document only, as the description notes.
  • collab.dispose() on the extension's output disposes the binding but leaves YCollabExtension's undo/redo handlers returning true and collab.status reading active; the JSDoc could say that an editor built from extensions is disposed with editor.dispose().
  • Without a base, describeUnsyncedEdits reports a block a collaborator added since as deleted (inline comment on packages/lexical-y/src/headless.ts).
  • The cause-precedence test in PublicApi.test.ts reaches YBinding's private warn; a document that brings both causes would also pin the order in which they are emitted.

Automated review (maintainer link)

});
}
}
for (const [id, was] of againstById) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: without a base, against is the current document (line 498), so a block a collaborator added after the user went stale is reported here as change: 'deleted' with yours: null (with edits [b1] and current [b1, b9], the report holds ['b9', 'deleted']). A recovery view would list it as the user's deletion. Skipping deletions when base is null, or reporting them with a separate change value, would avoid that.

// 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,

@zurfyx
zurfyx added this pull request to the merge queue Nov 13, 2026
Merged via the queue into main with commit 730f18e Nov 13, 2026
57 checks passed

This branch was successfully deployed

2 active deployments
Preview – lexical — 8920efb6 Deployed Nov 13, 2026 by vercel[bot]
Preview – lexical-playground — 8920efb6 Deployed Nov 13, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant