Skip to content

Commit 5580137

Browse files
author
Wojtach
committed
Merge branch 'main' into ios-44-collection-change-listeners
2 parents 2ae5242 + 65f9c9d commit 5580137

File tree

7 files changed

+294
-12
lines changed

7 files changed

+294
-12
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
@@ -258,6 +258,12 @@ @interface RCT_EXTERN_MODULE(CblReactnative, RCTEventEmitter)
258258
withResolver:(RCTPromiseResolveBlock)resolve
259259
withRejecter:(RCTPromiseRejectBlock)reject)
260260

261+
RCT_EXTERN_METHOD(replicator_AddDocumentChangeListener:
262+
(NSString *)changeListenerToken
263+
withReplicatorId:(NSString *)replicatorId
264+
withResolver:(RCTPromiseResolveBlock)resolve
265+
withRejecter:(RCTPromiseRejectBlock)reject)
266+
261267
RCT_EXTERN_METHOD(replicator_Cleanup:
262268
(NSString *)replicatorId
263269
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
@@ -1151,6 +1152,37 @@ class CblReactnative: RCTEventEmitter {
11511152
resolve(nil)
11521153
}
11531154
}
1155+
1156+
@objc(replicator_AddDocumentChangeListener:withReplicatorId:withResolver:withRejecter:)
1157+
func replicator_AddDocumentChangeListener(
1158+
changeListenerToken: NSString,
1159+
replicatorId: NSString,
1160+
resolve: @escaping RCTPromiseResolveBlock,
1161+
reject: @escaping RCTPromiseRejectBlock)
1162+
{
1163+
var errorMessage = ""
1164+
let replId = String(replicatorId)
1165+
let token = String(changeListenerToken)
1166+
1167+
backgroundQueue.async {
1168+
guard let replicator = ReplicatorManager.shared.getReplicator(replicatorId: replId) else {
1169+
errorMessage = "No such replicator found for id \(replId)"
1170+
reject("REPLICATOR_ERROR", errorMessage, nil)
1171+
return
1172+
}
1173+
1174+
let listener = replicator.addDocumentReplicationListener(withQueue: self.backgroundQueue, { change in
1175+
let documentJson = ReplicatorHelper.generateReplicationJson(change.documents, isPush: change.isPush)
1176+
let resultData = NSMutableDictionary()
1177+
resultData.setValue(token, forKey: "token")
1178+
resultData.setValue(documentJson, forKey: "documents")
1179+
self.logger.debug ("::SWIFT DEBUG:: Sending event \(self.kReplicatorDocumentChange), with data: \(resultData)")
1180+
self.sendEvent(withName: self.kReplicatorDocumentChange, body: resultData)
1181+
})
1182+
self.replicatorDocumentChangeListeners[token] = listener
1183+
resolve(nil)
1184+
}
1185+
}
11541186

11551187
@objc(replicator_Cleanup:withResolver:withRejecter:)
11561188
func replicator_Cleanup(
@@ -1305,13 +1337,18 @@ class CblReactnative: RCTEventEmitter {
13051337
}
13061338
backgroundQueue.async {
13071339
if let listener = self.replicatorChangeListeners[token] as? ListenerToken {
1308-
replicator.removeChangeListener(withToken: listener)
1309-
self.replicatorChangeListeners.removeValue(forKey: token)
1310-
resolve(nil)
1311-
1312-
} else {
1313-
reject("REPLICATOR_ERROR", "No such replicator listener found with token \(token)", nil)
1314-
}
1340+
replicator.removeChangeListener(withToken: listener)
1341+
self.replicatorChangeListeners.removeValue(forKey: token)
1342+
resolve(nil)
1343+
return
1344+
} else if let listener = self.replicatorDocumentChangeListeners[token] as? ListenerToken {
1345+
replicator.removeChangeListener(withToken: listener)
1346+
self.replicatorDocumentChangeListeners.removeValue(forKey: token)
1347+
resolve(nil)
1348+
return
1349+
} else {
1350+
reject("REPLICATOR_ERROR", "No such listener found with token \(token)", nil)
1351+
}
13151352

13161353
}
13171354
}

src/CblReactNativeEngine.tsx

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

11141114
replicator_AddDocumentChangeListener(
1115-
// eslint-disable-next-line
11161115
args: ReplicationChangeListenerArgs,
1117-
// eslint-disable-next-line
11181116
lcb: ListenerCallback
11191117
): Promise<void> {
1120-
return Promise.resolve(undefined);
1118+
//need to track the listener callback for later use due to how React Native events work
1119+
if (
1120+
this._replicatorDocumentChangeListeners.has(args.changeListenerToken) ||
1121+
this._emitterSubscriptions.has(args.changeListenerToken + '_doc')
1122+
) {
1123+
throw new Error(
1124+
'ERROR: changeListenerToken already exists and is registered to listen to document callbacks, cannot add a new one'
1125+
);
1126+
}
1127+
1128+
// Set up document change listener if not already done
1129+
if (!this._isReplicatorDocumentChangeEventSetup) {
1130+
const docSubscription = this._eventEmitter.addListener(
1131+
this._eventReplicatorDocumentChange,
1132+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1133+
(results: any) => {
1134+
this.debugLog(
1135+
`::DEBUG:: Received event ${this._eventReplicatorDocumentChange}`
1136+
);
1137+
const token = results.token as string;
1138+
const data = results?.documents;
1139+
const error = results?.error;
1140+
1141+
if (token === undefined || token === null || token.length === 0) {
1142+
this.debugLog(
1143+
'::ERROR:: No token to resolve back to proper callback for Replicator Document Change'
1144+
);
1145+
throw new Error(
1146+
'ERROR: No token to resolve back to proper callback'
1147+
);
1148+
}
1149+
1150+
const callback = this._replicatorDocumentChangeListeners.get(token);
1151+
if (callback !== undefined) {
1152+
callback(data, error);
1153+
} else {
1154+
this.debugLog(
1155+
`Error: Could not find callback method for document change token: ${token}.`
1156+
);
1157+
throw new Error(
1158+
`Error: Could not find callback method for document change token: ${token}.`
1159+
);
1160+
}
1161+
}
1162+
);
1163+
1164+
this._emitterSubscriptions.set(
1165+
this._eventReplicatorDocumentChange,
1166+
docSubscription
1167+
);
1168+
this._isReplicatorDocumentChangeEventSetup = true;
1169+
}
1170+
1171+
return new Promise((resolve, reject) => {
1172+
this.CblReactNative.replicator_AddDocumentChangeListener(
1173+
args.changeListenerToken,
1174+
args.replicatorId
1175+
).then(
1176+
() => {
1177+
this._replicatorDocumentChangeListeners.set(
1178+
args.changeListenerToken,
1179+
lcb
1180+
);
1181+
this.debugLog(
1182+
`::DEBUG:: replicator_AddDocumentChangeListener added successfully with token: ${args.changeListenerToken}`
1183+
);
1184+
resolve();
1185+
},
1186+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1187+
(error: any) => {
1188+
this._replicatorDocumentChangeListeners.delete(
1189+
args.changeListenerToken
1190+
);
1191+
reject(error);
1192+
}
1193+
);
1194+
});
11211195
}
11221196

11231197
replicator_Cleanup(args: ReplicatorArgs): Promise<void> {
@@ -1208,6 +1282,17 @@ export class CblReactNativeEngine implements ICoreEngine {
12081282
replicator_RemoveChangeListener(
12091283
args: ReplicationChangeListenerArgs
12101284
): Promise<void> {
1285+
if (this._replicatorDocumentChangeListeners.has(args.changeListenerToken)) {
1286+
this._replicatorDocumentChangeListeners.delete(args.changeListenerToken);
1287+
// Remove any subscription with the doc suffix
1288+
if (this._emitterSubscriptions.has(args.changeListenerToken + '_doc')) {
1289+
this._emitterSubscriptions
1290+
.get(args.changeListenerToken + '_doc')
1291+
?.remove();
1292+
this._emitterSubscriptions.delete(args.changeListenerToken + '_doc');
1293+
}
1294+
}
1295+
12111296
//remove the event subscription or you will have a leak
12121297
if (this._emitterSubscriptions.has(args.changeListenerToken)) {
12131298
this._emitterSubscriptions.get(args.changeListenerToken)?.remove();

src/cblite-js

0 commit comments

Comments
 (0)