@rpcbase/client 0.198.0 → 0.205.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,13 @@
1
1
  /* @flow */
2
2
  import {Platform} from "react-native"
3
- import {useCallback, useEffect, useState, useMemo, useId, useRef} from "react"
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useState,
7
+ useMemo,
8
+ useId,
9
+ useRef,
10
+ } from "react"
4
11
  import debug from "debug"
5
12
  import _omit from "lodash/omit"
6
13
  import LZString from "lz-string"
@@ -16,198 +23,213 @@ import useData from "./useData"
16
23
 
17
24
  const log = debug("rb:rts:useQuery")
18
25
 
26
+ const getUseQuery =
27
+ (register_query) =>
28
+ (model_name, query = {}, options = {}) => {
29
+ const id = useId()
30
+
31
+ // TODO: should the uid be gone here ? it's part of the auth layer, not this here
32
+ // TODO: retrieve this from future AuthContext in client
33
+ const uid = useMemo(() => {
34
+ // TODO: why is there a options.userId here?? (it was for mobile we need to unify this)
35
+ const _uid = Platform.OS === "web" ? getUid() : options.userId
36
+ return _uid
37
+ }, [])
38
+
39
+ // used to track if data was loaded synchronously (no need to show any loader)
40
+ const hasInitiallySetFromStorage = useRef(false)
41
+ const hasFirstReply = useRef(false)
42
+ const hasNetworkReply = useRef(false)
43
+ const lastDocRef = useRef(null)
44
+ // const [page, setPage] = useState(0)
45
+
46
+ const {
47
+ key = "",
48
+ projection = {},
49
+ sort = {},
50
+ useStorage = false,
51
+ } = options
52
+
53
+ const storageKey = useMemo(() => {
54
+ return `${uid}${key ? `.${key}` : ""}.${model_name}.${JSON.stringify(query)}.${JSON.stringify(projection)}.${JSON.stringify(sort)}`
55
+ }, [uid, key, model_name, query, projection, sort])
56
+
57
+ const [source, setSource] = useState()
58
+
59
+ const dataRef = useRef(null)
60
+ const [data, setData] = useData({
61
+ useStorage,
62
+ storageKey,
63
+ hasInitiallySetFromStorage,
64
+ })
65
+
66
+ const [error, setError] = useState()
67
+
68
+ const [loading, setLoading] = useState(() => {
69
+ if (hasInitiallySetFromStorage.current) {
70
+ return false
71
+ }
72
+ return true
73
+ })
19
74
 
20
- const getUseQuery = (register_query) => (
21
- model_name,
22
- query = {},
23
- options = {},
24
- ) => {
25
- const id = useId()
26
-
27
- // TODO: should the uid be gone here ? it's part of the auth layer, not this here
28
- // TODO: retrieve this from future AuthContext in client
29
- const uid = useMemo(() => {
30
- // TODO: why is there a options.userId here?? (it was for mobile we need to unify this)
31
- const _uid = Platform.OS === "web" ? getUid() : options.userId
32
- return _uid
33
- }, [])
34
-
35
- // used to track if data was loaded synchronously (no need to show any loader)
36
- const hasInitiallySetFromStorage = useRef(false)
37
- const hasFirstReply = useRef(false)
38
- const hasNetworkReply = useRef(false)
39
- const lastDocRef = useRef(null)
40
- // const [page, setPage] = useState(0)
41
-
42
- const {
43
- key = "",
44
- projection = {},
45
- sort = {},
46
- useStorage = false,
47
- } = options
48
-
49
- const storageKey = useMemo(() => {
50
- return `${uid}${key ? `.${key}` : ""}.${model_name}.${JSON.stringify(query)}.${JSON.stringify(projection)}.${JSON.stringify(sort)}`
51
- }, [uid, key, model_name, query, projection, sort])
52
-
53
- const [source, setSource] = useState()
54
-
55
- const dataRef = useRef(null)
56
- const [data, setData] = useData({useStorage, storageKey, hasInitiallySetFromStorage})
57
-
58
- const [error, setError] = useState()
59
-
60
- const [loading, setLoading] = useState(() => {
61
- if (hasInitiallySetFromStorage.current) {
62
- return false
63
- }
64
- return true
65
- })
66
-
67
- useEffect(() => {
68
- if (options.debug) {
69
- console.log("use query", model_name, query, options)
70
- }
71
- }, [model_name, query, options])
72
-
73
- const applyNewData = (newData, context) => {
74
- setData(newData)
75
- // set data in a ref so that it doesn't force re-rendering ie: unsubscribe / resubscribe
76
- dataRef.current = newData
77
-
78
- // useStorage currently used as a fast local cache, indexedDB
79
- // we only save network queries
80
- // TODO: use localstorage in react native and pouchdb everywhere
81
- if (useStorage && context.source === "network") {
82
- if (Platform.OS === "web") {
83
- localStorage.setItem(storageKey, LZString.compressToUTF16(JSON.stringify(newData)))
84
- } else {
85
- // TODO: this is done in pouchDB nOW ?????
86
- // TODO: RN MMKV
87
- console.log("mmkv NYI")
88
- }
89
- }
90
-
91
- if (newData?.length > 0) {
92
- lastDocRef.current = newData[newData.length - 1]
93
- }
94
- }
95
-
96
- const applyContext = (newContext) => {
97
- if (newContext.source !== source) {
98
- setSource(newContext.source)
99
- }
100
- }
101
-
102
- useEffect(() => {
103
- const queryKey = key || id
104
-
105
- if (!model_name) {
106
- console.warn("attempting to register query with empty collection, skipping")
107
- return
108
- }
109
-
110
- if (options.debug ) {
111
- console.log("register query", model_name, query, options)
112
- }
113
-
114
- const start = Date.now()
115
-
116
- log("will register query", model_name, query)
117
-
118
- const unsubscribe = register_query(model_name, query, {...options, key: queryKey}, (err, queryResult, context) => {
119
- log("callback answer with context", context, queryResult?.length)
120
-
121
- // believe it or not, the network can be faster than indexeddb...
122
- if (context.source === "cache" && hasNetworkReply.current) {
123
- log("skipping cache arriving later than network")
124
- return
125
- }
126
-
127
- // mark if we received from network
128
- if (context.source === "network" && !hasNetworkReply.current) {
129
- hasNetworkReply.current = true
130
- }
131
-
132
- if (options.debug) {
133
- console.log("query took", Date.now() - start, model_name, query)
134
- }
75
+ useEffect(() => {
76
+ if (options.debug) {
77
+ console.log("use query", model_name, query, options)
78
+ }
79
+ }, [model_name, query, options])
80
+
81
+ const applyNewData = (newData, context) => {
82
+ setData(newData)
83
+ // set data in a ref so that it doesn't force re-rendering ie: unsubscribe / resubscribe
84
+ dataRef.current = newData
85
+
86
+ // useStorage currently used as a fast local cache, indexedDB
87
+ // we only save network queries
88
+ // TODO: use localstorage in react native and pouchdb everywhere
89
+ if (useStorage && context.source === "network") {
90
+ if (Platform.OS === "web") {
91
+ localStorage.setItem(
92
+ storageKey,
93
+ LZString.compressToUTF16(JSON.stringify(newData)),
94
+ )
95
+ } else {
96
+ // TODO: this is done in pouchDB nOW ?????
97
+ // TODO: RN MMKV
98
+ console.log("mmkv NYI")
99
+ }
100
+ }
135
101
 
136
- setLoading(false)
137
- if (err) {
138
- setError(err)
139
- return
102
+ if (newData?.length > 0) {
103
+ lastDocRef.current = newData[newData.length - 1]
104
+ }
140
105
  }
141
106
 
142
- log("query callback", model_name, queryKey, JSON.stringify(query))
143
-
144
- // return if no data (this should be handled already)
145
- if (!queryResult) return
146
-
147
- let newData
148
- if (Array.isArray(queryResult)) {
149
- newData = queryResult.map((o) => _omit(o, "__txn_id"))
150
- } else {
151
- newData = _omit(queryResult, "__txn_id")
107
+ const applyContext = (newContext) => {
108
+ if (newContext.source !== source) {
109
+ setSource(newContext.source)
110
+ }
152
111
  }
153
112
 
154
- // we return once in any case
155
- if (!hasFirstReply.current) {
156
- hasFirstReply.current = true
113
+ useEffect(() => {
114
+ const queryKey = key || id
157
115
 
158
- // skip if we already have the data
159
- if (isEqualValues(data, newData)) {
160
- applyContext(context)
116
+ if (!model_name) {
117
+ console.warn(
118
+ "attempting to register query with empty collection, skipping",
119
+ )
161
120
  return
162
121
  }
163
122
 
164
- applyContext(context)
165
- applyNewData(newData, context)
166
- return
167
- }
168
-
169
- // TODO: this should be handled by the consumer with the context (cache or network)
170
- if (context.is_local && options.skipLocal && hasFirstReply.current) {
171
- log("skipping local update", key)
172
- return
173
- }
174
-
175
- if (__DEV__ && isEqual(data, newData) && !isEqualValues(data, newData)) {
176
- alert("EQUALITY MISMATCH THIS SHOULD NOT HAPPEN!", data, newData)
177
- }
178
-
179
- if (!isEqualValues(dataRef.current, newData)) {
180
- applyContext(context)
181
- applyNewData(newData, context)
182
- } else {
183
- applyContext(context)
184
- }
185
- })
186
-
187
- return () => {
188
- log && log("useQuery cleanup unsubscribe()")
189
- typeof unsubscribe === "function" && unsubscribe()
190
- }
191
-
192
- // WARNING: do not change the hooks dependencies param or you risk creating infinite loops as it unsubscribes on cleanup
193
- // TODO: this isnt right we need to update on options change too
194
- }, [JSON.stringify(query), key])
195
-
123
+ if (options.debug) {
124
+ console.log("register query", model_name, query, options)
125
+ }
196
126
 
197
- const loadNextPage = useCallback(() => {
198
- console.log("loadNextPage NYI")
199
- }, [])
127
+ const start = Date.now()
128
+
129
+ log("will register query", model_name, query)
130
+
131
+ const unsubscribe = register_query(
132
+ model_name,
133
+ query,
134
+ {...options, key: queryKey, uid},
135
+ (err, queryResult, context) => {
136
+ log("callback answer with context", context, queryResult?.length)
137
+
138
+ // believe it or not, the network can be faster than indexeddb...
139
+ if (context.source === "cache" && hasNetworkReply.current) {
140
+ log("skipping cache arriving later than network")
141
+ return
142
+ }
143
+
144
+ // mark if we received from network
145
+ if (context.source === "network" && !hasNetworkReply.current) {
146
+ hasNetworkReply.current = true
147
+ }
148
+
149
+ if (options.debug) {
150
+ console.log("query took", Date.now() - start, model_name, query)
151
+ }
152
+
153
+ setLoading(false)
154
+ if (err) {
155
+ setError(err)
156
+ return
157
+ }
158
+
159
+ log("query callback", model_name, queryKey, JSON.stringify(query))
160
+
161
+ // return if no data (this should be handled already)
162
+ if (!queryResult) return
163
+
164
+ let newData
165
+ if (Array.isArray(queryResult)) {
166
+ newData = queryResult.map((o) => _omit(o, "__txn_id"))
167
+ } else {
168
+ newData = _omit(queryResult, "__txn_id")
169
+ }
170
+
171
+ // we return once in any case
172
+ if (!hasFirstReply.current) {
173
+ hasFirstReply.current = true
174
+
175
+ // skip if we already have the data
176
+ if (isEqualValues(data, newData)) {
177
+ applyContext(context)
178
+ return
179
+ }
180
+
181
+ applyContext(context)
182
+ applyNewData(newData, context)
183
+ return
184
+ }
185
+
186
+ // TODO: this should be handled by the consumer with the context (cache or network)
187
+ if (context.is_local && options.skipLocal && hasFirstReply.current) {
188
+ log("skipping local update", key)
189
+ return
190
+ }
191
+
192
+ if (
193
+ __DEV__ &&
194
+ isEqual(data, newData) &&
195
+ !isEqualValues(data, newData)
196
+ ) {
197
+ alert("EQUALITY MISMATCH THIS SHOULD NOT HAPPEN!", data, newData)
198
+ }
199
+
200
+ if (!isEqualValues(dataRef.current, newData)) {
201
+ applyContext(context)
202
+ applyNewData(newData, context)
203
+ } else {
204
+ applyContext(context)
205
+ }
206
+ },
207
+ )
208
+
209
+ return () => {
210
+ log && log("useQuery cleanup unsubscribe()")
211
+ typeof unsubscribe === "function" && unsubscribe()
212
+ }
200
213
 
214
+ // WARNING: do not change the hooks dependencies param or you risk creating infinite loops as it unsubscribes on cleanup
215
+ // TODO: this isnt right we need to update on options change too
216
+ }, [JSON.stringify(query), key])
201
217
 
202
- const result = useMemo(() => ({data, source, error, loading, loadNextPage}), [data, source, error, loading, loadNextPage])
218
+ const loadNextPage = useCallback(() => {
219
+ console.log("loadNextPage NYI")
220
+ }, [])
203
221
 
204
- // TODO:
205
- // if (Array.isArray(result.data) && !result.source) {
206
- // console.warn("RESULT HAS NO SOURCE", {data, error, loading, source})
207
- // }
222
+ const result = useMemo(
223
+ () => ({data, source, error, loading, loadNextPage}),
224
+ [data, source, error, loading, loadNextPage],
225
+ )
208
226
 
209
- return result
210
- }
227
+ // TODO:
228
+ // if (Array.isArray(result.data) && !result.source) {
229
+ // console.warn("RESULT HAS NO SOURCE", {data, error, loading, source})
230
+ // }
211
231
 
232
+ return result
233
+ }
212
234
 
213
235
  export default getUseQuery
package/rts/rts.js CHANGED
@@ -39,7 +39,7 @@ export const add_local_txn = (txn_id) => {
39
39
 
40
40
 
41
41
  // TODO: add compression / decompression
42
- const dispatch_query_payload = (payload) => {
42
+ const dispatch_query_payload = (payload, uid) => {
43
43
  log("dispatch_query_payload", payload)
44
44
 
45
45
  const {model_name, query_key} = payload
@@ -83,19 +83,19 @@ const dispatch_query_payload = (payload) => {
83
83
 
84
84
  // TODO: pouchdb on react native
85
85
  if (Platform.OS === "web") {
86
- store.update_docs(model_name, data)
86
+ store.update_docs(model_name, data, uid)
87
87
  }
88
88
  }
89
89
 
90
90
 
91
- export const connect = (tenant_id) => new Promise((resolve) => {
92
-
93
- if (tenant_id) {
94
- log("rts client will connect")
95
- } else {
96
- log("no tenant_id, rts connect will skip")
91
+ export const connect = (tenant_id, user_id) => new Promise((resolve) => {
92
+ if (!tenant_id) {
93
+ log("missing tenant_id, skipping")
97
94
  return
98
95
  }
96
+ assert(user_id, "missing user_id")
97
+
98
+ log("rts client will connect")
99
99
 
100
100
  _socket = io(getBaseUrl(), {
101
101
  forceNew: true,
@@ -126,7 +126,7 @@ export const connect = (tenant_id) => new Promise((resolve) => {
126
126
 
127
127
  _socket.on("query_payload", (payload) => {
128
128
  // console.log("socket:query_payload", payload)
129
- dispatch_query_payload(payload)
129
+ dispatch_query_payload(payload, user_id)
130
130
  })
131
131
 
132
132
  _socket.on("delete_doc", (payload) => {
@@ -151,13 +151,13 @@ export const disconnect = () => {
151
151
  }
152
152
 
153
153
 
154
- export const reconnect = (tenant_id) => {
154
+ export const reconnect = (tenant_id, user_id) => {
155
155
  log("socket will force reconnect")
156
156
 
157
157
  // destroy current socket if exists
158
158
  disconnect()
159
159
 
160
- connect(tenant_id)
160
+ connect(tenant_id, user_id)
161
161
  }
162
162
 
163
163
  // register a query
@@ -204,7 +204,7 @@ export const register_query = (model_name, query, _options, _callback) => {
204
204
 
205
205
  if (Platform.OS === "web") {
206
206
  // run the query from the cache a first time
207
- store.run_query({model_name, query, query_key, options}, callback)
207
+ store.run_query({model_name, query, options}, callback)
208
208
  }
209
209
 
210
210
  return () => {
package/rts/signout.ts ADDED
@@ -0,0 +1,8 @@
1
+ import * as get_collection from "./store/get_collection"
2
+ import {disconnect} from "./rts"
3
+
4
+
5
+ export const signout = async() => {
6
+ get_collection.destroy_all()
7
+ disconnect()
8
+ }
@@ -22,24 +22,34 @@ PouchDB.prefix = prefix
22
22
  PouchDB.plugin(IndexedDBAdapter)
23
23
  PouchDB.plugin(FindPlugin)
24
24
 
25
- const _cols_store = Object.create(null)
25
+ let _cols_store = Object.create(null)
26
26
 
27
- const get_collection = (col_name) => {
27
+ export const get_collection = (col_name, options) => {
28
28
 
29
29
  if (!col_name) {
30
30
  console.warn("supplied invalid / empty collection name to rts")
31
31
  }
32
+ if (!options.uid) {
33
+ console.warn("rts: get_collection: missing options.uid")
34
+ }
35
+
36
+ const col_key = `${options.uid}/${col_name}`
32
37
 
33
- if (_cols_store[col_name]) {
34
- return _cols_store[col_name]
38
+ if (_cols_store[col_key]) {
39
+ return _cols_store[col_key]
35
40
  } else {
36
41
  // https://pouchdb.com/api.html#create_database
37
- const col = new PouchDB(col_name, { adapter: "indexeddb", revs_limit: 1 })
38
- _cols_store[col_name] = col
42
+ const col = new PouchDB(col_key, {adapter: "indexeddb", revs_limit: 1})
43
+ _cols_store[col_key] = col
39
44
 
40
45
  return col
41
46
  }
42
47
 
43
48
  }
44
49
 
45
- export default get_collection
50
+
51
+ export const destroy_all = async() => {
52
+ await Promise.map(Object.values(_cols_store), (db) => db.destroy())
53
+
54
+ _cols_store = Object.create(null)
55
+ }
@@ -5,13 +5,13 @@ import "./debug"
5
5
 
6
6
  import {UNDERSCORE_PREFIX} from "./constants"
7
7
 
8
- import get_collection from "./get_collection"
8
+ import {get_collection} from "./get_collection"
9
9
  import update_docs from "./update_docs"
10
10
  import satisfies_projection from "./satisfies_projection"
11
11
  import replace_query_keys from "./replace_query_keys"
12
12
 
13
- const log = debug("rb:rts:store")
14
13
 
14
+ const log = debug("rb:rts:store")
15
15
 
16
16
  // TODO: listening for changes
17
17
  // https://github.com/pouchdb/pouchdb/tree/master/packages/node_modules/pouchdb-find#dbcreateindexindex--callback
@@ -39,12 +39,12 @@ const log = debug("rb:rts:store")
39
39
 
40
40
  // TODO: implement store in a shared worker
41
41
  // TODO: should we filter all docs by projection ? or just the ones where the projection isn't complete ?
42
- const run_query = async({model_name, query, query_key, options}, callback) => {
43
- // console.log("run_query", {model_name, query, query_key, options})
42
+ const run_query = async({model_name, query, options}, callback) => {
43
+ log("run_query", {model_name, query, options})
44
44
  // console.time("store run_query")
45
45
 
46
46
  // TODO: we should prefix model_name with RB_TENANT_ID + env_id
47
- const collection = get_collection(model_name)
47
+ const collection = get_collection(model_name, options)
48
48
 
49
49
  const replaced_query = replace_query_keys(query, (k) => (k.startsWith("_") && k !== "_id") ? `${UNDERSCORE_PREFIX}${k}` : k)
50
50
 
@@ -64,7 +64,7 @@ const run_query = async({model_name, query, query_key, options}, callback) => {
64
64
  .map(({_rev, ...doc}) => {
65
65
  // TODO: handle projections here
66
66
  const remapped_doc = Object.entries(doc).reduce((new_doc, [key, value]) => {
67
- let new_key = key.startsWith('$_') ? key.replace(/^\$_/, "") : key
67
+ let new_key = key.startsWith("$_") ? key.replace(/^\$_/, "") : key
68
68
  new_doc[new_key] = value
69
69
  return new_doc
70
70
  }, {})
@@ -82,7 +82,7 @@ const run_query = async({model_name, query, query_key, options}, callback) => {
82
82
  mapped_docs = mapped_docs.sort((a, b) => {
83
83
  for (const key in options.sort) {
84
84
  // Check if property exists on both objects
85
- if (a.hasOwnProperty(key) && b.hasOwnProperty(key)) {
85
+ if (Object.hasOwn(a, key) && Object.hasOwn(b, key)) {
86
86
  const dir = options.sort[key] // Direction of sorting: 1 or -1
87
87
 
88
88
  if (a[key] < b[key]) return -1 * dir
@@ -1,11 +1,12 @@
1
1
  /* @flow */
2
- import get_collection from "./get_collection"
2
+
3
+ import {get_collection} from "./get_collection"
3
4
 
4
5
  import {UNDERSCORE_PREFIX} from "./constants"
5
6
 
6
7
 
7
- const update_docs = async(model_name, data) => {
8
- const collection = get_collection(model_name)
8
+ const update_docs = async(model_name, data, uid) => {
9
+ const collection = get_collection(model_name, {uid})
9
10
 
10
11
  const all_ids = data.map((doc) => doc._id)
11
12
 
@@ -31,7 +32,7 @@ const update_docs = async(model_name, data) => {
31
32
 
32
33
  const op = Object.entries(mongo_doc)
33
34
  .reduce((new_doc, [key, value]) => {
34
- let new_key = key !== "_id" && key.startsWith('_') ? `${UNDERSCORE_PREFIX}${key}` : key
35
+ let new_key = key !== "_id" && key.startsWith("_") ? `${UNDERSCORE_PREFIX}${key}` : key
35
36
  new_doc[new_key] = value
36
37
  return new_doc
37
38
  }, current_doc)
@@ -4,7 +4,7 @@ import {forwardRef, useImperativeHandle, useEffect, useState, useRef} from "reac
4
4
  import {FormProvider} from "react-hook-form"
5
5
 
6
6
  import ActivityIndicator from "../../ActivityIndicator"
7
- import SubmitButton from "../../SubmitButton"
7
+ import {SubmitButton} from "../../../form/SubmitButton"
8
8
 
9
9
  import Modal from "../Modal"
10
10