Skip to content

Commit

Permalink
Make it possible to extend ApolloCache<NormalizedCacheObject> in a In…
Browse files Browse the repository at this point in the history
…MemoryCache compatible way
  • Loading branch information
msand committed Aug 28, 2024
1 parent 82d8cb4 commit 475ff2f
Show file tree
Hide file tree
Showing 13 changed files with 103 additions and 59 deletions.
39 changes: 27 additions & 12 deletions src/cache/core/types/Cache.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { DataProxy } from "./DataProxy.js";
import type { DataProxy } from "./DataProxy.js";
import type { AllFieldsModifier, Modifiers } from "./common.js";
import type { ApolloCache } from "../cache.js";

export namespace Cache {
// @ts-ignore
const _ = ""; // Make typescript export something
export type WatchCallback<TData = any> = (
diff: Cache.DiffResult<TData>,
lastDiff?: Cache.DiffResult<TData>
) => void;

export interface ReadOptions<TVariables = any, TData = any>
extends DataProxy.Query<TVariables, TData> {
export type ReadOptions<TVariables = any, TData = any> = DataProxy.Query<
TVariables,
TData
> & {
rootId?: string;
previousResult?: any;
optimistic: boolean;
Expand All @@ -22,7 +26,7 @@ export namespace Cache {
* the risk of memory leaks.
*/
canonizeResults?: boolean;
}
};

export interface WriteOptions<TResult = any, TVariables = any>
extends Omit<DataProxy.Query<TVariables, TResult>, "id">,
Expand Down Expand Up @@ -104,12 +108,23 @@ export namespace Cache {
) => any;
}

export import DiffResult = DataProxy.DiffResult;
export import ReadQueryOptions = DataProxy.ReadQueryOptions;
export import ReadFragmentOptions = DataProxy.ReadFragmentOptions;
export import WriteQueryOptions = DataProxy.WriteQueryOptions;
export import WriteFragmentOptions = DataProxy.WriteFragmentOptions;
export import UpdateQueryOptions = DataProxy.UpdateQueryOptions;
export import UpdateFragmentOptions = DataProxy.UpdateFragmentOptions;
export import Fragment = DataProxy.Fragment;
export type DiffResult<T> = DataProxy.DiffResult<T>;
export type ReadQueryOptions<TData, TVariables> = DataProxy.ReadQueryOptions<
TData,
TVariables
>;
export type ReadFragmentOptions<TData, TVariables> =
DataProxy.ReadFragmentOptions<TData, TVariables>;
export type WriteQueryOptions<TData, TVariables> =
DataProxy.WriteQueryOptions<TData, TVariables>;
export type WriteFragmentOptions<TData, TVariables> =
DataProxy.WriteFragmentOptions<TData, TVariables>;
export type UpdateQueryOptions<TData, TVariables> =
DataProxy.UpdateQueryOptions<TData, TVariables>;
export type UpdateFragmentOptions<TData, TVariables> =
DataProxy.UpdateFragmentOptions<TData, TVariables>;
export type Fragment<TVariables, TData> = DataProxy.Fragment<
TVariables,
TData
>;
}
1 change: 1 addition & 0 deletions src/cache/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type {
WatchFragmentResult,
} from "./core/cache.js";
export { ApolloCache } from "./core/cache.js";
// eslint-disable-next-line @typescript-eslint/consistent-type-exports
export { Cache } from "./core/types/Cache.js";
export type { DataProxy } from "./core/types/DataProxy.js";
export type {
Expand Down
4 changes: 3 additions & 1 deletion src/cache/inmemory/__tests__/optimistic.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import gql from "graphql-tag";

import { InMemoryCache } from "../inMemoryCache";
import { ApolloCache } from "../../core/cache";
import type { NormalizedCacheObject } from "../types";

describe("optimistic cache layers", () => {
it("return === results for repeated reads", () => {
Expand Down Expand Up @@ -28,7 +30,7 @@ describe("optimistic cache layers", () => {
}
`;

function readOptimistic(cache: InMemoryCache) {
function readOptimistic(cache: ApolloCache<NormalizedCacheObject>) {
return cache.readQuery<{ book: any }>({ query }, true);
}

Expand Down
6 changes: 3 additions & 3 deletions src/cache/inmemory/entityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ import type {
} from "../core/types/common.js";
import type { DocumentNode, FieldNode, SelectionSetNode } from "graphql";

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

export abstract class EntityStore implements NormalizedCache {
protected data: NormalizedCacheObject = Object.create(null);
Expand Down
41 changes: 22 additions & 19 deletions src/cache/inmemory/inMemoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,23 @@ import type { OperationVariables } from "../../core/index.js";
import { getInMemoryCacheMemoryInternals } from "../../utilities/caching/getMemoryInternals.js";

type BroadcastOptions = Pick<
Cache.BatchOptions<InMemoryCache>,
Cache.BatchOptions<ApolloCache<NormalizedCacheObject>>,
"optimistic" | "onWatchUpdated"
>;

export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
private data!: EntityStore;
private optimisticData!: EntityStore;
data!: EntityStore;
optimisticData!: EntityStore;

protected config: InMemoryCacheConfig;
private watches = new Set<Cache.WatchOptions>();
private addTypename: boolean;
config: InMemoryCacheConfig;
watches = new Set<Cache.WatchOptions>();
addTypename: boolean;

private storeReader!: StoreReader;
private storeWriter!: StoreWriter;
private addTypenameTransform = new DocumentTransform(addTypenameToDocument);
storeReader!: StoreReader;
storeWriter!: StoreWriter;
addTypenameTransform = new DocumentTransform(addTypenameToDocument);

private maybeBroadcastWatch!: OptimisticWrapperFunction<
maybeBroadcastWatch!: OptimisticWrapperFunction<
[Cache.WatchOptions, BroadcastOptions?],
any,
[Cache.WatchOptions]
Expand Down Expand Up @@ -80,7 +80,7 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
this.init();
}

private init() {
init() {
// Passing { resultCaching: false } in the InMemoryCache constructor options
// will completely disable dependency tracking, which will improve memory
// usage but worsen the performance of repeated reads.
Expand All @@ -99,7 +99,7 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
this.resetResultCache();
}

private resetResultCache(resetResultIdentities?: boolean) {
resetResultCache(resetResultIdentities?: boolean) {
const previousReader = this.storeReader;
const { fragments } = this.config;

Expand Down Expand Up @@ -411,10 +411,13 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
}
}

private txCount = 0;
txCount = 0;

public batch<TUpdateResult>(
options: Cache.BatchOptions<InMemoryCache, TUpdateResult>
options: Cache.BatchOptions<
ApolloCache<NormalizedCacheObject>,
TUpdateResult
>
): TUpdateResult {
const {
update,
Expand Down Expand Up @@ -515,7 +518,7 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
}

public performTransaction(
update: (cache: InMemoryCache) => any,
update: (cache: ApolloCache<NormalizedCacheObject>) => any,
optimisticId?: string | null
) {
return this.batch({
Expand All @@ -528,18 +531,18 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
return this.addTypenameToDocument(this.addFragmentsToDocument(document));
}

protected broadcastWatches(options?: BroadcastOptions) {
broadcastWatches(options?: BroadcastOptions) {
if (!this.txCount) {
this.watches.forEach((c) => this.maybeBroadcastWatch(c, options));
}
}

private addFragmentsToDocument(document: DocumentNode) {
addFragmentsToDocument(document: DocumentNode) {
const { fragments } = this.config;
return fragments ? fragments.transform(document) : document;
}

private addTypenameToDocument(document: DocumentNode) {
addTypenameToDocument(document: DocumentNode) {
if (this.addTypename) {
return this.addTypenameTransform.transformDocument(document);
}
Expand All @@ -552,7 +555,7 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
// simpler to check for changes after recomputing a result but before
// broadcasting it, but this wrapping approach allows us to skip both
// the recomputation and the broadcast, in most cases.
private broadcastWatch(c: Cache.WatchOptions, options?: BroadcastOptions) {
broadcastWatch(c: Cache.WatchOptions, options?: BroadcastOptions) {
const { lastDiff } = c;

// Both WatchOptions and DiffOptions extend ReadOptions, and DiffOptions
Expand Down
4 changes: 2 additions & 2 deletions src/core/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1705,9 +1705,9 @@ interface FetchConcastInfo {
// Metadata properties that can be returned in addition to the Concast.
fromLink: boolean;
}
interface SourcesAndInfo<TData> extends FetchConcastInfo {
export interface SourcesAndInfo<TData> extends FetchConcastInfo {
sources: ConcastSourcesArray<ApolloQueryResult<TData>>;
}
interface ConcastAndInfo<TData> extends FetchConcastInfo {
export interface ConcastAndInfo<TData> extends FetchConcastInfo {
concast: Concast<ApolloQueryResult<TData>>;
}
12 changes: 7 additions & 5 deletions src/core/__tests__/ObservableQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ import { SubscriptionObserver } from "zen-observable-ts";
import { waitFor } from "@testing-library/react";
import { ObservableStream } from "../../testing/internal";

export const mockFetchQuery = (queryManager: QueryManager<any>) => {
const fetchConcastWithInfo = queryManager["fetchConcastWithInfo"];
const fetchQueryByPolicy: QueryManager<any>["fetchQueryByPolicy"] = (
queryManager as any
).fetchQueryByPolicy;
export const mockFetchQuery = (
queryManager: QueryManager<NormalizedCacheObject>
) => {
const fetchConcastWithInfo: QueryManager<NormalizedCacheObject>["fetchConcastWithInfo"] =
queryManager["fetchConcastWithInfo"];
const fetchQueryByPolicy: QueryManager<NormalizedCacheObject>["fetchQueryByPolicy"] =
queryManager["fetchQueryByPolicy"];

const mock = <
T extends typeof fetchConcastWithInfo | typeof fetchQueryByPolicy,
Expand Down
3 changes: 2 additions & 1 deletion src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ export type {
WatchFragmentOptions,
WatchFragmentResult,
} from "../cache/index.js";
// eslint-disable-next-line @typescript-eslint/consistent-type-exports
export { Cache } from "../cache/index.js";
export {
Cache,
ApolloCache,
InMemoryCache,
MissingFieldError,
Expand Down
2 changes: 1 addition & 1 deletion src/link/persisted-queries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export const createPersistedQueryLink = (
(networkError.result.errors as GraphQLFormattedError[]);
}
if (isNonEmptyArray(networkErrors)) {
graphQLErrors.push(...networkErrors);
graphQLErrors.push(...(networkErrors as GraphQLFormattedError[]));
}

const disablePayload: ErrorResponse = {
Expand Down
2 changes: 1 addition & 1 deletion src/link/retry/__tests__/delayFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe("buildDelayFunction", () => {
}

function delayRange(delayFunction: SimpleDelayFunction, count: number) {
const results = [];
const results: number[] = [];
for (let i = 1; i <= count; i++) {
results.push(delayFunction(i));
}
Expand Down
2 changes: 1 addition & 1 deletion src/react/hoc/__tests__/queries/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ describe("queries", () => {
});
const Container = graphql<Vars, Data>(query)(() => null);

let errorCaught = null;
let errorCaught: unknown = null;
try {
const { unmount } = render(
<ApolloProvider client={client}>
Expand Down
4 changes: 2 additions & 2 deletions src/react/ssr/__tests__/useLazyQuery.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @jest-environment node */
import React from "react";
import React, { ReactElement } from "react";
import { DocumentNode } from "graphql";
import gql from "graphql-tag";
import { mockSingleLink } from "../../../testing";
Expand Down Expand Up @@ -44,7 +44,7 @@ describe("useLazyQuery Hook SSR", () => {
});

const Component = () => {
let html = null;
let html: ReactElement | null = null;
const [execute, { loading, called, data }] = useLazyQuery(CAR_QUERY);

if (!loading && !called) {
Expand Down
42 changes: 31 additions & 11 deletions src/utilities/common/__tests__/mergeDeep.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
import { mergeDeep, mergeDeepArray, DeepMerger } from "../mergeDeep";

type Value = {
value: number;
};

type Nested = {
[p: `nested${number}`]: Value;
value?: number;
};

type FromNested = {
[p: `from${number}`]: Value | Nested;
nested: Nested;
value?: number;
};

type UniqueConflict = {
[p: `unique${number}`]: Value | FromNested;
conflict: FromNested;
};

describe("mergeDeep", function () {
it("should return an object if first argument falsy", function () {
expect(mergeDeep()).toEqual({});
Expand Down Expand Up @@ -41,15 +61,15 @@ describe("mergeDeep", function () {
});

it("should resolve conflicts among more than two objects", function () {
const sources = [];
const sources: UniqueConflict[] = [];

for (let i = 0; i < 100; ++i) {
sources.push({
["unique" + i]: { value: i },
[`unique${i}`]: { value: i },
conflict: {
["from" + i]: { value: i },
[`from${i}`]: { value: i },
nested: {
["nested" + i]: { value: i },
[`nested${i}`]: { value: i },
},
},
});
Expand All @@ -58,17 +78,17 @@ describe("mergeDeep", function () {
const merged = mergeDeep(...sources);

sources.forEach((source, i) => {
expect(merged["unique" + i].value).toBe(i);
expect(source["unique" + i]).toBe(merged["unique" + i]);
expect(merged[`unique${i}`].value).toBe(i);
expect(source[`unique${i}`]).toBe(merged[`unique${i}`]);

expect(merged.conflict).not.toBe(source.conflict);
expect(merged.conflict["from" + i].value).toBe(i);
expect(merged.conflict["from" + i]).toBe(source.conflict["from" + i]);
expect(merged.conflict[`from${i}`].value).toBe(i);
expect(merged.conflict[`from${i}`]).toBe(source.conflict[`from${i}`]);

expect(merged.conflict.nested).not.toBe(source.conflict.nested);
expect(merged.conflict.nested["nested" + i].value).toBe(i);
expect(merged.conflict.nested["nested" + i]).toBe(
source.conflict.nested["nested" + i]
expect(merged.conflict.nested[`nested${i}`].value).toBe(i);
expect(merged.conflict.nested[`nested${i}`]).toBe(
source.conflict.nested[`nested${i}`]
);
});
});
Expand Down

0 comments on commit 475ff2f

Please sign in to comment.