style(storage,workspace): satisfy the repository lint gate
eslint --fix formatting sweep plus the manual residue: sync method bodies drop async behind Promise-returning signatures (the sqlite unit routes primitives through a settle() guard preserving the never-throws- synchronously contract), catch callbacks type their reason as unknown, loadAll's global slot is plain unknown (null semantics stay in JSDoc), a non-null assertion becomes a narrowing, and unsafe any assignments in tests gain explicit types. One justified eslint-disable for prefer-promise-reject-errors follows the core/session precedent — wrapping would discard the original StorageError code.
This commit is contained in:
@@ -164,9 +164,9 @@ export class DomainImpl {
|
||||
const host: TableHost = {
|
||||
domainName: spec.name,
|
||||
unit,
|
||||
enqueue: (job) => this.enqueue(job),
|
||||
assertReadable: () => this.assertReadable(),
|
||||
emitChanged: (change) => this.ctx.emit('domain/changed', change),
|
||||
enqueue: job => this.enqueue(job),
|
||||
assertReadable: () => { this.assertReadable() },
|
||||
emitChanged: (change) => { this.ctx.emit('domain/changed', change) },
|
||||
}
|
||||
for (const [table, tableRecords] of records) {
|
||||
this.tables.set(table, new KvTableImpl(host, table, tableRecords))
|
||||
@@ -178,7 +178,7 @@ export class DomainImpl {
|
||||
this.assertReadable()
|
||||
return this.globalValue
|
||||
},
|
||||
set: (value) => this.enqueue(async () => {
|
||||
set: value => this.enqueue(async () => {
|
||||
await this.unit.setGlobal(value)
|
||||
this.globalValue = value
|
||||
host.emitChanged({ domain: this.name, table: '', key: '', operation: 'put', value })
|
||||
|
||||
@@ -113,11 +113,12 @@ export class DomainFacility {
|
||||
}
|
||||
// A null stored global means "never written": serve `initial` without
|
||||
// materializing it — the first `set` writes.
|
||||
const globalValue = spec.global === undefined
|
||||
const globalSpec = spec.global
|
||||
const globalValue = globalSpec === undefined
|
||||
? undefined
|
||||
: snapshot.global === null
|
||||
? spec.global.initial
|
||||
: parseRecord(spec.name, '', '', () => spec.global!.schema.parse(snapshot.global))
|
||||
? globalSpec.initial
|
||||
: parseRecord(spec.name, '', '', () => globalSpec.schema.parse(snapshot.global))
|
||||
const domain = new DomainImpl(this.ctx, spec, unit, tables, globalValue)
|
||||
// The open-domain table entry is itself the effect: registration and
|
||||
// the drain-then-unlist teardown live in one closure.
|
||||
|
||||
@@ -45,7 +45,7 @@ export interface DomainSpec {
|
||||
|
||||
/** Key type of one declared table, recovered from its phantom carrier. */
|
||||
export type TableKeyOf<S extends DomainSpec, N extends keyof S['tables']> =
|
||||
S['tables'][N] extends DomainTableSpec<infer K, unknown> ? K : never
|
||||
S['tables'][N] extends DomainTableSpec<infer K> ? K : never
|
||||
|
||||
/** Value type of one declared table. */
|
||||
export type TableValueOf<S extends DomainSpec, N extends keyof S['tables']> =
|
||||
|
||||
@@ -174,14 +174,14 @@ describe('KvTable writes', () => {
|
||||
const table = (await facility.open(spec)).table('items')
|
||||
await table.put('counter', { label: 'c', count: 0 })
|
||||
await Promise.all(Array.from({ length: 50 }, () =>
|
||||
table.update('counter', (current) => ({ ...current, count: current.count + 1 }))))
|
||||
table.update('counter', current => ({ ...current, count: current.count + 1 }))))
|
||||
expect(table.get('counter')).toEqual({ label: 'c', count: 50 })
|
||||
})
|
||||
|
||||
it('update rejects a missing key; delete reports prior existence', async () => {
|
||||
const { facility } = await harness()
|
||||
const table = (await facility.open(spec)).table('items')
|
||||
await expect(table.update('ghost', (v) => v)).rejects.toMatchObject({ code: 'missing-key' })
|
||||
await expect(table.update('ghost', v => v)).rejects.toMatchObject({ code: 'missing-key' })
|
||||
await table.put('a', { label: 'x', count: 1 })
|
||||
await expect(table.delete('a')).resolves.toBe(true)
|
||||
await expect(table.delete('a')).resolves.toBe(false)
|
||||
@@ -192,7 +192,7 @@ describe('KvTable writes', () => {
|
||||
const domain = await facility.open(spec)
|
||||
const table = domain.table('items')
|
||||
await table.put('a', { label: 'x', count: 1 })
|
||||
await table.update('a', (current) => ({ ...current, count: 2 }))
|
||||
await table.update('a', current => ({ ...current, count: 2 }))
|
||||
await table.delete('a')
|
||||
await table.delete('a') // no event: already absent
|
||||
await domain.global.set({ theme: 'dark' })
|
||||
@@ -216,7 +216,7 @@ describe('durability failure', () => {
|
||||
|
||||
pool.failNextWrites = 3
|
||||
await expect(table.put('a', { label: 'x', count: 99 })).rejects.toThrow(/injected/)
|
||||
await expect(table.update('a', (c) => ({ ...c, count: c.count + 1 }))).rejects.toThrow(/injected/)
|
||||
await expect(table.update('a', c => ({ ...c, count: c.count + 1 }))).rejects.toThrow(/injected/)
|
||||
await expect(table.delete('a')).rejects.toThrow(/injected/)
|
||||
|
||||
// Reads still serve the pre-failure record; no events leaked.
|
||||
@@ -225,7 +225,7 @@ describe('durability failure', () => {
|
||||
expect(changes).toHaveLength(seen)
|
||||
|
||||
// The chain survives rejections: the next write lands cleanly with no residue.
|
||||
await table.update('a', (c) => ({ ...c, count: c.count + 1 }))
|
||||
await table.update('a', c => ({ ...c, count: c.count + 1 }))
|
||||
expect(table.get('a')).toEqual({ label: 'x', count: 2 })
|
||||
})
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
import { StorageError } from '@deepseek-ai/dsh-storage'
|
||||
import type { KvFacet, KvUnit, KvUnitDescriptor, StorageBackend } from '@deepseek-ai/dsh-storage'
|
||||
|
||||
/** One unit's medium: tables of records plus the global slot. */
|
||||
/** One unit's medium: tables of records plus the global slot (`null` = never written). */
|
||||
export interface MemoryMedium {
|
||||
tables: Map<string, Map<string, unknown>>
|
||||
global: unknown | null
|
||||
global: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +70,7 @@ class MemoryKvUnit implements KvUnit {
|
||||
}
|
||||
}
|
||||
|
||||
async loadAll(): Promise<{ tables: Record<string, Record<string, unknown>>; global: unknown | null }> {
|
||||
async loadAll(): Promise<{ tables: Record<string, Record<string, unknown>>; global: unknown }> {
|
||||
this.assertOpen()
|
||||
const tables: Record<string, Record<string, unknown>> = {}
|
||||
for (const table of this.descriptor.tables) {
|
||||
|
||||
@@ -29,7 +29,7 @@ async function setup() {
|
||||
return { ctx, facility }
|
||||
}
|
||||
|
||||
const invariantViolation = expect.objectContaining<Partial<InvariantError>>({
|
||||
const invariantViolation: unknown = expect.objectContaining<Partial<InvariantError>>({
|
||||
code: 'INVARIANT',
|
||||
packageName: '@deepseek-ai/dsh-domain',
|
||||
})
|
||||
@@ -40,42 +40,42 @@ describe('domain change-event invariants', () => {
|
||||
const domain = await facility.open(spec)
|
||||
const rows = domain.table('rows')
|
||||
await rows.put('a', { n: 1 })
|
||||
await rows.update('a', (current) => ({ n: current.n + 1 }))
|
||||
await rows.update('a', current => ({ n: current.n + 1 }))
|
||||
await expect(rows.delete('a')).resolves.toBe(true)
|
||||
await domain.global.set({ n: 5 })
|
||||
})
|
||||
|
||||
it('rejects an event for a domain that is not open', async () => {
|
||||
const { ctx } = await setup()
|
||||
expect(() => ctx.emit('domain/changed', {
|
||||
expect(() => { ctx.emit('domain/changed', {
|
||||
domain: 'ghost', table: 'rows', key: 'a', operation: 'put', value: { n: 1 },
|
||||
})).toThrow(invariantViolation)
|
||||
}) }).toThrow(invariantViolation)
|
||||
})
|
||||
|
||||
it('rejects a put event whose value is not the in-memory record', async () => {
|
||||
const { ctx, facility } = await setup()
|
||||
const domain = await facility.open(spec)
|
||||
await domain.table('rows').put('a', { n: 1 })
|
||||
expect(() => ctx.emit('domain/changed', {
|
||||
expect(() => { ctx.emit('domain/changed', {
|
||||
domain: 'inv', table: 'rows', key: 'a', operation: 'put', value: { n: 999 },
|
||||
})).toThrow(invariantViolation)
|
||||
}) }).toThrow(invariantViolation)
|
||||
})
|
||||
|
||||
it('rejects a deletion event while the record is still in memory', async () => {
|
||||
const { ctx, facility } = await setup()
|
||||
const domain = await facility.open(spec)
|
||||
await domain.table('rows').put('a', { n: 1 })
|
||||
expect(() => ctx.emit('domain/changed', {
|
||||
expect(() => { ctx.emit('domain/changed', {
|
||||
domain: 'inv', table: 'rows', key: 'a', operation: 'deleted',
|
||||
})).toThrow(invariantViolation)
|
||||
}) }).toThrow(invariantViolation)
|
||||
})
|
||||
|
||||
it('rejects a global event whose value is not the in-memory global', async () => {
|
||||
const { ctx, facility } = await setup()
|
||||
await facility.open(spec)
|
||||
expect(() => ctx.emit('domain/changed', {
|
||||
expect(() => { ctx.emit('domain/changed', {
|
||||
domain: 'inv', table: '', key: '', operation: 'put', value: { n: 42 },
|
||||
})).toThrow(invariantViolation)
|
||||
}) }).toThrow(invariantViolation)
|
||||
})
|
||||
|
||||
it('tolerates operations outside the closed union without failing falsely', async () => {
|
||||
@@ -84,8 +84,8 @@ describe('domain change-event invariants', () => {
|
||||
await domain.table('rows').put('a', { n: 1 })
|
||||
// Merge-hostile input: the closed union's satisfies-never default arm is
|
||||
// unreachable in typed code; an untyped emit must not crash the check.
|
||||
expect(() => ctx.emit('domain/changed', {
|
||||
expect(() => { ctx.emit('domain/changed', {
|
||||
domain: 'inv', table: 'rows', key: 'a', operation: 'exotic',
|
||||
} as unknown as DomainChanged)).not.toThrow()
|
||||
} as unknown as DomainChanged) }).not.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -8,10 +8,10 @@
|
||||
import { StorageError } from '@deepseek-ai/dsh-storage'
|
||||
import type { KvUnitDescriptor } from '@deepseek-ai/dsh-storage'
|
||||
|
||||
/** In-memory authoritative state of one unit; the file is its projection. */
|
||||
/** In-memory authoritative state of one unit; the file is its projection. `global` is `null` until first written. */
|
||||
export interface UnitState {
|
||||
version: number
|
||||
global: unknown | null
|
||||
global: unknown
|
||||
tables: Map<string, Map<string, unknown>>
|
||||
}
|
||||
|
||||
|
||||
@@ -36,10 +36,10 @@ export async function openJsonUnit(
|
||||
const state: UnitState =
|
||||
text === undefined
|
||||
? {
|
||||
version: descriptor.version,
|
||||
global: null,
|
||||
tables: new Map(descriptor.tables.map((table) => [table, new Map()])),
|
||||
}
|
||||
version: descriptor.version,
|
||||
global: null,
|
||||
tables: new Map(descriptor.tables.map(table => [table, new Map<string, unknown>()])),
|
||||
}
|
||||
: parse(text, descriptor)
|
||||
return new JsonKvUnit(descriptor, path, state, onClose)
|
||||
}
|
||||
@@ -56,13 +56,13 @@ class JsonKvUnit implements KvUnit {
|
||||
private readonly onClose: () => void,
|
||||
) {}
|
||||
|
||||
async loadAll(): Promise<{ tables: Record<string, Record<string, unknown>>; global: unknown | null }> {
|
||||
loadAll(): Promise<{ tables: Record<string, Record<string, unknown>>; global: unknown }> {
|
||||
this.assertOpen()
|
||||
const tables: Record<string, Record<string, unknown>> = {}
|
||||
for (const [table, records] of this.state.tables) {
|
||||
tables[table] = Object.fromEntries(records)
|
||||
}
|
||||
return { tables, global: this.state.global }
|
||||
return Promise.resolve({ tables, global: this.state.global })
|
||||
}
|
||||
|
||||
async putRecord(table: string, key: string, value: unknown): Promise<void> {
|
||||
@@ -73,7 +73,7 @@ class JsonKvUnit implements KvUnit {
|
||||
records.set(key, value)
|
||||
// Roll back on a failed publish: memory is authoritative, so a rejected
|
||||
// write must not survive in memory (or ride along with the next publish).
|
||||
await this.publish().catch(async (error) => {
|
||||
await this.publish().catch((error: unknown) => {
|
||||
if (hadKey) records.set(key, previous)
|
||||
else records.delete(key)
|
||||
throw error
|
||||
@@ -86,7 +86,7 @@ class JsonKvUnit implements KvUnit {
|
||||
if (!records.has(key)) return
|
||||
const previous = records.get(key)
|
||||
records.delete(key)
|
||||
await this.publish().catch(async (error) => {
|
||||
await this.publish().catch((error: unknown) => {
|
||||
records.set(key, previous)
|
||||
throw error
|
||||
})
|
||||
@@ -99,7 +99,7 @@ class JsonKvUnit implements KvUnit {
|
||||
}
|
||||
const previous = this.state.global
|
||||
this.state.global = value
|
||||
await this.publish().catch(async (error) => {
|
||||
await this.publish().catch((error: unknown) => {
|
||||
this.state.global = previous
|
||||
throw error
|
||||
})
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('json backend specifics', () => {
|
||||
const root = await freshRoot()
|
||||
const backend = new JsonStorageBackend(root)
|
||||
await backend.kv.open(descriptor)
|
||||
await expect(backend.kv.open(descriptor)).rejects.toThrowError(/already open/)
|
||||
await expect(backend.kv.open(descriptor)).rejects.toThrow(/already open/)
|
||||
await backend.close()
|
||||
})
|
||||
|
||||
@@ -208,13 +208,15 @@ describe('json backend specifics', () => {
|
||||
const bigWrite = unit.putRecord('t', 'big', { blob: 'x'.repeat(4 * 1024 * 1024) })
|
||||
await unit.close()
|
||||
await expect(bigWrite).resolves.toBeUndefined()
|
||||
const onDisk = JSON.parse(await readFile(join(root, 'shape.json'), 'utf8'))
|
||||
expect(onDisk.tables.t.big).toBeDefined()
|
||||
const onDisk = JSON.parse(await readFile(join(root, 'shape.json'), 'utf8')) as {
|
||||
tables: Record<string, Record<string, unknown>>
|
||||
}
|
||||
expect(onDisk.tables['t']?.['big']).toBeDefined()
|
||||
|
||||
const backend2 = new JsonStorageBackend(root)
|
||||
const opening = backend2.kv.open(descriptor)
|
||||
const closing = backend2.close()
|
||||
await expect(opening.then((u) => u.putRecord('t', 'x', {}))).rejects.toMatchObject({ code: 'closed' })
|
||||
await expect(opening.then(u => u.putRecord('t', 'x', {}))).rejects.toMatchObject({ code: 'closed' })
|
||||
await closing
|
||||
})
|
||||
})
|
||||
@@ -62,24 +62,25 @@ export class SqliteKvUnit implements KvUnit {
|
||||
: undefined
|
||||
}
|
||||
|
||||
async loadAll(): Promise<{ tables: Record<string, Record<string, unknown>>; global: unknown | null }> {
|
||||
this.ensureOpen()
|
||||
const tables: Record<string, Record<string, unknown>> = {}
|
||||
for (const [name, statements] of this.tables) {
|
||||
// Null prototype: record keys are arbitrary strings, so '__proto__'
|
||||
// must land as an own property instead of mutating the prototype.
|
||||
const records: Record<string, unknown> = Object.create(null) as Record<string, unknown>
|
||||
for (const row of statements.selectAll.all() as unknown as Array<{ key: string; value: string }>) {
|
||||
records[row.key] = this.parseValue(row.value, `table '${name}' key '${row.key}'`)
|
||||
loadAll(): Promise<{ tables: Record<string, Record<string, unknown>>; global: unknown }> {
|
||||
return this.settle(() => {
|
||||
const tables: Record<string, Record<string, unknown>> = {}
|
||||
for (const [name, statements] of this.tables) {
|
||||
// Null prototype: record keys are arbitrary strings, so '__proto__'
|
||||
// must land as an own property instead of mutating the prototype.
|
||||
const records: Record<string, unknown> = Object.create(null) as Record<string, unknown>
|
||||
for (const row of statements.selectAll.all() as unknown as Array<{ key: string; value: string }>) {
|
||||
records[row.key] = this.parseValue(row.value, `table '${name}' key '${row.key}'`)
|
||||
}
|
||||
tables[name] = records
|
||||
}
|
||||
tables[name] = records
|
||||
}
|
||||
let global: unknown = null
|
||||
if (this.globalSelect !== undefined) {
|
||||
const row = this.globalSelect.get(this.descriptor.name) as { value: string } | undefined
|
||||
if (row !== undefined) global = this.parseValue(row.value, 'global slot')
|
||||
}
|
||||
return { tables, global }
|
||||
let global: unknown = null
|
||||
if (this.globalSelect !== undefined) {
|
||||
const row = this.globalSelect.get(this.descriptor.name) as { value: string } | undefined
|
||||
if (row !== undefined) global = this.parseValue(row.value, 'global slot')
|
||||
}
|
||||
return { tables, global }
|
||||
})
|
||||
}
|
||||
|
||||
/** Parse one stored value column, mapping bad JSON to `malformed-medium`. */
|
||||
@@ -95,28 +96,48 @@ export class SqliteKvUnit implements KvUnit {
|
||||
}
|
||||
}
|
||||
|
||||
async putRecord(table: string, key: string, value: unknown): Promise<void> {
|
||||
this.ensureOpen()
|
||||
this.statementsFor(table).upsert.run(key, JSON.stringify(value))
|
||||
putRecord(table: string, key: string, value: unknown): Promise<void> {
|
||||
return this.settle(() => {
|
||||
this.statementsFor(table).upsert.run(key, JSON.stringify(value))
|
||||
})
|
||||
}
|
||||
|
||||
async deleteRecord(table: string, key: string): Promise<void> {
|
||||
this.ensureOpen()
|
||||
this.statementsFor(table).remove.run(key)
|
||||
deleteRecord(table: string, key: string): Promise<void> {
|
||||
return this.settle(() => {
|
||||
this.statementsFor(table).remove.run(key)
|
||||
})
|
||||
}
|
||||
|
||||
async setGlobal(value: unknown): Promise<void> {
|
||||
this.ensureOpen()
|
||||
if (this.globalUpsert === undefined) {
|
||||
throw new Error(`kv unit '${this.descriptor.name}' declared no global slot`)
|
||||
setGlobal(value: unknown): Promise<void> {
|
||||
return this.settle(() => {
|
||||
if (this.globalUpsert === undefined) {
|
||||
throw new Error(`kv unit '${this.descriptor.name}' declared no global slot`)
|
||||
}
|
||||
this.globalUpsert.run(this.descriptor.name, JSON.stringify(value))
|
||||
})
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
if (!this.closed) {
|
||||
this.closed = true
|
||||
this.onClose()
|
||||
}
|
||||
this.globalUpsert.run(this.descriptor.name, JSON.stringify(value))
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (this.closed) return
|
||||
this.closed = true
|
||||
this.onClose()
|
||||
/**
|
||||
* Run one synchronous primitive behind the closed guard, mapping a throw to
|
||||
* a rejection so the Promise-returning contract never throws synchronously.
|
||||
*/
|
||||
private settle<T>(operation: () => T): Promise<T> {
|
||||
try {
|
||||
this.ensureOpen()
|
||||
return Promise.resolve(operation())
|
||||
} catch (error) {
|
||||
// Non-Error throws can only enter through JSON.stringify propagating a
|
||||
// value's own toJSON throw; wrap those, preserve every real Error.
|
||||
return Promise.reject(error instanceof Error ? error : new Error(String(error)))
|
||||
}
|
||||
}
|
||||
|
||||
private ensureOpen(): void {
|
||||
|
||||
@@ -171,6 +171,17 @@ describe('sqlite backend specifics', () => {
|
||||
await reopened.close()
|
||||
})
|
||||
|
||||
it('wraps a non-Error toJSON throw into an Error rejection', async () => {
|
||||
const backend = backendAt(':memory:')
|
||||
const unit = await backend.kv.open(DESCRIPTOR)
|
||||
// JSON.stringify propagates a value's own toJSON throw verbatim; the unit
|
||||
// must still reject with an Error instance.
|
||||
const hostile = { toJSON: () => { throw 'not an error' } }
|
||||
await expect(unit.putRecord('records', 'k', hostile)).rejects.toThrow('not an error')
|
||||
await expect(unit.putRecord('records', 'k', hostile)).rejects.toBeInstanceOf(Error)
|
||||
await backend.close()
|
||||
})
|
||||
|
||||
it('rejects setGlobal on a unit without a global slot and writes to undeclared tables', async () => {
|
||||
const backend = backendAt(':memory:')
|
||||
const unit = await backend.kv.open({ ...DESCRIPTOR, hasGlobal: false })
|
||||
|
||||
@@ -69,7 +69,7 @@ export interface KvUnit {
|
||||
* @returns every table's records keyed by table name, plus the global
|
||||
* singleton (`null` when never written or not declared).
|
||||
*/
|
||||
loadAll(): Promise<{ tables: Record<string, Record<string, unknown>>; global: unknown | null }>
|
||||
loadAll(): Promise<{ tables: Record<string, Record<string, unknown>>; global: unknown }>
|
||||
|
||||
/**
|
||||
* Upsert one record durably. Overwrite semantics: an existing key is replaced.
|
||||
|
||||
@@ -76,7 +76,7 @@ export class Storage extends Service {
|
||||
|
||||
/** 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
|
||||
return this.form('domain' as keyof StorageForms)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ expect.extend({
|
||||
received()
|
||||
} catch (error) {
|
||||
const pass = Object.entries(expected).every(
|
||||
(entry) => (error as Record<string, unknown>)[entry[0]] === entry[1],
|
||||
entry => (error as Record<string, unknown>)[entry[0]] === entry[1],
|
||||
)
|
||||
return { pass, message: () => `expected thrown error to match ${JSON.stringify(expected)}, got ${String(error)}` }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user