Skip to content

Commit 65f9c9d

Browse files
Merge pull request #41 from Wojtach/ios-38-replicator-document-change
Replicator document change
2 parents 04c10fb + e7af87b commit 65f9c9d

File tree

9 files changed

+296
-14
lines changed

9 files changed

+296
-14
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import React, { useContext, useState } from 'react';
2+
import { Replicator } from 'cbl-reactnative';
3+
import ReplicatorDocumentChangeContext from '@/providers/ReplicationDocumentChangeContext';
4+
import ReplicatorStatusTokenContext from '@/providers/ReplicatorStatusTokenContext';
5+
import start from '@/service/replicator/start';
6+
import stop from '@/service/replicator/stop';
7+
import ReplicatorIdActionForm from '@/components/ReplicatorIdActionForm/ReplicatorIdActionForm';
8+
import { useStyleScheme } from '@/components/Themed/Themed';
9+
import { SafeAreaView } from 'react-native';
10+
import ResultListView from '@/components/ResultsListView/ResultsListView';
11+
12+
export default function DocumentReplicationScreen() {
13+
const styles = useStyleScheme();
14+
const { documentChangeMessages, setDocumentChangeMessages } = useContext(
15+
ReplicatorDocumentChangeContext
16+
)!;
17+
const { statusToken, setStatusToken } = useContext(
18+
ReplicatorStatusTokenContext
19+
)!;
20+
const [informationMessages, setInformationMessages] = useState<string[]>([]);
21+
const [selectedReplicatorId, setSelectedReplicatorId] = useState<string>('');
22+
const [documentTokens, setDocumentTokens] = useState<Record<string, string>>({});
23+
24+
function reset() {}
25+
26+
async function update(replicator: Replicator): Promise<void> {
27+
const replId = replicator.getId();
28+
if (replId !== undefined) {
29+
const replicatorId = replId.toString();
30+
setSelectedReplicatorId(replicatorId);
31+
try {
32+
const token = documentTokens[replicatorId];
33+
if (token === undefined) {
34+
setInformationMessages((prev) => [
35+
...prev,
36+
`::Information: Replicator <${replicatorId}> Starting Document Change listener...`,
37+
]);
38+
const changeToken = await replicator.addDocumentChangeListener((documentReplication) => {
39+
const date = new Date().toISOString();
40+
const docs = documentReplication.documents;
41+
const direction = documentReplication.isPush ? 'PUSH' : 'PULL';
42+
43+
const newMessages = docs.map(doc => {
44+
const flags = doc.flags ? doc.flags.join(', ') : 'none';
45+
const error = doc.error ? `, Error: ${doc.error}` : '';
46+
return `${date}::Doc:: ${direction} - Scope: ${doc.scopeName}, Collection: ${doc.collectionName}, ID: ${doc.id}, Flags: ${flags}${error}`;
47+
});
48+
49+
setInformationMessages((prev) => [...prev, ...newMessages]);
50+
});
51+
52+
setDocumentTokens((prev) => {
53+
return {
54+
...prev,
55+
[replicatorId]: changeToken,
56+
};
57+
});
58+
59+
setInformationMessages((prev) => [
60+
...prev,
61+
`::Information: Replicator <${replicatorId}> Document listener registered, starting replicator...`,
62+
]);
63+
64+
await start(replicator, false);
65+
} else {
66+
setInformationMessages((prev) => [
67+
...prev,
68+
`::Information: Replicator <${replicatorId}> Document Change already running with token: <${token}>.`,
69+
]);
70+
}
71+
} catch (error) {
72+
setInformationMessages((prev) => [
73+
...prev,
74+
// @ts-ignore
75+
`::ERROR: ${error.message}`,
76+
]);
77+
}
78+
} else {
79+
setInformationMessages((prev) => [
80+
...prev,
81+
`::ERROR: ReplicatorId is undefined`,
82+
]);
83+
}
84+
}
85+
86+
async function stopReplicator(replicator: Replicator): Promise<void> {
87+
try {
88+
const replId = replicator.getId();
89+
if (replId !== undefined) {
90+
const replicatorId = replId.toString();
91+
setInformationMessages((prev) => [
92+
...prev,
93+
`::Information: Stopping Replicator with replicatorId: <${replicatorId}>.`,
94+
]);
95+
await stop(replicator);
96+
97+
const token = documentTokens[replicatorId];
98+
if (token) {
99+
setInformationMessages((prev) => [
100+
...prev,
101+
`::Information: Removing document change listener with token <${token}> from Replicator with replicatorId: <${replicatorId}>.`,
102+
]);
103+
await replicator.removeChangeListener(token);
104+
105+
setDocumentTokens((prev) => {
106+
const newTokens = { ...prev };
107+
delete newTokens[replicatorId];
108+
return newTokens;
109+
});
110+
111+
setInformationMessages((prev) => [
112+
...prev,
113+
`::Information: Removed document change listener with token <${token}> from Replicator with replicatorId: <${replicatorId}>.`,
114+
]);
115+
}
116+
} else {
117+
setInformationMessages((prev) => [
118+
...prev,
119+
`::Error: Couldn't get replicatorId from replicator.`,
120+
]);
121+
}
122+
} catch (error) {
123+
setInformationMessages((prev) => [
124+
...prev,
125+
// @ts-ignore
126+
`::ERROR: ${error.message}`,
127+
]);
128+
}
129+
}
130+
131+
const filteredDocumentChangeMessages =
132+
documentChangeMessages[selectedReplicatorId] || [];
133+
const combinedMessages = [
134+
...informationMessages,
135+
...filteredDocumentChangeMessages,
136+
];
137+
138+
return (
139+
<SafeAreaView style={styles.container}>
140+
<ReplicatorIdActionForm
141+
handleUpdatePressed={update}
142+
handleResetPressed={reset}
143+
handleStopPressed={stopReplicator}
144+
screenTitle="Document Replication"
145+
/>
146+
<ResultListView useScrollView={true} messages={combinedMessages} />
147+
</SafeAreaView>
148+
);
149+
}

expo-example/hooks/useReplicationNavigationSections.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ export function useReplicationNavigationSections() {
1919
title: 'Replicator Stop',
2020
path: '/replication/stop',
2121
},
22+
{
23+
id: 4,
24+
title: 'Replicator Documents Status',
25+
path: '/replication/documentStatus',
26+
},
2227
{
2328
id: 7,
2429
title: 'Status Changes',

ios/CblReactnative.mm

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,12 @@ @interface RCT_EXTERN_MODULE(CblReactnative, RCTEventEmitter)
232232
withResolver:(RCTPromiseResolveBlock)resolve
233233
withRejecter:(RCTPromiseRejectBlock)reject)
234234

235+
RCT_EXTERN_METHOD(replicator_AddDocumentChangeListener:
236+
(NSString *)changeListenerToken
237+
withReplicatorId:(NSString *)replicatorId
238+
withResolver:(RCTPromiseResolveBlock)resolve
239+
withRejecter:(RCTPromiseRejectBlock)reject)
240+
235241
RCT_EXTERN_METHOD(replicator_Cleanup:
236242
(NSString *)replicatorId
237243
withResolver:(RCTPromiseResolveBlock)resolve

ios/CblReactnative.swift

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class CblReactnative: RCTEventEmitter {
1717
var queryChangeListeners = [String: Any]()
1818

1919
var replicatorChangeListeners = [String: Any]()
20+
var replicatorDocumentChangeListeners = [String: Any]()
2021

2122
var queryCount: Int = 0
2223
var replicatorCount: Int = 0
@@ -1011,6 +1012,37 @@ class CblReactnative: RCTEventEmitter {
10111012
resolve(nil)
10121013
}
10131014
}
1015+
1016+
@objc(replicator_AddDocumentChangeListener:withReplicatorId:withResolver:withRejecter:)
1017+
func replicator_AddDocumentChangeListener(
1018+
changeListenerToken: NSString,
1019+
replicatorId: NSString,
1020+
resolve: @escaping RCTPromiseResolveBlock,
1021+
reject: @escaping RCTPromiseRejectBlock)
1022+
{
1023+
var errorMessage = ""
1024+
let replId = String(replicatorId)
1025+
let token = String(changeListenerToken)
1026+
1027+
backgroundQueue.async {
1028+
guard let replicator = ReplicatorManager.shared.getReplicator(replicatorId: replId) else {
1029+
errorMessage = "No such replicator found for id \(replId)"
1030+
reject("REPLICATOR_ERROR", errorMessage, nil)
1031+
return
1032+
}
1033+
1034+
let listener = replicator.addDocumentReplicationListener(withQueue: self.backgroundQueue, { change in
1035+
let documentJson = ReplicatorHelper.generateReplicationJson(change.documents, isPush: change.isPush)
1036+
let resultData = NSMutableDictionary()
1037+
resultData.setValue(token, forKey: "token")
1038+
resultData.setValue(documentJson, forKey: "documents")
1039+
self.logger.debug ("::SWIFT DEBUG:: Sending event \(self.kReplicatorDocumentChange), with data: \(resultData)")
1040+
self.sendEvent(withName: self.kReplicatorDocumentChange, body: resultData)
1041+
})
1042+
self.replicatorDocumentChangeListeners[token] = listener
1043+
resolve(nil)
1044+
}
1045+
}
10141046

10151047
@objc(replicator_Cleanup:withResolver:withRejecter:)
10161048
func replicator_Cleanup(
@@ -1165,13 +1197,18 @@ class CblReactnative: RCTEventEmitter {
11651197
}
11661198
backgroundQueue.async {
11671199
if let listener = self.replicatorChangeListeners[token] as? ListenerToken {
1168-
replicator.removeChangeListener(withToken: listener)
1169-
self.replicatorChangeListeners.removeValue(forKey: token)
1170-
resolve(nil)
1171-
1172-
} else {
1173-
reject("REPLICATOR_ERROR", "No such replicator listener found with token \(token)", nil)
1174-
}
1200+
replicator.removeChangeListener(withToken: listener)
1201+
self.replicatorChangeListeners.removeValue(forKey: token)
1202+
resolve(nil)
1203+
return
1204+
} else if let listener = self.replicatorDocumentChangeListeners[token] as? ListenerToken {
1205+
replicator.removeChangeListener(withToken: listener)
1206+
self.replicatorDocumentChangeListeners.removeValue(forKey: token)
1207+
resolve(nil)
1208+
return
1209+
} else {
1210+
reject("REPLICATOR_ERROR", "No such listener found with token \(token)", nil)
1211+
}
11751212

11761213
}
11771214
}

ios/cbl-js-swift

src/CblReactNativeEngine.tsx

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -975,12 +975,86 @@ export class CblReactNativeEngine implements ICoreEngine {
975975
}
976976

977977
replicator_AddDocumentChangeListener(
978-
// eslint-disable-next-line
979978
args: ReplicationChangeListenerArgs,
980-
// eslint-disable-next-line
981979
lcb: ListenerCallback
982980
): Promise<void> {
983-
return Promise.resolve(undefined);
981+
//need to track the listener callback for later use due to how React Native events work
982+
if (
983+
this._replicatorDocumentChangeListeners.has(args.changeListenerToken) ||
984+
this._emitterSubscriptions.has(args.changeListenerToken + '_doc')
985+
) {
986+
throw new Error(
987+
'ERROR: changeListenerToken already exists and is registered to listen to document callbacks, cannot add a new one'
988+
);
989+
}
990+
991+
// Set up document change listener if not already done
992+
if (!this._isReplicatorDocumentChangeEventSetup) {
993+
const docSubscription = this._eventEmitter.addListener(
994+
this._eventReplicatorDocumentChange,
995+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
996+
(results: any) => {
997+
this.debugLog(
998+
`::DEBUG:: Received event ${this._eventReplicatorDocumentChange}`
999+
);
1000+
const token = results.token as string;
1001+
const data = results?.documents;
1002+
const error = results?.error;
1003+
1004+
if (token === undefined || token === null || token.length === 0) {
1005+
this.debugLog(
1006+
'::ERROR:: No token to resolve back to proper callback for Replicator Document Change'
1007+
);
1008+
throw new Error(
1009+
'ERROR: No token to resolve back to proper callback'
1010+
);
1011+
}
1012+
1013+
const callback = this._replicatorDocumentChangeListeners.get(token);
1014+
if (callback !== undefined) {
1015+
callback(data, error);
1016+
} else {
1017+
this.debugLog(
1018+
`Error: Could not find callback method for document change token: ${token}.`
1019+
);
1020+
throw new Error(
1021+
`Error: Could not find callback method for document change token: ${token}.`
1022+
);
1023+
}
1024+
}
1025+
);
1026+
1027+
this._emitterSubscriptions.set(
1028+
this._eventReplicatorDocumentChange,
1029+
docSubscription
1030+
);
1031+
this._isReplicatorDocumentChangeEventSetup = true;
1032+
}
1033+
1034+
return new Promise((resolve, reject) => {
1035+
this.CblReactNative.replicator_AddDocumentChangeListener(
1036+
args.changeListenerToken,
1037+
args.replicatorId
1038+
).then(
1039+
() => {
1040+
this._replicatorDocumentChangeListeners.set(
1041+
args.changeListenerToken,
1042+
lcb
1043+
);
1044+
this.debugLog(
1045+
`::DEBUG:: replicator_AddDocumentChangeListener added successfully with token: ${args.changeListenerToken}`
1046+
);
1047+
resolve();
1048+
},
1049+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1050+
(error: any) => {
1051+
this._replicatorDocumentChangeListeners.delete(
1052+
args.changeListenerToken
1053+
);
1054+
reject(error);
1055+
}
1056+
);
1057+
});
9841058
}
9851059

9861060
replicator_Cleanup(args: ReplicatorArgs): Promise<void> {
@@ -1071,6 +1145,17 @@ export class CblReactNativeEngine implements ICoreEngine {
10711145
replicator_RemoveChangeListener(
10721146
args: ReplicationChangeListenerArgs
10731147
): Promise<void> {
1148+
if (this._replicatorDocumentChangeListeners.has(args.changeListenerToken)) {
1149+
this._replicatorDocumentChangeListeners.delete(args.changeListenerToken);
1150+
// Remove any subscription with the doc suffix
1151+
if (this._emitterSubscriptions.has(args.changeListenerToken + '_doc')) {
1152+
this._emitterSubscriptions
1153+
.get(args.changeListenerToken + '_doc')
1154+
?.remove();
1155+
this._emitterSubscriptions.delete(args.changeListenerToken + '_doc');
1156+
}
1157+
}
1158+
10741159
//remove the event subscription or you will have a leak
10751160
if (this._emitterSubscriptions.has(args.changeListenerToken)) {
10761161
this._emitterSubscriptions.get(args.changeListenerToken)?.remove();

0 commit comments

Comments
 (0)