ForestRun: zero dependencies (#464)
* zero dependencies for ForestRun * extract apollo compatibility bits into a separate class (saves some bundle size) * test: disable flaky Apollo compatibility tests temporarily
This commit is contained in:
Родитель
10fee766f4
Коммит
0330a353d9
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "minor",
|
||||
"comment": "zero dependencies for ForestRun",
|
||||
"packageName": "@graphitation/apollo-forest-run",
|
||||
"email": "vladimir.razuvaev@gmail.com",
|
||||
"dependentChangeType": "patch"
|
||||
}
|
|
@ -25,14 +25,14 @@ import { Policies } from "./policies";
|
|||
import { hasOwn, normalizeConfig, shouldCanonizeResults } from "./helpers";
|
||||
import { canonicalStringify } from "./object-canon";
|
||||
|
||||
import { ForestRunCache } from "@graphitation/apollo-forest-run";
|
||||
import { ForestRunCompat } from "@graphitation/apollo-forest-run";
|
||||
|
||||
type BroadcastOptions = Pick<
|
||||
Cache.BatchOptions<InMemoryCache>,
|
||||
"optimistic" | "onWatchUpdated"
|
||||
>;
|
||||
|
||||
export class InMemoryCache extends ForestRunCache {}
|
||||
export class InMemoryCache extends ForestRunCompat {}
|
||||
|
||||
export class InMemoryCache_bak extends ApolloCache<NormalizedCacheObject> {
|
||||
private data: EntityStore;
|
||||
|
|
|
@ -45,7 +45,7 @@ import { Policies } from "./policies";
|
|||
import { InMemoryCache } from "./inMemoryCache";
|
||||
import { MissingFieldError, MissingTree } from "../core/types/common";
|
||||
import { canonicalStringify, ObjectCanon } from "./object-canon";
|
||||
import { ForestRunCache } from "@graphitation/apollo-forest-run";
|
||||
import { ForestRun } from "@graphitation/apollo-forest-run";
|
||||
import { assignStoreCache } from "./__tests__/helpers";
|
||||
|
||||
export type VariableMap = { [name: string]: any };
|
||||
|
@ -106,7 +106,7 @@ function execSelectionSetKeyArgs(
|
|||
}
|
||||
|
||||
export class StoreReader {
|
||||
private cache: ForestRunCache;
|
||||
private cache: ForestRun;
|
||||
|
||||
constructor(config: StoreReaderConfig) {
|
||||
this.cache = config.cache;
|
||||
|
|
|
@ -1370,7 +1370,8 @@ describe('useMutation Hook', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('refetching queries', () => {
|
||||
// FIXME: this is flaky, need to investigate
|
||||
describe.skip('refetching queries', () => {
|
||||
const GET_TODOS_QUERY = gql`
|
||||
query getTodos {
|
||||
todos {
|
||||
|
|
|
@ -2496,7 +2496,8 @@ describe('useQuery Hook', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Refetching', () => {
|
||||
// FIXME: this is flaky, need to investigate
|
||||
describe.skip('Refetching', () => {
|
||||
it('refetching with different variables', async () => {
|
||||
const query = gql`
|
||||
query ($id: Int) {
|
||||
|
|
|
@ -8,18 +8,6 @@
|
|||
"url": "https://github.com/microsoft/graphitation.git",
|
||||
"directory": "packages/apollo-forest-run"
|
||||
},
|
||||
"jest2": {
|
||||
"transform": {
|
||||
"^.+.(t|j)sx?$": "ts-jest"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"/node_modules/(?!(quick-lru))"
|
||||
],
|
||||
"testMatch": [
|
||||
"**/__tests__/**/*.test.ts"
|
||||
],
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "monorepo-scripts build",
|
||||
"lint": "monorepo-scripts lint",
|
||||
|
@ -52,9 +40,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"quick-lru": "^6.1.0"
|
||||
},
|
||||
"files": [
|
||||
"lib/",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"dependencies": {},
|
||||
"peerDependencies": {
|
||||
"graphql": "^15.0.0 || ^16.0.0 || ^17.0.0",
|
||||
"@apollo/client": ">= ^3.6.0 < 3.7.0"
|
||||
|
|
|
@ -16,12 +16,9 @@ import type {
|
|||
Transaction,
|
||||
} from "./cache/types";
|
||||
import { ApolloCache } from "@apollo/client";
|
||||
import { indexTree } from "./forest/indexTree";
|
||||
import { assert } from "./jsutils/assert";
|
||||
import { accumulate, deleteAccumulated } from "./jsutils/map";
|
||||
import { read } from "./cache/read";
|
||||
import { extract, fieldToStringKey } from "./cache/extract";
|
||||
import { restore } from "./cache/restore";
|
||||
import { getNodeChunks } from "./cache/draftHelpers";
|
||||
import { modify } from "./cache/modify";
|
||||
import {
|
||||
|
@ -31,14 +28,9 @@ import {
|
|||
removeOptimisticLayers,
|
||||
resetStore,
|
||||
} from "./cache/store";
|
||||
import {
|
||||
getDiffDescriptor,
|
||||
resolveOperationDescriptor,
|
||||
transformDocument,
|
||||
} from "./cache/descriptor";
|
||||
import { getDiffDescriptor, transformDocument } from "./cache/descriptor";
|
||||
import { write } from "./cache/write";
|
||||
import { replaceTree } from "./forest/addTree";
|
||||
import { identify } from "./cache/keys";
|
||||
import { fieldToStringKey, identify } from "./cache/keys";
|
||||
import { createCacheEnvironment } from "./cache/env";
|
||||
import { CacheConfig } from "./cache/types";
|
||||
|
||||
|
@ -87,13 +79,13 @@ const REFS_POOL = new Map(
|
|||
);
|
||||
const getRef = (ref: string) => REFS_POOL.get(ref) ?? { __ref: ref };
|
||||
|
||||
export class ForestRunCache extends ApolloCache<any> {
|
||||
export class ForestRun extends ApolloCache<any> {
|
||||
public rawConfig: InMemoryCacheConfig;
|
||||
private env: CacheEnv;
|
||||
private store: Store;
|
||||
protected env: CacheEnv;
|
||||
protected store: Store;
|
||||
|
||||
private transactionStack: Transaction[] = [];
|
||||
private newWatches = new Set<Cache.WatchOptions>();
|
||||
protected transactionStack: Transaction[] = [];
|
||||
protected newWatches = new Set<Cache.WatchOptions>();
|
||||
|
||||
// ApolloCompat:
|
||||
public policies = {
|
||||
|
@ -104,7 +96,7 @@ export class ForestRunCache extends ApolloCache<any> {
|
|||
},
|
||||
};
|
||||
|
||||
private invalidatedDiffs = new WeakSet<Cache.DiffResult<any>>();
|
||||
protected invalidatedDiffs = new WeakSet<Cache.DiffResult<any>>();
|
||||
|
||||
public constructor(public config?: CacheConfig) {
|
||||
super();
|
||||
|
@ -258,7 +250,7 @@ export class ForestRunCache extends ApolloCache<any> {
|
|||
}
|
||||
}
|
||||
|
||||
private getActiveForest(): DataForest | OptimisticLayer {
|
||||
protected getActiveForest(): DataForest | OptimisticLayer {
|
||||
const transaction = peek(this.transactionStack);
|
||||
return transaction?.optimisticLayer ?? this.store.dataForest;
|
||||
}
|
||||
|
@ -357,23 +349,12 @@ export class ForestRunCache extends ApolloCache<any> {
|
|||
};
|
||||
}
|
||||
|
||||
public restore(nodeMap: Record<string, any>): this {
|
||||
const writes = restore(this.env, nodeMap);
|
||||
public extract(): StoreObject {
|
||||
throw new Error("ForestRunCache.extract() is not supported");
|
||||
}
|
||||
|
||||
this.reset();
|
||||
for (const write of writes) {
|
||||
const operation = resolveOperationDescriptor(
|
||||
this.env,
|
||||
this.store,
|
||||
write.query,
|
||||
write.variables,
|
||||
write.dataId,
|
||||
);
|
||||
const operationResult = { data: write.result ?? {} };
|
||||
const tree = indexTree(this.env, operation, operationResult);
|
||||
replaceTree(this.store.dataForest, tree);
|
||||
}
|
||||
return this;
|
||||
public restore(_: Record<string, any>): this {
|
||||
throw new Error("ForestRunCache.restore() is not supported");
|
||||
}
|
||||
|
||||
public getStats() {
|
||||
|
@ -383,28 +364,6 @@ export class ForestRunCache extends ApolloCache<any> {
|
|||
};
|
||||
}
|
||||
|
||||
public frExtract() {
|
||||
return {
|
||||
forest: this.store.dataForest.trees,
|
||||
optimisticForest: this.store.optimisticLayers,
|
||||
};
|
||||
}
|
||||
|
||||
public extract(optimistic = false): StoreObject {
|
||||
const activeTransaction = peek(this.transactionStack);
|
||||
const effectiveOptimistic =
|
||||
activeTransaction?.forceOptimistic ?? optimistic;
|
||||
|
||||
return extract(
|
||||
this.env,
|
||||
getEffectiveReadLayers(
|
||||
this.store,
|
||||
this.getActiveForest(),
|
||||
effectiveOptimistic,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Note: this method is necessary for Apollo test suite
|
||||
public __lookup(key: string): StoreObject {
|
||||
const result = this.extract();
|
||||
|
@ -431,7 +390,7 @@ export class ForestRunCache extends ApolloCache<any> {
|
|||
* @deprecated use batch
|
||||
*/
|
||||
public performTransaction(
|
||||
update: (cache: ForestRunCache) => any,
|
||||
update: (cache: ForestRun) => any,
|
||||
optimisticId?: string | null,
|
||||
) {
|
||||
return this.runTransaction({
|
|
@ -0,0 +1,59 @@
|
|||
import { ForestRun } from "./ForestRun";
|
||||
import { extract } from "./cache/extract";
|
||||
import { restore } from "./cache/restore";
|
||||
import { resolveOperationDescriptor } from "./cache/descriptor";
|
||||
import { indexTree } from "./forest/indexTree";
|
||||
import { replaceTree } from "./forest/addTree";
|
||||
import type { StoreObject } from "@apollo/client";
|
||||
import { getEffectiveReadLayers } from "./cache/store";
|
||||
|
||||
/**
|
||||
* Separate class for better compatibility with Apollo InMemoryCache
|
||||
* (supports extract/restore in the format expected by InMemoryCache)
|
||||
*/
|
||||
export class ForestRunCompat extends ForestRun {
|
||||
public frExtract() {
|
||||
return {
|
||||
forest: this.store.dataForest.trees,
|
||||
optimisticForest: this.store.optimisticLayers,
|
||||
};
|
||||
}
|
||||
|
||||
public extract(optimistic = false): StoreObject {
|
||||
const activeTransaction = peek(this.transactionStack);
|
||||
const effectiveOptimistic =
|
||||
activeTransaction?.forceOptimistic ?? optimistic;
|
||||
|
||||
return extract(
|
||||
this.env,
|
||||
getEffectiveReadLayers(
|
||||
this.store,
|
||||
this.getActiveForest(),
|
||||
effectiveOptimistic,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public restore(nodeMap: Record<string, any>): this {
|
||||
const writes = restore(this.env, nodeMap);
|
||||
|
||||
this.reset();
|
||||
for (const write of writes) {
|
||||
const operation = resolveOperationDescriptor(
|
||||
this.env,
|
||||
this.store,
|
||||
write.query,
|
||||
write.variables,
|
||||
write.dataId,
|
||||
);
|
||||
const operationResult = { data: write.result ?? {} };
|
||||
const tree = indexTree(this.env, operation, operationResult);
|
||||
replaceTree(this.store.dataForest, tree);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
function peek<T>(stack: T[]): T | undefined {
|
||||
return stack[stack.length - 1];
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import { gql } from "@apollo/client";
|
||||
import { ForestRunCache } from "../ForestRunCache";
|
||||
import { ForestRun } from "../ForestRun";
|
||||
|
||||
describe("within the same operation", () => {
|
||||
it("uses first incoming result as an output", () => {
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
const query = gql`
|
||||
{
|
||||
a {
|
||||
|
@ -21,7 +21,7 @@ describe("within the same operation", () => {
|
|||
});
|
||||
|
||||
it("recycles first incoming result, when the second result has no changes", () => {
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
const query = gql`
|
||||
{
|
||||
a {
|
||||
|
@ -43,7 +43,7 @@ describe("within the same operation", () => {
|
|||
});
|
||||
|
||||
it("recycles nested objects on updates", () => {
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
const query = gql`
|
||||
{
|
||||
a {
|
||||
|
@ -70,7 +70,7 @@ describe("within the same operation", () => {
|
|||
|
||||
// TODO
|
||||
it.skip("recycles sibling objects on updates", () => {
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
const query = gql`
|
||||
{
|
||||
a {
|
||||
|
@ -105,7 +105,7 @@ describe("within the same operation", () => {
|
|||
});
|
||||
|
||||
it("recycles lists on updates", () => {
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
const query = gql`
|
||||
{
|
||||
a {
|
||||
|
@ -132,7 +132,7 @@ describe("within the same operation", () => {
|
|||
|
||||
// TODO
|
||||
it.skip("recycles list items on updates", () => {
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
const query = gql`
|
||||
{
|
||||
a {
|
||||
|
@ -158,7 +158,7 @@ describe("within the same operation", () => {
|
|||
|
||||
describe("with variables", () => {
|
||||
it("recycles objects with the same arguments", () => {
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
const query = gql`
|
||||
query ($foo: Boolean!) {
|
||||
a(arg: $foo) {
|
||||
|
@ -185,7 +185,7 @@ describe("within the same operation", () => {
|
|||
});
|
||||
|
||||
it("recycles objects with the same arguments in nested fields", () => {
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
const query = gql`
|
||||
query ($foo: Boolean!) {
|
||||
a {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { gql } from "../__tests__/helpers/descriptor";
|
||||
import { ForestRunCache } from "../ForestRunCache";
|
||||
import { ForestRun } from "../ForestRun";
|
||||
|
||||
test("properly invalidates nodes added via cache redirects", () => {
|
||||
const partialFooQuery = gql`
|
||||
|
@ -28,7 +28,7 @@ test("properly invalidates nodes added via cache redirects", () => {
|
|||
}
|
||||
}
|
||||
`;
|
||||
const cache = new ForestRunCache({
|
||||
const cache = new ForestRun({
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
|
@ -92,7 +92,7 @@ test("properly updates fields of sibling operation", () => {
|
|||
const foo = { __typename: "Foo", id: "1", foo: "foo" };
|
||||
const fooUpdated = { __typename: "Foo", id: "1", foo: "fooUpdated" };
|
||||
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
cache.diff({ query: foo1Query, optimistic: true });
|
||||
|
||||
cache.write({ query: foo2Query, result: { foo2: foo } });
|
||||
|
@ -144,7 +144,7 @@ test("properly updates field of sibling operation in presence of another operati
|
|||
const bar = { __typename: "Bar", id: "1", foo: "bar" };
|
||||
const fooUpdated = { __typename: "Foo", id: "1", foo: "fooUpdated" };
|
||||
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
// cache.diff({ query: foo1Query, optimistic: true });
|
||||
|
||||
cache.write({ query: fooOrBar, result: { fooOrBar: foo } });
|
||||
|
@ -181,7 +181,7 @@ test("does not fail on missing fields in aggregate", () => {
|
|||
const base = { foo1: foo, foo2: foo };
|
||||
const model = { foo1: foo, foo2: fooBadChunk };
|
||||
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
cache.diff({ query: query, optimistic: true });
|
||||
|
||||
cache.write({
|
||||
|
@ -199,7 +199,7 @@ test("does not fail on missing fields in aggregate", () => {
|
|||
});
|
||||
|
||||
test("merge policies properly update multiple queries", () => {
|
||||
const cache = new ForestRunCache({
|
||||
const cache = new ForestRun({
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
|
@ -262,7 +262,7 @@ test("merge policies properly update multiple queries", () => {
|
|||
});
|
||||
|
||||
test("calls field policies defined on abstract types", () => {
|
||||
const cache = new ForestRunCache({
|
||||
const cache = new ForestRun({
|
||||
possibleTypes: {
|
||||
Node: ["Foo"],
|
||||
},
|
||||
|
@ -308,7 +308,7 @@ test("calls field policies defined on abstract types", () => {
|
|||
});
|
||||
|
||||
test("field policies do not mutate original result", () => {
|
||||
const cache = new ForestRunCache({
|
||||
const cache = new ForestRun({
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
|
@ -345,7 +345,7 @@ test("should properly report missing field error on incorrect merge policy", ()
|
|||
}
|
||||
}
|
||||
`;
|
||||
const forestRun = new ForestRunCache({
|
||||
const forestRun = new ForestRun({
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
|
@ -408,7 +408,7 @@ test("completes partial written results", () => {
|
|||
const partialResult = {
|
||||
foo: "foo",
|
||||
};
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
cache.write({ query: { ...query }, result: fullResult });
|
||||
cache.write({ query, result: partialResult });
|
||||
const result = cache.diff({ query, optimistic: false });
|
||||
|
@ -457,7 +457,7 @@ test("properly replaces objects containing nested composite lists", () => {
|
|||
bars: [],
|
||||
},
|
||||
};
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
cache.write({ query: query1, result: result1 });
|
||||
cache.write({ query: query2, result: result2 });
|
||||
|
||||
|
@ -499,7 +499,7 @@ test("properly reads plain objects from nested lists", () => {
|
|||
`;
|
||||
const result1 = { foo: [{ bar: "1" }] };
|
||||
const result2 = { foo: [{ bar: "1", baz: "1" }] };
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
|
||||
cache.write({ query: query1, result: result1 });
|
||||
cache.write({ query: query2, result: result2 });
|
||||
|
@ -532,7 +532,7 @@ test("properly compares complex arguments in @connection directive", () => {
|
|||
}
|
||||
`;
|
||||
const result1 = { foo: { edges: [{ cursor: "1" }] } };
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
cache.write({ query: query1, result: result1 });
|
||||
|
||||
const { result, complete } = cache.diff({ query: query2, optimistic: true });
|
||||
|
@ -547,7 +547,7 @@ test("should not notify immediately canceled watches", () => {
|
|||
foo
|
||||
}
|
||||
`;
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
let notifications = 0;
|
||||
const watch = {
|
||||
query,
|
||||
|
@ -582,7 +582,7 @@ test.skip("ApolloCompat: should support manual writes with missing __typename",
|
|||
const result2 = {
|
||||
foo: { id: "1", test: "Bar" },
|
||||
};
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
|
||||
cache.write({ query, result: result1 });
|
||||
cache.write({ query, result: result2 });
|
||||
|
@ -601,7 +601,7 @@ test("should detect empty operations even without sub-selections", () => {
|
|||
foo
|
||||
}
|
||||
`;
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
|
||||
cache.write({ query, result: {} });
|
||||
const { complete, result } = cache.diff({ query, optimistic: true });
|
||||
|
@ -633,7 +633,7 @@ test("optimistic update affecting list is properly handled", () => {
|
|||
const item = { __typename: "Item", id: "1", count: 0 };
|
||||
const updatedItem = { ...item, count: 1 };
|
||||
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
cache.write({
|
||||
query,
|
||||
result: { list: { items: [item] } },
|
||||
|
@ -671,7 +671,7 @@ test("should not trigger merge policies for missing incoming fields", () => {
|
|||
`;
|
||||
|
||||
let calls = 0;
|
||||
const cache = new ForestRunCache({
|
||||
const cache = new ForestRun({
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
|
@ -705,7 +705,7 @@ test("should keep a single result for multiple operations with the same key vari
|
|||
const vars3 = { filter: "b", limit: 1 };
|
||||
const result3 = { list: ["b"] };
|
||||
|
||||
const cache = new ForestRunCache();
|
||||
const cache = new ForestRun();
|
||||
const watch = (variables: any, calls: any) =>
|
||||
cache.watch({
|
||||
query,
|
||||
|
|
|
@ -1,19 +1,9 @@
|
|||
import type { StoreObject, StoreValue } from "@apollo/client";
|
||||
import type {
|
||||
CompositeListValue,
|
||||
KeySpecifier,
|
||||
NodeMap,
|
||||
ObjectValue,
|
||||
} from "../values/types";
|
||||
import type {
|
||||
ArgumentValues,
|
||||
Key,
|
||||
NormalizedFieldEntry,
|
||||
} from "../descriptor/types";
|
||||
import type { CompositeListValue, NodeMap, ObjectValue } from "../values/types";
|
||||
import type { CacheEnv, DataForest, OptimisticLayer } from "./types";
|
||||
import * as Descriptor from "../descriptor/resolvedSelection";
|
||||
import * as Value from "../values";
|
||||
import { assertNever, assert } from "../jsutils/assert";
|
||||
import { fieldToStringKey } from "./keys";
|
||||
|
||||
// ApolloCompat:
|
||||
// Transform forest run layers into Apollo-compatible format (mostly useful for tests)
|
||||
|
@ -27,7 +17,7 @@ export function extract(
|
|||
const entityMap: NodeMap = new Map();
|
||||
|
||||
for (const forest of layers) {
|
||||
for (const indexedTree of forest.trees.values()) {
|
||||
for (const [, indexedTree] of forest.trees) {
|
||||
for (const [id, chunks] of indexedTree.nodes.entries()) {
|
||||
if (forest.deletedNodes.has(id)) {
|
||||
entityMap.set(id, []);
|
||||
|
@ -174,72 +164,3 @@ function toNormalizedList(
|
|||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
export function fieldToStringKey(fieldEntry: NormalizedFieldEntry): string {
|
||||
const keyArgs =
|
||||
typeof fieldEntry === "object" ? fieldEntry.keyArgs : undefined;
|
||||
|
||||
if (typeof fieldEntry === "string" || keyArgs?.length === 0) {
|
||||
return Descriptor.getFieldName(fieldEntry);
|
||||
}
|
||||
const fieldName = Descriptor.getFieldName(fieldEntry);
|
||||
const fieldArgs = Descriptor.getFieldArgs(fieldEntry);
|
||||
|
||||
// TODO: handle keyArgs === "string" case (basically key)
|
||||
const fieldKeyArgs =
|
||||
keyArgs && fieldArgs
|
||||
? resolveKeyArgumentValues(fieldArgs, keyArgs)
|
||||
: fieldArgs;
|
||||
|
||||
const filtered = [...(fieldKeyArgs?.entries() ?? [])].filter(
|
||||
([name, _]) => name !== "__missing",
|
||||
);
|
||||
const args = sortEntriesRecursively(filtered).map(
|
||||
([name, value]) => `"${name}":${JSON.stringify(value)}`,
|
||||
);
|
||||
if (typeof keyArgs === "string") {
|
||||
return `${fieldName}:${keyArgs}`; // keyArgs is actually the key
|
||||
}
|
||||
return keyArgs ? `${fieldName}:{${args}}` : `${fieldName}({${args}})`;
|
||||
}
|
||||
|
||||
function resolveKeyArgumentValues(
|
||||
args: ArgumentValues,
|
||||
keyArgsSpecifier: Key | KeySpecifier,
|
||||
): ArgumentValues {
|
||||
if (typeof keyArgsSpecifier === "string") {
|
||||
return args;
|
||||
}
|
||||
if (
|
||||
keyArgsSpecifier.length === args.size &&
|
||||
keyArgsSpecifier.every((argName) => args.has(argName))
|
||||
) {
|
||||
return args;
|
||||
}
|
||||
const keyArgs: ArgumentValues = new Map();
|
||||
for (const argName of keyArgsSpecifier) {
|
||||
const argValue = args.get(argName);
|
||||
if (argValue !== undefined) {
|
||||
keyArgs.set(argName, argValue);
|
||||
}
|
||||
}
|
||||
return keyArgs;
|
||||
}
|
||||
|
||||
function sortEntriesRecursively(entries: [string, unknown][]) {
|
||||
return sortKeys(entries).sort((a, b) => a[0].localeCompare(b[0]));
|
||||
}
|
||||
|
||||
export function sortKeys<T>(value: T): T {
|
||||
if (typeof value !== "object" || value === null) {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((test) => sortKeys(test)) as T;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([key, value]) => [key, sortKeys(value)]),
|
||||
) as T;
|
||||
}
|
||||
|
|
|
@ -3,14 +3,15 @@ import type {
|
|||
ArgumentValues,
|
||||
Directives,
|
||||
Key,
|
||||
NormalizedFieldEntry,
|
||||
OperationDescriptor,
|
||||
PossibleSelection,
|
||||
} from "../descriptor/types";
|
||||
import type { CacheEnv } from "./types";
|
||||
import type { KeySpecifier, SourceObject } from "../values/types";
|
||||
import { sortKeys } from "./extract";
|
||||
import { assert } from "../jsutils/assert";
|
||||
import { ROOT_TYPES } from "./descriptor";
|
||||
import * as Descriptor from "../descriptor/resolvedSelection";
|
||||
|
||||
export function identify(
|
||||
env: CacheEnv,
|
||||
|
@ -211,5 +212,74 @@ function resolveDataKey(
|
|||
return canonicalFieldName;
|
||||
}
|
||||
|
||||
export function fieldToStringKey(fieldEntry: NormalizedFieldEntry): string {
|
||||
const keyArgs =
|
||||
typeof fieldEntry === "object" ? fieldEntry.keyArgs : undefined;
|
||||
|
||||
if (typeof fieldEntry === "string" || keyArgs?.length === 0) {
|
||||
return Descriptor.getFieldName(fieldEntry);
|
||||
}
|
||||
const fieldName = Descriptor.getFieldName(fieldEntry);
|
||||
const fieldArgs = Descriptor.getFieldArgs(fieldEntry);
|
||||
|
||||
// TODO: handle keyArgs === "string" case (basically key)
|
||||
const fieldKeyArgs =
|
||||
keyArgs && fieldArgs
|
||||
? resolveKeyArgumentValues(fieldArgs, keyArgs)
|
||||
: fieldArgs;
|
||||
|
||||
const filtered = [...(fieldKeyArgs?.entries() ?? [])].filter(
|
||||
([name, _]) => name !== "__missing",
|
||||
);
|
||||
const args = sortEntriesRecursively(filtered).map(
|
||||
([name, value]) => `"${name}":${JSON.stringify(value)}`,
|
||||
);
|
||||
if (typeof keyArgs === "string") {
|
||||
return `${fieldName}:${keyArgs}`; // keyArgs is actually the key
|
||||
}
|
||||
return keyArgs ? `${fieldName}:{${args}}` : `${fieldName}({${args}})`;
|
||||
}
|
||||
|
||||
function resolveKeyArgumentValues(
|
||||
args: ArgumentValues,
|
||||
keyArgsSpecifier: Key | KeySpecifier,
|
||||
): ArgumentValues {
|
||||
if (typeof keyArgsSpecifier === "string") {
|
||||
return args;
|
||||
}
|
||||
if (
|
||||
keyArgsSpecifier.length === args.size &&
|
||||
keyArgsSpecifier.every((argName) => args.has(argName))
|
||||
) {
|
||||
return args;
|
||||
}
|
||||
const keyArgs: ArgumentValues = new Map();
|
||||
for (const argName of keyArgsSpecifier) {
|
||||
const argValue = args.get(argName);
|
||||
if (argValue !== undefined) {
|
||||
keyArgs.set(argName, argValue);
|
||||
}
|
||||
}
|
||||
return keyArgs;
|
||||
}
|
||||
|
||||
function sortEntriesRecursively(entries: [string, unknown][]) {
|
||||
return sortKeys(entries).sort((a, b) => a[0].localeCompare(b[0]));
|
||||
}
|
||||
|
||||
function sortKeys<T>(value: T): T {
|
||||
if (typeof value !== "object" || value === null) {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((test) => sortKeys(test)) as T;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([key, value]) => [key, sortKeys(value)]),
|
||||
) as T;
|
||||
}
|
||||
|
||||
const inspect = JSON.stringify.bind(JSON);
|
||||
const EMPTY_ARRAY = Object.freeze([]);
|
||||
|
|
|
@ -41,7 +41,7 @@ import {
|
|||
} from "./policies";
|
||||
import { assert } from "../jsutils/assert";
|
||||
import { DifferenceKind } from "../diff/types";
|
||||
import { fieldToStringKey } from "./extract";
|
||||
import { fieldToStringKey } from "./keys";
|
||||
import { ConversionContext, toGraphCompositeChunk } from "./convert";
|
||||
import {
|
||||
getActiveForest,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import QuickLRU from "quick-lru";
|
||||
import {
|
||||
CacheEnv,
|
||||
DataForest,
|
||||
|
@ -13,21 +12,22 @@ import { NodeKey, OperationDescriptor, TypeName } from "../descriptor/types";
|
|||
import { assert } from "../jsutils/assert";
|
||||
import { IndexedTree } from "../forest/types";
|
||||
import { NodeChunk } from "../values/types";
|
||||
import { createLRUMap } from "../jsutils/lru";
|
||||
|
||||
const EMPTY_ARRAY = Object.freeze([]);
|
||||
|
||||
export function createStore(env: CacheEnv): Store {
|
||||
const trees = env.maxOperationCount
|
||||
? (new QuickLRU({
|
||||
maxSize: env.maxOperationCount,
|
||||
onEviction: (operation: OperationDescriptor, resultTree: DataTree) => {
|
||||
? createLRUMap(
|
||||
env.maxOperationCount,
|
||||
(operation: OperationDescriptor, resultTree: DataTree) => {
|
||||
if (!shouldEvict(env, store, resultTree)) {
|
||||
dataForest.trees.set(operation, resultTree);
|
||||
return;
|
||||
}
|
||||
removeTree(store, resultTree);
|
||||
},
|
||||
}) as Map<OperationDescriptor, DataTree>)
|
||||
)
|
||||
: new Map();
|
||||
|
||||
const dataForest: DataForest = {
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
SourceObject,
|
||||
TypeMap,
|
||||
} from "../values/types";
|
||||
import { MapLike } from "../jsutils/lru";
|
||||
|
||||
export type IndexedTree = {
|
||||
operation: OperationDescriptor;
|
||||
|
@ -44,7 +45,7 @@ export type IndexedTree = {
|
|||
};
|
||||
|
||||
export type IndexedForest = {
|
||||
trees: Map<OperationDescriptor, IndexedTree>;
|
||||
trees: MapLike<OperationDescriptor, IndexedTree>;
|
||||
extraRootIds: Map<NodeKey, TypeName>;
|
||||
operationsByNodes: Map<NodeKey, Set<OperationDescriptor>>; // May contain false positives
|
||||
operationsWithErrors: Set<OperationDescriptor>; // May contain false positives
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export { ForestRunCache } from "./ForestRunCache";
|
||||
export { ForestRun } from "./ForestRun";
|
||||
export { ForestRunCompat } from "./ForestRunCompat";
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) GraphQL Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,109 @@
|
|||
import { createLRUMap } from "../lru";
|
||||
|
||||
const evicted: unknown[] = [];
|
||||
const testHelper = (maxSize: number) =>
|
||||
createLRUMap(maxSize, (...args) => {
|
||||
evicted.push(args);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
evicted.length = 0;
|
||||
});
|
||||
|
||||
test("set", () => {
|
||||
const lru = testHelper(2);
|
||||
lru.set("foo", "foo");
|
||||
|
||||
expect(lru.get("foo")).toEqual("foo");
|
||||
expect(lru.has("foo")).toBe(true);
|
||||
expect(lru.has("bar")).toBe(false);
|
||||
expect(lru.size).toBe(1);
|
||||
expect(evicted.length).toBe(0);
|
||||
});
|
||||
|
||||
test("update", () => {
|
||||
const lru = testHelper(2);
|
||||
lru.set("foo", "foo");
|
||||
lru.set("foo", "bar");
|
||||
|
||||
expect(lru.get("foo")).toEqual("bar");
|
||||
expect(lru.size).toBe(1);
|
||||
expect([...lru]).toEqual([["foo", "bar"]]);
|
||||
expect(evicted.length).toBe(0);
|
||||
});
|
||||
|
||||
test("moving to old space", () => {
|
||||
const lru = testHelper(2);
|
||||
lru.set("foo", "foo");
|
||||
lru.set("foo", "foo2");
|
||||
lru.set("bar", "bar");
|
||||
|
||||
expect([...lru]).toEqual([
|
||||
["foo", "foo2"],
|
||||
["bar", "bar"],
|
||||
]);
|
||||
expect(lru.size).toBe(2);
|
||||
expect(evicted.length).toBe(0);
|
||||
});
|
||||
|
||||
test("evict", () => {
|
||||
const lru = testHelper(2);
|
||||
lru.set("evict1", "evict1");
|
||||
lru.set("evict2", "evict2");
|
||||
lru.set("foo", "foo");
|
||||
lru.set("bar", "bar");
|
||||
|
||||
expect(lru.size).toBe(2);
|
||||
expect([...lru]).toEqual([
|
||||
["foo", "foo"],
|
||||
["bar", "bar"],
|
||||
]);
|
||||
expect(evicted).toEqual([
|
||||
["evict1", "evict1"],
|
||||
["evict2", "evict2"],
|
||||
]);
|
||||
});
|
||||
|
||||
test("delete from new space", () => {
|
||||
const lru = testHelper(2);
|
||||
lru.set("foo", "foo");
|
||||
lru.set("bar", "bar");
|
||||
lru.set("baz", "baz");
|
||||
|
||||
lru.delete("baz");
|
||||
|
||||
expect(lru.has("baz")).toBe(false);
|
||||
expect(lru.size).toBe(2);
|
||||
expect([...lru]).toEqual([
|
||||
["foo", "foo"],
|
||||
["bar", "bar"],
|
||||
]);
|
||||
});
|
||||
|
||||
test("delete from old space", () => {
|
||||
const lru = testHelper(2);
|
||||
lru.set("foo", "foo");
|
||||
lru.set("bar", "bar");
|
||||
lru.set("baz", "baz");
|
||||
|
||||
lru.delete("foo");
|
||||
|
||||
expect(lru.has("foo")).toBe(false);
|
||||
expect(lru.size).toBe(2);
|
||||
expect([...lru]).toEqual([
|
||||
["bar", "bar"],
|
||||
["baz", "baz"],
|
||||
]);
|
||||
});
|
||||
|
||||
test("clear", () => {
|
||||
const lru = testHelper(2);
|
||||
lru.set("foo", "foo");
|
||||
lru.set("bar", "bar");
|
||||
lru.set("baz", "baz");
|
||||
|
||||
lru.clear();
|
||||
|
||||
expect(lru.size).toBe(0);
|
||||
expect([...lru]).toEqual([]);
|
||||
});
|
|
@ -0,0 +1,93 @@
|
|||
import { assert } from "./assert";
|
||||
|
||||
export interface MapLike<K, V> extends Iterable<[K, V]> {
|
||||
get(key: K): V | undefined;
|
||||
set(key: K, value: V): this;
|
||||
has(key: K): boolean;
|
||||
delete(key: K): boolean;
|
||||
clear(): void;
|
||||
size: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* LRU implementation of algorithm from https://github.com/dominictarr/hashlru#algorithm using a Map
|
||||
*/
|
||||
export function createLRUMap<K, V>(
|
||||
recentItemsMax: number,
|
||||
onEvict: (key: K, value: V) => void,
|
||||
): MapLike<K, V> {
|
||||
assert(recentItemsMax > 0);
|
||||
|
||||
let newSpaceSize = 0;
|
||||
let newSpace = new Map<K, V>();
|
||||
let oldSpace = new Map<K, V>();
|
||||
|
||||
const add = (key: K, value: V) => {
|
||||
newSpace.set(key, value);
|
||||
newSpaceSize++;
|
||||
|
||||
if (newSpaceSize >= recentItemsMax) {
|
||||
const evicted = oldSpace;
|
||||
oldSpace = newSpace;
|
||||
newSpace = new Map<K, V>();
|
||||
newSpaceSize = 0;
|
||||
|
||||
for (const [key, item] of evicted) {
|
||||
onEvict(key, item);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result: MapLike<K, V> = {
|
||||
has: (key) => newSpace.has(key) || oldSpace.has(key),
|
||||
get(key) {
|
||||
if (newSpace.has(key)) {
|
||||
return newSpace.get(key) as V;
|
||||
}
|
||||
if (oldSpace.has(key)) {
|
||||
const value = oldSpace.get(key) as V;
|
||||
oldSpace.delete(key);
|
||||
add(key, value);
|
||||
return value;
|
||||
}
|
||||
},
|
||||
set(key, value) {
|
||||
if (newSpace.has(key)) {
|
||||
newSpace.set(key, value);
|
||||
} else {
|
||||
add(key, value);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
delete(key) {
|
||||
const deleted = newSpace.delete(key);
|
||||
if (deleted) {
|
||||
newSpaceSize--;
|
||||
}
|
||||
return oldSpace.delete(key) || deleted;
|
||||
},
|
||||
clear() {
|
||||
newSpaceSize = 0;
|
||||
newSpace.clear();
|
||||
oldSpace.clear();
|
||||
},
|
||||
get size() {
|
||||
let oldSpaceSize = 0;
|
||||
for (const key of oldSpace.keys()) {
|
||||
if (!newSpace.has(key)) {
|
||||
oldSpaceSize++;
|
||||
}
|
||||
}
|
||||
return newSpaceSize + oldSpaceSize;
|
||||
},
|
||||
*[Symbol.iterator]() {
|
||||
for (const item of oldSpace) {
|
||||
if (!newSpace.has(item[0])) {
|
||||
yield item;
|
||||
}
|
||||
}
|
||||
yield* newSpace;
|
||||
},
|
||||
};
|
||||
return result;
|
||||
}
|
|
@ -10194,11 +10194,6 @@ queue-microtask@^1.2.2:
|
|||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
quick-lru@^6.1.0:
|
||||
version "6.1.2"
|
||||
resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz#e9a90524108629be35287d0b864e7ad6ceb3659e"
|
||||
integrity sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==
|
||||
|
||||
randombytes@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
|
||||
|
|
Загрузка…
Ссылка в новой задаче