Skip to content

Commit

Permalink
Fully eliminate internal EntityStore#modify method.
Browse files Browse the repository at this point in the history
  • Loading branch information
benjamn committed May 16, 2020
1 parent d16c605 commit fd2c8fa
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 125 deletions.
25 changes: 0 additions & 25 deletions src/cache/core/types/common.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
import { DocumentNode } from 'graphql';

import {
isReference,
StoreValue,
StoreObject,
Reference
} from '../../../utilities/graphql/storeUtils';

import { ToReferenceFunction } from '../../inmemory/entityStore';

// The Readonly<T> type only really works for object types, since it marks
// all of the object's properties as readonly, but there are many cases when
// a generic type parameter like TExisting might be a string or some other
Expand All @@ -18,22 +9,6 @@ import { ToReferenceFunction } from '../../inmemory/entityStore';
// Readonly<any>, somewhat surprisingly.
export type SafeReadonly<T> = T extends object ? Readonly<T> : T;

export type Modifier<T> = (value: T, details: {
DELETE: any;
fieldName: string;
storeFieldName: string;
isReference: typeof isReference;
toReference: ToReferenceFunction;
readField<V = StoreValue>(
fieldName: string,
objOrRef?: StoreObject | Reference,
): SafeReadonly<V>;
}) => T;

export type Modifiers = {
[fieldName: string]: Modifier<any>;
}

export class MissingFieldError {
constructor(
public readonly message: string,
Expand Down
163 changes: 64 additions & 99 deletions src/cache/inmemory/entityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,11 @@ import { DeepMerger } from '../../utilities/common/mergeDeep';
import { maybeDeepFreeze } from '../../utilities/common/maybeDeepFreeze';
import { canUseWeakMap } from '../../utilities/common/canUse';
import { NormalizedCache, NormalizedCacheObject } from './types';
import { fieldNameFromStoreName } from './helpers';
import { hasOwn, fieldNameFromStoreName } from './helpers';
import { Policies } from './policies';
import { Modifier, Modifiers, SafeReadonly } from '../core/types/common';
import { SafeReadonly } from '../core/types/common';
import { Cache } from '../core/types/Cache';

const hasOwn = Object.prototype.hasOwnProperty;

const DELETE: any = Object.create(null);
const delModifier: Modifier<any> = () => DELETE;

export abstract class EntityStore implements NormalizedCache {
protected data: NormalizedCacheObject = Object.create(null);

Expand Down Expand Up @@ -77,101 +72,43 @@ export abstract class EntityStore implements NormalizedCache {
this instanceof Layer ? this.parent.lookup(dataId, dependOnExistence) : void 0;
}

public merge(dataId: string, incoming: StoreObject): void {
public merge(dataId: string, incoming: StoreObject): StoreObject {
const existing = this.lookup(dataId);
const merged = new DeepMerger(storeObjectReconciler).merge(existing, incoming);
const merged: StoreObject =
new DeepMerger(storeObjectReconciler).merge(existing, incoming);

// Even if merged === existing, existing may have come from a lower
// layer, so we always need to set this.data[dataId] on this level.
this.data[dataId] = merged;

if (merged !== existing) {
delete this.refs[dataId];
if (this.group.caching) {
const fieldsToDirty: Record<string, 1> = Object.create(null);
// If we added a new StoreObject where there was previously none, dirty
// anything that depended on the existence of this dataId, such as the
// EntityStore#has method.
if (!existing) fieldsToDirty.__exists = 1;
// Now invalidate dependents who called getFieldValue for any fields
// that are changing as a result of this merge.
Object.keys(incoming).forEach(storeFieldName => {
if (!existing || existing[storeFieldName] !== merged[storeFieldName]) {
fieldsToDirty[fieldNameFromStoreName(storeFieldName)] = 1;
// If merged[storeFieldName] has become undefined, and this is the
// Root layer, actually delete the property from the merged object,
// which is guaranteed to have been created fresh in this method.
if (merged[storeFieldName] === void 0 && !(this instanceof Layer)) {
delete merged[storeFieldName];
}
}
});
Object.keys(fieldsToDirty).forEach(
fieldName => this.group.dirty(dataId, fieldName));
}
}
}

public modify(
dataId: string,
modifiers: Modifier<any> | Modifiers,
): boolean {
const storeObject = this.lookup(dataId);

if (storeObject) {
const changedFields: Record<string, any> = Object.create(null);
let needToMerge = false;
let allDeleted = true;

const readField = <V = StoreValue>(
fieldName: string,
objOrRef?: StoreObject | Reference,
) => this.getFieldValue<V>(objOrRef || makeReference(dataId), fieldName);

Object.keys(storeObject).forEach(storeFieldName => {
const fieldName = fieldNameFromStoreName(storeFieldName);
let fieldValue = storeObject[storeFieldName];
if (fieldValue === void 0) return;
const modify: Modifier<StoreValue> = typeof modifiers === "function"
? modifiers
: modifiers[storeFieldName] || modifiers[fieldName];
if (modify) {
let newValue = modify === delModifier ? DELETE :
modify(maybeDeepFreeze(fieldValue), {
DELETE,
fieldName,
storeFieldName,
isReference,
toReference: this.toReference,
readField,
});
if (newValue === DELETE) newValue = void 0;
if (newValue !== fieldValue) {
changedFields[storeFieldName] = newValue;
needToMerge = true;
fieldValue = newValue;
const fieldsToDirty: Record<string, 1> = Object.create(null);

// If we added a new StoreObject where there was previously none,
// dirty anything that depended on the existence of this dataId,
// such as the EntityStore#has method.
if (!existing) fieldsToDirty.__exists = 1;

// Now invalidate dependents who called getFieldValue for any fields
// that are changing as a result of this merge.
Object.keys(incoming).forEach(storeFieldName => {
if (!existing || existing[storeFieldName] !== merged[storeFieldName]) {
fieldsToDirty[fieldNameFromStoreName(storeFieldName)] = 1;

// If merged[storeFieldName] has become undefined, and this is the
// Root layer, actually delete the property from the merged object,
// which is guaranteed to have been created fresh in this method.
if (merged[storeFieldName] === void 0 && !(this instanceof Layer)) {
delete merged[storeFieldName];
}
}
if (fieldValue !== void 0) {
allDeleted = false;
}
});

if (needToMerge) {
this.merge(dataId, changedFields);

if (allDeleted) {
if (this instanceof Layer) {
this.data[dataId] = void 0;
} else {
delete this.data[dataId];
}
this.group.dirty(dataId, "__exists");
}

return true;
}
Object.keys(fieldsToDirty).forEach(
fieldName => this.group.dirty(dataId, fieldName));
}

return false;
return merged;
}

// If called with only one argument, removes the entire entity
Expand All @@ -186,15 +123,43 @@ export abstract class EntityStore implements NormalizedCache {
args?: Record<string, any>,
) {
const storeObject = this.lookup(dataId);
if (storeObject) {
const typename = this.getFieldValue<string>(storeObject, "__typename");
const storeFieldName = fieldName && args
? this.policies.getStoreFieldName(typename, fieldName, args)
: fieldName;
return this.modify(dataId, storeFieldName ? {
[storeFieldName]: delModifier,
} : delModifier);
if (!storeObject) return false;

const changedFields: Record<string, any> = Object.create(null);

if (fieldName && args) {
// Since we have args, we can compute the specific storeFieldName to
// be deleted (if it exists).
const storeFieldName = this.policies.getStoreFieldName(
this.getFieldValue<string>(storeObject, "__typename"), fieldName, args);
if (storeObject[storeFieldName] !== void 0) {
changedFields[storeFieldName] = void 0;
}
} else {
// Since we don't have specific args, loop over all the keys of
// storeObject and delete the ones that match fieldName.
Object.keys(storeObject).forEach(storeFieldName => {
if (storeObject[storeFieldName] !== void 0 &&
(!fieldName || // If no fieldName, all fields match.
fieldName === fieldNameFromStoreName(storeFieldName))) {
changedFields[storeFieldName] = void 0;
}
});
}

if (Object.keys(changedFields).length) {
const merged = this.merge(dataId, changedFields);
if (Object.keys(merged).every(key => merged[key] === void 0)) {
if (this instanceof Layer) {
this.data[dataId] = void 0;
} else {
delete this.data[dataId];
}
this.group.dirty(dataId, "__exists");
}
return true;
}

return false;
}

Expand Down
2 changes: 2 additions & 0 deletions src/cache/inmemory/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
} from '../../utilities/graphql/storeUtils';
import { DeepMerger, ReconcilerFunction } from '../../utilities/common/mergeDeep';

export const hasOwn = Object.prototype.hasOwnProperty;

export function getTypenameFromStoreObject(
store: NormalizedCache,
objectOrReference: StoreObject | Reference,
Expand Down
2 changes: 1 addition & 1 deletion src/cache/inmemory/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export declare type IdGetter = (
export interface NormalizedCache {
has(dataId: string): boolean;
get(dataId: string, fieldName: string): StoreValue;
merge(dataId: string, incoming: StoreObject): void;
merge(dataId: string, incoming: StoreObject): StoreObject;
clear(): void;

// non-Map elements:
Expand Down

0 comments on commit fd2c8fa

Please sign in to comment.