import type { ParsedUri, UriPlugin } from "@suaveplan/storage-uri"; import type { StorageLike } from "@suaveplan/types"; /** Minimal in-memory `StorageLike` for tests. Key shape is opaque. */ export class MemStore implements StorageLike { readonly #map = new Map(); public readOnly = false; public failExists = false; public failList = false; public failGetWith: Error | undefined; public delayGetMs = 0; public seed(key: string, bytes: Uint8Array): void { this.#map.set(key, bytes); } async get(key: string): Promise { if (this.delayGetMs > 0) { await new Promise((r) => setTimeout(r, this.delayGetMs)); } if (this.failGetWith) throw this.failGetWith; return this.#map.get(key); } async set(key: string, value: Uint8Array): Promise { if (this.readOnly) { const err: Error & { code?: string } = new Error("read-only"); err.code = "STORAGE_URI.WRITE_UNSUPPORTED"; throw err; } this.#map.set(key, value); } async delete(key: string): Promise { return this.#map.delete(key); } async exists(key: string): Promise { if (this.failExists) return false; return this.#map.has(key); } async list(opts?: { prefix?: string; limit?: number }): Promise<{ keys: string[]; total: number; hasMore: boolean; continuationToken?: string | undefined; }> { if (this.failList) throw new Error("list-failed"); const prefix = opts?.prefix ?? ""; const keys = [...this.#map.keys()].filter((k) => k.startsWith(prefix)); const limited = opts?.limit ? keys.slice(0, opts.limit) : keys; return { keys: limited, total: keys.length, hasMore: opts?.limit ? keys.length > opts.limit : false, }; } async clear(): Promise { this.#map.clear(); } } /** * UriPlugin wrapping a single `MemStore`. URI shape: * `:///` ⇒ key dispatched as * `/` (the storage-uri resolver passes whatever * `parse(uri).key` returns to the adapter; we use the canonical * URL-recovered shape directly). * * The second-argument record form (`{ : store }`) is preserved * for ergonomic test composition: when the URI's authority matches one of * the registered bucket names, the matching store handles the operation. */ export function memPlugin( scheme: string, stores: MemStore | Record ): UriPlugin { const single = stores instanceof MemStore; function pick(key: string): { store: MemStore; inner: string } { if (single) return { store: stores, inner: key }; const slash = key.indexOf("/"); const bucket = slash >= 0 ? key.slice(0, slash) : key; const store = (stores as Record)[bucket]; if (!store) throw new Error(`unknown bucket: ${bucket}`); return { store, inner: key }; } return { scheme, adapter: { async get(key) { const { store, inner } = pick(key); return store.get(inner); }, async set(key, value) { const { store, inner } = pick(key); return store.set(inner, value); }, async delete(key) { const { store, inner } = pick(key); return store.delete(inner); }, async exists(key) { const { store, inner } = pick(key); return store.exists(inner); }, async list(opts) { const prefix = opts?.prefix ?? ""; if (single) { const inner = await stores.list({ prefix, ...(opts?.limit !== undefined ? { limit: opts.limit } : {}), }); return inner; } const slash = prefix.indexOf("/"); const bucket = slash >= 0 ? prefix.slice(0, slash) : prefix; const store = (stores as Record)[bucket]; if (!store) return { keys: [], total: 0, hasMore: false }; const inner = await store.list({ prefix, ...(opts?.limit !== undefined ? { limit: opts.limit } : {}), }); return inner; }, async clear() { if (single) return stores.clear(); for (const s of Object.values( stores as Record )) { await s.clear(); } }, }, parse(uri): ParsedUri { const m = new URL(uri); const host = m.host; const path = m.pathname.replace(/^\//, ""); const key = path.length > 0 ? `${host}/${path}` : host; return { key }; }, build(key) { const slash = key.indexOf("/"); if (slash < 0) return `${scheme}://${key}`; return `${scheme}://${key.slice(0, slash)}/${key.slice(slash + 1)}`; }, }; }