Files
suaveplan/packages/persistence/storage/storage-uri-plugin-fallback/tests/setup/mem-plugin.ts
2026-05-27 23:47:53 +01:00

152 lines
5.3 KiB
TypeScript

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<string, Uint8Array>();
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<Uint8Array | undefined> {
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<void> {
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<boolean> {
return this.#map.delete(key);
}
async exists(key: string): Promise<boolean> {
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<void> {
this.#map.clear();
}
}
/**
* UriPlugin wrapping a single `MemStore`. URI shape:
* `<scheme>://<authority>/<path>` ⇒ key dispatched as
* `<authority>/<path>` (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 (`{ <bucket>: 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<string, MemStore>
): 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<string, MemStore>)[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<string, MemStore>)[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<string, MemStore>
)) {
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)}`;
},
};
}