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 dab41ae commit 615d4c0
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 52 deletions.
183 changes: 158 additions & 25 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 @@ -487,19 +590,38 @@ class Snapshot<T = unknown> implements SnapshotInterface {

/*************** Data Access ***************/
/** @see https://dataclient.io/docs/api/Snapshot#getResponse */
getResponse = <
getResponse<E extends EndpointInterface>(
endpoint: E,
...args: readonly [null]
): {
data: DenormalizeNullable<E['schema']>;
expiryStatus: ExpiryStatus;
expiresAt: number;
};

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

getResponse<
E extends Pick<EndpointInterface, 'key' | 'schema' | 'invalidIfStale'>,
Args extends readonly [...Parameters<E['key']>],
>(
endpoint: E,
...args: Args
...args: readonly [...Parameters<E['key']>] | readonly [null]
): {
data: DenormalizeNullable<E['schema']>;
expiryStatus: ExpiryStatus;
expiresAt: number;
} => {
} {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return this.controller.getResponse(endpoint, ...args, this.state);
};
}

/** @see https://dataclient.io/docs/api/Snapshot#getError */
getError = <
Expand All @@ -511,4 +633,15 @@ class Snapshot<T = unknown> implements SnapshotInterface {
): ErrorTypes | undefined => {
return this.controller.getError(endpoint, ...args, this.state);
};

/**
* Queries the store for a Querable schema
* @see https://dataclient.io/docs/api/Snapshot#query
*/
query<S extends Queryable>(
schema: S,
...args: SchemaArgs<S>
): DenormalizeNullable<S> | undefined {
return this.controller.query(schema, ...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 615d4c0

@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: 615d4c0 Previous: 5771da3 Ratio
normalizeLong 404 ops/sec (±2.62%) 447 ops/sec (±1.68%) 1.11
infer All 10507 ops/sec (±0.53%) 9629 ops/sec (±1.67%) 0.92
denormalizeLong 327 ops/sec (±3.42%) 321 ops/sec (±2.22%) 0.98
denormalizeLong donotcache 897 ops/sec (±1.04%) 888 ops/sec (±0.46%) 0.99
denormalizeShort donotcache 500x 1435 ops/sec (±0.54%) 1351 ops/sec (±0.13%) 0.94
denormalizeShort 500x 996 ops/sec (±0.56%) 958 ops/sec (±0.24%) 0.96
denormalizeLong with mixin Entity 321 ops/sec (±1.20%) 303 ops/sec (±0.26%) 0.94
denormalizeLong withCache 8300 ops/sec (±0.58%) 6836 ops/sec (±0.26%) 0.82
denormalizeLongAndShort withEntityCacheOnly 1741 ops/sec (±0.61%) 1562 ops/sec (±0.31%) 0.90
denormalizeLong All withCache 6781 ops/sec (±0.63%) 6337 ops/sec (±0.17%) 0.93
denormalizeLong Query-sorted withCache 6729 ops/sec (±0.59%) 6647 ops/sec (±0.38%) 0.99
getResponse 5757 ops/sec (±1.38%) 4951 ops/sec (±0.88%) 0.86
getResponse (null) 3897327 ops/sec (±0.58%) 2888601 ops/sec (±0.21%) 0.74
getResponse (clear cache) 322 ops/sec (±0.75%) 292 ops/sec (±1.10%) 0.91
getSmallResponse 2396 ops/sec (±0.54%) 2328 ops/sec (±0.30%) 0.97
getResponse Collection 5467 ops/sec (±1.21%) 5124 ops/sec (±1.06%) 0.94
setLong 395 ops/sec (±2.83%) 437 ops/sec (±2.25%) 1.11
setLongWithMerge 189 ops/sec (±1.49%) 189 ops/sec (±0.36%) 1
setLongWithSimpleMerge 199 ops/sec (±1.49%) 202 ops/sec (±0.27%) 1.02

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

Please sign in to comment.