Skip to content

Commit

Permalink
perf: implement weakAtomFamily (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
tien authored Aug 15, 2024
1 parent c2c3a61 commit ceb8723
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/tiny-worms-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@reactive-dot/react": patch
---

Implemented `atomFamily` using `WeakMap`: this change allows for the automatic garbage collection of unused values, improving memory efficiency and overall performance.
6 changes: 3 additions & 3 deletions packages/react/src/stores/accounts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { withAtomFamilyErrorCatcher } from "../utils/jotai.js";
import { weakAtomFamily, withAtomFamilyErrorCatcher } from "../utils/jotai.js";
import { chainSpecDataAtomFamily } from "./client.js";
import { walletsAtom } from "./wallets.js";
import {
Expand All @@ -7,9 +7,9 @@ import {
type PolkadotAccount,
} from "@reactive-dot/core";
import type { Atom } from "jotai";
import { atomFamily, atomWithObservable } from "jotai/utils";
import { atomWithObservable } from "jotai/utils";

export const accountsAtom = atomFamily(
export const accountsAtom = weakAtomFamily(
(chainId: ChainId): Atom<PolkadotAccount[] | Promise<PolkadotAccount[]>> =>
withAtomFamilyErrorCatcher(
accountsAtom,
Expand Down
7 changes: 4 additions & 3 deletions packages/react/src/stores/block.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { weakAtomFamily } from "../utils/jotai.js";
import { clientAtomFamily } from "./client.js";
import { type ChainId, getBlock } from "@reactive-dot/core";
import { atomFamily, atomWithObservable } from "jotai/utils";
import { atomWithObservable } from "jotai/utils";
import { from } from "rxjs";
import { switchMap } from "rxjs/operators";

export const finalizedBlockAtomFamily = atomFamily((chainId: ChainId) =>
export const finalizedBlockAtomFamily = weakAtomFamily((chainId: ChainId) =>
atomWithObservable((get) =>
from(get(clientAtomFamily(chainId))).pipe(
switchMap((client) => getBlock(client, { tag: "finalized" })),
),
),
);

export const bestBlockAtomFamily = atomFamily((chainId: ChainId) =>
export const bestBlockAtomFamily = weakAtomFamily((chainId: ChainId) =>
atomWithObservable((get) =>
from(get(clientAtomFamily(chainId))).pipe(
switchMap((client) => getBlock(client, { tag: "best" })),
Expand Down
8 changes: 4 additions & 4 deletions packages/react/src/stores/client.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { weakAtomFamily } from "../utils/jotai.js";
import { chainConfigsAtom } from "./config.js";
import type { ChainId } from "@reactive-dot/core";
import { getClient, ReDotError } from "@reactive-dot/core";
import { atom } from "jotai";
import { atomFamily } from "jotai/utils";

export const clientAtomFamily = atomFamily((chainId: ChainId) =>
export const clientAtomFamily = weakAtomFamily((chainId: ChainId) =>
atom(async (get) => {
const chainConfig = get(chainConfigsAtom)[chainId];

Expand All @@ -16,15 +16,15 @@ export const clientAtomFamily = atomFamily((chainId: ChainId) =>
}),
);

export const chainSpecDataAtomFamily = atomFamily((chainId: ChainId) =>
export const chainSpecDataAtomFamily = weakAtomFamily((chainId: ChainId) =>
atom(async (get) => {
const client = await get(clientAtomFamily(chainId));

return client.getChainSpecData();
}),
);

export const typedApiAtomFamily = atomFamily((chainId: ChainId) =>
export const typedApiAtomFamily = weakAtomFamily((chainId: ChainId) =>
atom(async (get) => {
const config = get(chainConfigsAtom)[chainId];

Expand Down
8 changes: 4 additions & 4 deletions packages/react/src/stores/query.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { withAtomFamilyErrorCatcher } from "../utils/jotai.js";
import { weakAtomFamily, withAtomFamilyErrorCatcher } from "../utils/jotai.js";
import { stringify } from "../utils/vanilla.js";
import { typedApiAtomFamily } from "./client.js";
import {
Expand All @@ -10,10 +10,10 @@ import {
type QueryInstruction,
} from "@reactive-dot/core";
import { atom, type Atom, type WritableAtom } from "jotai";
import { atomFamily, atomWithObservable, atomWithRefresh } from "jotai/utils";
import { atomWithObservable, atomWithRefresh } from "jotai/utils";
import { from, switchMap, type Observable } from "rxjs";

const instructionPayloadAtomFamily = atomFamily(
const instructionPayloadAtomFamily = weakAtomFamily(
(param: {
chainId: ChainId;
instruction: Exclude<
Expand Down Expand Up @@ -76,7 +76,7 @@ export function getQueryInstructionPayloadAtoms(

// TODO: should be memoized within render function instead
// https://github.com/pmndrs/jotai/discussions/1553
export const queryPayloadAtomFamily = atomFamily(
export const queryPayloadAtomFamily = weakAtomFamily(
(param: { chainId: ChainId; query: Query }): Atom<unknown> =>
withAtomFamilyErrorCatcher(
queryPayloadAtomFamily,
Expand Down
90 changes: 84 additions & 6 deletions packages/react/src/utils/jotai.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,102 @@
import { QueryError } from "@reactive-dot/core";
import type { Atom, Getter } from "jotai";
import type { AtomFamily } from "jotai/vanilla/utils/atomFamily";
import { Observable } from "rxjs";
import { catchError } from "rxjs/operators";

type WeakAtomFamily<TParam, TAtomType> = ((param: TParam) => TAtomType) & {
remove: (param: TParam) => void;
};

export function weakAtomFamily<TParam, TAtomType extends Atom<unknown>>(
initializeAtom: (param: TParam) => TAtomType,
areEqual?: (a: TParam, b: TParam) => boolean,
): WeakAtomFamily<TParam, TAtomType> {
// TODO: should use `Symbol` as `WeakMap` key instead
// https://bugzilla.mozilla.org/show_bug.cgi?id=1710433
const keys = new Map<TParam, object>();
const atoms = new WeakMap<object, TAtomType>();

const getKey = (param: TParam) => {
if (areEqual === undefined) {
return keys.get(param);
}

for (const [key, value] of keys) {
if (areEqual(key, param)) {
return value;
}
}

return undefined;
};

const deleteKey = (param: TParam) => {
if (areEqual === undefined) {
return keys.delete(param);
}

for (const key of keys.keys()) {
if (areEqual(key, param)) {
return keys.delete(param);
}
}

return false;
};

return Object.assign(
(param: TParam) => {
const key = getKey(param);

if (key !== undefined) {
const atom = atoms.get(key);

if (atom !== undefined) {
return atom;
}
}

const newKey = {};
keys.set(param, newKey);

const newAtom = initializeAtom(param);
atoms.set(newKey, newAtom);

return newAtom;
},
{
remove: (param: TParam) => {
const key = getKey(param);

if (key === undefined) {
return;
}

deleteKey(param);
atoms.delete(key);
},
},
);
}

export class AtomFamilyError extends QueryError {
constructor(
readonly atomFamily: AtomFamily<unknown, unknown>,
readonly atomFamily: WeakAtomFamily<unknown, unknown>,
readonly param: unknown,
message: string | undefined,
options?: ErrorOptions,
) {
super(message, options);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
static fromAtomFamilyError<TError, TAtomFamily extends AtomFamily<any, any>>(
static fromAtomFamilyError<
TError,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TAtomFamily extends WeakAtomFamily<any, any>,
>(
error: TError,
atomFamily: TAtomFamily,
param: TAtomFamily extends AtomFamily<infer Param, infer _>
param: TAtomFamily extends WeakAtomFamily<infer Param, infer _>
? Param
: unknown,
message?: string,
Expand All @@ -36,7 +114,7 @@ export function withAtomFamilyErrorCatcher<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TAtomCreator extends (read: TRead, ...args: any[]) => Atom<unknown>,
>(
atomFamily: AtomFamily<TParam, unknown>,
atomFamily: WeakAtomFamily<TParam, unknown>,
param: TParam,
atomCreator: TAtomCreator,
): TAtomCreator {
Expand Down

0 comments on commit ceb8723

Please sign in to comment.