Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/mst-query/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/mst-query/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mst-query",
"version": "4.1.1",
"version": "4.1.2",
"description": "Query library for mobx-state-tree",
"source": "src/index.ts",
"type": "module",
Expand Down
59 changes: 47 additions & 12 deletions packages/mst-query/src/MstQueryHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,15 @@ type NotifyOptions = {
onMutate?: boolean;
};

export type CacheOptions = {
cacheTime?: number;
cacheKey?: string;
};

type OnResponseOptions = {
shouldUpdate?: boolean;
updateRecorder?: IPatchRecorder;
};
} & CacheOptions;

export class DisposedError extends Error {}

Expand All @@ -65,6 +70,10 @@ export class QueryObserver {
return this.query.__MstQueryHandler;
}

get queryStore() {
return this.handler.queryClient.queryStore;
}

subscribe() {
if (this.query) {
this.handler.addQueryObserver(this);
Expand Down Expand Up @@ -116,12 +125,25 @@ export class QueryObserver {
}

if (options.initialData && !options.isMounted) {
this.handler.hydrate(options.initialData, options);

const isStale = isDataStale(options.initialDataUpdatedAt, options.staleTime);
if (!isStale) {
this.handler.hydrate(options);
} else {
if (isStale) {
this.handler.queryWhenChanged(options);
}
} else if (options.cacheKey) {
const cacheEntry = this.queryStore.getQueryData(
this.handler.type,
options.cacheKey,
);
if (cacheEntry) {
this.handler.hydrate(cacheEntry.data, options);
}

const isStale = !cacheEntry || isDataStale(cacheEntry.cachedAt, options.staleTime);
if (isStale) {
this.handler.queryWhenChanged(options);
}
} else {
if (!options.isRequestEqual) {
this.query.setData(null);
Expand Down Expand Up @@ -269,7 +291,7 @@ export class MstQueryHandler {
return;
}

const notInitialized = !this.isFetched && !this.isLoading && !this.cachedAt;
const notInitialized = !this.isFetched && !this.isLoading;
Comment thread
k-ode marked this conversation as resolved.
if (!options.isMounted) {
if (notInitialized) {
return this.model.query(options);
Expand Down Expand Up @@ -314,7 +336,7 @@ export class MstQueryHandler {

query(options: any = {}): Promise<() => any> {
return this.run(options).then(
(result) => this.onSuccess(result),
(result) => this.onSuccess(result, options),
(err) => this.onError(err),
);
}
Expand Down Expand Up @@ -389,7 +411,7 @@ export class MstQueryHandler {

let data;
if (shouldUpdate) {
data = this.setData(result);
data = this.setData(result, options);
} else {
data = this.prepareData(result);
}
Expand Down Expand Up @@ -527,7 +549,9 @@ export class MstQueryHandler {
return merge(data, this.type.properties.data, this.queryClient.config.env, true);
}

setData(data: any) {
setData(data: any, options: CacheOptions = {}) {
const opts = options ?? this.options;

this.model.__MstQueryHandlerAction(() => {
if (isStateTreeNode(data)) {
if (isReferenceType(this.type.properties.data)) {
Expand All @@ -542,20 +566,31 @@ export class MstQueryHandler {
this.queryClient.config.env,
);
}

if (opts.cacheKey && this.model.data) {
this.queryClient.queryStore.setQueryData(
this.type,
opts.cacheKey,
this.model,
opts.cacheTime,
Comment thread
k-ode marked this conversation as resolved.
);
}
});

return this.model.data;
}

hydrate(options: any) {
const { initialData, request, pagination } = options;
hydrate(data: any, options: any) {
const { enabled, request, pagination } = options;

this.setVariables({ request, pagination });
if (enabled) {
this.setVariables({ request, pagination });
}
this.options.meta = options.meta;

this.isLoading = false;

this.setData(initialData);
this.setData(data, options);
this.cachedAt = new Date();
}

Expand Down
1 change: 1 addition & 0 deletions packages/mst-query/src/QueryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const defaultConfig = {
env: {},
queryOptions: {
staleTime: 0,
cacheTime: 0,
refetchOnMount: 'if-stale',
refetchOnChanged: 'all',
},
Expand Down
46 changes: 43 additions & 3 deletions packages/mst-query/src/QueryStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,22 @@ import {
getIdentifier,
IAnyComplexType,
} from 'mobx-state-tree';
import { observable, action, makeObservable} from 'mobx';
import { observable, action, makeObservable } from 'mobx';

export const getKey = (type: IAnyComplexType, id: string | number) => {
return `${type.name}:${id}`;
};

type QueryCacheEntry = {
cachedAt: number;
data: any;
timeout: number;
};

export class QueryStore {
#scheduledGc = null as null | number;
#queryClient: any;
#queryData = new Map() as Map<string, QueryCacheEntry>;
#cache = new Map() as Map<string, any>;

models = new Map() as Map<string, any>;
Expand All @@ -31,9 +38,38 @@ export class QueryStore {
this.#queryClient = queryClient;
}

getQueryData(type: IAnyComplexType, key: string) {
return this.#queryData.get(getKey(type, key));
}

setQueryData(
type: IAnyComplexType,
key: string,
model: Instance<IAnyModelType>,
cacheTime: number = 0,
) {
const existingEntry = this.#queryData.get(getKey(type, key));
if (existingEntry) {
window.clearTimeout(existingEntry.timeout);
}

const cacheEntry = {
cachedAt: Date.now(),
data: model.data,
timeout: window.setTimeout(() => {
this.removeQueryData(type, key);
}, cacheTime),
};
this.#queryData.set(getKey(type, key), cacheEntry);
}

removeQueryData(type: IAnyComplexType, key: string) {
this.#queryData.delete(getKey(type, key));
}

getQueries<T extends IAnyModelType>(
queryDef: T,
matcherFn: (query: Instance<T>) => boolean = () => true
matcherFn: (query: Instance<T>) => boolean = () => true,
): Instance<T>[] {
let results = [];
const arr = this.#cache.get(queryDef.name) ?? [];
Expand Down Expand Up @@ -66,7 +102,7 @@ export class QueryStore {
'delete',
getType(obj),
getIdentifier(obj),
obj
obj,
);
}

Expand All @@ -93,6 +129,10 @@ export class QueryStore {

collectSeenIdentifiers(query.data, seenIdentifiers);
collectSeenIdentifiers(query.request, seenIdentifiers);

for (let [_, queryData] of this.#queryData) {
collectSeenIdentifiers(queryData.data, seenIdentifiers);
}
}
}

Expand Down
6 changes: 3 additions & 3 deletions packages/mst-query/src/create.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { types, IAnyType, flow, SnapshotIn, Instance } from 'mobx-state-tree';
import { MstQueryHandler } from './MstQueryHandler';
import { type CacheOptions, MstQueryHandler } from './MstQueryHandler';

type TypeOrFrozen<T> = T extends IAnyType ? T : ReturnType<typeof types.frozen>;

Expand Down Expand Up @@ -141,8 +141,8 @@ export function createQuery<TData extends IAnyType, TRequest extends IAnyType>(
invalidate() {
self.__MstQueryHandler.invalidate();
},
setData(data: any) {
return self.__MstQueryHandler.setData(data);
setData(data: any, options?: CacheOptions) {
return self.__MstQueryHandler.setData(data, options);
},
abort: self.__MstQueryHandler.abort,
}));
Expand Down
4 changes: 2 additions & 2 deletions packages/mst-query/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from './create';
import { Context } from './QueryClientProvider';
import { QueryClient } from './QueryClient';
import { EmptyPagination, EmptyRequest, QueryObserver } from './MstQueryHandler';
import { CacheOptions, EmptyPagination, EmptyRequest, QueryObserver } from './MstQueryHandler';
import { useEvent } from './utils';

function mergeWithDefaultOptions(key: string, options: any, queryClient: QueryClient<any>) {
Expand All @@ -32,7 +32,7 @@ type QueryOptions<T extends Instance<QueryReturnType>> = {
initialData?: any;
initialDataUpdatedAt?: number;
meta?: { [key: string]: any };
};
} & CacheOptions;

export function useQuery<T extends Instance<QueryReturnType>>(
query: T,
Expand Down
79 changes: 76 additions & 3 deletions packages/mst-query/tests/mstQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,55 @@ test('useQuery - reactive request', async () => {
configureMobx({ enforceActions: 'observed' });
});

test('useQuery - cacheKey and cacheTime', async () => {
const { render, q, queryClient } = setup();

configureMobx({ enforceActions: 'never' });

const getItem = vi.fn(({ request }) => api.getItem({ request }));
const testApi = {
...api,
getItem,
};

let id = observable.box('test');
const Comp = observer(() => {
const { query } = useQuery(q.itemQuery, {
request: { id: id.get() },
cacheKey: id.get(),
cacheTime: 25,
staleTime: 25,
meta: { getItem: testApi.getItem },
});
return <div></div>;
});
render(<Comp />);

await wait(0);
expect(q.itemQuery.data?.id).toBe('test');

id.set('different-test');
await wait(0);
expect(q.itemQuery.data?.id).toBe('different-test');
expect(q.itemQuery.variables.request?.id).toBe('different-test');

id.set('test');
await wait(0);
expect(q.itemQuery.data?.id).toBe('test');
expect(getItem).toHaveBeenCalledTimes(2);

await wait(50);
queryClient.queryStore.runGc();
await wait(0);

id.set('different-test');
await wait(0);
expect(q.itemQuery.data?.id).toBe('different-test');
expect(getItem).toHaveBeenCalledTimes(3);

configureMobx({ enforceActions: 'observed' });
});

test('onQueryMore', async () => {
const { render, q } = setup();

Expand Down Expand Up @@ -900,7 +949,7 @@ test('useQuery should not run when initialData is passed and staleTime is larger
expect(q.itemQuery.data?.id).toBe('different-test');
expect(q.itemQuery.variables.request?.id).toBe('different-test');
expect(loadingStates).toEqual([true, false]);
expect(dataStates).toEqual(['test', null, "different-test"]);
expect(dataStates).toEqual(['test', null, 'different-test']);

isLoadingReaction();
dataReaction();
Expand Down Expand Up @@ -978,6 +1027,30 @@ test('useQuery should run when initialData is given and invalidate is called', a
configureMobx({ enforceActions: 'observed' });
});

test('useQuery should set initialData when enabled is false', async () => {
const { render, q } = setup();

configureMobx({ enforceActions: 'never' });

let id = observable.box('test');
const initialData = await api.getItem({ request: { id: id.get() } });

const Comp = observer(() => {
useQuery(q.itemQuery, {
initialData,
enabled: false,
});
return <div></div>;
});
render(<Comp />);

await wait(0);

expect(q.itemQuery.data?.id).toBe('test');

configureMobx({ enforceActions: 'observed' });
});

test('refetchOnRequestChanged function', async () => {
const { render, q } = setup();

Expand All @@ -995,9 +1068,9 @@ test('refetchOnRequestChanged function', async () => {
const Comp = observer(() => {
useQuery(q.itemQuery, {
request: { id: id.get(), id2: id2.get() },
refetchOnChanged({ prevRequest }) {
refetchOnChanged({ prevRequest }) {
return prevRequest.id !== id.get();
},
},
staleTime: 5000,
meta: { getItem: testApi.getItem },
});
Expand Down