feat(storage): storage hub with named backend registry and data-form mounts

ctx.storage is a pure registration hub: multiple named backends stay
mounted side by side, data forms (domain first) mount via the
merge-extensible StorageForms map. src/backend.ts is the normative
KV-facet contract; tests/contract.ts is the shared conformance suite
every backend runs. Backends expose data-shape facets (kv now, an
append-log facet reserved for the future session-backend migration).
This commit is contained in:
imccyu
2026-07-24 19:06:48 +08:00
parent f7b36bd36d
commit e90b0d51df
12 changed files with 564 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
/**
* Storage hub (`ctx.storage`): a named backend registry plus mounted
* data-form facilities. The hub itself performs no IO — backends own media,
* data forms (the domain layer first) own semantics.
* @module @deepseek-ai/dsh-storage
*/
import { Context, Service } from 'cordis'
import { StorageError } from './error.ts'
import { BackendRegistry } from './registry.ts'
export { BackendRegistry } from './registry.ts'
export { StorageError } from './error.ts'
export type { StorageErrorCode } from './error.ts'
export { UNIT_NAME_RE } from './backend.ts'
export type { StorageBackend, KvFacet, KvUnit, KvUnitDescriptor } from './backend.ts'
declare module 'cordis' {
interface Context {
storage: Storage
}
}
/**
* Data forms mountable on the hub, keyed by form name. Form owners extend
* this map via declaration merging (the domain layer merges
* `domain: DomainFacility`) and mount the facility in their `apply`.
*/
export interface StorageForms {}
/**
* The storage hub service. Backends register under `backend`; data forms
* mount under their `StorageForms` key and are reached as `ctx.storage.<form>`.
*/
export class Storage extends Service {
/** Named backend table; multiple backends stay mounted side by side. */
readonly backend = new BackendRegistry()
private readonly forms = new Map<keyof StorageForms, unknown>()
constructor(ctx: Context) {
super(ctx, 'storage')
}
/**
* Mount a data-form facility on the hub. Mounting is an effect: the
* returned disposer unmounts the form.
* @param form - Form key declared in {@link StorageForms}.
* @param facility - The facility instance to expose.
* @returns the disposer that unmounts the form.
*/
mount<K extends keyof StorageForms>(form: K, facility: StorageForms[K]): () => void {
if (this.forms.has(form)) {
throw new StorageError('duplicate-mount', `storage form '${String(form)}' is already mounted`)
}
this.forms.set(form, facility)
return () => {
this.forms.delete(form)
}
}
/**
* Resolve a mounted data form.
* @param form - Form key declared in {@link StorageForms}.
* @returns the mounted facility.
*/
form<K extends keyof StorageForms>(form: K): StorageForms[K] {
if (!this.forms.has(form)) {
throw new StorageError('form-not-mounted', `storage form '${String(form)}' is not mounted`)
}
return this.forms.get(form) as StorageForms[K]
}
/** Domain data form; present once the domain layer plugin is loaded. */
get domain(): StorageForms extends { domain: infer D } ? D : never {
return this.form('domain' as keyof StorageForms) as StorageForms extends { domain: infer D } ? D : never
}
}
/**
* Mount the storage hub service.
* @param ctx - Plugin context.
*/
export function apply(ctx: Context) {
ctx.plugin(Storage)
}