Removed Zustand storage, cleanup up auth storage IO interfaces, implementations, and usage.
This commit is contained in:
parent
512f45d357
commit
3c65bdb4c6
4 changed files with 863 additions and 1008 deletions
|
|
@ -1,32 +0,0 @@
|
|||
/**
|
||||
* File-based storage adapter for Zustand persist middleware.
|
||||
* Stores entire store state as JSON in a single file using atomic I/O.
|
||||
*/
|
||||
|
||||
import { createJSONStorage } from "zustand/middleware";
|
||||
import { readStoreFile, writeStoreFile, deleteStoreFile } from "../store-io.js";
|
||||
|
||||
export interface FileStorageAdapterOptions {
|
||||
/** Full path to the storage file */
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Zustand storage adapter that reads/writes from a file.
|
||||
* Conforms to Zustand's StateStorage interface.
|
||||
*/
|
||||
export function createFileStorageAdapter(
|
||||
options: FileStorageAdapterOptions,
|
||||
): ReturnType<typeof createJSONStorage> {
|
||||
return createJSONStorage(() => ({
|
||||
getItem: async () => readStoreFile(options.filePath),
|
||||
// Do not introduce an `await` before writeStoreFile() here: it registers
|
||||
// the write in pendingWrites synchronously, which is load-bearing for
|
||||
// flushStoreFileWrites() (a microtask hop before registration would make a
|
||||
// flush called right after setItem find an empty map and return early,
|
||||
// silently regressing tests to non-deterministic).
|
||||
setItem: async (_name: string, value: string) =>
|
||||
writeStoreFile(options.filePath, value),
|
||||
removeItem: async () => deleteStoreFile(options.filePath),
|
||||
}));
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
/**
|
||||
* Storage adapters for Zustand persist middleware.
|
||||
* Provides adapters for file, remote HTTP, and browser storage.
|
||||
*/
|
||||
|
||||
export { createFileStorageAdapter } from "./file-storage.js";
|
||||
export type { FileStorageAdapterOptions } from "./file-storage.js";
|
||||
|
||||
export { createRemoteStorageAdapter } from "./remote-storage.js";
|
||||
export type { RemoteStorageAdapterOptions } from "./remote-storage.js";
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
/**
|
||||
* Remote HTTP storage adapter for Zustand persist middleware.
|
||||
* Stores entire store state via HTTP API (GET/POST/DELETE /api/storage/:storeId).
|
||||
*/
|
||||
|
||||
import { createJSONStorage } from "zustand/middleware";
|
||||
|
||||
export interface RemoteStorageAdapterOptions {
|
||||
/** Base URL of the remote server (e.g. http://localhost:3000) */
|
||||
baseUrl: string;
|
||||
/** Store ID (e.g. "oauth", "inspector-settings") */
|
||||
storeId: string;
|
||||
/** Optional auth token for x-mcp-remote-auth header */
|
||||
authToken?: string;
|
||||
/** Fetch function to use (default: globalThis.fetch) */
|
||||
fetchFn?: typeof fetch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Zustand storage adapter that reads/writes via HTTP API.
|
||||
* Conforms to Zustand's StateStorage interface.
|
||||
*/
|
||||
export function createRemoteStorageAdapter(
|
||||
options: RemoteStorageAdapterOptions,
|
||||
): ReturnType<typeof createJSONStorage> {
|
||||
const baseUrl = options.baseUrl.replace(/\/$/, "");
|
||||
const fetchFn = options.fetchFn ?? globalThis.fetch;
|
||||
|
||||
return createJSONStorage(() => ({
|
||||
getItem: async (_name: string) => {
|
||||
const headers: Record<string, string> = {};
|
||||
if (options.authToken) {
|
||||
headers["x-mcp-remote-auth"] = `Bearer ${options.authToken}`;
|
||||
}
|
||||
|
||||
const res = await fetchFn(`${baseUrl}/api/storage/${options.storeId}`, {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status === 404) {
|
||||
return null;
|
||||
}
|
||||
throw new Error(`Failed to read store: ${res.status}`);
|
||||
}
|
||||
|
||||
const store = await res.json();
|
||||
// Zustand stores: { state: {...}, version: number }
|
||||
// API returns the stored blob. If empty, Zustand hasn't initialized yet.
|
||||
if (Object.keys(store).length === 0) {
|
||||
return null; // Empty store means not initialized yet
|
||||
}
|
||||
// Return the stored Zustand format as string
|
||||
return JSON.stringify(store);
|
||||
},
|
||||
setItem: async (_name: string, value: string) => {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (options.authToken) {
|
||||
headers["x-mcp-remote-auth"] = `Bearer ${options.authToken}`;
|
||||
}
|
||||
|
||||
// Zustand gives us the full persisted format as a string
|
||||
// Store it as-is (the API treats it as an opaque blob)
|
||||
const res = await fetchFn(`${baseUrl}/api/storage/${options.storeId}`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: value, // Already a JSON string from Zustand
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to write store: ${res.status}`);
|
||||
}
|
||||
},
|
||||
removeItem: async (_name: string) => {
|
||||
const headers: Record<string, string> = {};
|
||||
if (options.authToken) {
|
||||
headers["x-mcp-remote-auth"] = `Bearer ${options.authToken}`;
|
||||
}
|
||||
|
||||
const res = await fetchFn(`${baseUrl}/api/storage/${options.storeId}`, {
|
||||
method: "DELETE",
|
||||
headers,
|
||||
});
|
||||
|
||||
// 404 is fine (already deleted), but other errors should propagate
|
||||
if (!res.ok && res.status !== 404) {
|
||||
throw new Error(`Failed to delete store: ${res.status}`);
|
||||
}
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Shared storage path resolution, validation, and atomic file I/O.
|
||||
* Used by the file storage adapter and the remote server's /api/storage routes.
|
||||
* Used by OAuth file persistence and the remote server's /api/storage routes.
|
||||
*/
|
||||
|
||||
import * as path from "node:path";
|
||||
|
|
@ -57,15 +57,7 @@ export async function readStoreFile(filePath: string): Promise<string | null> {
|
|||
/**
|
||||
* In-flight writeStoreFile() promises, keyed by resolved path. Lets callers
|
||||
* await persistence completion via flushStoreFileWrites() instead of polling
|
||||
* the file — Zustand's persist middleware invokes writeStoreFile() fire-and-
|
||||
* forget, so the in-memory store updates synchronously while the file write
|
||||
* lags. Entries are removed once their write settles.
|
||||
*
|
||||
* Load-bearing: pendingWrites.set() below runs synchronously before the first
|
||||
* await in writeStoreFile(), so a flushStoreFileWrites() called right after a
|
||||
* persist sees the in-flight entry. Callers (e.g. the storage adapter's
|
||||
* setItem) must not introduce an await before writeStoreFile() — doing so would
|
||||
* let a flush run before registration and return early.
|
||||
* the file. Entries are removed once their write settles.
|
||||
*/
|
||||
const pendingWrites = new Map<string, Promise<void>>();
|
||||
|
||||
|
|
@ -104,9 +96,8 @@ export async function writeStoreFile(
|
|||
|
||||
/**
|
||||
* Await pending writeStoreFile() writes — those for `filePath` if given, else
|
||||
* all of them. Use in tests after triggering persistence (Zustand persist
|
||||
* writes fire-and-forget) instead of polling the file, and for graceful
|
||||
* shutdown. Resolves immediately when nothing is in flight.
|
||||
* all of them. Use in tests after triggering persistence instead of polling
|
||||
* the file, and for graceful shutdown. Resolves immediately when nothing is in flight.
|
||||
*/
|
||||
export async function flushStoreFileWrites(filePath?: string): Promise<void> {
|
||||
if (filePath !== undefined) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue