fix(storage,workspace): post-review hardening
Review findings applied across the group: - storage hub: stale disposers no longer remove a successor registration; the package now default-exports the Storage service class per the service-package export shape. - json backend: failed publishes roll back the authoritative memory state (a rejected write can no longer resurface via get() or ride the next publish); close() drains in-flight writes and blocks in-flight opens; double-open rejects as a plain caller error instead of malformed-medium. - sqlite backend: loadAll builds records on a null prototype (__proto__ keys round-trip instead of polluting), user_version is stamped only after the schema is fully created, and corrupt record JSON rejects as malformed-medium instead of a bare SyntaxError. - domain form: writes persist before mutating authoritative memory or emitting; DomainChanged is a put/deleted discriminated union. - workspace: attach/detach idempotence decided on the write chain (stale snapshots no longer short-circuit), create() requires a directory, and startup fails loud on duplicate stored paths. Eleven regression tests pin the fixed behaviors.
This commit is contained in:
@@ -55,7 +55,10 @@ export class Storage extends Service {
|
||||
}
|
||||
this.forms.set(form, facility)
|
||||
return () => {
|
||||
this.forms.delete(form)
|
||||
// Same stale-disposer guard as BackendRegistry.register.
|
||||
if (this.forms.get(form) === facility) {
|
||||
this.forms.delete(form)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,10 +80,7 @@ export class Storage extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount the storage hub service.
|
||||
* @param ctx - Plugin context.
|
||||
*/
|
||||
export function apply(ctx: Context) {
|
||||
ctx.plugin(Storage)
|
||||
}
|
||||
// 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
|
||||
|
||||
@@ -28,7 +28,11 @@ export class BackendRegistry {
|
||||
}
|
||||
this.backends.set(name, backend)
|
||||
return () => {
|
||||
this.backends.delete(name)
|
||||
// Remove only this registration's contribution: after dispose + re-register,
|
||||
// a stale disposer firing again must not remove the successor.
|
||||
if (this.backends.get(name) === backend) {
|
||||
this.backends.delete(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { BackendRegistry, Storage, apply } from '../src/index.ts'
|
||||
import Storage, { BackendRegistry } from '../src/index.ts'
|
||||
import type { StorageBackend } from '../src/index.ts'
|
||||
|
||||
const fakeBackend = (): StorageBackend => ({ close: async () => {} })
|
||||
@@ -27,7 +27,7 @@ describe('BackendRegistry', () => {
|
||||
describe('Storage service', () => {
|
||||
it('mounts on the context and exposes registry plus form mounting', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin({ apply })
|
||||
await ctx.plugin(Storage)
|
||||
expect(ctx.storage).toBeInstanceOf(Storage)
|
||||
|
||||
const facility = { marker: true }
|
||||
|
||||
Reference in New Issue
Block a user