diff --git a/src/cache/core/__tests__/cache.ts b/src/cache/core/__tests__/cache.ts index f1417add255..2c2100ed1ce 100644 --- a/src/cache/core/__tests__/cache.ts +++ b/src/cache/core/__tests__/cache.ts @@ -21,6 +21,7 @@ class TestCache extends ApolloCache { } public performTransaction(transaction: (c: ApolloCache) => void): void { + transaction(this); } public read(query: Cache.ReadOptions): T | null { diff --git a/src/cache/core/cache.ts b/src/cache/core/cache.ts index db5c63879a8..6f3f2db248e 100644 --- a/src/cache/core/cache.ts +++ b/src/cache/core/cache.ts @@ -59,11 +59,16 @@ export abstract class ApolloCache implements DataProxy { // provide a default batch implementation that's just another way of calling // performTransaction. Subclasses of ApolloCache (such as InMemoryCache) can // override the batch method to do more interesting things with its options. - public batch(options: Cache.BatchOptions) { + public batch(options: Cache.BatchOptions): U { const optimisticId = typeof options.optimistic === "string" ? options.optimistic : options.optimistic === false ? null : void 0; - this.performTransaction(options.update, optimisticId); + let updateResult: U; + this.performTransaction( + () => updateResult = options.update(this), + optimisticId, + ); + return updateResult!; } public abstract performTransaction( diff --git a/src/cache/core/types/Cache.ts b/src/cache/core/types/Cache.ts index 98a21afd4f5..43d854ed743 100644 --- a/src/cache/core/types/Cache.ts +++ b/src/cache/core/types/Cache.ts @@ -54,10 +54,10 @@ export namespace Cache { broadcast?: boolean; } - export interface BatchOptions> { + export interface BatchOptions, U = void> { // Same as the first parameter of performTransaction, except the cache // argument will have the subclass type rather than ApolloCache. - update(cache: C): void; + update(cache: C): U; // Passing a string for this option creates a new optimistic layer, with the // given string as its layer.id, just like passing a string for the @@ -66,7 +66,7 @@ export namespace Cache { // against the current top layer of the cache), and passing false is the // same as passing null (running the operation against root/non-optimistic // cache data). - optimistic: string | boolean; + optimistic?: string | boolean; // If you specify the ID of an optimistic layer using this option, that // layer will be removed as part of the batch transaction, triggering at diff --git a/src/cache/inmemory/__tests__/cache.ts b/src/cache/inmemory/__tests__/cache.ts index 7794721ecae..cae97479d98 100644 --- a/src/cache/inmemory/__tests__/cache.ts +++ b/src/cache/inmemory/__tests__/cache.ts @@ -1377,7 +1377,7 @@ describe('Cache', () => { const dirtied = new Map>(); - cache.batch({ + const aUpdateResult = cache.batch({ update(cache) { cache.writeQuery({ query: aQuery, @@ -1385,12 +1385,14 @@ describe('Cache', () => { a: "ay", }, }); + return "aQuery updated"; }, optimistic: true, onWatchUpdated(w, diff) { dirtied.set(w, diff); }, }); + expect(aUpdateResult).toBe("aQuery updated"); expect(dirtied.size).toBe(2); expect(dirtied.has(aInfo.watch)).toBe(true); @@ -1418,7 +1420,7 @@ describe('Cache', () => { dirtied.clear(); - cache.batch({ + const bUpdateResult = cache.batch({ update(cache) { cache.writeQuery({ query: bQuery, @@ -1426,12 +1428,14 @@ describe('Cache', () => { b: "bee", }, }); + // Not returning anything, so beUpdateResult will be undefined. }, optimistic: true, onWatchUpdated(w, diff) { dirtied.set(w, diff); }, }); + expect(bUpdateResult).toBeUndefined(); expect(dirtied.size).toBe(2); expect(dirtied.has(aInfo.watch)).toBe(false); @@ -1654,6 +1658,98 @@ describe('Cache', () => { abInfo.cancel(); bInfo.cancel(); }); + + it("returns options.update result for optimistic and non-optimistic batches", () => { + const cache = new InMemoryCache; + const expected = Symbol.for("expected"); + + expect(cache.batch({ + optimistic: false, + update(c) { + c.writeQuery({ + query: gql`query { value }`, + data: { value: 12345 }, + }); + return expected; + }, + })).toBe(expected); + + expect(cache.batch({ + optimistic: false, + update(c) { + c.reset(); + return expected; + }, + })).toBe(expected); + + expect(cache.batch({ + optimistic: false, + update(c) { + c.writeQuery({ + query: gql`query { optimistic }`, + data: { optimistic: false }, + }); + return expected; + }, + onWatchUpdated() { + throw new Error("onWatchUpdated should not have been called"); + }, + })).toBe(expected); + + expect(cache.batch({ + optimistic: true, + update(c) { + return expected; + }, + })).toBe(expected); + + expect(cache.batch({ + optimistic: true, + update(c) { + c.writeQuery({ + query: gql`query { optimistic }`, + data: { optimistic: true }, + }); + return expected; + }, + onWatchUpdated() { + throw new Error("onWatchUpdated should not have been called"); + }, + })).toBe(expected); + + expect(cache.batch({ + // The optimistic option defaults to true. + // optimistic: true, + update(c) { + return expected; + }, + })).toBe(expected); + + expect(cache.batch({ + optimistic: "some optimistic ID", + update(c) { + expect(c.readQuery({ + query: gql`query { __typename }`, + })).toEqual({ __typename: "Query" }); + return expected; + }, + })).toBe(expected); + + const optimisticId = "some optimistic ID"; + expect(cache.batch({ + optimistic: optimisticId, + update(c) { + c.writeQuery({ + query: gql`query { optimistic }`, + data: { optimistic: optimisticId }, + }); + return expected; + }, + onWatchUpdated() { + throw new Error("onWatchUpdated should not have been called"); + }, + })).toBe(expected); + }); }); describe('performTransaction', () => { diff --git a/src/cache/inmemory/inMemoryCache.ts b/src/cache/inmemory/inMemoryCache.ts index f48a210f071..59c13267456 100644 --- a/src/cache/inmemory/inMemoryCache.ts +++ b/src/cache/inmemory/inMemoryCache.ts @@ -378,7 +378,7 @@ export class InMemoryCache extends ApolloCache { private txCount = 0; - public batch(options: Cache.BatchOptions) { + public batch(options: Cache.BatchOptions): U { const { update, optimistic = true, @@ -386,14 +386,15 @@ export class InMemoryCache extends ApolloCache { onWatchUpdated, } = options; - const perform = (layer?: EntityStore) => { + let updateResult: U; + const perform = (layer?: EntityStore): U => { const { data, optimisticData } = this; ++this.txCount; if (layer) { this.data = this.optimisticData = layer; } try { - update(this); + return updateResult = update(this); } finally { --this.txCount; this.data = data; @@ -472,6 +473,8 @@ export class InMemoryCache extends ApolloCache { // options.onWatchUpdated. this.broadcastWatches(options); } + + return updateResult!; } public performTransaction(