80 lines
2.8 KiB
TypeScript
80 lines
2.8 KiB
TypeScript
|
|
/**
|
||
|
|
* Global shutdown-hook lifecycle — a library must never keep its host process
|
||
|
|
* alive. init() registers process-level SIGTERM/SIGINT/beforeExit listeners
|
||
|
|
* (once, globally) whose signal handles are ref'd by Node; if they outlive the
|
||
|
|
* last brain, a bare script hangs forever after close() (consumer-reported on
|
||
|
|
* the GA pair). The contract pinned here:
|
||
|
|
*
|
||
|
|
* - first init() → exactly one listener added per signal
|
||
|
|
* - more inits → no additional listeners (registered once)
|
||
|
|
* - close() → listeners survive while OTHER instances remain live
|
||
|
|
* - last close() → listeners removed, counts back to baseline
|
||
|
|
* - re-init → hooks re-register (the once-flag resets)
|
||
|
|
*/
|
||
|
|
import { describe, it, expect } from 'vitest'
|
||
|
|
import { Brainy, NounType } from '../../src/index.js'
|
||
|
|
|
||
|
|
const SIGNALS = ['SIGTERM', 'SIGINT', 'beforeExit'] as const
|
||
|
|
|
||
|
|
function listenerCounts(): Record<string, number> {
|
||
|
|
return Object.fromEntries(SIGNALS.map((s) => [s, process.listeners(s as any).length]))
|
||
|
|
}
|
||
|
|
|
||
|
|
function makeBrain(): any {
|
||
|
|
return new Brainy({ requireSubtype: false, storage: { type: 'memory' }, plugins: [], silent: true })
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('Shutdown-hook lifecycle (process must exit after the last close)', () => {
|
||
|
|
it('registers each hook once on first init and removes ALL of them on the last close', async () => {
|
||
|
|
const baseline = listenerCounts()
|
||
|
|
|
||
|
|
const brain = makeBrain()
|
||
|
|
await brain.init()
|
||
|
|
for (const s of SIGNALS) {
|
||
|
|
expect(process.listeners(s as any).length).toBe(baseline[s] + 1) // exactly one each
|
||
|
|
}
|
||
|
|
|
||
|
|
await brain.close()
|
||
|
|
expect(listenerCounts()).toEqual(baseline) // gone — the loop can drain
|
||
|
|
})
|
||
|
|
|
||
|
|
it('keeps the hooks while any other instance is live; only the LAST close deregisters', async () => {
|
||
|
|
const baseline = listenerCounts()
|
||
|
|
|
||
|
|
const first = makeBrain()
|
||
|
|
const second = makeBrain()
|
||
|
|
await first.init()
|
||
|
|
await second.init()
|
||
|
|
for (const s of SIGNALS) {
|
||
|
|
expect(process.listeners(s as any).length).toBe(baseline[s] + 1) // once globally, not per instance
|
||
|
|
}
|
||
|
|
|
||
|
|
await first.close()
|
||
|
|
for (const s of SIGNALS) {
|
||
|
|
expect(process.listeners(s as any).length).toBe(baseline[s] + 1) // second is still live
|
||
|
|
}
|
||
|
|
|
||
|
|
await second.close()
|
||
|
|
expect(listenerCounts()).toEqual(baseline)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('re-registers after a full close/re-init cycle (the once-flag resets)', async () => {
|
||
|
|
const baseline = listenerCounts()
|
||
|
|
|
||
|
|
const first = makeBrain()
|
||
|
|
await first.init()
|
||
|
|
await first.close()
|
||
|
|
expect(listenerCounts()).toEqual(baseline)
|
||
|
|
|
||
|
|
const second = makeBrain()
|
||
|
|
await second.init()
|
||
|
|
const id = await second.add({ data: 'still fully functional', type: NounType.Concept })
|
||
|
|
expect(id).toBeTruthy()
|
||
|
|
for (const s of SIGNALS) {
|
||
|
|
expect(process.listeners(s as any).length).toBe(baseline[s] + 1)
|
||
|
|
}
|
||
|
|
await second.close()
|
||
|
|
expect(listenerCounts()).toEqual(baseline)
|
||
|
|
})
|
||
|
|
})
|