Skip to content

feat(collab): share-session link + vault-synced collabId (multi-user join)#185

Merged
thetechjon merged 1 commit into
devfrom
feat/collab-join-share-link-and-vault-sync
Jun 12, 2026
Merged

feat(collab): share-session link + vault-synced collabId (multi-user join)#185
thetechjon merged 1 commit into
devfrom
feat/collab-join-share-link-and-vault-sync

Conversation

@thetechjon

Copy link
Copy Markdown
Collaborator

Two complementary ways for a second person to join the same live-collab room. The collab ENGINE (Y.Doc + y-websocket + awareness/cursors) already exists; this PR is purely about how two clients end up in the SAME room. The room = the note's stable collabId UUID.

Feature A — share-session link (works for anyone, no shared repo)

  • A Share button in the status bar (next to the CollabPill), visible only when collab is configured (getConfiguredUrl() non-null) and a note is open. Clicking it: ensures the note has a collabId (ensureCollabId), copies https://<origin>/?collab=<collabId>&title=<url-encoded title> to the clipboard, and toasts "Collaboration link copied. Anyone with the link can edit this note live."
  • On load, ?collab=<id> opens the matching local note, or creates a new EMPTY local note seeded with that collabId (+ the title param). The joiner is seeded empty on purpose: createCollabBinding's seed-on-empty only fires for non-empty local content, so a joiner never pushes its own (empty) body into the room — it receives the room's content over the CRDT wire. The collab/title params are stripped via history.replaceState so a refresh does not re-trigger.
  • Security: the collabId is a v4 UUID (unguessable). The link grants edit access to that one room and leaks nothing else — documented in the button tooltip + the collabShare.ts header.

Feature B — vault-synced collabId (repo-sharers auto-share the room)

  • serializeNote now emits a collabId: YAML frontmatter block, but only for notes that actually carry a collabId; parseNote reads it back into note.collabId. Two people syncing the same GitHub vault therefore converge on the same room per note. Follows the existing conditional-frontmatter convention (extended, not a parallel system).

Sync-safety (how churn is prevented)

  • No mass churn. A note without a collabId serializes byte-for-byte as before (body only), so the overwhelming majority of notes are untouched. Only a note that has gained a collabId re-serializes — a one-time clean metadata update for that one note.
  • Lossless round-trip. No blank line is emitted between the closing --- and the body, so parseNote returns exactly the body bytes and re-serializing reproduces identical bytes (no phantom leading-blank-line drift). Verified by collabIdFrontmatter.test.ts (apply → re-pull settles to unchanged).
  • Clean update, never a conflict. The two-SHA classifier already treats "remote changed, local didn't" as remoteUpdated; canonicalLocalSha now threads the collabId so the baseline matches serializeNote(note) and the note settles to unchanged. When both sides changed but the bodies are identical (only the collabId metadata differs), the classifier short-circuits to a clean remoteUpdated instead of opening a conflict tab.
  • Remote/repo collabId wins so collaborators converge; the local id is kept when the remote carries none (never clobbered with undefined).
  • No frontmatter leak into body. The autoMerged apply path and the conflict-resolution paths re-parse the merged bytes (like the manual-merge path already did), so a collabId: block can never end up inside note.content.
  • The shell background-fill / on-open loader adopts a remote collabId when a cloned note's body streams in, so cloned-vault notes join the right room.

Constraints / gating

  • Collab stays gated on NEXT_PUBLIC_YJS_WS_URL (Preview/beta only). No Vercel/env changes. The Share button and all collab wiring are dormant in production.

Tests

  • collabShare.test.ts — link generation (origin + collabId + encoded title, title omitted when blank, collab-before-title order), param parse, and the create-or-open join decision (joiner note is EMPTY; existing note reused, no duplicate; default title fallback).
  • collabIdFrontmatter.test.ts — serialize/parse round-trip (with and without tags), gaining a collabId classifies as remoteUpdated not conflict + re-pulls unchanged, and remote collabId convergence.
  • collabExtension.test.ts — explicit joiner seed-skip (empty initialContent never seeded, even after sync).
  • Full suite green (3002 passed). npm run lint clean, tsc --noEmit zero errors.
  • npm run e2e:sync ran against the live test repo — all 17 scenarios pass, including the frontmatter round-trip and no-churn / special-char / smart-punctuation scenarios, confirming the serialization change does not break real clone/push/pull/round-trip or introduce churn.

🤖 Generated with Claude Code

…join)

Two complementary ways for a second person to join the same live-collab
room, where room = the note's stable collabId UUID.

Feature A — share-session link (works for anyone, no shared repo):
- A "Share" button in the status bar, visible only when collab is
  configured (getConfiguredUrl() non-null). It mints/reuses the note's
  collabId, copies https://<origin>/?collab=<id>&title=<title> to the
  clipboard, and toasts. The UUID room id is the only credential.
- On load, ?collab=<id> opens the matching local note, or creates an
  EMPTY one seeded with that collabId (joiner receives content over the
  CRDT wire — no local seed), then strips the params via replaceState.

Feature B — vault-synced collabId (repo-sharers auto-share the room):
- serializeNote emits a `collabId:` YAML frontmatter block ONLY for
  notes that carry one; parseNote reads it back. Round-trip is lossless
  (no blank line after the closing ---).

Sync-safety: only collab notes re-serialize, so normal notes see zero
churn (a note that gains a collabId is a one-time clean metadata
update). The two-SHA classifier treats "gained a collabId" as a clean
remoteUpdated, never a conflict; canonicalLocalSha now threads the
collabId so baselines match and a re-pull settles to `unchanged`. A
remote collabId wins so collaborators converge (local id kept when
remote has none). The autoMerged + conflict-resolution apply paths
re-parse merged bytes so frontmatter never leaks into the body.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thetechjon thetechjon merged commit 90950cb into dev Jun 12, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant