From 60508cbe46e62a9fd65cbbe2259fc9c36dc43b99 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 15 Jul 2026 16:20:21 -0500 Subject: [PATCH] Add workspace client adapters --- CHANGELOG.md | 1 + ROADMAP.md | 4 +-- src/workspace.ts | 82 +++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 74 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed9d48a..202320f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Recorded the active development direction: use the local HotView/hot-reload setup for iterative app work and defer web/local builds and hosted deployment unless explicitly requested. - Started Phase 4 shared app core cleanup by adding an explicit `StudyDataClient` contract and browser/Tauri data adapters behind the existing study client API. +- Added explicit workspace/auth client contracts so local HotView storage and hosted workspace persistence sit behind the same app-core boundary. ## 0.1.31 - 2026-07-15 diff --git a/ROADMAP.md b/ROADMAP.md index 630a222..c5a26e5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -118,12 +118,12 @@ Definition of done: Goal: stop treating local preview, hosted web, and Tauri as separate code paths. -Current foundation note: `src/studyDataClient.ts` now defines the app-level resource/query contract, and `src/studyClient.ts` routes existing UI calls through browser and Tauri adapters. The browser adapter still owns the HotView JSON/JSONL package loading path, but the UI no longer needs to know which adapter is active. +Current foundation note: `src/studyDataClient.ts` now defines the app-level resource/query contract, and `src/studyClient.ts` routes existing UI calls through browser and Tauri adapters. `src/workspace.ts` also defines workspace/auth client contracts so local HotView storage and hosted persistence share the same app-facing boundary. The browser adapter still owns the HotView JSON/JSONL package loading path, but the UI no longer needs to know which adapter is active. Tasks: - Extract a client/service interface for translations, pages, Strong's links, notes, commentaries, dictionaries, maps, audio, sessions, settings, and timelines. Started with `StudyDataClient` for resource/query behavior. -- Keep UI components pointed at that interface instead of hardcoding browser static paths or direct Tauri commands. +- Keep UI components pointed at that interface instead of hardcoding browser static paths or direct Tauri commands. Started for resource/query and workspace persistence calls. - Define the session schema once: active translation, page anchor, selected verse, enabled resources, visual settings, notes, timeline, and exports. - Add a migration path from `localStorage` sessions to hosted persistence and Tauri SQLite persistence. diff --git a/src/workspace.ts b/src/workspace.ts index f794d08..4c8b9ff 100644 --- a/src/workspace.ts +++ b/src/workspace.ts @@ -75,6 +75,18 @@ export type HostedSession = { user: HostedUser | null } +export type WorkspaceClient = { + loadSnapshot(): Promise | null> + saveSnapshot(snapshot: WorkspaceSnapshot): Promise +} + +export type AuthClient = { + enabled: boolean + loadSession(): Promise + login(username: string, password: string): Promise + logout(): Promise +} + export function hostedWorkspaceEnabled() { return remoteWorkspaceEnabled } @@ -159,9 +171,7 @@ export function selectActiveSessionId(sessions: StudySession[], requestedId?: st } export async function loadWorkspaceSnapshot(): Promise { - const remoteSnapshot = remoteWorkspaceEnabled ? await loadRemoteWorkspaceSnapshot() : null - const localSnapshot = remoteSnapshot ? null : loadLocalWorkspaceSnapshot() - const source = remoteSnapshot ?? localSnapshot + const source = await getWorkspaceClient().loadSnapshot() const sessions = normalizeSessions(source?.sessions) const activeSessionId = selectActiveSessionId(sessions, source?.activeSessionId) @@ -173,9 +183,7 @@ export async function loadWorkspaceSnapshot(): Promise { } export function saveWorkspaceSnapshot(snapshot: WorkspaceSnapshot): Promise { - saveLocalWorkspaceSnapshot(snapshot) - if (!remoteWorkspaceEnabled) return Promise.resolve() - return saveRemoteWorkspaceSnapshot(snapshot) + return getWorkspaceClient().saveSnapshot(snapshot) } function loadLocalWorkspaceSnapshot(): Partial { @@ -234,8 +242,49 @@ async function saveRemoteWorkspaceSnapshot(snapshot: WorkspaceSnapshot) { } } -export async function loadHostedSession(): Promise { - if (!remoteWorkspaceEnabled) return { authenticated: false, user: null } +const localWorkspaceClient: WorkspaceClient = { + loadSnapshot: async () => loadLocalWorkspaceSnapshot(), + saveSnapshot: async (snapshot) => { + saveLocalWorkspaceSnapshot(snapshot) + }, +} + +const hostedWorkspaceClient: WorkspaceClient = { + loadSnapshot: async () => { + const remoteSnapshot = await loadRemoteWorkspaceSnapshot() + return remoteSnapshot ?? loadLocalWorkspaceSnapshot() + }, + saveSnapshot: async (snapshot) => { + saveLocalWorkspaceSnapshot(snapshot) + await saveRemoteWorkspaceSnapshot(snapshot) + }, +} + +const inactiveAuthClient: AuthClient = { + enabled: false, + loadSession: async () => ({ authenticated: false, user: null }), + login: async () => { + throw new Error('Hosted login is not enabled in this workspace mode.') + }, + logout: async () => undefined, +} + +const hostedAuthClient: AuthClient = { + enabled: true, + loadSession: loadRemoteHostedSession, + login: loginRemoteHostedWorkspace, + logout: logoutRemoteHostedWorkspace, +} + +export function getWorkspaceClient(): WorkspaceClient { + return remoteWorkspaceEnabled ? hostedWorkspaceClient : localWorkspaceClient +} + +export function getAuthClient(): AuthClient { + return remoteWorkspaceEnabled ? hostedAuthClient : inactiveAuthClient +} + +async function loadRemoteHostedSession(): Promise { const response = await fetch(`${remoteWorkspaceApiBase}/session`, { credentials: 'include', }) @@ -245,7 +294,7 @@ export async function loadHostedSession(): Promise { return response.json() as Promise } -export async function loginHostedWorkspace(username: string, password: string): Promise { +async function loginRemoteHostedWorkspace(username: string, password: string): Promise { const response = await fetch(`${remoteWorkspaceApiBase}/login`, { method: 'POST', credentials: 'include', @@ -260,8 +309,7 @@ export async function loginHostedWorkspace(username: string, password: string): return response.json() as Promise } -export async function logoutHostedWorkspace(): Promise { - if (!remoteWorkspaceEnabled) return +async function logoutRemoteHostedWorkspace(): Promise { const response = await fetch(`${remoteWorkspaceApiBase}/logout`, { method: 'POST', credentials: 'include', @@ -270,3 +318,15 @@ export async function logoutHostedWorkspace(): Promise { throw new Error(`Unable to log out: ${response.status} ${response.statusText}`) } } + +export function loadHostedSession(): Promise { + return getAuthClient().loadSession() +} + +export function loginHostedWorkspace(username: string, password: string): Promise { + return getAuthClient().login(username, password) +} + +export function logoutHostedWorkspace(): Promise { + return getAuthClient().logout() +}