fix(rnotes): resolve global scope for notebook docIds and seeding

The rnotes module has defaultScope: 'global' but several code paths
used the raw space slug instead of the resolved scope:

- subscribeNotebook() built docIds with space slug (e.g. jeff🎶...)
  instead of resolved scope (global🎶...), breaking Automerge sync
- seedDemoIfEmpty() seeded notebooks under space-scoped paths, orphaning
  them when the API queries global-scoped prefix
- onSpaceCreate() created default notebook under space scope

Also migrated 3 orphaned jeff-scoped notebook files to global/ on server.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-09 14:36:38 -07:00
parent f5388ecc2c
commit fe031094a4
2 changed files with 20 additions and 11 deletions

View File

@ -423,8 +423,12 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
// ── Sync (via shared runtime) ── // ── Sync (via shared runtime) ──
private async subscribeNotebook(notebookId: string) { private async subscribeNotebook(notebookId: string) {
this.subscribedDocId = `${this.space}:notes:notebooks:${notebookId}`;
const runtime = (window as any).__rspaceOfflineRuntime; const runtime = (window as any).__rspaceOfflineRuntime;
// Resolve scope: rnotes is globally-scoped, so use 'global' prefix
const dataSpace = runtime?.isInitialized
? (runtime.resolveDocSpace?.('rnotes') || this.space)
: this.space;
this.subscribedDocId = `${dataSpace}:notes:notebooks:${notebookId}`;
if (runtime?.isInitialized) { if (runtime?.isInitialized) {
try { try {

View File

@ -12,6 +12,7 @@ import * as Automerge from "@automerge/automerge";
import { renderShell } from "../../server/shell"; import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule, SpaceLifecycleContext } from "../../shared/module"; import type { RSpaceModule, SpaceLifecycleContext } from "../../shared/module";
import { resolveDataSpace } from "../../shared/scope-resolver";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing"; import { renderLanding } from "./landing";
import { notebookSchema, notebookDocId, connectionsDocId, createNoteItem } from "./schemas"; import { notebookSchema, notebookDocId, connectionsDocId, createNoteItem } from "./schemas";
@ -127,16 +128,19 @@ function findNote(space: string, noteId: string): { docId: string; doc: Notebook
function seedDemoIfEmpty(space: string) { function seedDemoIfEmpty(space: string) {
if (!_syncServer) return; if (!_syncServer) return;
// Resolve effective data space (global for rnotes by default)
const dataSpace = resolveDataSpace("rnotes", space);
// If the space already has notebooks, skip // If the space already has notebooks, skip
if (listNotebooks(space).length > 0) return; if (listNotebooks(dataSpace).length > 0) return;
const now = Date.now(); const now = Date.now();
// Notebook 1: Project Ideas // Notebook 1: Project Ideas
const nb1Id = newId(); const nb1Id = newId();
const nb1DocId = notebookDocId(space, nb1Id); const nb1DocId = notebookDocId(dataSpace, nb1Id);
const nb1Doc = Automerge.change(Automerge.init<NotebookDoc>(), "Seed: Project Ideas", (d) => { const nb1Doc = Automerge.change(Automerge.init<NotebookDoc>(), "Seed: Project Ideas", (d) => {
d.meta = { module: "notes", collection: "notebooks", version: 1, spaceSlug: space, createdAt: now }; d.meta = { module: "notes", collection: "notebooks", version: 1, spaceSlug: dataSpace, createdAt: now };
d.notebook = { id: nb1Id, title: "Project Ideas", slug: "project-ideas", description: "Brainstorms and design notes for the r* ecosystem", coverColor: "#6366f1", isPublic: true, createdAt: now, updatedAt: now }; d.notebook = { id: nb1Id, title: "Project Ideas", slug: "project-ideas", description: "Brainstorms and design notes for the r* ecosystem", coverColor: "#6366f1", isPublic: true, createdAt: now, updatedAt: now };
d.items = {}; d.items = {};
}); });
@ -144,9 +148,9 @@ function seedDemoIfEmpty(space: string) {
// Notebook 2: Meeting Notes // Notebook 2: Meeting Notes
const nb2Id = newId(); const nb2Id = newId();
const nb2DocId = notebookDocId(space, nb2Id); const nb2DocId = notebookDocId(dataSpace, nb2Id);
const nb2Doc = Automerge.change(Automerge.init<NotebookDoc>(), "Seed: Meeting Notes", (d) => { const nb2Doc = Automerge.change(Automerge.init<NotebookDoc>(), "Seed: Meeting Notes", (d) => {
d.meta = { module: "notes", collection: "notebooks", version: 1, spaceSlug: space, createdAt: now }; d.meta = { module: "notes", collection: "notebooks", version: 1, spaceSlug: dataSpace, createdAt: now };
d.notebook = { id: nb2Id, title: "Meeting Notes", slug: "meeting-notes", description: "Weekly standups, design reviews, and retrospectives", coverColor: "#f59e0b", isPublic: true, createdAt: now, updatedAt: now }; d.notebook = { id: nb2Id, title: "Meeting Notes", slug: "meeting-notes", description: "Weekly standups, design reviews, and retrospectives", coverColor: "#f59e0b", isPublic: true, createdAt: now, updatedAt: now };
d.items = {}; d.items = {};
}); });
@ -154,9 +158,9 @@ function seedDemoIfEmpty(space: string) {
// Notebook 3: How-To Guides // Notebook 3: How-To Guides
const nb3Id = newId(); const nb3Id = newId();
const nb3DocId = notebookDocId(space, nb3Id); const nb3DocId = notebookDocId(dataSpace, nb3Id);
const nb3Doc = Automerge.change(Automerge.init<NotebookDoc>(), "Seed: How-To Guides", (d) => { const nb3Doc = Automerge.change(Automerge.init<NotebookDoc>(), "Seed: How-To Guides", (d) => {
d.meta = { module: "notes", collection: "notebooks", version: 1, spaceSlug: space, createdAt: now }; d.meta = { module: "notes", collection: "notebooks", version: 1, spaceSlug: dataSpace, createdAt: now };
d.notebook = { id: nb3Id, title: "How-To Guides", slug: "how-to-guides", description: "Tutorials and onboarding guides for contributors", coverColor: "#10b981", isPublic: true, createdAt: now, updatedAt: now }; d.notebook = { id: nb3Id, title: "How-To Guides", slug: "how-to-guides", description: "Tutorials and onboarding guides for contributors", coverColor: "#10b981", isPublic: true, createdAt: now, updatedAt: now };
d.items = {}; d.items = {};
}); });
@ -1043,9 +1047,10 @@ export const notesModule: RSpaceModule = {
async onSpaceCreate(ctx: SpaceLifecycleContext) { async onSpaceCreate(ctx: SpaceLifecycleContext) {
if (!_syncServer) return; if (!_syncServer) return;
// Create a default "My Notes" notebook doc for the new space // Create a default "My Notes" notebook doc, using resolved scope
const dataSpace = resolveDataSpace("rnotes", ctx.spaceSlug);
const notebookId = "default"; const notebookId = "default";
const docId = notebookDocId(ctx.spaceSlug, notebookId); const docId = notebookDocId(dataSpace, notebookId);
if (_syncServer.getDoc(docId)) return; // already exists if (_syncServer.getDoc(docId)) return; // already exists
@ -1055,7 +1060,7 @@ export const notesModule: RSpaceModule = {
module: "notes", module: "notes",
collection: "notebooks", collection: "notebooks",
version: 1, version: 1,
spaceSlug: ctx.spaceSlug, spaceSlug: dataSpace,
createdAt: Date.now(), createdAt: Date.now(),
}; };
d.notebook = { d.notebook = {