@rpcbase/client 0.340.0 → 0.342.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.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +132 -4
- package/dist/notificationsRealtime.d.ts +18 -0
- package/dist/notificationsRealtime.d.ts.map +1 -0
- package/dist/rts/index.js +18 -786
- package/dist/useQuery-C2L-Rgat.js +788 -0
- package/package.json +1 -1
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
import { useId, useState, useRef, useEffect, useMemo } from "react";
|
|
2
|
+
const UNDERSCORE_PREFIX = "$_";
|
|
3
|
+
const DEFAULT_FIND_LIMIT = 4096;
|
|
4
|
+
let storeConfig = null;
|
|
5
|
+
let pouchDbPromise = null;
|
|
6
|
+
let lastAppliedPrefix = null;
|
|
7
|
+
const collections = /* @__PURE__ */ new Map();
|
|
8
|
+
const dbNamesByPrefix = /* @__PURE__ */ new Map();
|
|
9
|
+
const unwrapDefault = (mod) => {
|
|
10
|
+
if (!mod || typeof mod !== "object") return mod;
|
|
11
|
+
const maybe = mod;
|
|
12
|
+
return maybe.default ?? mod;
|
|
13
|
+
};
|
|
14
|
+
const ensureBrowser$1 = () => {
|
|
15
|
+
if (typeof window === "undefined") {
|
|
16
|
+
throw new Error("RTS PouchDB store can only be used in the browser");
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const computeBasePrefix = ({ tenantId, appName }) => {
|
|
20
|
+
let prefix = "rb/";
|
|
21
|
+
if (appName) prefix += `${appName}/`;
|
|
22
|
+
prefix += `${tenantId}/`;
|
|
23
|
+
return prefix;
|
|
24
|
+
};
|
|
25
|
+
const getDbNamesKey = (prefix) => `rb:rts:pouchDbs:${prefix}`;
|
|
26
|
+
const getPrefixOverrideKey = ({ tenantId, appName }) => `rb:rts:pouchPrefix:${appName ?? ""}:${tenantId}`;
|
|
27
|
+
const readPrefixOverride = ({ tenantId, appName }) => {
|
|
28
|
+
try {
|
|
29
|
+
const value = window.localStorage.getItem(getPrefixOverrideKey({ tenantId, appName }));
|
|
30
|
+
if (!value) return null;
|
|
31
|
+
if (!value.endsWith("/")) return `${value}/`;
|
|
32
|
+
return value;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const getPrefix = () => {
|
|
38
|
+
if (!storeConfig) {
|
|
39
|
+
throw new Error("RTS PouchDB store is not configured");
|
|
40
|
+
}
|
|
41
|
+
if (storeConfig.prefix) return storeConfig.prefix;
|
|
42
|
+
const basePrefix = computeBasePrefix({ tenantId: storeConfig.tenantId, appName: storeConfig.appName });
|
|
43
|
+
const override = readPrefixOverride({ tenantId: storeConfig.tenantId, appName: storeConfig.appName });
|
|
44
|
+
return override ?? basePrefix;
|
|
45
|
+
};
|
|
46
|
+
const loadDbNames = (prefix) => {
|
|
47
|
+
const existing = dbNamesByPrefix.get(prefix);
|
|
48
|
+
if (existing) return existing;
|
|
49
|
+
const names = /* @__PURE__ */ new Set();
|
|
50
|
+
try {
|
|
51
|
+
const raw = window.localStorage.getItem(getDbNamesKey(prefix));
|
|
52
|
+
if (raw) {
|
|
53
|
+
const parsed = JSON.parse(raw);
|
|
54
|
+
if (Array.isArray(parsed)) {
|
|
55
|
+
for (const value of parsed) {
|
|
56
|
+
if (typeof value === "string" && value) names.add(value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
return names;
|
|
62
|
+
}
|
|
63
|
+
dbNamesByPrefix.set(prefix, names);
|
|
64
|
+
return names;
|
|
65
|
+
};
|
|
66
|
+
const persistDbNames = (prefix, names) => {
|
|
67
|
+
try {
|
|
68
|
+
if (!names.size) {
|
|
69
|
+
window.localStorage.removeItem(getDbNamesKey(prefix));
|
|
70
|
+
dbNamesByPrefix.delete(prefix);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
window.localStorage.setItem(getDbNamesKey(prefix), JSON.stringify(Array.from(names)));
|
|
74
|
+
dbNamesByPrefix.set(prefix, names);
|
|
75
|
+
} catch {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const registerDbName = (prefix, dbName) => {
|
|
80
|
+
if (!prefix || !dbName) return;
|
|
81
|
+
const names = loadDbNames(prefix);
|
|
82
|
+
if (names.has(dbName)) return;
|
|
83
|
+
names.add(dbName);
|
|
84
|
+
persistDbNames(prefix, names);
|
|
85
|
+
};
|
|
86
|
+
const unregisterDbName = (prefix, dbName) => {
|
|
87
|
+
if (!prefix || !dbName) return;
|
|
88
|
+
const names = loadDbNames(prefix);
|
|
89
|
+
if (!names.delete(dbName)) return;
|
|
90
|
+
persistDbNames(prefix, names);
|
|
91
|
+
};
|
|
92
|
+
const getPouchDb = async () => {
|
|
93
|
+
ensureBrowser$1();
|
|
94
|
+
if (!pouchDbPromise) {
|
|
95
|
+
pouchDbPromise = (async () => {
|
|
96
|
+
const [core, indexedDbAdapter, findPlugin] = await Promise.all([
|
|
97
|
+
import("pouchdb-core"),
|
|
98
|
+
import("pouchdb-adapter-indexeddb"),
|
|
99
|
+
import("pouchdb-find")
|
|
100
|
+
]);
|
|
101
|
+
const PouchDB = unwrapDefault(core);
|
|
102
|
+
PouchDB.plugin(unwrapDefault(indexedDbAdapter));
|
|
103
|
+
PouchDB.plugin(unwrapDefault(findPlugin));
|
|
104
|
+
return PouchDB;
|
|
105
|
+
})();
|
|
106
|
+
}
|
|
107
|
+
return pouchDbPromise;
|
|
108
|
+
};
|
|
109
|
+
const applyPrefix = (PouchDB) => {
|
|
110
|
+
const prefix = getPrefix();
|
|
111
|
+
if (prefix === lastAppliedPrefix) return;
|
|
112
|
+
PouchDB.prefix = prefix;
|
|
113
|
+
lastAppliedPrefix = prefix;
|
|
114
|
+
};
|
|
115
|
+
const configureRtsPouchStore = (config) => {
|
|
116
|
+
storeConfig = config;
|
|
117
|
+
lastAppliedPrefix = null;
|
|
118
|
+
collections.clear();
|
|
119
|
+
};
|
|
120
|
+
const getCollection = async (modelName, options) => {
|
|
121
|
+
const PouchDB = await getPouchDb();
|
|
122
|
+
applyPrefix(PouchDB);
|
|
123
|
+
const prefix = getPrefix();
|
|
124
|
+
const dbName = `${options.uid}/${modelName}`;
|
|
125
|
+
const dbKey = `${prefix}${dbName}`;
|
|
126
|
+
const existing = collections.get(dbKey);
|
|
127
|
+
if (existing) return existing;
|
|
128
|
+
registerDbName(prefix, dbName);
|
|
129
|
+
const db = new PouchDB(dbName, { adapter: "indexeddb", revs_limit: 1 });
|
|
130
|
+
collections.set(dbKey, db);
|
|
131
|
+
return db;
|
|
132
|
+
};
|
|
133
|
+
const replaceQueryKeys = (value, replaceKey) => {
|
|
134
|
+
if (typeof value !== "object" || value === null) {
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
if (Array.isArray(value)) {
|
|
138
|
+
return value.map((item) => replaceQueryKeys(item, replaceKey));
|
|
139
|
+
}
|
|
140
|
+
const obj = value;
|
|
141
|
+
const next = Object.create(Object.getPrototypeOf(obj));
|
|
142
|
+
for (const key of Object.keys(obj)) {
|
|
143
|
+
if (/^\$/.test(key) || /\.\d+$/.test(key)) {
|
|
144
|
+
throw new Error(`replaceQueryKeys: Unexpected key format: ${key}`);
|
|
145
|
+
}
|
|
146
|
+
const newKey = replaceKey(key);
|
|
147
|
+
next[newKey] = replaceQueryKeys(obj[key], replaceKey);
|
|
148
|
+
}
|
|
149
|
+
return next;
|
|
150
|
+
};
|
|
151
|
+
const getKeys = (obj, parentKey = "") => {
|
|
152
|
+
const keys = [];
|
|
153
|
+
for (const key of Object.keys(obj)) {
|
|
154
|
+
const nextKey = parentKey ? `${parentKey}.${key}` : key;
|
|
155
|
+
const value = obj[key];
|
|
156
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
157
|
+
keys.push(...getKeys(value, nextKey));
|
|
158
|
+
} else {
|
|
159
|
+
keys.push(nextKey);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return keys;
|
|
163
|
+
};
|
|
164
|
+
const satisfiesProjection = (doc, projection) => {
|
|
165
|
+
const docKeys = new Set(getKeys(doc));
|
|
166
|
+
const projectionKeys = new Set(Object.keys(projection).filter((key) => projection[key] === 1));
|
|
167
|
+
if (!projectionKeys.has("_id")) {
|
|
168
|
+
docKeys.delete("_id");
|
|
169
|
+
}
|
|
170
|
+
if (projectionKeys.size > docKeys.size) return false;
|
|
171
|
+
for (const key of projectionKeys) {
|
|
172
|
+
if (!docKeys.has(key)) return false;
|
|
173
|
+
}
|
|
174
|
+
return true;
|
|
175
|
+
};
|
|
176
|
+
const remapDocFromStorage = (doc) => {
|
|
177
|
+
const next = {};
|
|
178
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
179
|
+
const newKey = key.startsWith(UNDERSCORE_PREFIX) ? key.replace(/^\$_/, "") : key;
|
|
180
|
+
next[newKey] = value;
|
|
181
|
+
}
|
|
182
|
+
return next;
|
|
183
|
+
};
|
|
184
|
+
const runQuery = async ({
|
|
185
|
+
modelName,
|
|
186
|
+
query = {},
|
|
187
|
+
options
|
|
188
|
+
}) => {
|
|
189
|
+
const collection = await getCollection(modelName, { uid: options.uid });
|
|
190
|
+
const replacedQuery = replaceQueryKeys(
|
|
191
|
+
query,
|
|
192
|
+
(key) => key.startsWith("_") && key !== "_id" ? `${UNDERSCORE_PREFIX}${key}` : key
|
|
193
|
+
);
|
|
194
|
+
const limit = typeof options.limit === "number" ? Math.abs(options.limit) : DEFAULT_FIND_LIMIT;
|
|
195
|
+
const { docs } = await collection.find({
|
|
196
|
+
selector: replacedQuery,
|
|
197
|
+
limit
|
|
198
|
+
});
|
|
199
|
+
let mappedDocs = docs.map(({ _rev: _revIgnored, ...rest }) => remapDocFromStorage(rest));
|
|
200
|
+
if (options.projection) {
|
|
201
|
+
mappedDocs = mappedDocs.filter((doc) => satisfiesProjection(doc, options.projection));
|
|
202
|
+
}
|
|
203
|
+
if (options.sort) {
|
|
204
|
+
mappedDocs = mappedDocs.sort((a, b) => {
|
|
205
|
+
for (const key of Object.keys(options.sort)) {
|
|
206
|
+
if (!Object.hasOwn(a, key) || !Object.hasOwn(b, key)) continue;
|
|
207
|
+
const dir = options.sort[key];
|
|
208
|
+
const aVal = a[key];
|
|
209
|
+
const bVal = b[key];
|
|
210
|
+
if (typeof aVal === "number" && typeof bVal === "number") {
|
|
211
|
+
if (aVal < bVal) return -1 * dir;
|
|
212
|
+
if (aVal > bVal) return 1 * dir;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (typeof aVal === "string" && typeof bVal === "string") {
|
|
216
|
+
if (aVal < bVal) return -1 * dir;
|
|
217
|
+
if (aVal > bVal) return 1 * dir;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return 0;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return { data: mappedDocs, context: { source: "cache" } };
|
|
225
|
+
};
|
|
226
|
+
const updateDocs = async (modelName, data, uid) => {
|
|
227
|
+
const collection = await getCollection(modelName, { uid });
|
|
228
|
+
const allIds = data.map((doc) => doc._id).filter(Boolean);
|
|
229
|
+
if (!allIds.length) return;
|
|
230
|
+
const { docs: currentDocs } = await collection.find({
|
|
231
|
+
selector: { _id: { $in: allIds } },
|
|
232
|
+
fields: ["_id", "_rev"],
|
|
233
|
+
limit: allIds.length
|
|
234
|
+
});
|
|
235
|
+
const currentDocsById = currentDocs.reduce((acc, doc) => {
|
|
236
|
+
const id = String(doc._id ?? "");
|
|
237
|
+
if (id) acc[id] = doc;
|
|
238
|
+
return acc;
|
|
239
|
+
}, {});
|
|
240
|
+
const newDocs = data.map((mongoDoc) => {
|
|
241
|
+
const currentDoc = currentDocsById[mongoDoc._id] ?? { _id: mongoDoc._id };
|
|
242
|
+
const nextDoc = Object.entries(mongoDoc).reduce((acc, [key, value]) => {
|
|
243
|
+
const newKey = key !== "_id" && key.startsWith("_") ? `${UNDERSCORE_PREFIX}${key}` : key;
|
|
244
|
+
acc[newKey] = value;
|
|
245
|
+
return acc;
|
|
246
|
+
}, { ...currentDoc });
|
|
247
|
+
const rev = currentDoc._rev;
|
|
248
|
+
if (typeof rev === "string" && rev) {
|
|
249
|
+
nextDoc._rev = rev;
|
|
250
|
+
} else {
|
|
251
|
+
delete nextDoc._rev;
|
|
252
|
+
}
|
|
253
|
+
return nextDoc;
|
|
254
|
+
});
|
|
255
|
+
await collection.bulkDocs(newDocs);
|
|
256
|
+
};
|
|
257
|
+
const deleteDocs = async (modelName, ids, uid) => {
|
|
258
|
+
const collection = await getCollection(modelName, { uid });
|
|
259
|
+
const allIds = ids.map((id) => String(id ?? "")).filter(Boolean);
|
|
260
|
+
if (!allIds.length) return;
|
|
261
|
+
const { docs: currentDocs } = await collection.find({
|
|
262
|
+
selector: { _id: { $in: allIds } },
|
|
263
|
+
fields: ["_id", "_rev"],
|
|
264
|
+
limit: allIds.length
|
|
265
|
+
});
|
|
266
|
+
const deletions = currentDocs.map((doc) => ({
|
|
267
|
+
_id: String(doc?._id ?? ""),
|
|
268
|
+
_rev: doc?._rev,
|
|
269
|
+
_deleted: true
|
|
270
|
+
})).filter((doc) => doc._id && typeof doc._rev === "string" && doc._rev);
|
|
271
|
+
if (!deletions.length) return;
|
|
272
|
+
await collection.bulkDocs(deletions);
|
|
273
|
+
};
|
|
274
|
+
const destroyCollection = async (modelName, uid) => {
|
|
275
|
+
const collection = await getCollection(modelName, { uid });
|
|
276
|
+
const prefix = getPrefix();
|
|
277
|
+
const dbName = `${uid}/${modelName}`;
|
|
278
|
+
collections.delete(`${prefix}${dbName}`);
|
|
279
|
+
unregisterDbName(prefix, dbName);
|
|
280
|
+
await collection.destroy();
|
|
281
|
+
};
|
|
282
|
+
const resetRtsPouchStore = ({ tenantId, appName }) => {
|
|
283
|
+
ensureBrowser$1();
|
|
284
|
+
const basePrefix = computeBasePrefix({ tenantId, appName });
|
|
285
|
+
const oldPrefix = readPrefixOverride({ tenantId, appName }) ?? basePrefix;
|
|
286
|
+
const dbNames = Array.from(loadDbNames(oldPrefix));
|
|
287
|
+
const openDbs = Array.from(collections.entries()).filter(([key]) => key.startsWith(oldPrefix)).map(([, db]) => db);
|
|
288
|
+
void (async () => {
|
|
289
|
+
const remaining = new Set(dbNames);
|
|
290
|
+
await Promise.all(openDbs.map((db) => db.destroy().catch(() => {
|
|
291
|
+
})));
|
|
292
|
+
if (remaining.size) {
|
|
293
|
+
const PouchDB = await getPouchDb();
|
|
294
|
+
const PouchDBForPrefix = PouchDB.defaults?.({}) ?? PouchDB;
|
|
295
|
+
PouchDBForPrefix.prefix = oldPrefix;
|
|
296
|
+
await Promise.all(Array.from(remaining).map(async (name) => {
|
|
297
|
+
const db = new PouchDBForPrefix(name, { adapter: "indexeddb", revs_limit: 1 });
|
|
298
|
+
await db.destroy().then(() => {
|
|
299
|
+
remaining.delete(name);
|
|
300
|
+
}).catch(() => {
|
|
301
|
+
});
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
if (remaining.size) {
|
|
305
|
+
persistDbNames(oldPrefix, remaining);
|
|
306
|
+
} else {
|
|
307
|
+
persistDbNames(oldPrefix, /* @__PURE__ */ new Set());
|
|
308
|
+
}
|
|
309
|
+
})();
|
|
310
|
+
const newPrefix = `${basePrefix}reset-${Date.now().toString(16)}/`;
|
|
311
|
+
try {
|
|
312
|
+
window.localStorage.setItem(getPrefixOverrideKey({ tenantId, appName }), newPrefix);
|
|
313
|
+
} catch {
|
|
314
|
+
return newPrefix;
|
|
315
|
+
}
|
|
316
|
+
lastAppliedPrefix = null;
|
|
317
|
+
collections.clear();
|
|
318
|
+
return newPrefix;
|
|
319
|
+
};
|
|
320
|
+
const destroyAllCollections = async () => {
|
|
321
|
+
const dbs = Array.from(collections.values());
|
|
322
|
+
await Promise.all(dbs.map((db) => db.destroy()));
|
|
323
|
+
collections.clear();
|
|
324
|
+
};
|
|
325
|
+
const TENANT_ID_QUERY_PARAM = "rb-tenant-id";
|
|
326
|
+
const RTS_CHANGES_ROUTE = "/api/rb/rts/changes";
|
|
327
|
+
const MAX_TXN_BUF = 2048;
|
|
328
|
+
let socket = null;
|
|
329
|
+
let connectPromise = null;
|
|
330
|
+
let explicitDisconnect = false;
|
|
331
|
+
let currentTenantId = null;
|
|
332
|
+
let currentUid = null;
|
|
333
|
+
let connectOptions = {};
|
|
334
|
+
const localTxnBuf = [];
|
|
335
|
+
const queryCallbacks = /* @__PURE__ */ new Map();
|
|
336
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
337
|
+
const messageCallbacks = /* @__PURE__ */ new Map();
|
|
338
|
+
let reconnectTimer = null;
|
|
339
|
+
let reconnectAttempts = 0;
|
|
340
|
+
let syncPromise = null;
|
|
341
|
+
let syncKey = null;
|
|
342
|
+
const ensureBrowser = () => {
|
|
343
|
+
if (typeof window === "undefined") {
|
|
344
|
+
throw new Error("RTS websocket client can only be used in the browser");
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
const buildSocketUrl = (tenantId, _uid, options) => {
|
|
348
|
+
if (options.url) {
|
|
349
|
+
const url = new URL(options.url);
|
|
350
|
+
url.searchParams.set(TENANT_ID_QUERY_PARAM, tenantId);
|
|
351
|
+
return url.toString();
|
|
352
|
+
}
|
|
353
|
+
const base = new URL(window.location.href);
|
|
354
|
+
base.protocol = base.protocol === "https:" ? "wss:" : "ws:";
|
|
355
|
+
base.pathname = options.path ?? "/rts";
|
|
356
|
+
base.search = "";
|
|
357
|
+
base.hash = "";
|
|
358
|
+
base.searchParams.set(TENANT_ID_QUERY_PARAM, tenantId);
|
|
359
|
+
return base.toString();
|
|
360
|
+
};
|
|
361
|
+
const computeQueryKey = (query, options) => {
|
|
362
|
+
const key = options.key ?? "";
|
|
363
|
+
const projection = options.projection ? JSON.stringify(options.projection) : "";
|
|
364
|
+
const sort = options.sort ? JSON.stringify(options.sort) : "";
|
|
365
|
+
const limit = typeof options.limit === "number" ? String(options.limit) : "";
|
|
366
|
+
return `${key}${JSON.stringify(query)}${projection}${sort}${limit}`;
|
|
367
|
+
};
|
|
368
|
+
const sendToServer = (message) => {
|
|
369
|
+
if (!socket) return;
|
|
370
|
+
if (socket.readyState !== WebSocket.OPEN) return;
|
|
371
|
+
socket.send(JSON.stringify(message));
|
|
372
|
+
};
|
|
373
|
+
const resubscribeAll = () => {
|
|
374
|
+
for (const sub of subscriptions.values()) {
|
|
375
|
+
sendToServer({
|
|
376
|
+
type: "register-query",
|
|
377
|
+
modelName: sub.modelName,
|
|
378
|
+
queryKey: sub.queryKey,
|
|
379
|
+
query: sub.query,
|
|
380
|
+
options: sub.options
|
|
381
|
+
});
|
|
382
|
+
sendToServer({
|
|
383
|
+
type: "run-query",
|
|
384
|
+
modelName: sub.modelName,
|
|
385
|
+
queryKey: sub.queryKey,
|
|
386
|
+
query: sub.query,
|
|
387
|
+
options: sub.options
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const clearReconnectTimer = () => {
|
|
392
|
+
if (reconnectTimer === null) return;
|
|
393
|
+
if (typeof window !== "undefined") {
|
|
394
|
+
window.clearTimeout(reconnectTimer);
|
|
395
|
+
}
|
|
396
|
+
reconnectTimer = null;
|
|
397
|
+
};
|
|
398
|
+
const scheduleReconnect = () => {
|
|
399
|
+
clearReconnectTimer();
|
|
400
|
+
if (explicitDisconnect) return;
|
|
401
|
+
if (!currentTenantId || !currentUid) return;
|
|
402
|
+
const cfg = connectOptions.reconnect ?? {};
|
|
403
|
+
const maxAttempts = cfg.attempts ?? 128;
|
|
404
|
+
const delayMs = cfg.delayMs ?? 400;
|
|
405
|
+
const delayMaxMs = cfg.delayMaxMs ?? 1e4;
|
|
406
|
+
if (reconnectAttempts >= maxAttempts) return;
|
|
407
|
+
const delay = Math.min(delayMaxMs, delayMs * Math.pow(2, reconnectAttempts));
|
|
408
|
+
reconnectAttempts += 1;
|
|
409
|
+
reconnectTimer = window.setTimeout(() => {
|
|
410
|
+
void connectInternal(currentTenantId, currentUid, connectOptions, { resetReconnectAttempts: false });
|
|
411
|
+
}, delay);
|
|
412
|
+
};
|
|
413
|
+
const handleQueryPayload = (payload) => {
|
|
414
|
+
const { modelName, queryKey, data, error, txnId } = payload;
|
|
415
|
+
const cbKey = `${modelName}.${queryKey}`;
|
|
416
|
+
const callbacks = queryCallbacks.get(cbKey);
|
|
417
|
+
if (!callbacks || !callbacks.size) return;
|
|
418
|
+
const isLocal = !!(txnId && localTxnBuf.includes(txnId));
|
|
419
|
+
const context = { source: "network", isLocal, txnId };
|
|
420
|
+
if (error) {
|
|
421
|
+
for (const cb of callbacks) cb(error, void 0, context);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
for (const cb of callbacks) cb(null, data, context);
|
|
425
|
+
if (!currentUid) return;
|
|
426
|
+
const docs = Array.isArray(data) ? data.filter((doc) => {
|
|
427
|
+
if (!doc || typeof doc !== "object") return false;
|
|
428
|
+
return typeof doc._id === "string";
|
|
429
|
+
}) : [];
|
|
430
|
+
if (!docs.length) return;
|
|
431
|
+
void updateDocs(modelName, docs, currentUid).catch(() => {
|
|
432
|
+
});
|
|
433
|
+
};
|
|
434
|
+
const handleEvent = (payload) => {
|
|
435
|
+
const callbacks = messageCallbacks.get(payload.event);
|
|
436
|
+
if (!callbacks || !callbacks.size) return;
|
|
437
|
+
for (const cb of callbacks) cb(payload.payload);
|
|
438
|
+
};
|
|
439
|
+
const handleMessage = (event) => {
|
|
440
|
+
let parsed;
|
|
441
|
+
try {
|
|
442
|
+
parsed = JSON.parse(typeof event.data === "string" ? event.data : String(event.data));
|
|
443
|
+
} catch {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (!parsed || typeof parsed !== "object") return;
|
|
447
|
+
const message = parsed;
|
|
448
|
+
if (message.type === "query-payload") {
|
|
449
|
+
handleQueryPayload(message);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (message.type === "event") {
|
|
453
|
+
handleEvent(message);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
const addLocalTxn = (txnId) => {
|
|
457
|
+
if (!txnId) return;
|
|
458
|
+
localTxnBuf.push(txnId);
|
|
459
|
+
if (localTxnBuf.length > MAX_TXN_BUF) {
|
|
460
|
+
localTxnBuf.shift();
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
const getSyncStorageKey = ({ tenantId, uid, appName }) => `rb:rts:changesSeq:${appName ?? ""}:${tenantId}:${uid}`;
|
|
464
|
+
const readStoredSeq = (key) => {
|
|
465
|
+
try {
|
|
466
|
+
const raw = window.localStorage.getItem(key);
|
|
467
|
+
const num = raw ? Number(raw) : 0;
|
|
468
|
+
return Number.isFinite(num) && num >= 0 ? Math.floor(num) : 0;
|
|
469
|
+
} catch {
|
|
470
|
+
return 0;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
const writeStoredSeq = (key, value) => {
|
|
474
|
+
try {
|
|
475
|
+
window.localStorage.setItem(key, String(Math.max(0, Math.floor(value))));
|
|
476
|
+
} catch {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
const applyChangeBatch = async (changes, uid) => {
|
|
481
|
+
const resetModels = /* @__PURE__ */ new Set();
|
|
482
|
+
const deletesByModel = /* @__PURE__ */ new Map();
|
|
483
|
+
for (const change of changes) {
|
|
484
|
+
const modelName = typeof change.modelName === "string" ? change.modelName : "";
|
|
485
|
+
if (!modelName) continue;
|
|
486
|
+
if (change.op === "reset_model") {
|
|
487
|
+
resetModels.add(modelName);
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if (change.op === "delete") {
|
|
491
|
+
const docId = typeof change.docId === "string" ? change.docId : "";
|
|
492
|
+
if (!docId) continue;
|
|
493
|
+
const existing = deletesByModel.get(modelName) ?? [];
|
|
494
|
+
existing.push(docId);
|
|
495
|
+
deletesByModel.set(modelName, existing);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
for (const modelName of resetModels) {
|
|
499
|
+
await destroyCollection(modelName, uid).catch(() => {
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
for (const [modelName, ids] of deletesByModel.entries()) {
|
|
503
|
+
if (resetModels.has(modelName)) continue;
|
|
504
|
+
await deleteDocs(modelName, ids, uid).catch(() => {
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
const syncRtsChanges = async (tenantId, uid, options = {}) => {
|
|
509
|
+
ensureBrowser();
|
|
510
|
+
if (!tenantId || !uid) return;
|
|
511
|
+
const storageKey = getSyncStorageKey({ tenantId, uid, appName: options.appName });
|
|
512
|
+
let sinceSeq = readStoredSeq(storageKey);
|
|
513
|
+
for (let i = 0; i < 32; i += 1) {
|
|
514
|
+
const url = `${RTS_CHANGES_ROUTE}?${TENANT_ID_QUERY_PARAM}=${encodeURIComponent(tenantId)}`;
|
|
515
|
+
const response = await fetch(url, {
|
|
516
|
+
method: "POST",
|
|
517
|
+
credentials: "include",
|
|
518
|
+
headers: { "Content-Type": "application/json" },
|
|
519
|
+
body: JSON.stringify({ sinceSeq, limit: 2e3 })
|
|
520
|
+
});
|
|
521
|
+
if (!response.ok) return;
|
|
522
|
+
const payload = await response.json().catch(() => null);
|
|
523
|
+
if (!payload || typeof payload !== "object") return;
|
|
524
|
+
const ok = payload.ok;
|
|
525
|
+
if (ok !== true) return;
|
|
526
|
+
const latestSeq = Number(payload.latestSeq ?? 0);
|
|
527
|
+
const needsFullResync = Boolean(payload.needsFullResync);
|
|
528
|
+
if (needsFullResync) {
|
|
529
|
+
resetRtsPouchStore({ tenantId, appName: options.appName });
|
|
530
|
+
writeStoredSeq(storageKey, latestSeq);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const changesRaw = payload.changes;
|
|
534
|
+
const changes = Array.isArray(changesRaw) ? changesRaw : [];
|
|
535
|
+
const normalized = changes.map((c) => {
|
|
536
|
+
if (!c || typeof c !== "object") return null;
|
|
537
|
+
const obj = c;
|
|
538
|
+
const seq = Number(obj.seq ?? 0);
|
|
539
|
+
const modelName = typeof obj.modelName === "string" ? obj.modelName : String(obj.modelName ?? "");
|
|
540
|
+
const op = obj.op === "reset_model" ? "reset_model" : "delete";
|
|
541
|
+
const docId = typeof obj.docId === "string" && obj.docId ? obj.docId : obj.docId ? String(obj.docId) : void 0;
|
|
542
|
+
return { seq, modelName, op, ...docId ? { docId } : {} };
|
|
543
|
+
}).filter((c) => c !== null).filter(
|
|
544
|
+
(c) => Number.isFinite(c.seq) && c.seq > 0 && c.modelName && (c.op === "reset_model" || !!c.docId)
|
|
545
|
+
);
|
|
546
|
+
if (!normalized.length) {
|
|
547
|
+
writeStoredSeq(storageKey, latestSeq);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
await applyChangeBatch(normalized, uid);
|
|
551
|
+
const lastSeq = normalized.reduce((max, c) => c.seq > max ? c.seq : max, sinceSeq);
|
|
552
|
+
sinceSeq = lastSeq;
|
|
553
|
+
writeStoredSeq(storageKey, sinceSeq);
|
|
554
|
+
if (latestSeq > 0 && sinceSeq >= latestSeq) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
const ensureSynced = (tenantId, uid, options) => {
|
|
560
|
+
if (options.syncChanges === false) return;
|
|
561
|
+
const key = `${options.appName ?? ""}:${tenantId}:${uid}`;
|
|
562
|
+
if (syncPromise && syncKey === key) return;
|
|
563
|
+
syncKey = key;
|
|
564
|
+
syncPromise = syncRtsChanges(tenantId, uid, { appName: options.appName }).catch(() => {
|
|
565
|
+
}).finally(() => {
|
|
566
|
+
if (syncKey === key) {
|
|
567
|
+
syncPromise = null;
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
};
|
|
571
|
+
const connectInternal = (tenantId, uid, options, { resetReconnectAttempts }) => {
|
|
572
|
+
ensureBrowser();
|
|
573
|
+
if (!tenantId) return Promise.resolve();
|
|
574
|
+
if (!uid) throw new Error("Missing uid");
|
|
575
|
+
currentTenantId = tenantId;
|
|
576
|
+
currentUid = uid;
|
|
577
|
+
connectOptions = options;
|
|
578
|
+
if (options.configureStore !== false) {
|
|
579
|
+
configureRtsPouchStore({ tenantId, appName: options.appName });
|
|
580
|
+
}
|
|
581
|
+
ensureSynced(tenantId, uid, options);
|
|
582
|
+
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
|
|
583
|
+
return connectPromise ?? Promise.resolve();
|
|
584
|
+
}
|
|
585
|
+
explicitDisconnect = false;
|
|
586
|
+
clearReconnectTimer();
|
|
587
|
+
const url = buildSocketUrl(tenantId, uid, options);
|
|
588
|
+
connectPromise = new Promise((resolve, reject) => {
|
|
589
|
+
if (resetReconnectAttempts) reconnectAttempts = 0;
|
|
590
|
+
let opened = false;
|
|
591
|
+
let settled = false;
|
|
592
|
+
socket = new WebSocket(url);
|
|
593
|
+
socket.addEventListener("open", () => {
|
|
594
|
+
opened = true;
|
|
595
|
+
settled = true;
|
|
596
|
+
reconnectAttempts = 0;
|
|
597
|
+
resubscribeAll();
|
|
598
|
+
resolve();
|
|
599
|
+
});
|
|
600
|
+
socket.addEventListener("message", handleMessage);
|
|
601
|
+
socket.addEventListener("close", (event) => {
|
|
602
|
+
if (!opened && !settled) {
|
|
603
|
+
settled = true;
|
|
604
|
+
reject(new Error(`RTS WebSocket closed before opening (code=${event.code})`));
|
|
605
|
+
}
|
|
606
|
+
socket = null;
|
|
607
|
+
connectPromise = null;
|
|
608
|
+
scheduleReconnect();
|
|
609
|
+
});
|
|
610
|
+
socket.addEventListener("error", (err) => {
|
|
611
|
+
if (settled) return;
|
|
612
|
+
settled = true;
|
|
613
|
+
reject(err instanceof Error ? err : new Error("RTS WebSocket error"));
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
return connectPromise;
|
|
617
|
+
};
|
|
618
|
+
const connect = (tenantId, uid, options = {}) => {
|
|
619
|
+
return connectInternal(tenantId, uid, options, { resetReconnectAttempts: true });
|
|
620
|
+
};
|
|
621
|
+
const disconnect = () => {
|
|
622
|
+
explicitDisconnect = true;
|
|
623
|
+
clearReconnectTimer();
|
|
624
|
+
if (socket) {
|
|
625
|
+
try {
|
|
626
|
+
socket.close();
|
|
627
|
+
} catch {
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
socket = null;
|
|
631
|
+
connectPromise = null;
|
|
632
|
+
};
|
|
633
|
+
const reconnect = (tenantId, uid, options = {}) => {
|
|
634
|
+
disconnect();
|
|
635
|
+
return connect(tenantId, uid, options);
|
|
636
|
+
};
|
|
637
|
+
const registerQuery = (modelName, query, optionsOrCallback, callbackMaybe) => {
|
|
638
|
+
let options;
|
|
639
|
+
let callback;
|
|
640
|
+
if (typeof optionsOrCallback === "function") {
|
|
641
|
+
options = {};
|
|
642
|
+
callback = optionsOrCallback;
|
|
643
|
+
} else {
|
|
644
|
+
options = optionsOrCallback ?? {};
|
|
645
|
+
callback = callbackMaybe;
|
|
646
|
+
}
|
|
647
|
+
if (!callback) return void 0;
|
|
648
|
+
if (!modelName) return void 0;
|
|
649
|
+
const queryKey = computeQueryKey(query, options);
|
|
650
|
+
const cbKey = `${modelName}.${queryKey}`;
|
|
651
|
+
const set = queryCallbacks.get(cbKey) ?? /* @__PURE__ */ new Set();
|
|
652
|
+
set.add(callback);
|
|
653
|
+
queryCallbacks.set(cbKey, set);
|
|
654
|
+
subscriptions.set(cbKey, { modelName, query, options, queryKey });
|
|
655
|
+
if (currentUid) {
|
|
656
|
+
void runQuery({
|
|
657
|
+
modelName,
|
|
658
|
+
query,
|
|
659
|
+
options: {
|
|
660
|
+
uid: currentUid,
|
|
661
|
+
projection: options.projection,
|
|
662
|
+
sort: options.sort,
|
|
663
|
+
limit: options.limit
|
|
664
|
+
}
|
|
665
|
+
}).then(({ data, context }) => {
|
|
666
|
+
callback?.(null, data, context);
|
|
667
|
+
}).catch(() => {
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
sendToServer({ type: "run-query", modelName, queryKey, query, options });
|
|
671
|
+
sendToServer({ type: "register-query", modelName, queryKey, query, options });
|
|
672
|
+
return () => {
|
|
673
|
+
sendToServer({ type: "remove-query", modelName, queryKey });
|
|
674
|
+
const callbacks = queryCallbacks.get(cbKey);
|
|
675
|
+
callbacks?.delete(callback);
|
|
676
|
+
if (callbacks && callbacks.size === 0) {
|
|
677
|
+
queryCallbacks.delete(cbKey);
|
|
678
|
+
subscriptions.delete(cbKey);
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
};
|
|
682
|
+
const sendMessage = (event, payload) => {
|
|
683
|
+
sendToServer({ type: "event", event, payload });
|
|
684
|
+
};
|
|
685
|
+
const onMessage = (event, callback) => {
|
|
686
|
+
const set = messageCallbacks.get(event) ?? /* @__PURE__ */ new Set();
|
|
687
|
+
set.add(callback);
|
|
688
|
+
messageCallbacks.set(event, set);
|
|
689
|
+
return () => {
|
|
690
|
+
const callbacks = messageCallbacks.get(event);
|
|
691
|
+
callbacks?.delete(callback);
|
|
692
|
+
if (callbacks && callbacks.size === 0) messageCallbacks.delete(event);
|
|
693
|
+
};
|
|
694
|
+
};
|
|
695
|
+
const useQuery = (modelName, query = {}, options = {}) => {
|
|
696
|
+
const id = useId();
|
|
697
|
+
const key = options.key ?? id;
|
|
698
|
+
const projectionJson = options.projection ? JSON.stringify(options.projection) : "";
|
|
699
|
+
const sortJson = options.sort ? JSON.stringify(options.sort) : "";
|
|
700
|
+
const limitStr = typeof options.limit === "number" ? String(options.limit) : "";
|
|
701
|
+
const [data, setData] = useState(void 0);
|
|
702
|
+
const [source, setSource] = useState(void 0);
|
|
703
|
+
const [error, setError] = useState(void 0);
|
|
704
|
+
const [loading, setLoading] = useState(true);
|
|
705
|
+
const hasFirstReply = useRef(false);
|
|
706
|
+
const hasNetworkReply = useRef(false);
|
|
707
|
+
const lastDataJsonRef = useRef("");
|
|
708
|
+
useEffect(() => {
|
|
709
|
+
hasFirstReply.current = false;
|
|
710
|
+
hasNetworkReply.current = false;
|
|
711
|
+
lastDataJsonRef.current = "";
|
|
712
|
+
setLoading(true);
|
|
713
|
+
setError(void 0);
|
|
714
|
+
}, [modelName, key, JSON.stringify(query), projectionJson, sortJson, limitStr]);
|
|
715
|
+
useEffect(() => {
|
|
716
|
+
if (!modelName) return;
|
|
717
|
+
const unsubscribe = registerQuery(
|
|
718
|
+
modelName,
|
|
719
|
+
query,
|
|
720
|
+
{
|
|
721
|
+
key,
|
|
722
|
+
projection: options.projection,
|
|
723
|
+
sort: options.sort,
|
|
724
|
+
limit: options.limit
|
|
725
|
+
},
|
|
726
|
+
(err, result, context) => {
|
|
727
|
+
if (context.source === "cache" && hasNetworkReply.current) return;
|
|
728
|
+
if (context.source === "network") {
|
|
729
|
+
hasNetworkReply.current = true;
|
|
730
|
+
}
|
|
731
|
+
setLoading(false);
|
|
732
|
+
if (err) {
|
|
733
|
+
setError(err);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (!Array.isArray(result)) return;
|
|
737
|
+
if (context.source === "network" && context.isLocal && options.skipLocal && hasFirstReply.current) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
hasFirstReply.current = true;
|
|
741
|
+
let nextJson = "";
|
|
742
|
+
try {
|
|
743
|
+
nextJson = JSON.stringify(result);
|
|
744
|
+
} catch {
|
|
745
|
+
nextJson = "";
|
|
746
|
+
}
|
|
747
|
+
if (nextJson && nextJson === lastDataJsonRef.current) {
|
|
748
|
+
setSource(context.source);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
lastDataJsonRef.current = nextJson;
|
|
752
|
+
setSource(context.source);
|
|
753
|
+
setData(result);
|
|
754
|
+
}
|
|
755
|
+
);
|
|
756
|
+
return () => {
|
|
757
|
+
unsubscribe?.();
|
|
758
|
+
};
|
|
759
|
+
}, [modelName, key, JSON.stringify(query), projectionJson, sortJson, limitStr]);
|
|
760
|
+
return useMemo(
|
|
761
|
+
() => ({
|
|
762
|
+
data,
|
|
763
|
+
source,
|
|
764
|
+
error,
|
|
765
|
+
loading
|
|
766
|
+
}),
|
|
767
|
+
[data, source, error, loading]
|
|
768
|
+
);
|
|
769
|
+
};
|
|
770
|
+
export {
|
|
771
|
+
destroyCollection as a,
|
|
772
|
+
deleteDocs as b,
|
|
773
|
+
configureRtsPouchStore as c,
|
|
774
|
+
destroyAllCollections as d,
|
|
775
|
+
runQuery as e,
|
|
776
|
+
addLocalTxn as f,
|
|
777
|
+
getCollection as g,
|
|
778
|
+
connect as h,
|
|
779
|
+
disconnect as i,
|
|
780
|
+
reconnect as j,
|
|
781
|
+
registerQuery as k,
|
|
782
|
+
syncRtsChanges as l,
|
|
783
|
+
useQuery as m,
|
|
784
|
+
onMessage as o,
|
|
785
|
+
resetRtsPouchStore as r,
|
|
786
|
+
sendMessage as s,
|
|
787
|
+
updateDocs as u
|
|
788
|
+
};
|