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,
|
type PerspectiveManager,
|
||||||
} from './createPerspectiveManager/createPerspectiveManager.svelte';
|
} 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.
|
* BaseQueryStore is intentionally NOT re-exported here.
|
||||||
* It pulls @tanstack/query-core, so routing it through this leaf barrel would
|
* It pulls @tanstack/query-core, so routing it through this leaf barrel would
|
||||||
|
|||||||
Reference in New Issue
Block a user