feat(sync): sync Web and Desktop projects with Host Server SQLite and Main Project Directory

This commit is contained in:
Schwitzz
2026-10-30 16:35:11 -05:00
parent 668470da2b
commit aee9c428fc
7 changed files with 367 additions and 54 deletions
+39 -10
View File
@@ -13,7 +13,7 @@ import type {
import type { StorageAdapter, StoreEvent } from "./storage.js";
export interface ServerClientOptions {
/** Base URL of the PubScript server, e.g. `http://localhost:8080`. */
/** Base URL of the PubScript server, e.g. `http://localhost:8080` or `""` for same-origin. */
baseUrl: string;
/** Provide a custom fetch (e.g. for tests). Defaults to global fetch. */
fetch?: typeof fetch;
@@ -37,6 +37,7 @@ export function createServerClient(opts: ServerClientOptions): StorageAdapter {
if (token) headers.authorization = `Bearer ${token}`;
const res = await fetchImpl(`${opts.baseUrl}${path}`, {
method,
credentials: "include",
headers,
body: body === undefined ? null : JSON.stringify(body),
});
@@ -53,8 +54,9 @@ export function createServerClient(opts: ServerClientOptions): StorageAdapter {
return r.projects;
},
async createProject(p: NewProject): Promise<Project> {
async createProject(p: NewProject & { id?: string }): Promise<Project> {
const r = await req<{ project: Project }>("POST", "/api/projects", {
...(p.id ? { id: p.id } : {}),
title: p.title,
language: p.language,
cover: p.cover,
@@ -67,8 +69,14 @@ export function createServerClient(opts: ServerClientOptions): StorageAdapter {
return r.project;
},
async deleteProject(id: string): Promise<void> {
await req("DELETE", `/api/projects/${id}`);
},
async createBook(b: { projectId: string; title: string }): Promise<Book> {
const r = await req<{ book: Book }>("POST", `/api/projects/${b.projectId}/books`, { title: b.title });
const r = await req<{ book: Book }>("POST", `/api/projects/${b.projectId}/books`, {
title: b.title,
});
return r.book;
},
@@ -86,6 +94,9 @@ export function createServerClient(opts: ServerClientOptions): StorageAdapter {
await req("POST", `/api/chapters/${doc.id}/autosave`, {
rev: doc.rev,
doc: doc.doc,
title: doc.title,
projectId: doc.projectId,
bookId: doc.bookId,
});
},
@@ -98,23 +109,43 @@ export function createServerClient(opts: ServerClientOptions): StorageAdapter {
}
},
async deleteChapter(id: string): Promise<void> {
await req("DELETE", `/api/chapters/${id}`);
},
async saveEntity(e: Entity): Promise<void> {
if ("fields" in e) {
// WorldItem — the server nests the world kind inside `fields`.
await req("POST", `/api/projects/${e.projectId}/entities`, {
id: e.id, kind: "world", name: e.name, fields: { kind: e.kind, ...e.fields },
id: e.id,
kind: "world",
name: e.name,
fields: { kind: e.kind, ...e.fields },
});
} else if ("body" in e) {
// Note
await req("POST", `/api/projects/${e.projectId}/entities`, {
id: e.id, kind: "note", body: e.body, pinned: e.pinned, tags: e.tags, linkedTo: e.linkedTo,
id: e.id,
kind: "note",
body: e.body,
pinned: e.pinned,
tags: e.tags,
linkedTo: e.linkedTo,
});
} else {
// Character
await req("POST", `/api/projects/${e.projectId}/entities`, {
id: e.id, kind: "character", name: e.name, role: e.role, bio: e.bio,
appearance: e.appearance, voice: e.voice, relationships: e.relationships,
arc: e.arc, status: e.status, tags: e.tags,
id: e.id,
kind: "character",
name: e.name,
role: e.role,
bio: e.bio,
appearance: e.appearance,
voice: e.voice,
relationships: e.relationships,
arc: e.arc,
status: e.status,
tags: e.tags,
});
}
},
@@ -212,8 +243,6 @@ export function createServerClient(opts: ServerClientOptions): StorageAdapter {
},
onEvent(_cb: (e: StoreEvent) => void): () => void {
// Live updates arrive over the `/api/ws` socket; wiring that up is the
// app's concern. For the contract, return a no-op unsubscribe.
return () => {};
},
};