99 lines
3.4 KiB
TypeScript
99 lines
3.4 KiB
TypeScript
/**
|
|
* 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'
|
|
|
|
/**
|
|
* Derive the Cordis lifecycle service that one named backend plugin provides.
|
|
* Domain-form providers inject these keys so activation cannot race backend
|
|
* registration even though callers continue resolving backends through the
|
|
* storage registry.
|
|
* @param name - Backend registry name.
|
|
* @returns the corresponding lifecycle-only service key.
|
|
*/
|
|
export function storageBackendServiceKey(name: string): string {
|
|
return `storage.backend.${name}`
|
|
}
|
|
|
|
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 () => {
|
|
// Same stale-disposer guard as BackendRegistry.register.
|
|
if (this.forms.get(form) === facility) {
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Service packages default-export their service class and nothing else
|
|
// plugin-shaped (packages/AGENTS.md): mixing a default export with a
|
|
// function-plugin `apply` makes the Loader drop the plugin namespace.
|
|
export default Storage
|