/* eslint-disable consistent-return */ /* eslint-disable effector/mandatory-scope-binding */ /* eslint-disable effector/no-watch */ import type { Node } from 'effector'; import { clearNode, attach, // combine, createEffect, createNode, createStore, withRegion, } from 'effector'; export const KvFactory = (modelFactory: (factoryArgs: A) => R) => { type ID = string; type FactoryArgs = Parameters[0]; type RegionsKv = Record; const $regions = createStore({}); // <<-- const $modelsKv = createStore>({}); // <<-- // create const createFx = attach({ source: $regions, mapParams: ({ modelId, params }: { modelId: ID; params: FactoryArgs }, regions) => ({ modelId, params, regions, }), effect: createEffect< { modelId: string; params: FactoryArgs; regions: RegionsKv; }, { modelId: string; regions: RegionsKv; model: any } >(({ modelId, params, regions }) => { const owner = createNode(); regions[modelId] = { owner, ref: null }; withRegion(owner, () => { regions[modelId].ref = modelFactory(params); }); const model = regions[modelId].ref; return { modelId, model, regions }; }), }); $modelsKv.on(createFx.doneData, (currentModels, { modelId, model }) => ({ ...currentModels, [modelId]: model, })); $regions.on(createFx.doneData, (_, { regions }) => ({ ...regions })); // dispose const disposeFx = attach({ source: $regions, mapParams: (modelId: ID, regions) => ({ modelId, regions }), effect: createEffect< { modelId: string; regions: RegionsKv }, { modelId: string; regions: RegionsKv } >(({ modelId, regions }) => { if (!regions[modelId]) { throw new Error(`${modelId} not found in regions`); } clearNode(regions[modelId].owner); const { [modelId]: _, ...withoutModelID } = regions; return { modelId, regions: withoutModelID }; }), }); const disposeManyFx = createEffect((modelIds) => modelIds.forEach((modelId) => disposeFx(modelId)), ); $modelsKv.on(disposeFx.doneData, (currentModels, { modelId }) => { const newModels = { ...currentModels }; delete newModels[modelId]; return newModels; }); $regions.on(disposeFx.doneData, (_, { regions }) => ({ ...regions })); // reset const resetFx = attach({ source: $regions, mapParams: (_: void, regions) => ({ regions }), effect: createEffect<{ regions: RegionsKv }, void>(({ regions }) => { if (Object.keys(regions).length === 0) return; if (process.env.NEXT_PUBLIC_APP_STAGE !== 'production') { console.log('disposeAll works'); } Object.keys(regions).forEach((modelId) => { if (!regions[modelId]) { console.warn(`${modelId} not found in regions`); return; } const { owner } = regions[modelId]; clearNode(owner); regions[modelId].ref = null; // @ts-ignore no-error here, just cleanup delete regions[modelId].owner; delete regions[modelId].ref; delete regions[modelId]; }); }), }); $modelsKv.on(resetFx.doneData, () => ({})); $regions.on(resetFx.doneData, () => ({})); // if (process.env.NEXT_PUBLIC_APP_STAGE !== 'production') { // combine({ // regions: $regions.map(Object.keys), // models: $modelsKv.map(Object.keys), // }).watch(console.log); // } return { $modelsKv, $models: $modelsKv.map(Object.values), $modelsIds: $modelsKv.map(Object.keys), createOne: createFx, createMany: createEffect< { params: FactoryArgs; modelId: ID }[], { modelId: string; regions: RegionsKv; model: any }[] >(async (items) => Promise.all(items.map(({ modelId, params }) => createFx({ modelId, params }))), ), disposeOne: disposeFx, disposeMany: disposeManyFx, disposeAll: resetFx, }; }; /** Example // Factory const SummFactory = ({ a, b }: { a: number; b: number }) => { const $summ = createStore(a + b); return { $summ }; }; // KvModelFactory const KvModelFactory = KvFactory(SummFactory); // Creating a single disposable model KvModelFactory.createOne({ modelId: '1', params: { a: 1, b: 1 } }); const getDataFx = createEffect(() => Promise.resolve([ { a: 3, b: 5 }, { a: 4, b: 6 }, { a: 6, b: 4 }, ]), ); // Create a disposable model for each element of the array at once sample({ clock: getDataFx.doneData, fn: (items) => items.map((item, idx) => ({ modelId: String(idx), params: { ...item } })), target: KvModelFactory.createMany, }); getDataFx(); // Cleanup separate getDataFx.doneData.watch(() => { setTimeout(() => { KvModelFactory.disposeOne('1'); }); }); // But basically, when new data comes in, you should probably clean everything - that would be the only simple solution setTimeout(() => KvModelFactory.disposeAll(), 2000); */ // TODO: somehow correct the typing to not require params for `createOne`, `createMany` if the factory has no arguments