@basictech/react 0.9.0-beta.0 → 0.9.0-beta.1
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/changelog.md +34 -0
- package/dist/index.d.mts +269 -27
- package/dist/index.d.ts +269 -27
- package/dist/index.js +790 -129
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +786 -128
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/readme.md +66 -12
package/dist/index.mjs
CHANGED
|
@@ -38,7 +38,7 @@ var init_config = __esm({
|
|
|
38
38
|
var version;
|
|
39
39
|
var init_package = __esm({
|
|
40
40
|
"package.json"() {
|
|
41
|
-
version = "0.9.0-beta.
|
|
41
|
+
version = "0.9.0-beta.1";
|
|
42
42
|
}
|
|
43
43
|
});
|
|
44
44
|
|
|
@@ -138,6 +138,22 @@ var init_network = __esm({
|
|
|
138
138
|
// src/react/hooks.ts
|
|
139
139
|
import { useContext, useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
140
140
|
import { useLiveQuery } from "dexie-react-hooks";
|
|
141
|
+
function useQuery(querier, deps = []) {
|
|
142
|
+
const client = useContext(BasicClientContext);
|
|
143
|
+
const activeUserId = useSyncExternalStore(
|
|
144
|
+
client ? client.subscribe : noopSubscribe,
|
|
145
|
+
() => client?.getSnapshot().activeUser?.id ?? null,
|
|
146
|
+
() => null
|
|
147
|
+
);
|
|
148
|
+
return useLiveQuery(async () => {
|
|
149
|
+
try {
|
|
150
|
+
return await querier();
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if (err instanceof Error && err.name === "DatabaseClosedError") return void 0;
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
}, [...deps, activeUserId]);
|
|
156
|
+
}
|
|
141
157
|
function useBasicClient() {
|
|
142
158
|
const client = useContext(BasicClientContext);
|
|
143
159
|
if (!client) {
|
|
@@ -155,6 +171,7 @@ function useAuth() {
|
|
|
155
171
|
() => ({
|
|
156
172
|
isReady: snapshot.isReady,
|
|
157
173
|
isSignedIn: snapshot.isSignedIn,
|
|
174
|
+
isAnonymous: snapshot.isAnonymous,
|
|
158
175
|
status: snapshot.authStatus,
|
|
159
176
|
errorCode: snapshot.authErrorCode,
|
|
160
177
|
user: snapshot.user,
|
|
@@ -211,9 +228,11 @@ function useShares() {
|
|
|
211
228
|
},
|
|
212
229
|
[client]
|
|
213
230
|
);
|
|
231
|
+
const activeUserId = snapshot.activeUser?.id ?? null;
|
|
214
232
|
useEffect(() => {
|
|
215
233
|
if (isSignedIn) void refresh();
|
|
216
|
-
|
|
234
|
+
else setState({ granted: [], received: [], isLoading: false, error: null });
|
|
235
|
+
}, [isSignedIn, refresh, activeUserId]);
|
|
217
236
|
return { ...state, refresh };
|
|
218
237
|
}
|
|
219
238
|
function useShare(shareId) {
|
|
@@ -223,6 +242,7 @@ function useShare(shareId) {
|
|
|
223
242
|
const [error, setError] = useState(null);
|
|
224
243
|
const [revoked, setRevoked] = useState(false);
|
|
225
244
|
const canMount = !!shareId && snapshot.isSignedIn && snapshot.authStatus !== "reauth_required";
|
|
245
|
+
const activeUserId = snapshot.activeUser?.id ?? null;
|
|
226
246
|
useEffect(() => {
|
|
227
247
|
if (!canMount || !shareId) return;
|
|
228
248
|
let cancelled = false;
|
|
@@ -246,13 +266,28 @@ function useShare(shareId) {
|
|
|
246
266
|
void client.unmountShare(shareId).catch(() => {
|
|
247
267
|
});
|
|
248
268
|
};
|
|
249
|
-
}, [client, shareId, canMount]);
|
|
269
|
+
}, [client, shareId, canMount, activeUserId]);
|
|
250
270
|
return {
|
|
251
271
|
db: handle?.db ?? null,
|
|
252
272
|
status: revoked ? "revoked" : error ? "error" : handle ? "mounted" : "mounting",
|
|
253
273
|
error
|
|
254
274
|
};
|
|
255
275
|
}
|
|
276
|
+
function useUsers() {
|
|
277
|
+
const client = useBasicClient();
|
|
278
|
+
const snapshot = useClientSnapshot(client);
|
|
279
|
+
return useMemo(
|
|
280
|
+
() => ({
|
|
281
|
+
users: snapshot.users,
|
|
282
|
+
activeUser: snapshot.activeUser,
|
|
283
|
+
isAnonymous: snapshot.isAnonymous,
|
|
284
|
+
switchUser: (id) => client.switchUser(id),
|
|
285
|
+
addUser: () => client.addUser(),
|
|
286
|
+
removeUser: (id) => client.removeUser(id)
|
|
287
|
+
}),
|
|
288
|
+
[client, snapshot]
|
|
289
|
+
);
|
|
290
|
+
}
|
|
256
291
|
function useBasic() {
|
|
257
292
|
const client = useBasicClient();
|
|
258
293
|
const snapshot = useClientSnapshot(client);
|
|
@@ -263,6 +298,8 @@ function useBasic() {
|
|
|
263
298
|
...auth,
|
|
264
299
|
db: client.db,
|
|
265
300
|
sync,
|
|
301
|
+
users: snapshot.users,
|
|
302
|
+
activeUser: snapshot.activeUser,
|
|
266
303
|
devInfo: snapshot.devInfo,
|
|
267
304
|
refreshSchemaStatus: () => client.refreshSchemaStatus(),
|
|
268
305
|
client
|
|
@@ -270,12 +307,13 @@ function useBasic() {
|
|
|
270
307
|
[client, snapshot, auth, sync]
|
|
271
308
|
);
|
|
272
309
|
}
|
|
273
|
-
var
|
|
310
|
+
var noopSubscribe;
|
|
274
311
|
var init_hooks = __esm({
|
|
275
312
|
"src/react/hooks.ts"() {
|
|
276
313
|
"use strict";
|
|
277
314
|
init_context();
|
|
278
|
-
|
|
315
|
+
noopSubscribe = () => () => {
|
|
316
|
+
};
|
|
279
317
|
}
|
|
280
318
|
});
|
|
281
319
|
|
|
@@ -295,7 +333,8 @@ function toneForSync(mode, status) {
|
|
|
295
333
|
if (mode === "rest") return "muted";
|
|
296
334
|
if (status === "online") return "ok";
|
|
297
335
|
if (status === "connecting") return "warn";
|
|
298
|
-
if (status === "offline" || status === "idle" || status === "stopped"
|
|
336
|
+
if (status === "offline" || status === "idle" || status === "stopped" || status === "local")
|
|
337
|
+
return "muted";
|
|
299
338
|
return "bad";
|
|
300
339
|
}
|
|
301
340
|
function toneForSchema(info) {
|
|
@@ -309,6 +348,8 @@ function syncStatusLabel(status) {
|
|
|
309
348
|
switch (status) {
|
|
310
349
|
case "idle":
|
|
311
350
|
return "Idle";
|
|
351
|
+
case "local":
|
|
352
|
+
return "Local only";
|
|
312
353
|
case "connecting":
|
|
313
354
|
return "Connecting";
|
|
314
355
|
case "online":
|
|
@@ -485,18 +526,22 @@ function BasicDevToolbar({ enabled = true, debug }) {
|
|
|
485
526
|
const {
|
|
486
527
|
isReady,
|
|
487
528
|
isSignedIn,
|
|
529
|
+
isAnonymous,
|
|
488
530
|
user,
|
|
489
531
|
did,
|
|
490
532
|
scope,
|
|
491
533
|
missingScopes,
|
|
492
534
|
sync,
|
|
535
|
+
users,
|
|
536
|
+
activeUser,
|
|
493
537
|
devInfo,
|
|
494
538
|
refreshSchemaStatus,
|
|
495
539
|
client
|
|
496
540
|
} = useBasic();
|
|
497
541
|
const dbMode = client.mode;
|
|
498
542
|
const syncStatus = sync.status;
|
|
499
|
-
const indexedDbName = dbMode === "sync" && client.projectId ? `basic-sync:${client.projectId}` : null;
|
|
543
|
+
const indexedDbName = dbMode === "sync" && client.projectId ? `basic-sync:${client.projectId}${activeUser?.keyspace ? `:${activeUser.keyspace}` : ""}` : null;
|
|
544
|
+
const activeUserLabel = activeUser ? `${isAnonymous ? "anonymous" : activeUser.email || activeUser.name || activeUser.did || "account"} (${activeUser.id.slice(0, 8)}\u2026)${users.length > 1 ? ` \xB7 ${users.length} users` : ""}` : "\u2014";
|
|
500
545
|
const [open, setOpen] = useState2(false);
|
|
501
546
|
const [refreshing, setRefreshing] = useState2(false);
|
|
502
547
|
const [copied, setCopied] = useState2(false);
|
|
@@ -533,9 +578,11 @@ function BasicDevToolbar({ enabled = true, debug }) {
|
|
|
533
578
|
syncStatus,
|
|
534
579
|
pendingOps: sync.pendingCount,
|
|
535
580
|
indexedDbName,
|
|
581
|
+
activeUser: activeUser ? { id: activeUser.id, kind: activeUser.kind, did: activeUser.did } : null,
|
|
582
|
+
userCount: users.length,
|
|
536
583
|
schema: devInfo
|
|
537
584
|
};
|
|
538
|
-
}, [isReady, isSignedIn, did, user, scope, dbMode, syncStatus, sync.pendingCount, indexedDbName, devInfo, missingList]);
|
|
585
|
+
}, [isReady, isSignedIn, did, user, scope, dbMode, syncStatus, sync.pendingCount, indexedDbName, activeUser, users.length, devInfo, missingList]);
|
|
539
586
|
const handleCopy = useCallback(async () => {
|
|
540
587
|
try {
|
|
541
588
|
await navigator.clipboard.writeText(JSON.stringify(debugPayload, null, 2));
|
|
@@ -673,6 +720,17 @@ function BasicDevToolbar({ enabled = true, debug }) {
|
|
|
673
720
|
children: user ? displayUserLine(user) : "\u2014"
|
|
674
721
|
}
|
|
675
722
|
),
|
|
723
|
+
/* @__PURE__ */ jsx(
|
|
724
|
+
CopyableRow,
|
|
725
|
+
{
|
|
726
|
+
rowKey: "activeProfile",
|
|
727
|
+
label: "Profile",
|
|
728
|
+
copyText: activeUser?.id ?? "",
|
|
729
|
+
copiedKey: rowCopied,
|
|
730
|
+
onCopied: onRowCopied,
|
|
731
|
+
children: activeUserLabel
|
|
732
|
+
}
|
|
733
|
+
),
|
|
676
734
|
/* @__PURE__ */ jsx(
|
|
677
735
|
CopyableRow,
|
|
678
736
|
{
|
|
@@ -1091,11 +1149,15 @@ var AuthManager = class {
|
|
|
1091
1149
|
this.requestedScopes = config.scopes;
|
|
1092
1150
|
this.initCrossTabSync();
|
|
1093
1151
|
}
|
|
1152
|
+
get instanceKey() {
|
|
1153
|
+
return this.config.instanceKey ?? "";
|
|
1154
|
+
}
|
|
1094
1155
|
initCrossTabSync() {
|
|
1095
1156
|
if (typeof BroadcastChannel === "undefined") return;
|
|
1096
1157
|
try {
|
|
1097
1158
|
this.channel = new BroadcastChannel("basic-auth");
|
|
1098
1159
|
this.channel.onmessage = (event) => {
|
|
1160
|
+
if ((event.data?.userKey ?? "") !== this.instanceKey) return;
|
|
1099
1161
|
if (event.data?.type === "token_refreshed") {
|
|
1100
1162
|
log("Received token refresh from another tab");
|
|
1101
1163
|
void this.handleExternalTokenRefresh(event.data);
|
|
@@ -1123,19 +1185,32 @@ var AuthManager = class {
|
|
|
1123
1185
|
broadcastTokenRefresh() {
|
|
1124
1186
|
this.channel?.postMessage({
|
|
1125
1187
|
type: "token_refreshed",
|
|
1188
|
+
userKey: this.instanceKey,
|
|
1126
1189
|
accessToken: this.token?.access_token,
|
|
1127
1190
|
did: this.did,
|
|
1128
1191
|
tokenScope: this.tokenScope
|
|
1129
1192
|
});
|
|
1130
1193
|
}
|
|
1131
1194
|
broadcastSignIn() {
|
|
1132
|
-
this.channel?.postMessage({ type: "signed_in" });
|
|
1195
|
+
this.channel?.postMessage({ type: "signed_in", userKey: this.instanceKey });
|
|
1133
1196
|
}
|
|
1134
1197
|
broadcastSignOut() {
|
|
1135
|
-
this.channel?.postMessage({ type: "signed_out" });
|
|
1198
|
+
this.channel?.postMessage({ type: "signed_out", userKey: this.instanceKey });
|
|
1136
1199
|
}
|
|
1137
1200
|
broadcastSessionInvalidated(code) {
|
|
1138
|
-
this.channel?.postMessage({
|
|
1201
|
+
this.channel?.postMessage({
|
|
1202
|
+
type: "session_invalidated",
|
|
1203
|
+
userKey: this.instanceKey,
|
|
1204
|
+
code
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
/** Release resources (cross-tab channel). Used when switching users. */
|
|
1208
|
+
destroy() {
|
|
1209
|
+
try {
|
|
1210
|
+
this.channel?.close();
|
|
1211
|
+
} catch {
|
|
1212
|
+
}
|
|
1213
|
+
this.channel = null;
|
|
1139
1214
|
}
|
|
1140
1215
|
// ------------------------------------------------------------------
|
|
1141
1216
|
// Public API
|
|
@@ -2563,6 +2638,7 @@ var SyncConnection = class {
|
|
|
2563
2638
|
import Dexie from "dexie";
|
|
2564
2639
|
var META_CURSOR = "cursor";
|
|
2565
2640
|
var META_CHANNEL = "channel";
|
|
2641
|
+
var META_OWNER = "owner_did";
|
|
2566
2642
|
var SyncStore = class {
|
|
2567
2643
|
db;
|
|
2568
2644
|
name;
|
|
@@ -2619,6 +2695,32 @@ var SyncStore = class {
|
|
|
2619
2695
|
const row = await this.meta.get(META_CHANNEL);
|
|
2620
2696
|
return typeof row?.value === "string" ? row.value : null;
|
|
2621
2697
|
}
|
|
2698
|
+
/**
|
|
2699
|
+
* The account DID this keyspace's confirmed data belongs to. Absent for
|
|
2700
|
+
* anonymous-era data (which may be merged into whichever account signs in).
|
|
2701
|
+
*/
|
|
2702
|
+
async getOwner() {
|
|
2703
|
+
const row = await this.meta.get(META_OWNER);
|
|
2704
|
+
return typeof row?.value === "string" ? row.value : null;
|
|
2705
|
+
}
|
|
2706
|
+
async setOwner(did) {
|
|
2707
|
+
await this.meta.put({ key: META_OWNER, value: did });
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Clear everything (views, server state, pending, rejected, meta) without
|
|
2711
|
+
* deleting the database — used when the keyspace changes owners.
|
|
2712
|
+
*/
|
|
2713
|
+
async wipeAll() {
|
|
2714
|
+
await this.db.transaction("rw", this.allStores, async () => {
|
|
2715
|
+
await this.server.clear();
|
|
2716
|
+
await this.pending.clear();
|
|
2717
|
+
await this.rejected.clear();
|
|
2718
|
+
await this.meta.clear();
|
|
2719
|
+
for (const tableName of this.tableNames) {
|
|
2720
|
+
await this.view(tableName).clear();
|
|
2721
|
+
}
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2622
2724
|
// -------------------------------------------------------------------
|
|
2623
2725
|
// Pending / rejected
|
|
2624
2726
|
// -------------------------------------------------------------------
|
|
@@ -2831,7 +2933,11 @@ var SyncEngine = class {
|
|
|
2831
2933
|
subs = /* @__PURE__ */ new Map();
|
|
2832
2934
|
limits = { ...DEFAULT_LIMITS };
|
|
2833
2935
|
actor = null;
|
|
2834
|
-
|
|
2936
|
+
/** Own-sub store is open (local reads/writes work). */
|
|
2937
|
+
storesOpen = false;
|
|
2938
|
+
/** A live connection is wanted (vs. local-only / paused). */
|
|
2939
|
+
connectIntended = false;
|
|
2940
|
+
openingLocal = null;
|
|
2835
2941
|
revokedInfo = null;
|
|
2836
2942
|
connectionStatus = "idle";
|
|
2837
2943
|
_status = "idle";
|
|
@@ -2906,26 +3012,76 @@ var SyncEngine = class {
|
|
|
2906
3012
|
// -------------------------------------------------------------------
|
|
2907
3013
|
// Lifecycle
|
|
2908
3014
|
// -------------------------------------------------------------------
|
|
2909
|
-
/**
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
3015
|
+
/**
|
|
3016
|
+
* Open the own-channel keyspace for local reads/writes — no connection,
|
|
3017
|
+
* no token needed. This is the anonymous / offline-cold-start entry point.
|
|
3018
|
+
* Idempotent.
|
|
3019
|
+
*/
|
|
3020
|
+
async openLocal() {
|
|
3021
|
+
if (this.subs.has(OWN_SUB)) {
|
|
3022
|
+
this.storesOpen = true;
|
|
3023
|
+
this.recomputeStatus();
|
|
2915
3024
|
return;
|
|
2916
3025
|
}
|
|
2917
|
-
this.
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
3026
|
+
if (!this.openingLocal) {
|
|
3027
|
+
this.openingLocal = (async () => {
|
|
3028
|
+
const sub = await this.openSub(OWN_SUB, null);
|
|
3029
|
+
this.subs.set(OWN_SUB, sub);
|
|
3030
|
+
this.storesOpen = true;
|
|
3031
|
+
})().finally(() => {
|
|
3032
|
+
this.openingLocal = null;
|
|
3033
|
+
});
|
|
2922
3034
|
}
|
|
3035
|
+
await this.openingLocal;
|
|
3036
|
+
this.recomputeStatus();
|
|
3037
|
+
}
|
|
3038
|
+
/**
|
|
3039
|
+
* Open the keyspace (if needed) and start syncing. Idempotent.
|
|
3040
|
+
* Note: a `CONNECTION_REVOKED` latch is NOT cleared here — reconnecting a
|
|
3041
|
+
* revoked app connection requires a fresh consent flow. Call
|
|
3042
|
+
* {@link clearRevoked} (or rebind the engine) after re-authorization.
|
|
3043
|
+
*/
|
|
3044
|
+
async connect() {
|
|
3045
|
+
await this.openLocal();
|
|
3046
|
+
if (this.revokedInfo) {
|
|
3047
|
+
this.log("connect() ignored: app connection is revoked");
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
3050
|
+
this.connectIntended = true;
|
|
2923
3051
|
this.connection.start();
|
|
2924
3052
|
this.recomputeStatus();
|
|
2925
3053
|
}
|
|
3054
|
+
/** Clear the revocation latch (after the user re-authorized the app). */
|
|
3055
|
+
clearRevoked() {
|
|
3056
|
+
this.revokedInfo = null;
|
|
3057
|
+
this.recomputeStatus();
|
|
3058
|
+
}
|
|
3059
|
+
/** @deprecated alias of {@link connect} */
|
|
3060
|
+
async start() {
|
|
3061
|
+
return this.connect();
|
|
3062
|
+
}
|
|
3063
|
+
/**
|
|
3064
|
+
* Disconnect but keep stores open: local reads/writes keep working and
|
|
3065
|
+
* ops queue for the next connect. Used on reauth_required.
|
|
3066
|
+
*/
|
|
3067
|
+
pause() {
|
|
3068
|
+
this.connectIntended = false;
|
|
3069
|
+
this.connection.stop();
|
|
3070
|
+
for (const t of this.timers) clearTimeout(t);
|
|
3071
|
+
this.timers.clear();
|
|
3072
|
+
for (const sub of this.subs.values()) {
|
|
3073
|
+
sub.active = false;
|
|
3074
|
+
for (const p of sub.pending) {
|
|
3075
|
+
p.sent = false;
|
|
3076
|
+
p.ackedSeq = void 0;
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
this.recomputeStatus();
|
|
3080
|
+
}
|
|
2926
3081
|
/** Close the socket and stores; local data is kept. */
|
|
2927
3082
|
stop() {
|
|
2928
|
-
this.
|
|
3083
|
+
this.connectIntended = false;
|
|
3084
|
+
this.storesOpen = false;
|
|
2929
3085
|
this.connection.stop();
|
|
2930
3086
|
for (const t of this.timers) clearTimeout(t);
|
|
2931
3087
|
this.timers.clear();
|
|
@@ -2953,10 +3109,10 @@ var SyncEngine = class {
|
|
|
2953
3109
|
try {
|
|
2954
3110
|
const idb = globalThis.indexedDB;
|
|
2955
3111
|
if (idb && typeof idb.databases === "function") {
|
|
2956
|
-
const
|
|
3112
|
+
const base = this.baseDbName;
|
|
2957
3113
|
const dbs = await idb.databases();
|
|
2958
3114
|
for (const info of dbs) {
|
|
2959
|
-
if (info.name && info.name.startsWith(
|
|
3115
|
+
if (info.name && (info.name === base || info.name.startsWith(`${base}:share:`))) {
|
|
2960
3116
|
await new Promise((resolve) => {
|
|
2961
3117
|
const req = idb.deleteDatabase(info.name);
|
|
2962
3118
|
req.onsuccess = req.onerror = req.onblocked = () => resolve();
|
|
@@ -3054,7 +3210,7 @@ var SyncEngine = class {
|
|
|
3054
3210
|
// -------------------------------------------------------------------
|
|
3055
3211
|
handleConnectionStatus(status) {
|
|
3056
3212
|
this.connectionStatus = status;
|
|
3057
|
-
if (status === "offline" || status === "connecting" || status === "auth_failed") {
|
|
3213
|
+
if (status === "offline" || status === "connecting" || status === "auth_failed" || status === "stopped") {
|
|
3058
3214
|
for (const sub of this.subs.values()) {
|
|
3059
3215
|
sub.active = false;
|
|
3060
3216
|
for (const p of sub.pending) {
|
|
@@ -3142,13 +3298,13 @@ var SyncEngine = class {
|
|
|
3142
3298
|
switch (msg.code) {
|
|
3143
3299
|
case "CONNECTION_REVOKED":
|
|
3144
3300
|
this.revokedInfo = { code: msg.code, message: msg.message };
|
|
3145
|
-
this.
|
|
3301
|
+
this.connectIntended = false;
|
|
3146
3302
|
this.connection.stop();
|
|
3147
3303
|
this.recomputeStatus();
|
|
3148
3304
|
this.emit("revoked", { code: msg.code, message: msg.message });
|
|
3149
3305
|
return;
|
|
3150
3306
|
case "UNSUPPORTED_VERSION":
|
|
3151
|
-
this.
|
|
3307
|
+
this.connectIntended = false;
|
|
3152
3308
|
this.connection.stop();
|
|
3153
3309
|
this.recomputeStatus();
|
|
3154
3310
|
return;
|
|
@@ -3183,7 +3339,7 @@ var SyncEngine = class {
|
|
|
3183
3339
|
// Subscription state machine (serialized per sub via `chain`)
|
|
3184
3340
|
// -------------------------------------------------------------------
|
|
3185
3341
|
async openSub(key, shareId) {
|
|
3186
|
-
const name = shareId ? `${this.
|
|
3342
|
+
const name = shareId ? `${this.baseDbName}:share:${shareId}` : this.baseDbName;
|
|
3187
3343
|
const store = new SyncStore(name, this.schema);
|
|
3188
3344
|
const [cursor, pendingRows] = await Promise.all([store.getCursor(), store.loadPending()]);
|
|
3189
3345
|
return {
|
|
@@ -3204,6 +3360,16 @@ var SyncEngine = class {
|
|
|
3204
3360
|
/** Bootstrap if needed, then bind the stream on the current socket. */
|
|
3205
3361
|
async activateSub(sub) {
|
|
3206
3362
|
if (!this.connection.isOnline) return;
|
|
3363
|
+
if (sub.bootstrapped && !sub.shareId && this.opts.getOwnerDid) {
|
|
3364
|
+
try {
|
|
3365
|
+
const did = await this.opts.getOwnerDid() ?? null;
|
|
3366
|
+
if (did) {
|
|
3367
|
+
const stamped = await sub.store.getOwner();
|
|
3368
|
+
if (stamped && stamped !== did) sub.bootstrapped = false;
|
|
3369
|
+
}
|
|
3370
|
+
} catch {
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3207
3373
|
if (!sub.bootstrapped || sub.cursor < 0) {
|
|
3208
3374
|
try {
|
|
3209
3375
|
await this.bootstrapSub(sub);
|
|
@@ -3222,6 +3388,26 @@ var SyncEngine = class {
|
|
|
3222
3388
|
}
|
|
3223
3389
|
/** Cold start = snapshot + tail; never log replay (§5). Pending survives. */
|
|
3224
3390
|
async bootstrapSub(sub) {
|
|
3391
|
+
let ownerDid = null;
|
|
3392
|
+
if (!sub.shareId && this.opts.getOwnerDid) {
|
|
3393
|
+
try {
|
|
3394
|
+
ownerDid = await this.opts.getOwnerDid() ?? null;
|
|
3395
|
+
} catch {
|
|
3396
|
+
ownerDid = null;
|
|
3397
|
+
}
|
|
3398
|
+
if (ownerDid) {
|
|
3399
|
+
const stamped = await sub.store.getOwner();
|
|
3400
|
+
if (stamped && stamped !== ownerDid) {
|
|
3401
|
+
this.log(
|
|
3402
|
+
`keyspace owned by ${stamped} but session is ${ownerDid} \u2014 wiping local data before bootstrap`
|
|
3403
|
+
);
|
|
3404
|
+
await sub.store.wipeAll();
|
|
3405
|
+
sub.pending = [];
|
|
3406
|
+
sub.appliedOpIds.clear();
|
|
3407
|
+
sub.cursor = -1;
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3225
3411
|
const snapshot = await this.opts.fetchSnapshot(
|
|
3226
3412
|
sub.shareId ? { share: sub.shareId } : void 0
|
|
3227
3413
|
);
|
|
@@ -3230,6 +3416,9 @@ var SyncEngine = class {
|
|
|
3230
3416
|
records: snapshot.records ?? {},
|
|
3231
3417
|
cursor: snapshot.cursor ?? 0
|
|
3232
3418
|
});
|
|
3419
|
+
if (!sub.shareId && ownerDid) {
|
|
3420
|
+
await sub.store.setOwner(ownerDid);
|
|
3421
|
+
}
|
|
3233
3422
|
sub.cursor = snapshot.cursor ?? 0;
|
|
3234
3423
|
sub.bootstrapped = true;
|
|
3235
3424
|
sub.appliedOpIds.clear();
|
|
@@ -3353,6 +3542,11 @@ var SyncEngine = class {
|
|
|
3353
3542
|
get dbPrefix() {
|
|
3354
3543
|
return this.opts.dbNamePrefix ?? "basic-sync";
|
|
3355
3544
|
}
|
|
3545
|
+
/** Base database name for this keyspace (multi-user: includes the user id). */
|
|
3546
|
+
get baseDbName() {
|
|
3547
|
+
const base = `${this.dbPrefix}:${this.projectId}`;
|
|
3548
|
+
return this.opts.keyspaceId ? `${base}:${this.opts.keyspaceId}` : base;
|
|
3549
|
+
}
|
|
3356
3550
|
enqueue(sub, task) {
|
|
3357
3551
|
sub.chain = sub.chain.then(task).catch((err) => {
|
|
3358
3552
|
this.log(`task failed on '${sub.key}':`, err);
|
|
@@ -3370,10 +3564,12 @@ var SyncEngine = class {
|
|
|
3370
3564
|
let status;
|
|
3371
3565
|
if (this.revokedInfo) status = "revoked";
|
|
3372
3566
|
else if (this.connectionStatus === "auth_failed") status = "auth_required";
|
|
3373
|
-
else if (!this.
|
|
3567
|
+
else if (!this.storesOpen) status = this.connectionStatus === "stopped" ? "stopped" : "idle";
|
|
3568
|
+
else if (!this.connectIntended) status = "local";
|
|
3374
3569
|
else if (this.connectionStatus === "online") status = "online";
|
|
3375
3570
|
else if (this.connectionStatus === "connecting") status = "connecting";
|
|
3376
3571
|
else if (this.connectionStatus === "idle") status = "connecting";
|
|
3572
|
+
else if (this.connectionStatus === "stopped") status = "local";
|
|
3377
3573
|
else status = "offline";
|
|
3378
3574
|
if (status !== this._status) {
|
|
3379
3575
|
this._status = status;
|
|
@@ -3399,7 +3595,7 @@ var SyncTable = class {
|
|
|
3399
3595
|
const sub = this.engine.getSubscription(this.subKey);
|
|
3400
3596
|
if (!sub) {
|
|
3401
3597
|
throw new Error(
|
|
3402
|
-
`subscription '${this.subKey}' is not open \u2014
|
|
3598
|
+
`subscription '${this.subKey}' is not open \u2014 wait for the client to be ready (isReady) before using the db`
|
|
3403
3599
|
);
|
|
3404
3600
|
}
|
|
3405
3601
|
return sub.store;
|
|
@@ -3529,6 +3725,171 @@ var RestDb = class {
|
|
|
3529
3725
|
}
|
|
3530
3726
|
};
|
|
3531
3727
|
|
|
3728
|
+
// src/core/users.ts
|
|
3729
|
+
init_config();
|
|
3730
|
+
var PrefixedStorage = class {
|
|
3731
|
+
constructor(inner, prefix) {
|
|
3732
|
+
this.inner = inner;
|
|
3733
|
+
this.prefix = prefix;
|
|
3734
|
+
}
|
|
3735
|
+
get(key) {
|
|
3736
|
+
return this.inner.get(this.prefix + key);
|
|
3737
|
+
}
|
|
3738
|
+
set(key, value) {
|
|
3739
|
+
return this.inner.set(this.prefix + key, value);
|
|
3740
|
+
}
|
|
3741
|
+
remove(key) {
|
|
3742
|
+
return this.inner.remove(this.prefix + key);
|
|
3743
|
+
}
|
|
3744
|
+
};
|
|
3745
|
+
function registryKey(projectId) {
|
|
3746
|
+
return `basic_users:${projectId}`;
|
|
3747
|
+
}
|
|
3748
|
+
function activeUserSessionKey(projectId) {
|
|
3749
|
+
return `basic_active_user:${projectId}`;
|
|
3750
|
+
}
|
|
3751
|
+
var UserRegistry = class {
|
|
3752
|
+
constructor(storage, projectId) {
|
|
3753
|
+
this.storage = storage;
|
|
3754
|
+
this.projectId = projectId;
|
|
3755
|
+
}
|
|
3756
|
+
// -------------------------------------------------------------------
|
|
3757
|
+
// Registry CRUD
|
|
3758
|
+
// -------------------------------------------------------------------
|
|
3759
|
+
async list() {
|
|
3760
|
+
const raw = await this.storage.get(registryKey(this.projectId));
|
|
3761
|
+
if (!raw) return [];
|
|
3762
|
+
try {
|
|
3763
|
+
const parsed = JSON.parse(raw);
|
|
3764
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
3765
|
+
} catch {
|
|
3766
|
+
return [];
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
async save(users) {
|
|
3770
|
+
await this.storage.set(registryKey(this.projectId), JSON.stringify(users));
|
|
3771
|
+
}
|
|
3772
|
+
async get(id) {
|
|
3773
|
+
const users = await this.list();
|
|
3774
|
+
return users.find((u) => u.id === id) ?? null;
|
|
3775
|
+
}
|
|
3776
|
+
async createAnon() {
|
|
3777
|
+
const id = mintOpId();
|
|
3778
|
+
const now = Date.now();
|
|
3779
|
+
const profile = {
|
|
3780
|
+
id,
|
|
3781
|
+
kind: "anon",
|
|
3782
|
+
keyspace: id,
|
|
3783
|
+
storagePrefix: `u:${id}:`,
|
|
3784
|
+
createdAt: now,
|
|
3785
|
+
lastActiveAt: now
|
|
3786
|
+
};
|
|
3787
|
+
const users = await this.list();
|
|
3788
|
+
users.push(profile);
|
|
3789
|
+
await this.save(users);
|
|
3790
|
+
log(`created anonymous user ${id}`);
|
|
3791
|
+
return profile;
|
|
3792
|
+
}
|
|
3793
|
+
async update(id, patch) {
|
|
3794
|
+
const users = await this.list();
|
|
3795
|
+
const idx = users.findIndex((u) => u.id === id);
|
|
3796
|
+
if (idx < 0) return null;
|
|
3797
|
+
users[idx] = { ...users[idx], ...patch };
|
|
3798
|
+
await this.save(users);
|
|
3799
|
+
return users[idx];
|
|
3800
|
+
}
|
|
3801
|
+
async remove(id) {
|
|
3802
|
+
const users = await this.list();
|
|
3803
|
+
await this.save(users.filter((u) => u.id !== id));
|
|
3804
|
+
if (this.getActiveIdRaw() === id) this.clearActiveId();
|
|
3805
|
+
}
|
|
3806
|
+
/** The profile (if any) already bound to an account DID. */
|
|
3807
|
+
async findByDid(did) {
|
|
3808
|
+
const users = await this.list();
|
|
3809
|
+
return users.find((u) => u.did === did) ?? null;
|
|
3810
|
+
}
|
|
3811
|
+
// -------------------------------------------------------------------
|
|
3812
|
+
// Active user (per-tab)
|
|
3813
|
+
// -------------------------------------------------------------------
|
|
3814
|
+
getActiveIdRaw() {
|
|
3815
|
+
try {
|
|
3816
|
+
return sessionStorage.getItem(activeUserSessionKey(this.projectId));
|
|
3817
|
+
} catch {
|
|
3818
|
+
return null;
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
setActiveId(id) {
|
|
3822
|
+
try {
|
|
3823
|
+
sessionStorage.setItem(activeUserSessionKey(this.projectId), id);
|
|
3824
|
+
} catch {
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
clearActiveId() {
|
|
3828
|
+
try {
|
|
3829
|
+
sessionStorage.removeItem(activeUserSessionKey(this.projectId));
|
|
3830
|
+
} catch {
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
/**
|
|
3834
|
+
* Resolve the active profile for this tab: sessionStorage choice if it
|
|
3835
|
+
* still exists, else the most recently active profile, else null.
|
|
3836
|
+
*/
|
|
3837
|
+
async resolveActive() {
|
|
3838
|
+
const users = await this.list();
|
|
3839
|
+
const activeId = this.getActiveIdRaw();
|
|
3840
|
+
if (activeId) {
|
|
3841
|
+
const match = users.find((u) => u.id === activeId);
|
|
3842
|
+
if (match) return match;
|
|
3843
|
+
}
|
|
3844
|
+
if (users.length === 0) return null;
|
|
3845
|
+
const recent = [...users].sort((a, b) => b.lastActiveAt - a.lastActiveAt)[0];
|
|
3846
|
+
this.setActiveId(recent.id);
|
|
3847
|
+
return recent;
|
|
3848
|
+
}
|
|
3849
|
+
async touch(id) {
|
|
3850
|
+
await this.update(id, { lastActiveAt: Date.now() });
|
|
3851
|
+
}
|
|
3852
|
+
// -------------------------------------------------------------------
|
|
3853
|
+
// Legacy adoption
|
|
3854
|
+
// -------------------------------------------------------------------
|
|
3855
|
+
/**
|
|
3856
|
+
* Adopt a pre-multi-user session as the first profile. Idempotent: runs
|
|
3857
|
+
* only when the registry is empty and a bare refresh token exists. The
|
|
3858
|
+
* adopted profile keeps the unprefixed storage keys and the legacy
|
|
3859
|
+
* keyspace name, so nothing needs to move.
|
|
3860
|
+
*/
|
|
3861
|
+
async adoptLegacySession() {
|
|
3862
|
+
const users = await this.list();
|
|
3863
|
+
if (users.length > 0) return null;
|
|
3864
|
+
const legacyRefresh = await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
|
|
3865
|
+
if (!legacyRefresh) return null;
|
|
3866
|
+
let cachedUser = null;
|
|
3867
|
+
try {
|
|
3868
|
+
const raw = await this.storage.get(STORAGE_KEYS.USER_INFO);
|
|
3869
|
+
if (raw) cachedUser = JSON.parse(raw);
|
|
3870
|
+
} catch {
|
|
3871
|
+
}
|
|
3872
|
+
const now = Date.now();
|
|
3873
|
+
const profile = {
|
|
3874
|
+
id: mintOpId(),
|
|
3875
|
+
kind: "account",
|
|
3876
|
+
did: cachedUser?.sub ?? null,
|
|
3877
|
+
email: cachedUser?.email ?? null,
|
|
3878
|
+
name: cachedUser?.name ?? null,
|
|
3879
|
+
picture: cachedUser?.picture ?? null,
|
|
3880
|
+
keyspace: "",
|
|
3881
|
+
// legacy `basic-sync:{projectId}` database
|
|
3882
|
+
storagePrefix: "",
|
|
3883
|
+
// legacy unprefixed auth keys
|
|
3884
|
+
createdAt: now,
|
|
3885
|
+
lastActiveAt: now
|
|
3886
|
+
};
|
|
3887
|
+
await this.save([profile]);
|
|
3888
|
+
log("adopted legacy single-user session as profile", profile.id);
|
|
3889
|
+
return profile;
|
|
3890
|
+
}
|
|
3891
|
+
};
|
|
3892
|
+
|
|
3532
3893
|
// src/utils/schema.ts
|
|
3533
3894
|
init_config();
|
|
3534
3895
|
import { validateSchema, compareSchemas } from "@basictech/schema";
|
|
@@ -3778,21 +4139,38 @@ var DEFAULTS = {
|
|
|
3778
4139
|
function deriveSyncUrl(pdsUrl) {
|
|
3779
4140
|
return pdsUrl.replace(/^http/, "ws").replace(/\/$/, "") + "/sync/";
|
|
3780
4141
|
}
|
|
4142
|
+
function ephemeralLegacyProfile() {
|
|
4143
|
+
const now = Date.now();
|
|
4144
|
+
return {
|
|
4145
|
+
id: "default",
|
|
4146
|
+
kind: "anon",
|
|
4147
|
+
keyspace: "",
|
|
4148
|
+
storagePrefix: "",
|
|
4149
|
+
createdAt: now,
|
|
4150
|
+
lastActiveAt: now
|
|
4151
|
+
};
|
|
4152
|
+
}
|
|
3781
4153
|
var BasicClient = class {
|
|
3782
|
-
auth;
|
|
3783
4154
|
rest;
|
|
3784
|
-
engine;
|
|
3785
4155
|
mode;
|
|
3786
4156
|
config;
|
|
3787
4157
|
projectId;
|
|
3788
|
-
|
|
4158
|
+
users;
|
|
4159
|
+
rawStorage;
|
|
3789
4160
|
restDb;
|
|
3790
4161
|
debug;
|
|
4162
|
+
anonymousEnabled;
|
|
4163
|
+
authConfig;
|
|
4164
|
+
syncUrl;
|
|
4165
|
+
binding;
|
|
4166
|
+
usersCache = [];
|
|
3791
4167
|
devInfo = null;
|
|
3792
4168
|
syncEnabled = false;
|
|
3793
4169
|
schemaChecked = false;
|
|
3794
4170
|
started = false;
|
|
3795
|
-
|
|
4171
|
+
signOutInProgress = false;
|
|
4172
|
+
/** Serializes profile transitions (switch, dispose, sign-out fallthrough). */
|
|
4173
|
+
profileOps = Promise.resolve();
|
|
3796
4174
|
mounts = /* @__PURE__ */ new Map();
|
|
3797
4175
|
listeners = /* @__PURE__ */ new Set();
|
|
3798
4176
|
snapshot;
|
|
@@ -3801,95 +4179,155 @@ var BasicClient = class {
|
|
|
3801
4179
|
this.debug = config.debug ?? false;
|
|
3802
4180
|
this.mode = config.mode ?? "sync";
|
|
3803
4181
|
this.projectId = config.schema?.project_id || config.project_id;
|
|
3804
|
-
|
|
4182
|
+
this.anonymousEnabled = this.mode === "sync" && (config.anonymous ?? true);
|
|
4183
|
+
this.authConfig = {
|
|
3805
4184
|
scopes: Array.isArray(config.auth?.scopes) ? config.auth.scopes.join(" ") : config.auth?.scopes || DEFAULTS.scopes,
|
|
3806
4185
|
pds_url: config.auth?.pds_url || DEFAULTS.pds_url,
|
|
3807
4186
|
admin_url: config.auth?.admin_url || DEFAULTS.admin_url
|
|
3808
4187
|
};
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
this.
|
|
3812
|
-
{
|
|
3813
|
-
projectId: this.projectId,
|
|
3814
|
-
scopes: authConfig.scopes,
|
|
3815
|
-
pdsUrl: authConfig.pds_url,
|
|
3816
|
-
adminUrl: authConfig.admin_url,
|
|
3817
|
-
debug: this.debug
|
|
3818
|
-
},
|
|
3819
|
-
storage,
|
|
3820
|
-
() => this.handleAuthChange()
|
|
3821
|
-
);
|
|
4188
|
+
this.syncUrl = config.auth?.sync_url || deriveSyncUrl(this.authConfig.pds_url);
|
|
4189
|
+
this.rawStorage = config.storage || new LocalStorageAdapter();
|
|
4190
|
+
this.users = this.mode === "sync" && this.projectId ? new UserRegistry(this.rawStorage, this.projectId) : null;
|
|
3822
4191
|
this.rest = new RestClient({
|
|
3823
|
-
baseUrl: authConfig.pds_url,
|
|
4192
|
+
baseUrl: this.authConfig.pds_url,
|
|
3824
4193
|
projectId: this.projectId ?? "",
|
|
3825
4194
|
getToken: (opts) => this.auth.getToken(opts),
|
|
3826
4195
|
log: this.debug ? log : void 0
|
|
3827
4196
|
});
|
|
3828
|
-
if (this.mode === "sync" && this.projectId && this.config.schema?.tables) {
|
|
3829
|
-
this.engine = new SyncEngine({
|
|
3830
|
-
projectId: this.projectId,
|
|
3831
|
-
schema: this.config.schema,
|
|
3832
|
-
wsUrl: syncUrl,
|
|
3833
|
-
getToken: (opts) => this.auth.getToken(opts),
|
|
3834
|
-
fetchSnapshot: (opts) => this.rest.getSnapshot(opts),
|
|
3835
|
-
WebSocketImpl: config.WebSocketImpl,
|
|
3836
|
-
log
|
|
3837
|
-
});
|
|
3838
|
-
this.syncDb = new SyncDb(this.engine, OWN_SUB);
|
|
3839
|
-
this.wireEngineEvents(this.engine);
|
|
3840
|
-
} else {
|
|
3841
|
-
this.engine = null;
|
|
3842
|
-
this.syncDb = null;
|
|
3843
|
-
}
|
|
3844
4197
|
this.restDb = new RestDb(this.rest, this.config.schema);
|
|
4198
|
+
this.binding = this.createBinding(ephemeralLegacyProfile());
|
|
3845
4199
|
this.snapshot = this.buildSnapshot();
|
|
3846
4200
|
}
|
|
3847
4201
|
// -------------------------------------------------------------------
|
|
3848
4202
|
// Public surface
|
|
3849
4203
|
// -------------------------------------------------------------------
|
|
3850
|
-
|
|
4204
|
+
get auth() {
|
|
4205
|
+
return this.binding.auth;
|
|
4206
|
+
}
|
|
4207
|
+
get engine() {
|
|
4208
|
+
return this.binding.engine;
|
|
4209
|
+
}
|
|
4210
|
+
/** The database handle for the active user. Identity changes on switch. */
|
|
3851
4211
|
get db() {
|
|
3852
|
-
if (this.mode === "sync" && this.syncDb) return this.syncDb;
|
|
4212
|
+
if (this.mode === "sync" && this.binding.syncDb) return this.binding.syncDb;
|
|
3853
4213
|
return this.restDb;
|
|
3854
4214
|
}
|
|
3855
|
-
|
|
4215
|
+
get activeUser() {
|
|
4216
|
+
return this.users ? this.binding.profile : null;
|
|
4217
|
+
}
|
|
4218
|
+
/** Bootstrap: version migrations, profile resolution, schema check, auth init. */
|
|
3856
4219
|
async start() {
|
|
3857
4220
|
if (this.started) return;
|
|
3858
4221
|
this.started = true;
|
|
3859
4222
|
try {
|
|
3860
|
-
const updater = createVersionUpdater(this.
|
|
4223
|
+
const updater = createVersionUpdater(this.rawStorage, version, getMigrations());
|
|
3861
4224
|
const result = await updater.checkAndUpdate();
|
|
3862
4225
|
if (result.updated) log(`SDK storage migrated ${result.fromVersion} \u2192 ${result.toVersion}`);
|
|
3863
4226
|
} catch (err) {
|
|
3864
4227
|
log("version updater failed:", err);
|
|
3865
4228
|
}
|
|
3866
|
-
void this.checkSchema().then(() => this.
|
|
3867
|
-
await this.
|
|
3868
|
-
|
|
3869
|
-
|
|
4229
|
+
void this.checkSchema().then(() => this.syncLifecycle());
|
|
4230
|
+
await this.queueProfileOp(async () => {
|
|
4231
|
+
let profile = null;
|
|
4232
|
+
if (this.users) {
|
|
4233
|
+
await this.users.adoptLegacySession();
|
|
4234
|
+
profile = await this.users.resolveActive();
|
|
4235
|
+
if (!profile && this.anonymousEnabled) {
|
|
4236
|
+
profile = await this.users.createAnon();
|
|
4237
|
+
}
|
|
4238
|
+
if (profile) this.users.setActiveId(profile.id);
|
|
4239
|
+
}
|
|
4240
|
+
await this.activateProfile(profile ?? ephemeralLegacyProfile(), { initial: true });
|
|
4241
|
+
await this.refreshUsers();
|
|
4242
|
+
});
|
|
3870
4243
|
}
|
|
3871
|
-
/**
|
|
4244
|
+
/**
|
|
4245
|
+
* Sign out the active user: server-side revoke, wipe the profile's local
|
|
4246
|
+
* data, drop the profile, and fall through to the next (or a fresh
|
|
4247
|
+
* anonymous) user.
|
|
4248
|
+
*/
|
|
3872
4249
|
async signOut() {
|
|
3873
|
-
await this.
|
|
3874
|
-
|
|
4250
|
+
await this.queueProfileOp(async () => {
|
|
4251
|
+
const { profile, auth, engine } = this.binding;
|
|
4252
|
+
this.signOutInProgress = true;
|
|
4253
|
+
try {
|
|
4254
|
+
await auth.signOut();
|
|
4255
|
+
} finally {
|
|
4256
|
+
this.signOutInProgress = false;
|
|
4257
|
+
}
|
|
4258
|
+
this.mounts.clear();
|
|
4259
|
+
try {
|
|
4260
|
+
await engine?.destroyLocal();
|
|
4261
|
+
} catch (err) {
|
|
4262
|
+
log("local data teardown failed:", err);
|
|
4263
|
+
}
|
|
4264
|
+
if (this.users && profile.id !== "default") {
|
|
4265
|
+
await this.users.remove(profile.id);
|
|
4266
|
+
}
|
|
4267
|
+
await this.activateNextProfileLocked();
|
|
4268
|
+
await this.refreshUsers();
|
|
4269
|
+
});
|
|
4270
|
+
}
|
|
4271
|
+
// ---------------- multi-user ----------------
|
|
4272
|
+
/** Switch this tab to another local user. */
|
|
4273
|
+
async switchUser(id) {
|
|
4274
|
+
await this.queueProfileOp(async () => {
|
|
4275
|
+
if (!this.users) throw new Error("multiple users require sync mode with a project id");
|
|
4276
|
+
if (this.binding.profile.id === id) return;
|
|
4277
|
+
const target = await this.users.get(id);
|
|
4278
|
+
if (!target) throw new Error(`unknown user '${id}'`);
|
|
4279
|
+
this.users.setActiveId(id);
|
|
4280
|
+
await this.users.touch(id);
|
|
4281
|
+
await this.activateProfile(target);
|
|
4282
|
+
await this.refreshUsers();
|
|
4283
|
+
});
|
|
4284
|
+
}
|
|
4285
|
+
/** Create a fresh anonymous user and switch to it. */
|
|
4286
|
+
async addUser() {
|
|
4287
|
+
let created = null;
|
|
4288
|
+
await this.queueProfileOp(async () => {
|
|
4289
|
+
if (!this.users) throw new Error("multiple users require sync mode with a project id");
|
|
4290
|
+
created = await this.users.createAnon();
|
|
4291
|
+
this.users.setActiveId(created.id);
|
|
4292
|
+
await this.activateProfile(created);
|
|
4293
|
+
await this.refreshUsers();
|
|
4294
|
+
});
|
|
4295
|
+
return created;
|
|
4296
|
+
}
|
|
4297
|
+
/**
|
|
4298
|
+
* Remove a local user: best-effort server-side revoke, wipe its keyspace
|
|
4299
|
+
* and auth storage, drop the profile. Removing the active user signs out.
|
|
4300
|
+
*/
|
|
4301
|
+
async removeUser(id) {
|
|
4302
|
+
if (this.binding.profile.id === id) {
|
|
4303
|
+
return this.signOut();
|
|
4304
|
+
}
|
|
4305
|
+
await this.queueProfileOp(async () => {
|
|
4306
|
+
if (!this.users) return;
|
|
4307
|
+
const profile = await this.users.get(id);
|
|
4308
|
+
if (!profile) return;
|
|
4309
|
+
await this.disposeProfileData(profile);
|
|
4310
|
+
await this.users.remove(id);
|
|
4311
|
+
await this.refreshUsers();
|
|
4312
|
+
});
|
|
3875
4313
|
}
|
|
3876
4314
|
/** Stop connections and listeners; local data is kept. */
|
|
3877
4315
|
stop() {
|
|
3878
4316
|
this.started = false;
|
|
3879
|
-
this.engine?.stop();
|
|
3880
|
-
for (const fn of this.
|
|
4317
|
+
this.binding.engine?.stop();
|
|
4318
|
+
for (const fn of this.binding.sessionCleanup) {
|
|
3881
4319
|
try {
|
|
3882
4320
|
fn();
|
|
3883
4321
|
} catch {
|
|
3884
4322
|
}
|
|
3885
4323
|
}
|
|
3886
|
-
this.
|
|
4324
|
+
this.binding.sessionCleanup = [];
|
|
3887
4325
|
}
|
|
3888
4326
|
/** Re-run the remote schema status check (dev toolbar). */
|
|
3889
4327
|
async refreshSchemaStatus() {
|
|
3890
4328
|
this.schemaChecked = false;
|
|
3891
4329
|
await this.checkSchema();
|
|
3892
|
-
this.
|
|
4330
|
+
this.syncLifecycle();
|
|
3893
4331
|
}
|
|
3894
4332
|
async listRejected() {
|
|
3895
4333
|
return this.engine?.listRejected() ?? [];
|
|
@@ -3905,13 +4343,14 @@ var BasicClient = class {
|
|
|
3905
4343
|
}
|
|
3906
4344
|
/** Mount a share: separate local keyspace + subscription. */
|
|
3907
4345
|
async mountShare(shareId) {
|
|
3908
|
-
|
|
4346
|
+
const engine = this.engine;
|
|
4347
|
+
if (!engine) throw new Error("shares require sync mode");
|
|
3909
4348
|
const existing = this.mounts.get(shareId);
|
|
3910
4349
|
if (existing) return existing;
|
|
3911
|
-
await
|
|
4350
|
+
await engine.mountShare(shareId);
|
|
3912
4351
|
const handle = {
|
|
3913
4352
|
shareId,
|
|
3914
|
-
db: new SyncDb(
|
|
4353
|
+
db: new SyncDb(engine, shareSubKey(shareId))
|
|
3915
4354
|
};
|
|
3916
4355
|
this.mounts.set(shareId, handle);
|
|
3917
4356
|
this.publish();
|
|
@@ -3935,50 +4374,260 @@ var BasicClient = class {
|
|
|
3935
4374
|
return this.snapshot;
|
|
3936
4375
|
};
|
|
3937
4376
|
// -------------------------------------------------------------------
|
|
3938
|
-
//
|
|
4377
|
+
// Profile binding & transitions
|
|
3939
4378
|
// -------------------------------------------------------------------
|
|
3940
|
-
|
|
3941
|
-
const
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
4379
|
+
createBinding(profile) {
|
|
4380
|
+
const storage = profile.storagePrefix ? new PrefixedStorage(this.rawStorage, profile.storagePrefix) : this.rawStorage;
|
|
4381
|
+
const binding = { profile, cleanup: [], sessionCleanup: [] };
|
|
4382
|
+
const auth = new AuthManager(
|
|
4383
|
+
{
|
|
4384
|
+
projectId: this.projectId,
|
|
4385
|
+
scopes: this.authConfig.scopes,
|
|
4386
|
+
pdsUrl: this.authConfig.pds_url,
|
|
4387
|
+
adminUrl: this.authConfig.admin_url,
|
|
4388
|
+
debug: this.debug,
|
|
4389
|
+
instanceKey: profile.storagePrefix
|
|
4390
|
+
},
|
|
4391
|
+
storage,
|
|
4392
|
+
() => {
|
|
4393
|
+
if (this.binding === binding) this.handleAuthChange();
|
|
4394
|
+
}
|
|
4395
|
+
);
|
|
4396
|
+
binding.auth = auth;
|
|
4397
|
+
if (this.mode === "sync" && this.projectId && this.config.schema?.tables) {
|
|
4398
|
+
const engine = new SyncEngine({
|
|
4399
|
+
projectId: this.projectId,
|
|
4400
|
+
schema: this.config.schema,
|
|
4401
|
+
wsUrl: this.syncUrl,
|
|
4402
|
+
getToken: (opts) => auth.getToken(opts),
|
|
4403
|
+
fetchSnapshot: (opts) => this.rest.getSnapshot(opts),
|
|
4404
|
+
WebSocketImpl: this.config.WebSocketImpl,
|
|
4405
|
+
keyspaceId: profile.keyspace,
|
|
4406
|
+
getOwnerDid: () => auth.did,
|
|
4407
|
+
log
|
|
4408
|
+
});
|
|
4409
|
+
binding.engine = engine;
|
|
4410
|
+
binding.syncDb = new SyncDb(engine, OWN_SUB);
|
|
4411
|
+
binding.cleanup.push(
|
|
4412
|
+
engine.on("status", () => this.publish()),
|
|
4413
|
+
engine.on("change", () => this.publish()),
|
|
4414
|
+
engine.on("rejected", ({ rejection }) => {
|
|
4415
|
+
log("op rejected:", rejection.error, rejection.op);
|
|
4416
|
+
this.publish();
|
|
4417
|
+
}),
|
|
4418
|
+
engine.on("revoked", ({ code, message }) => {
|
|
4419
|
+
log("connection revoked:", code, message);
|
|
4420
|
+
void auth.reconcileSession("connection revoked", { forceRefresh: true, throttleMs: 0 }).catch(() => {
|
|
4421
|
+
});
|
|
4422
|
+
this.publish();
|
|
4423
|
+
})
|
|
4424
|
+
);
|
|
3946
4425
|
} else {
|
|
3947
|
-
|
|
4426
|
+
binding.engine = null;
|
|
4427
|
+
binding.syncDb = null;
|
|
3948
4428
|
}
|
|
4429
|
+
return binding;
|
|
4430
|
+
}
|
|
4431
|
+
/**
|
|
4432
|
+
* Bind and boot a profile. Publishes the new binding first so React
|
|
4433
|
+
* subscriptions re-attach to the new db, then tears the old binding down
|
|
4434
|
+
* on the next tick (avoids in-flight live queries hitting a closed store).
|
|
4435
|
+
*/
|
|
4436
|
+
async activateProfile(profile, options) {
|
|
4437
|
+
let old = null;
|
|
4438
|
+
if (options?.initial && this.bindingMatches(profile)) {
|
|
4439
|
+
this.binding.profile = profile;
|
|
4440
|
+
} else {
|
|
4441
|
+
old = this.binding;
|
|
4442
|
+
this.binding = this.createBinding(profile);
|
|
4443
|
+
if (options?.initial) {
|
|
4444
|
+
for (const fn of [...old.cleanup, ...old.sessionCleanup]) fn();
|
|
4445
|
+
old.engine?.stop();
|
|
4446
|
+
old.auth.destroy();
|
|
4447
|
+
old = null;
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
await this.binding.auth.initialize();
|
|
4451
|
+
this.binding.sessionCleanup.push(this.binding.auth.setupNetworkListeners());
|
|
4452
|
+
this.mounts.clear();
|
|
4453
|
+
this.syncLifecycle();
|
|
3949
4454
|
this.publish();
|
|
4455
|
+
if (old) {
|
|
4456
|
+
setTimeout(() => {
|
|
4457
|
+
try {
|
|
4458
|
+
for (const fn of [...old.cleanup, ...old.sessionCleanup]) fn();
|
|
4459
|
+
old.engine?.stop();
|
|
4460
|
+
old.auth.destroy();
|
|
4461
|
+
} catch {
|
|
4462
|
+
}
|
|
4463
|
+
}, 50);
|
|
4464
|
+
}
|
|
3950
4465
|
}
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
4466
|
+
bindingMatches(profile) {
|
|
4467
|
+
return this.binding.profile.storagePrefix === profile.storagePrefix && this.binding.profile.keyspace === profile.keyspace;
|
|
4468
|
+
}
|
|
4469
|
+
/** After sign-out/disposal: resume on the next profile or a fresh anon one. */
|
|
4470
|
+
async activateNextProfileLocked() {
|
|
4471
|
+
let next = null;
|
|
4472
|
+
if (this.users) {
|
|
4473
|
+
next = await this.users.resolveActive();
|
|
4474
|
+
if (!next && this.anonymousEnabled) {
|
|
4475
|
+
next = await this.users.createAnon();
|
|
4476
|
+
}
|
|
4477
|
+
if (next) this.users.setActiveId(next.id);
|
|
3956
4478
|
}
|
|
4479
|
+
await this.activateProfile(next ?? ephemeralLegacyProfile());
|
|
3957
4480
|
}
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
4481
|
+
/** Wipe a (non-active) profile's local footprint: keyspace dbs + auth keys. */
|
|
4482
|
+
async disposeProfileData(profile) {
|
|
4483
|
+
const storage = profile.storagePrefix ? new PrefixedStorage(this.rawStorage, profile.storagePrefix) : this.rawStorage;
|
|
4484
|
+
try {
|
|
4485
|
+
const refreshToken = await storage.get(STORAGE_KEYS.REFRESH_TOKEN);
|
|
4486
|
+
if (refreshToken) {
|
|
4487
|
+
await fetch(`${this.authConfig.pds_url}/auth/revoke`, {
|
|
4488
|
+
method: "POST",
|
|
4489
|
+
headers: { "Content-Type": "application/json" },
|
|
4490
|
+
body: JSON.stringify({ token: refreshToken, token_type_hint: "refresh_token" })
|
|
4491
|
+
});
|
|
4492
|
+
}
|
|
4493
|
+
} catch {
|
|
4494
|
+
}
|
|
4495
|
+
for (const key of Object.values(STORAGE_KEYS)) {
|
|
3961
4496
|
try {
|
|
3962
|
-
await
|
|
3963
|
-
} catch
|
|
3964
|
-
log("local data teardown failed:", err);
|
|
4497
|
+
await storage.remove(key);
|
|
4498
|
+
} catch {
|
|
3965
4499
|
}
|
|
3966
4500
|
}
|
|
4501
|
+
await this.deleteKeyspaceDatabases(profile.keyspace);
|
|
4502
|
+
}
|
|
4503
|
+
async deleteKeyspaceDatabases(keyspace) {
|
|
4504
|
+
if (!this.projectId) return;
|
|
4505
|
+
const base = keyspace ? `basic-sync:${this.projectId}:${keyspace}` : `basic-sync:${this.projectId}`;
|
|
4506
|
+
try {
|
|
4507
|
+
const idb = globalThis.indexedDB;
|
|
4508
|
+
if (!idb) return;
|
|
4509
|
+
const names = [base];
|
|
4510
|
+
if (typeof idb.databases === "function") {
|
|
4511
|
+
const dbs = await idb.databases();
|
|
4512
|
+
for (const info of dbs) {
|
|
4513
|
+
if (info.name && info.name.startsWith(`${base}:share:`)) names.push(info.name);
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
for (const name of names) {
|
|
4517
|
+
await new Promise((resolve) => {
|
|
4518
|
+
const req = idb.deleteDatabase(name);
|
|
4519
|
+
req.onsuccess = req.onerror = req.onblocked = () => resolve();
|
|
4520
|
+
});
|
|
4521
|
+
}
|
|
4522
|
+
} catch {
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4525
|
+
// -------------------------------------------------------------------
|
|
4526
|
+
// Orchestration
|
|
4527
|
+
// -------------------------------------------------------------------
|
|
4528
|
+
/** Previous auth status, for transition detection (revoked-latch clearing). */
|
|
4529
|
+
lastAuthStatus = null;
|
|
4530
|
+
handleAuthChange() {
|
|
4531
|
+
const status = this.auth.authStatus;
|
|
4532
|
+
if (status === "authenticated" && this.lastAuthStatus !== "authenticated" && this.engine?.status === "revoked") {
|
|
4533
|
+
this.engine.clearRevoked();
|
|
4534
|
+
}
|
|
4535
|
+
this.lastAuthStatus = status;
|
|
4536
|
+
if (status === "signed_out" && !this.signOutInProgress && this.started) {
|
|
4537
|
+
const profile = this.binding.profile;
|
|
4538
|
+
if (this.users && profile.kind === "account") {
|
|
4539
|
+
void this.queueProfileOp(async () => {
|
|
4540
|
+
if (this.binding.profile.id !== profile.id) return;
|
|
4541
|
+
if (this.binding.auth.authStatus !== "signed_out") return;
|
|
4542
|
+
this.mounts.clear();
|
|
4543
|
+
try {
|
|
4544
|
+
await this.binding.engine?.destroyLocal();
|
|
4545
|
+
} catch {
|
|
4546
|
+
}
|
|
4547
|
+
await this.users.remove(profile.id);
|
|
4548
|
+
await this.activateNextProfileLocked();
|
|
4549
|
+
await this.refreshUsers();
|
|
4550
|
+
});
|
|
4551
|
+
return;
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
this.syncLifecycle();
|
|
4555
|
+
void this.maybeUpgradeProfile();
|
|
3967
4556
|
this.publish();
|
|
3968
4557
|
}
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
4558
|
+
/**
|
|
4559
|
+
* Drive the engine from auth + schema state:
|
|
4560
|
+
* - local keyspace opens with no token (anonymous mode / offline cold start)
|
|
4561
|
+
* - connect when a session exists (recovering counts — the connection
|
|
4562
|
+
* retries token acquisition itself)
|
|
4563
|
+
* - reauth_required pauses the connection, keeps local data usable
|
|
4564
|
+
*/
|
|
4565
|
+
syncLifecycle() {
|
|
4566
|
+
const { engine, auth, profile } = this.binding;
|
|
4567
|
+
if (!engine || !this.started) return;
|
|
4568
|
+
const status = auth.authStatus;
|
|
4569
|
+
if (status === "reauth_required") {
|
|
4570
|
+
engine.pause();
|
|
4571
|
+
return;
|
|
4572
|
+
}
|
|
4573
|
+
const localAllowed = this.anonymousEnabled || auth.isSignedIn || profile.kind === "account";
|
|
4574
|
+
if (!localAllowed) return;
|
|
4575
|
+
const schemaLocallyUsable = this.devInfo === null || this.devInfo.status !== "invalid";
|
|
4576
|
+
if (!schemaLocallyUsable) return;
|
|
4577
|
+
void engine.openLocal().then(() => {
|
|
4578
|
+
if (this.binding.engine !== engine) return;
|
|
4579
|
+
if (this.syncEnabled && auth.isSignedIn && auth.authStatus !== "reauth_required") {
|
|
4580
|
+
return engine.connect();
|
|
4581
|
+
}
|
|
4582
|
+
}).catch((err) => log("sync lifecycle failed:", err));
|
|
4583
|
+
}
|
|
4584
|
+
/**
|
|
4585
|
+
* After sign-in: bind the account identity to the active profile
|
|
4586
|
+
* (anonymous → account upgrade) and dedupe against an existing profile
|
|
4587
|
+
* for the same DID.
|
|
4588
|
+
*/
|
|
4589
|
+
async maybeUpgradeProfile() {
|
|
4590
|
+
const { profile, auth } = this.binding;
|
|
4591
|
+
if (!this.users || auth.authStatus !== "authenticated" || !auth.did) return;
|
|
4592
|
+
if (profile.id === "default") return;
|
|
4593
|
+
const did = auth.did;
|
|
4594
|
+
const user = auth.user;
|
|
4595
|
+
const needsUpdate = profile.did !== did || profile.kind !== "account" || profile.email !== (user?.email ?? profile.email) || profile.name !== (user?.name ?? profile.name);
|
|
4596
|
+
if (!needsUpdate) return;
|
|
4597
|
+
await this.queueProfileOp(async () => {
|
|
4598
|
+
if (this.binding.profile.id !== profile.id) return;
|
|
4599
|
+
if (!this.users) return;
|
|
4600
|
+
const existing = await this.users.findByDid(did);
|
|
4601
|
+
if (existing && existing.id !== profile.id) {
|
|
4602
|
+
log(`deduping user profiles for ${did}: dropping ${existing.id}`);
|
|
4603
|
+
await this.disposeProfileData(existing);
|
|
4604
|
+
await this.users.remove(existing.id);
|
|
4605
|
+
}
|
|
4606
|
+
const updated = await this.users.update(profile.id, {
|
|
4607
|
+
kind: "account",
|
|
4608
|
+
did,
|
|
4609
|
+
email: user?.email ?? profile.email ?? null,
|
|
4610
|
+
name: user?.name ?? profile.name ?? null,
|
|
4611
|
+
picture: user?.picture ?? profile.picture ?? null,
|
|
4612
|
+
lastActiveAt: Date.now()
|
|
3979
4613
|
});
|
|
3980
|
-
|
|
4614
|
+
if (updated) {
|
|
4615
|
+
this.binding.profile = updated;
|
|
4616
|
+
}
|
|
4617
|
+
await this.refreshUsers();
|
|
4618
|
+
});
|
|
4619
|
+
}
|
|
4620
|
+
async refreshUsers() {
|
|
4621
|
+
if (this.users) {
|
|
4622
|
+
this.usersCache = await this.users.list();
|
|
4623
|
+
}
|
|
4624
|
+
this.publish();
|
|
4625
|
+
}
|
|
4626
|
+
queueProfileOp(task) {
|
|
4627
|
+
this.profileOps = this.profileOps.then(task).catch((err) => {
|
|
4628
|
+
log("profile operation failed:", err);
|
|
3981
4629
|
});
|
|
4630
|
+
return this.profileOps;
|
|
3982
4631
|
}
|
|
3983
4632
|
async checkSchema() {
|
|
3984
4633
|
if (this.schemaChecked) return;
|
|
@@ -4022,7 +4671,7 @@ var BasicClient = class {
|
|
|
4022
4671
|
this.syncEnabled = result.schemaStatus.valid || remoteCheckInconclusive && locallyPublishable;
|
|
4023
4672
|
if (!result.schemaStatus.valid) {
|
|
4024
4673
|
if (status === "unpublished") {
|
|
4025
|
-
log("Schema not published (version 0) \u2014 sync is disabled
|
|
4674
|
+
log("Schema not published (version 0) \u2014 sync is disabled, local-only mode.");
|
|
4026
4675
|
} else if (remoteCheckInconclusive && locallyPublishable) {
|
|
4027
4676
|
log("Schema registry check failed \u2014 proceeding with the local schema (offline-first).");
|
|
4028
4677
|
}
|
|
@@ -4043,19 +4692,23 @@ var BasicClient = class {
|
|
|
4043
4692
|
this.publish();
|
|
4044
4693
|
}
|
|
4045
4694
|
buildSnapshot() {
|
|
4695
|
+
const { auth, engine, profile } = this.binding;
|
|
4046
4696
|
return {
|
|
4047
|
-
isReady:
|
|
4048
|
-
isSignedIn:
|
|
4049
|
-
authStatus:
|
|
4050
|
-
authErrorCode:
|
|
4051
|
-
user:
|
|
4052
|
-
did:
|
|
4053
|
-
scope:
|
|
4054
|
-
syncStatus:
|
|
4055
|
-
pendingCount:
|
|
4697
|
+
isReady: auth.isAuthReady,
|
|
4698
|
+
isSignedIn: auth.isSignedIn,
|
|
4699
|
+
authStatus: auth.authStatus,
|
|
4700
|
+
authErrorCode: auth.authErrorCode,
|
|
4701
|
+
user: auth.user,
|
|
4702
|
+
did: auth.did,
|
|
4703
|
+
scope: auth.tokenScope,
|
|
4704
|
+
syncStatus: engine?.status ?? "idle",
|
|
4705
|
+
pendingCount: engine?.pendingCount ?? 0,
|
|
4056
4706
|
syncEnabled: this.syncEnabled,
|
|
4057
4707
|
devInfo: this.devInfo,
|
|
4058
|
-
mode: this.mode
|
|
4708
|
+
mode: this.mode,
|
|
4709
|
+
users: this.usersCache,
|
|
4710
|
+
activeUser: this.users ? profile : null,
|
|
4711
|
+
isAnonymous: this.users ? profile.kind === "anon" && !auth.isSignedIn : false
|
|
4059
4712
|
};
|
|
4060
4713
|
}
|
|
4061
4714
|
publish() {
|
|
@@ -4086,6 +4739,7 @@ function BasicProvider({
|
|
|
4086
4739
|
storage,
|
|
4087
4740
|
debug = false,
|
|
4088
4741
|
mode = "sync",
|
|
4742
|
+
anonymous = true,
|
|
4089
4743
|
devToolbar = false,
|
|
4090
4744
|
renderWhileLoading = false
|
|
4091
4745
|
}) {
|
|
@@ -4097,7 +4751,8 @@ function BasicProvider({
|
|
|
4097
4751
|
auth,
|
|
4098
4752
|
storage,
|
|
4099
4753
|
debug,
|
|
4100
|
-
mode
|
|
4754
|
+
mode,
|
|
4755
|
+
anonymous
|
|
4101
4756
|
});
|
|
4102
4757
|
}
|
|
4103
4758
|
const client = clientRef.current;
|
|
@@ -4128,6 +4783,7 @@ export {
|
|
|
4128
4783
|
NotAuthenticatedError,
|
|
4129
4784
|
OWN_SUB,
|
|
4130
4785
|
PROTOCOL_VERSION,
|
|
4786
|
+
PrefixedStorage,
|
|
4131
4787
|
RestClient,
|
|
4132
4788
|
RestDb,
|
|
4133
4789
|
RestError,
|
|
@@ -4136,6 +4792,7 @@ export {
|
|
|
4136
4792
|
SyncDb,
|
|
4137
4793
|
SyncEngine,
|
|
4138
4794
|
SyncStore,
|
|
4795
|
+
UserRegistry,
|
|
4139
4796
|
applyOpToData,
|
|
4140
4797
|
createBasicClient,
|
|
4141
4798
|
isAuthError,
|
|
@@ -4154,6 +4811,7 @@ export {
|
|
|
4154
4811
|
useQuery,
|
|
4155
4812
|
useShare,
|
|
4156
4813
|
useShares,
|
|
4157
|
-
useSyncStatus
|
|
4814
|
+
useSyncStatus,
|
|
4815
|
+
useUsers
|
|
4158
4816
|
};
|
|
4159
4817
|
//# sourceMappingURL=index.mjs.map
|