Skip to content

Commit

Permalink
feat: useQuery
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker committed Feb 15, 2024
1 parent dab41ae commit dbc825a
Show file tree
Hide file tree
Showing 15 changed files with 272 additions and 66 deletions.
4 changes: 4 additions & 0 deletions examples/benchmark/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Endpoint,
Entity,
normalize,
WeakEntityMap,
} from './dist/index.js';
import { printStatus } from './printStatus.js';
import {
Expand Down Expand Up @@ -113,6 +114,9 @@ export default function addReducerSuite(suite) {
controller.globalCache = {
entities: {},
results: {},
queries: new Map(),
inputEndpointCache: {},
infer: new WeakEntityMap(),
};
return controller.getResponse(getProject, cachedState);
})
Expand Down
207 changes: 177 additions & 30 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,22 +340,52 @@ 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;
} => {
};

getResponse(
endpoint: EndpointInterface,
...rest: readonly [...unknown[], State<unknown>]
): {
data: unknown;
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)
.map(ensurePojo) as Parameters<E['key']>;
// handle FormData
.map(ensurePojo);
const isActive = args.length !== 1 || args[0] !== null;
const key = isActive ? endpoint.key(...args) : '';
const cacheResults = isActive ? state.results[key] : undefined;
Expand All @@ -360,17 +395,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 @@ -389,26 +432,92 @@ export default class Controller<
: cacheResults && !endpoint.invalidIfStale ? ExpiryStatus.Valid
: ExpiryStatus.InvalidIfStale,
expiresAt: expiresAt || 0,
} as {
data: DenormalizeNullable<E['schema']>;
expiryStatus: ExpiryStatus;
expiresAt: number;
};
}

if (!this.globalCache.results[key])
this.globalCache.results[key] = new WeakEntityMap();
return this.getSchemaResponse(
results,
schema as Exclude<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 +531,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,20 +595,48 @@ 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;
} => {
return this.controller.getResponse(endpoint, ...args, this.state);
};

getResponse<
E extends Pick<EndpointInterface, 'key' | 'schema' | 'invalidIfStale'>,
>(
endpoint: E,
...args: readonly [...Parameters<E['key']>] | readonly [null]
): {
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'>,
Expand All @@ -511,4 +647,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
Loading

1 comment on commit dbc825a

@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: dbc825a Previous: 5771da3 Ratio
normalizeLong 368 ops/sec (±2.69%) 447 ops/sec (±1.68%) 1.21
infer All 9717 ops/sec (±0.63%) 9629 ops/sec (±1.67%) 0.99
denormalizeLong 317 ops/sec (±2.58%) 321 ops/sec (±2.22%) 1.01
denormalizeLong donotcache 839 ops/sec (±1.94%) 888 ops/sec (±0.46%) 1.06
denormalizeShort donotcache 500x 1327 ops/sec (±0.40%) 1351 ops/sec (±0.13%) 1.02
denormalizeShort 500x 949 ops/sec (±0.35%) 958 ops/sec (±0.24%) 1.01
denormalizeLong with mixin Entity 304 ops/sec (±0.33%) 303 ops/sec (±0.26%) 1.00
denormalizeLong withCache 7593 ops/sec (±0.30%) 6836 ops/sec (±0.26%) 0.90
denormalizeLongAndShort withEntityCacheOnly 1591 ops/sec (±0.90%) 1562 ops/sec (±0.31%) 0.98
denormalizeLong All withCache 7005 ops/sec (±0.28%) 6337 ops/sec (±0.17%) 0.90
denormalizeLong Query-sorted withCache 7000 ops/sec (±0.45%) 6647 ops/sec (±0.38%) 0.95
getResponse 5699 ops/sec (±1.33%) 4951 ops/sec (±0.88%) 0.87
getResponse (null) 4032593 ops/sec (±0.29%) 2888601 ops/sec (±0.21%) 0.72
getResponse (clear cache) 303 ops/sec (±0.44%) 292 ops/sec (±1.10%) 0.96
getSmallResponse 2265 ops/sec (±0.28%) 2328 ops/sec (±0.30%) 1.03
getSmallInferredResponse 1940 ops/sec (±1.20%) 1765 ops/sec (±0.34%) 0.91
getResponse Query-sorted 3064 ops/sec (±0.37%) 687 ops/sec (±1.09%) 0.22
getResponse Collection 5211 ops/sec (±1.93%) 5124 ops/sec (±1.06%) 0.98
setLong 358 ops/sec (±3.16%) 437 ops/sec (±2.25%) 1.22
setLongWithMerge 167 ops/sec (±2.13%) 189 ops/sec (±0.36%) 1.13
setLongWithSimpleMerge 174 ops/sec (±1.75%) 202 ops/sec (±0.27%) 1.16

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

Please sign in to comment.