fix(storage,workspace): review-bot findings — emit isolation, domain ownership, null globals

- domain/changed emission is isolated from the write path: an observer
  throwing synchronously can no longer turn a committed (durable +
  in-memory) write into a rejection; the failure is logged and later
  writes proceed.
- Domain lifecycle belongs to the opening consumer: Domain gains an
  idempotent close() (drain, unit close, reservation release), the
  facility stops registering effects on its own context and instead
  closes any still-open domains on unmount; WorkspaceRegistry holds its
  domain through its own effect, so disposing and re-mounting the
  consumer no longer wedges on already-open.
- defineDomain rejects a global schema accepting null at declaration
  time: JSON null is the medium's absence sentinel, so a nullable global
  could never round-trip; failing loud at the spec keeps set(null)
  unrepresentable.

Regression tests cover all three (hostile listener, close/reopen and
consumer re-mount, nullable-global rejection).
This commit is contained in:
imccyu
2026-07-25 00:01:21 +08:00
parent 1bddf5269a
commit 507dd25a3a
7 changed files with 160 additions and 36 deletions
@@ -84,6 +84,9 @@ export class WorkspaceRegistry extends Service {
/** Open the domain and rebuild the entity cache before the service is published as active. */
protected async [Service.init](): Promise<void> {
const domain = await this.ctx.storage.domain.open(workspaceDomainSpec)
// This registry owns the domain handle it opened: closing on fiber
// disposal frees the domain name, so a re-plugged registry can reopen it.
this.ctx.effect(() => () => domain.close(), 'workspace.domainClose')
this.table = domain.table('workspaces')
const persistence = this.ctx.get('sessionPersistence')
if (persistence !== undefined) {