Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/lazy-entity-clone.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@data-client/normalizr': patch
---

Move entity table POJO clone from getNewEntities to setEntity

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`).
55 changes: 25 additions & 30 deletions packages/normalizr/src/normalize/NormalizeDelegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,20 @@ import { getCheckLoop } from './getCheckLoop.js';
import { POJODelegate } from '../delegate/Delegate.js';
import { INVALID } from '../denormalize/symbol.js';

type MetaEntry = { fetchedAt: number; date: number; expiresAt: number };

/** Full normalize() logic for POJO state */
export class NormalizeDelegate
extends POJODelegate
implements INormalizeDelegate
{
declare readonly entitiesMeta: {
[entityKey: string]: {
[pk: string]: {
date: number;
expiresAt: number;
fetchedAt: number;
};
[pk: string]: MetaEntry;
};
};

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

protected newEntities = new Map<string, Map<string, any>>();
Expand All @@ -35,15 +33,11 @@ export class NormalizeDelegate
indexes: NormalizedIndex;
entitiesMeta: {
[entityKey: string]: {
[pk: string]: {
date: number;
expiresAt: number;
fetchedAt: number;
};
[pk: string]: MetaEntry;
};
};
},
actionMeta: { fetchedAt: number; date: number; expiresAt: number },
actionMeta: MetaEntry,
) {
super(state);
this.entitiesMeta = state.entitiesMeta;
Expand All @@ -56,19 +50,12 @@ export class NormalizeDelegate
}

protected getNewEntities(key: string): Map<string, any> {
// first time we come across this type of entity
if (!this.newEntities.has(key)) {
this.newEntities.set(key, new Map());
// we will be editing these, so we need to clone them first
this.entities[key] = {
...this.entities[key],
};
this.entitiesMeta[key] = {
...this.entitiesMeta[key],
};
let map = this.newEntities.get(key);
if (map === undefined) {
map = new Map();
this.newEntities.set(key, map);
}

return this.newEntities.get(key) as Map<string, any>;
return map;
}

protected getNewIndexes(key: string): Map<string, any> {
Expand Down Expand Up @@ -124,11 +111,23 @@ export class NormalizeDelegate
schema: { key: string; indexes?: any },
pk: string,
entity: any,
meta: { fetchedAt: number; date: number; expiresAt: number } = this.meta,
meta: MetaEntry = this.meta,
) {
const key = schema.key;
const newEntities = this.getNewEntities(key);
const updateMeta = !newEntities.has(pk);

// Clone tables here (not in getNewEntities) so getNewEntities stays a
// pure Map operation. V8/Maglev bails out on this.entities access there
// due to insufficient type feedback; moving the clone here lets
// getNewEntities recover to compiled code after its initial warmup deopt.
// Benchmarks show no throughput change, but the function stays in Maglev
// instead of falling back to the interpreter.
if (updateMeta && newEntities.size === 0) {
this.entities[key] = { ...this.entities[key] };
this.entitiesMeta[key] = { ...this.entitiesMeta[key] };
}

newEntities.set(pk, entity);

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

protected _setMeta(
key: string,
pk: string,
meta: { fetchedAt: number; date: number; expiresAt: number },
) {
protected _setMeta(key: string, pk: string, meta: MetaEntry) {
this.entitiesMeta[key][pk] = meta;
}

Expand Down
Loading