// Repository for reading and writing tool workspaces. // // Concurrency model: optimistic locking on an integer `version`. A client loads // a workspace (getting its version), edits locally, then saves with that same // version as `baseVersion`. If nobody else saved in between, the row's version // still matches and the write wins (bumping version). If someone else saved // first, the versions differ, no row updates, and we report a conflict so the // client can reload instead of clobbering their teammate. import { query } from './index'; import { defaultStateFor } from '$lib/defaults'; import type { ToolId, WorkspaceEnvelope } from '$lib/types'; interface Row { tool: ToolId; name: string; state: unknown; version: number; updated_at: Date; updated_by: string | null; } function toEnvelope(row: Row): WorkspaceEnvelope { return { tool: row.tool, name: row.name, state: row.state as T, version: row.version, updatedAt: row.updated_at.toISOString(), updatedBy: row.updated_by }; } /** * Load a workspace. If it does not exist yet, seed it from the tool's default * baseline state so the first visitor sees a populated, useful tool. */ export async function getOrCreateWorkspace( tool: ToolId, name: string ): Promise> { const existing = await query( 'SELECT tool, name, state, version, updated_at, updated_by FROM workspaces WHERE tool = $1 AND name = $2', [tool, name] ); if (existing.rows.length > 0) { return toEnvelope(existing.rows[0]); } const seed = defaultStateFor(tool); // ON CONFLICT guards against a race where two first-visitors insert at once. const inserted = await query( `INSERT INTO workspaces (tool, name, state) VALUES ($1, $2, $3) ON CONFLICT (tool, name) DO UPDATE SET tool = EXCLUDED.tool RETURNING tool, name, state, version, updated_at, updated_by`, [tool, name, seed] ); return toEnvelope(inserted.rows[0]); } export type SaveResult = | { ok: true; workspace: WorkspaceEnvelope } | { ok: false; conflict: WorkspaceEnvelope }; /** Save state with optimistic concurrency. */ export async function saveWorkspace( tool: ToolId, name: string, state: T, baseVersion: number, updatedBy: string | null ): Promise> { const updated = await query( `UPDATE workspaces SET state = $3, version = version + 1, updated_at = now(), updated_by = $5 WHERE tool = $1 AND name = $2 AND version = $4 RETURNING tool, name, state, version, updated_at, updated_by`, [tool, name, state, baseVersion, updatedBy] ); if (updated.rows.length === 1) { return { ok: true, workspace: toEnvelope(updated.rows[0]) }; } // No row updated: either the workspace does not exist, or the version moved. // Return the current server copy so the client can reconcile. const current = await getOrCreateWorkspace(tool, name); return { ok: false, conflict: current }; } /** Reset a workspace back to the tool's baseline. */ export async function resetWorkspace( tool: ToolId, name: string ): Promise> { const seed = defaultStateFor(tool); const reset = await query( `INSERT INTO workspaces (tool, name, state) VALUES ($1, $2, $3) ON CONFLICT (tool, name) DO UPDATE SET state = EXCLUDED.state, version = workspaces.version + 1, updated_at = now(), updated_by = NULL RETURNING tool, name, state, version, updated_at, updated_by`, [tool, name, seed] ); return toEnvelope(reset.rows[0]); }