File size: 3,559 Bytes
94193b5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | /**
* Server-only adapter factory
*
* Creates per-workspace SQLiteAdapter instances cached by workspaceId.
* Each workspace's data lives in data/workspaces/{workspaceId}/osws.sqlite.
*
* Backward compatibility:
* - getSQLiteAdapter() returns the default adapter (data/osws.sqlite)
* Used by public routes (analytics, deployment serving) that don't need workspace context
* - getWorkspaceAdapter(workspaceId) returns a per-workspace adapter
* Used by all authenticated routes
*/
import 'server-only';
import path from 'path';
import { StorageAdapter } from './types';
import { SQLiteAdapter } from './sqlite-adapter';
// Default adapter for backward compatibility and public routes
let defaultAdapter: SQLiteAdapter | null = null;
// Per-workspace adapter cache
const workspaceAdapters = new Map<string, { adapter: SQLiteAdapter; lastAccess: number }>();
// Evict adapters idle for more than 10 minutes
const IDLE_TIMEOUT_MS = 10 * 60 * 1000;
function getDataDir(): string {
return process.env.DATA_DIR || path.join(process.cwd(), 'data');
}
/**
* Validate workspaceId is a UUID to prevent path traversal
*/
function validateWorkspaceId(workspaceId: string): void {
if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(workspaceId)) {
throw new Error(`Invalid workspace ID format: ${workspaceId}`);
}
}
/**
* Get a per-workspace SQLiteAdapter (cached, creates workspace DB if needed)
* This is the primary factory for all authenticated routes.
*
* Special cases:
* - 'default' falls back to the default adapter (data/osws.sqlite)
* Used by legacy single-user admin and desktop (Electron app) modes
*/
export function getWorkspaceAdapter(workspaceId: string): SQLiteAdapter {
// Legacy admin, desktop, and default workspace use the shared database
if (workspaceId === 'default' || workspaceId === 'admin' || workspaceId === 'desktop' || workspaceId === 'instance-api') {
return getSQLiteAdapter();
}
validateWorkspaceId(workspaceId);
const cached = workspaceAdapters.get(workspaceId);
if (cached) {
cached.lastAccess = Date.now();
return cached.adapter;
}
const dbPath = path.join(getDataDir(), 'workspaces', workspaceId, 'osws.sqlite');
const adapter = new SQLiteAdapter(dbPath);
workspaceAdapters.set(workspaceId, { adapter, lastAccess: Date.now() });
return adapter;
}
/**
* Get the default SQLiteAdapter (backward compatible singleton)
* Used by public routes that don't require user context:
* - Analytics tracking/reading
* - Deployment function invocation
* - Admin dashboard (system-wide stats)
*/
export function getSQLiteAdapter(): SQLiteAdapter {
if (!defaultAdapter) {
defaultAdapter = new SQLiteAdapter();
}
return defaultAdapter;
}
/**
* @deprecated Use getWorkspaceAdapter(workspaceId) for authenticated routes
* Kept for backward compatibility during migration
*/
export async function createServerAdapter(): Promise<StorageAdapter> {
return getSQLiteAdapter();
}
// Periodic cleanup of idle adapters (guarded to prevent leaks on hot reload)
const CLEANUP_KEY = '__osw_workspace_cleanup';
if (typeof setInterval !== 'undefined' && !(globalThis as Record<string, unknown>)[CLEANUP_KEY]) {
(globalThis as Record<string, unknown>)[CLEANUP_KEY] = setInterval(() => {
const now = Date.now();
for (const [workspaceId, entry] of workspaceAdapters) {
if (now - entry.lastAccess > IDLE_TIMEOUT_MS) {
entry.adapter.close?.();
workspaceAdapters.delete(workspaceId);
}
}
}, 60_000);
}
|