Initial Commit
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
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)}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user