Skip to content

Commit

Permalink
feat: useQuery
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker committed Feb 14, 2024
1 parent 0555a19 commit ee279cb
Show file tree
Hide file tree
Showing 11 changed files with 211 additions and 86 deletions.
182 changes: 123 additions & 59 deletions packages/core/src/controller/Controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type {
ErrorTypes,
SnapshotInterface,
DenormalizeCache,
Schema,
Denormalize,
Queryable,
SchemaArgs,
ResultCache,
} from '@data-client/normalizr';
import {
WeakEntityMap,
Expand Down Expand Up @@ -33,7 +35,7 @@ import ensurePojo from './ensurePojo.js';
import type { EndpointUpdateFunction } from './types.js';
import { initialState } from '../state/reducer/createReducer.js';
import selectMeta from '../state/selectMeta.js';
import type { ActionTypes, State } from '../types.js';
import type { ActionTypes, State, DenormalizeCache } from '../types.js';

export type GenericDispatch = (value: any) => Promise<void>;
export type DataClientDispatch = (value: ActionTypes) => Promise<void>;
Expand Down Expand Up @@ -85,6 +87,9 @@ export default class Controller<
globalCache = {
entities: {},
results: {},
queries: new Map(),
inputEndpointCache: {},
infer: new WeakEntityMap(),
},
}: ConstructorProps<D> = {}) {
this.dispatch = dispatch;
Expand Down Expand Up @@ -335,21 +340,42 @@ export default class Controller<
* Gets the (globally referentially stable) response for a given endpoint/args pair from state given.
* @see https://dataclient.io/docs/api/Controller#getResponse
*/
getResponse = <
getResponse<E extends EndpointInterface>(
endpoint: E,
...rest: readonly [null, State<unknown>]
): {
data: DenormalizeNullable<E['schema']>;
expiryStatus: ExpiryStatus;
expiresAt: number;
};

getResponse<E extends EndpointInterface>(
endpoint: E,
...rest: readonly [...Parameters<E>, State<unknown>]
): {
data: DenormalizeNullable<E['schema']>;
expiryStatus: ExpiryStatus;
expiresAt: number;
};

getResponse<
E extends Pick<EndpointInterface, 'key' | 'schema' | 'invalidIfStale'>,
Args extends readonly [...Parameters<E['key']>] | readonly [null],
>(
endpoint: E,
...rest: [...Args, State<unknown>]
...rest: readonly [
...(readonly [...Parameters<E['key']>] | readonly [null]),
State<unknown>,
]
): {
data: DenormalizeNullable<E['schema']>;
expiryStatus: ExpiryStatus;
expiresAt: number;
} => {
} {
const state = rest[rest.length - 1] as State<unknown>;
// this is typescript generics breaking
const args: any = rest
.slice(0, rest.length - 1)
// handle FormData
.map(ensurePojo) as Parameters<E['key']>;
const isActive = args.length !== 1 || args[0] !== null;
const key = isActive ? endpoint.key(...args) : '';
Expand All @@ -360,17 +386,25 @@ export default class Controller<

let invalidResults = false;
let results;
let resultCache: ResultCache;
if (cacheResults === undefined && endpoint.schema !== undefined) {
results = inferResults(
endpoint.schema,
args,
state.indexes,
state.entities,
);
if (!this.globalCache.inputEndpointCache[key])
this.globalCache.inputEndpointCache[key] = inferResults(
endpoint.schema,
args,
state.indexes,
state.entities,
);
results = this.globalCache.inputEndpointCache[key];

invalidResults = !validateInference(results);
if (!expiresAt && invalidResults) expiresAt = 1;
resultCache = this.globalCache.infer;
} else {
results = cacheResults;
if (!this.globalCache.results[key])
this.globalCache.results[key] = new WeakEntityMap();
resultCache = this.globalCache.results[key];
}

if (!isActive) {
Expand All @@ -396,19 +430,89 @@ export default class Controller<
};
}

if (!this.globalCache.results[key])
this.globalCache.results[key] = new WeakEntityMap();
return this.getSchemaResponse(
results,
schema as Exclude<E['schema'], undefined>,
args,
state,
expiresAt,
resultCache,
endpoint.invalidIfStale || invalidResults,
meta,
);
}

/**
* Queries the store for a Querable schema
* @see https://dataclient.io/docs/api/Controller#query
*/
query<S extends Queryable>(
schema: S,
...rest: readonly [
...SchemaArgs<S>,
Pick<State<unknown>, 'entities' | 'entityMeta'>,
]
): DenormalizeNullable<S> | undefined {
const state = rest[rest.length - 1] as State<unknown>;
// this is typescript generics breaking
const args: any = rest
.slice(0, rest.length - 1)
.map(ensurePojo) as SchemaArgs<S>;

// MEMOIZE inferResults - vary on schema + args
// NOTE: different orders can result in cache busting here; but since it's just a perf penalty we will allow for now
const key = JSON.stringify(args);
if (!this.globalCache.queries.has(schema)) {
this.globalCache.queries.set(schema, {});
}
const querySchemaCache = this.globalCache.queries.get(schema) as {
[key: string]: unknown;
};
if (!querySchemaCache[key])
querySchemaCache[key] = inferResults(
schema,
args,
state.indexes,
state.entities,
);
const results = querySchemaCache[key];
// END BLOCK

const data = denormalizeCached(
results,
schema,
state.entities,
this.globalCache.entities,
this.globalCache.infer,
args,
).data;
return typeof data === 'symbol' ? undefined : (data as any);
}

private getSchemaResponse<S extends Schema>(
input: any,
schema: S,
args: any,
state: State<unknown>,
expiresAt: number,
resultCache: ResultCache,
invalidIfStale: boolean,
meta: { error?: unknown; invalidated?: unknown } = {},
): {
data: DenormalizeNullable<S>;
expiryStatus: ExpiryStatus;
expiresAt: number;
} {
// second argument is false if any entities are missing
// eslint-disable-next-line prefer-const
const { data, paths } = denormalizeCached(
results,
input,
schema,
state.entities,
this.globalCache.entities,
this.globalCache.results[key],
resultCache,
args,
) as { data: DenormalizeNullable<E['schema']>; paths: Path[] };
) as { data: DenormalizeNullable<S>; paths: Path[] };
const invalidDenormalize = typeof data === 'symbol';

// fallback to entity expiry time
Expand All @@ -422,12 +526,11 @@ export default class Controller<
const expiryStatus =
meta?.invalidated || (invalidDenormalize && !meta?.error) ?
ExpiryStatus.Invalid
: invalidDenormalize || endpoint.invalidIfStale || invalidResults ?
ExpiryStatus.InvalidIfStale
: invalidDenormalize || invalidIfStale ? ExpiryStatus.InvalidIfStale
: ExpiryStatus.Valid;

return { data, expiryStatus, expiresAt };
};
}
}

// benchmark: https://www.measurethat.net/Benchmarks/Show/24691/0/min-reducer-vs-imperative-with-paths
Expand Down Expand Up @@ -473,42 +576,3 @@ function schemaHasEntity(schema: Schema): boolean {
}

export type { ErrorTypes };

class Snapshot<T = unknown> implements SnapshotInterface {
private state: State<T>;
private controller: Controller;
readonly fetchedAt: number;

constructor(controller: Controller, state: State<T>, fetchedAt = 0) {
this.state = state;
this.controller = controller;
this.fetchedAt = fetchedAt;
}

/*************** Data Access ***************/
/** @see https://dataclient.io/docs/api/Snapshot#getResponse */
getResponse = <
E extends Pick<EndpointInterface, 'key' | 'schema' | 'invalidIfStale'>,
Args extends readonly [...Parameters<E['key']>],
>(
endpoint: E,
...args: Args
): {
data: DenormalizeNullable<E['schema']>;
expiryStatus: ExpiryStatus;
expiresAt: number;
} => {
return this.controller.getResponse(endpoint, ...args, this.state);
};

/** @see https://dataclient.io/docs/api/Snapshot#getError */
getError = <
E extends Pick<EndpointInterface, 'key'>,
Args extends readonly [...Parameters<E['key']>],
>(
endpoint: E,
...args: Args
): ErrorTypes | undefined => {
return this.controller.getError(endpoint, ...args, this.state);
};
}
5 changes: 4 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@ export type {
EndpointInterface,
EntityInterface,
ResolveType,
DenormalizeCache,
EntityCache,
ResultCache,
DenormalizeNullable,
Denormalize,
Normalize,
NormalizeNullable,
FetchFunction,
EndpointExtraOptions,
Queryable,
SchemaArgs,
} from '@data-client/normalizr';
export { ExpiryStatus } from '@data-client/normalizr';
export {
Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { NormalizedIndex } from '@data-client/normalizr';
import type {
UpdateFunction,
AbstractInstanceType,
EntityCache,
ResultCache,
Queryable,
} from '@data-client/normalizr';
import type { ErrorTypes } from '@data-client/normalizr';

Expand Down Expand Up @@ -44,6 +47,24 @@ export interface State<T> {
readonly lastReset: number;
}

export interface DenormalizeCache {
entities: EntityCache;
results: {
[key: string]: ResultCache;
};
infer: ResultCache;

inputEndpointCache: {
[key: string]: unknown;
};
queries: Map<
Queryable,
{
[key: string]: unknown;
}
>;
}

export * from './actions.js';

export interface Manager<Actions = ActionTypes> {
Expand Down
3 changes: 2 additions & 1 deletion packages/endpoint/src/queryEndpoint.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
EntityTable,
NormalizedIndex,
Queryable,
SchemaSimple,
} from './interface.js';
import type { Denormalize, SchemaArgs } from './normal.js';
Expand All @@ -10,7 +11,7 @@ import type { Denormalize, SchemaArgs } from './normal.js';
* @see https://dataclient.io/rest/api/Query
*/
export class Query<
S extends SchemaSimple,
S extends Queryable,
P extends SchemaArgs<S> = SchemaArgs<S>,
R = Denormalize<S>,
> {
Expand Down
8 changes: 4 additions & 4 deletions packages/endpoint/src/schemas/__tests__/denormalize.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import {
denormalizeCached as denormalizeCore,
Schema,
DenormalizeCache,
EntityCache,
ResultCache,
WeakEntityMap,
Denormalize,
DenormalizeNullable,
INVALID,
} from '@data-client/normalizr';

export const denormalizeSimple = <S extends Schema>(
input: any,
schema: S | undefined,
entities: any,
entityCache: DenormalizeCache['entities'] = {},
resultCache: DenormalizeCache['results'][string] = new WeakEntityMap(),
entityCache: EntityCache = {},
resultCache: ResultCache = new WeakEntityMap(),
args: any[] = [],
): Denormalize<S> | DenormalizeNullable<S> | symbol =>
denormalizeCore(input, schema, entities, entityCache, resultCache, args)
Expand Down
11 changes: 8 additions & 3 deletions packages/normalizr/src/denormalize/denormalizeCached.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import GlobalCache from './globalCache.js';
import getUnvisit from './unvisit.js';
import type { Schema } from '../interface.js';
import type { DenormalizeNullable, DenormalizeCache, Path } from '../types.js';
import type {
DenormalizeNullable,
EntityCache,
Path,
ResultCache,
} from '../types.js';
import WeakEntityMap, { getEntities } from '../WeakEntityMap.js';

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function denormalize<S extends Schema>(
input: unknown,
schema: S | undefined,
entities: any,
entityCache: DenormalizeCache['entities'] = {},
resultCache: DenormalizeCache['results'][string] = new WeakEntityMap(),
entityCache: EntityCache = {},
resultCache: ResultCache = new WeakEntityMap(),
args: readonly any[] = [],
): {
data: DenormalizeNullable<S> | symbol;
Expand Down
Loading

1 comment on commit ee279cb

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: ee279cb Previous: 5771da3 Ratio
normalizeLong 380 ops/sec (±2.74%) 447 ops/sec (±1.68%) 1.18
infer All 9941 ops/sec (±0.52%) 9629 ops/sec (±1.67%) 0.97
denormalizeLong 309 ops/sec (±3.02%) 321 ops/sec (±2.22%) 1.04
denormalizeLong donotcache 849 ops/sec (±1.00%) 888 ops/sec (±0.46%) 1.05
denormalizeShort donotcache 500x 1378 ops/sec (±0.17%) 1351 ops/sec (±0.13%) 0.98
denormalizeShort 500x 960 ops/sec (±0.24%) 958 ops/sec (±0.24%) 1.00
denormalizeLong with mixin Entity 299 ops/sec (±0.42%) 303 ops/sec (±0.26%) 1.01
denormalizeLong withCache 7783 ops/sec (±0.19%) 6836 ops/sec (±0.26%) 0.88
denormalizeLongAndShort withEntityCacheOnly 1641 ops/sec (±0.41%) 1562 ops/sec (±0.31%) 0.95
denormalizeLong All withCache 7045 ops/sec (±0.19%) 6337 ops/sec (±0.17%) 0.90
denormalizeLong Query-sorted withCache 6776 ops/sec (±0.20%) 6647 ops/sec (±0.38%) 0.98
getResponse 5602 ops/sec (±1.07%) 4951 ops/sec (±0.88%) 0.88
getResponse (null) 3664090 ops/sec (±0.47%) 2888601 ops/sec (±0.21%) 0.79
getResponse (clear cache) 300 ops/sec (±0.64%) 292 ops/sec (±1.10%) 0.97
getSmallResponse 2255 ops/sec (±0.33%) 2328 ops/sec (±0.30%) 1.03
getResponse Collection 4991 ops/sec (±1.29%) 5124 ops/sec (±1.06%) 1.03
setLong 369 ops/sec (±3.10%) 437 ops/sec (±2.25%) 1.18
setLongWithMerge 175 ops/sec (±1.51%) 189 ops/sec (±0.36%) 1.08
setLongWithSimpleMerge 186 ops/sec (±1.50%) 202 ops/sec (±0.27%) 1.09

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.