Skip to content

Commit

Permalink
Add implementation of freshChain
Browse files Browse the repository at this point in the history
  • Loading branch information
igorkamyshev committed Sep 4, 2023
1 parent 5a4ca2d commit 843686f
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 10 deletions.
69 changes: 69 additions & 0 deletions packages/atomic-router/src/__tests__/fresh.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { createQuery } from '@farfetched/core';
import { allSettled, createWatch, fork } from 'effector';
import { describe, expect, test, vi } from 'vitest';
import { createDefer } from '../defer';
import { freshChain } from '../fresh';

describe('freshChain', () => {
test('should start query after beforeOpen call and call openOn, respect $stale state', async () => {
const firstDefer = createDefer();
const secondDefer = createDefer();

const handler = vi
.fn()
.mockImplementationOnce(() => firstDefer.promise)
.mockImplementationOnce(() => secondDefer.promise);

const query = createQuery({ handler });
const chain = freshChain(query);

const scope = fork();

const openOnListener = vi.fn();
createWatch({ unit: chain.openOn, fn: openOnListener, scope });
const cancelOnListener = vi.fn();
createWatch({ unit: chain.cancelOn, fn: cancelOnListener, scope });

// First open — execute
allSettled(chain.beforeOpen, {
scope,
params: { params: undefined, query: {} },
});

expect(handler).toBeCalledTimes(1);
expect(openOnListener).not.toBeCalled();
expect(cancelOnListener).not.toBeCalled();

firstDefer.resolve(null);
await allSettled(scope);

expect(openOnListener).toBeCalledTimes(1);
expect(cancelOnListener).not.toBeCalled();

// Second open — just openOp immediately
await allSettled(chain.beforeOpen, {
scope,
params: { params: undefined, query: {} },
});

expect(handler).toBeCalledTimes(1);
expect(openOnListener).toBeCalledTimes(2);
expect(cancelOnListener).not.toBeCalled();

// Third open — execute, because of changed params
allSettled(chain.beforeOpen, {
scope,
params: { params: undefined, query: { some: 2 } },
});

expect(handler).toBeCalledTimes(2);
expect(openOnListener).toBeCalledTimes(2);
expect(cancelOnListener).not.toBeCalled();

secondDefer.resolve(null);
await allSettled(scope);

expect(openOnListener).toBeCalledTimes(3);
expect(cancelOnListener).not.toBeCalled();
});
});
21 changes: 20 additions & 1 deletion packages/atomic-router/src/fresh.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { type Query } from '@farfetched/core';
import { type RouteParamsAndQuery } from 'atomic-router';
import { createEvent, sample } from 'effector';

import { type ChainProtocol } from './chain.ptorocol';

Expand All @@ -13,5 +15,22 @@ export function freshChain<RouteParams extends Record<string, any>>(
export function freshChain(
query: Query<any, any, any, any>
): ChainProtocol<any> {
return {} as any;
const beforeOpen = createEvent<RouteParamsAndQuery<any>>();
const openOn = createEvent<any>();
const cancelOn = createEvent<any>();

sample({ clock: beforeOpen, target: query.refresh });
sample({
clock: [
query.finished.success,
query.__.lowLevelAPI.refreshSkipDueToFreshness,
],
target: openOn,
});
sample({
clock: [query.finished.failure, query.finished.skip],
target: cancelOn,
});

return { beforeOpen, openOn, cancelOn };
}
38 changes: 32 additions & 6 deletions packages/core/src/query/create_headless_query.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { createStore, sample, createEvent, Store, attach } from 'effector';
import {
createStore,
sample,
createEvent,
Store,
attach,
split,
} from 'effector';

import { Contract } from '../contract/type';
import { InvalidDataError } from '../errors/type';
Expand Down Expand Up @@ -138,12 +145,30 @@ export function createHeadlessQuery<
until: operation.$enabled,
});

const refreshSkipDueToFreshness = createEvent<void>();

const { haveToStart, __: haveToSkip } = split(
sample({
clock: postponedRefresh,
source: { stale: $stale, latestParams: operation.__.$latestParams },
fn: ({ stale, latestParams }, params) => ({
haveToStart: stale || !isEqual(params ?? null, latestParams),
params,
}),
}),
{
haveToStart: ({ haveToStart }) => haveToStart,
}
);

sample({
clock: haveToSkip,
fn: () => null,
target: refreshSkipDueToFreshness,
});
sample({
clock: postponedRefresh,
source: { stale: $stale, latestParams: operation.__.$latestParams },
filter: ({ stale, latestParams }, params) =>
stale || !isEqual(params ?? null, latestParams),
fn: (_, params) => params,
clock: haveToStart,
fn: ({ params }) => params,
target: operation.start,
});

Expand Down Expand Up @@ -219,6 +244,7 @@ export function createHeadlessQuery<
...operation,
__: {
...operation.__,
lowLevelAPI: { ...operation.__.lowLevelAPI, refreshSkipDueToFreshness },
experimentalAPI: { attach: attachProtocol },
},
'@@unitShape': unitShapeProtocol,
Expand Down
12 changes: 11 additions & 1 deletion packages/core/src/query/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,18 @@ export interface QueryMeta<Data, InitialData> {
initialData: InitialData;
}

export type QueryExtraLowLevelAPI = {
refreshSkipDueToFreshness: Event<void>;
};

export interface Query<Params, Data, Error, InitialData = null>
extends RemoteOperation<Params, Data, Error, QueryMeta<Data, InitialData>> {
extends RemoteOperation<
Params,
Data,
Error,
QueryMeta<Data, InitialData>,
QueryExtraLowLevelAPI
> {
/**
* Start fetching data if it is absent or stale.
*/
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/remote_operation/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ interface DefaultMeta {
name: string;
}

export interface RemoteOperation<Params, Data, Error, Meta> {
export interface RemoteOperation<
Params,
Data,
Error,
Meta,
ExtraLowLevelAPI = Record<string, never>
> {
/**
* Reactive current request status
*
Expand Down Expand Up @@ -91,7 +97,7 @@ export interface RemoteOperation<Params, Data, Error, Meta> {
paramsAreMeaningless: boolean;
revalidate: Event<{ params: Params; refresh: boolean }>;
startWithMeta: Event<{ params: Params; meta: ExecutionMeta }>;
};
} & ExtraLowLevelAPI;
experimentalAPI?: {
attach: <Source, NewParams>(config: {
source: Store<Source>;
Expand Down

0 comments on commit 843686f

Please sign in to comment.