Skip to content

enhance: ImmutableJS support moved to /immutable export #3468

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
4 changes: 1 addition & 3 deletions packages/core/src/controller/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,8 +544,6 @@ export default class Controller<
};
}

// second argument is false if any entities are missing

const { data, paths } = this.memo.denormalize(
schema,
input,
Expand Down Expand Up @@ -652,7 +650,7 @@ function entityExpiresAt(
},
) {
let expiresAt = Infinity;
for (const { pk, key } of paths) {
for (const { key, pk } of paths) {
const entityExpiry = entitiesMeta[key]?.[pk]?.expiresAt;
// expiresAt will always resolve to false with any comparison
if (entityExpiry < expiresAt) expiresAt = entityExpiry;
Expand Down
20 changes: 11 additions & 9 deletions packages/core/src/state/GCPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ export class GCPolicy implements GCInterface {
if (key)
this.endpointCount.set(key, (this.endpointCount.get(key) ?? 0) + 1);
paths.forEach(path => {
if (!this.entityCount.has(path.key)) {
this.entityCount.set(path.key, new Map<string, number>());
const { key, pk } = path;
if (!this.entityCount.has(key)) {
this.entityCount.set(key, new Map<string, number>());
}
const instanceCount = this.entityCount.get(path.key)!;
instanceCount.set(path.pk, (instanceCount.get(path.pk) ?? 0) + 1);
const instanceCount = this.entityCount.get(key)!;
instanceCount.set(pk, (instanceCount.get(pk) ?? 0) + 1);
});

// decrement
Expand All @@ -68,18 +69,19 @@ export class GCPolicy implements GCInterface {
}
}
paths.forEach(path => {
if (!this.entityCount.has(path.key)) {
const { key, pk } = path;
if (!this.entityCount.has(key)) {
return;
}
const instanceCount = this.entityCount.get(path.key)!;
const entityCount = instanceCount.get(path.pk)!;
const instanceCount = this.entityCount.get(key)!;
const entityCount = instanceCount.get(pk)!;
if (entityCount !== undefined) {
if (entityCount <= 1) {
instanceCount.delete(path.pk);
instanceCount.delete(pk);
// queue for cleanup
this.entitiesQ.push(path);
} else {
instanceCount.set(path.pk, entityCount - 1);
instanceCount.set(pk, entityCount - 1);
}
}
});
Expand Down
16 changes: 1 addition & 15 deletions packages/endpoint/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,7 @@ export type { Array, Invalidate, Collection, DefaultArgs } from './schema.js';
export { default as Entity } from './schemas/Entity.js';
export { default as EntityMixin } from './schemas/EntityMixin.js';
export { default as validateRequired } from './validateRequired.js';
export type {
EndpointInterface,
ReadEndpoint,
MutateEndpoint,
Schema,
IQueryDelegate,
INormalizeDelegate,
SnapshotInterface,
ExpiryStatusInterface,
SchemaSimple,
SchemaClass,
PolymorphicInterface,
Queryable,
Mergeable,
} from './interface.js';
export * from './interface.js';
export type { EntityFields } from './schemas/EntityFields.js';
export type {
AbstractInstanceType,
Expand Down
23 changes: 19 additions & 4 deletions packages/endpoint/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,24 +113,37 @@ export interface Visit {
creating?: boolean;
}

/** Used in denormalize. Lookup to find an entity in the store table */
export interface EntityPath {
key: string;
pk: string;
}

export type IndexPath = [key: string, index: string, value: string];
export type EntitiesPath = [key: string];

/** Returns true if a circular reference is found */
export interface CheckLoop {
(entityKey: string, pk: string, input: object): boolean;
}

/** Get Array of entities with map function applied */
/** Get all normalized entities of one type from store */
export interface GetEntities {
(key: string): { readonly [pk: string]: any } | undefined;
}
/** Get normalized Entity from store */
export interface GetEntity {
(entityKey: string | symbol): { readonly [pk: string]: any } | undefined;
(entityKey: string | symbol, pk: string | number): any;
(key: string, pk: string): any;
}
/** Get PK using an Entity Index */
export interface GetIndex {
/** getIndex('User', 'username', 'ntucker') */
(entityKey: string, field: string, value: string): string | undefined;
(...path: IndexPath): string | undefined;
}

/** Accessors to the currently processing state while building query */
export interface IQueryDelegate {
getEntities: GetEntities;
getEntity: GetEntity;
getIndex: GetIndex;
/** Return to consider results invalid */
Expand All @@ -141,6 +154,8 @@ export interface IQueryDelegate {
export interface INormalizeDelegate {
/** Action meta-data for this normalize call */
readonly meta: { fetchedAt: number; date: number; expiresAt: number };
/** Get all normalized entities of one type from store */
getEntities: GetEntities;
/** Gets any previously normalized entity from store */
getEntity: GetEntity;
/** Updates an entity using merge lifecycles when it has previously been set */
Expand Down
4 changes: 2 additions & 2 deletions packages/endpoint/src/schemas/All.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default class AllSchema<

queryKey(args: any, unvisit: any, delegate: IQueryDelegate): any {
if (this.isSingleSchema) {
const entitiesEntry = delegate.getEntity(this.schema.key);
const entitiesEntry = delegate.getEntities(this.schema.key);
// we must wait until there are entries for any 'All' query to be Valid
if (entitiesEntry === undefined) return delegate.INVALID;
return Object.values(entitiesEntry).map(
Expand All @@ -36,7 +36,7 @@ export default class AllSchema<
let found = false;
const list = Object.values(this.schema as Record<string, any>).flatMap(
(schema: EntityInterface) => {
const entitiesEntry = delegate.getEntity(schema.key);
const entitiesEntry = delegate.getEntities(schema.key);
if (entitiesEntry === undefined) return [];
found = true;
return Object.entries(entitiesEntry).map(([key, entity]) => ({
Expand Down
6 changes: 3 additions & 3 deletions packages/endpoint/src/schemas/Collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,9 @@ export default class CollectionSchema<

queryKey(args: Args, unvisit: unknown, delegate: IQueryDelegate): any {
if (this.argsKey) {
const id = this.pk(undefined, undefined, '', args);
const pk = this.pk(undefined, undefined, '', args);
// ensure this actually has entity or we shouldn't try to use it in our query
if (delegate.getEntity(this.key, id)) return id;
if (delegate.getEntity(this.key, pk)) return pk;
}
}

Expand Down Expand Up @@ -326,7 +326,7 @@ function normalizeCreate(
// parent is args when not nested
const filterCollections = (this.createCollectionFilter as any)(...args);
// add to any collections that match this
const entities = delegate.getEntity(this.key);
const entities = delegate.getEntities(this.key);
if (entities)
Object.keys(entities).forEach(collectionPk => {
if (!filterCollections(JSON.parse(collectionPk))) return;
Expand Down
12 changes: 6 additions & 6 deletions packages/endpoint/src/schemas/EntityMixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,9 +327,9 @@ export default function EntityMixin<TBase extends Constructor>(
delegate: IQueryDelegate,
): any {
if (!args[0]) return;
const id = queryKeyCandidate(this, args, delegate);
const pk = queryKeyCandidate(this, args, delegate);
// ensure this actually has entity or we shouldn't try to use it in our query
if (id && delegate.getEntity(this.key, id)) return id;
if (pk && delegate.getEntity(this.key, pk)) return pk;
}

static denormalize<T extends typeof EntityMixin>(
Expand Down Expand Up @@ -484,8 +484,8 @@ function queryKeyCandidate(
// Was able to infer the entity's primary key from params
if (id !== undefined && id !== '') return id;
// now attempt lookup in indexes
const indexName = indexFromParams(args[0], schema.indexes);
if (!indexName) return;
const value = (args[0] as Record<string, any>)[indexName];
return delegate.getIndex(schema.key, indexName, value);
const field = indexFromParams(args[0], schema.indexes);
if (!field) return;
const value = (args[0] as Record<string, any>)[field];
return delegate.getIndex(schema.key, field, value);
}
23 changes: 14 additions & 9 deletions packages/endpoint/src/schemas/__tests__/All.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
MemoCache,
denormalize,
INVALID,
PlainDelegate,
} from '@data-client/normalizr';
import { DelegateImmutable } from '@data-client/normalizr/immutable';
import { IDEntity } from '__tests__/new';

import { schema } from '../..';
Expand Down Expand Up @@ -101,15 +103,16 @@ describe.each([[]])(`${schema.All.name} normalization (%s)`, () => {
});

describe.each([
['direct', <T>(data: T) => data, <T>(data: T) => data],
['direct', <T>(data: T) => data, <T>(data: T) => data, PlainDelegate],
[
'immutable',
fromJSState,
(v: any) => (typeof v?.toJS === 'function' ? v.toJS() : v),
DelegateImmutable,
],
])(
`${schema.Array.name} denormalization (%s)`,
(_, createInput, createOutput) => {
(_, createInput, createOutput, MyDelegate) => {
test('denormalizes a single entity', () => {
class Cat extends IDEntity {}
const state: State<unknown> = createInput({
Expand All @@ -123,7 +126,9 @@ describe.each([
indexes: {},
}) as any;
const sch = new schema.All(Cat);
expect(new Controller().get(sch, state)).toMatchSnapshot();
expect(
new Controller({ memo: new MemoCache(MyDelegate) }).get(sch, state),
).toMatchSnapshot();
});

test('denormalizes nested in object', () => {
Expand All @@ -140,7 +145,7 @@ describe.each([
});
// use memocache because we don't support 'object' schemas in controller yet
expect(
new MemoCache().query(catSchema, [], state).data,
new MemoCache(MyDelegate).query(catSchema, [], state).data,
).toMatchSnapshot();
});

Expand All @@ -156,7 +161,7 @@ describe.each([
},
indexes: {},
});
const value = new MemoCache().query(catSchema, [], state).data;
const value = new MemoCache(MyDelegate).query(catSchema, [], state).data;
expect(value).not.toEqual(expect.any(Symbol));
if (typeof value === 'symbol' || value === undefined) return;
expect(createOutput(value.results)).toMatchSnapshot();
Expand All @@ -177,7 +182,7 @@ describe.each([
},
indexes: {},
});
const value = new MemoCache().query(catSchema, [], state).data;
const value = new MemoCache(MyDelegate).query(catSchema, [], state).data;
expect(value).not.toEqual(expect.any(Symbol));
if (typeof value === 'symbol' || value === undefined) return;
expect(createOutput(value.results).length).toBe(2);
Expand Down Expand Up @@ -244,7 +249,7 @@ describe.each([
},
indexes: {},
});
const value = new MemoCache().query(catSchema, [], state).data;
const value = new MemoCache(MyDelegate).query(catSchema, [], state).data;
expect(createOutput(value)).toEqual(expect.any(Symbol));
});

Expand Down Expand Up @@ -276,7 +281,7 @@ describe.each([
},
indexes: {},
});
const value = new MemoCache().query(listSchema, [], state).data;
const value = new MemoCache(MyDelegate).query(listSchema, [], state).data;
expect(createOutput(value)).toEqual(expect.any(Symbol));
});

Expand Down Expand Up @@ -339,7 +344,7 @@ describe.each([
},
indexes: {},
});
const value = new MemoCache().query(listSchema, [], state).data;
const value = new MemoCache(MyDelegate).query(listSchema, [], state).data;
expect(value).not.toEqual(expect.any(Symbol));
if (typeof value === 'symbol') return;
expect(value).toMatchSnapshot();
Expand Down
33 changes: 22 additions & 11 deletions packages/endpoint/src/schemas/__tests__/Query.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// eslint-env jest
import { MemoCache } from '@data-client/normalizr';
import { MemoCache, PlainDelegate } from '@data-client/normalizr';
import { DelegateImmutable } from '@data-client/normalizr/immutable';
import { useQuery, useSuspense, __INTERNAL__ } from '@data-client/react';
import { RestEndpoint } from '@data-client/rest';
import { IDEntity } from '__tests__/new';
Expand All @@ -26,13 +27,14 @@ class User extends IDEntity {
}

describe.each([
['direct', <T>(data: T) => data, <T>(data: T) => data],
['direct', <T>(data: T) => data, <T>(data: T) => data, PlainDelegate],
[
'immutable',
fromJSState,
(v: any) => (typeof v?.toJS === 'function' ? v.toJS() : v),
DelegateImmutable,
],
])(`input (%s)`, (_, createInput, createOutput) => {
])(`input (%s)`, (_, createInput, createOutput, MyDelegate) => {
const SCHEMA_CASES = [
['All', new schema.Object({ results: new schema.All(User) })],
[
Expand Down Expand Up @@ -76,7 +78,7 @@ describe.each([
},
});
const users: DenormalizeNullable<typeof sortedUsers> | symbol =
new MemoCache().query(sortedUsers, [], state).data;
new MemoCache(MyDelegate).query(sortedUsers, [], state).data;
expect(users).not.toEqual(expect.any(Symbol));
if (typeof users === 'symbol') return;
expect(users && users[0].name).toBe('Zeta');
Expand All @@ -101,21 +103,26 @@ describe.each([
},
});
expect(
new MemoCache().query(sortedUsers, [{ asc: true }], state).data,
new MemoCache(MyDelegate).query(sortedUsers, [{ asc: true }], state)
.data,
).toMatchSnapshot();
});

test('denormalizes should not be found when no entities are present', () => {
const state = {
const state = createInput({
...initialState,
entities: {
DOG: {
1: { id: '1', name: 'Milo' },
2: { id: '2', name: 'Jake' },
},
},
};
const { data } = new MemoCache().query(sortedUsers, [], state);
});
const { data } = new MemoCache(MyDelegate).query(
sortedUsers,
[],
state,
);

expect(createOutput(data)).not.toEqual(expect.any(Array));
});
Expand Down Expand Up @@ -152,20 +159,24 @@ describe.each([
});
const totalCount:
| DenormalizeNullable<typeof userCountByAdmin>
| symbol = new MemoCache().query(userCountByAdmin, [], state).data;
| symbol = new MemoCache(MyDelegate).query(
userCountByAdmin,
[],
state,
).data;

expect(totalCount).toBe(4);
const nonAdminCount:
| DenormalizeNullable<typeof userCountByAdmin>
| symbol = new MemoCache().query(
| symbol = new MemoCache(MyDelegate).query(
userCountByAdmin,
[{ isAdmin: false }],
state,
).data;
expect(nonAdminCount).toBe(3);
const adminCount:
| DenormalizeNullable<typeof userCountByAdmin>
| symbol = new MemoCache().query(
| symbol = new MemoCache(MyDelegate).query(
userCountByAdmin,
[{ isAdmin: true }],
state,
Expand Down
4 changes: 4 additions & 0 deletions packages/normalizr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@
"react-native": "./lib/index.js",
"default": "./lib/index.js"
},
"./immutable": {
"types": "./lib/immutable.d.ts",
"default": "./lib/immutable.js"
},
"./package.json": "./package.json"
},
"type": "module",
Expand Down
Loading