feat: realtime collaboration PoC (codemirror + tiptap)#447
Closed
dschmidt wants to merge 5 commits into
Closed
Conversation
Yjs/Hocuspocus-based collaborative editing for OpenCloud as a generic wrapper, validated with two apps over markdown (CodeMirror, Tiptap). - Hocuspocus sidecar: token validation via Graph /me, effective ACL via Graph /permissions allowedValues (same Reva PermissionSet that backs WebDAV oc:permissions, so owner/share/space-role are merged). Anti-spoof identity stamp on awareness updates. - CollaborativeWrapper: provider lifecycle, empty-user awareness bootstrap, etag save loop without soft-lock (412 -> HEAD probe -> retry), hydration election by lowest awareness clientId. - Adapter contract decouples wrapper from editor: hasContent, hydrate, serialize. CodeMirror and Tiptap apps each provide one small adapter against the same wrapper. - E2E Playwright suites per app, integration suite for two-peer Yjs sync via the sidecar.
d7bb5a3 to
449f08f
Compare
PoC e2e + integration suites now pass green (codemirror 5/5, tiptap 4/4,
integration 8/8) against OC 6.2.0.
- web-pkg / web-client: ^3.0.0 → ^7.0.0 in both packages (mismatched
major rejected by Module Federation as "Shared module must be provided
by host"; all other apps in the repo were already on ^7)
- drop unused `storeToRefs` import (pinia named export is not federated
in dev builds and broke the codemirror vite build); read serverUrl
directly off the store inside the computed instead
- tiptap deps: ^2.10 → ^3.20.4 to align with what web-pkg's editor
already pulls in transitively (avoids parallel v2+v3 in the store and
the tiptap-markdown → @tiptap/pm/model resolution failure)
- replace community `tiptap-markdown` with the official `@tiptap/markdown`
(same package web's editor uses); adapter switches to
`setContent(md, { contentType: 'markdown' })` + `editor.getMarkdown()`
- drop `@tiptap/extension-collaboration-cursor@3.0.0`: it still imports
`yCursorPlugin` from upstream `y-prosemirror`, while
`@tiptap/extension-collaboration@3` has migrated to Tiptap's own
`@tiptap/y-tiptap` fork — the two ship distinct `ySyncPluginKey`
instances, so the cursor extension cannot find the sync state and
throws "Cannot read properties of undefined (reading 'doc')" on init.
A 12-line in-place Extension wires `@tiptap/y-tiptap`'s `yCursorPlugin`
directly with a `cursorBuilder` that emits the same
`.collaboration-cursor__caret` / `__label` DOM the consumer CSS and
the e2e test expect.
- vite.config: dedupe `yjs` / `y-prosemirror` / `y-protocols` in the
tiptap bundle so duplicated instances don't break `instanceof` checks
- CollaborativeWrapper: pass `provider` through to the editor component
alongside `awareness` (the cursor wiring needs the provider for the
underlying awareness; codemirror ignores the extra prop)
Drop the wrapper's own top-bar + Save button + 60s save loop + beforeUnload hook. Declaring `update:currentContent` on the app entry component is enough for AppWrapper to flip into `isEditor` mode, render the standard Save action in the topbar, bind Ctrl+S, arm the unsaved-changes route-leave modal, and run its own auto-save loop. Avoids the prior parallel-chrome look and lets the collab wrapper focus on the Y.Doc / etag / lifecycle concerns that AppWrapper has no view of. - CollaborativeWrapper now emits debounced `update:currentContent` from Y.Doc updates (skipping our own internal-origin transactions so a fresh hydrate doesn't echo the loaded content back as a fake user edit) - watch on `props.resource.etag` mirrors AppWrapper's post-save etag into `_oc_meta.etag` so the sidecar's next stale-state probe sees the current authoritative tag - compact status strip (connection state + lifecycle errors) is all that's left of the wrapper's chrome; save / dirty / etag UX comes from AppWrapper - App.vue declares the emit and forwards it from the wrapper - save-back e2e drives Ctrl+S instead of the removed `collab-save` button and asserts persistence by polling WebDAV (no client-side "saving done" signal to latch on to)
…unset The wrapper now branches on a new `realtimeUrl` prop: - set: collab-mode as before (Hocuspocus provider, awareness from provider, hydration waits for `onSynced`) - unset / null: local-mode — still a `Y.Doc` + `Awareness` pair so editor bindings (CodeMirror's yCollab, Tiptap's collaboration plugin) stay on a single codepath; hydration runs immediately from `currentContent`, no network, no peers, status reads `local` `onProviderSynced` and `recoverFromStaleState` now take an `Awareness` parameter directly so they're provider-tolerant; `lockForReload` accepts a null provider. The standalone `Awareness` in local-mode degenerates the hydration election to "we win unconditionally" (no peers in the room), which is the desired behaviour. `TiptapEditor` makes `provider` optional and only registers `CollaborationCursor` when a provider is present — no peers to render in local mode means no cursor extension to wire up. `CodeMirrorEditor` needs no change (its `yCollab` binding only ever needed `awareness`). App.vue forwards `applicationConfig?.realtimeUrl` from the app config into the wrapper, so the dev compose's `opencloud.apps.yaml` is the single source of truth for whether collab is wired up. The previous auto-derivation from `configStore.serverUrl + /realtime` is gone — apps that want collab now opt in via config. `y-protocols` lifted from transitive to explicit dependency in both package.json files to anchor the `Awareness` import. Dev-mode config gains realtimeUrl entries for codemirror and tiptap so the e2e suites keep exercising the collab path.
… suite
The wrapper's `watchEffect((onCleanup) => { ... })` re-ran whenever any
tracked prop changed inside the body — including `props.resource`
mutations from AppWrapper's post-save `resourcesStore.upsertResource`.
Each save would tear down and rebuild the Y.Doc, dropping any in-flight
peer edits. The shared-file e2e didn't catch it because the two peers'
saves are far apart in test time and the wrapper re-hydrates cleanly
from `currentContent`.
Replace with `watch(sessionKey, ..., { immediate: true })` where
sessionKey = `${documentName}::${realtimeUrl ?? 'local'}`. Vue's
computed equality check guarantees an identity-preserving resource
update (same id, different etag) leaves the watch dormant — the Y.Doc
and provider survive.
Adds the first wrapper unit suite (vitest + happy-dom + @vue/test-utils),
13 specs covering both modes (local + collab), the debounced
`update:currentContent` emit, the `_oc_meta.etag` mirror, the cleanup
contract, and an explicit regression test for the rebuild bug above.
- `HocuspocusProvider` is mocked via a class returned from `vi.hoisted`
so `new HocuspocusProvider(...)` inside the wrapper finds a real
constructable, and the test can fish out the instance to trigger
`onSynced` / `onAuthenticationFailed` manually
- `useAuthStore` is mocked to a plain `{ accessToken }` object so the
wrapper doesn't need a real pinia in the test
- The CodeMirror markdown adapter is reused as-is — pure, Y.Text-only,
matches the contract a real adapter would honour
7 tasks
Contributor
Author
|
Superseded by opencloud-eu/web#2561 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
THIS IS A PoC EXPERIMENT. NOT READY FOR MERGING OR PRODUCTION.
To be honest I started this before I saw the standard editor in Web was ported to TipTap already - on the pro side it was easier to experiment with a very simple and basic editor not integrated with anything yet :)
Yjs/Hocuspocus-based collaborative editing for OpenCloud as a generic wrapper, validated with two apps over markdown (CodeMirror, Tiptap).
Related Issue
How Has This Been Tested?
Types of changes