Refactor/reacrhitecture to fsd+ #49
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import { createSingleton } from './createSingleton';
|
||||
|
||||
describe('createSingleton', () => {
|
||||
it('does not call the factory until the first get (lazy)', () => {
|
||||
const factory = vi.fn(() => ({ id: 1 }));
|
||||
createSingleton(factory);
|
||||
expect(factory).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('constructs on first get and memoizes the instance', () => {
|
||||
const factory = vi.fn(() => ({ id: 1 }));
|
||||
const singleton = createSingleton(factory);
|
||||
|
||||
const a = singleton.get();
|
||||
const b = singleton.get();
|
||||
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it('rebuilds a fresh instance after reset', () => {
|
||||
let count = 0;
|
||||
const singleton = createSingleton(() => ({ id: ++count }));
|
||||
|
||||
const first = singleton.get();
|
||||
singleton.reset();
|
||||
const second = singleton.get();
|
||||
|
||||
expect(first).not.toBe(second);
|
||||
expect(second.id).toBe(2);
|
||||
});
|
||||
|
||||
it('runs teardown once, with the live instance, on reset', () => {
|
||||
const teardown = vi.fn();
|
||||
const singleton = createSingleton(() => ({ id: 1 }), teardown);
|
||||
|
||||
const instance = singleton.get();
|
||||
singleton.reset();
|
||||
|
||||
expect(teardown).toHaveBeenCalledTimes(1);
|
||||
expect(teardown).toHaveBeenCalledWith(instance);
|
||||
});
|
||||
|
||||
it('treats reset before any get as a no-op (no teardown, no throw)', () => {
|
||||
const teardown = vi.fn();
|
||||
const singleton = createSingleton(() => ({ id: 1 }), teardown);
|
||||
|
||||
expect(() => singleton.reset()).not.toThrow();
|
||||
expect(teardown).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not run teardown again on a second consecutive reset', () => {
|
||||
const teardown = vi.fn();
|
||||
const singleton = createSingleton(() => ({ id: 1 }), teardown);
|
||||
|
||||
singleton.get();
|
||||
singleton.reset();
|
||||
singleton.reset();
|
||||
|
||||
expect(teardown).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('works without a teardown', () => {
|
||||
const singleton = createSingleton(() => ({ id: 1 }));
|
||||
|
||||
singleton.get();
|
||||
expect(() => singleton.reset()).not.toThrow();
|
||||
expect(singleton.get().id).toBe(1);
|
||||
});
|
||||
|
||||
it('caches a falsy instance value without re-running the factory', () => {
|
||||
const factory = vi.fn(() => undefined);
|
||||
const singleton = createSingleton<undefined>(factory);
|
||||
|
||||
singleton.get();
|
||||
singleton.get();
|
||||
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* A lazily-constructed singleton accessor pair.
|
||||
*/
|
||||
export interface Singleton<T> {
|
||||
/**
|
||||
* Returns the instance, constructing it on the first call and reusing it
|
||||
* thereafter.
|
||||
*/
|
||||
get: () => T;
|
||||
/**
|
||||
* Tears down the current instance (if built) and clears it, so the next
|
||||
* `get()` rebuilds. Used by specs to avoid shared state between tests.
|
||||
*/
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standardizes the lazy `getX()` / `__resetX()` singleton pattern used by the
|
||||
* app's stores.
|
||||
*
|
||||
* The instance is built on the first `get()` and reused afterwards; `reset()`
|
||||
* runs the optional teardown against the live instance and clears it. Building
|
||||
* lazily keeps the owning module inert at import — construction happens only on
|
||||
* first access, never at module eval.
|
||||
*
|
||||
* @param factory - Builds the instance on first access.
|
||||
* @param teardown - Optional cleanup run against the live instance on reset
|
||||
* (e.g. disposing an `$effect.root` via the instance's `destroy()`).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const catalog = createSingleton(() => new FontCatalogStore({ limit: 50 }), c => c.destroy());
|
||||
* export const getFontCatalog = catalog.get;
|
||||
* export const __resetFontCatalog = catalog.reset;
|
||||
* ```
|
||||
*/
|
||||
export function createSingleton<T>(factory: () => T, teardown?: (instance: T) => void): Singleton<T> {
|
||||
let instance: T | undefined;
|
||||
let initialized = false;
|
||||
|
||||
return {
|
||||
get: () => {
|
||||
if (!initialized) {
|
||||
instance = factory();
|
||||
initialized = true;
|
||||
}
|
||||
return instance as T;
|
||||
},
|
||||
reset: () => {
|
||||
if (initialized) {
|
||||
teardown?.(instance as T);
|
||||
}
|
||||
instance = undefined;
|
||||
initialized = false;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -137,6 +137,20 @@ export {
|
||||
type PerspectiveManager,
|
||||
} from './createPerspectiveManager/createPerspectiveManager.svelte';
|
||||
|
||||
/**
|
||||
* Lazy singletons
|
||||
*/
|
||||
export {
|
||||
/**
|
||||
* Lazy `getX()` / `__resetX()` singleton accessor factory
|
||||
*/
|
||||
createSingleton,
|
||||
/**
|
||||
* Singleton accessor pair type
|
||||
*/
|
||||
type Singleton,
|
||||
} from './createSingleton/createSingleton';
|
||||
|
||||
/*
|
||||
* BaseQueryStore is intentionally NOT re-exported here.
|
||||
* It pulls @tanstack/query-core, so routing it through this leaf barrel would
|
||||
|
||||
Reference in New Issue
Block a user