import { type ModuleStorage, type ModuleStorageBody, ModuleStorageError, type ModuleStorageListOptions, type ModuleStorageListing, type ModuleStorageObject, type ModuleStorageObjectMetadata, type ModuleStoragePutOptions, } from "@ekklesia/contracts"; import type { DiscoveredModule, ModuleStorageLookup } from "@ekklesia/core/modules"; import { createModuleStorageLookup, type StorageProvider } from "@ekklesia/core/storage"; /** Builds and drains Web streams without depending on DOM-only helper types, which this package's * TypeScript configuration deliberately does not load. */ function streamOf(bytes: Uint8Array): ReadableStream { return new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); }, }); } async function collect(stream: ReadableStream): Promise { const reader = stream.getReader(); const chunks: Uint8Array[] = []; let total = 0; for (;;) { const { done, value } = await reader.read(); if (done) { break; } chunks.push(value); total += value.byteLength; } const merged = new Uint8Array(total); let offset = 0; for (const chunk of chunks) { merged.set(chunk, offset); offset += chunk.byteLength; } return merged; } interface StoredObject { readonly bytes: Uint8Array; readonly contentType?: string; readonly lastModified: Date; } /** * An in-memory `StorageProvider` for tests that exercise **wiring, scoping, and routing** rather * than the S3 protocol. * * It implements the same interface the real S3 provider does, so everything above the provider seam * — binding resolution, the scoped facade, lifecycle eligibility, route wiring — is exercised * unchanged. What it deliberately does *not* reproduce is the protocol itself: real SeaweedFS * semantics (non-idempotent `CreateBucket`, `NoSuchKey` vs `NoSuchBucket`, ETag quoting, streaming * bodies) are proven against a live server in `core/tests/module-storage.integration.ts`, so this * double can never quietly stand in for a provider guarantee nobody checked. * * It keys everything by **physical** bucket, exactly as the real provider does, which is what lets a * test assert that two modules declaring the same logical name really did land in different * namespaces. */ export function createInMemoryStorageProvider(): StorageProvider & { readonly buckets: ReadonlyMap>; ensureBucketCalls: number; } { const buckets = new Map>(); const state = { ensureBucketCalls: 0 }; const objectsIn = (physicalBucket: string): Map => { const bucket = buckets.get(physicalBucket); if (!bucket) { throw new ModuleStorageError("STORAGE_BUCKET_UNAVAILABLE"); } return bucket; }; const metadataFor = (key: string, stored: StoredObject): ModuleStorageObjectMetadata => Object.freeze({ contentLength: stored.bytes.byteLength, key, lastModified: stored.lastModified, ...(stored.contentType === undefined ? {} : { contentType: stored.contentType }), }); const provider: StorageProvider = { async close(): Promise { // Nothing to release. }, async delete(physicalBucket: string, key: string): Promise { objectsIn(physicalBucket).delete(key); }, async ensureBucket(physicalBucket: string): Promise { state.ensureBucketCalls += 1; if (!buckets.has(physicalBucket)) { buckets.set(physicalBucket, new Map()); } }, async get(physicalBucket: string, key: string): Promise { const stored = objectsIn(physicalBucket).get(key); if (!stored) { throw new ModuleStorageError("STORAGE_OBJECT_NOT_FOUND", key); } return Object.freeze({ ...metadataFor(key, stored), body: streamOf(stored.bytes), }); }, async head(physicalBucket: string, key: string): Promise { const stored = objectsIn(physicalBucket).get(key); if (!stored) { throw new ModuleStorageError("STORAGE_OBJECT_NOT_FOUND", key); } return metadataFor(key, stored); }, async list( physicalBucket: string, options: ModuleStorageListOptions, ): Promise { const entries = [...objectsIn(physicalBucket).entries()] .filter(([key]) => options.prefix === undefined || key.startsWith(options.prefix)) .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)); return Object.freeze({ objects: Object.freeze(entries.map(([key, stored]) => metadataFor(key, stored))), }); }, async probe(): Promise { // Always reachable. }, async put( physicalBucket: string, key: string, body: ModuleStorageBody, options: ModuleStoragePutOptions, ): Promise { const bytes = typeof body === "string" ? new TextEncoder().encode(body) : body instanceof Uint8Array ? body : await collect(body as ReadableStream); const stored: StoredObject = { bytes, lastModified: new Date(0), ...(options.contentType === undefined ? {} : { contentType: options.contentType }), }; objectsIn(physicalBucket).set(key, stored); return metadataFor(key, stored); }, }; return Object.assign(provider, { buckets: buckets as ReadonlyMap>, get ensureBucketCalls(): number { return state.ensureBucketCalls; }, }); } export interface ModuleStorageOption { readonly snapshots: ModuleStorageLookup; readonly providerConfigured: boolean; } /** * Builds the `createApiApp` storage option the same way `startApi` does — resolve every module's * declared buckets against one provider and one prefix — rather than hand-writing a map, so a test * can never assert against a shape the real composition would not produce. */ export function moduleStorageOptionFor( discoveredModules: readonly DiscoveredModule[], provider?: StorageProvider, ): ModuleStorageOption { return { providerConfigured: provider !== undefined, snapshots: createModuleStorageLookup( provider, "ekklesia", discoveredModules.map((module) => ({ logicalBucketIds: module.manifest.storage.buckets.map((bucket) => bucket.id), moduleId: module.manifest.id, })), ), }; } /** The storage option for a `createApiApp` caller that exercises no storage at all — the explicit * "this installation has no object storage" statement the required option asks for. */ export function emptyModuleStorage(): ModuleStorageOption { return { providerConfigured: false, snapshots: new Map() }; }