mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
refactor(core): remove platform coupling — StorageAdapter, sonner, barrel cleanup
P0: Replace all localStorage calls in packages/core with StorageAdapter - Create StorageAdapter interface (getItem/setItem/removeItem) - Auth store factory now requires storage parameter - Workspace store factory accepts optional storage parameter - WSProvider accepts storage prop for token retrieval - apps/web/platform/ passes localStorage as the web implementation P1: Remove sonner UI dependency from packages/core - Replace toast.error() in workspace store with onError callback - Move sonner import to apps/web/platform/workspace.ts - Remove sonner from packages/core/package.json dependencies P2: Delete 5 pure re-export barrel files in apps/web/features/ - features/issues/index.ts, modals/index.ts, navigation/index.ts, workspace/index.ts, inbox/index.ts — all had zero consumers - features/ now only contains auth/ (web-only cookie + initializer) and landing/ (web-only pages) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
// Inbox server state is managed by TanStack Query.
|
||||
// See core/inbox/ for queries, mutations, and WS updaters.
|
||||
export {
|
||||
inboxKeys,
|
||||
inboxListOptions,
|
||||
deduplicateInboxItems,
|
||||
useMarkInboxRead,
|
||||
useArchiveInbox,
|
||||
useMarkAllInboxRead,
|
||||
useArchiveAllInbox,
|
||||
useArchiveAllReadInbox,
|
||||
useArchiveCompletedInbox,
|
||||
} from "@multica/core/inbox";
|
||||
@@ -1,5 +0,0 @@
|
||||
export { useIssueStore } from "@multica/core/issues";
|
||||
export { useIssueViewStore, createIssueViewStore } from "@multica/core/issues/stores/view-store";
|
||||
export { ViewStoreProvider, useViewStore } from "@multica/core/issues/stores/view-store-context";
|
||||
export { StatusIcon, PriorityIcon, StatusPicker, PriorityPicker, AssigneePicker } from "@multica/views/issues/components";
|
||||
export * from "@multica/core/issues/config";
|
||||
@@ -1,2 +0,0 @@
|
||||
export { useModalStore } from "@multica/core/modals";
|
||||
export { ModalRegistry } from "@multica/views/modals/registry";
|
||||
@@ -1 +0,0 @@
|
||||
export { useNavigationStore } from "@multica/core/navigation";
|
||||
@@ -1,3 +0,0 @@
|
||||
export { useWorkspaceStore } from "@/platform/workspace";
|
||||
export { useActorName } from "@multica/core/workspace/hooks";
|
||||
export { WorkspaceAvatar } from "@multica/views/workspace/workspace-avatar";
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
|
||||
export const useAuthStore = createAuthStore({
|
||||
api,
|
||||
storage: localStorage,
|
||||
onLogin: setLoggedInCookie,
|
||||
onLogout: clearLoggedInCookie,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { createWorkspaceStore, registerWorkspaceStore } from "@multica/core/workspace";
|
||||
import { toast } from "sonner";
|
||||
import { api } from "./api";
|
||||
|
||||
export const useWorkspaceStore = createWorkspaceStore(api);
|
||||
export const useWorkspaceStore = createWorkspaceStore(api, {
|
||||
storage: localStorage,
|
||||
onError: (msg) => toast.error(msg),
|
||||
});
|
||||
|
||||
registerWorkspaceStore(useWorkspaceStore);
|
||||
|
||||
@@ -13,6 +13,7 @@ export function WebWSProvider({ children }: { children: React.ReactNode }) {
|
||||
wsUrl={WS_URL}
|
||||
authStore={useAuthStore}
|
||||
workspaceStore={useWorkspaceStore}
|
||||
storage={localStorage}
|
||||
onToast={(message, type) => {
|
||||
if (type === "error") toast.error(message);
|
||||
else toast.info(message);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { create } from "zustand";
|
||||
import type { User } from "../types";
|
||||
import type { User, StorageAdapter } from "../types";
|
||||
import type { ApiClient } from "../api/client";
|
||||
|
||||
export interface AuthStoreOptions {
|
||||
api: ApiClient;
|
||||
storage: StorageAdapter;
|
||||
onLogin?: () => void;
|
||||
onLogout?: () => void;
|
||||
}
|
||||
@@ -21,14 +22,14 @@ export interface AuthState {
|
||||
}
|
||||
|
||||
export function createAuthStore(options: AuthStoreOptions) {
|
||||
const { api, onLogin, onLogout } = options;
|
||||
const { api, storage, onLogin, onLogout } = options;
|
||||
|
||||
return create<AuthState>((set) => ({
|
||||
user: null,
|
||||
isLoading: true,
|
||||
|
||||
initialize: async () => {
|
||||
const token = localStorage.getItem("multica_token");
|
||||
const token = storage.getItem("multica_token");
|
||||
if (!token) {
|
||||
set({ isLoading: false });
|
||||
return;
|
||||
@@ -42,7 +43,7 @@ export function createAuthStore(options: AuthStoreOptions) {
|
||||
} catch {
|
||||
api.setToken(null);
|
||||
api.setWorkspaceId(null);
|
||||
localStorage.removeItem("multica_token");
|
||||
storage.removeItem("multica_token");
|
||||
set({ user: null, isLoading: false });
|
||||
}
|
||||
},
|
||||
@@ -53,7 +54,7 @@ export function createAuthStore(options: AuthStoreOptions) {
|
||||
|
||||
verifyCode: async (email: string, code: string) => {
|
||||
const { token, user } = await api.verifyCode(email, code);
|
||||
localStorage.setItem("multica_token", token);
|
||||
storage.setItem("multica_token", token);
|
||||
api.setToken(token);
|
||||
onLogin?.();
|
||||
set({ user });
|
||||
@@ -62,7 +63,7 @@ export function createAuthStore(options: AuthStoreOptions) {
|
||||
|
||||
loginWithGoogle: async (code: string, redirectUri: string) => {
|
||||
const { token, user } = await api.googleLogin(code, redirectUri);
|
||||
localStorage.setItem("multica_token", token);
|
||||
storage.setItem("multica_token", token);
|
||||
api.setToken(token);
|
||||
onLogin?.();
|
||||
set({ user });
|
||||
@@ -70,7 +71,7 @@ export function createAuthStore(options: AuthStoreOptions) {
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
localStorage.removeItem("multica_token");
|
||||
storage.removeItem("multica_token");
|
||||
api.setToken(null);
|
||||
api.setWorkspaceId(null);
|
||||
onLogout?.();
|
||||
|
||||
@@ -45,8 +45,7 @@
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "catalog:",
|
||||
"@tanstack/react-query-devtools": "^5.96.2",
|
||||
"zustand": "catalog:",
|
||||
"sonner": "^2.0.0"
|
||||
"zustand": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "catalog:"
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { WSClient } from "../api/ws-client";
|
||||
import type { WSEventType } from "../types";
|
||||
import type { WSEventType, StorageAdapter } from "../types";
|
||||
import type { StoreApi, UseBoundStore } from "zustand";
|
||||
import type { AuthState } from "../auth/store";
|
||||
import type { WorkspaceStore } from "../workspace/store";
|
||||
@@ -34,6 +34,8 @@ export interface WSProviderProps {
|
||||
authStore: UseBoundStore<StoreApi<AuthState>>;
|
||||
/** Platform-created workspace store instance */
|
||||
workspaceStore: UseBoundStore<StoreApi<WorkspaceStore>>;
|
||||
/** Platform-specific storage adapter for reading auth tokens */
|
||||
storage: StorageAdapter;
|
||||
/** Optional callback for showing toast messages (platform-specific, e.g. sonner) */
|
||||
onToast?: (message: string, type?: "info" | "error") => void;
|
||||
}
|
||||
@@ -43,6 +45,7 @@ export function WSProvider({
|
||||
wsUrl,
|
||||
authStore,
|
||||
workspaceStore,
|
||||
storage,
|
||||
onToast,
|
||||
}: WSProviderProps) {
|
||||
const user = authStore((s) => s.user);
|
||||
@@ -53,7 +56,7 @@ export function WSProvider({
|
||||
useEffect(() => {
|
||||
if (!user || !workspace) return;
|
||||
|
||||
const token = localStorage.getItem("multica_token");
|
||||
const token = storage.getItem("multica_token");
|
||||
if (!token) return;
|
||||
|
||||
const ws = new WSClient(wsUrl, { logger: createLogger("ws") });
|
||||
@@ -67,7 +70,7 @@ export function WSProvider({
|
||||
wsRef.current = null;
|
||||
setWsClient(null);
|
||||
};
|
||||
}, [user, workspace, wsUrl]);
|
||||
}, [user, workspace, wsUrl, storage]);
|
||||
|
||||
const stores: RealtimeSyncStores = { authStore, workspaceStore };
|
||||
|
||||
|
||||
@@ -29,3 +29,4 @@ export type { IssueSubscriber } from "./subscriber";
|
||||
export type * from "./events";
|
||||
export type * from "./api";
|
||||
export type { Attachment } from "./attachment";
|
||||
export type { StorageAdapter } from "./storage";
|
||||
|
||||
5
packages/core/types/storage.ts
Normal file
5
packages/core/types/storage.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface StorageAdapter {
|
||||
getItem(key: string): string | null;
|
||||
setItem(key: string, value: string): void;
|
||||
removeItem(key: string): void;
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
import { create } from "zustand";
|
||||
import { toast } from "sonner";
|
||||
import type { Workspace } from "../types";
|
||||
import type { Workspace, StorageAdapter } from "../types";
|
||||
import type { ApiClient } from "../api/client";
|
||||
import { createLogger } from "../logger";
|
||||
|
||||
const logger = createLogger("workspace-store");
|
||||
|
||||
interface WorkspaceStoreOptions {
|
||||
storage?: StorageAdapter;
|
||||
onError?: (message: string) => void;
|
||||
}
|
||||
|
||||
interface WorkspaceState {
|
||||
workspace: Workspace | null;
|
||||
workspaces: Workspace[];
|
||||
@@ -31,7 +35,10 @@ interface WorkspaceActions {
|
||||
|
||||
export type WorkspaceStore = WorkspaceState & WorkspaceActions;
|
||||
|
||||
export function createWorkspaceStore(api: ApiClient) {
|
||||
export function createWorkspaceStore(api: ApiClient, options?: WorkspaceStoreOptions) {
|
||||
const storage = options?.storage;
|
||||
const onError = options?.onError;
|
||||
|
||||
return create<WorkspaceStore>((set, get) => ({
|
||||
// State
|
||||
workspace: null,
|
||||
@@ -50,13 +57,13 @@ export function createWorkspaceStore(api: ApiClient) {
|
||||
|
||||
if (!nextWorkspace) {
|
||||
api.setWorkspaceId(null);
|
||||
localStorage.removeItem("multica_workspace_id");
|
||||
storage?.removeItem("multica_workspace_id");
|
||||
set({ workspace: null });
|
||||
return null;
|
||||
}
|
||||
|
||||
api.setWorkspaceId(nextWorkspace.id);
|
||||
localStorage.setItem("multica_workspace_id", nextWorkspace.id);
|
||||
storage?.setItem("multica_workspace_id", nextWorkspace.id);
|
||||
set({ workspace: nextWorkspace });
|
||||
logger.debug("hydrate workspace", nextWorkspace.name, nextWorkspace.id);
|
||||
|
||||
@@ -73,7 +80,7 @@ export function createWorkspaceStore(api: ApiClient) {
|
||||
if (!ws) return;
|
||||
|
||||
api.setWorkspaceId(ws.id);
|
||||
localStorage.setItem("multica_workspace_id", ws.id);
|
||||
storage?.setItem("multica_workspace_id", ws.id);
|
||||
|
||||
// All data caches (issues, inbox, members, agents, skills, runtimes)
|
||||
// are managed by TanStack Query, keyed by wsId — auto-refetch on switch.
|
||||
@@ -84,14 +91,14 @@ export function createWorkspaceStore(api: ApiClient) {
|
||||
|
||||
refreshWorkspaces: async () => {
|
||||
const { workspace, hydrateWorkspace } = get();
|
||||
const storedWorkspaceId = localStorage.getItem("multica_workspace_id");
|
||||
const storedWorkspaceId = storage?.getItem("multica_workspace_id") ?? null;
|
||||
try {
|
||||
const wsList = await api.listWorkspaces();
|
||||
hydrateWorkspace(wsList, workspace?.id ?? storedWorkspaceId);
|
||||
return wsList;
|
||||
} catch (e) {
|
||||
logger.error("failed to refresh workspaces", e);
|
||||
toast.error("Failed to refresh workspaces");
|
||||
onError?.("Failed to refresh workspaces");
|
||||
return get().workspaces;
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user