Skip to content

Instantly share code, notes, and snippets.

@7iomka
Last active August 9, 2024 19:45
Show Gist options
  • Save 7iomka/7b57c21a780bcf3a2d5b937e0f59aa44 to your computer and use it in GitHub Desktop.
Save 7iomka/7b57c21a780bcf3a2d5b937e0f59aa44 to your computer and use it in GitHub Desktop.
Disposable
/* 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 = <A, R>(modelFactory: (factoryArgs: A) => R) => {
type ID = string;
type FactoryArgs = Parameters<typeof modelFactory>[0];
type RegionsKv = Record<ID, { owner: Node; ref: any | null }>;
const $regions = createStore<RegionsKv>({}); // <<--
const $modelsKv = createStore<Record<ID, any>>({}); // <<--
// 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<string[], any>((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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment