From 831d42e96f848a6aa4a8bdd80b724f6b42884f18 Mon Sep 17 00:00:00 2001 From: Dara Hayes Date: Mon, 9 Mar 2020 16:36:52 +0000 Subject: [PATCH] feat: allow for adding conflict listeners after client creation (#393) --- docs/ref-conflict-client.md | 8 +-- .../integration_test/test/conflict.test.js | 56 +++++++++++++++++++ .../offix-client/src/ApolloOfflineClient.ts | 26 ++++++++- .../conflicts/CompositeConflictListener.ts | 38 +++++++++++++ packages/offix-client/src/apollo/index.ts | 1 + .../src/config/ApolloOfflineClientConfig.ts | 9 ++- 6 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 packages/offix-client/src/apollo/conflicts/CompositeConflictListener.ts diff --git a/docs/ref-conflict-client.md b/docs/ref-conflict-client.md index 3a69d159d..5403f8ddb 100644 --- a/docs/ref-conflict-client.md +++ b/docs/ref-conflict-client.md @@ -143,9 +143,7 @@ class ConflictLogger implements ConflictListener { } } -let config = { -... - conflictListener: new ConflictLogger() -... -} +const listener = new ConflictLogger() + +client.addConflictListener(listener) ``` diff --git a/packages/offix-client/integration_test/test/conflict.test.js b/packages/offix-client/integration_test/test/conflict.test.js index 5f7b5f68c..3affe3f55 100644 --- a/packages/offix-client/integration_test/test/conflict.test.js +++ b/packages/offix-client/integration_test/test/conflict.test.js @@ -325,6 +325,62 @@ describe('Conflicts', function () { }); + describe('all conflict listeners should be called', function() { + it('should succeed', async function() { + + let client = await newClient({ networkStatus: newNetworkStatus(), offlineStorage: new TestStore() }); + let conflictedClient = await newClient({ networkStatus: newNetworkStatus(), offlineStorage: new TestStore() }); + + const response = await client.mutate({ + mutation: CREATE_TASK, + variables: newTask + }); + + const task = response.data.createTask; + + await conflictedClient.query({ + query: FIND_ALL_TASKS + }) + + await client.offlineMutate({ + mutation: UPDATE_TASK, + returnType: 'Task', + variables: { + id: task.id, + version: task.version, + description: 'updated description', + title: 'updated title' + } + }) + + let conflictListenerCallCount = 0; + + conflictedClient.addConflictListener({ + conflictOccurred() { + conflictListenerCallCount++ + } + }); + + conflictedClient.addConflictListener({ + conflictOccurred() { + conflictListenerCallCount++ + } + }) + + await conflictedClient.offlineMutate({ + mutation: UPDATE_TASK, + returnType: 'Task', + variables: { + id: task.id, + version: task.version, + description: 'updated description again', + title: 'updated title' + } + }) + expect(conflictListenerCallCount).to.equal(2); + }) + }) + describe('merge should be called for mergeable conflict', function () { it('should succeed', async function () { diff --git a/packages/offix-client/src/ApolloOfflineClient.ts b/packages/offix-client/src/ApolloOfflineClient.ts index 8faafe7af..3c6657a75 100644 --- a/packages/offix-client/src/ApolloOfflineClient.ts +++ b/packages/offix-client/src/ApolloOfflineClient.ts @@ -15,10 +15,11 @@ import { ApolloQueueEntryOperation, ApolloOfflineQueueListener, getBaseStateFromCache, - ApolloCacheWithData + ApolloCacheWithData, + CompositeConflictListener } from "./apollo"; import { NetworkStatus } from "offix-offline"; -import { ObjectState } from "offix-conflicts-client"; +import { ObjectState, ConflictListener } from "offix-conflicts-client"; import { ApolloOfflineClientOptions, InputMapper } from "./config/ApolloOfflineClientOptions"; import { ApolloOfflineClientConfig } from "./config/ApolloOfflineClientConfig"; @@ -32,6 +33,8 @@ export class ApolloOfflineClient extends ApolloClient { public offlineStore?: ApolloOfflineStore; // interface that performs conflict detection and resolution public conflictProvider: ObjectState; + // composite conflict listener object that calls all listeners provided by users + public conflictListener: CompositeConflictListener; // the network status interface that determines online/offline state public networkStatus: NetworkStatus; // the in memory queue that holds offline data @@ -48,6 +51,7 @@ export class ApolloOfflineClient extends ApolloClient { super(config); this.initialized = false; + this.conflictListener = config.conflictListener; this.mutationCacheUpdates = config.mutationCacheUpdates; this.conflictProvider = config.conflictProvider; this.inputMapper = config.inputMapper; @@ -134,6 +138,24 @@ export class ApolloOfflineClient extends ApolloClient { this.scheduler.registerOfflineQueueListener(listener); } + /** + * Add new listener for conflict related events + * + * @param listener + */ + public addConflictListener(listener: ConflictListener){ + this.conflictListener.addConflictListener(listener); + } + + /** + * remove a conflict listener + * + * @param listener + */ + public removeConflictListener(listener: ConflictListener) { + this.conflictListener.removeConflictListener(listener); + } + protected createOfflineMutationOptions( options: MutationHelperOptions): MutationOptions { options.inputMapper = this.inputMapper; diff --git a/packages/offix-client/src/apollo/conflicts/CompositeConflictListener.ts b/packages/offix-client/src/apollo/conflicts/CompositeConflictListener.ts new file mode 100644 index 000000000..dd13fa445 --- /dev/null +++ b/packages/offix-client/src/apollo/conflicts/CompositeConflictListener.ts @@ -0,0 +1,38 @@ +import { ConflictListener, ConflictResolutionData } from "offix-conflicts-client"; + + +/** + * Composite Conflict Listener class that can accept register and remove individual listener functions as needed + * Gets passed down to the conflict link + */ +export class CompositeConflictListener implements ConflictListener { + + private listeners: ConflictListener[] = []; + + addConflictListener(listener: ConflictListener) { + this.listeners.push(listener); + } + + removeConflictListener(listener: ConflictListener) { + const index = this.listeners.indexOf(listener); + if (index >= 0) { + this.listeners.splice(index, 1); + } + } + + mergeOccurred(operationName: string, resolvedData: ConflictResolutionData, server: ConflictResolutionData, client: ConflictResolutionData) { + for (const listener of this.listeners) { + if (listener.mergeOccurred) { + listener.mergeOccurred(operationName, resolvedData, server, client); + } + } + } + + conflictOccurred(operationName: string, resolvedData: ConflictResolutionData, server: ConflictResolutionData, client: ConflictResolutionData) { + for (const listener of this.listeners) { + if (listener.conflictOccurred) { + listener.conflictOccurred(operationName, resolvedData, server, client); + } + } + } +} diff --git a/packages/offix-client/src/apollo/index.ts b/packages/offix-client/src/apollo/index.ts index fa77e8792..2faddbd79 100644 --- a/packages/offix-client/src/apollo/index.ts +++ b/packages/offix-client/src/apollo/index.ts @@ -4,3 +4,4 @@ export * from "./LinksBuilder"; export * from "./optimisticResponseHelpers"; export * from "./conflicts/baseHelpers"; export * from "./conflicts/ConflictLink"; +export * from "./conflicts/CompositeConflictListener"; diff --git a/packages/offix-client/src/config/ApolloOfflineClientConfig.ts b/packages/offix-client/src/config/ApolloOfflineClientConfig.ts index 42fc81b23..d1bed1632 100644 --- a/packages/offix-client/src/config/ApolloOfflineClientConfig.ts +++ b/packages/offix-client/src/config/ApolloOfflineClientConfig.ts @@ -7,7 +7,6 @@ import { ApolloOfflineClientOptions, InputMapper } from "./ApolloOfflineClientOp import { NetworkStatus } from "offix-offline"; import { ConflictResolutionStrategy, - ConflictListener, UseClient, VersionedState, ObjectState @@ -15,7 +14,7 @@ import { import { createDefaultCacheStorage } from "../cache"; import { ApolloLink } from "apollo-link"; import { CacheUpdates } from "offix-cache"; -import { ApolloOfflineQueueListener, createDefaultLink } from "../apollo"; +import { ApolloOfflineQueueListener, createDefaultLink, CompositeConflictListener } from "../apollo"; import { CachePersistor } from "apollo-cache-persist"; /** @@ -31,7 +30,7 @@ export class ApolloOfflineClientConfig implements ApolloOfflineClientOptions { public terminatingLink: ApolloLink | undefined; public cacheStorage: PersistentStore; public offlineStorage: PersistentStore; - public conflictListener?: ConflictListener; + public conflictListener: CompositeConflictListener; public mutationCacheUpdates?: CacheUpdates; public cachePersistor?: CachePersistor; public link?: ApolloLink; @@ -56,6 +55,10 @@ export class ApolloOfflineClientConfig implements ApolloOfflineClientOptions { this.offlineStorage = options.offlineStorage || createDefaultOfflineStorage(); this.conflictStrategy = options.conflictStrategy || UseClient; this.conflictProvider = options.conflictProvider || new VersionedState(); + this.conflictListener = new CompositeConflictListener(); + if (options.conflictListener) { + this.conflictListener.addConflictListener(options.conflictListener); + } this.link = createDefaultLink(this); } }