@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.js
CHANGED
|
@@ -60,7 +60,7 @@ var init_config = __esm({
|
|
|
60
60
|
var version;
|
|
61
61
|
var init_package = __esm({
|
|
62
62
|
"package.json"() {
|
|
63
|
-
version = "0.9.0-beta.
|
|
63
|
+
version = "0.9.0-beta.1";
|
|
64
64
|
}
|
|
65
65
|
});
|
|
66
66
|
|
|
@@ -159,6 +159,22 @@ var init_network = __esm({
|
|
|
159
159
|
});
|
|
160
160
|
|
|
161
161
|
// src/react/hooks.ts
|
|
162
|
+
function useQuery(querier, deps = []) {
|
|
163
|
+
const client = (0, import_react2.useContext)(BasicClientContext);
|
|
164
|
+
const activeUserId = (0, import_react2.useSyncExternalStore)(
|
|
165
|
+
client ? client.subscribe : noopSubscribe,
|
|
166
|
+
() => client?.getSnapshot().activeUser?.id ?? null,
|
|
167
|
+
() => null
|
|
168
|
+
);
|
|
169
|
+
return (0, import_dexie_react_hooks.useLiveQuery)(async () => {
|
|
170
|
+
try {
|
|
171
|
+
return await querier();
|
|
172
|
+
} catch (err) {
|
|
173
|
+
if (err instanceof Error && err.name === "DatabaseClosedError") return void 0;
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
}, [...deps, activeUserId]);
|
|
177
|
+
}
|
|
162
178
|
function useBasicClient() {
|
|
163
179
|
const client = (0, import_react2.useContext)(BasicClientContext);
|
|
164
180
|
if (!client) {
|
|
@@ -176,6 +192,7 @@ function useAuth() {
|
|
|
176
192
|
() => ({
|
|
177
193
|
isReady: snapshot.isReady,
|
|
178
194
|
isSignedIn: snapshot.isSignedIn,
|
|
195
|
+
isAnonymous: snapshot.isAnonymous,
|
|
179
196
|
status: snapshot.authStatus,
|
|
180
197
|
errorCode: snapshot.authErrorCode,
|
|
181
198
|
user: snapshot.user,
|
|
@@ -232,9 +249,11 @@ function useShares() {
|
|
|
232
249
|
},
|
|
233
250
|
[client]
|
|
234
251
|
);
|
|
252
|
+
const activeUserId = snapshot.activeUser?.id ?? null;
|
|
235
253
|
(0, import_react2.useEffect)(() => {
|
|
236
254
|
if (isSignedIn) void refresh();
|
|
237
|
-
|
|
255
|
+
else setState({ granted: [], received: [], isLoading: false, error: null });
|
|
256
|
+
}, [isSignedIn, refresh, activeUserId]);
|
|
238
257
|
return { ...state, refresh };
|
|
239
258
|
}
|
|
240
259
|
function useShare(shareId) {
|
|
@@ -244,6 +263,7 @@ function useShare(shareId) {
|
|
|
244
263
|
const [error, setError] = (0, import_react2.useState)(null);
|
|
245
264
|
const [revoked, setRevoked] = (0, import_react2.useState)(false);
|
|
246
265
|
const canMount = !!shareId && snapshot.isSignedIn && snapshot.authStatus !== "reauth_required";
|
|
266
|
+
const activeUserId = snapshot.activeUser?.id ?? null;
|
|
247
267
|
(0, import_react2.useEffect)(() => {
|
|
248
268
|
if (!canMount || !shareId) return;
|
|
249
269
|
let cancelled = false;
|
|
@@ -267,13 +287,28 @@ function useShare(shareId) {
|
|
|
267
287
|
void client.unmountShare(shareId).catch(() => {
|
|
268
288
|
});
|
|
269
289
|
};
|
|
270
|
-
}, [client, shareId, canMount]);
|
|
290
|
+
}, [client, shareId, canMount, activeUserId]);
|
|
271
291
|
return {
|
|
272
292
|
db: handle?.db ?? null,
|
|
273
293
|
status: revoked ? "revoked" : error ? "error" : handle ? "mounted" : "mounting",
|
|
274
294
|
error
|
|
275
295
|
};
|
|
276
296
|
}
|
|
297
|
+
function useUsers() {
|
|
298
|
+
const client = useBasicClient();
|
|
299
|
+
const snapshot = useClientSnapshot(client);
|
|
300
|
+
return (0, import_react2.useMemo)(
|
|
301
|
+
() => ({
|
|
302
|
+
users: snapshot.users,
|
|
303
|
+
activeUser: snapshot.activeUser,
|
|
304
|
+
isAnonymous: snapshot.isAnonymous,
|
|
305
|
+
switchUser: (id) => client.switchUser(id),
|
|
306
|
+
addUser: () => client.addUser(),
|
|
307
|
+
removeUser: (id) => client.removeUser(id)
|
|
308
|
+
}),
|
|
309
|
+
[client, snapshot]
|
|
310
|
+
);
|
|
311
|
+
}
|
|
277
312
|
function useBasic() {
|
|
278
313
|
const client = useBasicClient();
|
|
279
314
|
const snapshot = useClientSnapshot(client);
|
|
@@ -284,6 +319,8 @@ function useBasic() {
|
|
|
284
319
|
...auth,
|
|
285
320
|
db: client.db,
|
|
286
321
|
sync,
|
|
322
|
+
users: snapshot.users,
|
|
323
|
+
activeUser: snapshot.activeUser,
|
|
287
324
|
devInfo: snapshot.devInfo,
|
|
288
325
|
refreshSchemaStatus: () => client.refreshSchemaStatus(),
|
|
289
326
|
client
|
|
@@ -291,14 +328,15 @@ function useBasic() {
|
|
|
291
328
|
[client, snapshot, auth, sync]
|
|
292
329
|
);
|
|
293
330
|
}
|
|
294
|
-
var import_react2, import_dexie_react_hooks,
|
|
331
|
+
var import_react2, import_dexie_react_hooks, noopSubscribe;
|
|
295
332
|
var init_hooks = __esm({
|
|
296
333
|
"src/react/hooks.ts"() {
|
|
297
334
|
"use strict";
|
|
298
335
|
import_react2 = require("react");
|
|
299
336
|
import_dexie_react_hooks = require("dexie-react-hooks");
|
|
300
337
|
init_context();
|
|
301
|
-
|
|
338
|
+
noopSubscribe = () => () => {
|
|
339
|
+
};
|
|
302
340
|
}
|
|
303
341
|
});
|
|
304
342
|
|
|
@@ -316,7 +354,8 @@ function toneForSync(mode, status) {
|
|
|
316
354
|
if (mode === "rest") return "muted";
|
|
317
355
|
if (status === "online") return "ok";
|
|
318
356
|
if (status === "connecting") return "warn";
|
|
319
|
-
if (status === "offline" || status === "idle" || status === "stopped"
|
|
357
|
+
if (status === "offline" || status === "idle" || status === "stopped" || status === "local")
|
|
358
|
+
return "muted";
|
|
320
359
|
return "bad";
|
|
321
360
|
}
|
|
322
361
|
function toneForSchema(info) {
|
|
@@ -330,6 +369,8 @@ function syncStatusLabel(status) {
|
|
|
330
369
|
switch (status) {
|
|
331
370
|
case "idle":
|
|
332
371
|
return "Idle";
|
|
372
|
+
case "local":
|
|
373
|
+
return "Local only";
|
|
333
374
|
case "connecting":
|
|
334
375
|
return "Connecting";
|
|
335
376
|
case "online":
|
|
@@ -506,18 +547,22 @@ function BasicDevToolbar({ enabled = true, debug }) {
|
|
|
506
547
|
const {
|
|
507
548
|
isReady,
|
|
508
549
|
isSignedIn,
|
|
550
|
+
isAnonymous,
|
|
509
551
|
user,
|
|
510
552
|
did,
|
|
511
553
|
scope,
|
|
512
554
|
missingScopes,
|
|
513
555
|
sync,
|
|
556
|
+
users,
|
|
557
|
+
activeUser,
|
|
514
558
|
devInfo,
|
|
515
559
|
refreshSchemaStatus,
|
|
516
560
|
client
|
|
517
561
|
} = useBasic();
|
|
518
562
|
const dbMode = client.mode;
|
|
519
563
|
const syncStatus = sync.status;
|
|
520
|
-
const indexedDbName = dbMode === "sync" && client.projectId ? `basic-sync:${client.projectId}` : null;
|
|
564
|
+
const indexedDbName = dbMode === "sync" && client.projectId ? `basic-sync:${client.projectId}${activeUser?.keyspace ? `:${activeUser.keyspace}` : ""}` : null;
|
|
565
|
+
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";
|
|
521
566
|
const [open, setOpen] = (0, import_react3.useState)(false);
|
|
522
567
|
const [refreshing, setRefreshing] = (0, import_react3.useState)(false);
|
|
523
568
|
const [copied, setCopied] = (0, import_react3.useState)(false);
|
|
@@ -554,9 +599,11 @@ function BasicDevToolbar({ enabled = true, debug }) {
|
|
|
554
599
|
syncStatus,
|
|
555
600
|
pendingOps: sync.pendingCount,
|
|
556
601
|
indexedDbName,
|
|
602
|
+
activeUser: activeUser ? { id: activeUser.id, kind: activeUser.kind, did: activeUser.did } : null,
|
|
603
|
+
userCount: users.length,
|
|
557
604
|
schema: devInfo
|
|
558
605
|
};
|
|
559
|
-
}, [isReady, isSignedIn, did, user, scope, dbMode, syncStatus, sync.pendingCount, indexedDbName, devInfo, missingList]);
|
|
606
|
+
}, [isReady, isSignedIn, did, user, scope, dbMode, syncStatus, sync.pendingCount, indexedDbName, activeUser, users.length, devInfo, missingList]);
|
|
560
607
|
const handleCopy = (0, import_react3.useCallback)(async () => {
|
|
561
608
|
try {
|
|
562
609
|
await navigator.clipboard.writeText(JSON.stringify(debugPayload, null, 2));
|
|
@@ -694,6 +741,17 @@ function BasicDevToolbar({ enabled = true, debug }) {
|
|
|
694
741
|
children: user ? displayUserLine(user) : "\u2014"
|
|
695
742
|
}
|
|
696
743
|
),
|
|
744
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
745
|
+
CopyableRow,
|
|
746
|
+
{
|
|
747
|
+
rowKey: "activeProfile",
|
|
748
|
+
label: "Profile",
|
|
749
|
+
copyText: activeUser?.id ?? "",
|
|
750
|
+
copiedKey: rowCopied,
|
|
751
|
+
onCopied: onRowCopied,
|
|
752
|
+
children: activeUserLabel
|
|
753
|
+
}
|
|
754
|
+
),
|
|
697
755
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
698
756
|
CopyableRow,
|
|
699
757
|
{
|
|
@@ -943,6 +1001,7 @@ __export(index_exports, {
|
|
|
943
1001
|
NotAuthenticatedError: () => NotAuthenticatedError,
|
|
944
1002
|
OWN_SUB: () => OWN_SUB,
|
|
945
1003
|
PROTOCOL_VERSION: () => PROTOCOL_VERSION,
|
|
1004
|
+
PrefixedStorage: () => PrefixedStorage,
|
|
946
1005
|
RestClient: () => RestClient,
|
|
947
1006
|
RestDb: () => RestDb,
|
|
948
1007
|
RestError: () => RestError,
|
|
@@ -951,6 +1010,7 @@ __export(index_exports, {
|
|
|
951
1010
|
SyncDb: () => SyncDb,
|
|
952
1011
|
SyncEngine: () => SyncEngine,
|
|
953
1012
|
SyncStore: () => SyncStore,
|
|
1013
|
+
UserRegistry: () => UserRegistry,
|
|
954
1014
|
applyOpToData: () => applyOpToData,
|
|
955
1015
|
createBasicClient: () => createBasicClient,
|
|
956
1016
|
isAuthError: () => isAuthError,
|
|
@@ -969,7 +1029,8 @@ __export(index_exports, {
|
|
|
969
1029
|
useQuery: () => useQuery,
|
|
970
1030
|
useShare: () => useShare,
|
|
971
1031
|
useShares: () => useShares,
|
|
972
|
-
useSyncStatus: () => useSyncStatus
|
|
1032
|
+
useSyncStatus: () => useSyncStatus,
|
|
1033
|
+
useUsers: () => useUsers
|
|
973
1034
|
});
|
|
974
1035
|
module.exports = __toCommonJS(index_exports);
|
|
975
1036
|
|
|
@@ -1156,11 +1217,15 @@ var AuthManager = class {
|
|
|
1156
1217
|
this.requestedScopes = config.scopes;
|
|
1157
1218
|
this.initCrossTabSync();
|
|
1158
1219
|
}
|
|
1220
|
+
get instanceKey() {
|
|
1221
|
+
return this.config.instanceKey ?? "";
|
|
1222
|
+
}
|
|
1159
1223
|
initCrossTabSync() {
|
|
1160
1224
|
if (typeof BroadcastChannel === "undefined") return;
|
|
1161
1225
|
try {
|
|
1162
1226
|
this.channel = new BroadcastChannel("basic-auth");
|
|
1163
1227
|
this.channel.onmessage = (event) => {
|
|
1228
|
+
if ((event.data?.userKey ?? "") !== this.instanceKey) return;
|
|
1164
1229
|
if (event.data?.type === "token_refreshed") {
|
|
1165
1230
|
log("Received token refresh from another tab");
|
|
1166
1231
|
void this.handleExternalTokenRefresh(event.data);
|
|
@@ -1188,19 +1253,32 @@ var AuthManager = class {
|
|
|
1188
1253
|
broadcastTokenRefresh() {
|
|
1189
1254
|
this.channel?.postMessage({
|
|
1190
1255
|
type: "token_refreshed",
|
|
1256
|
+
userKey: this.instanceKey,
|
|
1191
1257
|
accessToken: this.token?.access_token,
|
|
1192
1258
|
did: this.did,
|
|
1193
1259
|
tokenScope: this.tokenScope
|
|
1194
1260
|
});
|
|
1195
1261
|
}
|
|
1196
1262
|
broadcastSignIn() {
|
|
1197
|
-
this.channel?.postMessage({ type: "signed_in" });
|
|
1263
|
+
this.channel?.postMessage({ type: "signed_in", userKey: this.instanceKey });
|
|
1198
1264
|
}
|
|
1199
1265
|
broadcastSignOut() {
|
|
1200
|
-
this.channel?.postMessage({ type: "signed_out" });
|
|
1266
|
+
this.channel?.postMessage({ type: "signed_out", userKey: this.instanceKey });
|
|
1201
1267
|
}
|
|
1202
1268
|
broadcastSessionInvalidated(code) {
|
|
1203
|
-
this.channel?.postMessage({
|
|
1269
|
+
this.channel?.postMessage({
|
|
1270
|
+
type: "session_invalidated",
|
|
1271
|
+
userKey: this.instanceKey,
|
|
1272
|
+
code
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
/** Release resources (cross-tab channel). Used when switching users. */
|
|
1276
|
+
destroy() {
|
|
1277
|
+
try {
|
|
1278
|
+
this.channel?.close();
|
|
1279
|
+
} catch {
|
|
1280
|
+
}
|
|
1281
|
+
this.channel = null;
|
|
1204
1282
|
}
|
|
1205
1283
|
// ------------------------------------------------------------------
|
|
1206
1284
|
// Public API
|
|
@@ -2628,6 +2706,7 @@ var SyncConnection = class {
|
|
|
2628
2706
|
var import_dexie = __toESM(require("dexie"));
|
|
2629
2707
|
var META_CURSOR = "cursor";
|
|
2630
2708
|
var META_CHANNEL = "channel";
|
|
2709
|
+
var META_OWNER = "owner_did";
|
|
2631
2710
|
var SyncStore = class {
|
|
2632
2711
|
db;
|
|
2633
2712
|
name;
|
|
@@ -2684,6 +2763,32 @@ var SyncStore = class {
|
|
|
2684
2763
|
const row = await this.meta.get(META_CHANNEL);
|
|
2685
2764
|
return typeof row?.value === "string" ? row.value : null;
|
|
2686
2765
|
}
|
|
2766
|
+
/**
|
|
2767
|
+
* The account DID this keyspace's confirmed data belongs to. Absent for
|
|
2768
|
+
* anonymous-era data (which may be merged into whichever account signs in).
|
|
2769
|
+
*/
|
|
2770
|
+
async getOwner() {
|
|
2771
|
+
const row = await this.meta.get(META_OWNER);
|
|
2772
|
+
return typeof row?.value === "string" ? row.value : null;
|
|
2773
|
+
}
|
|
2774
|
+
async setOwner(did) {
|
|
2775
|
+
await this.meta.put({ key: META_OWNER, value: did });
|
|
2776
|
+
}
|
|
2777
|
+
/**
|
|
2778
|
+
* Clear everything (views, server state, pending, rejected, meta) without
|
|
2779
|
+
* deleting the database — used when the keyspace changes owners.
|
|
2780
|
+
*/
|
|
2781
|
+
async wipeAll() {
|
|
2782
|
+
await this.db.transaction("rw", this.allStores, async () => {
|
|
2783
|
+
await this.server.clear();
|
|
2784
|
+
await this.pending.clear();
|
|
2785
|
+
await this.rejected.clear();
|
|
2786
|
+
await this.meta.clear();
|
|
2787
|
+
for (const tableName of this.tableNames) {
|
|
2788
|
+
await this.view(tableName).clear();
|
|
2789
|
+
}
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2687
2792
|
// -------------------------------------------------------------------
|
|
2688
2793
|
// Pending / rejected
|
|
2689
2794
|
// -------------------------------------------------------------------
|
|
@@ -2896,7 +3001,11 @@ var SyncEngine = class {
|
|
|
2896
3001
|
subs = /* @__PURE__ */ new Map();
|
|
2897
3002
|
limits = { ...DEFAULT_LIMITS };
|
|
2898
3003
|
actor = null;
|
|
2899
|
-
|
|
3004
|
+
/** Own-sub store is open (local reads/writes work). */
|
|
3005
|
+
storesOpen = false;
|
|
3006
|
+
/** A live connection is wanted (vs. local-only / paused). */
|
|
3007
|
+
connectIntended = false;
|
|
3008
|
+
openingLocal = null;
|
|
2900
3009
|
revokedInfo = null;
|
|
2901
3010
|
connectionStatus = "idle";
|
|
2902
3011
|
_status = "idle";
|
|
@@ -2971,26 +3080,76 @@ var SyncEngine = class {
|
|
|
2971
3080
|
// -------------------------------------------------------------------
|
|
2972
3081
|
// Lifecycle
|
|
2973
3082
|
// -------------------------------------------------------------------
|
|
2974
|
-
/**
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
3083
|
+
/**
|
|
3084
|
+
* Open the own-channel keyspace for local reads/writes — no connection,
|
|
3085
|
+
* no token needed. This is the anonymous / offline-cold-start entry point.
|
|
3086
|
+
* Idempotent.
|
|
3087
|
+
*/
|
|
3088
|
+
async openLocal() {
|
|
3089
|
+
if (this.subs.has(OWN_SUB)) {
|
|
3090
|
+
this.storesOpen = true;
|
|
3091
|
+
this.recomputeStatus();
|
|
2980
3092
|
return;
|
|
2981
3093
|
}
|
|
2982
|
-
this.
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
3094
|
+
if (!this.openingLocal) {
|
|
3095
|
+
this.openingLocal = (async () => {
|
|
3096
|
+
const sub = await this.openSub(OWN_SUB, null);
|
|
3097
|
+
this.subs.set(OWN_SUB, sub);
|
|
3098
|
+
this.storesOpen = true;
|
|
3099
|
+
})().finally(() => {
|
|
3100
|
+
this.openingLocal = null;
|
|
3101
|
+
});
|
|
2987
3102
|
}
|
|
3103
|
+
await this.openingLocal;
|
|
3104
|
+
this.recomputeStatus();
|
|
3105
|
+
}
|
|
3106
|
+
/**
|
|
3107
|
+
* Open the keyspace (if needed) and start syncing. Idempotent.
|
|
3108
|
+
* Note: a `CONNECTION_REVOKED` latch is NOT cleared here — reconnecting a
|
|
3109
|
+
* revoked app connection requires a fresh consent flow. Call
|
|
3110
|
+
* {@link clearRevoked} (or rebind the engine) after re-authorization.
|
|
3111
|
+
*/
|
|
3112
|
+
async connect() {
|
|
3113
|
+
await this.openLocal();
|
|
3114
|
+
if (this.revokedInfo) {
|
|
3115
|
+
this.log("connect() ignored: app connection is revoked");
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
this.connectIntended = true;
|
|
2988
3119
|
this.connection.start();
|
|
2989
3120
|
this.recomputeStatus();
|
|
2990
3121
|
}
|
|
3122
|
+
/** Clear the revocation latch (after the user re-authorized the app). */
|
|
3123
|
+
clearRevoked() {
|
|
3124
|
+
this.revokedInfo = null;
|
|
3125
|
+
this.recomputeStatus();
|
|
3126
|
+
}
|
|
3127
|
+
/** @deprecated alias of {@link connect} */
|
|
3128
|
+
async start() {
|
|
3129
|
+
return this.connect();
|
|
3130
|
+
}
|
|
3131
|
+
/**
|
|
3132
|
+
* Disconnect but keep stores open: local reads/writes keep working and
|
|
3133
|
+
* ops queue for the next connect. Used on reauth_required.
|
|
3134
|
+
*/
|
|
3135
|
+
pause() {
|
|
3136
|
+
this.connectIntended = false;
|
|
3137
|
+
this.connection.stop();
|
|
3138
|
+
for (const t of this.timers) clearTimeout(t);
|
|
3139
|
+
this.timers.clear();
|
|
3140
|
+
for (const sub of this.subs.values()) {
|
|
3141
|
+
sub.active = false;
|
|
3142
|
+
for (const p of sub.pending) {
|
|
3143
|
+
p.sent = false;
|
|
3144
|
+
p.ackedSeq = void 0;
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
this.recomputeStatus();
|
|
3148
|
+
}
|
|
2991
3149
|
/** Close the socket and stores; local data is kept. */
|
|
2992
3150
|
stop() {
|
|
2993
|
-
this.
|
|
3151
|
+
this.connectIntended = false;
|
|
3152
|
+
this.storesOpen = false;
|
|
2994
3153
|
this.connection.stop();
|
|
2995
3154
|
for (const t of this.timers) clearTimeout(t);
|
|
2996
3155
|
this.timers.clear();
|
|
@@ -3018,10 +3177,10 @@ var SyncEngine = class {
|
|
|
3018
3177
|
try {
|
|
3019
3178
|
const idb = globalThis.indexedDB;
|
|
3020
3179
|
if (idb && typeof idb.databases === "function") {
|
|
3021
|
-
const
|
|
3180
|
+
const base = this.baseDbName;
|
|
3022
3181
|
const dbs = await idb.databases();
|
|
3023
3182
|
for (const info of dbs) {
|
|
3024
|
-
if (info.name && info.name.startsWith(
|
|
3183
|
+
if (info.name && (info.name === base || info.name.startsWith(`${base}:share:`))) {
|
|
3025
3184
|
await new Promise((resolve) => {
|
|
3026
3185
|
const req = idb.deleteDatabase(info.name);
|
|
3027
3186
|
req.onsuccess = req.onerror = req.onblocked = () => resolve();
|
|
@@ -3119,7 +3278,7 @@ var SyncEngine = class {
|
|
|
3119
3278
|
// -------------------------------------------------------------------
|
|
3120
3279
|
handleConnectionStatus(status) {
|
|
3121
3280
|
this.connectionStatus = status;
|
|
3122
|
-
if (status === "offline" || status === "connecting" || status === "auth_failed") {
|
|
3281
|
+
if (status === "offline" || status === "connecting" || status === "auth_failed" || status === "stopped") {
|
|
3123
3282
|
for (const sub of this.subs.values()) {
|
|
3124
3283
|
sub.active = false;
|
|
3125
3284
|
for (const p of sub.pending) {
|
|
@@ -3207,13 +3366,13 @@ var SyncEngine = class {
|
|
|
3207
3366
|
switch (msg.code) {
|
|
3208
3367
|
case "CONNECTION_REVOKED":
|
|
3209
3368
|
this.revokedInfo = { code: msg.code, message: msg.message };
|
|
3210
|
-
this.
|
|
3369
|
+
this.connectIntended = false;
|
|
3211
3370
|
this.connection.stop();
|
|
3212
3371
|
this.recomputeStatus();
|
|
3213
3372
|
this.emit("revoked", { code: msg.code, message: msg.message });
|
|
3214
3373
|
return;
|
|
3215
3374
|
case "UNSUPPORTED_VERSION":
|
|
3216
|
-
this.
|
|
3375
|
+
this.connectIntended = false;
|
|
3217
3376
|
this.connection.stop();
|
|
3218
3377
|
this.recomputeStatus();
|
|
3219
3378
|
return;
|
|
@@ -3248,7 +3407,7 @@ var SyncEngine = class {
|
|
|
3248
3407
|
// Subscription state machine (serialized per sub via `chain`)
|
|
3249
3408
|
// -------------------------------------------------------------------
|
|
3250
3409
|
async openSub(key, shareId) {
|
|
3251
|
-
const name = shareId ? `${this.
|
|
3410
|
+
const name = shareId ? `${this.baseDbName}:share:${shareId}` : this.baseDbName;
|
|
3252
3411
|
const store = new SyncStore(name, this.schema);
|
|
3253
3412
|
const [cursor, pendingRows] = await Promise.all([store.getCursor(), store.loadPending()]);
|
|
3254
3413
|
return {
|
|
@@ -3269,6 +3428,16 @@ var SyncEngine = class {
|
|
|
3269
3428
|
/** Bootstrap if needed, then bind the stream on the current socket. */
|
|
3270
3429
|
async activateSub(sub) {
|
|
3271
3430
|
if (!this.connection.isOnline) return;
|
|
3431
|
+
if (sub.bootstrapped && !sub.shareId && this.opts.getOwnerDid) {
|
|
3432
|
+
try {
|
|
3433
|
+
const did = await this.opts.getOwnerDid() ?? null;
|
|
3434
|
+
if (did) {
|
|
3435
|
+
const stamped = await sub.store.getOwner();
|
|
3436
|
+
if (stamped && stamped !== did) sub.bootstrapped = false;
|
|
3437
|
+
}
|
|
3438
|
+
} catch {
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3272
3441
|
if (!sub.bootstrapped || sub.cursor < 0) {
|
|
3273
3442
|
try {
|
|
3274
3443
|
await this.bootstrapSub(sub);
|
|
@@ -3287,6 +3456,26 @@ var SyncEngine = class {
|
|
|
3287
3456
|
}
|
|
3288
3457
|
/** Cold start = snapshot + tail; never log replay (§5). Pending survives. */
|
|
3289
3458
|
async bootstrapSub(sub) {
|
|
3459
|
+
let ownerDid = null;
|
|
3460
|
+
if (!sub.shareId && this.opts.getOwnerDid) {
|
|
3461
|
+
try {
|
|
3462
|
+
ownerDid = await this.opts.getOwnerDid() ?? null;
|
|
3463
|
+
} catch {
|
|
3464
|
+
ownerDid = null;
|
|
3465
|
+
}
|
|
3466
|
+
if (ownerDid) {
|
|
3467
|
+
const stamped = await sub.store.getOwner();
|
|
3468
|
+
if (stamped && stamped !== ownerDid) {
|
|
3469
|
+
this.log(
|
|
3470
|
+
`keyspace owned by ${stamped} but session is ${ownerDid} \u2014 wiping local data before bootstrap`
|
|
3471
|
+
);
|
|
3472
|
+
await sub.store.wipeAll();
|
|
3473
|
+
sub.pending = [];
|
|
3474
|
+
sub.appliedOpIds.clear();
|
|
3475
|
+
sub.cursor = -1;
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3290
3479
|
const snapshot = await this.opts.fetchSnapshot(
|
|
3291
3480
|
sub.shareId ? { share: sub.shareId } : void 0
|
|
3292
3481
|
);
|
|
@@ -3295,6 +3484,9 @@ var SyncEngine = class {
|
|
|
3295
3484
|
records: snapshot.records ?? {},
|
|
3296
3485
|
cursor: snapshot.cursor ?? 0
|
|
3297
3486
|
});
|
|
3487
|
+
if (!sub.shareId && ownerDid) {
|
|
3488
|
+
await sub.store.setOwner(ownerDid);
|
|
3489
|
+
}
|
|
3298
3490
|
sub.cursor = snapshot.cursor ?? 0;
|
|
3299
3491
|
sub.bootstrapped = true;
|
|
3300
3492
|
sub.appliedOpIds.clear();
|
|
@@ -3418,6 +3610,11 @@ var SyncEngine = class {
|
|
|
3418
3610
|
get dbPrefix() {
|
|
3419
3611
|
return this.opts.dbNamePrefix ?? "basic-sync";
|
|
3420
3612
|
}
|
|
3613
|
+
/** Base database name for this keyspace (multi-user: includes the user id). */
|
|
3614
|
+
get baseDbName() {
|
|
3615
|
+
const base = `${this.dbPrefix}:${this.projectId}`;
|
|
3616
|
+
return this.opts.keyspaceId ? `${base}:${this.opts.keyspaceId}` : base;
|
|
3617
|
+
}
|
|
3421
3618
|
enqueue(sub, task) {
|
|
3422
3619
|
sub.chain = sub.chain.then(task).catch((err) => {
|
|
3423
3620
|
this.log(`task failed on '${sub.key}':`, err);
|
|
@@ -3435,10 +3632,12 @@ var SyncEngine = class {
|
|
|
3435
3632
|
let status;
|
|
3436
3633
|
if (this.revokedInfo) status = "revoked";
|
|
3437
3634
|
else if (this.connectionStatus === "auth_failed") status = "auth_required";
|
|
3438
|
-
else if (!this.
|
|
3635
|
+
else if (!this.storesOpen) status = this.connectionStatus === "stopped" ? "stopped" : "idle";
|
|
3636
|
+
else if (!this.connectIntended) status = "local";
|
|
3439
3637
|
else if (this.connectionStatus === "online") status = "online";
|
|
3440
3638
|
else if (this.connectionStatus === "connecting") status = "connecting";
|
|
3441
3639
|
else if (this.connectionStatus === "idle") status = "connecting";
|
|
3640
|
+
else if (this.connectionStatus === "stopped") status = "local";
|
|
3442
3641
|
else status = "offline";
|
|
3443
3642
|
if (status !== this._status) {
|
|
3444
3643
|
this._status = status;
|
|
@@ -3464,7 +3663,7 @@ var SyncTable = class {
|
|
|
3464
3663
|
const sub = this.engine.getSubscription(this.subKey);
|
|
3465
3664
|
if (!sub) {
|
|
3466
3665
|
throw new Error(
|
|
3467
|
-
`subscription '${this.subKey}' is not open \u2014
|
|
3666
|
+
`subscription '${this.subKey}' is not open \u2014 wait for the client to be ready (isReady) before using the db`
|
|
3468
3667
|
);
|
|
3469
3668
|
}
|
|
3470
3669
|
return sub.store;
|
|
@@ -3594,6 +3793,171 @@ var RestDb = class {
|
|
|
3594
3793
|
}
|
|
3595
3794
|
};
|
|
3596
3795
|
|
|
3796
|
+
// src/core/users.ts
|
|
3797
|
+
init_config();
|
|
3798
|
+
var PrefixedStorage = class {
|
|
3799
|
+
constructor(inner, prefix) {
|
|
3800
|
+
this.inner = inner;
|
|
3801
|
+
this.prefix = prefix;
|
|
3802
|
+
}
|
|
3803
|
+
get(key) {
|
|
3804
|
+
return this.inner.get(this.prefix + key);
|
|
3805
|
+
}
|
|
3806
|
+
set(key, value) {
|
|
3807
|
+
return this.inner.set(this.prefix + key, value);
|
|
3808
|
+
}
|
|
3809
|
+
remove(key) {
|
|
3810
|
+
return this.inner.remove(this.prefix + key);
|
|
3811
|
+
}
|
|
3812
|
+
};
|
|
3813
|
+
function registryKey(projectId) {
|
|
3814
|
+
return `basic_users:${projectId}`;
|
|
3815
|
+
}
|
|
3816
|
+
function activeUserSessionKey(projectId) {
|
|
3817
|
+
return `basic_active_user:${projectId}`;
|
|
3818
|
+
}
|
|
3819
|
+
var UserRegistry = class {
|
|
3820
|
+
constructor(storage, projectId) {
|
|
3821
|
+
this.storage = storage;
|
|
3822
|
+
this.projectId = projectId;
|
|
3823
|
+
}
|
|
3824
|
+
// -------------------------------------------------------------------
|
|
3825
|
+
// Registry CRUD
|
|
3826
|
+
// -------------------------------------------------------------------
|
|
3827
|
+
async list() {
|
|
3828
|
+
const raw = await this.storage.get(registryKey(this.projectId));
|
|
3829
|
+
if (!raw) return [];
|
|
3830
|
+
try {
|
|
3831
|
+
const parsed = JSON.parse(raw);
|
|
3832
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
3833
|
+
} catch {
|
|
3834
|
+
return [];
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
async save(users) {
|
|
3838
|
+
await this.storage.set(registryKey(this.projectId), JSON.stringify(users));
|
|
3839
|
+
}
|
|
3840
|
+
async get(id) {
|
|
3841
|
+
const users = await this.list();
|
|
3842
|
+
return users.find((u) => u.id === id) ?? null;
|
|
3843
|
+
}
|
|
3844
|
+
async createAnon() {
|
|
3845
|
+
const id = mintOpId();
|
|
3846
|
+
const now = Date.now();
|
|
3847
|
+
const profile = {
|
|
3848
|
+
id,
|
|
3849
|
+
kind: "anon",
|
|
3850
|
+
keyspace: id,
|
|
3851
|
+
storagePrefix: `u:${id}:`,
|
|
3852
|
+
createdAt: now,
|
|
3853
|
+
lastActiveAt: now
|
|
3854
|
+
};
|
|
3855
|
+
const users = await this.list();
|
|
3856
|
+
users.push(profile);
|
|
3857
|
+
await this.save(users);
|
|
3858
|
+
log(`created anonymous user ${id}`);
|
|
3859
|
+
return profile;
|
|
3860
|
+
}
|
|
3861
|
+
async update(id, patch) {
|
|
3862
|
+
const users = await this.list();
|
|
3863
|
+
const idx = users.findIndex((u) => u.id === id);
|
|
3864
|
+
if (idx < 0) return null;
|
|
3865
|
+
users[idx] = { ...users[idx], ...patch };
|
|
3866
|
+
await this.save(users);
|
|
3867
|
+
return users[idx];
|
|
3868
|
+
}
|
|
3869
|
+
async remove(id) {
|
|
3870
|
+
const users = await this.list();
|
|
3871
|
+
await this.save(users.filter((u) => u.id !== id));
|
|
3872
|
+
if (this.getActiveIdRaw() === id) this.clearActiveId();
|
|
3873
|
+
}
|
|
3874
|
+
/** The profile (if any) already bound to an account DID. */
|
|
3875
|
+
async findByDid(did) {
|
|
3876
|
+
const users = await this.list();
|
|
3877
|
+
return users.find((u) => u.did === did) ?? null;
|
|
3878
|
+
}
|
|
3879
|
+
// -------------------------------------------------------------------
|
|
3880
|
+
// Active user (per-tab)
|
|
3881
|
+
// -------------------------------------------------------------------
|
|
3882
|
+
getActiveIdRaw() {
|
|
3883
|
+
try {
|
|
3884
|
+
return sessionStorage.getItem(activeUserSessionKey(this.projectId));
|
|
3885
|
+
} catch {
|
|
3886
|
+
return null;
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
setActiveId(id) {
|
|
3890
|
+
try {
|
|
3891
|
+
sessionStorage.setItem(activeUserSessionKey(this.projectId), id);
|
|
3892
|
+
} catch {
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
clearActiveId() {
|
|
3896
|
+
try {
|
|
3897
|
+
sessionStorage.removeItem(activeUserSessionKey(this.projectId));
|
|
3898
|
+
} catch {
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
/**
|
|
3902
|
+
* Resolve the active profile for this tab: sessionStorage choice if it
|
|
3903
|
+
* still exists, else the most recently active profile, else null.
|
|
3904
|
+
*/
|
|
3905
|
+
async resolveActive() {
|
|
3906
|
+
const users = await this.list();
|
|
3907
|
+
const activeId = this.getActiveIdRaw();
|
|
3908
|
+
if (activeId) {
|
|
3909
|
+
const match = users.find((u) => u.id === activeId);
|
|
3910
|
+
if (match) return match;
|
|
3911
|
+
}
|
|
3912
|
+
if (users.length === 0) return null;
|
|
3913
|
+
const recent = [...users].sort((a, b) => b.lastActiveAt - a.lastActiveAt)[0];
|
|
3914
|
+
this.setActiveId(recent.id);
|
|
3915
|
+
return recent;
|
|
3916
|
+
}
|
|
3917
|
+
async touch(id) {
|
|
3918
|
+
await this.update(id, { lastActiveAt: Date.now() });
|
|
3919
|
+
}
|
|
3920
|
+
// -------------------------------------------------------------------
|
|
3921
|
+
// Legacy adoption
|
|
3922
|
+
// -------------------------------------------------------------------
|
|
3923
|
+
/**
|
|
3924
|
+
* Adopt a pre-multi-user session as the first profile. Idempotent: runs
|
|
3925
|
+
* only when the registry is empty and a bare refresh token exists. The
|
|
3926
|
+
* adopted profile keeps the unprefixed storage keys and the legacy
|
|
3927
|
+
* keyspace name, so nothing needs to move.
|
|
3928
|
+
*/
|
|
3929
|
+
async adoptLegacySession() {
|
|
3930
|
+
const users = await this.list();
|
|
3931
|
+
if (users.length > 0) return null;
|
|
3932
|
+
const legacyRefresh = await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
|
|
3933
|
+
if (!legacyRefresh) return null;
|
|
3934
|
+
let cachedUser = null;
|
|
3935
|
+
try {
|
|
3936
|
+
const raw = await this.storage.get(STORAGE_KEYS.USER_INFO);
|
|
3937
|
+
if (raw) cachedUser = JSON.parse(raw);
|
|
3938
|
+
} catch {
|
|
3939
|
+
}
|
|
3940
|
+
const now = Date.now();
|
|
3941
|
+
const profile = {
|
|
3942
|
+
id: mintOpId(),
|
|
3943
|
+
kind: "account",
|
|
3944
|
+
did: cachedUser?.sub ?? null,
|
|
3945
|
+
email: cachedUser?.email ?? null,
|
|
3946
|
+
name: cachedUser?.name ?? null,
|
|
3947
|
+
picture: cachedUser?.picture ?? null,
|
|
3948
|
+
keyspace: "",
|
|
3949
|
+
// legacy `basic-sync:{projectId}` database
|
|
3950
|
+
storagePrefix: "",
|
|
3951
|
+
// legacy unprefixed auth keys
|
|
3952
|
+
createdAt: now,
|
|
3953
|
+
lastActiveAt: now
|
|
3954
|
+
};
|
|
3955
|
+
await this.save([profile]);
|
|
3956
|
+
log("adopted legacy single-user session as profile", profile.id);
|
|
3957
|
+
return profile;
|
|
3958
|
+
}
|
|
3959
|
+
};
|
|
3960
|
+
|
|
3597
3961
|
// src/utils/schema.ts
|
|
3598
3962
|
var import_schema2 = require("@basictech/schema");
|
|
3599
3963
|
init_config();
|
|
@@ -3843,21 +4207,38 @@ var DEFAULTS = {
|
|
|
3843
4207
|
function deriveSyncUrl(pdsUrl) {
|
|
3844
4208
|
return pdsUrl.replace(/^http/, "ws").replace(/\/$/, "") + "/sync/";
|
|
3845
4209
|
}
|
|
4210
|
+
function ephemeralLegacyProfile() {
|
|
4211
|
+
const now = Date.now();
|
|
4212
|
+
return {
|
|
4213
|
+
id: "default",
|
|
4214
|
+
kind: "anon",
|
|
4215
|
+
keyspace: "",
|
|
4216
|
+
storagePrefix: "",
|
|
4217
|
+
createdAt: now,
|
|
4218
|
+
lastActiveAt: now
|
|
4219
|
+
};
|
|
4220
|
+
}
|
|
3846
4221
|
var BasicClient = class {
|
|
3847
|
-
auth;
|
|
3848
4222
|
rest;
|
|
3849
|
-
engine;
|
|
3850
4223
|
mode;
|
|
3851
4224
|
config;
|
|
3852
4225
|
projectId;
|
|
3853
|
-
|
|
4226
|
+
users;
|
|
4227
|
+
rawStorage;
|
|
3854
4228
|
restDb;
|
|
3855
4229
|
debug;
|
|
4230
|
+
anonymousEnabled;
|
|
4231
|
+
authConfig;
|
|
4232
|
+
syncUrl;
|
|
4233
|
+
binding;
|
|
4234
|
+
usersCache = [];
|
|
3856
4235
|
devInfo = null;
|
|
3857
4236
|
syncEnabled = false;
|
|
3858
4237
|
schemaChecked = false;
|
|
3859
4238
|
started = false;
|
|
3860
|
-
|
|
4239
|
+
signOutInProgress = false;
|
|
4240
|
+
/** Serializes profile transitions (switch, dispose, sign-out fallthrough). */
|
|
4241
|
+
profileOps = Promise.resolve();
|
|
3861
4242
|
mounts = /* @__PURE__ */ new Map();
|
|
3862
4243
|
listeners = /* @__PURE__ */ new Set();
|
|
3863
4244
|
snapshot;
|
|
@@ -3866,95 +4247,155 @@ var BasicClient = class {
|
|
|
3866
4247
|
this.debug = config.debug ?? false;
|
|
3867
4248
|
this.mode = config.mode ?? "sync";
|
|
3868
4249
|
this.projectId = config.schema?.project_id || config.project_id;
|
|
3869
|
-
|
|
4250
|
+
this.anonymousEnabled = this.mode === "sync" && (config.anonymous ?? true);
|
|
4251
|
+
this.authConfig = {
|
|
3870
4252
|
scopes: Array.isArray(config.auth?.scopes) ? config.auth.scopes.join(" ") : config.auth?.scopes || DEFAULTS.scopes,
|
|
3871
4253
|
pds_url: config.auth?.pds_url || DEFAULTS.pds_url,
|
|
3872
4254
|
admin_url: config.auth?.admin_url || DEFAULTS.admin_url
|
|
3873
4255
|
};
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
this.
|
|
3877
|
-
{
|
|
3878
|
-
projectId: this.projectId,
|
|
3879
|
-
scopes: authConfig.scopes,
|
|
3880
|
-
pdsUrl: authConfig.pds_url,
|
|
3881
|
-
adminUrl: authConfig.admin_url,
|
|
3882
|
-
debug: this.debug
|
|
3883
|
-
},
|
|
3884
|
-
storage,
|
|
3885
|
-
() => this.handleAuthChange()
|
|
3886
|
-
);
|
|
4256
|
+
this.syncUrl = config.auth?.sync_url || deriveSyncUrl(this.authConfig.pds_url);
|
|
4257
|
+
this.rawStorage = config.storage || new LocalStorageAdapter();
|
|
4258
|
+
this.users = this.mode === "sync" && this.projectId ? new UserRegistry(this.rawStorage, this.projectId) : null;
|
|
3887
4259
|
this.rest = new RestClient({
|
|
3888
|
-
baseUrl: authConfig.pds_url,
|
|
4260
|
+
baseUrl: this.authConfig.pds_url,
|
|
3889
4261
|
projectId: this.projectId ?? "",
|
|
3890
4262
|
getToken: (opts) => this.auth.getToken(opts),
|
|
3891
4263
|
log: this.debug ? log : void 0
|
|
3892
4264
|
});
|
|
3893
|
-
if (this.mode === "sync" && this.projectId && this.config.schema?.tables) {
|
|
3894
|
-
this.engine = new SyncEngine({
|
|
3895
|
-
projectId: this.projectId,
|
|
3896
|
-
schema: this.config.schema,
|
|
3897
|
-
wsUrl: syncUrl,
|
|
3898
|
-
getToken: (opts) => this.auth.getToken(opts),
|
|
3899
|
-
fetchSnapshot: (opts) => this.rest.getSnapshot(opts),
|
|
3900
|
-
WebSocketImpl: config.WebSocketImpl,
|
|
3901
|
-
log
|
|
3902
|
-
});
|
|
3903
|
-
this.syncDb = new SyncDb(this.engine, OWN_SUB);
|
|
3904
|
-
this.wireEngineEvents(this.engine);
|
|
3905
|
-
} else {
|
|
3906
|
-
this.engine = null;
|
|
3907
|
-
this.syncDb = null;
|
|
3908
|
-
}
|
|
3909
4265
|
this.restDb = new RestDb(this.rest, this.config.schema);
|
|
4266
|
+
this.binding = this.createBinding(ephemeralLegacyProfile());
|
|
3910
4267
|
this.snapshot = this.buildSnapshot();
|
|
3911
4268
|
}
|
|
3912
4269
|
// -------------------------------------------------------------------
|
|
3913
4270
|
// Public surface
|
|
3914
4271
|
// -------------------------------------------------------------------
|
|
3915
|
-
|
|
4272
|
+
get auth() {
|
|
4273
|
+
return this.binding.auth;
|
|
4274
|
+
}
|
|
4275
|
+
get engine() {
|
|
4276
|
+
return this.binding.engine;
|
|
4277
|
+
}
|
|
4278
|
+
/** The database handle for the active user. Identity changes on switch. */
|
|
3916
4279
|
get db() {
|
|
3917
|
-
if (this.mode === "sync" && this.syncDb) return this.syncDb;
|
|
4280
|
+
if (this.mode === "sync" && this.binding.syncDb) return this.binding.syncDb;
|
|
3918
4281
|
return this.restDb;
|
|
3919
4282
|
}
|
|
3920
|
-
|
|
4283
|
+
get activeUser() {
|
|
4284
|
+
return this.users ? this.binding.profile : null;
|
|
4285
|
+
}
|
|
4286
|
+
/** Bootstrap: version migrations, profile resolution, schema check, auth init. */
|
|
3921
4287
|
async start() {
|
|
3922
4288
|
if (this.started) return;
|
|
3923
4289
|
this.started = true;
|
|
3924
4290
|
try {
|
|
3925
|
-
const updater = createVersionUpdater(this.
|
|
4291
|
+
const updater = createVersionUpdater(this.rawStorage, version, getMigrations());
|
|
3926
4292
|
const result = await updater.checkAndUpdate();
|
|
3927
4293
|
if (result.updated) log(`SDK storage migrated ${result.fromVersion} \u2192 ${result.toVersion}`);
|
|
3928
4294
|
} catch (err) {
|
|
3929
4295
|
log("version updater failed:", err);
|
|
3930
4296
|
}
|
|
3931
|
-
void this.checkSchema().then(() => this.
|
|
3932
|
-
await this.
|
|
3933
|
-
|
|
3934
|
-
|
|
4297
|
+
void this.checkSchema().then(() => this.syncLifecycle());
|
|
4298
|
+
await this.queueProfileOp(async () => {
|
|
4299
|
+
let profile = null;
|
|
4300
|
+
if (this.users) {
|
|
4301
|
+
await this.users.adoptLegacySession();
|
|
4302
|
+
profile = await this.users.resolveActive();
|
|
4303
|
+
if (!profile && this.anonymousEnabled) {
|
|
4304
|
+
profile = await this.users.createAnon();
|
|
4305
|
+
}
|
|
4306
|
+
if (profile) this.users.setActiveId(profile.id);
|
|
4307
|
+
}
|
|
4308
|
+
await this.activateProfile(profile ?? ephemeralLegacyProfile(), { initial: true });
|
|
4309
|
+
await this.refreshUsers();
|
|
4310
|
+
});
|
|
3935
4311
|
}
|
|
3936
|
-
/**
|
|
4312
|
+
/**
|
|
4313
|
+
* Sign out the active user: server-side revoke, wipe the profile's local
|
|
4314
|
+
* data, drop the profile, and fall through to the next (or a fresh
|
|
4315
|
+
* anonymous) user.
|
|
4316
|
+
*/
|
|
3937
4317
|
async signOut() {
|
|
3938
|
-
await this.
|
|
3939
|
-
|
|
4318
|
+
await this.queueProfileOp(async () => {
|
|
4319
|
+
const { profile, auth, engine } = this.binding;
|
|
4320
|
+
this.signOutInProgress = true;
|
|
4321
|
+
try {
|
|
4322
|
+
await auth.signOut();
|
|
4323
|
+
} finally {
|
|
4324
|
+
this.signOutInProgress = false;
|
|
4325
|
+
}
|
|
4326
|
+
this.mounts.clear();
|
|
4327
|
+
try {
|
|
4328
|
+
await engine?.destroyLocal();
|
|
4329
|
+
} catch (err) {
|
|
4330
|
+
log("local data teardown failed:", err);
|
|
4331
|
+
}
|
|
4332
|
+
if (this.users && profile.id !== "default") {
|
|
4333
|
+
await this.users.remove(profile.id);
|
|
4334
|
+
}
|
|
4335
|
+
await this.activateNextProfileLocked();
|
|
4336
|
+
await this.refreshUsers();
|
|
4337
|
+
});
|
|
4338
|
+
}
|
|
4339
|
+
// ---------------- multi-user ----------------
|
|
4340
|
+
/** Switch this tab to another local user. */
|
|
4341
|
+
async switchUser(id) {
|
|
4342
|
+
await this.queueProfileOp(async () => {
|
|
4343
|
+
if (!this.users) throw new Error("multiple users require sync mode with a project id");
|
|
4344
|
+
if (this.binding.profile.id === id) return;
|
|
4345
|
+
const target = await this.users.get(id);
|
|
4346
|
+
if (!target) throw new Error(`unknown user '${id}'`);
|
|
4347
|
+
this.users.setActiveId(id);
|
|
4348
|
+
await this.users.touch(id);
|
|
4349
|
+
await this.activateProfile(target);
|
|
4350
|
+
await this.refreshUsers();
|
|
4351
|
+
});
|
|
4352
|
+
}
|
|
4353
|
+
/** Create a fresh anonymous user and switch to it. */
|
|
4354
|
+
async addUser() {
|
|
4355
|
+
let created = null;
|
|
4356
|
+
await this.queueProfileOp(async () => {
|
|
4357
|
+
if (!this.users) throw new Error("multiple users require sync mode with a project id");
|
|
4358
|
+
created = await this.users.createAnon();
|
|
4359
|
+
this.users.setActiveId(created.id);
|
|
4360
|
+
await this.activateProfile(created);
|
|
4361
|
+
await this.refreshUsers();
|
|
4362
|
+
});
|
|
4363
|
+
return created;
|
|
4364
|
+
}
|
|
4365
|
+
/**
|
|
4366
|
+
* Remove a local user: best-effort server-side revoke, wipe its keyspace
|
|
4367
|
+
* and auth storage, drop the profile. Removing the active user signs out.
|
|
4368
|
+
*/
|
|
4369
|
+
async removeUser(id) {
|
|
4370
|
+
if (this.binding.profile.id === id) {
|
|
4371
|
+
return this.signOut();
|
|
4372
|
+
}
|
|
4373
|
+
await this.queueProfileOp(async () => {
|
|
4374
|
+
if (!this.users) return;
|
|
4375
|
+
const profile = await this.users.get(id);
|
|
4376
|
+
if (!profile) return;
|
|
4377
|
+
await this.disposeProfileData(profile);
|
|
4378
|
+
await this.users.remove(id);
|
|
4379
|
+
await this.refreshUsers();
|
|
4380
|
+
});
|
|
3940
4381
|
}
|
|
3941
4382
|
/** Stop connections and listeners; local data is kept. */
|
|
3942
4383
|
stop() {
|
|
3943
4384
|
this.started = false;
|
|
3944
|
-
this.engine?.stop();
|
|
3945
|
-
for (const fn of this.
|
|
4385
|
+
this.binding.engine?.stop();
|
|
4386
|
+
for (const fn of this.binding.sessionCleanup) {
|
|
3946
4387
|
try {
|
|
3947
4388
|
fn();
|
|
3948
4389
|
} catch {
|
|
3949
4390
|
}
|
|
3950
4391
|
}
|
|
3951
|
-
this.
|
|
4392
|
+
this.binding.sessionCleanup = [];
|
|
3952
4393
|
}
|
|
3953
4394
|
/** Re-run the remote schema status check (dev toolbar). */
|
|
3954
4395
|
async refreshSchemaStatus() {
|
|
3955
4396
|
this.schemaChecked = false;
|
|
3956
4397
|
await this.checkSchema();
|
|
3957
|
-
this.
|
|
4398
|
+
this.syncLifecycle();
|
|
3958
4399
|
}
|
|
3959
4400
|
async listRejected() {
|
|
3960
4401
|
return this.engine?.listRejected() ?? [];
|
|
@@ -3970,13 +4411,14 @@ var BasicClient = class {
|
|
|
3970
4411
|
}
|
|
3971
4412
|
/** Mount a share: separate local keyspace + subscription. */
|
|
3972
4413
|
async mountShare(shareId) {
|
|
3973
|
-
|
|
4414
|
+
const engine = this.engine;
|
|
4415
|
+
if (!engine) throw new Error("shares require sync mode");
|
|
3974
4416
|
const existing = this.mounts.get(shareId);
|
|
3975
4417
|
if (existing) return existing;
|
|
3976
|
-
await
|
|
4418
|
+
await engine.mountShare(shareId);
|
|
3977
4419
|
const handle = {
|
|
3978
4420
|
shareId,
|
|
3979
|
-
db: new SyncDb(
|
|
4421
|
+
db: new SyncDb(engine, shareSubKey(shareId))
|
|
3980
4422
|
};
|
|
3981
4423
|
this.mounts.set(shareId, handle);
|
|
3982
4424
|
this.publish();
|
|
@@ -4000,50 +4442,260 @@ var BasicClient = class {
|
|
|
4000
4442
|
return this.snapshot;
|
|
4001
4443
|
};
|
|
4002
4444
|
// -------------------------------------------------------------------
|
|
4003
|
-
//
|
|
4445
|
+
// Profile binding & transitions
|
|
4004
4446
|
// -------------------------------------------------------------------
|
|
4005
|
-
|
|
4006
|
-
const
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4447
|
+
createBinding(profile) {
|
|
4448
|
+
const storage = profile.storagePrefix ? new PrefixedStorage(this.rawStorage, profile.storagePrefix) : this.rawStorage;
|
|
4449
|
+
const binding = { profile, cleanup: [], sessionCleanup: [] };
|
|
4450
|
+
const auth = new AuthManager(
|
|
4451
|
+
{
|
|
4452
|
+
projectId: this.projectId,
|
|
4453
|
+
scopes: this.authConfig.scopes,
|
|
4454
|
+
pdsUrl: this.authConfig.pds_url,
|
|
4455
|
+
adminUrl: this.authConfig.admin_url,
|
|
4456
|
+
debug: this.debug,
|
|
4457
|
+
instanceKey: profile.storagePrefix
|
|
4458
|
+
},
|
|
4459
|
+
storage,
|
|
4460
|
+
() => {
|
|
4461
|
+
if (this.binding === binding) this.handleAuthChange();
|
|
4462
|
+
}
|
|
4463
|
+
);
|
|
4464
|
+
binding.auth = auth;
|
|
4465
|
+
if (this.mode === "sync" && this.projectId && this.config.schema?.tables) {
|
|
4466
|
+
const engine = new SyncEngine({
|
|
4467
|
+
projectId: this.projectId,
|
|
4468
|
+
schema: this.config.schema,
|
|
4469
|
+
wsUrl: this.syncUrl,
|
|
4470
|
+
getToken: (opts) => auth.getToken(opts),
|
|
4471
|
+
fetchSnapshot: (opts) => this.rest.getSnapshot(opts),
|
|
4472
|
+
WebSocketImpl: this.config.WebSocketImpl,
|
|
4473
|
+
keyspaceId: profile.keyspace,
|
|
4474
|
+
getOwnerDid: () => auth.did,
|
|
4475
|
+
log
|
|
4476
|
+
});
|
|
4477
|
+
binding.engine = engine;
|
|
4478
|
+
binding.syncDb = new SyncDb(engine, OWN_SUB);
|
|
4479
|
+
binding.cleanup.push(
|
|
4480
|
+
engine.on("status", () => this.publish()),
|
|
4481
|
+
engine.on("change", () => this.publish()),
|
|
4482
|
+
engine.on("rejected", ({ rejection }) => {
|
|
4483
|
+
log("op rejected:", rejection.error, rejection.op);
|
|
4484
|
+
this.publish();
|
|
4485
|
+
}),
|
|
4486
|
+
engine.on("revoked", ({ code, message }) => {
|
|
4487
|
+
log("connection revoked:", code, message);
|
|
4488
|
+
void auth.reconcileSession("connection revoked", { forceRefresh: true, throttleMs: 0 }).catch(() => {
|
|
4489
|
+
});
|
|
4490
|
+
this.publish();
|
|
4491
|
+
})
|
|
4492
|
+
);
|
|
4011
4493
|
} else {
|
|
4012
|
-
|
|
4494
|
+
binding.engine = null;
|
|
4495
|
+
binding.syncDb = null;
|
|
4013
4496
|
}
|
|
4497
|
+
return binding;
|
|
4498
|
+
}
|
|
4499
|
+
/**
|
|
4500
|
+
* Bind and boot a profile. Publishes the new binding first so React
|
|
4501
|
+
* subscriptions re-attach to the new db, then tears the old binding down
|
|
4502
|
+
* on the next tick (avoids in-flight live queries hitting a closed store).
|
|
4503
|
+
*/
|
|
4504
|
+
async activateProfile(profile, options) {
|
|
4505
|
+
let old = null;
|
|
4506
|
+
if (options?.initial && this.bindingMatches(profile)) {
|
|
4507
|
+
this.binding.profile = profile;
|
|
4508
|
+
} else {
|
|
4509
|
+
old = this.binding;
|
|
4510
|
+
this.binding = this.createBinding(profile);
|
|
4511
|
+
if (options?.initial) {
|
|
4512
|
+
for (const fn of [...old.cleanup, ...old.sessionCleanup]) fn();
|
|
4513
|
+
old.engine?.stop();
|
|
4514
|
+
old.auth.destroy();
|
|
4515
|
+
old = null;
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
await this.binding.auth.initialize();
|
|
4519
|
+
this.binding.sessionCleanup.push(this.binding.auth.setupNetworkListeners());
|
|
4520
|
+
this.mounts.clear();
|
|
4521
|
+
this.syncLifecycle();
|
|
4014
4522
|
this.publish();
|
|
4523
|
+
if (old) {
|
|
4524
|
+
setTimeout(() => {
|
|
4525
|
+
try {
|
|
4526
|
+
for (const fn of [...old.cleanup, ...old.sessionCleanup]) fn();
|
|
4527
|
+
old.engine?.stop();
|
|
4528
|
+
old.auth.destroy();
|
|
4529
|
+
} catch {
|
|
4530
|
+
}
|
|
4531
|
+
}, 50);
|
|
4532
|
+
}
|
|
4015
4533
|
}
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4534
|
+
bindingMatches(profile) {
|
|
4535
|
+
return this.binding.profile.storagePrefix === profile.storagePrefix && this.binding.profile.keyspace === profile.keyspace;
|
|
4536
|
+
}
|
|
4537
|
+
/** After sign-out/disposal: resume on the next profile or a fresh anon one. */
|
|
4538
|
+
async activateNextProfileLocked() {
|
|
4539
|
+
let next = null;
|
|
4540
|
+
if (this.users) {
|
|
4541
|
+
next = await this.users.resolveActive();
|
|
4542
|
+
if (!next && this.anonymousEnabled) {
|
|
4543
|
+
next = await this.users.createAnon();
|
|
4544
|
+
}
|
|
4545
|
+
if (next) this.users.setActiveId(next.id);
|
|
4021
4546
|
}
|
|
4547
|
+
await this.activateProfile(next ?? ephemeralLegacyProfile());
|
|
4022
4548
|
}
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4549
|
+
/** Wipe a (non-active) profile's local footprint: keyspace dbs + auth keys. */
|
|
4550
|
+
async disposeProfileData(profile) {
|
|
4551
|
+
const storage = profile.storagePrefix ? new PrefixedStorage(this.rawStorage, profile.storagePrefix) : this.rawStorage;
|
|
4552
|
+
try {
|
|
4553
|
+
const refreshToken = await storage.get(STORAGE_KEYS.REFRESH_TOKEN);
|
|
4554
|
+
if (refreshToken) {
|
|
4555
|
+
await fetch(`${this.authConfig.pds_url}/auth/revoke`, {
|
|
4556
|
+
method: "POST",
|
|
4557
|
+
headers: { "Content-Type": "application/json" },
|
|
4558
|
+
body: JSON.stringify({ token: refreshToken, token_type_hint: "refresh_token" })
|
|
4559
|
+
});
|
|
4560
|
+
}
|
|
4561
|
+
} catch {
|
|
4562
|
+
}
|
|
4563
|
+
for (const key of Object.values(STORAGE_KEYS)) {
|
|
4026
4564
|
try {
|
|
4027
|
-
await
|
|
4028
|
-
} catch
|
|
4029
|
-
log("local data teardown failed:", err);
|
|
4565
|
+
await storage.remove(key);
|
|
4566
|
+
} catch {
|
|
4030
4567
|
}
|
|
4031
4568
|
}
|
|
4569
|
+
await this.deleteKeyspaceDatabases(profile.keyspace);
|
|
4570
|
+
}
|
|
4571
|
+
async deleteKeyspaceDatabases(keyspace) {
|
|
4572
|
+
if (!this.projectId) return;
|
|
4573
|
+
const base = keyspace ? `basic-sync:${this.projectId}:${keyspace}` : `basic-sync:${this.projectId}`;
|
|
4574
|
+
try {
|
|
4575
|
+
const idb = globalThis.indexedDB;
|
|
4576
|
+
if (!idb) return;
|
|
4577
|
+
const names = [base];
|
|
4578
|
+
if (typeof idb.databases === "function") {
|
|
4579
|
+
const dbs = await idb.databases();
|
|
4580
|
+
for (const info of dbs) {
|
|
4581
|
+
if (info.name && info.name.startsWith(`${base}:share:`)) names.push(info.name);
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
4584
|
+
for (const name of names) {
|
|
4585
|
+
await new Promise((resolve) => {
|
|
4586
|
+
const req = idb.deleteDatabase(name);
|
|
4587
|
+
req.onsuccess = req.onerror = req.onblocked = () => resolve();
|
|
4588
|
+
});
|
|
4589
|
+
}
|
|
4590
|
+
} catch {
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
// -------------------------------------------------------------------
|
|
4594
|
+
// Orchestration
|
|
4595
|
+
// -------------------------------------------------------------------
|
|
4596
|
+
/** Previous auth status, for transition detection (revoked-latch clearing). */
|
|
4597
|
+
lastAuthStatus = null;
|
|
4598
|
+
handleAuthChange() {
|
|
4599
|
+
const status = this.auth.authStatus;
|
|
4600
|
+
if (status === "authenticated" && this.lastAuthStatus !== "authenticated" && this.engine?.status === "revoked") {
|
|
4601
|
+
this.engine.clearRevoked();
|
|
4602
|
+
}
|
|
4603
|
+
this.lastAuthStatus = status;
|
|
4604
|
+
if (status === "signed_out" && !this.signOutInProgress && this.started) {
|
|
4605
|
+
const profile = this.binding.profile;
|
|
4606
|
+
if (this.users && profile.kind === "account") {
|
|
4607
|
+
void this.queueProfileOp(async () => {
|
|
4608
|
+
if (this.binding.profile.id !== profile.id) return;
|
|
4609
|
+
if (this.binding.auth.authStatus !== "signed_out") return;
|
|
4610
|
+
this.mounts.clear();
|
|
4611
|
+
try {
|
|
4612
|
+
await this.binding.engine?.destroyLocal();
|
|
4613
|
+
} catch {
|
|
4614
|
+
}
|
|
4615
|
+
await this.users.remove(profile.id);
|
|
4616
|
+
await this.activateNextProfileLocked();
|
|
4617
|
+
await this.refreshUsers();
|
|
4618
|
+
});
|
|
4619
|
+
return;
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
this.syncLifecycle();
|
|
4623
|
+
void this.maybeUpgradeProfile();
|
|
4032
4624
|
this.publish();
|
|
4033
4625
|
}
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4626
|
+
/**
|
|
4627
|
+
* Drive the engine from auth + schema state:
|
|
4628
|
+
* - local keyspace opens with no token (anonymous mode / offline cold start)
|
|
4629
|
+
* - connect when a session exists (recovering counts — the connection
|
|
4630
|
+
* retries token acquisition itself)
|
|
4631
|
+
* - reauth_required pauses the connection, keeps local data usable
|
|
4632
|
+
*/
|
|
4633
|
+
syncLifecycle() {
|
|
4634
|
+
const { engine, auth, profile } = this.binding;
|
|
4635
|
+
if (!engine || !this.started) return;
|
|
4636
|
+
const status = auth.authStatus;
|
|
4637
|
+
if (status === "reauth_required") {
|
|
4638
|
+
engine.pause();
|
|
4639
|
+
return;
|
|
4640
|
+
}
|
|
4641
|
+
const localAllowed = this.anonymousEnabled || auth.isSignedIn || profile.kind === "account";
|
|
4642
|
+
if (!localAllowed) return;
|
|
4643
|
+
const schemaLocallyUsable = this.devInfo === null || this.devInfo.status !== "invalid";
|
|
4644
|
+
if (!schemaLocallyUsable) return;
|
|
4645
|
+
void engine.openLocal().then(() => {
|
|
4646
|
+
if (this.binding.engine !== engine) return;
|
|
4647
|
+
if (this.syncEnabled && auth.isSignedIn && auth.authStatus !== "reauth_required") {
|
|
4648
|
+
return engine.connect();
|
|
4649
|
+
}
|
|
4650
|
+
}).catch((err) => log("sync lifecycle failed:", err));
|
|
4651
|
+
}
|
|
4652
|
+
/**
|
|
4653
|
+
* After sign-in: bind the account identity to the active profile
|
|
4654
|
+
* (anonymous → account upgrade) and dedupe against an existing profile
|
|
4655
|
+
* for the same DID.
|
|
4656
|
+
*/
|
|
4657
|
+
async maybeUpgradeProfile() {
|
|
4658
|
+
const { profile, auth } = this.binding;
|
|
4659
|
+
if (!this.users || auth.authStatus !== "authenticated" || !auth.did) return;
|
|
4660
|
+
if (profile.id === "default") return;
|
|
4661
|
+
const did = auth.did;
|
|
4662
|
+
const user = auth.user;
|
|
4663
|
+
const needsUpdate = profile.did !== did || profile.kind !== "account" || profile.email !== (user?.email ?? profile.email) || profile.name !== (user?.name ?? profile.name);
|
|
4664
|
+
if (!needsUpdate) return;
|
|
4665
|
+
await this.queueProfileOp(async () => {
|
|
4666
|
+
if (this.binding.profile.id !== profile.id) return;
|
|
4667
|
+
if (!this.users) return;
|
|
4668
|
+
const existing = await this.users.findByDid(did);
|
|
4669
|
+
if (existing && existing.id !== profile.id) {
|
|
4670
|
+
log(`deduping user profiles for ${did}: dropping ${existing.id}`);
|
|
4671
|
+
await this.disposeProfileData(existing);
|
|
4672
|
+
await this.users.remove(existing.id);
|
|
4673
|
+
}
|
|
4674
|
+
const updated = await this.users.update(profile.id, {
|
|
4675
|
+
kind: "account",
|
|
4676
|
+
did,
|
|
4677
|
+
email: user?.email ?? profile.email ?? null,
|
|
4678
|
+
name: user?.name ?? profile.name ?? null,
|
|
4679
|
+
picture: user?.picture ?? profile.picture ?? null,
|
|
4680
|
+
lastActiveAt: Date.now()
|
|
4044
4681
|
});
|
|
4045
|
-
|
|
4682
|
+
if (updated) {
|
|
4683
|
+
this.binding.profile = updated;
|
|
4684
|
+
}
|
|
4685
|
+
await this.refreshUsers();
|
|
4686
|
+
});
|
|
4687
|
+
}
|
|
4688
|
+
async refreshUsers() {
|
|
4689
|
+
if (this.users) {
|
|
4690
|
+
this.usersCache = await this.users.list();
|
|
4691
|
+
}
|
|
4692
|
+
this.publish();
|
|
4693
|
+
}
|
|
4694
|
+
queueProfileOp(task) {
|
|
4695
|
+
this.profileOps = this.profileOps.then(task).catch((err) => {
|
|
4696
|
+
log("profile operation failed:", err);
|
|
4046
4697
|
});
|
|
4698
|
+
return this.profileOps;
|
|
4047
4699
|
}
|
|
4048
4700
|
async checkSchema() {
|
|
4049
4701
|
if (this.schemaChecked) return;
|
|
@@ -4087,7 +4739,7 @@ var BasicClient = class {
|
|
|
4087
4739
|
this.syncEnabled = result.schemaStatus.valid || remoteCheckInconclusive && locallyPublishable;
|
|
4088
4740
|
if (!result.schemaStatus.valid) {
|
|
4089
4741
|
if (status === "unpublished") {
|
|
4090
|
-
log("Schema not published (version 0) \u2014 sync is disabled
|
|
4742
|
+
log("Schema not published (version 0) \u2014 sync is disabled, local-only mode.");
|
|
4091
4743
|
} else if (remoteCheckInconclusive && locallyPublishable) {
|
|
4092
4744
|
log("Schema registry check failed \u2014 proceeding with the local schema (offline-first).");
|
|
4093
4745
|
}
|
|
@@ -4108,19 +4760,23 @@ var BasicClient = class {
|
|
|
4108
4760
|
this.publish();
|
|
4109
4761
|
}
|
|
4110
4762
|
buildSnapshot() {
|
|
4763
|
+
const { auth, engine, profile } = this.binding;
|
|
4111
4764
|
return {
|
|
4112
|
-
isReady:
|
|
4113
|
-
isSignedIn:
|
|
4114
|
-
authStatus:
|
|
4115
|
-
authErrorCode:
|
|
4116
|
-
user:
|
|
4117
|
-
did:
|
|
4118
|
-
scope:
|
|
4119
|
-
syncStatus:
|
|
4120
|
-
pendingCount:
|
|
4765
|
+
isReady: auth.isAuthReady,
|
|
4766
|
+
isSignedIn: auth.isSignedIn,
|
|
4767
|
+
authStatus: auth.authStatus,
|
|
4768
|
+
authErrorCode: auth.authErrorCode,
|
|
4769
|
+
user: auth.user,
|
|
4770
|
+
did: auth.did,
|
|
4771
|
+
scope: auth.tokenScope,
|
|
4772
|
+
syncStatus: engine?.status ?? "idle",
|
|
4773
|
+
pendingCount: engine?.pendingCount ?? 0,
|
|
4121
4774
|
syncEnabled: this.syncEnabled,
|
|
4122
4775
|
devInfo: this.devInfo,
|
|
4123
|
-
mode: this.mode
|
|
4776
|
+
mode: this.mode,
|
|
4777
|
+
users: this.usersCache,
|
|
4778
|
+
activeUser: this.users ? profile : null,
|
|
4779
|
+
isAnonymous: this.users ? profile.kind === "anon" && !auth.isSignedIn : false
|
|
4124
4780
|
};
|
|
4125
4781
|
}
|
|
4126
4782
|
publish() {
|
|
@@ -4151,6 +4807,7 @@ function BasicProvider({
|
|
|
4151
4807
|
storage,
|
|
4152
4808
|
debug = false,
|
|
4153
4809
|
mode = "sync",
|
|
4810
|
+
anonymous = true,
|
|
4154
4811
|
devToolbar = false,
|
|
4155
4812
|
renderWhileLoading = false
|
|
4156
4813
|
}) {
|
|
@@ -4162,7 +4819,8 @@ function BasicProvider({
|
|
|
4162
4819
|
auth,
|
|
4163
4820
|
storage,
|
|
4164
4821
|
debug,
|
|
4165
|
-
mode
|
|
4822
|
+
mode,
|
|
4823
|
+
anonymous
|
|
4166
4824
|
});
|
|
4167
4825
|
}
|
|
4168
4826
|
const client = clientRef.current;
|
|
@@ -4194,6 +4852,7 @@ init_BasicDevToolbar();
|
|
|
4194
4852
|
NotAuthenticatedError,
|
|
4195
4853
|
OWN_SUB,
|
|
4196
4854
|
PROTOCOL_VERSION,
|
|
4855
|
+
PrefixedStorage,
|
|
4197
4856
|
RestClient,
|
|
4198
4857
|
RestDb,
|
|
4199
4858
|
RestError,
|
|
@@ -4202,6 +4861,7 @@ init_BasicDevToolbar();
|
|
|
4202
4861
|
SyncDb,
|
|
4203
4862
|
SyncEngine,
|
|
4204
4863
|
SyncStore,
|
|
4864
|
+
UserRegistry,
|
|
4205
4865
|
applyOpToData,
|
|
4206
4866
|
createBasicClient,
|
|
4207
4867
|
isAuthError,
|
|
@@ -4220,6 +4880,7 @@ init_BasicDevToolbar();
|
|
|
4220
4880
|
useQuery,
|
|
4221
4881
|
useShare,
|
|
4222
4882
|
useShares,
|
|
4223
|
-
useSyncStatus
|
|
4883
|
+
useSyncStatus,
|
|
4884
|
+
useUsers
|
|
4224
4885
|
});
|
|
4225
4886
|
//# sourceMappingURL=index.js.map
|