* 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:
Vladimir Razuvaev 2024-10-24 20:35:25 +02:00 коммит произвёл GitHub
Родитель 10fee766f4
Коммит 0330a353d9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
20 изменённых файлов: 409 добавлений и 222 удалений

Просмотреть файл

@ -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;
}

72
packages/apollo-forest-run/src/cache/keys.ts поставляемый
Просмотреть файл

@ -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,

10
packages/apollo-forest-run/src/cache/store.ts поставляемый
Просмотреть файл

@ -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"