Skip to content

Commit ea0a4dc

Browse files
committed
enhance: Hoist state to delegate
1 parent 2bf65dd commit ea0a4dc

File tree

3 files changed

+209
-19
lines changed

3 files changed

+209
-19
lines changed

packages/normalizr/src/delegate/BaseDelegate.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,6 @@ import type { Dep } from '../memo/WeakDependencyMap.js';
99

1010
/** Basic state interfaces for normalize side */
1111
export abstract class BaseDelegate {
12-
declare entities: any;
13-
declare indexes: any;
14-
15-
constructor({ entities, indexes }: { entities: any; indexes: any }) {
16-
this.entities = entities;
17-
this.indexes = indexes;
18-
}
19-
2012
abstract getEntities(key: string): EntitiesInterface | undefined;
2113
abstract getEntity(key: string, pk: string): object | undefined;
2214
abstract getIndex(...path: IndexPath): object | undefined;

packages/normalizr/src/delegate/Delegate.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,28 @@ import type {
55
} from '../interface.js';
66
import { BaseDelegate } from './BaseDelegate.js';
77

8-
/** Basic POJO state interfaces for normalize side */
8+
/** Basic POJO state interfaces for normalize side
9+
* Used directly as QueryDelegate, and inherited by NormalizeDelegate
10+
*/
911
export class POJODelegate extends BaseDelegate {
10-
declare entities: EntityTable;
11-
declare indexes: {
12-
[entityKey: string]: {
13-
[indexName: string]: { [lookup: string]: string };
14-
};
12+
declare state: {
13+
entities: EntityTable;
14+
indexes: NormalizedIndex;
1515
};
1616

1717
constructor(state: { entities: EntityTable; indexes: NormalizedIndex }) {
18-
super(state);
18+
super();
19+
this.state = state;
1920
}
2021

2122
// we must expose the entities object to track in our WeakDependencyMap
2223
// however, this should not be part of the public API
2324
protected getEntitiesObject(key: string): object | undefined {
24-
return this.entities[key];
25+
return this.state.entities[key];
2526
}
2627

2728
getEntities(key: string): EntitiesInterface | undefined {
28-
const entities = this.entities[key];
29+
const entities = this.state.entities[key];
2930
if (entities === undefined) return undefined;
3031
return {
3132
keys(): IterableIterator<string> {
@@ -38,12 +39,12 @@ export class POJODelegate extends BaseDelegate {
3839
}
3940

4041
getEntity(key: string, pk: string): any {
41-
return this.entities[key]?.[pk];
42+
return this.state.entities[key]?.[pk];
4243
}
4344

4445
// this is different return value than QuerySnapshot
4546
getIndex(key: string, field: string): object | undefined {
46-
return this.indexes[key]?.[field];
47+
return this.state.indexes[key]?.[field];
4748
}
4849

4950
getIndexEnd(entity: object | undefined, value: string) {
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import type {
2+
INormalizeDelegate,
3+
Mergeable,
4+
EntitiesInterface,
5+
} from '../interface.js';
6+
import { getCheckLoop } from './getCheckLoop.js';
7+
import { ImmDelegate } from '../delegate/Delegate.imm.js';
8+
import { INVALID } from '../denormalize/symbol.js';
9+
10+
export type ImmutableJSEntityTable = {
11+
get(key: string): EntitiesInterface | undefined;
12+
getIn(k: [key: string, pk: string]): { toJS(): any } | undefined;
13+
setIn(k: [key: string, pk: string], value: any);
14+
};
15+
type ImmutableJSMeta = {
16+
getIn(k: [key: string, pk: string]):
17+
| {
18+
date: number;
19+
expiresAt: number;
20+
fetchedAt: number;
21+
}
22+
| undefined;
23+
setIn(
24+
k: [key: string, pk: string],
25+
value: {
26+
date: number;
27+
expiresAt: number;
28+
fetchedAt: number;
29+
},
30+
);
31+
};
32+
33+
/** Full normalize() logic for ImmutableJS state */
34+
export class NormalizeDelegate
35+
extends ImmDelegate
36+
implements INormalizeDelegate
37+
{
38+
declare readonly entitiesMeta: ImmutableJSMeta;
39+
40+
declare readonly meta: { fetchedAt: number; date: number; expiresAt: number };
41+
declare checkLoop: (entityKey: string, pk: string, input: object) => boolean;
42+
43+
protected newEntities = new Map<string, Map<string, any>>();
44+
protected newIndexes = new Map<string, Map<string, any>>();
45+
46+
constructor(
47+
state: {
48+
entities: ImmutableJSEntityTable;
49+
indexes: ImmutableJSEntityTable;
50+
entitiesMeta: ImmutableJSMeta;
51+
},
52+
actionMeta: { fetchedAt: number; date: number; expiresAt: number },
53+
) {
54+
super(state);
55+
this.entitiesMeta = state.entitiesMeta;
56+
this.meta = actionMeta;
57+
this.checkLoop = getCheckLoop();
58+
}
59+
60+
protected getNewEntity(key: string, pk: string) {
61+
return this.getNewEntities(key).get(pk);
62+
}
63+
64+
protected getNewEntities(key: string): Map<string, any> {
65+
// first time we come across this type of entity
66+
if (!this.newEntities.has(key)) {
67+
this.newEntities.set(key, new Map());
68+
}
69+
70+
return this.newEntities.get(key) as Map<string, any>;
71+
}
72+
73+
protected getNewIndexes(key: string): Map<string, any> {
74+
if (!this.newIndexes.has(key)) {
75+
this.newIndexes.set(key, new Map());
76+
}
77+
return this.newIndexes.get(key) as Map<string, any>;
78+
}
79+
80+
/** Updates an entity using merge lifecycles when it has previously been set */
81+
mergeEntity(
82+
schema: Mergeable & { indexes?: any },
83+
pk: string,
84+
incomingEntity: any,
85+
) {
86+
const key = schema.key;
87+
88+
// default when this is completely new entity
89+
let nextEntity = incomingEntity;
90+
let nextMeta = this.meta;
91+
92+
// if we already processed this entity during this normalization (in another nested place)
93+
let entity = this.getNewEntity(key, pk);
94+
if (entity) {
95+
nextEntity = schema.merge(entity, incomingEntity);
96+
} else {
97+
// if we find it in the store
98+
entity = this.getEntity(key, pk);
99+
if (entity) {
100+
const meta = this.getMeta(key, pk);
101+
nextEntity = schema.mergeWithStore(
102+
meta,
103+
nextMeta,
104+
entity,
105+
incomingEntity,
106+
);
107+
nextMeta = schema.mergeMetaWithStore(
108+
meta,
109+
nextMeta,
110+
entity,
111+
incomingEntity,
112+
);
113+
}
114+
}
115+
116+
// once we have computed the merged values, set them
117+
this.setEntity(schema, pk, nextEntity, nextMeta);
118+
}
119+
120+
/** Sets an entity overwriting any previously set values */
121+
setEntity(
122+
schema: { key: string; indexes?: any },
123+
pk: string,
124+
entity: any,
125+
meta: { fetchedAt: number; date: number; expiresAt: number } = this.meta,
126+
) {
127+
const key = schema.key;
128+
const newEntities = this.getNewEntities(key);
129+
const updateMeta = !newEntities.has(pk);
130+
newEntities.set(pk, entity);
131+
132+
// update index
133+
if (schema.indexes) {
134+
// typescript should know indexes is defined now
135+
this.handleIndexes(schema as any, pk, entity);
136+
}
137+
138+
// set this after index updates so we know what indexes to remove from
139+
this._setEntity(key, pk, entity);
140+
141+
if (updateMeta) this._setMeta(key, pk, meta);
142+
}
143+
144+
handleIndexes(
145+
schema: { key: string; indexes: any },
146+
pk: string,
147+
entity: any,
148+
) {
149+
const { key } = schema;
150+
const newIndexes = this.getNewIndexes(key);
151+
const storeEntity = this.entities.getIn([key, pk]);
152+
for (const index of schema.indexes) {
153+
if (!newIndexes.has(index)) {
154+
newIndexes.set(index, this.indexes.getIn([key, index]) ?? {});
155+
}
156+
const indexMap = newIndexes.get(index);
157+
if (storeEntity) {
158+
delete indexMap[storeEntity[index]];
159+
}
160+
// entity already in cache but the index changed
161+
if (storeEntity && storeEntity[index] !== entity[index]) {
162+
indexMap[storeEntity[index]] = INVALID;
163+
}
164+
if (index in entity) {
165+
indexMap[entity[index]] = pk;
166+
} /* istanbul ignore next */ else if (
167+
process.env.NODE_ENV !== 'production'
168+
) {
169+
console.warn(`Index not found in entity. Indexes must be top-level members of your entity.
170+
Index: ${index}
171+
Entity: ${JSON.stringify(entity, undefined, 2)}`);
172+
}
173+
}
174+
}
175+
176+
/** Invalidates an entity, potentially triggering suspense */
177+
invalidate(schema: { key: string; indexes?: any }, pk: string) {
178+
// set directly: any queued updates are meaningless with delete
179+
this.setEntity(schema, pk, INVALID);
180+
}
181+
182+
protected _setEntity(key: string, pk: string, entity: any) {
183+
this.entities.setIn([key, pk], entity);
184+
}
185+
186+
protected _setMeta(
187+
key: string,
188+
pk: string,
189+
meta: { fetchedAt: number; date: number; expiresAt: number },
190+
) {
191+
this.entitiesMeta.setIn([key, pk], meta);
192+
}
193+
194+
getMeta(key: string, pk: string) {
195+
return this.entitiesMeta.getIn([key, pk]);
196+
}
197+
}

0 commit comments

Comments
 (0)