Skip to content

Commit

Permalink
Merge pull request #184 from Jujulego/feat/control-refresh-on-first-load
Browse files Browse the repository at this point in the history
Control refresh on first load
  • Loading branch information
julien-capellari committed Sep 9, 2022
2 parents 31ff2f7 + e78aeec commit 8491910
Show file tree
Hide file tree
Showing 15 changed files with 373 additions and 42 deletions.
2 changes: 2 additions & 0 deletions .yarn/versions/c249cfb3.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
undecided:
- "@jujulego/aegis-core"
4 changes: 2 additions & 2 deletions packages/aegis/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@jujulego/aegis",
"version": "1.0.0-rc.4",
"version": "1.0.0-rc.5",
"license": "MIT",
"author": "Julien Capellari <julien.capellari@google.com>",
"repository": {
Expand Down Expand Up @@ -35,7 +35,7 @@
"regenerator-runtime": "^0.13.0"
},
"dependencies": {
"@jujulego/aegis-core": "^1.0.0-rc.2"
"@jujulego/aegis-core": "^1.0.0-rc.3"
},
"devDependencies": {
"@jujulego/flow": "1.1.0",
Expand Down
55 changes: 46 additions & 9 deletions packages/aegis/src/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,33 @@ import {
} from './wrappers';

// Types
export type AegisRefresh = 'always' | 'if-unknown' | 'never';

export interface AegisOptions {
/**
* Indicates if aegis should refresh function call.
* Accepted values:
* - 'never': never refresh
* - 'if-unknown': refresh only if data is unknown (=> not in store)
* - 'always': always refresh
*
* This only affects method call, not further .refresh() calls
*
* @default if-unknown
*/
refresh?: AegisRefresh;

/**
* Strategy to use when refreshing while a request is already running
* Accepted values:
* - 'keep': keeps running query, and do not start a new one
* - 'replace': cancel running query, and start a new one
*
* @default keep
*/
strategy?: RefreshStrategy;
}

export interface AegisEntityItem<D, I extends AegisId> {
/**
* Returns an AegisItem by item's id
Expand All @@ -33,9 +60,9 @@ export interface AegisEntityItem<D, I extends AegisId> {
*
* @param fetcher
* @param id Function extracting id from fetcher arguments
* @param strategy Refresh strategy to use on first load
* @param options
*/
query<A extends unknown[]>(fetcher: (...args: A) => PromiseLike<D>, id: AegisIdExtractor<A, I>, strategy?: RefreshStrategy): (...args: A) => AegisItem<D, I> & Refreshable<D>;
query<A extends unknown[]>(fetcher: (...args: A) => PromiseLike<D>, id: AegisIdExtractor<A, I>, options?: AegisOptions): (...args: A) => AegisItem<D, I> & Refreshable<D>;

/**
* Mutates (or creates) an unknown item (an item we don't know the id)
Expand Down Expand Up @@ -79,9 +106,9 @@ export interface AegisEntityList<D> {
* Fetcher arguments will be prepended by list's key
*
* @param fetcher
* @param strategy Refresh strategy to use on first load
* @param options
*/
query<A extends unknown[]>(fetcher: (...args: A) => PromiseLike<D[]>, strategy?: RefreshStrategy): (key: string, ...args: A) => AegisList<D> & Refreshable<D[]>;
query<A extends unknown[]>(fetcher: (...args: A) => PromiseLike<D[]>, options?: AegisOptions): (key: string, ...args: A) => AegisList<D> & Refreshable<D[]>;
}

export interface AegisEntity<D, I extends AegisId> {
Expand All @@ -102,14 +129,19 @@ export function $entity<D, I extends AegisId>(name: string, store: Store, extrac

// Item methods
function queryItem<A extends unknown[]>(fetcher: (...args: A) => PromiseLike<D>): (...args: A) => AegisUnknownItem<D, I>;
function queryItem<A extends unknown[]>(fetcher: (...args: A) => PromiseLike<D>, id: AegisIdExtractor<A, I>, strategy?: RefreshStrategy): (...args: A) => AegisItem<D, I> & Refreshable<D>;
function queryItem<A extends unknown[]>(fetcher: (...args: A) => PromiseLike<D>, id?: AegisIdExtractor<A, I>, strategy: RefreshStrategy = 'keep') {
function queryItem<A extends unknown[]>(fetcher: (...args: A) => PromiseLike<D>, id: AegisIdExtractor<A, I>, options?: AegisOptions): (...args: A) => AegisItem<D, I> & Refreshable<D>;
function queryItem<A extends unknown[]>(fetcher: (...args: A) => PromiseLike<D>, id?: AegisIdExtractor<A, I>, options: AegisOptions = {}) {
const { refresh = 'if-unknown', strategy = 'keep' } = options;

return (...args: A) => {
if (!id) {
return $item<D, I>(entity, $queryfy(fetcher(...args)));
} else {
const item = $item(entity, id(...args), () => $queryfy(fetcher(...args)));
item.refresh(strategy);

if (refresh === 'always' || (refresh === 'if-unknown' && item.$item.state === 'unknown')) {
item.refresh(strategy);
}

return item;
}
Expand Down Expand Up @@ -146,10 +178,15 @@ export function $entity<D, I extends AegisId>(name: string, store: Store, extrac
}

// List methods
function queryList<A extends unknown[]>(fetcher: (...args: A) => PromiseLike<D[]>, strategy: RefreshStrategy = 'keep') {
function queryList<A extends unknown[]>(fetcher: (...args: A) => PromiseLike<D[]>, options: AegisOptions = {}) {
const { refresh = 'if-unknown', strategy = 'keep' } = options;

return (key: string, ...args: A) => {
const list = $list(entity, key, () => $queryfy(fetcher(...args)));
list.$list.refresh(() => $queryfy(fetcher(...args)), strategy);

if (refresh === 'always' || (refresh === 'if-unknown' && list.$list.state === 'unknown')) {
list.$list.refresh(() => $queryfy(fetcher(...args)), strategy);
}

return list;
};
Expand Down
222 changes: 218 additions & 4 deletions packages/aegis/tests/entity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ describe('$entity', () => {
// Wrap fetcher
const q2 = new Query<Test>();
const querier = jest.fn((id: string) => q2);
const fetcher = entity.$item.query(querier, (id) => id, 'keep');
const fetcher = entity.$item.query(querier, (id) => id, { strategy: 'keep' });

// Call
const $item = fetcher('test');
Expand All @@ -92,6 +92,113 @@ describe('$entity', () => {
expect($item.isLoading).toBe(true);
expect($item.$item.manager.query).toBe(q1);
});

it('should wrap fetcher so it is not called if item is already cached or loaded [refresh: if-unknown] (only for known item)', () => {
const entity = $entity('Test', store, (test: Test) => test.id);

// Wrap fetcher
const q1 = new Query<Test>();
const querier = jest.fn((id: string) => q1);
const fetcher = entity.$item.query(querier, (id) => id, { refresh: 'if-unknown' });

// Call while unknown
let $item = fetcher('test');

expect(querier).toHaveBeenCalled();

expect($item.isLoading).toBe(true);
expect($item.$item.manager.query).toBe(q1);

// Complete first query
q1.complete({ id: 'test', success: true });

expect($item.isLoading).toBe(false);
expect($item.$item.state).toBe('loaded');

// Reset mock
const q2 = new Query<Test>();

querier.mockReset();
querier.mockReturnValue(q2);

// Call while loaded
$item = fetcher('test');

expect(querier).not.toHaveBeenCalled();

expect($item.isLoading).toBe(false);
expect($item.$item.manager.query).toBe(q1);
});

it('should wrap fetcher so it is always called [refresh: always] (only for known item)', () => {
const entity = $entity('Test', store, (test: Test) => test.id);

// Wrap fetcher
const q1 = new Query<Test>();
const querier = jest.fn((id: string) => q1);
const fetcher = entity.$item.query(querier, (id) => id, { refresh: 'always' });

// Call while unknown
let $item = fetcher('test');

expect(querier).toHaveBeenCalled();

expect($item.isLoading).toBe(true);
expect($item.$item.manager.query).toBe(q1);

// Complete first query
q1.complete({ id: 'test', success: true });

expect($item.isLoading).toBe(false);
expect($item.$item.state).toBe('loaded');

// Reset mock
const q2 = new Query<Test>();

querier.mockReset();
querier.mockReturnValue(q2);

// Call while loaded
$item = fetcher('test');

expect(querier).toHaveBeenCalled();

expect($item.isLoading).toBe(true);
expect($item.$item.manager.query).toBe(q2);
});

it('should wrap fetcher so it is never called [refresh: never] (only for known item)', () => {
const entity = $entity('Test', store, (test: Test) => test.id);

// Wrap fetcher
const query = new Query<Test>();
const querier = jest.fn((id: string) => query);
const fetcher = entity.$item.query(querier, (id) => id, { refresh: 'never' });

// Call while unknown
let $item = fetcher('test');

expect(querier).not.toHaveBeenCalled();

expect($item.isLoading).toBe(false);
expect($item.$item.manager.query).toBeUndefined();

// Fill store
$item.data = { id: 'test', success: true };

expect($item.$item.state).toBe('cached');

// Reset mock
querier.mockReset();

// Call while cached
$item = fetcher('test');

expect(querier).not.toHaveBeenCalled();

expect($item.isLoading).toBe(false);
expect($item.$item.manager.query).toBeUndefined();
});
});

describe('$entity().$item.mutation', () => {
Expand Down Expand Up @@ -218,7 +325,7 @@ describe('$entity', () => {
// Wrap fetcher
const q2 = new Query<Test[]>();
const querier = jest.fn((_a: number, _b: string) => q2);
const fetcher = entity.$list.query(querier, 'keep');
const fetcher = entity.$list.query(querier, { strategy: 'keep' });

// Call
const $list = fetcher('test', 1, 'successful');
Expand All @@ -228,6 +335,113 @@ describe('$entity', () => {
expect($list.isLoading).toBe(true);
expect($list.$list.manager.query).toBe(q1);
});

it('should wrap fetcher so it is not called if item is already cached or loaded [refresh: if-unknown]', () => {
const entity = $entity('Test', store, (test: Test) => test.id);

// Wrap fetcher
const q1 = new Query<Test[]>();
const querier = jest.fn(() => q1);
const fetcher = entity.$list.query(querier, { refresh: 'if-unknown' });

// Call while unknown
let $list = fetcher('test');

expect(querier).toHaveBeenCalled();

expect($list.isLoading).toBe(true);
expect($list.$list.manager.query).toBe(q1);

// Complete first query
q1.complete([{ id: 'test', success: true }]);

expect($list.isLoading).toBe(false);
expect($list.$list.state).toBe('loaded');

// Reset mock
const q2 = new Query<Test[]>();

querier.mockReset();
querier.mockReturnValue(q2);

// Call while loaded
$list = fetcher('test');

expect(querier).not.toHaveBeenCalled();

expect($list.isLoading).toBe(false);
expect($list.$list.manager.query).toBe(q1);
});

it('should wrap fetcher so it is always called [refresh: always]', () => {
const entity = $entity('Test', store, (test: Test) => test.id);

// Wrap fetcher
const q1 = new Query<Test[]>();
const querier = jest.fn(() => q1);
const fetcher = entity.$list.query(querier, { refresh: 'always' });

// Call while unknown
let $list = fetcher('test');

expect(querier).toHaveBeenCalled();

expect($list.isLoading).toBe(true);
expect($list.$list.manager.query).toBe(q1);

// Complete first query
q1.complete([{ id: 'test', success: true }]);

expect($list.isLoading).toBe(false);
expect($list.$list.state).toBe('loaded');

// Reset mock
const q2 = new Query<Test[]>();

querier.mockReset();
querier.mockReturnValue(q2);

// Call while loaded
$list = fetcher('test');

expect(querier).toHaveBeenCalled();

expect($list.isLoading).toBe(true);
expect($list.$list.manager.query).toBe(q2);
});

it('should wrap fetcher so it is never called [refresh: never]', () => {
const entity = $entity('Test', store, (test: Test) => test.id);

// Wrap fetcher
const query = new Query<Test[]>();
const querier = jest.fn(() => query);
const fetcher = entity.$list.query(querier, { refresh: 'never' });

// Call while unknown
let $list = fetcher('test');

expect(querier).not.toHaveBeenCalled();

expect($list.isLoading).toBe(false);
expect($list.$list.manager.query).toBeUndefined();

// Fill store
$list.data = [{ id: 'test', success: true }];

expect($list.$list.state).toBe('cached');

// Reset mock
querier.mockReset();

// Call while cached
$list = fetcher('test');

expect(querier).not.toHaveBeenCalled();

expect($list.isLoading).toBe(false);
expect($list.$list.manager.query).toBeUndefined();
});
});
});

Expand All @@ -236,8 +450,8 @@ describe('$entity', () => {
const entity = $entity('Test', store, (test: Test) => test.id)
.$protocol(({ $item, $list }) => ({
create: $item.query(() => new Query()),
getById: $item.query((id: string) => new Query(), (id) => id, 'keep'),
findAll: $list.query(() => new Query(), 'replace'),
getById: $item.query((id: string) => new Query(), (id) => id, { strategy: 'keep' }),
findAll: $list.query(() => new Query(), { strategy: 'replace' }),
updateById: $item.mutate((id: string, body: unknown) => new Query(), (id) => id),
deleteById: $item.delete((id: string) => new Query(), (id) => id),
}));
Expand Down
4 changes: 2 additions & 2 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@
"test": "jest"
},
"peerDependencies": {
"@jujulego/aegis": "1.0.0-rc.4",
"@jujulego/aegis": "1.0.0-rc.5",
"regenerator-runtime": "^0.13.0"
},
"devDependencies": {
"@jujulego/aegis": "1.0.0-rc.4",
"@jujulego/aegis": "1.0.0-rc.5",
"@jujulego/flow": "1.1.0",
"@swc/cli": "0.1.57",
"@swc/core": "1.2.239",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@jujulego/aegis-core",
"version": "1.0.0-rc.2",
"version": "1.0.0-rc.3",
"license": "MIT",
"author": "Julien Capellari <julien.capellari@google.com>",
"repository": {
Expand Down

0 comments on commit 8491910

Please sign in to comment.