151 lines
4.3 KiB
TypeScript
151 lines
4.3 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
type ReactNode,
|
|
useContext,
|
|
useEffect,
|
|
useState,
|
|
} from "react";
|
|
import { SFWorkspaceStore } from "./sf-workspace-store";
|
|
|
|
/**
|
|
* ProjectStoreManager maintains a Map<string, SFWorkspaceStore> of per-project
|
|
* stores with SSE lifecycle management. Only the active project's store keeps its
|
|
* SSE connection open — background stores are disconnected to save resources.
|
|
*
|
|
* Exposes a useSyncExternalStore-compatible interface for React components to
|
|
* reactively read the active project path.
|
|
*/
|
|
export class ProjectStoreManager {
|
|
private stores = new Map<string, SFWorkspaceStore>();
|
|
private activeProjectCwd: string | null = null;
|
|
private listeners = new Set<() => void>();
|
|
|
|
// ─── useSyncExternalStore interface ──────────────────────────────────────
|
|
|
|
subscribe = (listener: () => void): (() => void) => {
|
|
this.listeners.add(listener);
|
|
return () => this.listeners.delete(listener);
|
|
};
|
|
|
|
getSnapshot = (): string | null => this.activeProjectCwd;
|
|
|
|
// ─── Public API ──────────────────────────────────────────────────────────
|
|
|
|
getActiveStore(): SFWorkspaceStore | null {
|
|
if (!this.activeProjectCwd) return null;
|
|
return this.stores.get(this.activeProjectCwd) ?? null;
|
|
}
|
|
|
|
getActiveProjectCwd(): string | null {
|
|
return this.activeProjectCwd;
|
|
}
|
|
|
|
/**
|
|
* Switch to the given project. Disconnects SSE on the previous active store,
|
|
* creates a new store if needed (lazily), reconnects SSE on re-activated stores.
|
|
*/
|
|
switchProject(projectCwd: string): SFWorkspaceStore {
|
|
// Disconnect SSE on current active store
|
|
if (this.activeProjectCwd && this.activeProjectCwd !== projectCwd) {
|
|
const prev = this.stores.get(this.activeProjectCwd);
|
|
if (prev) prev.disconnectSSE();
|
|
}
|
|
|
|
// Get or create store for new project
|
|
let store = this.stores.get(projectCwd);
|
|
if (!store) {
|
|
store = new SFWorkspaceStore(projectCwd);
|
|
this.stores.set(projectCwd, store);
|
|
store.start();
|
|
} else {
|
|
// Reconnect SSE on re-activated store
|
|
store.reconnectSSE();
|
|
}
|
|
|
|
this.activeProjectCwd = projectCwd;
|
|
this.notify();
|
|
return store;
|
|
}
|
|
|
|
/** Dispose all stores and clear manager state. */
|
|
disposeAll(): void {
|
|
for (const store of this.stores.values()) {
|
|
store.dispose();
|
|
}
|
|
this.stores.clear();
|
|
this.activeProjectCwd = null;
|
|
this.notify();
|
|
}
|
|
|
|
/** Close a single project's store and switch to another if it was active. */
|
|
closeProject(projectCwd: string): void {
|
|
const store = this.stores.get(projectCwd);
|
|
if (!store) return;
|
|
|
|
store.dispose();
|
|
this.stores.delete(projectCwd);
|
|
|
|
// If we closed the active project, switch to another or clear
|
|
if (this.activeProjectCwd === projectCwd) {
|
|
const remaining = Array.from(this.stores.keys());
|
|
if (remaining.length > 0) {
|
|
// Switch to the first remaining project
|
|
const next = this.stores.get(remaining[0])!;
|
|
this.activeProjectCwd = remaining[0];
|
|
next.reconnectSSE();
|
|
} else {
|
|
this.activeProjectCwd = null;
|
|
}
|
|
}
|
|
|
|
this.notify();
|
|
}
|
|
|
|
/** Number of active project stores. */
|
|
getProjectCount(): number {
|
|
return this.stores.size;
|
|
}
|
|
|
|
/** Get all active project paths. */
|
|
getActiveProjectPaths(): string[] {
|
|
return Array.from(this.stores.keys());
|
|
}
|
|
|
|
private notify(): void {
|
|
for (const listener of this.listeners) listener();
|
|
}
|
|
}
|
|
|
|
// ─── React Context + Provider + Hook ──────────────────────────────────────
|
|
|
|
export const ProjectStoreManagerContext =
|
|
createContext<ProjectStoreManager | null>(null);
|
|
|
|
export function ProjectStoreManagerProvider({
|
|
children,
|
|
}: {
|
|
children: ReactNode;
|
|
}) {
|
|
const [manager] = useState(() => new ProjectStoreManager());
|
|
|
|
useEffect(() => {
|
|
return () => manager.disposeAll();
|
|
}, [manager]);
|
|
|
|
return (
|
|
<ProjectStoreManagerContext.Provider value={manager}>
|
|
{children}
|
|
</ProjectStoreManagerContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useProjectStoreManager(): ProjectStoreManager {
|
|
const mgr = useContext(ProjectStoreManagerContext);
|
|
if (!mgr)
|
|
throw new Error(
|
|
"useProjectStoreManager must be used within ProjectStoreManagerProvider",
|
|
);
|
|
return mgr;
|
|
}
|