Add workspace client adapters

This commit is contained in:
2026-07-15 16:20:21 -05:00
parent de383596c7
commit 60508cbe46
3 changed files with 74 additions and 13 deletions
+1
View File
@@ -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
+2 -2
View File
@@ -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.
+71 -11
View File
@@ -75,6 +75,18 @@ export type HostedSession = {
user: HostedUser | null
}
export type WorkspaceClient = {
loadSnapshot(): Promise<Partial<WorkspaceSnapshot> | null>
saveSnapshot(snapshot: WorkspaceSnapshot): Promise<void>
}
export type AuthClient = {
enabled: boolean
loadSession(): Promise<HostedSession>
login(username: string, password: string): Promise<HostedSession>
logout(): Promise<void>
}
export function hostedWorkspaceEnabled() {
return remoteWorkspaceEnabled
}
@@ -159,9 +171,7 @@ export function selectActiveSessionId(sessions: StudySession[], requestedId?: st
}
export async function loadWorkspaceSnapshot(): Promise<WorkspaceSnapshot> {
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<WorkspaceSnapshot> {
}
export function saveWorkspaceSnapshot(snapshot: WorkspaceSnapshot): Promise<void> {
saveLocalWorkspaceSnapshot(snapshot)
if (!remoteWorkspaceEnabled) return Promise.resolve()
return saveRemoteWorkspaceSnapshot(snapshot)
return getWorkspaceClient().saveSnapshot(snapshot)
}
function loadLocalWorkspaceSnapshot(): Partial<WorkspaceSnapshot> {
@@ -234,8 +242,49 @@ async function saveRemoteWorkspaceSnapshot(snapshot: WorkspaceSnapshot) {
}
}
export async function loadHostedSession(): Promise<HostedSession> {
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<HostedSession> {
const response = await fetch(`${remoteWorkspaceApiBase}/session`, {
credentials: 'include',
})
@@ -245,7 +294,7 @@ export async function loadHostedSession(): Promise<HostedSession> {
return response.json() as Promise<HostedSession>
}
export async function loginHostedWorkspace(username: string, password: string): Promise<HostedSession> {
async function loginRemoteHostedWorkspace(username: string, password: string): Promise<HostedSession> {
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<HostedSession>
}
export async function logoutHostedWorkspace(): Promise<void> {
if (!remoteWorkspaceEnabled) return
async function logoutRemoteHostedWorkspace(): Promise<void> {
const response = await fetch(`${remoteWorkspaceApiBase}/logout`, {
method: 'POST',
credentials: 'include',
@@ -270,3 +318,15 @@ export async function logoutHostedWorkspace(): Promise<void> {
throw new Error(`Unable to log out: ${response.status} ${response.statusText}`)
}
}
export function loadHostedSession(): Promise<HostedSession> {
return getAuthClient().loadSession()
}
export function loginHostedWorkspace(username: string, password: string): Promise<HostedSession> {
return getAuthClient().login(username, password)
}
export function logoutHostedWorkspace(): Promise<void> {
return getAuthClient().logout()
}