Confirmed and fixed, each with a regression test that failed first:
- Concurrent update() lost patches (merge over one stale snapshot):
per-namespace serialized write queues; a failed write cannot poison
the queue for later writers.
- Fixed-name .tmp write followed planted symlinks and kept stale modes:
random-suffix sibling, exclusive-create (wx), 0600, cleanup on
failure, then rename.
- A throwing settings/updated listener escaped commit and permanently
wedged the provider reload chain (rejected refreshTask): commit now
contains listener failures (INVARIANT-coded errors still propagate),
async watcher rejections are adopted and contained
(watch callbacks are officially void | Promise<void>), and the
provider chains refreshes on a settled tail with an error log.
- No way to remove a user override: scope/service replace(section)
sets the user section wholesale; replace({}) re-inherits base and
schema defaults.
- The three-primitive provider contract did not hold (base never
called load()): the base Service.init loads and publishes once;
settings-local delegates via yield* super[Service.init]().
- Dispose did not quiesce: teardown flags closed, closes the watcher,
then awaits queued/in-flight reloads; closed is re-checked across
await points.
- Invariant now checks the authoritative relation with the seam's own
deepEqualJson: emitted next must equal settings.get(ns), and
next/prev must differ structurally (cosmokit dependency dropped).
- New docs/core-data-structures/settings.{md,zh.md} with type-equiv
blocks + manifest entries; catalog types moved from exemptions to
LINK_MAP; website page registered.
Both packages stay at per-file 100% coverage.
55 lines
1.9 KiB
TypeScript
55 lines
1.9 KiB
TypeScript
/**
|
|
* In-memory settings provider fixture: the smallest real subclass of the seam,
|
|
* used by the base-class behavior suite in place of a file- or network-backed
|
|
* provider. Kept in `tests/` because production providers live in their own
|
|
* packages.
|
|
*/
|
|
|
|
import { Settings, type SettingsNamespace } from '../src/index.ts'
|
|
|
|
/** In-memory provider exposing the protected seam hooks to tests. */
|
|
export class MemorySettings extends Settings {
|
|
/** Raw document the provider "storage" currently holds. */
|
|
doc: Record<string, unknown>
|
|
/** Every persist() call observed, in order. */
|
|
persisted: Array<{ ns: SettingsNamespace; section: Record<string, unknown> }> = []
|
|
/** When false, update() must reject before reaching persist(). */
|
|
writableFlag: boolean
|
|
|
|
/** Artificial persist latency so tests can interleave concurrent updates. */
|
|
persistDelayMs: number
|
|
|
|
constructor(ctx: ConstructorParameters<typeof Settings>[0], options?: {
|
|
doc?: Record<string, unknown>
|
|
writable?: boolean
|
|
persistDelayMs?: number
|
|
}) {
|
|
super(ctx)
|
|
this.doc = structuredClone(options?.doc ?? {})
|
|
this.writableFlag = options?.writable ?? true
|
|
this.persistDelayMs = options?.persistDelayMs ?? 0
|
|
}
|
|
|
|
get writable(): boolean {
|
|
return this.writableFlag
|
|
}
|
|
|
|
protected load(): Promise<Record<string, unknown>> {
|
|
return Promise.resolve(structuredClone(this.doc))
|
|
}
|
|
|
|
protected async persist(ns: SettingsNamespace, section: Record<string, unknown>): Promise<void> {
|
|
if (this.persistDelayMs > 0) {
|
|
await new Promise(resolve => setTimeout(resolve, this.persistDelayMs))
|
|
}
|
|
this.persisted.push({ ns, section: structuredClone(section) })
|
|
this.doc[ns] = structuredClone(section)
|
|
}
|
|
|
|
/** Simulate an external storage change reaching the provider. */
|
|
pushExternal(doc: Record<string, unknown>): void {
|
|
this.doc = structuredClone(doc)
|
|
this.publish(structuredClone(doc))
|
|
}
|
|
}
|