Skip to content

Commit 7df6a49

Browse files
authored
enhance(normalizr): Lazy-clone entity tables to fix getNewEntities deopt (#3884)
getNewEntities eagerly cloned entity and meta table POJOs on first access per key, causing a Maglev bailout ("Insufficient type feedback for generic named access") because this.entities lacked stable type feedback at optimization time. Move the clone to setEntity (lazy, on first write per entity type) so getNewEntities stays a pure Map operation that Maglev can optimize and keep optimized. Also extract MetaEntry type alias to reduce repetition. Made-with: Cursor
1 parent 7fa293f commit 7df6a49

2 files changed

Lines changed: 32 additions & 30 deletions

File tree

.changeset/lazy-entity-clone.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@data-client/normalizr': patch
3+
---
4+
5+
Move entity table POJO clone from getNewEntities to setEntity
6+
7+
Lazy-clone entity and meta tables on first write per entity type instead of eagerly in getNewEntities. This keeps getNewEntities as a pure Map operation, eliminating its V8 Maglev bailout ("Insufficient type feedback for generic named access" on `this.entities`).

packages/normalizr/src/normalize/NormalizeDelegate.ts

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,20 @@ import { getCheckLoop } from './getCheckLoop.js';
88
import { POJODelegate } from '../delegate/Delegate.js';
99
import { INVALID } from '../denormalize/symbol.js';
1010

11+
type MetaEntry = { fetchedAt: number; date: number; expiresAt: number };
12+
1113
/** Full normalize() logic for POJO state */
1214
export class NormalizeDelegate
1315
extends POJODelegate
1416
implements INormalizeDelegate
1517
{
1618
declare readonly entitiesMeta: {
1719
[entityKey: string]: {
18-
[pk: string]: {
19-
date: number;
20-
expiresAt: number;
21-
fetchedAt: number;
22-
};
20+
[pk: string]: MetaEntry;
2321
};
2422
};
2523

26-
declare readonly meta: { fetchedAt: number; date: number; expiresAt: number };
24+
declare readonly meta: MetaEntry;
2725
declare checkLoop: (entityKey: string, pk: string, input: object) => boolean;
2826

2927
protected newEntities = new Map<string, Map<string, any>>();
@@ -35,15 +33,11 @@ export class NormalizeDelegate
3533
indexes: NormalizedIndex;
3634
entitiesMeta: {
3735
[entityKey: string]: {
38-
[pk: string]: {
39-
date: number;
40-
expiresAt: number;
41-
fetchedAt: number;
42-
};
36+
[pk: string]: MetaEntry;
4337
};
4438
};
4539
},
46-
actionMeta: { fetchedAt: number; date: number; expiresAt: number },
40+
actionMeta: MetaEntry,
4741
) {
4842
super(state);
4943
this.entitiesMeta = state.entitiesMeta;
@@ -56,19 +50,12 @@ export class NormalizeDelegate
5650
}
5751

5852
protected getNewEntities(key: string): Map<string, any> {
59-
// first time we come across this type of entity
60-
if (!this.newEntities.has(key)) {
61-
this.newEntities.set(key, new Map());
62-
// we will be editing these, so we need to clone them first
63-
this.entities[key] = {
64-
...this.entities[key],
65-
};
66-
this.entitiesMeta[key] = {
67-
...this.entitiesMeta[key],
68-
};
53+
let map = this.newEntities.get(key);
54+
if (map === undefined) {
55+
map = new Map();
56+
this.newEntities.set(key, map);
6957
}
70-
71-
return this.newEntities.get(key) as Map<string, any>;
58+
return map;
7259
}
7360

7461
protected getNewIndexes(key: string): Map<string, any> {
@@ -124,11 +111,23 @@ export class NormalizeDelegate
124111
schema: { key: string; indexes?: any },
125112
pk: string,
126113
entity: any,
127-
meta: { fetchedAt: number; date: number; expiresAt: number } = this.meta,
114+
meta: MetaEntry = this.meta,
128115
) {
129116
const key = schema.key;
130117
const newEntities = this.getNewEntities(key);
131118
const updateMeta = !newEntities.has(pk);
119+
120+
// Clone tables here (not in getNewEntities) so getNewEntities stays a
121+
// pure Map operation. V8/Maglev bails out on this.entities access there
122+
// due to insufficient type feedback; moving the clone here lets
123+
// getNewEntities recover to compiled code after its initial warmup deopt.
124+
// Benchmarks show no throughput change, but the function stays in Maglev
125+
// instead of falling back to the interpreter.
126+
if (updateMeta && newEntities.size === 0) {
127+
this.entities[key] = { ...this.entities[key] };
128+
this.entitiesMeta[key] = { ...this.entitiesMeta[key] };
129+
}
130+
132131
newEntities.set(pk, entity);
133132

134133
// update index
@@ -159,11 +158,7 @@ export class NormalizeDelegate
159158
(this.entities[key] as any)[pk] = entity;
160159
}
161160

162-
protected _setMeta(
163-
key: string,
164-
pk: string,
165-
meta: { fetchedAt: number; date: number; expiresAt: number },
166-
) {
161+
protected _setMeta(key: string, pk: string, meta: MetaEntry) {
167162
this.entitiesMeta[key][pk] = meta;
168163
}
169164

0 commit comments

Comments
 (0)