Skip to content

Commit ea30043

Browse files
Merge pull request #49 from Wojtach/feat/live-query-listener
Feat: Live query listener
2 parents 4a237ce + fe58edb commit ea30043

File tree

7 files changed

+346
-7
lines changed

7 files changed

+346
-7
lines changed
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import React, { useContext, useState } from 'react';
2+
import { MutableDocument, Query } from 'cbl-reactnative';
3+
import { useStyleScheme } from '@/components/Themed/Themed';
4+
import { Button, Text, SafeAreaView, View } from 'react-native';
5+
import ResultListView from '@/components/ResultsListView/ResultsListView';
6+
import DatabaseScopeCollectionActionForm from '@/components/DatabaseScopeCollectionActionForm/DatabaseScopeCollectionActionForm';
7+
import useNavigationBarTitleOption from '@/hooks/useNativgationBarTitle';
8+
import { useNavigation } from '@react-navigation/native';
9+
import DatabaseContext from '@/providers/DatabaseContext';
10+
11+
export default function CollectionStatusScreen() {
12+
const { databases } = useContext(DatabaseContext)!;
13+
const [databaseName, setDatabaseName] = useState<string>('');
14+
const [scopeName, setScopeName] = useState<string>('');
15+
const [collectionName, setCollectionName] = useState<string>('');
16+
17+
const [isListenerAdded, setIsListenerAdded] = useState(false);
18+
const [token, setToken] = useState<string>('');
19+
const [query, setQuery] = useState<Query | null>(null);
20+
21+
const styles = useStyleScheme();
22+
const navigation = useNavigation();
23+
useNavigationBarTitleOption('Live Query', navigation);
24+
const [informationMessages, setInformationMessages] = useState<string[]>([]);
25+
26+
async function update(): Promise<void> {
27+
if (isListenerAdded && token !== '') {
28+
setInformationMessages(['::Information:: Listener already added.']);
29+
return;
30+
}
31+
32+
const database = databases[databaseName];
33+
if (!database) {
34+
setInformationMessages((prev) => [
35+
...prev,
36+
`::ERROR: Database ${databaseName} not found`,
37+
]);
38+
return;
39+
}
40+
41+
if (isListenerAdded && !!token) {
42+
setInformationMessages((prev) => [
43+
...prev,
44+
`::Information: Query Change listener already started`,
45+
]);
46+
return;
47+
}
48+
49+
try {
50+
const collection = await database.collection(collectionName, scopeName);
51+
52+
if (!collection) {
53+
setInformationMessages((prev) => [
54+
...prev,
55+
`::ERROR: ${scopeName}.${collectionName} not found`,
56+
]);
57+
return;
58+
}
59+
60+
setInformationMessages((prev) => [
61+
...prev,
62+
`::Information: Query Starting Change listener...`,
63+
]);
64+
65+
const queryString = `SELECT * FROM ${scopeName}.${collectionName} WHERE type = 'live-query'`;
66+
const query = database.createQuery(queryString);
67+
68+
const listenerToken = await query.addChangeListener((change) => {
69+
const date = new Date().toISOString();
70+
if (change.error) {
71+
setInformationMessages((prev) => [
72+
...prev,
73+
`${date} ::Change Listener Error:: ${change.error}`,
74+
]);
75+
return;
76+
}
77+
78+
if (change.results.length > 0) {
79+
const results = change.results.map((doc) => JSON.stringify(doc));
80+
setInformationMessages((prev) => [
81+
...prev,
82+
`${date} ::Information:: Query changed with: `,
83+
...results,
84+
]);
85+
} else {
86+
setInformationMessages((prev) => [
87+
...prev,
88+
`${date} ::Information:: No data in results`,
89+
]);
90+
}
91+
});
92+
93+
setQuery(query);
94+
setIsListenerAdded(true);
95+
setToken(listenerToken);
96+
setInformationMessages((prev) => [
97+
...prev,
98+
'::Information:: Query Listening for changes',
99+
]);
100+
} catch (error) {
101+
// @ts-ignore
102+
setInformationMessages((prev) => [...prev, `::ERROR: ${error.message}`]);
103+
}
104+
}
105+
106+
async function addDocument() {
107+
if (!(databaseName in databases)) {
108+
setInformationMessages((prev) => [
109+
...prev,
110+
'::Error:: Database is not set up',
111+
]);
112+
return;
113+
}
114+
115+
const database = databases[databaseName];
116+
const collection = await database.collection(collectionName, scopeName);
117+
118+
if (!collection) {
119+
setInformationMessages((prev) => [
120+
...prev,
121+
'::Error:: Database is not set up',
122+
]);
123+
return;
124+
}
125+
126+
const id = Math.floor(Math.random() * 1000).toString();
127+
const doc = new MutableDocument(`doc-${id}`);
128+
doc.setString('__id', id);
129+
doc.setString('type', 'live-query');
130+
await collection.save(doc);
131+
setInformationMessages((prev) => [
132+
...prev,
133+
`::Information:: Document with id ${doc.getId()} added.`,
134+
]);
135+
}
136+
137+
async function stop(): Promise<void> {
138+
const database = databases[databaseName];
139+
if (database != null && isListenerAdded && query) {
140+
await query.removeChangeListener(token);
141+
setIsListenerAdded(false);
142+
setInformationMessages([`::Information: Removed change listener`]);
143+
}
144+
setToken('');
145+
setDatabaseName('');
146+
setCollectionName('');
147+
setScopeName('');
148+
setQuery(null);
149+
}
150+
151+
return (
152+
<SafeAreaView style={styles.container}>
153+
<DatabaseScopeCollectionActionForm
154+
databaseName={databaseName}
155+
setDatabaseName={setDatabaseName}
156+
scopeName={scopeName}
157+
setScopeName={setScopeName}
158+
collectionName={collectionName}
159+
setCollectionName={setCollectionName}
160+
handleUpdatePressed={update}
161+
handleStopPressed={stop}
162+
/>
163+
<View
164+
style={{ flexDirection: 'column', paddingTop: 10, paddingBottom: 10 }}
165+
>
166+
<Text style={styles.header}>Step 1. Run listener</Text>
167+
<Text style={styles.header}>
168+
Step 2. Tap "Create document" button to see changes
169+
</Text>
170+
</View>
171+
<Button onPress={addDocument} title="Create document" color="#428cff" />
172+
<ResultListView
173+
style={styles}
174+
useScrollView={true}
175+
messages={informationMessages}
176+
/>
177+
</SafeAreaView>
178+
);
179+
}

expo-example/hooks/useQueryNavigationSections.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export function useQueryNavigationSections() {
1010
path: '/query/sqlPlusPlus',
1111
},
1212
{ id: 2, title: 'Query Parameters', path: '/query/parameters' },
13+
{ id: 3, title: 'Live Query', path: '/query/liveQuery' },
1314
],
1415
},
1516
];

ios/CblReactnative.mm

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,19 @@ @interface RCT_EXTERN_MODULE(CblReactnative, RCTEventEmitter)
236236

237237
// MARK: - SQL++ Query Functions
238238

239+
RCT_EXTERN_METHOD(query_AddChangeListener:
240+
(NSString *)changeListenerToken
241+
withQuery:(NSString *)query
242+
withParameters:(NSDictionary *)parameters
243+
fromDatabaseWithName:(NSString *)name
244+
withResolver:(RCTPromiseResolveBlock)resolve
245+
withRejecter:(RCTPromiseRejectBlock)reject)
246+
247+
RCT_EXTERN_METHOD(query_RemoveChangeListener:
248+
(NSString *)changeListenerToken
249+
withResolver:(RCTPromiseResolveBlock)resolve
250+
withRejecter:(RCTPromiseRejectBlock)reject)
251+
239252
RCT_EXTERN_METHOD(query_Execute:
240253
(NSString *)query
241254
withParameters: (NSDictionary *)parameters

ios/CblReactnative.swift

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,87 @@ class CblReactnative: RCTEventEmitter {
10411041
}
10421042

10431043
// MARK: - SQL++ Query Functions
1044+
@objc(query_AddChangeListener:withQuery:withParameters:fromDatabaseWithName:withResolver:withRejecter:)
1045+
func query_AddChangeListener(
1046+
changeListenerToken: NSString,
1047+
query: NSString,
1048+
parameters: NSDictionary,
1049+
name: NSString,
1050+
resolve: @escaping RCTPromiseResolveBlock,
1051+
reject: @escaping RCTPromiseRejectBlock
1052+
) -> Void {
1053+
let (isError, databaseName) = DataAdapter.shared.adaptDatabaseName(name: name, reject: reject)
1054+
let (isTokenError, token) = DataAdapter.shared.adaptNonEmptyString(value: changeListenerToken, propertyName: "changeListenerToken", reject: reject)
1055+
let (isQueryError, queryString) = DataAdapter.shared.adaptNonEmptyString(value: query, propertyName: "query", reject: reject)
1056+
1057+
if isError || isTokenError || isQueryError {
1058+
return
1059+
}
1060+
1061+
backgroundQueue.async {
1062+
do {
1063+
guard let database = DatabaseManager.shared.getDatabase(databaseName) else {
1064+
reject("DATABASE_ERROR", "Could not find database with name \(databaseName)", nil)
1065+
return
1066+
}
1067+
1068+
let query = try database.createQuery(queryString)
1069+
1070+
if parameters.count > 0 {
1071+
let params = try QueryHelper.getParamatersFromJson(parameters as? [String: Any] ?? [:])
1072+
query.parameters = params
1073+
}
1074+
1075+
let listener = query.addChangeListener(withQueue: self.backgroundQueue) { [weak self] (change) in
1076+
guard let self = self else { return }
1077+
1078+
let resultData = NSMutableDictionary()
1079+
resultData.setValue(token, forKey: "token")
1080+
1081+
if let results = change.results {
1082+
// Convert results to JSON format
1083+
let resultJSONs = results.map { $0.toJSON() }
1084+
let jsonArray = "[" + resultJSONs.joined(separator: ",") + "]"
1085+
resultData.setValue(jsonArray, forKey: "data")
1086+
}
1087+
1088+
if let error = change.error {
1089+
resultData.setValue(error.localizedDescription, forKey: "error")
1090+
}
1091+
1092+
self.sendEvent(withName: self.kQueryChange, body: resultData)
1093+
}
1094+
1095+
self.queryChangeListeners[token] = listener
1096+
resolve(nil)
1097+
} catch let error as NSError {
1098+
reject("DATABASE_ERROR", error.localizedDescription, nil)
1099+
} catch {
1100+
reject("DATABASE_ERROR", error.localizedDescription, nil)
1101+
}
1102+
}
1103+
}
1104+
1105+
@objc(query_RemoveChangeListener:withResolver:withRejecter:)
1106+
func query_RemoveChangeListener(
1107+
changeListenerToken: NSString,
1108+
resolve: @escaping RCTPromiseResolveBlock,
1109+
reject: @escaping RCTPromiseRejectBlock
1110+
) -> Void {
1111+
let token = String(changeListenerToken)
1112+
1113+
backgroundQueue.async {
1114+
if let listener = self.queryChangeListeners[token] as? ListenerToken {
1115+
listener.remove()
1116+
self.queryChangeListeners.removeValue(forKey: token)
1117+
resolve(nil)
1118+
return
1119+
}
1120+
1121+
reject("DATABASE_ERROR", "No query listener found for token \(token)", nil)
1122+
}
1123+
}
1124+
10441125
@objc(query_Execute:
10451126
withParameters:
10461127
fromDatabaseWithName:

0 commit comments

Comments
 (0)