A typed, generic implementation of the ConfigurableResponse primitive used alongside OutputTracker and EventEmitter to build null objects that can be seeded with pre configured responses (including errors) for tests.
Forked and generified from EkoLite, the public dojo demo from ekohacks.
A null wrapper (e.g. FileStorage.createNull()) is used in tests instead of a real implementation. ConfigurableResponse<T> lets a test pre seed a queue of responses the null should return for each operation, including error throwing:
const storage = FileStorage.createNull({
exists: [true, false, new Error('stat failed')],
});
await storage.exists('a.txt'); // true
await storage.exists('b.txt'); // false
await storage.exists('c.txt'); // throws Error('stat failed')
await storage.exists('d.txt'); // throws 'queue exhausted'The generic parameter T is the type the null would normally return for that operation. void for methods that return nothing (save, remove), concrete types for methods that do (boolean for exists, a result shape for exec, and so on).
Without the generic, every null signature devolves to unknown[], which lets any JS value through and makes the API lie to its readers. With ConfigurableResponse<T>, a null's signature describes exactly what it will accept, and TypeScript catches misuse at the call site.
T | Errorin the queue: a response is either a value of typeT, or anErrorto throw.- Default
T = unknownkeeps older callers compiling during migration. - Exhaustion throws by default. A test that runs out of configured responses will fail loudly rather than silently returning a default (which would mask missed configuration).
hasNext()exists for consumers that explicitly want lenient fallback behaviour. Calling it beforenext()gives the consumer the option to skip and use a default instead of throwing.
import { ConfigurableResponse } from './configurableResponse.ts';
class StubbedFileSystem {
private store = new Map<string, Buffer>();
private existsResponses?: ConfigurableResponse<boolean>;
constructor(options: { exists?: (boolean | Error)[] } = {}) {
if (options.exists) {
this.existsResponses = new ConfigurableResponse(options.exists);
}
}
exists(name: string): Promise<boolean> {
if (this.existsResponses?.hasNext()) {
return Promise.resolve(this.existsResponses.next());
}
return Promise.resolve(this.store.has(name));
}
}For a method that returns nothing, type it as ConfigurableResponse<void>:
private saveResponses?: ConfigurableResponse<void>;
// caller: save: [new Error('disk full')]
// any non Error value is a type error at compile timeFor a method that returns a domain shape:
private runnerResponses = new Map<string, ConfigurableResponse<ScriptResult | string>>();
// caller: echo: ['hello', new Error('crashed')]ConfigurableResponse usually ships alongside two helpers:
EventEmitter: internal pub/sub for recording what operations happened against the null.OutputTracker: reads the emitter to give tests an assertable history.
All three together are "the Nullable infrastructure". Each is independently useful.
MIT, do whatever you want with it.