@basictech/react 0.8.0-beta.4 → 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 +65 -0
- package/dist/index.d.mts +1258 -285
- package/dist/index.d.ts +1258 -285
- package/dist/index.js +2898 -1425
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2858 -1418
- package/dist/index.mjs.map +1 -1
- package/package.json +27 -8
- package/readme.md +331 -214
- package/.turbo/turbo-build.log +0 -24
- package/AUTH_IMPLEMENTATION_GUIDE.md +0 -2011
- package/src/AuthContext.tsx +0 -591
- package/src/config.ts +0 -9
- package/src/context.tsx +0 -122
- package/src/core/auth/AuthManager.ts +0 -1371
- package/src/core/db/RemoteCollection.ts +0 -308
- package/src/core/db/RemoteDB.ts +0 -40
- package/src/core/db/index.ts +0 -7
- package/src/core/db/types.ts +0 -140
- package/src/dev/BasicDevToolbar.tsx +0 -665
- package/src/index.ts +0 -36
- package/src/sync/index.ts +0 -288
- package/src/sync/syncProtocol.js +0 -291
- package/src/sync/tokenRegistry.ts +0 -20
- package/src/updater/updateMigrations.ts +0 -22
- package/src/updater/versionUpdater.ts +0 -153
- package/src/utils/network.ts +0 -135
- package/src/utils/normalizeClientId.ts +0 -22
- package/src/utils/resolveDid.ts +0 -101
- package/src/utils/schema.ts +0 -119
- package/src/utils/storage.ts +0 -67
- package/tsconfig.json +0 -9
- package/tsup.config.ts +0 -11
package/dist/index.js
CHANGED
|
@@ -30,6 +30,16 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
));
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
|
+
// src/react/context.ts
|
|
34
|
+
var import_react, BasicClientContext;
|
|
35
|
+
var init_context = __esm({
|
|
36
|
+
"src/react/context.ts"() {
|
|
37
|
+
"use strict";
|
|
38
|
+
import_react = require("react");
|
|
39
|
+
BasicClientContext = (0, import_react.createContext)(null);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
33
43
|
// src/config.ts
|
|
34
44
|
var log;
|
|
35
45
|
var init_config = __esm({
|
|
@@ -46,242 +56,11 @@ var init_config = __esm({
|
|
|
46
56
|
}
|
|
47
57
|
});
|
|
48
58
|
|
|
49
|
-
// src/sync/tokenRegistry.ts
|
|
50
|
-
function setTokenGetter(url, fn) {
|
|
51
|
-
registry.set(url, fn);
|
|
52
|
-
}
|
|
53
|
-
function getTokenGetter(url) {
|
|
54
|
-
return registry.get(url);
|
|
55
|
-
}
|
|
56
|
-
var registry;
|
|
57
|
-
var init_tokenRegistry = __esm({
|
|
58
|
-
"src/sync/tokenRegistry.ts"() {
|
|
59
|
-
"use strict";
|
|
60
|
-
registry = /* @__PURE__ */ new Map();
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// src/sync/syncProtocol.js
|
|
65
|
-
var syncProtocol_exports = {};
|
|
66
|
-
__export(syncProtocol_exports, {
|
|
67
|
-
syncProtocol: () => syncProtocol
|
|
68
|
-
});
|
|
69
|
-
function decodeJwtExp(token) {
|
|
70
|
-
try {
|
|
71
|
-
var parts = token.split(".");
|
|
72
|
-
if (parts.length !== 3) return null;
|
|
73
|
-
var payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
|
|
74
|
-
return typeof payload.exp === "number" ? payload.exp : null;
|
|
75
|
-
} catch (_) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
var import_dexie, syncProtocol;
|
|
80
|
-
var init_syncProtocol = __esm({
|
|
81
|
-
"src/sync/syncProtocol.js"() {
|
|
82
|
-
"use strict";
|
|
83
|
-
"use client";
|
|
84
|
-
import_dexie = require("dexie");
|
|
85
|
-
init_config();
|
|
86
|
-
init_tokenRegistry();
|
|
87
|
-
syncProtocol = function() {
|
|
88
|
-
log("Initializing syncProtocol");
|
|
89
|
-
var RECONNECT_DELAY = 5e3;
|
|
90
|
-
var TOKEN_REFRESH_BUFFER = 60;
|
|
91
|
-
import_dexie.Dexie.Syncable.registerSyncProtocol("websocket", {
|
|
92
|
-
sync: function(context, url, options, baseRevision, syncedRevision, changes, partial, applyRemoteChanges, onChangesAccepted, onSuccess, onError) {
|
|
93
|
-
var requestId = 0;
|
|
94
|
-
var acceptCallbacks = {};
|
|
95
|
-
var refreshTimer = null;
|
|
96
|
-
var pendingTokenUpdate = null;
|
|
97
|
-
log("Connecting to", url);
|
|
98
|
-
var ws = new WebSocket(url);
|
|
99
|
-
function sendChanges(changes2, baseRevision2, partial2, onChangesAccepted2) {
|
|
100
|
-
log("sendChanges", changes2.length, baseRevision2);
|
|
101
|
-
++requestId;
|
|
102
|
-
acceptCallbacks[requestId.toString()] = onChangesAccepted2;
|
|
103
|
-
ws.send(
|
|
104
|
-
JSON.stringify({
|
|
105
|
-
type: "changes",
|
|
106
|
-
changes: changes2,
|
|
107
|
-
partial: partial2,
|
|
108
|
-
baseRevision: baseRevision2,
|
|
109
|
-
requestId
|
|
110
|
-
})
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
function clearRefreshTimer() {
|
|
114
|
-
if (refreshTimer) {
|
|
115
|
-
clearTimeout(refreshTimer);
|
|
116
|
-
refreshTimer = null;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
function sendTokenUpdate(token) {
|
|
120
|
-
if (ws.readyState !== WebSocket.OPEN) return false;
|
|
121
|
-
pendingTokenUpdate = token;
|
|
122
|
-
ws.send(JSON.stringify({ type: "tokenUpdate", authToken: token }));
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
function resolveGetToken() {
|
|
126
|
-
var fn = getTokenGetter(url);
|
|
127
|
-
if (!fn) throw new Error("No token getter registered for " + url);
|
|
128
|
-
return fn;
|
|
129
|
-
}
|
|
130
|
-
function scheduleTokenRefresh(tokenStr) {
|
|
131
|
-
clearRefreshTimer();
|
|
132
|
-
var exp = decodeJwtExp(tokenStr);
|
|
133
|
-
if (!exp) return;
|
|
134
|
-
var msUntilRefresh = (exp - TOKEN_REFRESH_BUFFER) * 1e3 - Date.now();
|
|
135
|
-
if (msUntilRefresh <= 0) return;
|
|
136
|
-
log("Scheduling proactive token refresh in", Math.round(msUntilRefresh / 1e3), "s");
|
|
137
|
-
refreshTimer = setTimeout(async function() {
|
|
138
|
-
try {
|
|
139
|
-
var newToken = await resolveGetToken()({ forceRefresh: true });
|
|
140
|
-
if (sendTokenUpdate(newToken)) {
|
|
141
|
-
log("Sending tokenUpdate on existing WebSocket");
|
|
142
|
-
}
|
|
143
|
-
} catch (err) {
|
|
144
|
-
log("Proactive token refresh failed (non-fatal):", err);
|
|
145
|
-
}
|
|
146
|
-
}, msUntilRefresh);
|
|
147
|
-
}
|
|
148
|
-
ws.onopen = async function(event) {
|
|
149
|
-
try {
|
|
150
|
-
var token = await resolveGetToken()();
|
|
151
|
-
log("Opening socket - sending clientIdentity", context.clientIdentity);
|
|
152
|
-
ws.send(
|
|
153
|
-
JSON.stringify({
|
|
154
|
-
type: "clientIdentity",
|
|
155
|
-
clientIdentity: context.clientIdentity || null,
|
|
156
|
-
authToken: token,
|
|
157
|
-
schema: options.schema
|
|
158
|
-
})
|
|
159
|
-
);
|
|
160
|
-
scheduleTokenRefresh(token);
|
|
161
|
-
} catch (err) {
|
|
162
|
-
log("Failed to get token for WebSocket:", err);
|
|
163
|
-
ws.close();
|
|
164
|
-
onError("Authentication failed: " + (err.message || err), RECONNECT_DELAY);
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
function handleVisibilityResume() {
|
|
168
|
-
if (document.visibilityState === "visible" && ws.readyState === WebSocket.OPEN) {
|
|
169
|
-
log("Page became visible - refreshing token for WebSocket");
|
|
170
|
-
resolveGetToken()({ forceRefresh: true }).then(function(newToken) {
|
|
171
|
-
sendTokenUpdate(newToken);
|
|
172
|
-
}).catch(function(err) {
|
|
173
|
-
log("Token refresh on visibility resume failed:", err);
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
if (typeof document !== "undefined") {
|
|
178
|
-
document.addEventListener("visibilitychange", handleVisibilityResume);
|
|
179
|
-
}
|
|
180
|
-
function cleanupVisibilityListener() {
|
|
181
|
-
if (typeof document !== "undefined") {
|
|
182
|
-
document.removeEventListener("visibilitychange", handleVisibilityResume);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
ws.onerror = function(event) {
|
|
186
|
-
clearRefreshTimer();
|
|
187
|
-
cleanupVisibilityListener();
|
|
188
|
-
ws.close();
|
|
189
|
-
log("ws.onerror", event);
|
|
190
|
-
onError(event?.message, RECONNECT_DELAY);
|
|
191
|
-
};
|
|
192
|
-
ws.onclose = function(event) {
|
|
193
|
-
clearRefreshTimer();
|
|
194
|
-
cleanupVisibilityListener();
|
|
195
|
-
onError("Socket closed: " + event.reason, RECONNECT_DELAY);
|
|
196
|
-
};
|
|
197
|
-
var isFirstRound = true;
|
|
198
|
-
ws.onmessage = function(event) {
|
|
199
|
-
try {
|
|
200
|
-
var requestFromServer = JSON.parse(event.data);
|
|
201
|
-
log("requestFromServer", requestFromServer, { isFirstRound });
|
|
202
|
-
if (requestFromServer.type == "clientIdentity") {
|
|
203
|
-
context.clientIdentity = requestFromServer.clientIdentity;
|
|
204
|
-
context.save();
|
|
205
|
-
sendChanges(changes, baseRevision, partial, onChangesAccepted);
|
|
206
|
-
ws.send(
|
|
207
|
-
JSON.stringify({
|
|
208
|
-
type: "subscribe",
|
|
209
|
-
syncedRevision
|
|
210
|
-
})
|
|
211
|
-
);
|
|
212
|
-
} else if (requestFromServer.type == "changes") {
|
|
213
|
-
applyRemoteChanges(
|
|
214
|
-
requestFromServer.changes,
|
|
215
|
-
requestFromServer.currentRevision,
|
|
216
|
-
requestFromServer.partial
|
|
217
|
-
);
|
|
218
|
-
if (isFirstRound && !requestFromServer.partial) {
|
|
219
|
-
onSuccess({
|
|
220
|
-
// Specify a react function that will react on additional client changes
|
|
221
|
-
react: function(changes2, baseRevision2, partial2, onChangesAccepted2) {
|
|
222
|
-
sendChanges(
|
|
223
|
-
changes2,
|
|
224
|
-
baseRevision2,
|
|
225
|
-
partial2,
|
|
226
|
-
onChangesAccepted2
|
|
227
|
-
);
|
|
228
|
-
},
|
|
229
|
-
disconnect: function() {
|
|
230
|
-
clearRefreshTimer();
|
|
231
|
-
cleanupVisibilityListener();
|
|
232
|
-
ws.close();
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
isFirstRound = false;
|
|
236
|
-
}
|
|
237
|
-
} else if (requestFromServer.type == "tokenUpdateAck") {
|
|
238
|
-
if (requestFromServer.ok) {
|
|
239
|
-
scheduleTokenRefresh(requestFromServer.authToken || pendingTokenUpdate);
|
|
240
|
-
pendingTokenUpdate = null;
|
|
241
|
-
} else {
|
|
242
|
-
log("tokenUpdate rejected by server:", requestFromServer.code || requestFromServer.message);
|
|
243
|
-
pendingTokenUpdate = null;
|
|
244
|
-
ws.close(4001, requestFromServer.code || "token_update_failed");
|
|
245
|
-
onError(
|
|
246
|
-
requestFromServer.message || "Authentication refresh failed",
|
|
247
|
-
RECONNECT_DELAY
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
} else if (requestFromServer.type == "ack") {
|
|
251
|
-
var requestId2 = requestFromServer.requestId;
|
|
252
|
-
var acceptCallback = acceptCallbacks[requestId2.toString()];
|
|
253
|
-
acceptCallback();
|
|
254
|
-
delete acceptCallbacks[requestId2.toString()];
|
|
255
|
-
} else if (requestFromServer.type == "error") {
|
|
256
|
-
ws.close();
|
|
257
|
-
if (requestFromServer.code === "TOKEN_EXPIRED" || requestFromServer.code === "UNAUTHORIZED") {
|
|
258
|
-
log("Auth error from server, will reconnect with fresh token:", requestFromServer.message);
|
|
259
|
-
onError(requestFromServer.message, RECONNECT_DELAY);
|
|
260
|
-
} else {
|
|
261
|
-
onError(requestFromServer.message, Infinity);
|
|
262
|
-
}
|
|
263
|
-
} else {
|
|
264
|
-
log("unknown message", requestFromServer);
|
|
265
|
-
ws.close();
|
|
266
|
-
onError("unknown message", Infinity);
|
|
267
|
-
}
|
|
268
|
-
} catch (e) {
|
|
269
|
-
ws.close();
|
|
270
|
-
log("caught error", e);
|
|
271
|
-
onError(e, Infinity);
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
|
|
280
59
|
// package.json
|
|
281
60
|
var version;
|
|
282
61
|
var init_package = __esm({
|
|
283
62
|
"package.json"() {
|
|
284
|
-
version = "0.
|
|
63
|
+
version = "0.9.0-beta.1";
|
|
285
64
|
}
|
|
286
65
|
});
|
|
287
66
|
|
|
@@ -369,24 +148,6 @@ function cleanOAuthParamsFromUrl() {
|
|
|
369
148
|
log("Cleaned OAuth parameters from URL");
|
|
370
149
|
}
|
|
371
150
|
}
|
|
372
|
-
function getSyncStatus(statusCode) {
|
|
373
|
-
switch (statusCode) {
|
|
374
|
-
case -1:
|
|
375
|
-
return "ERROR";
|
|
376
|
-
case 0:
|
|
377
|
-
return "OFFLINE";
|
|
378
|
-
case 1:
|
|
379
|
-
return "CONNECTING";
|
|
380
|
-
case 2:
|
|
381
|
-
return "ONLINE";
|
|
382
|
-
case 3:
|
|
383
|
-
return "SYNCING";
|
|
384
|
-
case 4:
|
|
385
|
-
return "ERROR_WILL_RETRY";
|
|
386
|
-
default:
|
|
387
|
-
return "UNKNOWN";
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
151
|
var import_semver;
|
|
391
152
|
var init_network = __esm({
|
|
392
153
|
"src/utils/network.ts"() {
|
|
@@ -397,59 +158,185 @@ var init_network = __esm({
|
|
|
397
158
|
}
|
|
398
159
|
});
|
|
399
160
|
|
|
400
|
-
// src/
|
|
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
|
+
}
|
|
178
|
+
function useBasicClient() {
|
|
179
|
+
const client = (0, import_react2.useContext)(BasicClientContext);
|
|
180
|
+
if (!client) {
|
|
181
|
+
throw new Error("useBasic must be used within a <BasicProvider>");
|
|
182
|
+
}
|
|
183
|
+
return client;
|
|
184
|
+
}
|
|
185
|
+
function useClientSnapshot(client) {
|
|
186
|
+
return (0, import_react2.useSyncExternalStore)(client.subscribe, client.getSnapshot, client.getSnapshot);
|
|
187
|
+
}
|
|
188
|
+
function useAuth() {
|
|
189
|
+
const client = useBasicClient();
|
|
190
|
+
const snapshot = useClientSnapshot(client);
|
|
191
|
+
return (0, import_react2.useMemo)(
|
|
192
|
+
() => ({
|
|
193
|
+
isReady: snapshot.isReady,
|
|
194
|
+
isSignedIn: snapshot.isSignedIn,
|
|
195
|
+
isAnonymous: snapshot.isAnonymous,
|
|
196
|
+
status: snapshot.authStatus,
|
|
197
|
+
errorCode: snapshot.authErrorCode,
|
|
198
|
+
user: snapshot.user,
|
|
199
|
+
did: snapshot.did,
|
|
200
|
+
scope: snapshot.scope,
|
|
201
|
+
hasScope: (s) => client.auth.hasScope(s),
|
|
202
|
+
missingScopes: () => client.auth.missingScopes(),
|
|
203
|
+
signIn: (redirectUri) => client.auth.signIn(redirectUri),
|
|
204
|
+
signInWithHandle: (handle) => client.auth.signInWithHandle(handle),
|
|
205
|
+
signInWithCode: (code, state) => client.auth.signInWithCode(code, state),
|
|
206
|
+
signOut: () => client.signOut(),
|
|
207
|
+
getToken: (options) => client.auth.getToken(options),
|
|
208
|
+
getSignInUrl: (redirectUri) => client.auth.getSignInUrl(redirectUri)
|
|
209
|
+
}),
|
|
210
|
+
[client, snapshot]
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
function useDb() {
|
|
214
|
+
const client = useBasicClient();
|
|
215
|
+
return client.db;
|
|
216
|
+
}
|
|
217
|
+
function useSyncStatus() {
|
|
218
|
+
const client = useBasicClient();
|
|
219
|
+
const snapshot = useClientSnapshot(client);
|
|
220
|
+
return (0, import_react2.useMemo)(
|
|
221
|
+
() => ({
|
|
222
|
+
status: snapshot.syncStatus,
|
|
223
|
+
enabled: snapshot.syncEnabled,
|
|
224
|
+
pendingCount: snapshot.pendingCount,
|
|
225
|
+
listRejected: () => client.listRejected(),
|
|
226
|
+
clearRejected: () => client.clearRejected()
|
|
227
|
+
}),
|
|
228
|
+
[client, snapshot]
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
function useShares() {
|
|
232
|
+
const client = useBasicClient();
|
|
233
|
+
const snapshot = useClientSnapshot(client);
|
|
234
|
+
const [state, setState] = (0, import_react2.useState)({ granted: [], received: [], isLoading: false, error: null });
|
|
235
|
+
const isSignedIn = snapshot.isSignedIn && snapshot.authStatus === "authenticated";
|
|
236
|
+
const refresh = (0, import_react2.useMemo)(
|
|
237
|
+
() => async () => {
|
|
238
|
+
setState((s) => ({ ...s, isLoading: true, error: null }));
|
|
239
|
+
try {
|
|
240
|
+
const { granted, received } = await client.listShares();
|
|
241
|
+
setState({ granted, received, isLoading: false, error: null });
|
|
242
|
+
} catch (err) {
|
|
243
|
+
setState((s) => ({
|
|
244
|
+
...s,
|
|
245
|
+
isLoading: false,
|
|
246
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
[client]
|
|
251
|
+
);
|
|
252
|
+
const activeUserId = snapshot.activeUser?.id ?? null;
|
|
253
|
+
(0, import_react2.useEffect)(() => {
|
|
254
|
+
if (isSignedIn) void refresh();
|
|
255
|
+
else setState({ granted: [], received: [], isLoading: false, error: null });
|
|
256
|
+
}, [isSignedIn, refresh, activeUserId]);
|
|
257
|
+
return { ...state, refresh };
|
|
258
|
+
}
|
|
259
|
+
function useShare(shareId) {
|
|
260
|
+
const client = useBasicClient();
|
|
261
|
+
const snapshot = useClientSnapshot(client);
|
|
262
|
+
const [handle, setHandle] = (0, import_react2.useState)(null);
|
|
263
|
+
const [error, setError] = (0, import_react2.useState)(null);
|
|
264
|
+
const [revoked, setRevoked] = (0, import_react2.useState)(false);
|
|
265
|
+
const canMount = !!shareId && snapshot.isSignedIn && snapshot.authStatus !== "reauth_required";
|
|
266
|
+
const activeUserId = snapshot.activeUser?.id ?? null;
|
|
267
|
+
(0, import_react2.useEffect)(() => {
|
|
268
|
+
if (!canMount || !shareId) return;
|
|
269
|
+
let cancelled = false;
|
|
270
|
+
setError(null);
|
|
271
|
+
setRevoked(false);
|
|
272
|
+
client.mountShare(shareId).then((h) => {
|
|
273
|
+
if (!cancelled) setHandle(h);
|
|
274
|
+
}).catch((err) => {
|
|
275
|
+
if (!cancelled) setError(err instanceof Error ? err : new Error(String(err)));
|
|
276
|
+
});
|
|
277
|
+
const offSubError = client.engine?.on("suberror", ({ sub, code }) => {
|
|
278
|
+
if (sub === `share:${shareId}` && (code === "SHARE_REVOKED" || code === "CONNECTION_REVOKED")) {
|
|
279
|
+
setRevoked(true);
|
|
280
|
+
setHandle(null);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
return () => {
|
|
284
|
+
cancelled = true;
|
|
285
|
+
offSubError?.();
|
|
286
|
+
setHandle(null);
|
|
287
|
+
void client.unmountShare(shareId).catch(() => {
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
}, [client, shareId, canMount, activeUserId]);
|
|
291
|
+
return {
|
|
292
|
+
db: handle?.db ?? null,
|
|
293
|
+
status: revoked ? "revoked" : error ? "error" : handle ? "mounted" : "mounting",
|
|
294
|
+
error
|
|
295
|
+
};
|
|
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
|
+
}
|
|
401
312
|
function useBasic() {
|
|
402
|
-
|
|
313
|
+
const client = useBasicClient();
|
|
314
|
+
const snapshot = useClientSnapshot(client);
|
|
315
|
+
const auth = useAuth();
|
|
316
|
+
const sync = useSyncStatus();
|
|
317
|
+
return (0, import_react2.useMemo)(
|
|
318
|
+
() => ({
|
|
319
|
+
...auth,
|
|
320
|
+
db: client.db,
|
|
321
|
+
sync,
|
|
322
|
+
users: snapshot.users,
|
|
323
|
+
activeUser: snapshot.activeUser,
|
|
324
|
+
devInfo: snapshot.devInfo,
|
|
325
|
+
refreshSchemaStatus: () => client.refreshSchemaStatus(),
|
|
326
|
+
client
|
|
327
|
+
}),
|
|
328
|
+
[client, snapshot, auth, sync]
|
|
329
|
+
);
|
|
403
330
|
}
|
|
404
|
-
var
|
|
405
|
-
var
|
|
406
|
-
"src/
|
|
331
|
+
var import_react2, import_dexie_react_hooks, noopSubscribe;
|
|
332
|
+
var init_hooks = __esm({
|
|
333
|
+
"src/react/hooks.ts"() {
|
|
407
334
|
"use strict";
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
DBStatus2["CONNECTING"] = "CONNECTING";
|
|
413
|
-
DBStatus2["ONLINE"] = "ONLINE";
|
|
414
|
-
DBStatus2["SYNCING"] = "SYNCING";
|
|
415
|
-
DBStatus2["ERROR"] = "ERROR";
|
|
416
|
-
DBStatus2["ERROR_WILL_RETRY"] = "ERROR_WILL_RETRY";
|
|
417
|
-
DBStatus2["ERROR_TOKEN_EXPIRED"] = "ERROR_TOKEN_EXPIRED";
|
|
418
|
-
return DBStatus2;
|
|
419
|
-
})(DBStatus || {});
|
|
420
|
-
noDb = {
|
|
421
|
-
collection: () => {
|
|
422
|
-
throw new Error("no basicdb found - initialization failed. double check your schema.");
|
|
423
|
-
}
|
|
335
|
+
import_react2 = require("react");
|
|
336
|
+
import_dexie_react_hooks = require("dexie-react-hooks");
|
|
337
|
+
init_context();
|
|
338
|
+
noopSubscribe = () => () => {
|
|
424
339
|
};
|
|
425
|
-
BasicContext = (0, import_react.createContext)({
|
|
426
|
-
isReady: false,
|
|
427
|
-
isSignedIn: false,
|
|
428
|
-
authStatus: "bootstrapping",
|
|
429
|
-
authErrorCode: null,
|
|
430
|
-
user: null,
|
|
431
|
-
did: null,
|
|
432
|
-
scope: null,
|
|
433
|
-
hasScope: () => false,
|
|
434
|
-
missingScopes: () => [],
|
|
435
|
-
signIn: () => Promise.resolve(),
|
|
436
|
-
signInWithHandle: () => Promise.resolve(),
|
|
437
|
-
signOut: () => Promise.resolve(),
|
|
438
|
-
signInWithCode: () => Promise.resolve({ success: false }),
|
|
439
|
-
getToken: (_options) => Promise.reject(new Error("no token")),
|
|
440
|
-
getSignInUrl: () => Promise.resolve(""),
|
|
441
|
-
db: noDb,
|
|
442
|
-
dbStatus: "LOADING" /* LOADING */,
|
|
443
|
-
dbMode: "sync",
|
|
444
|
-
devInfo: null,
|
|
445
|
-
refreshSchemaStatus: async () => {
|
|
446
|
-
},
|
|
447
|
-
isAuthReady: false,
|
|
448
|
-
signin: () => Promise.resolve(),
|
|
449
|
-
signout: () => Promise.resolve(),
|
|
450
|
-
signinWithCode: () => Promise.resolve({ success: false }),
|
|
451
|
-
getSignInLink: () => Promise.resolve("")
|
|
452
|
-
});
|
|
453
340
|
}
|
|
454
341
|
});
|
|
455
342
|
|
|
@@ -463,11 +350,12 @@ function toneForAuth(isReady, isSignedIn) {
|
|
|
463
350
|
if (isSignedIn) return "ok";
|
|
464
351
|
return "warn";
|
|
465
352
|
}
|
|
466
|
-
function
|
|
467
|
-
if (
|
|
468
|
-
if (
|
|
469
|
-
if (
|
|
470
|
-
if (
|
|
353
|
+
function toneForSync(mode, status) {
|
|
354
|
+
if (mode === "rest") return "muted";
|
|
355
|
+
if (status === "online") return "ok";
|
|
356
|
+
if (status === "connecting") return "warn";
|
|
357
|
+
if (status === "offline" || status === "idle" || status === "stopped" || status === "local")
|
|
358
|
+
return "muted";
|
|
471
359
|
return "bad";
|
|
472
360
|
}
|
|
473
361
|
function toneForSchema(info) {
|
|
@@ -477,24 +365,24 @@ function toneForSchema(info) {
|
|
|
477
365
|
if (info.status === "no_schema") return "muted";
|
|
478
366
|
return "bad";
|
|
479
367
|
}
|
|
480
|
-
function
|
|
368
|
+
function syncStatusLabel(status) {
|
|
481
369
|
switch (status) {
|
|
482
|
-
case "
|
|
483
|
-
return "
|
|
484
|
-
case "
|
|
485
|
-
return "
|
|
486
|
-
case "
|
|
370
|
+
case "idle":
|
|
371
|
+
return "Idle";
|
|
372
|
+
case "local":
|
|
373
|
+
return "Local only";
|
|
374
|
+
case "connecting":
|
|
487
375
|
return "Connecting";
|
|
488
|
-
case "
|
|
376
|
+
case "online":
|
|
489
377
|
return "Connected";
|
|
490
|
-
case "
|
|
491
|
-
return "
|
|
492
|
-
case "
|
|
493
|
-
return "
|
|
494
|
-
case "
|
|
495
|
-
return "
|
|
496
|
-
case "
|
|
497
|
-
return "
|
|
378
|
+
case "offline":
|
|
379
|
+
return "Offline";
|
|
380
|
+
case "auth_required":
|
|
381
|
+
return "Reauth required";
|
|
382
|
+
case "revoked":
|
|
383
|
+
return "Connection revoked";
|
|
384
|
+
case "stopped":
|
|
385
|
+
return "Stopped";
|
|
498
386
|
default:
|
|
499
387
|
return String(status);
|
|
500
388
|
}
|
|
@@ -583,9 +471,9 @@ function CopyableRow({
|
|
|
583
471
|
onCopied,
|
|
584
472
|
children
|
|
585
473
|
}) {
|
|
586
|
-
const [hover, setHover] = (0,
|
|
474
|
+
const [hover, setHover] = (0, import_react3.useState)(false);
|
|
587
475
|
const canCopy = copyText.length > 0;
|
|
588
|
-
const handleClick = (0,
|
|
476
|
+
const handleClick = (0, import_react3.useCallback)(
|
|
589
477
|
(e) => {
|
|
590
478
|
e.stopPropagation();
|
|
591
479
|
if (!canCopy) return;
|
|
@@ -659,25 +547,32 @@ function BasicDevToolbar({ enabled = true, debug }) {
|
|
|
659
547
|
const {
|
|
660
548
|
isReady,
|
|
661
549
|
isSignedIn,
|
|
550
|
+
isAnonymous,
|
|
662
551
|
user,
|
|
663
552
|
did,
|
|
664
553
|
scope,
|
|
665
554
|
missingScopes,
|
|
666
|
-
|
|
667
|
-
|
|
555
|
+
sync,
|
|
556
|
+
users,
|
|
557
|
+
activeUser,
|
|
668
558
|
devInfo,
|
|
669
|
-
refreshSchemaStatus
|
|
559
|
+
refreshSchemaStatus,
|
|
560
|
+
client
|
|
670
561
|
} = useBasic();
|
|
671
|
-
const
|
|
672
|
-
const
|
|
673
|
-
const
|
|
674
|
-
const
|
|
562
|
+
const dbMode = client.mode;
|
|
563
|
+
const syncStatus = sync.status;
|
|
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";
|
|
566
|
+
const [open, setOpen] = (0, import_react3.useState)(false);
|
|
567
|
+
const [refreshing, setRefreshing] = (0, import_react3.useState)(false);
|
|
568
|
+
const [copied, setCopied] = (0, import_react3.useState)(false);
|
|
569
|
+
const [rowCopied, setRowCopied] = (0, import_react3.useState)(null);
|
|
675
570
|
const show = enabled && typeof window !== "undefined" && isDevelopment(debug);
|
|
676
571
|
const authTone = toneForAuth(isReady, isSignedIn);
|
|
677
|
-
const dbTone =
|
|
572
|
+
const dbTone = toneForSync(dbMode, syncStatus);
|
|
678
573
|
const schemaTone = toneForSchema(devInfo);
|
|
679
|
-
const syncTone =
|
|
680
|
-
const handleRefreshSchema = (0,
|
|
574
|
+
const syncTone = dbTone;
|
|
575
|
+
const handleRefreshSchema = (0, import_react3.useCallback)(async () => {
|
|
681
576
|
setRefreshing(true);
|
|
682
577
|
try {
|
|
683
578
|
await refreshSchemaStatus();
|
|
@@ -686,7 +581,7 @@ function BasicDevToolbar({ enabled = true, debug }) {
|
|
|
686
581
|
}
|
|
687
582
|
}, [refreshSchemaStatus]);
|
|
688
583
|
const missingList = missingScopes();
|
|
689
|
-
const debugPayload = (0,
|
|
584
|
+
const debugPayload = (0, import_react3.useMemo)(() => {
|
|
690
585
|
return {
|
|
691
586
|
sdkVersion: version,
|
|
692
587
|
isReady,
|
|
@@ -701,12 +596,15 @@ function BasicDevToolbar({ enabled = true, debug }) {
|
|
|
701
596
|
scope,
|
|
702
597
|
missingScopes: missingList,
|
|
703
598
|
dbMode,
|
|
704
|
-
|
|
705
|
-
|
|
599
|
+
syncStatus,
|
|
600
|
+
pendingOps: sync.pendingCount,
|
|
601
|
+
indexedDbName,
|
|
602
|
+
activeUser: activeUser ? { id: activeUser.id, kind: activeUser.kind, did: activeUser.did } : null,
|
|
603
|
+
userCount: users.length,
|
|
706
604
|
schema: devInfo
|
|
707
605
|
};
|
|
708
|
-
}, [isReady, isSignedIn, did, user, scope, dbMode,
|
|
709
|
-
const handleCopy = (0,
|
|
606
|
+
}, [isReady, isSignedIn, did, user, scope, dbMode, syncStatus, sync.pendingCount, indexedDbName, activeUser, users.length, devInfo, missingList]);
|
|
607
|
+
const handleCopy = (0, import_react3.useCallback)(async () => {
|
|
710
608
|
try {
|
|
711
609
|
await navigator.clipboard.writeText(JSON.stringify(debugPayload, null, 2));
|
|
712
610
|
setCopied(true);
|
|
@@ -714,7 +612,7 @@ function BasicDevToolbar({ enabled = true, debug }) {
|
|
|
714
612
|
} catch {
|
|
715
613
|
}
|
|
716
614
|
}, [debugPayload]);
|
|
717
|
-
const onRowCopied = (0,
|
|
615
|
+
const onRowCopied = (0, import_react3.useCallback)((key) => {
|
|
718
616
|
setRowCopied(key);
|
|
719
617
|
setTimeout(() => setRowCopied((k) => k === key ? null : k), 1500);
|
|
720
618
|
}, []);
|
|
@@ -788,7 +686,7 @@ function BasicDevToolbar({ enabled = true, debug }) {
|
|
|
788
686
|
minWidth: 300,
|
|
789
687
|
maxWidth: "min(560px, calc(100vw - 24px))"
|
|
790
688
|
};
|
|
791
|
-
const syncStatusText =
|
|
689
|
+
const syncStatusText = dbMode === "rest" ? "REST mode" : `${syncStatusLabel(syncStatus)}${sync.pendingCount > 0 ? ` (${sync.pendingCount} pending)` : ""}`;
|
|
792
690
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: shell, children: [
|
|
793
691
|
open && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: panel, children: [
|
|
794
692
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginBottom: 12 }, children: [
|
|
@@ -843,6 +741,17 @@ function BasicDevToolbar({ enabled = true, debug }) {
|
|
|
843
741
|
children: user ? displayUserLine(user) : "\u2014"
|
|
844
742
|
}
|
|
845
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
|
+
),
|
|
846
755
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
847
756
|
CopyableRow,
|
|
848
757
|
{
|
|
@@ -883,10 +792,10 @@ function BasicDevToolbar({ enabled = true, debug }) {
|
|
|
883
792
|
{
|
|
884
793
|
rowKey: "indexedDb",
|
|
885
794
|
label: "IndexedDB",
|
|
886
|
-
copyText:
|
|
795
|
+
copyText: indexedDbName ?? "",
|
|
887
796
|
copiedKey: rowCopied,
|
|
888
797
|
onCopied: onRowCopied,
|
|
889
|
-
children:
|
|
798
|
+
children: indexedDbName ?? "\u2014"
|
|
890
799
|
}
|
|
891
800
|
),
|
|
892
801
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
@@ -1066,17 +975,16 @@ function BasicDevToolbar({ enabled = true, debug }) {
|
|
|
1066
975
|
)
|
|
1067
976
|
] });
|
|
1068
977
|
}
|
|
1069
|
-
var
|
|
978
|
+
var import_react3, import_jsx_runtime, PANEL_PAD_X;
|
|
1070
979
|
var init_BasicDevToolbar = __esm({
|
|
1071
980
|
"src/dev/BasicDevToolbar.tsx"() {
|
|
1072
981
|
"use strict";
|
|
1073
982
|
"use client";
|
|
1074
|
-
|
|
1075
|
-
|
|
983
|
+
import_react3 = require("react");
|
|
984
|
+
init_hooks();
|
|
1076
985
|
init_package();
|
|
1077
986
|
init_network();
|
|
1078
987
|
import_jsx_runtime = require("react/jsx-runtime");
|
|
1079
|
-
INDEXED_DB_NAME = "basicdb";
|
|
1080
988
|
PANEL_PAD_X = 12;
|
|
1081
989
|
}
|
|
1082
990
|
});
|
|
@@ -1084,595 +992,136 @@ var init_BasicDevToolbar = __esm({
|
|
|
1084
992
|
// src/index.ts
|
|
1085
993
|
var index_exports = {};
|
|
1086
994
|
__export(index_exports, {
|
|
995
|
+
AuthManager: () => AuthManager,
|
|
996
|
+
BasicClient: () => BasicClient,
|
|
1087
997
|
BasicDevToolbar: () => BasicDevToolbar,
|
|
1088
998
|
BasicProvider: () => BasicProvider,
|
|
1089
|
-
|
|
999
|
+
DEFAULT_LIMITS: () => DEFAULT_LIMITS,
|
|
1000
|
+
LocalStorageAdapter: () => LocalStorageAdapter,
|
|
1090
1001
|
NotAuthenticatedError: () => NotAuthenticatedError,
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1002
|
+
OWN_SUB: () => OWN_SUB,
|
|
1003
|
+
PROTOCOL_VERSION: () => PROTOCOL_VERSION,
|
|
1004
|
+
PrefixedStorage: () => PrefixedStorage,
|
|
1005
|
+
RestClient: () => RestClient,
|
|
1006
|
+
RestDb: () => RestDb,
|
|
1007
|
+
RestError: () => RestError,
|
|
1094
1008
|
STORAGE_KEYS: () => STORAGE_KEYS,
|
|
1009
|
+
SyncConnection: () => SyncConnection,
|
|
1010
|
+
SyncDb: () => SyncDb,
|
|
1011
|
+
SyncEngine: () => SyncEngine,
|
|
1012
|
+
SyncStore: () => SyncStore,
|
|
1013
|
+
UserRegistry: () => UserRegistry,
|
|
1014
|
+
applyOpToData: () => applyOpToData,
|
|
1015
|
+
createBasicClient: () => createBasicClient,
|
|
1016
|
+
isAuthError: () => isAuthError,
|
|
1017
|
+
isRebootstrapError: () => isRebootstrapError,
|
|
1018
|
+
isRevocationError: () => isRevocationError,
|
|
1019
|
+
isTerminalOpError: () => isTerminalOpError,
|
|
1020
|
+
mintOpId: () => mintOpId,
|
|
1095
1021
|
resolveDid: () => resolveDid,
|
|
1096
1022
|
resolveDidWebUrl: () => resolveDidWebUrl,
|
|
1097
1023
|
resolveHandle: () => resolveHandle,
|
|
1024
|
+
shareSubKey: () => shareSubKey,
|
|
1025
|
+
useAuth: () => useAuth,
|
|
1098
1026
|
useBasic: () => useBasic,
|
|
1099
|
-
|
|
1027
|
+
useBasicClient: () => useBasicClient,
|
|
1028
|
+
useDb: () => useDb,
|
|
1029
|
+
useQuery: () => useQuery,
|
|
1030
|
+
useShare: () => useShare,
|
|
1031
|
+
useShares: () => useShares,
|
|
1032
|
+
useSyncStatus: () => useSyncStatus,
|
|
1033
|
+
useUsers: () => useUsers
|
|
1100
1034
|
});
|
|
1101
1035
|
module.exports = __toCommonJS(index_exports);
|
|
1102
1036
|
|
|
1103
|
-
// src/
|
|
1104
|
-
var
|
|
1037
|
+
// src/react/BasicProvider.tsx
|
|
1038
|
+
var import_react4 = require("react");
|
|
1039
|
+
init_context();
|
|
1105
1040
|
|
|
1106
|
-
// src/
|
|
1107
|
-
var
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
var
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
var initPromise = null;
|
|
1114
|
-
async function initDexieExtensions() {
|
|
1115
|
-
if (dexieExtensionsLoaded) return;
|
|
1116
|
-
if (typeof window === "undefined") return;
|
|
1117
|
-
if (initPromise) return initPromise;
|
|
1118
|
-
initPromise = (async () => {
|
|
1119
|
-
try {
|
|
1120
|
-
await import("dexie-syncable");
|
|
1121
|
-
await import("dexie-observable");
|
|
1122
|
-
const { syncProtocol: syncProtocol2 } = await Promise.resolve().then(() => (init_syncProtocol(), syncProtocol_exports));
|
|
1123
|
-
syncProtocol2();
|
|
1124
|
-
dexieExtensionsLoaded = true;
|
|
1125
|
-
log("Dexie extensions loaded successfully");
|
|
1126
|
-
} catch (error) {
|
|
1127
|
-
console.error("Failed to load Dexie extensions:", error);
|
|
1128
|
-
throw error;
|
|
1129
|
-
}
|
|
1130
|
-
})();
|
|
1131
|
-
return initPromise;
|
|
1132
|
-
}
|
|
1133
|
-
var BasicSync = class extends import_dexie2.Dexie {
|
|
1134
|
-
basic_schema;
|
|
1135
|
-
constructor(name, options) {
|
|
1136
|
-
super(name, options);
|
|
1137
|
-
this.basic_schema = options.schema;
|
|
1138
|
-
this.version(1).stores(this._convertSchemaToDxSchema(this.basic_schema));
|
|
1139
|
-
this.version(2).stores({});
|
|
1140
|
-
this.Collection.prototype.get = this.Collection.prototype.toArray;
|
|
1141
|
-
}
|
|
1142
|
-
async connect({ getToken, ws_url }) {
|
|
1143
|
-
const WS_URL = ws_url || "wss://pds.basic.id/ws";
|
|
1144
|
-
log("Connecting to", WS_URL);
|
|
1145
|
-
setTokenGetter(WS_URL, getToken);
|
|
1146
|
-
await this.updateSyncNodes();
|
|
1147
|
-
log("Starting connection...");
|
|
1148
|
-
return this.syncable.connect("websocket", WS_URL, { schema: this.basic_schema });
|
|
1149
|
-
}
|
|
1150
|
-
async disconnect({ ws_url } = {}) {
|
|
1151
|
-
const WS_URL = ws_url || "wss://pds.basic.id/ws";
|
|
1152
|
-
return this.syncable.disconnect(WS_URL);
|
|
1153
|
-
}
|
|
1154
|
-
async updateSyncNodes() {
|
|
1155
|
-
try {
|
|
1156
|
-
const syncNodes = await this.table("_syncNodes").toArray();
|
|
1157
|
-
const localSyncNodes = syncNodes.filter((node) => node.type === "local");
|
|
1158
|
-
log("Local sync nodes:", localSyncNodes);
|
|
1159
|
-
if (localSyncNodes.length > 1) {
|
|
1160
|
-
const largestNodeId = Math.max(...localSyncNodes.map((node) => node.id));
|
|
1161
|
-
const largestNode = localSyncNodes.find((node) => node.id === largestNodeId);
|
|
1162
|
-
if (largestNode && largestNode.isMaster === 1) {
|
|
1163
|
-
log("Largest node is already the master. No changes needed.");
|
|
1164
|
-
return;
|
|
1165
|
-
}
|
|
1166
|
-
log("Largest node id:", largestNodeId);
|
|
1167
|
-
log("HEISENBUG: More than one local sync node found.");
|
|
1168
|
-
for (const node of localSyncNodes) {
|
|
1169
|
-
log(`Local sync node keys:`, node.id, node.isMaster);
|
|
1170
|
-
await this.table("_syncNodes").update(node.id, { isMaster: node.id === largestNodeId ? 1 : 0 });
|
|
1171
|
-
log(`HEISENBUG: Setting ${node.id} to ${node.id === largestNodeId ? "master" : "0"}`);
|
|
1172
|
-
}
|
|
1173
|
-
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1174
|
-
if (typeof window !== "undefined") {
|
|
1175
|
-
window.location.reload();
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
log("Sync nodes updated");
|
|
1179
|
-
} catch (error) {
|
|
1180
|
-
console.error("Error updating _syncNodes table:", error);
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
handleStatusChange(fn) {
|
|
1184
|
-
this.syncable.on("statusChanged", fn);
|
|
1185
|
-
}
|
|
1186
|
-
_convertSchemaToDxSchema(schema) {
|
|
1187
|
-
const stores = Object.entries(schema.tables).map(([key, table]) => {
|
|
1188
|
-
const indexedFields = Object.entries(table.fields).filter(([, field]) => field.indexed).map(([fieldKey]) => `,${fieldKey}`).join("");
|
|
1189
|
-
return {
|
|
1190
|
-
[key]: "id" + indexedFields
|
|
1191
|
-
};
|
|
1192
|
-
});
|
|
1193
|
-
return Object.assign({}, ...stores);
|
|
1041
|
+
// src/core/auth/AuthManager.ts
|
|
1042
|
+
var import_jwt_decode = require("jwt-decode");
|
|
1043
|
+
|
|
1044
|
+
// src/utils/storage.ts
|
|
1045
|
+
var LocalStorageAdapter = class {
|
|
1046
|
+
async get(key) {
|
|
1047
|
+
return localStorage.getItem(key);
|
|
1194
1048
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1049
|
+
async set(key, value) {
|
|
1050
|
+
localStorage.setItem(key, value);
|
|
1197
1051
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
throw new Error(`Table "${name}" not found in schema`);
|
|
1201
|
-
}
|
|
1202
|
-
const table = this.table(name);
|
|
1203
|
-
return {
|
|
1204
|
-
/**
|
|
1205
|
-
* Returns the underlying Dexie table
|
|
1206
|
-
* @type {Dexie.Table}
|
|
1207
|
-
*/
|
|
1208
|
-
ref: table,
|
|
1209
|
-
// --- WRITE ---- //
|
|
1210
|
-
/**
|
|
1211
|
-
* Add a new record - returns the full object with generated id
|
|
1212
|
-
*/
|
|
1213
|
-
add: async (data) => {
|
|
1214
|
-
const valid = (0, import_schema.validateData)(this.basic_schema, name, data);
|
|
1215
|
-
if (!valid.valid) {
|
|
1216
|
-
log("Invalid data", valid);
|
|
1217
|
-
throw new Error(valid.message || "Data validation failed");
|
|
1218
|
-
}
|
|
1219
|
-
const id = (0, import_uuid.v7)();
|
|
1220
|
-
const fullData = { id, ...data };
|
|
1221
|
-
await table.add(fullData);
|
|
1222
|
-
return fullData;
|
|
1223
|
-
},
|
|
1224
|
-
/**
|
|
1225
|
-
* Put (upsert) a record - returns the full object
|
|
1226
|
-
*/
|
|
1227
|
-
put: async (data) => {
|
|
1228
|
-
if (!data.id) {
|
|
1229
|
-
throw new Error("put() requires an id field");
|
|
1230
|
-
}
|
|
1231
|
-
const valid = (0, import_schema.validateData)(this.basic_schema, name, data);
|
|
1232
|
-
if (!valid.valid) {
|
|
1233
|
-
log("Invalid data", valid);
|
|
1234
|
-
throw new Error(valid.message || "Data validation failed");
|
|
1235
|
-
}
|
|
1236
|
-
await table.put(data);
|
|
1237
|
-
return data;
|
|
1238
|
-
},
|
|
1239
|
-
/**
|
|
1240
|
-
* Update an existing record - returns updated object or null
|
|
1241
|
-
*/
|
|
1242
|
-
update: async (id, data) => {
|
|
1243
|
-
if (!id) {
|
|
1244
|
-
throw new Error("update() requires an id");
|
|
1245
|
-
}
|
|
1246
|
-
const valid = (0, import_schema.validateData)(this.basic_schema, name, data, false);
|
|
1247
|
-
if (!valid.valid) {
|
|
1248
|
-
log("Invalid data", valid);
|
|
1249
|
-
throw new Error(valid.message || "Data validation failed");
|
|
1250
|
-
}
|
|
1251
|
-
const updated = await table.update(id, data);
|
|
1252
|
-
if (updated === 0) {
|
|
1253
|
-
return null;
|
|
1254
|
-
}
|
|
1255
|
-
const record = await table.get(id);
|
|
1256
|
-
return record || null;
|
|
1257
|
-
},
|
|
1258
|
-
/**
|
|
1259
|
-
* Delete a record - returns true if deleted, false if not found
|
|
1260
|
-
*/
|
|
1261
|
-
delete: async (id) => {
|
|
1262
|
-
if (!id) {
|
|
1263
|
-
throw new Error("delete() requires an id");
|
|
1264
|
-
}
|
|
1265
|
-
const exists = await table.get(id);
|
|
1266
|
-
if (!exists) {
|
|
1267
|
-
return false;
|
|
1268
|
-
}
|
|
1269
|
-
await table.delete(id);
|
|
1270
|
-
return true;
|
|
1271
|
-
},
|
|
1272
|
-
// --- READ ---- //
|
|
1273
|
-
/**
|
|
1274
|
-
* Get a single record by id - returns null if not found
|
|
1275
|
-
*/
|
|
1276
|
-
get: async (id) => {
|
|
1277
|
-
if (!id) {
|
|
1278
|
-
throw new Error("get() requires an id");
|
|
1279
|
-
}
|
|
1280
|
-
const record = await table.get(id);
|
|
1281
|
-
return record || null;
|
|
1282
|
-
},
|
|
1283
|
-
/**
|
|
1284
|
-
* Get all records in the collection
|
|
1285
|
-
*/
|
|
1286
|
-
getAll: async () => {
|
|
1287
|
-
return table.toArray();
|
|
1288
|
-
},
|
|
1289
|
-
// --- QUERY ---- //
|
|
1290
|
-
/**
|
|
1291
|
-
* Filter records using a predicate function
|
|
1292
|
-
*/
|
|
1293
|
-
filter: async (fn) => {
|
|
1294
|
-
return table.filter(fn).toArray();
|
|
1295
|
-
},
|
|
1296
|
-
/**
|
|
1297
|
-
* Get the raw Dexie table for advanced queries
|
|
1298
|
-
* @deprecated Use ref instead
|
|
1299
|
-
*/
|
|
1300
|
-
query: () => table
|
|
1301
|
-
};
|
|
1052
|
+
async remove(key) {
|
|
1053
|
+
localStorage.removeItem(key);
|
|
1302
1054
|
}
|
|
1303
1055
|
};
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
}
|
|
1056
|
+
var STORAGE_KEYS = {
|
|
1057
|
+
REFRESH_TOKEN: "basic_refresh_token",
|
|
1058
|
+
USER_INFO: "basic_user_info",
|
|
1059
|
+
AUTH_STATE: "basic_auth_state",
|
|
1060
|
+
REDIRECT_URI: "basic_redirect_uri",
|
|
1061
|
+
SERVER_URL: "basic_server_url",
|
|
1062
|
+
PDS_ENDPOINTS: "basic_pds_endpoints",
|
|
1063
|
+
LAST_CONNECT_REPORT: "basic_last_connect_report",
|
|
1064
|
+
DEBUG: "basic_debug",
|
|
1065
|
+
CODE_VERIFIER: "basic_code_verifier"
|
|
1315
1066
|
};
|
|
1316
1067
|
|
|
1317
|
-
// src/
|
|
1318
|
-
var
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1068
|
+
// src/utils/normalizeClientId.ts
|
|
1069
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1070
|
+
function normalizeClientId(projectId, adminHostname = "api.basic.tech") {
|
|
1071
|
+
if (!projectId) return projectId;
|
|
1072
|
+
if (projectId === "self") return projectId;
|
|
1073
|
+
if (projectId.startsWith("did:")) return projectId;
|
|
1074
|
+
if (UUID_RE.test(projectId)) {
|
|
1075
|
+
const hex = projectId.replace(/-/g, "").toLowerCase();
|
|
1076
|
+
return `did:web:${adminHostname}:projects:${hex}`;
|
|
1323
1077
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1078
|
+
return projectId;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// src/utils/resolveDid.ts
|
|
1082
|
+
function resolveDidWebUrl(did) {
|
|
1083
|
+
if (!did.startsWith("did:web:")) return null;
|
|
1084
|
+
const rest = did.slice(8);
|
|
1085
|
+
if (!rest) return null;
|
|
1086
|
+
const parts = rest.split(":");
|
|
1087
|
+
const hostname = parts[0].replace(/%3A/gi, ":");
|
|
1088
|
+
if (parts.length === 1) {
|
|
1089
|
+
return `https://${hostname}/.well-known/did.json`;
|
|
1331
1090
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1091
|
+
const pathParts = parts.slice(1).map((p) => decodeURIComponent(p));
|
|
1092
|
+
return `https://${hostname}/${pathParts.join("/")}/did.json`;
|
|
1093
|
+
}
|
|
1094
|
+
async function resolveFromDocument(did, didDocument) {
|
|
1095
|
+
const services = didDocument.service;
|
|
1096
|
+
const pdsService = services?.find(
|
|
1097
|
+
(s) => s.id === "#basic_pds" || s.id === `${did}#basic_pds`
|
|
1098
|
+
);
|
|
1099
|
+
if (!pdsService) {
|
|
1100
|
+
throw new Error(`DID document has no #basic_pds service entry`);
|
|
1336
1101
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
if (error instanceof Error) {
|
|
1342
|
-
const message = error.message.toLowerCase();
|
|
1343
|
-
return message.includes("no token") || message.includes("not authenticated") || message.includes("please sign in");
|
|
1344
|
-
}
|
|
1345
|
-
return false;
|
|
1102
|
+
const pdsUrl = pdsService.serviceEndpoint.replace(/\/+$/, "");
|
|
1103
|
+
const oauthRes = await fetch(`${pdsUrl}/auth/.well-known/openid-configuration`);
|
|
1104
|
+
if (!oauthRes.ok) {
|
|
1105
|
+
throw new Error(`Failed to fetch OpenID configuration from ${pdsUrl}: ${oauthRes.status}`);
|
|
1346
1106
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
}
|
|
1361
|
-
const response = await fetch(url, {
|
|
1362
|
-
method,
|
|
1363
|
-
headers,
|
|
1364
|
-
...body ? { body: JSON.stringify(body) } : {}
|
|
1365
|
-
});
|
|
1366
|
-
const responseData = await response.json().catch(() => ({}));
|
|
1367
|
-
if (!response.ok) {
|
|
1368
|
-
if (response.status === 401 && !isRetry) {
|
|
1369
|
-
this.log("Got 401, forcing token refresh and retrying...");
|
|
1370
|
-
await this.config.getToken({ forceRefresh: true });
|
|
1371
|
-
return this.request(method, path, body, true);
|
|
1372
|
-
}
|
|
1373
|
-
if (this.config.debug) {
|
|
1374
|
-
console.error(`[RemoteDB] Error ${response.status}:`, responseData);
|
|
1375
|
-
}
|
|
1376
|
-
if (this.config.onAuthError) {
|
|
1377
|
-
if (response.status === 401) {
|
|
1378
|
-
this.config.onAuthError({
|
|
1379
|
-
status: response.status,
|
|
1380
|
-
message: "Authentication failed",
|
|
1381
|
-
response: responseData,
|
|
1382
|
-
errorType: "expired",
|
|
1383
|
-
afterRetry: isRetry
|
|
1384
|
-
});
|
|
1385
|
-
} else if (response.status === 403) {
|
|
1386
|
-
this.config.onAuthError({
|
|
1387
|
-
status: response.status,
|
|
1388
|
-
message: responseData.message || "Forbidden - insufficient permissions or missing scope",
|
|
1389
|
-
response: responseData,
|
|
1390
|
-
errorType: "forbidden",
|
|
1391
|
-
afterRetry: isRetry
|
|
1392
|
-
});
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
const errorMessage = responseData.message || responseData.error || responseData.detail || (typeof responseData === "string" ? responseData : `API request failed: ${response.status}`);
|
|
1396
|
-
throw new RemoteDBError(errorMessage, response.status, responseData);
|
|
1397
|
-
}
|
|
1398
|
-
this.log("Response:", responseData);
|
|
1399
|
-
return responseData;
|
|
1107
|
+
const oauth = await oauthRes.json();
|
|
1108
|
+
return {
|
|
1109
|
+
did,
|
|
1110
|
+
didDocument,
|
|
1111
|
+
pdsUrl,
|
|
1112
|
+
authorization_endpoint: oauth.authorization_endpoint,
|
|
1113
|
+
token_endpoint: oauth.token_endpoint,
|
|
1114
|
+
userinfo_endpoint: oauth.userinfo_endpoint
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
async function resolveDid(did) {
|
|
1118
|
+
const url = resolveDidWebUrl(did);
|
|
1119
|
+
if (!url) {
|
|
1120
|
+
throw new Error(`Unsupported DID method: ${did}`);
|
|
1400
1121
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
validateData(data, checkRequired = true) {
|
|
1405
|
-
if (this.config.schema) {
|
|
1406
|
-
const result = (0, import_schema2.validateData)(this.config.schema, this.tableName, data, checkRequired);
|
|
1407
|
-
if (!result.valid) {
|
|
1408
|
-
throw new Error(result.message || "Data validation failed");
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
/**
|
|
1413
|
-
* Get the base path for this collection
|
|
1414
|
-
*/
|
|
1415
|
-
get basePath() {
|
|
1416
|
-
return `/account/${this.config.projectId}/db/${this.tableName}`;
|
|
1417
|
-
}
|
|
1418
|
-
/**
|
|
1419
|
-
* Add a new record to the collection
|
|
1420
|
-
* The server generates the ID
|
|
1421
|
-
* Requires authentication - throws NotAuthenticatedError if not signed in
|
|
1422
|
-
*/
|
|
1423
|
-
async add(data) {
|
|
1424
|
-
this.validateData(data, true);
|
|
1425
|
-
try {
|
|
1426
|
-
const result = await this.request(
|
|
1427
|
-
"POST",
|
|
1428
|
-
this.basePath,
|
|
1429
|
-
{ value: data }
|
|
1430
|
-
);
|
|
1431
|
-
return result.data;
|
|
1432
|
-
} catch (error) {
|
|
1433
|
-
if (this.isNotAuthenticatedError(error)) {
|
|
1434
|
-
throw new NotAuthenticatedError("Sign in required to add items");
|
|
1435
|
-
}
|
|
1436
|
-
throw error;
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
/**
|
|
1440
|
-
* Put (upsert) a record - requires id
|
|
1441
|
-
* Requires authentication - throws NotAuthenticatedError if not signed in
|
|
1442
|
-
*/
|
|
1443
|
-
async put(data) {
|
|
1444
|
-
if (!data.id) {
|
|
1445
|
-
throw new Error("put() requires an id field");
|
|
1446
|
-
}
|
|
1447
|
-
const { id, ...rest } = data;
|
|
1448
|
-
this.validateData(rest, true);
|
|
1449
|
-
try {
|
|
1450
|
-
const result = await this.request(
|
|
1451
|
-
"PUT",
|
|
1452
|
-
`${this.basePath}/${id}`,
|
|
1453
|
-
{ value: rest }
|
|
1454
|
-
);
|
|
1455
|
-
return result.data || data;
|
|
1456
|
-
} catch (error) {
|
|
1457
|
-
if (this.isNotAuthenticatedError(error)) {
|
|
1458
|
-
throw new NotAuthenticatedError("Sign in required to update items");
|
|
1459
|
-
}
|
|
1460
|
-
throw error;
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
/**
|
|
1464
|
-
* Update an existing record by id
|
|
1465
|
-
* Requires authentication - throws NotAuthenticatedError if not signed in
|
|
1466
|
-
*/
|
|
1467
|
-
async update(id, data) {
|
|
1468
|
-
if (!id) {
|
|
1469
|
-
throw new Error("update() requires an id");
|
|
1470
|
-
}
|
|
1471
|
-
this.validateData(data, false);
|
|
1472
|
-
try {
|
|
1473
|
-
const result = await this.request(
|
|
1474
|
-
"PATCH",
|
|
1475
|
-
`${this.basePath}/${id}`,
|
|
1476
|
-
{ value: data }
|
|
1477
|
-
);
|
|
1478
|
-
return result.data || null;
|
|
1479
|
-
} catch (error) {
|
|
1480
|
-
if (error instanceof RemoteDBError && error.status === 404) {
|
|
1481
|
-
return null;
|
|
1482
|
-
}
|
|
1483
|
-
if (this.isNotAuthenticatedError(error)) {
|
|
1484
|
-
throw new NotAuthenticatedError("Sign in required to update items");
|
|
1485
|
-
}
|
|
1486
|
-
throw error;
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
/**
|
|
1490
|
-
* Delete a record by id
|
|
1491
|
-
* Requires authentication - throws NotAuthenticatedError if not signed in
|
|
1492
|
-
*/
|
|
1493
|
-
async delete(id) {
|
|
1494
|
-
if (!id) {
|
|
1495
|
-
throw new Error("delete() requires an id");
|
|
1496
|
-
}
|
|
1497
|
-
try {
|
|
1498
|
-
await this.request(
|
|
1499
|
-
"DELETE",
|
|
1500
|
-
`${this.basePath}/${id}`
|
|
1501
|
-
);
|
|
1502
|
-
return true;
|
|
1503
|
-
} catch (error) {
|
|
1504
|
-
if (error instanceof RemoteDBError && error.status === 404) {
|
|
1505
|
-
return false;
|
|
1506
|
-
}
|
|
1507
|
-
if (this.isNotAuthenticatedError(error)) {
|
|
1508
|
-
throw new NotAuthenticatedError("Sign in required to delete items");
|
|
1509
|
-
}
|
|
1510
|
-
throw error;
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
/**
|
|
1514
|
-
* Get a single record by id
|
|
1515
|
-
* Returns null if not authenticated (graceful degradation for read operations)
|
|
1516
|
-
*/
|
|
1517
|
-
async get(id) {
|
|
1518
|
-
if (!id) {
|
|
1519
|
-
throw new Error("get() requires an id");
|
|
1520
|
-
}
|
|
1521
|
-
try {
|
|
1522
|
-
const result = await this.request(
|
|
1523
|
-
"GET",
|
|
1524
|
-
`${this.basePath}?id=${id}`
|
|
1525
|
-
);
|
|
1526
|
-
return result.data?.[0] || null;
|
|
1527
|
-
} catch (error) {
|
|
1528
|
-
if (this.isNotAuthenticatedError(error)) {
|
|
1529
|
-
this.log("Not authenticated - returning null for get()");
|
|
1530
|
-
}
|
|
1531
|
-
return null;
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
/**
|
|
1535
|
-
* Get all records in the collection
|
|
1536
|
-
* Returns empty array if not authenticated (graceful degradation for read operations)
|
|
1537
|
-
*/
|
|
1538
|
-
async getAll() {
|
|
1539
|
-
try {
|
|
1540
|
-
const result = await this.request(
|
|
1541
|
-
"GET",
|
|
1542
|
-
this.basePath
|
|
1543
|
-
);
|
|
1544
|
-
return result.data || [];
|
|
1545
|
-
} catch (error) {
|
|
1546
|
-
if (this.isNotAuthenticatedError(error)) {
|
|
1547
|
-
this.log("Not authenticated - returning empty array for getAll()");
|
|
1548
|
-
return [];
|
|
1549
|
-
}
|
|
1550
|
-
throw error;
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
/**
|
|
1554
|
-
* Filter records using a predicate function
|
|
1555
|
-
* Note: This fetches all records and filters client-side
|
|
1556
|
-
* Returns empty array if not authenticated (graceful degradation for read operations)
|
|
1557
|
-
*/
|
|
1558
|
-
async filter(fn) {
|
|
1559
|
-
const all = await this.getAll();
|
|
1560
|
-
return all.filter(fn);
|
|
1561
|
-
}
|
|
1562
|
-
/**
|
|
1563
|
-
* ref is not available for remote collections
|
|
1564
|
-
*/
|
|
1565
|
-
ref = void 0;
|
|
1566
|
-
};
|
|
1567
|
-
|
|
1568
|
-
// src/core/db/RemoteDB.ts
|
|
1569
|
-
var RemoteDB = class {
|
|
1570
|
-
config;
|
|
1571
|
-
collections = /* @__PURE__ */ new Map();
|
|
1572
|
-
constructor(config) {
|
|
1573
|
-
this.config = config;
|
|
1574
|
-
}
|
|
1575
|
-
/**
|
|
1576
|
-
* Get a collection by name
|
|
1577
|
-
* Collections are cached for reuse
|
|
1578
|
-
*/
|
|
1579
|
-
collection(name) {
|
|
1580
|
-
if (this.collections.has(name)) {
|
|
1581
|
-
return this.collections.get(name);
|
|
1582
|
-
}
|
|
1583
|
-
if (this.config.schema?.tables && !this.config.schema.tables[name]) {
|
|
1584
|
-
throw new Error(`Table "${name}" not found in schema`);
|
|
1585
|
-
}
|
|
1586
|
-
const collection = new RemoteCollection(name, this.config);
|
|
1587
|
-
this.collections.set(name, collection);
|
|
1588
|
-
return collection;
|
|
1589
|
-
}
|
|
1590
|
-
};
|
|
1591
|
-
|
|
1592
|
-
// src/core/auth/AuthManager.ts
|
|
1593
|
-
var import_jwt_decode = require("jwt-decode");
|
|
1594
|
-
|
|
1595
|
-
// src/utils/storage.ts
|
|
1596
|
-
var LocalStorageAdapter = class {
|
|
1597
|
-
async get(key) {
|
|
1598
|
-
return localStorage.getItem(key);
|
|
1599
|
-
}
|
|
1600
|
-
async set(key, value) {
|
|
1601
|
-
localStorage.setItem(key, value);
|
|
1602
|
-
}
|
|
1603
|
-
async remove(key) {
|
|
1604
|
-
localStorage.removeItem(key);
|
|
1605
|
-
}
|
|
1606
|
-
};
|
|
1607
|
-
var STORAGE_KEYS = {
|
|
1608
|
-
REFRESH_TOKEN: "basic_refresh_token",
|
|
1609
|
-
USER_INFO: "basic_user_info",
|
|
1610
|
-
AUTH_STATE: "basic_auth_state",
|
|
1611
|
-
REDIRECT_URI: "basic_redirect_uri",
|
|
1612
|
-
SERVER_URL: "basic_server_url",
|
|
1613
|
-
PDS_ENDPOINTS: "basic_pds_endpoints",
|
|
1614
|
-
LAST_CONNECT_REPORT: "basic_last_connect_report",
|
|
1615
|
-
DEBUG: "basic_debug",
|
|
1616
|
-
CODE_VERIFIER: "basic_code_verifier"
|
|
1617
|
-
};
|
|
1618
|
-
|
|
1619
|
-
// src/utils/normalizeClientId.ts
|
|
1620
|
-
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1621
|
-
function normalizeClientId(projectId, adminHostname = "api.basic.tech") {
|
|
1622
|
-
if (!projectId) return projectId;
|
|
1623
|
-
if (projectId === "self") return projectId;
|
|
1624
|
-
if (projectId.startsWith("did:")) return projectId;
|
|
1625
|
-
if (UUID_RE.test(projectId)) {
|
|
1626
|
-
const hex = projectId.replace(/-/g, "").toLowerCase();
|
|
1627
|
-
return `did:web:${adminHostname}:projects:${hex}`;
|
|
1628
|
-
}
|
|
1629
|
-
return projectId;
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
// src/utils/resolveDid.ts
|
|
1633
|
-
function resolveDidWebUrl(did) {
|
|
1634
|
-
if (!did.startsWith("did:web:")) return null;
|
|
1635
|
-
const rest = did.slice(8);
|
|
1636
|
-
if (!rest) return null;
|
|
1637
|
-
const parts = rest.split(":");
|
|
1638
|
-
const hostname = parts[0].replace(/%3A/gi, ":");
|
|
1639
|
-
if (parts.length === 1) {
|
|
1640
|
-
return `https://${hostname}/.well-known/did.json`;
|
|
1641
|
-
}
|
|
1642
|
-
const pathParts = parts.slice(1).map((p) => decodeURIComponent(p));
|
|
1643
|
-
return `https://${hostname}/${pathParts.join("/")}/did.json`;
|
|
1644
|
-
}
|
|
1645
|
-
async function resolveFromDocument(did, didDocument) {
|
|
1646
|
-
const services = didDocument.service;
|
|
1647
|
-
const pdsService = services?.find(
|
|
1648
|
-
(s) => s.id === "#basic_pds" || s.id === `${did}#basic_pds`
|
|
1649
|
-
);
|
|
1650
|
-
if (!pdsService) {
|
|
1651
|
-
throw new Error(`DID document has no #basic_pds service entry`);
|
|
1652
|
-
}
|
|
1653
|
-
const pdsUrl = pdsService.serviceEndpoint.replace(/\/+$/, "");
|
|
1654
|
-
const oauthRes = await fetch(`${pdsUrl}/auth/.well-known/openid-configuration`);
|
|
1655
|
-
if (!oauthRes.ok) {
|
|
1656
|
-
throw new Error(`Failed to fetch OpenID configuration from ${pdsUrl}: ${oauthRes.status}`);
|
|
1657
|
-
}
|
|
1658
|
-
const oauth = await oauthRes.json();
|
|
1659
|
-
return {
|
|
1660
|
-
did,
|
|
1661
|
-
didDocument,
|
|
1662
|
-
pdsUrl,
|
|
1663
|
-
authorization_endpoint: oauth.authorization_endpoint,
|
|
1664
|
-
token_endpoint: oauth.token_endpoint,
|
|
1665
|
-
userinfo_endpoint: oauth.userinfo_endpoint
|
|
1666
|
-
};
|
|
1667
|
-
}
|
|
1668
|
-
async function resolveDid(did) {
|
|
1669
|
-
const url = resolveDidWebUrl(did);
|
|
1670
|
-
if (!url) {
|
|
1671
|
-
throw new Error(`Unsupported DID method: ${did}`);
|
|
1672
|
-
}
|
|
1673
|
-
const didRes = await fetch(url);
|
|
1674
|
-
if (!didRes.ok) {
|
|
1675
|
-
throw new Error(`Failed to fetch DID document at ${url}: ${didRes.status}`);
|
|
1122
|
+
const didRes = await fetch(url);
|
|
1123
|
+
if (!didRes.ok) {
|
|
1124
|
+
throw new Error(`Failed to fetch DID document at ${url}: ${didRes.status}`);
|
|
1676
1125
|
}
|
|
1677
1126
|
const didDocument = await didRes.json();
|
|
1678
1127
|
return resolveFromDocument(did, didDocument);
|
|
@@ -1768,11 +1217,15 @@ var AuthManager = class {
|
|
|
1768
1217
|
this.requestedScopes = config.scopes;
|
|
1769
1218
|
this.initCrossTabSync();
|
|
1770
1219
|
}
|
|
1220
|
+
get instanceKey() {
|
|
1221
|
+
return this.config.instanceKey ?? "";
|
|
1222
|
+
}
|
|
1771
1223
|
initCrossTabSync() {
|
|
1772
1224
|
if (typeof BroadcastChannel === "undefined") return;
|
|
1773
1225
|
try {
|
|
1774
1226
|
this.channel = new BroadcastChannel("basic-auth");
|
|
1775
1227
|
this.channel.onmessage = (event) => {
|
|
1228
|
+
if ((event.data?.userKey ?? "") !== this.instanceKey) return;
|
|
1776
1229
|
if (event.data?.type === "token_refreshed") {
|
|
1777
1230
|
log("Received token refresh from another tab");
|
|
1778
1231
|
void this.handleExternalTokenRefresh(event.data);
|
|
@@ -1785,9 +1238,6 @@ var AuthManager = class {
|
|
|
1785
1238
|
log("Received sign-out from another tab");
|
|
1786
1239
|
this.resetAuthState("signed_out");
|
|
1787
1240
|
this.notify();
|
|
1788
|
-
if (typeof window !== "undefined") {
|
|
1789
|
-
window.location.reload();
|
|
1790
|
-
}
|
|
1791
1241
|
}
|
|
1792
1242
|
if (event.data?.type === "session_invalidated") {
|
|
1793
1243
|
log("Received session invalidation from another tab");
|
|
@@ -1803,19 +1253,32 @@ var AuthManager = class {
|
|
|
1803
1253
|
broadcastTokenRefresh() {
|
|
1804
1254
|
this.channel?.postMessage({
|
|
1805
1255
|
type: "token_refreshed",
|
|
1256
|
+
userKey: this.instanceKey,
|
|
1806
1257
|
accessToken: this.token?.access_token,
|
|
1807
1258
|
did: this.did,
|
|
1808
1259
|
tokenScope: this.tokenScope
|
|
1809
1260
|
});
|
|
1810
1261
|
}
|
|
1811
1262
|
broadcastSignIn() {
|
|
1812
|
-
this.channel?.postMessage({ type: "signed_in" });
|
|
1263
|
+
this.channel?.postMessage({ type: "signed_in", userKey: this.instanceKey });
|
|
1813
1264
|
}
|
|
1814
1265
|
broadcastSignOut() {
|
|
1815
|
-
this.channel?.postMessage({ type: "signed_out" });
|
|
1266
|
+
this.channel?.postMessage({ type: "signed_out", userKey: this.instanceKey });
|
|
1816
1267
|
}
|
|
1817
1268
|
broadcastSessionInvalidated(code) {
|
|
1818
|
-
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;
|
|
1819
1282
|
}
|
|
1820
1283
|
// ------------------------------------------------------------------
|
|
1821
1284
|
// Public API
|
|
@@ -2075,11 +1538,13 @@ var AuthManager = class {
|
|
|
2075
1538
|
}
|
|
2076
1539
|
}
|
|
2077
1540
|
/**
|
|
2078
|
-
*
|
|
2079
|
-
*
|
|
1541
|
+
* Sign out: revoke the session server-side (`POST /auth/logout`, best
|
|
1542
|
+
* effort), then clear auth state and storage. Does NOT handle sync/DB
|
|
1543
|
+
* cleanup — the client layer wraps this to add sync teardown.
|
|
2080
1544
|
*/
|
|
2081
1545
|
async signOut() {
|
|
2082
1546
|
log("signing out!");
|
|
1547
|
+
await this.revokeSessionOnServer();
|
|
2083
1548
|
this.resetAuthState("signed_out");
|
|
2084
1549
|
await this.storage.remove(STORAGE_KEYS.AUTH_STATE);
|
|
2085
1550
|
await this.storage.remove(STORAGE_KEYS.LAST_CONNECT_REPORT);
|
|
@@ -2087,6 +1552,30 @@ var AuthManager = class {
|
|
|
2087
1552
|
this.broadcastSignOut();
|
|
2088
1553
|
this.notify();
|
|
2089
1554
|
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Best-effort server-side revocation of the current device/session and
|
|
1557
|
+
* its refresh chain (Step 2 auth: logout is finally server-side).
|
|
1558
|
+
* Never blocks or fails the local sign-out.
|
|
1559
|
+
*/
|
|
1560
|
+
async revokeSessionOnServer() {
|
|
1561
|
+
try {
|
|
1562
|
+
let accessToken = null;
|
|
1563
|
+
try {
|
|
1564
|
+
accessToken = await this.getToken();
|
|
1565
|
+
} catch {
|
|
1566
|
+
accessToken = this.token?.access_token ?? null;
|
|
1567
|
+
}
|
|
1568
|
+
if (!accessToken) return;
|
|
1569
|
+
const endpoints = await this.getActivePdsEndpoints();
|
|
1570
|
+
await fetch(`${endpoints.pds_url}/auth/logout`, {
|
|
1571
|
+
method: "POST",
|
|
1572
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
1573
|
+
});
|
|
1574
|
+
log("Server-side logout succeeded");
|
|
1575
|
+
} catch (error) {
|
|
1576
|
+
log("Server-side logout failed (non-blocking):", error);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
2090
1579
|
async reconcileSession(reason = "manual", options) {
|
|
2091
1580
|
if (this.authStatus === "signed_out" || this.authStatus === "reauth_required") {
|
|
2092
1581
|
return;
|
|
@@ -2410,11 +1899,11 @@ var AuthManager = class {
|
|
|
2410
1899
|
...isRefreshToken ? { refresh_token: "[REDACTED]" } : { code: "[REDACTED]" },
|
|
2411
1900
|
...requestBody.code_verifier ? { code_verifier: "[REDACTED]" } : {}
|
|
2412
1901
|
});
|
|
2413
|
-
const
|
|
1902
|
+
const response = await fetch(endpoints.token_endpoint, {
|
|
2414
1903
|
method: "POST",
|
|
2415
1904
|
headers: { "Content-Type": "application/json" },
|
|
2416
1905
|
body: JSON.stringify(requestBody)
|
|
2417
|
-
}).
|
|
1906
|
+
}).catch((error) => {
|
|
2418
1907
|
log("Network error fetching token:", error);
|
|
2419
1908
|
if (!this.isOnline) {
|
|
2420
1909
|
this.pendingRefresh = true;
|
|
@@ -2424,6 +1913,18 @@ var AuthManager = class {
|
|
|
2424
1913
|
}
|
|
2425
1914
|
throw new Error("Network error during token refresh");
|
|
2426
1915
|
});
|
|
1916
|
+
if (response.status === 429) {
|
|
1917
|
+
log("Token endpoint rate limited (429) - will retry later");
|
|
1918
|
+
this.pendingRefresh = true;
|
|
1919
|
+
throw new Error(
|
|
1920
|
+
"Token endpoint rate limited - refresh will be retried"
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
const token = await response.json().catch(() => {
|
|
1924
|
+
throw new Error(
|
|
1925
|
+
`Token endpoint returned invalid JSON (status ${response.status})`
|
|
1926
|
+
);
|
|
1927
|
+
});
|
|
2427
1928
|
if (token.access_token) {
|
|
2428
1929
|
try {
|
|
2429
1930
|
const decoded = (0, import_jwt_decode.jwtDecode)(token.access_token);
|
|
@@ -2533,7 +2034,8 @@ var AuthManager = class {
|
|
|
2533
2034
|
isNetworkError(error) {
|
|
2534
2035
|
if (error instanceof TypeError) return true;
|
|
2535
2036
|
if (error instanceof Error) {
|
|
2536
|
-
return error.message.includes("offline") || error.message.includes("Network")
|
|
2037
|
+
return error.message.includes("offline") || error.message.includes("Network") || // 429 on the token endpoint: transient, keep the session alive
|
|
2038
|
+
error.message.includes("rate limited");
|
|
2537
2039
|
}
|
|
2538
2040
|
return false;
|
|
2539
2041
|
}
|
|
@@ -2748,142 +2250,1720 @@ var AuthManager = class {
|
|
|
2748
2250
|
}
|
|
2749
2251
|
};
|
|
2750
2252
|
|
|
2751
|
-
// src/
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2253
|
+
// src/core/http/RestClient.ts
|
|
2254
|
+
var RestError = class extends Error {
|
|
2255
|
+
status;
|
|
2256
|
+
code;
|
|
2257
|
+
response;
|
|
2258
|
+
constructor(message, status, code, response) {
|
|
2259
|
+
super(message);
|
|
2260
|
+
this.name = "RestError";
|
|
2261
|
+
this.status = status;
|
|
2262
|
+
this.code = code;
|
|
2263
|
+
this.response = response;
|
|
2264
|
+
}
|
|
2265
|
+
};
|
|
2266
|
+
var NotAuthenticatedError = class extends Error {
|
|
2267
|
+
constructor(message = "Not authenticated") {
|
|
2268
|
+
super(message);
|
|
2269
|
+
this.name = "NotAuthenticatedError";
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
var RestClient = class {
|
|
2273
|
+
opts;
|
|
2274
|
+
constructor(opts) {
|
|
2275
|
+
this.opts = { ...opts, baseUrl: opts.baseUrl.replace(/\/$/, "") };
|
|
2276
|
+
}
|
|
2277
|
+
get projectId() {
|
|
2278
|
+
return this.opts.projectId;
|
|
2279
|
+
}
|
|
2280
|
+
// -------------------------------------------------------------------
|
|
2281
|
+
// Sync surface
|
|
2282
|
+
// -------------------------------------------------------------------
|
|
2283
|
+
/** `GET /account/:project_id/db` — tables, enforced schema version, channel head. */
|
|
2284
|
+
async getDbInfo() {
|
|
2285
|
+
const res = await this.request("GET", `${this.dbPath}`);
|
|
2286
|
+
return res.data;
|
|
2287
|
+
}
|
|
2288
|
+
/** Bootstrap snapshot (SPEC §5). `share` bootstraps a mount; `table` filters. */
|
|
2289
|
+
async getSnapshot(options) {
|
|
2290
|
+
const query = new URLSearchParams();
|
|
2291
|
+
if (options?.share) query.set("share", options.share);
|
|
2292
|
+
if (options?.table) query.set("table", options.table);
|
|
2293
|
+
const qs = query.toString();
|
|
2294
|
+
const res = await this.request(
|
|
2295
|
+
"GET",
|
|
2296
|
+
`${this.dbPath}/snapshot${qs ? `?${qs}` : ""}`
|
|
2297
|
+
);
|
|
2298
|
+
return res.data;
|
|
2299
|
+
}
|
|
2300
|
+
/** Pull ordered ops after a cursor — the non-WebSocket sync path. */
|
|
2301
|
+
async getChanges(options) {
|
|
2302
|
+
const query = new URLSearchParams({ cursor: String(options.cursor) });
|
|
2303
|
+
if (options.limit) query.set("limit", String(options.limit));
|
|
2304
|
+
if (options.share) query.set("share", options.share);
|
|
2305
|
+
if (options.table) query.set("table", options.table);
|
|
2306
|
+
const res = await this.request(
|
|
2307
|
+
"GET",
|
|
2308
|
+
`${this.dbPath}/changes?${query.toString()}`
|
|
2309
|
+
);
|
|
2310
|
+
return res.data;
|
|
2766
2311
|
}
|
|
2312
|
+
// -------------------------------------------------------------------
|
|
2313
|
+
// Shares (multiplayer v1)
|
|
2314
|
+
// -------------------------------------------------------------------
|
|
2767
2315
|
/**
|
|
2768
|
-
*
|
|
2769
|
-
*
|
|
2770
|
-
* Example: "0.7.0-beta.1" and "0.7.0" are treated as the same version
|
|
2316
|
+
* Shares granted by and received by the caller. App tokens see only
|
|
2317
|
+
* shares involving their own app (the ones they can mount).
|
|
2771
2318
|
*/
|
|
2772
|
-
async
|
|
2773
|
-
const
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2319
|
+
async listShares() {
|
|
2320
|
+
const res = await this.request(
|
|
2321
|
+
"GET",
|
|
2322
|
+
"/account/shares"
|
|
2323
|
+
);
|
|
2324
|
+
return res.data;
|
|
2325
|
+
}
|
|
2326
|
+
// -------------------------------------------------------------------
|
|
2327
|
+
// CRUD on materialized state (REST-mode table API)
|
|
2328
|
+
// -------------------------------------------------------------------
|
|
2329
|
+
async list(table, query) {
|
|
2330
|
+
const qs = query ? `?${new URLSearchParams(query).toString()}` : "";
|
|
2331
|
+
const res = await this.request(
|
|
2332
|
+
"GET",
|
|
2333
|
+
`${this.dbPath}/${encodeURIComponent(table)}${qs}`
|
|
2334
|
+
);
|
|
2335
|
+
return res.data ?? [];
|
|
2336
|
+
}
|
|
2337
|
+
async getRecord(table, id) {
|
|
2338
|
+
try {
|
|
2339
|
+
const res = await this.request(
|
|
2340
|
+
"GET",
|
|
2341
|
+
`${this.dbPath}/${encodeURIComponent(table)}/${encodeURIComponent(id)}`
|
|
2342
|
+
);
|
|
2343
|
+
return res.data ?? null;
|
|
2344
|
+
} catch (err) {
|
|
2345
|
+
if (err instanceof RestError && err.status === 404) return null;
|
|
2346
|
+
throw err;
|
|
2780
2347
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2348
|
+
}
|
|
2349
|
+
/** `POST` — server mints the record id. */
|
|
2350
|
+
async createRecord(table, value) {
|
|
2351
|
+
const res = await this.request(
|
|
2352
|
+
"POST",
|
|
2353
|
+
`${this.dbPath}/${encodeURIComponent(table)}`,
|
|
2354
|
+
{ value }
|
|
2355
|
+
);
|
|
2356
|
+
return res.data;
|
|
2357
|
+
}
|
|
2358
|
+
/** `PUT` — full replace. REST semantics: 404 for missing records. */
|
|
2359
|
+
async putRecord(table, id, value) {
|
|
2360
|
+
try {
|
|
2361
|
+
const res = await this.request(
|
|
2362
|
+
"PUT",
|
|
2363
|
+
`${this.dbPath}/${encodeURIComponent(table)}/${encodeURIComponent(id)}`,
|
|
2364
|
+
{ value }
|
|
2365
|
+
);
|
|
2366
|
+
return res.data ?? null;
|
|
2367
|
+
} catch (err) {
|
|
2368
|
+
if (err instanceof RestError && err.status === 404) return null;
|
|
2369
|
+
throw err;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
/** `PATCH` — partial merge. 404 → null. */
|
|
2373
|
+
async patchRecord(table, id, value) {
|
|
2374
|
+
try {
|
|
2375
|
+
const res = await this.request(
|
|
2376
|
+
"PATCH",
|
|
2377
|
+
`${this.dbPath}/${encodeURIComponent(table)}/${encodeURIComponent(id)}`,
|
|
2378
|
+
{ value }
|
|
2379
|
+
);
|
|
2380
|
+
return res.data ?? null;
|
|
2381
|
+
} catch (err) {
|
|
2382
|
+
if (err instanceof RestError && err.status === 404) return null;
|
|
2383
|
+
throw err;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
/** `DELETE`. Returns false when the record did not exist. */
|
|
2387
|
+
async deleteRecord(table, id) {
|
|
2388
|
+
try {
|
|
2389
|
+
await this.request(
|
|
2390
|
+
"DELETE",
|
|
2391
|
+
`${this.dbPath}/${encodeURIComponent(table)}/${encodeURIComponent(id)}`
|
|
2392
|
+
);
|
|
2393
|
+
return true;
|
|
2394
|
+
} catch (err) {
|
|
2395
|
+
if (err instanceof RestError && err.status === 404) return false;
|
|
2396
|
+
throw err;
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
// -------------------------------------------------------------------
|
|
2400
|
+
// Internals
|
|
2401
|
+
// -------------------------------------------------------------------
|
|
2402
|
+
get dbPath() {
|
|
2403
|
+
return `/account/${encodeURIComponent(this.opts.projectId)}/db`;
|
|
2404
|
+
}
|
|
2405
|
+
/** Authenticated request; retries once with a force-refreshed token on 401. */
|
|
2406
|
+
async request(method, path, body, isRetry = false) {
|
|
2407
|
+
let token;
|
|
2408
|
+
try {
|
|
2409
|
+
token = await this.opts.getToken(isRetry ? { forceRefresh: true } : void 0);
|
|
2410
|
+
} catch (err) {
|
|
2411
|
+
throw new NotAuthenticatedError(
|
|
2412
|
+
err instanceof Error ? err.message : "could not get access token"
|
|
2413
|
+
);
|
|
2414
|
+
}
|
|
2415
|
+
const url = `${this.opts.baseUrl}${path}`;
|
|
2416
|
+
this.opts.log?.("[rest]", method, url);
|
|
2417
|
+
const headers = { Authorization: `Bearer ${token}` };
|
|
2418
|
+
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
2419
|
+
const response = await fetch(url, {
|
|
2420
|
+
method,
|
|
2421
|
+
headers,
|
|
2422
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
2423
|
+
});
|
|
2424
|
+
const responseData = await response.json().catch(() => ({}));
|
|
2425
|
+
if (!response.ok) {
|
|
2426
|
+
if (response.status === 401 && !isRetry) {
|
|
2427
|
+
this.opts.log?.("[rest] 401 \u2014 refreshing token and retrying once");
|
|
2428
|
+
return this.request(method, path, body, true);
|
|
2429
|
+
}
|
|
2430
|
+
const code = typeof responseData.error === "string" ? responseData.error : void 0;
|
|
2431
|
+
const message = typeof responseData.message === "string" && responseData.message || code || `request failed: ${response.status}`;
|
|
2432
|
+
throw new RestError(message, response.status, code, responseData);
|
|
2433
|
+
}
|
|
2434
|
+
return responseData;
|
|
2435
|
+
}
|
|
2436
|
+
};
|
|
2437
|
+
|
|
2438
|
+
// src/core/sync/SyncEngine.ts
|
|
2439
|
+
var import_schema = require("@basictech/schema");
|
|
2440
|
+
|
|
2441
|
+
// src/core/sync/protocol.ts
|
|
2442
|
+
var PROTOCOL_VERSION = 1;
|
|
2443
|
+
var TERMINAL_OP_ERRORS = /* @__PURE__ */ new Set([
|
|
2444
|
+
"SCHEMA_VALIDATION_FAILED",
|
|
2445
|
+
"UNKNOWN_TABLE",
|
|
2446
|
+
"RECORD_NOT_FOUND",
|
|
2447
|
+
"PERMISSION_DENIED",
|
|
2448
|
+
"PAYLOAD_TOO_LARGE",
|
|
2449
|
+
"CHANNEL_FULL",
|
|
2450
|
+
"BAD_MESSAGE"
|
|
2451
|
+
]);
|
|
2452
|
+
function isTerminalOpError(code, terminalFlag) {
|
|
2453
|
+
if (terminalFlag !== void 0) return terminalFlag;
|
|
2454
|
+
return code !== void 0 && TERMINAL_OP_ERRORS.has(code);
|
|
2455
|
+
}
|
|
2456
|
+
function isRebootstrapError(code) {
|
|
2457
|
+
return code === "SNAPSHOT_REQUIRED" || code === "RESET_REQUIRED";
|
|
2458
|
+
}
|
|
2459
|
+
function isRevocationError(code) {
|
|
2460
|
+
return code === "SHARE_REVOKED" || code === "CONNECTION_REVOKED";
|
|
2461
|
+
}
|
|
2462
|
+
function isAuthError(code) {
|
|
2463
|
+
return code === "UNAUTHORIZED" || code === "TOKEN_EXPIRED";
|
|
2464
|
+
}
|
|
2465
|
+
var DEFAULT_LIMITS = {
|
|
2466
|
+
max_ops_per_push: 500,
|
|
2467
|
+
max_op_bytes: 64 * 1024,
|
|
2468
|
+
replay_limit: 1e3
|
|
2469
|
+
};
|
|
2470
|
+
function mintOpId() {
|
|
2471
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
2472
|
+
return crypto.randomUUID();
|
|
2473
|
+
}
|
|
2474
|
+
return "op-" + Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
2475
|
+
}
|
|
2476
|
+
function cloneData(value) {
|
|
2477
|
+
return value === void 0 ? value : JSON.parse(JSON.stringify(value));
|
|
2478
|
+
}
|
|
2479
|
+
function applyOpToData(existing, op) {
|
|
2480
|
+
switch (op.type) {
|
|
2481
|
+
case "put":
|
|
2482
|
+
return cloneData(op.data ?? {});
|
|
2483
|
+
case "patch":
|
|
2484
|
+
if (existing === void 0) return void 0;
|
|
2485
|
+
return { ...existing, ...cloneData(op.data ?? {}) };
|
|
2486
|
+
case "delete":
|
|
2487
|
+
return void 0;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
// src/core/sync/SyncConnection.ts
|
|
2492
|
+
var INITIAL_RECONNECT_DELAY_MS = 500;
|
|
2493
|
+
var MAX_RECONNECT_DELAY_MS = 3e4;
|
|
2494
|
+
var SyncConnection = class {
|
|
2495
|
+
opts;
|
|
2496
|
+
WS;
|
|
2497
|
+
heartbeatMs;
|
|
2498
|
+
ws = null;
|
|
2499
|
+
_status = "idle";
|
|
2500
|
+
stopped = true;
|
|
2501
|
+
reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
2502
|
+
timers = /* @__PURE__ */ new Set();
|
|
2503
|
+
heartbeatTimer = null;
|
|
2504
|
+
/** One forced-refresh reconnect attempt per auth rejection. */
|
|
2505
|
+
authRetryUsed = false;
|
|
2506
|
+
removeOnlineListener = null;
|
|
2507
|
+
constructor(opts) {
|
|
2508
|
+
this.opts = opts;
|
|
2509
|
+
this.WS = opts.WebSocketImpl ?? globalThis.WebSocket;
|
|
2510
|
+
this.heartbeatMs = opts.heartbeatMs ?? 3e4;
|
|
2511
|
+
}
|
|
2512
|
+
get status() {
|
|
2513
|
+
return this._status;
|
|
2514
|
+
}
|
|
2515
|
+
get isOnline() {
|
|
2516
|
+
return this._status === "online" && this.ws?.readyState === 1;
|
|
2517
|
+
}
|
|
2518
|
+
start() {
|
|
2519
|
+
if (!this.stopped && this.ws) return;
|
|
2520
|
+
this.stopped = false;
|
|
2521
|
+
this.authRetryUsed = false;
|
|
2522
|
+
this.listenForNetwork();
|
|
2523
|
+
this.open();
|
|
2524
|
+
}
|
|
2525
|
+
stop() {
|
|
2526
|
+
this.stopped = true;
|
|
2527
|
+
this.clearTimers();
|
|
2528
|
+
this.removeOnlineListener?.();
|
|
2529
|
+
this.removeOnlineListener = null;
|
|
2530
|
+
if (this.ws) {
|
|
2531
|
+
try {
|
|
2532
|
+
this.ws.close();
|
|
2533
|
+
} catch {
|
|
2534
|
+
}
|
|
2535
|
+
this.ws = null;
|
|
2536
|
+
}
|
|
2537
|
+
this.setStatus("stopped");
|
|
2538
|
+
}
|
|
2539
|
+
/** Send a message; returns false when the socket is not open. */
|
|
2540
|
+
send(msg) {
|
|
2541
|
+
if (this.ws?.readyState === 1) {
|
|
2542
|
+
this.ws.send(JSON.stringify(msg));
|
|
2543
|
+
return true;
|
|
2544
|
+
}
|
|
2545
|
+
return false;
|
|
2546
|
+
}
|
|
2547
|
+
/** Refresh auth on the live socket (no reconnect). */
|
|
2548
|
+
async sendToken() {
|
|
2549
|
+
if (!this.isOnline) return;
|
|
2550
|
+
try {
|
|
2551
|
+
const token = await this.opts.getToken();
|
|
2552
|
+
this.send({ type: "token", token });
|
|
2553
|
+
} catch {
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
/**
|
|
2557
|
+
* The server rejected our token (`UNAUTHORIZED` / `TOKEN_EXPIRED` + close).
|
|
2558
|
+
* Retry once with a force-refreshed token; give up (status `auth_failed`)
|
|
2559
|
+
* when the refresh itself fails or a fresh token is rejected again.
|
|
2560
|
+
*/
|
|
2561
|
+
handleAuthRejection() {
|
|
2562
|
+
if (this.stopped) return;
|
|
2563
|
+
if (this.authRetryUsed) {
|
|
2564
|
+
this.log("fresh token rejected \u2014 giving up until reauth");
|
|
2565
|
+
this.stopped = true;
|
|
2566
|
+
this.clearTimers();
|
|
2567
|
+
this.setStatus("auth_failed");
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
this.authRetryUsed = true;
|
|
2571
|
+
this.log("token rejected \u2014 force refreshing and reconnecting");
|
|
2572
|
+
void (async () => {
|
|
2573
|
+
try {
|
|
2574
|
+
await this.opts.getToken({ forceRefresh: true });
|
|
2575
|
+
this.scheduleReconnect(0);
|
|
2576
|
+
} catch (err) {
|
|
2577
|
+
this.log("token refresh failed after auth rejection:", err);
|
|
2578
|
+
this.stopped = true;
|
|
2579
|
+
this.setStatus("auth_failed");
|
|
2580
|
+
}
|
|
2581
|
+
})();
|
|
2582
|
+
}
|
|
2583
|
+
// -------------------------------------------------------------------
|
|
2584
|
+
// Internals
|
|
2585
|
+
// -------------------------------------------------------------------
|
|
2586
|
+
open() {
|
|
2587
|
+
if (this.stopped) return;
|
|
2588
|
+
if (this.ws && (this.ws.readyState === 0 || this.ws.readyState === 1)) return;
|
|
2589
|
+
if (!this.WS) {
|
|
2590
|
+
this.log("no WebSocket implementation available");
|
|
2591
|
+
this.setStatus("offline");
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
this.setStatus("connecting");
|
|
2595
|
+
const ws = new this.WS(this.opts.wsUrl);
|
|
2596
|
+
this.ws = ws;
|
|
2597
|
+
ws.onopen = () => {
|
|
2598
|
+
void (async () => {
|
|
2599
|
+
try {
|
|
2600
|
+
const token = await this.opts.getToken();
|
|
2601
|
+
if (this.ws !== ws || ws.readyState !== 1) return;
|
|
2602
|
+
const hello = { type: "hello", version: PROTOCOL_VERSION, token };
|
|
2603
|
+
ws.send(JSON.stringify(hello));
|
|
2604
|
+
} catch (err) {
|
|
2605
|
+
this.log("could not get token for hello:", err);
|
|
2606
|
+
try {
|
|
2607
|
+
ws.close();
|
|
2608
|
+
} catch {
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
})();
|
|
2612
|
+
};
|
|
2613
|
+
ws.onmessage = (event) => {
|
|
2614
|
+
let msg;
|
|
2615
|
+
try {
|
|
2616
|
+
const text = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data);
|
|
2617
|
+
msg = JSON.parse(text);
|
|
2618
|
+
} catch {
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
this.handleMessage(msg);
|
|
2622
|
+
};
|
|
2623
|
+
ws.onclose = () => {
|
|
2624
|
+
if (this.ws !== ws) return;
|
|
2625
|
+
this.ws = null;
|
|
2626
|
+
this.stopHeartbeat();
|
|
2627
|
+
if (this.stopped) return;
|
|
2628
|
+
this.setStatus("offline");
|
|
2629
|
+
this.scheduleReconnect();
|
|
2630
|
+
};
|
|
2631
|
+
ws.onerror = () => {
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
handleMessage(msg) {
|
|
2635
|
+
if (msg.type === "welcome") {
|
|
2636
|
+
this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
2637
|
+
this.authRetryUsed = false;
|
|
2638
|
+
this.setStatus("online");
|
|
2639
|
+
this.startHeartbeat();
|
|
2640
|
+
this.opts.onWelcome(msg);
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
if (msg.type === "error" && !("sub" in msg && msg.sub) && isAuthError(msg.code)) {
|
|
2644
|
+
this.handleAuthRejection();
|
|
2645
|
+
this.opts.onMessage(msg);
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
this.opts.onMessage(msg);
|
|
2649
|
+
}
|
|
2650
|
+
startHeartbeat() {
|
|
2651
|
+
this.stopHeartbeat();
|
|
2652
|
+
const tick = () => {
|
|
2653
|
+
if (this.stopped) return;
|
|
2654
|
+
this.send({ type: "ping" });
|
|
2655
|
+
this.heartbeatTimer = setTimeout(tick, this.heartbeatMs);
|
|
2656
|
+
this.timers.add(this.heartbeatTimer);
|
|
2657
|
+
};
|
|
2658
|
+
this.heartbeatTimer = setTimeout(tick, this.heartbeatMs);
|
|
2659
|
+
this.timers.add(this.heartbeatTimer);
|
|
2660
|
+
}
|
|
2661
|
+
stopHeartbeat() {
|
|
2662
|
+
if (this.heartbeatTimer) {
|
|
2663
|
+
clearTimeout(this.heartbeatTimer);
|
|
2664
|
+
this.timers.delete(this.heartbeatTimer);
|
|
2665
|
+
this.heartbeatTimer = null;
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
scheduleReconnect(delayOverride) {
|
|
2669
|
+
if (this.stopped) return;
|
|
2670
|
+
const delay = delayOverride ?? this.reconnectDelay;
|
|
2671
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
|
|
2672
|
+
this.log(`reconnecting in ${delay}ms`);
|
|
2673
|
+
const timer = setTimeout(() => {
|
|
2674
|
+
this.timers.delete(timer);
|
|
2675
|
+
this.open();
|
|
2676
|
+
}, delay);
|
|
2677
|
+
this.timers.add(timer);
|
|
2678
|
+
}
|
|
2679
|
+
listenForNetwork() {
|
|
2680
|
+
if (this.removeOnlineListener || typeof window === "undefined") return;
|
|
2681
|
+
const handleOnline = () => {
|
|
2682
|
+
if (this.stopped) return;
|
|
2683
|
+
this.log("network online \u2014 reconnecting immediately");
|
|
2684
|
+
this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
2685
|
+
this.open();
|
|
2686
|
+
};
|
|
2687
|
+
window.addEventListener("online", handleOnline);
|
|
2688
|
+
this.removeOnlineListener = () => window.removeEventListener("online", handleOnline);
|
|
2689
|
+
}
|
|
2690
|
+
clearTimers() {
|
|
2691
|
+
for (const t of this.timers) clearTimeout(t);
|
|
2692
|
+
this.timers.clear();
|
|
2693
|
+
this.heartbeatTimer = null;
|
|
2694
|
+
}
|
|
2695
|
+
setStatus(status) {
|
|
2696
|
+
if (this._status === status) return;
|
|
2697
|
+
this._status = status;
|
|
2698
|
+
this.opts.onStatus(status);
|
|
2699
|
+
}
|
|
2700
|
+
log(...args) {
|
|
2701
|
+
this.opts.log?.("[sync-connection]", ...args);
|
|
2702
|
+
}
|
|
2703
|
+
};
|
|
2704
|
+
|
|
2705
|
+
// src/core/sync/SyncStore.ts
|
|
2706
|
+
var import_dexie = __toESM(require("dexie"));
|
|
2707
|
+
var META_CURSOR = "cursor";
|
|
2708
|
+
var META_CHANNEL = "channel";
|
|
2709
|
+
var META_OWNER = "owner_did";
|
|
2710
|
+
var SyncStore = class {
|
|
2711
|
+
db;
|
|
2712
|
+
name;
|
|
2713
|
+
tableNames;
|
|
2714
|
+
constructor(name, schema) {
|
|
2715
|
+
this.name = name;
|
|
2716
|
+
this.tableNames = Object.keys(schema.tables);
|
|
2717
|
+
this.db = new import_dexie.default(name);
|
|
2718
|
+
const stores = {
|
|
2719
|
+
_server: "[table+record_id], table",
|
|
2720
|
+
_pending: "++idx, op_id",
|
|
2721
|
+
_rejected: "++idx, op_id",
|
|
2722
|
+
_meta: "key"
|
|
2723
|
+
};
|
|
2724
|
+
for (const [tableName, table] of Object.entries(schema.tables)) {
|
|
2725
|
+
const indexed = Object.entries(table.fields).filter(([, f]) => f.indexed).map(([fieldName]) => `,${fieldName}`).join("");
|
|
2726
|
+
stores[tableName] = "id" + indexed;
|
|
2727
|
+
}
|
|
2728
|
+
this.db.version(Math.max(schema.version ?? 1, 1)).stores(stores);
|
|
2729
|
+
}
|
|
2730
|
+
/** The Dexie view table for an app table (what liveQuery reads). */
|
|
2731
|
+
view(table) {
|
|
2732
|
+
return this.db.table(table);
|
|
2733
|
+
}
|
|
2734
|
+
hasTable(table) {
|
|
2735
|
+
return this.tableNames.includes(table);
|
|
2736
|
+
}
|
|
2737
|
+
get tables() {
|
|
2738
|
+
return [...this.tableNames];
|
|
2739
|
+
}
|
|
2740
|
+
get server() {
|
|
2741
|
+
return this.db.table("_server");
|
|
2742
|
+
}
|
|
2743
|
+
get pending() {
|
|
2744
|
+
return this.db.table("_pending");
|
|
2745
|
+
}
|
|
2746
|
+
get rejected() {
|
|
2747
|
+
return this.db.table("_rejected");
|
|
2748
|
+
}
|
|
2749
|
+
get meta() {
|
|
2750
|
+
return this.db.table("_meta");
|
|
2751
|
+
}
|
|
2752
|
+
get allStores() {
|
|
2753
|
+
return ["_server", "_pending", "_rejected", "_meta", ...this.tableNames];
|
|
2754
|
+
}
|
|
2755
|
+
// -------------------------------------------------------------------
|
|
2756
|
+
// Meta
|
|
2757
|
+
// -------------------------------------------------------------------
|
|
2758
|
+
async getCursor() {
|
|
2759
|
+
const row = await this.meta.get(META_CURSOR);
|
|
2760
|
+
return typeof row?.value === "number" ? row.value : null;
|
|
2761
|
+
}
|
|
2762
|
+
async getChannel() {
|
|
2763
|
+
const row = await this.meta.get(META_CHANNEL);
|
|
2764
|
+
return typeof row?.value === "string" ? row.value : null;
|
|
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
|
+
}
|
|
2792
|
+
// -------------------------------------------------------------------
|
|
2793
|
+
// Pending / rejected
|
|
2794
|
+
// -------------------------------------------------------------------
|
|
2795
|
+
/** All pending ops in creation order (used to warm the in-memory queue). */
|
|
2796
|
+
async loadPending() {
|
|
2797
|
+
return this.pending.orderBy("idx").toArray();
|
|
2798
|
+
}
|
|
2799
|
+
async listRejected() {
|
|
2800
|
+
return this.rejected.orderBy("idx").toArray();
|
|
2801
|
+
}
|
|
2802
|
+
async clearRejected() {
|
|
2803
|
+
await this.rejected.clear();
|
|
2804
|
+
}
|
|
2805
|
+
/** Record a server ack for a pending op (echo not yet seen). */
|
|
2806
|
+
async markAcked(opId, seq) {
|
|
2807
|
+
await this.pending.where("op_id").equals(opId).modify({ acked_seq: seq });
|
|
2808
|
+
}
|
|
2809
|
+
// -------------------------------------------------------------------
|
|
2810
|
+
// Writes
|
|
2811
|
+
// -------------------------------------------------------------------
|
|
2812
|
+
/**
|
|
2813
|
+
* Enqueue a local op and apply it optimistically to the view.
|
|
2814
|
+
* Returns the resulting view record (null when the op deletes it).
|
|
2815
|
+
*/
|
|
2816
|
+
async addPending(op) {
|
|
2817
|
+
return this.db.transaction("rw", this.allStores, async () => {
|
|
2818
|
+
await this.pending.add({ op_id: op.op_id, op: cloneData(op) });
|
|
2819
|
+
return this.recomputeViewRecord(op.table, op.record_id);
|
|
2820
|
+
});
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Commit a batch of incoming server ops (already filtered/deduped by the
|
|
2824
|
+
* engine): update `_server`, drop confirmed pending ops, advance the
|
|
2825
|
+
* cursor, and rebase every affected view record — in one transaction.
|
|
2826
|
+
*/
|
|
2827
|
+
async commitIncoming(params) {
|
|
2828
|
+
const { applyOps, confirmedOpIds, cursor } = params;
|
|
2829
|
+
await this.db.transaction("rw", this.allStores, async () => {
|
|
2830
|
+
const affected = /* @__PURE__ */ new Set();
|
|
2831
|
+
for (const op of applyOps) affected.add(`${op.table}\0${op.record_id}`);
|
|
2832
|
+
for (const op of applyOps) {
|
|
2833
|
+
await this.applyToServer(op);
|
|
2834
|
+
}
|
|
2835
|
+
if (confirmedOpIds.length > 0) {
|
|
2836
|
+
const confirmedRows = await this.pending.where("op_id").anyOf(confirmedOpIds).toArray();
|
|
2837
|
+
for (const row of confirmedRows) affected.add(`${row.op.table}\0${row.op.record_id}`);
|
|
2838
|
+
await this.pending.where("op_id").anyOf(confirmedOpIds).delete();
|
|
2839
|
+
}
|
|
2840
|
+
await this.meta.put({ key: META_CURSOR, value: cursor });
|
|
2841
|
+
for (const key of affected) {
|
|
2842
|
+
const [table, recordId] = key.split("\0");
|
|
2843
|
+
await this.recomputeViewRecord(table, recordId);
|
|
2844
|
+
}
|
|
2845
|
+
});
|
|
2846
|
+
}
|
|
2847
|
+
/** Persist a cursor advance with no ops (empty `ops` message / pushed cursor). */
|
|
2848
|
+
async setCursor(cursor) {
|
|
2849
|
+
await this.meta.put({ key: META_CURSOR, value: cursor });
|
|
2850
|
+
}
|
|
2851
|
+
/**
|
|
2852
|
+
* Terminal rejection: remove from pending, park in the rejected store,
|
|
2853
|
+
* roll the view record back to server state + remaining pending ops.
|
|
2854
|
+
*/
|
|
2855
|
+
async rejectPending(opId, error, message) {
|
|
2856
|
+
return this.db.transaction("rw", this.allStores, async () => {
|
|
2857
|
+
const row = await this.pending.where("op_id").equals(opId).first();
|
|
2858
|
+
if (!row) return null;
|
|
2859
|
+
await this.pending.where("op_id").equals(opId).delete();
|
|
2860
|
+
const rejectedRow = {
|
|
2861
|
+
op_id: opId,
|
|
2862
|
+
op: row.op,
|
|
2863
|
+
error,
|
|
2864
|
+
message,
|
|
2865
|
+
rejected_at: Date.now()
|
|
2866
|
+
};
|
|
2867
|
+
await this.rejected.add(rejectedRow);
|
|
2868
|
+
await this.recomputeViewRecord(row.op.table, row.op.record_id);
|
|
2869
|
+
return rejectedRow;
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
// -------------------------------------------------------------------
|
|
2873
|
+
// Bootstrap
|
|
2874
|
+
// -------------------------------------------------------------------
|
|
2875
|
+
/**
|
|
2876
|
+
* Replace all server state from a snapshot (cold start or
|
|
2877
|
+
* SNAPSHOT_REQUIRED/RESET_REQUIRED recovery). Pending ops survive and are
|
|
2878
|
+
* re-applied on top of the fresh state.
|
|
2879
|
+
*/
|
|
2880
|
+
async replaceFromSnapshot(params) {
|
|
2881
|
+
const { channel, records, cursor } = params;
|
|
2882
|
+
await this.db.transaction("rw", this.allStores, async () => {
|
|
2883
|
+
await this.server.clear();
|
|
2884
|
+
for (const tableName of this.tableNames) {
|
|
2885
|
+
await this.view(tableName).clear();
|
|
2886
|
+
}
|
|
2887
|
+
for (const [tableName, tableRecords] of Object.entries(records)) {
|
|
2888
|
+
if (!this.hasTable(tableName)) continue;
|
|
2889
|
+
const serverRows = [];
|
|
2890
|
+
const viewRows = [];
|
|
2891
|
+
for (const [recordId, data] of Object.entries(tableRecords)) {
|
|
2892
|
+
serverRows.push({ table: tableName, record_id: recordId, data: data ?? {} });
|
|
2893
|
+
viewRows.push({ id: recordId, ...data ?? {} });
|
|
2894
|
+
}
|
|
2895
|
+
await this.server.bulkPut(serverRows);
|
|
2896
|
+
await this.view(tableName).bulkPut(viewRows);
|
|
2897
|
+
}
|
|
2898
|
+
const pendingRows = await this.pending.orderBy("idx").toArray();
|
|
2899
|
+
const affected = /* @__PURE__ */ new Set();
|
|
2900
|
+
for (const row of pendingRows) affected.add(`${row.op.table}\0${row.op.record_id}`);
|
|
2901
|
+
for (const key of affected) {
|
|
2902
|
+
const [table, recordId] = key.split("\0");
|
|
2903
|
+
if (this.hasTable(table)) await this.recomputeViewRecord(table, recordId);
|
|
2904
|
+
}
|
|
2905
|
+
await this.meta.put({ key: META_CURSOR, value: cursor });
|
|
2906
|
+
await this.meta.put({ key: META_CHANNEL, value: channel });
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
// -------------------------------------------------------------------
|
|
2910
|
+
// Reads
|
|
2911
|
+
// -------------------------------------------------------------------
|
|
2912
|
+
async getViewRecord(table, id) {
|
|
2913
|
+
const record = await this.view(table).get(id);
|
|
2914
|
+
return record ?? null;
|
|
2915
|
+
}
|
|
2916
|
+
async getViewRecords(table) {
|
|
2917
|
+
return this.view(table).toArray();
|
|
2918
|
+
}
|
|
2919
|
+
// -------------------------------------------------------------------
|
|
2920
|
+
// Lifecycle
|
|
2921
|
+
// -------------------------------------------------------------------
|
|
2922
|
+
close() {
|
|
2923
|
+
this.db.close();
|
|
2924
|
+
}
|
|
2925
|
+
/** Delete the underlying IndexedDB database (sign-out / revoked mount). */
|
|
2926
|
+
async destroy() {
|
|
2927
|
+
this.db.close();
|
|
2928
|
+
await import_dexie.default.delete(this.name);
|
|
2929
|
+
}
|
|
2930
|
+
// -------------------------------------------------------------------
|
|
2931
|
+
// Internals
|
|
2932
|
+
// -------------------------------------------------------------------
|
|
2933
|
+
async applyToServer(op) {
|
|
2934
|
+
if (!this.hasTable(op.table)) return;
|
|
2935
|
+
const existing = await this.server.get([op.table, op.record_id]);
|
|
2936
|
+
const next = applyOpToData(existing?.data, op);
|
|
2937
|
+
if (next === void 0) {
|
|
2938
|
+
await this.server.delete([op.table, op.record_id]);
|
|
2939
|
+
} else {
|
|
2940
|
+
await this.server.put({ table: op.table, record_id: op.record_id, data: next });
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
/**
|
|
2944
|
+
* Rebase one record: view = server data + pending ops for that record in
|
|
2945
|
+
* creation order. Must run inside a transaction covering all stores.
|
|
2946
|
+
*/
|
|
2947
|
+
async recomputeViewRecord(table, recordId) {
|
|
2948
|
+
if (!this.hasTable(table)) return null;
|
|
2949
|
+
const serverRow = await this.server.get([table, recordId]);
|
|
2950
|
+
let data = serverRow ? cloneData(serverRow.data) : void 0;
|
|
2951
|
+
const pendingRows = await this.pending.orderBy("idx").toArray();
|
|
2952
|
+
for (const row of pendingRows) {
|
|
2953
|
+
if (row.op.table === table && row.op.record_id === recordId) {
|
|
2954
|
+
data = applyOpToData(data, row.op);
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
if (data === void 0) {
|
|
2958
|
+
await this.view(table).delete(recordId);
|
|
2959
|
+
return null;
|
|
2960
|
+
}
|
|
2961
|
+
const viewRecord = { id: recordId, ...data };
|
|
2962
|
+
await this.view(table).put(viewRecord);
|
|
2963
|
+
return viewRecord;
|
|
2964
|
+
}
|
|
2965
|
+
};
|
|
2966
|
+
|
|
2967
|
+
// src/core/sync/SyncEngine.ts
|
|
2968
|
+
var OWN_SUB = "own";
|
|
2969
|
+
function shareSubKey(shareId) {
|
|
2970
|
+
return `share:${shareId}`;
|
|
2971
|
+
}
|
|
2972
|
+
var BoundedSet = class {
|
|
2973
|
+
constructor(cap = 2048) {
|
|
2974
|
+
this.cap = cap;
|
|
2975
|
+
}
|
|
2976
|
+
set = /* @__PURE__ */ new Set();
|
|
2977
|
+
order = [];
|
|
2978
|
+
has(value) {
|
|
2979
|
+
return this.set.has(value);
|
|
2980
|
+
}
|
|
2981
|
+
add(value) {
|
|
2982
|
+
if (this.set.has(value)) return;
|
|
2983
|
+
this.set.add(value);
|
|
2984
|
+
this.order.push(value);
|
|
2985
|
+
if (this.order.length > this.cap) {
|
|
2986
|
+
const evicted = this.order.shift();
|
|
2987
|
+
if (evicted !== void 0) this.set.delete(evicted);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
clear() {
|
|
2991
|
+
this.set.clear();
|
|
2992
|
+
this.order = [];
|
|
2993
|
+
}
|
|
2994
|
+
};
|
|
2995
|
+
var RETRY_FLUSH_DELAY_MS = 1200;
|
|
2996
|
+
var SyncEngine = class {
|
|
2997
|
+
projectId;
|
|
2998
|
+
schema;
|
|
2999
|
+
opts;
|
|
3000
|
+
connection;
|
|
3001
|
+
subs = /* @__PURE__ */ new Map();
|
|
3002
|
+
limits = { ...DEFAULT_LIMITS };
|
|
3003
|
+
actor = null;
|
|
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;
|
|
3009
|
+
revokedInfo = null;
|
|
3010
|
+
connectionStatus = "idle";
|
|
3011
|
+
_status = "idle";
|
|
3012
|
+
listeners = /* @__PURE__ */ new Map();
|
|
3013
|
+
timers = /* @__PURE__ */ new Set();
|
|
3014
|
+
constructor(opts) {
|
|
3015
|
+
this.opts = opts;
|
|
3016
|
+
this.projectId = opts.projectId;
|
|
3017
|
+
this.schema = opts.schema;
|
|
3018
|
+
this.connection = new SyncConnection({
|
|
3019
|
+
wsUrl: opts.wsUrl,
|
|
3020
|
+
getToken: opts.getToken,
|
|
3021
|
+
WebSocketImpl: opts.WebSocketImpl,
|
|
3022
|
+
heartbeatMs: opts.heartbeatMs,
|
|
3023
|
+
onWelcome: (msg) => this.handleWelcome(msg),
|
|
3024
|
+
onMessage: (msg) => this.handleMessage(msg),
|
|
3025
|
+
onStatus: (status) => this.handleConnectionStatus(status),
|
|
3026
|
+
log: opts.log
|
|
3027
|
+
});
|
|
3028
|
+
}
|
|
3029
|
+
// -------------------------------------------------------------------
|
|
3030
|
+
// Events
|
|
3031
|
+
// -------------------------------------------------------------------
|
|
3032
|
+
on(event, fn) {
|
|
3033
|
+
if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
3034
|
+
const set = this.listeners.get(event);
|
|
3035
|
+
set.add(fn);
|
|
3036
|
+
return () => set.delete(fn);
|
|
3037
|
+
}
|
|
3038
|
+
emit(event, data) {
|
|
3039
|
+
const set = this.listeners.get(event);
|
|
3040
|
+
if (!set) return;
|
|
3041
|
+
for (const fn of set) {
|
|
3042
|
+
try {
|
|
3043
|
+
fn(data);
|
|
3044
|
+
} catch (err) {
|
|
3045
|
+
this.log("listener error:", err);
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
// -------------------------------------------------------------------
|
|
3050
|
+
// Public state
|
|
3051
|
+
// -------------------------------------------------------------------
|
|
3052
|
+
get status() {
|
|
3053
|
+
return this._status;
|
|
3054
|
+
}
|
|
3055
|
+
get syncLimits() {
|
|
3056
|
+
return { ...this.limits };
|
|
3057
|
+
}
|
|
3058
|
+
get serverActor() {
|
|
3059
|
+
return this.actor;
|
|
3060
|
+
}
|
|
3061
|
+
getSubscription(key) {
|
|
3062
|
+
return this.subs.get(key);
|
|
3063
|
+
}
|
|
3064
|
+
get own() {
|
|
3065
|
+
return this.subs.get(OWN_SUB);
|
|
3066
|
+
}
|
|
3067
|
+
get pendingCount() {
|
|
3068
|
+
let n = 0;
|
|
3069
|
+
for (const sub of this.subs.values()) n += sub.pending.length;
|
|
3070
|
+
return n;
|
|
3071
|
+
}
|
|
3072
|
+
async listRejected(subKey = OWN_SUB) {
|
|
3073
|
+
const sub = this.subs.get(subKey);
|
|
3074
|
+
if (!sub) return [];
|
|
3075
|
+
return sub.store.listRejected();
|
|
3076
|
+
}
|
|
3077
|
+
async clearRejected(subKey = OWN_SUB) {
|
|
3078
|
+
await this.subs.get(subKey)?.store.clearRejected();
|
|
3079
|
+
}
|
|
3080
|
+
// -------------------------------------------------------------------
|
|
3081
|
+
// Lifecycle
|
|
3082
|
+
// -------------------------------------------------------------------
|
|
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();
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
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
|
+
});
|
|
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;
|
|
3119
|
+
this.connection.start();
|
|
3120
|
+
this.recomputeStatus();
|
|
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
|
+
}
|
|
3149
|
+
/** Close the socket and stores; local data is kept. */
|
|
3150
|
+
stop() {
|
|
3151
|
+
this.connectIntended = false;
|
|
3152
|
+
this.storesOpen = false;
|
|
3153
|
+
this.connection.stop();
|
|
3154
|
+
for (const t of this.timers) clearTimeout(t);
|
|
3155
|
+
this.timers.clear();
|
|
3156
|
+
for (const sub of this.subs.values()) {
|
|
3157
|
+
sub.active = false;
|
|
3158
|
+
sub.store.close();
|
|
3159
|
+
}
|
|
3160
|
+
this.subs.clear();
|
|
3161
|
+
this.recomputeStatus();
|
|
3162
|
+
}
|
|
3163
|
+
/**
|
|
3164
|
+
* Stop and delete every local database for this project (sign-out).
|
|
3165
|
+
* Best-effort discovery of mount keyspaces from previous sessions.
|
|
3166
|
+
*/
|
|
3167
|
+
async destroyLocal() {
|
|
3168
|
+
const open = [...this.subs.values()];
|
|
3169
|
+
this.stop();
|
|
3170
|
+
for (const sub of open) {
|
|
3171
|
+
try {
|
|
3172
|
+
await sub.store.destroy();
|
|
3173
|
+
} catch (err) {
|
|
3174
|
+
this.log("failed deleting local db:", err);
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
try {
|
|
3178
|
+
const idb = globalThis.indexedDB;
|
|
3179
|
+
if (idb && typeof idb.databases === "function") {
|
|
3180
|
+
const base = this.baseDbName;
|
|
3181
|
+
const dbs = await idb.databases();
|
|
3182
|
+
for (const info of dbs) {
|
|
3183
|
+
if (info.name && (info.name === base || info.name.startsWith(`${base}:share:`))) {
|
|
3184
|
+
await new Promise((resolve) => {
|
|
3185
|
+
const req = idb.deleteDatabase(info.name);
|
|
3186
|
+
req.onsuccess = req.onerror = req.onblocked = () => resolve();
|
|
3187
|
+
});
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
} catch {
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
// -------------------------------------------------------------------
|
|
3195
|
+
// Shares (mounts)
|
|
3196
|
+
// -------------------------------------------------------------------
|
|
3197
|
+
/**
|
|
3198
|
+
* Mount a share: separate keyspace `(project, share)` with its own cursor
|
|
3199
|
+
* and pending queue. Bootstraps + subscribes when the socket is online.
|
|
3200
|
+
*/
|
|
3201
|
+
async mountShare(shareId) {
|
|
3202
|
+
const key = shareSubKey(shareId);
|
|
3203
|
+
const existing = this.subs.get(key);
|
|
3204
|
+
if (existing) return existing;
|
|
3205
|
+
const sub = await this.openSub(key, shareId);
|
|
3206
|
+
this.subs.set(key, sub);
|
|
3207
|
+
if (this.connection.isOnline) {
|
|
3208
|
+
this.enqueue(sub, () => this.activateSub(sub));
|
|
3209
|
+
}
|
|
3210
|
+
return sub;
|
|
3211
|
+
}
|
|
3212
|
+
/** Unsubscribe a mount. Local cache is kept unless `purge` is set. */
|
|
3213
|
+
async unmountShare(shareId, options) {
|
|
3214
|
+
const key = shareSubKey(shareId);
|
|
3215
|
+
const sub = this.subs.get(key);
|
|
3216
|
+
if (!sub) return;
|
|
3217
|
+
if (sub.active) {
|
|
3218
|
+
this.connection.send({ type: "unsubscribe", sub: key });
|
|
3219
|
+
}
|
|
3220
|
+
this.subs.delete(key);
|
|
3221
|
+
if (options?.purge) {
|
|
3222
|
+
await sub.store.destroy();
|
|
3223
|
+
} else {
|
|
3224
|
+
sub.store.close();
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
// -------------------------------------------------------------------
|
|
3228
|
+
// Writes (responsibility 2 + 6: pending queue + optimistic rebase)
|
|
3229
|
+
// -------------------------------------------------------------------
|
|
3230
|
+
/**
|
|
3231
|
+
* Queue a local op, apply it optimistically, and push when online.
|
|
3232
|
+
* Returns the resulting view record (null when deleted).
|
|
3233
|
+
* Throws on local validation failure (fail fast — the server would
|
|
3234
|
+
* terminally reject it anyway).
|
|
3235
|
+
*/
|
|
3236
|
+
async apply(subKey, partial) {
|
|
3237
|
+
const sub = this.subs.get(subKey);
|
|
3238
|
+
if (!sub) throw new Error(`unknown subscription '${subKey}' \u2014 is the engine started?`);
|
|
3239
|
+
if (!sub.store.hasTable(partial.table)) {
|
|
3240
|
+
throw new Error(`table "${partial.table}" not found in schema`);
|
|
3241
|
+
}
|
|
3242
|
+
if (this.opts.validateWrites !== false && partial.type !== "delete") {
|
|
3243
|
+
const result = (0, import_schema.validateData)(
|
|
3244
|
+
this.schema,
|
|
3245
|
+
partial.table,
|
|
3246
|
+
partial.data ?? {},
|
|
3247
|
+
partial.type === "put"
|
|
3248
|
+
);
|
|
3249
|
+
if (!result.valid) {
|
|
3250
|
+
throw new Error(result.message || "data validation failed");
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
const op = {
|
|
3254
|
+
op_id: mintOpId(),
|
|
3255
|
+
type: partial.type,
|
|
3256
|
+
table: partial.table,
|
|
3257
|
+
record_id: partial.record_id,
|
|
3258
|
+
...partial.type !== "delete" ? { data: partial.data ?? {} } : {},
|
|
3259
|
+
base_seq: Math.max(sub.cursor, 0)
|
|
3260
|
+
};
|
|
3261
|
+
const bytes = JSON.stringify(op).length;
|
|
3262
|
+
if (bytes > this.limits.max_op_bytes) {
|
|
3263
|
+
throw new Error(
|
|
3264
|
+
`op exceeds max_op_bytes (${bytes} > ${this.limits.max_op_bytes}) \u2014 PAYLOAD_TOO_LARGE`
|
|
3265
|
+
);
|
|
3266
|
+
}
|
|
3267
|
+
let view = null;
|
|
3268
|
+
await this.enqueue(sub, async () => {
|
|
3269
|
+
view = await sub.store.addPending(op);
|
|
3270
|
+
sub.pending.push({ op, sent: false });
|
|
3271
|
+
});
|
|
3272
|
+
this.emit("change", { sub: sub.key, tables: [op.table] });
|
|
3273
|
+
this.flush(sub);
|
|
3274
|
+
return view;
|
|
3275
|
+
}
|
|
3276
|
+
// -------------------------------------------------------------------
|
|
3277
|
+
// Connection handling
|
|
3278
|
+
// -------------------------------------------------------------------
|
|
3279
|
+
handleConnectionStatus(status) {
|
|
3280
|
+
this.connectionStatus = status;
|
|
3281
|
+
if (status === "offline" || status === "connecting" || status === "auth_failed" || status === "stopped") {
|
|
3282
|
+
for (const sub of this.subs.values()) {
|
|
3283
|
+
sub.active = false;
|
|
3284
|
+
for (const p of sub.pending) {
|
|
3285
|
+
p.sent = false;
|
|
3286
|
+
p.ackedSeq = void 0;
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
this.recomputeStatus();
|
|
3291
|
+
}
|
|
3292
|
+
handleWelcome(msg) {
|
|
3293
|
+
this.actor = msg.actor;
|
|
3294
|
+
if (msg.limits) this.limits = { ...this.limits, ...msg.limits };
|
|
3295
|
+
for (const sub of this.subs.values()) {
|
|
3296
|
+
if (sub.status === "revoked") continue;
|
|
3297
|
+
this.enqueue(sub, () => this.activateSub(sub));
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
handleMessage(msg) {
|
|
3301
|
+
switch (msg.type) {
|
|
3302
|
+
case "subscribed":
|
|
3303
|
+
this.handleSubscribed(msg);
|
|
3304
|
+
return;
|
|
3305
|
+
case "ops": {
|
|
3306
|
+
const sub = this.subs.get(msg.sub);
|
|
3307
|
+
if (sub) this.enqueue(sub, () => this.processOps(sub, msg));
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
case "pushed": {
|
|
3311
|
+
const sub = this.subs.get(msg.sub);
|
|
3312
|
+
if (sub) this.enqueue(sub, () => this.processPushed(sub, msg));
|
|
3313
|
+
return;
|
|
3314
|
+
}
|
|
3315
|
+
case "error":
|
|
3316
|
+
this.handleError(msg);
|
|
3317
|
+
return;
|
|
3318
|
+
case "pong":
|
|
3319
|
+
case "token_ok":
|
|
3320
|
+
case "unsubscribed":
|
|
3321
|
+
case "welcome":
|
|
3322
|
+
return;
|
|
3323
|
+
default:
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
handleSubscribed(msg) {
|
|
3328
|
+
const sub = this.subs.get(msg.sub);
|
|
3329
|
+
if (!sub) return;
|
|
3330
|
+
sub.active = true;
|
|
3331
|
+
sub.status = "live";
|
|
3332
|
+
sub.schemaVersion = msg.schema_version;
|
|
3333
|
+
this.log(`subscribed '${sub.key}' channel=${msg.channel} cursor=${msg.cursor} head=${msg.head}`);
|
|
3334
|
+
this.flush(sub);
|
|
3335
|
+
}
|
|
3336
|
+
handleError(msg) {
|
|
3337
|
+
const subKey = msg.sub;
|
|
3338
|
+
this.log(`server error${subKey ? ` (sub ${subKey})` : ""}: ${msg.code} \u2014 ${msg.message ?? ""}`);
|
|
3339
|
+
if (subKey) {
|
|
3340
|
+
const sub = this.subs.get(subKey);
|
|
3341
|
+
if (!sub) return;
|
|
3342
|
+
this.emit("suberror", { sub: subKey, code: msg.code, message: msg.message });
|
|
3343
|
+
if (isRebootstrapError(msg.code)) {
|
|
3344
|
+
sub.active = false;
|
|
3345
|
+
sub.bootstrapped = false;
|
|
3346
|
+
for (const p of sub.pending) {
|
|
3347
|
+
p.sent = false;
|
|
3348
|
+
p.ackedSeq = void 0;
|
|
3349
|
+
}
|
|
3350
|
+
this.enqueue(sub, async () => {
|
|
3351
|
+
await this.activateSub(sub);
|
|
3352
|
+
});
|
|
3353
|
+
return;
|
|
3354
|
+
}
|
|
3355
|
+
if (msg.code === "SHARE_REVOKED" || msg.code === "CONNECTION_REVOKED") {
|
|
3356
|
+
sub.active = false;
|
|
3357
|
+
sub.status = "revoked";
|
|
3358
|
+
sub.revokedCode = msg.code;
|
|
3359
|
+
this.subs.delete(subKey);
|
|
3360
|
+
void sub.store.destroy().catch(() => {
|
|
3361
|
+
});
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3366
|
+
switch (msg.code) {
|
|
3367
|
+
case "CONNECTION_REVOKED":
|
|
3368
|
+
this.revokedInfo = { code: msg.code, message: msg.message };
|
|
3369
|
+
this.connectIntended = false;
|
|
3370
|
+
this.connection.stop();
|
|
3371
|
+
this.recomputeStatus();
|
|
3372
|
+
this.emit("revoked", { code: msg.code, message: msg.message });
|
|
3373
|
+
return;
|
|
3374
|
+
case "UNSUPPORTED_VERSION":
|
|
3375
|
+
this.connectIntended = false;
|
|
3376
|
+
this.connection.stop();
|
|
3377
|
+
this.recomputeStatus();
|
|
3378
|
+
return;
|
|
3379
|
+
case "TOO_MANY_OPS":
|
|
3380
|
+
case "RATE_LIMITED": {
|
|
3381
|
+
for (const sub of this.subs.values()) {
|
|
3382
|
+
for (const p of sub.pending) {
|
|
3383
|
+
if (p.ackedSeq === void 0) p.sent = false;
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
this.timer(() => {
|
|
3387
|
+
for (const sub of this.subs.values()) this.flush(sub);
|
|
3388
|
+
}, RETRY_FLUSH_DELAY_MS);
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
case "UNAUTHORIZED":
|
|
3392
|
+
case "TOKEN_EXPIRED":
|
|
3393
|
+
return;
|
|
3394
|
+
case "SNAPSHOT_REQUIRED":
|
|
3395
|
+
case "RESET_REQUIRED":
|
|
3396
|
+
for (const sub of this.subs.values()) {
|
|
3397
|
+
sub.active = false;
|
|
3398
|
+
sub.bootstrapped = false;
|
|
3399
|
+
this.enqueue(sub, () => this.activateSub(sub));
|
|
3400
|
+
}
|
|
3401
|
+
return;
|
|
3402
|
+
default:
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
// -------------------------------------------------------------------
|
|
3407
|
+
// Subscription state machine (serialized per sub via `chain`)
|
|
3408
|
+
// -------------------------------------------------------------------
|
|
3409
|
+
async openSub(key, shareId) {
|
|
3410
|
+
const name = shareId ? `${this.baseDbName}:share:${shareId}` : this.baseDbName;
|
|
3411
|
+
const store = new SyncStore(name, this.schema);
|
|
3412
|
+
const [cursor, pendingRows] = await Promise.all([store.getCursor(), store.loadPending()]);
|
|
3413
|
+
return {
|
|
3414
|
+
key,
|
|
3415
|
+
shareId,
|
|
3416
|
+
store,
|
|
3417
|
+
cursor: cursor ?? -1,
|
|
3418
|
+
// -1 = never bootstrapped
|
|
3419
|
+
pending: pendingRows.map((row) => ({ op: row.op, sent: false })),
|
|
3420
|
+
active: false,
|
|
3421
|
+
bootstrapped: cursor !== null,
|
|
3422
|
+
status: "initializing",
|
|
3423
|
+
appliedOpIds: new BoundedSet(),
|
|
3424
|
+
chain: Promise.resolve(),
|
|
3425
|
+
schemaVersion: null
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
/** Bootstrap if needed, then bind the stream on the current socket. */
|
|
3429
|
+
async activateSub(sub) {
|
|
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
|
+
}
|
|
3441
|
+
if (!sub.bootstrapped || sub.cursor < 0) {
|
|
3442
|
+
try {
|
|
3443
|
+
await this.bootstrapSub(sub);
|
|
3444
|
+
} catch (err) {
|
|
3445
|
+
this.log(`bootstrap failed for '${sub.key}':`, err);
|
|
3446
|
+
sub.status = "error";
|
|
3447
|
+
return;
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
this.connection.send({
|
|
3451
|
+
type: "subscribe",
|
|
3452
|
+
sub: sub.key,
|
|
3453
|
+
cursor: sub.cursor,
|
|
3454
|
+
...sub.shareId ? { share: sub.shareId } : this.opts.appName ? { app: this.opts.appName } : {}
|
|
3455
|
+
});
|
|
3456
|
+
}
|
|
3457
|
+
/** Cold start = snapshot + tail; never log replay (§5). Pending survives. */
|
|
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
|
+
}
|
|
3479
|
+
const snapshot = await this.opts.fetchSnapshot(
|
|
3480
|
+
sub.shareId ? { share: sub.shareId } : void 0
|
|
3481
|
+
);
|
|
3482
|
+
await sub.store.replaceFromSnapshot({
|
|
3483
|
+
channel: snapshot.channel,
|
|
3484
|
+
records: snapshot.records ?? {},
|
|
3485
|
+
cursor: snapshot.cursor ?? 0
|
|
3486
|
+
});
|
|
3487
|
+
if (!sub.shareId && ownerDid) {
|
|
3488
|
+
await sub.store.setOwner(ownerDid);
|
|
3489
|
+
}
|
|
3490
|
+
sub.cursor = snapshot.cursor ?? 0;
|
|
3491
|
+
sub.bootstrapped = true;
|
|
3492
|
+
sub.appliedOpIds.clear();
|
|
3493
|
+
this.log(`bootstrapped '${sub.key}' cursor=${sub.cursor}`);
|
|
3494
|
+
this.emit("change", { sub: sub.key, tables: sub.store.tables });
|
|
3495
|
+
}
|
|
3496
|
+
/**
|
|
3497
|
+
* Responsibilities 3+4+5: ordered apply, cursor advance, dedupe/confirm.
|
|
3498
|
+
* Runs inside the sub's serial chain.
|
|
3499
|
+
*/
|
|
3500
|
+
async processOps(sub, msg) {
|
|
3501
|
+
const applyOps = [];
|
|
3502
|
+
const confirmedOpIds = [];
|
|
3503
|
+
for (const op of msg.ops) {
|
|
3504
|
+
if (typeof op.seq !== "number" || op.seq <= sub.cursor) continue;
|
|
3505
|
+
sub.cursor = op.seq;
|
|
3506
|
+
if (sub.appliedOpIds.has(op.op_id)) continue;
|
|
3507
|
+
const idx = sub.pending.findIndex((p) => p.op.op_id === op.op_id);
|
|
3508
|
+
if (idx >= 0) {
|
|
3509
|
+
sub.pending.splice(idx, 1);
|
|
3510
|
+
confirmedOpIds.push(op.op_id);
|
|
3511
|
+
}
|
|
3512
|
+
if (!sub.store.hasTable(op.table)) {
|
|
3513
|
+
sub.appliedOpIds.add(op.op_id);
|
|
3514
|
+
continue;
|
|
3515
|
+
}
|
|
3516
|
+
applyOps.push(op);
|
|
3517
|
+
sub.appliedOpIds.add(op.op_id);
|
|
3518
|
+
}
|
|
3519
|
+
if (typeof msg.cursor === "number" && msg.cursor > sub.cursor) {
|
|
3520
|
+
sub.cursor = msg.cursor;
|
|
3521
|
+
}
|
|
3522
|
+
if (applyOps.length > 0 || confirmedOpIds.length > 0) {
|
|
3523
|
+
await sub.store.commitIncoming({ applyOps, confirmedOpIds, cursor: sub.cursor });
|
|
3524
|
+
const tables = [...new Set(applyOps.map((op) => op.table))];
|
|
3525
|
+
this.emit("change", { sub: sub.key, tables });
|
|
3526
|
+
} else {
|
|
3527
|
+
await sub.store.setCursor(sub.cursor);
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
/**
|
|
3531
|
+
* Push verdicts (§6.5, §8, §9). Success acks are recorded but the op stays
|
|
3532
|
+
* pending until its echo arrives in seq order — this preserves strict
|
|
3533
|
+
* ordered apply even when `pushed` races ahead of intermediate remote ops.
|
|
3534
|
+
* Terminal errors apply the poison-op rule; retryables back off.
|
|
3535
|
+
*/
|
|
3536
|
+
async processPushed(sub, msg) {
|
|
3537
|
+
let needsRetry = false;
|
|
3538
|
+
const changedTables = /* @__PURE__ */ new Set();
|
|
3539
|
+
for (const result of msg.results) {
|
|
3540
|
+
const idx = sub.pending.findIndex((p) => p.op.op_id === result.op_id);
|
|
3541
|
+
if ("seq" in result && typeof result.seq === "number") {
|
|
3542
|
+
if (idx >= 0) {
|
|
3543
|
+
if (result.seq <= sub.cursor) {
|
|
3544
|
+
const [entry] = sub.pending.splice(idx, 1);
|
|
3545
|
+
await sub.store.commitIncoming({
|
|
3546
|
+
applyOps: [],
|
|
3547
|
+
confirmedOpIds: [entry.op.op_id],
|
|
3548
|
+
cursor: sub.cursor
|
|
3549
|
+
});
|
|
3550
|
+
changedTables.add(entry.op.table);
|
|
3551
|
+
} else {
|
|
3552
|
+
sub.pending[idx].ackedSeq = result.seq;
|
|
3553
|
+
await sub.store.markAcked(result.op_id, result.seq);
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
continue;
|
|
3557
|
+
}
|
|
3558
|
+
if ("error" in result) {
|
|
3559
|
+
if (isTerminalOpError(result.error, result.terminal)) {
|
|
3560
|
+
if (idx >= 0) sub.pending.splice(idx, 1);
|
|
3561
|
+
const rejection = await sub.store.rejectPending(
|
|
3562
|
+
result.op_id,
|
|
3563
|
+
result.error,
|
|
3564
|
+
result.message
|
|
3565
|
+
);
|
|
3566
|
+
if (rejection) {
|
|
3567
|
+
changedTables.add(rejection.op.table);
|
|
3568
|
+
this.emit("rejected", { sub: sub.key, rejection });
|
|
3569
|
+
this.log(
|
|
3570
|
+
`op rejected (${result.error}): ${rejection.op.type} ${rejection.op.table}/${rejection.op.record_id}`
|
|
3571
|
+
);
|
|
3572
|
+
}
|
|
3573
|
+
} else if (idx >= 0) {
|
|
3574
|
+
sub.pending[idx].sent = false;
|
|
3575
|
+
needsRetry = true;
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
if (changedTables.size > 0) {
|
|
3580
|
+
this.emit("change", { sub: sub.key, tables: [...changedTables] });
|
|
3581
|
+
}
|
|
3582
|
+
if (needsRetry) {
|
|
3583
|
+
this.timer(() => this.flush(sub), RETRY_FLUSH_DELAY_MS);
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
/** Push unsent pending ops, chunked to `limits.max_ops_per_push`. */
|
|
3587
|
+
flush(sub) {
|
|
3588
|
+
if (!this.connection.isOnline || !sub.active) return;
|
|
3589
|
+
const unsent = sub.pending.filter((p) => !p.sent && p.ackedSeq === void 0);
|
|
3590
|
+
if (unsent.length === 0) return;
|
|
3591
|
+
const chunkSize = Math.max(1, this.limits.max_ops_per_push);
|
|
3592
|
+
for (let i = 0; i < unsent.length; i += chunkSize) {
|
|
3593
|
+
const chunk = unsent.slice(i, i + chunkSize);
|
|
3594
|
+
for (const p of chunk) p.sent = true;
|
|
3595
|
+
const ok = this.connection.send({
|
|
3596
|
+
type: "push",
|
|
3597
|
+
sub: sub.key,
|
|
3598
|
+
ops: chunk.map((p) => p.op)
|
|
3599
|
+
});
|
|
3600
|
+
if (!ok) {
|
|
3601
|
+
for (const p of chunk) p.sent = false;
|
|
3602
|
+
return;
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
this.log(`pushed ${unsent.length} op(s) on '${sub.key}'`);
|
|
3606
|
+
}
|
|
3607
|
+
// -------------------------------------------------------------------
|
|
3608
|
+
// Internals
|
|
3609
|
+
// -------------------------------------------------------------------
|
|
3610
|
+
get dbPrefix() {
|
|
3611
|
+
return this.opts.dbNamePrefix ?? "basic-sync";
|
|
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
|
+
}
|
|
3618
|
+
enqueue(sub, task) {
|
|
3619
|
+
sub.chain = sub.chain.then(task).catch((err) => {
|
|
3620
|
+
this.log(`task failed on '${sub.key}':`, err);
|
|
3621
|
+
});
|
|
3622
|
+
return sub.chain;
|
|
3623
|
+
}
|
|
3624
|
+
timer(fn, ms) {
|
|
3625
|
+
const t = setTimeout(() => {
|
|
3626
|
+
this.timers.delete(t);
|
|
3627
|
+
fn();
|
|
3628
|
+
}, ms);
|
|
3629
|
+
this.timers.add(t);
|
|
3630
|
+
}
|
|
3631
|
+
recomputeStatus() {
|
|
3632
|
+
let status;
|
|
3633
|
+
if (this.revokedInfo) status = "revoked";
|
|
3634
|
+
else if (this.connectionStatus === "auth_failed") status = "auth_required";
|
|
3635
|
+
else if (!this.storesOpen) status = this.connectionStatus === "stopped" ? "stopped" : "idle";
|
|
3636
|
+
else if (!this.connectIntended) status = "local";
|
|
3637
|
+
else if (this.connectionStatus === "online") status = "online";
|
|
3638
|
+
else if (this.connectionStatus === "connecting") status = "connecting";
|
|
3639
|
+
else if (this.connectionStatus === "idle") status = "connecting";
|
|
3640
|
+
else if (this.connectionStatus === "stopped") status = "local";
|
|
3641
|
+
else status = "offline";
|
|
3642
|
+
if (status !== this._status) {
|
|
3643
|
+
this._status = status;
|
|
3644
|
+
this.emit("status", status);
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
log(...args) {
|
|
3648
|
+
this.opts.log?.("[sync-engine]", ...args);
|
|
3649
|
+
}
|
|
3650
|
+
};
|
|
3651
|
+
|
|
3652
|
+
// src/core/db.ts
|
|
3653
|
+
function mintRecordId() {
|
|
3654
|
+
return mintOpId();
|
|
3655
|
+
}
|
|
3656
|
+
var SyncTable = class {
|
|
3657
|
+
constructor(engine, subKey, name) {
|
|
3658
|
+
this.engine = engine;
|
|
3659
|
+
this.subKey = subKey;
|
|
3660
|
+
this.name = name;
|
|
3661
|
+
}
|
|
3662
|
+
get store() {
|
|
3663
|
+
const sub = this.engine.getSubscription(this.subKey);
|
|
3664
|
+
if (!sub) {
|
|
3665
|
+
throw new Error(
|
|
3666
|
+
`subscription '${this.subKey}' is not open \u2014 wait for the client to be ready (isReady) before using the db`
|
|
3667
|
+
);
|
|
3668
|
+
}
|
|
3669
|
+
return sub.store;
|
|
3670
|
+
}
|
|
3671
|
+
get ref() {
|
|
3672
|
+
return this.store.view(this.name);
|
|
3673
|
+
}
|
|
3674
|
+
async create(data) {
|
|
3675
|
+
const id = mintRecordId();
|
|
3676
|
+
const view = await this.engine.apply(this.subKey, {
|
|
3677
|
+
type: "put",
|
|
3678
|
+
table: this.name,
|
|
3679
|
+
record_id: id,
|
|
3680
|
+
data
|
|
3681
|
+
});
|
|
3682
|
+
return view ?? { id, ...data };
|
|
3683
|
+
}
|
|
3684
|
+
async put(id, data) {
|
|
3685
|
+
if (!id) throw new Error("put() requires an id");
|
|
3686
|
+
const view = await this.engine.apply(this.subKey, {
|
|
3687
|
+
type: "put",
|
|
3688
|
+
table: this.name,
|
|
3689
|
+
record_id: id,
|
|
3690
|
+
data
|
|
3691
|
+
});
|
|
3692
|
+
return view ?? { id, ...data };
|
|
3693
|
+
}
|
|
3694
|
+
async patch(id, data) {
|
|
3695
|
+
if (!id) throw new Error("patch() requires an id");
|
|
3696
|
+
const existing = await this.store.getViewRecord(this.name, id);
|
|
3697
|
+
if (!existing) return null;
|
|
3698
|
+
const view = await this.engine.apply(this.subKey, {
|
|
3699
|
+
type: "patch",
|
|
3700
|
+
table: this.name,
|
|
3701
|
+
record_id: id,
|
|
3702
|
+
data
|
|
3703
|
+
});
|
|
3704
|
+
return view;
|
|
3705
|
+
}
|
|
3706
|
+
async delete(id) {
|
|
3707
|
+
if (!id) throw new Error("delete() requires an id");
|
|
3708
|
+
await this.engine.apply(this.subKey, {
|
|
3709
|
+
type: "delete",
|
|
3710
|
+
table: this.name,
|
|
3711
|
+
record_id: id
|
|
3712
|
+
});
|
|
3713
|
+
}
|
|
3714
|
+
async get(id) {
|
|
3715
|
+
return await this.store.getViewRecord(this.name, id);
|
|
3716
|
+
}
|
|
3717
|
+
async getAll() {
|
|
3718
|
+
return await this.store.getViewRecords(this.name);
|
|
3719
|
+
}
|
|
3720
|
+
async find(predicate) {
|
|
3721
|
+
const all = await this.getAll();
|
|
3722
|
+
return all.filter(predicate);
|
|
3723
|
+
}
|
|
3724
|
+
};
|
|
3725
|
+
var SyncDb = class {
|
|
3726
|
+
constructor(engine, subKey = OWN_SUB) {
|
|
3727
|
+
this.engine = engine;
|
|
3728
|
+
this.subKey = subKey;
|
|
3729
|
+
}
|
|
3730
|
+
kind = "sync";
|
|
3731
|
+
tables = /* @__PURE__ */ new Map();
|
|
3732
|
+
table(name) {
|
|
3733
|
+
if (!this.engine.schema.tables[name]) {
|
|
3734
|
+
throw new Error(`table "${name}" not found in schema`);
|
|
2785
3735
|
}
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
log(`Running migration from ${migration.fromVersion} to ${migration.toVersion}`);
|
|
2789
|
-
await migration.migrate(this.storage);
|
|
2790
|
-
} catch (error) {
|
|
2791
|
-
console.error(`Migration failed from ${migration.fromVersion} to ${migration.toVersion}:`, error);
|
|
2792
|
-
throw new Error(`Migration failed: ${error}`);
|
|
2793
|
-
}
|
|
3736
|
+
if (!this.tables.has(name)) {
|
|
3737
|
+
this.tables.set(name, new SyncTable(this.engine, this.subKey, name));
|
|
2794
3738
|
}
|
|
2795
|
-
|
|
2796
|
-
return { updated: true, fromVersion: storedVersion, toVersion: this.currentVersion };
|
|
3739
|
+
return this.tables.get(name);
|
|
2797
3740
|
}
|
|
2798
|
-
|
|
3741
|
+
};
|
|
3742
|
+
var RestTable = class {
|
|
3743
|
+
constructor(rest, name) {
|
|
3744
|
+
this.rest = rest;
|
|
3745
|
+
this.name = name;
|
|
3746
|
+
}
|
|
3747
|
+
async create(data) {
|
|
3748
|
+
const record = await this.rest.createRecord(this.name, data);
|
|
3749
|
+
return record;
|
|
3750
|
+
}
|
|
3751
|
+
async put(id, data) {
|
|
3752
|
+
if (!id) throw new Error("put() requires an id");
|
|
3753
|
+
const record = await this.rest.putRecord(this.name, id, data);
|
|
3754
|
+
if (!record) throw new Error(`record ${this.name}/${id} not found (REST put is replace-only)`);
|
|
3755
|
+
return record;
|
|
3756
|
+
}
|
|
3757
|
+
async patch(id, data) {
|
|
3758
|
+
if (!id) throw new Error("patch() requires an id");
|
|
3759
|
+
const record = await this.rest.patchRecord(this.name, id, data);
|
|
3760
|
+
return record;
|
|
3761
|
+
}
|
|
3762
|
+
async delete(id) {
|
|
3763
|
+
if (!id) throw new Error("delete() requires an id");
|
|
3764
|
+
await this.rest.deleteRecord(this.name, id);
|
|
3765
|
+
}
|
|
3766
|
+
async get(id) {
|
|
3767
|
+
const record = await this.rest.getRecord(this.name, id);
|
|
3768
|
+
return record;
|
|
3769
|
+
}
|
|
3770
|
+
async getAll() {
|
|
3771
|
+
return await this.rest.list(this.name);
|
|
3772
|
+
}
|
|
3773
|
+
async find(predicate) {
|
|
3774
|
+
const all = await this.getAll();
|
|
3775
|
+
return all.filter(predicate);
|
|
3776
|
+
}
|
|
3777
|
+
};
|
|
3778
|
+
var RestDb = class {
|
|
3779
|
+
constructor(rest, schema) {
|
|
3780
|
+
this.rest = rest;
|
|
3781
|
+
this.schema = schema;
|
|
3782
|
+
}
|
|
3783
|
+
kind = "rest";
|
|
3784
|
+
tables = /* @__PURE__ */ new Map();
|
|
3785
|
+
table(name) {
|
|
3786
|
+
if (this.schema?.tables && !this.schema.tables[name]) {
|
|
3787
|
+
throw new Error(`table "${name}" not found in schema`);
|
|
3788
|
+
}
|
|
3789
|
+
if (!this.tables.has(name)) {
|
|
3790
|
+
this.tables.set(name, new RestTable(this.rest, name));
|
|
3791
|
+
}
|
|
3792
|
+
return this.tables.get(name);
|
|
3793
|
+
}
|
|
3794
|
+
};
|
|
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 [];
|
|
2799
3830
|
try {
|
|
2800
|
-
const
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
return
|
|
2804
|
-
} catch (error) {
|
|
2805
|
-
console.warn("Failed to get stored version:", error);
|
|
2806
|
-
return null;
|
|
3831
|
+
const parsed = JSON.parse(raw);
|
|
3832
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
3833
|
+
} catch {
|
|
3834
|
+
return [];
|
|
2807
3835
|
}
|
|
2808
3836
|
}
|
|
2809
|
-
async
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
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
|
|
2813
3854
|
};
|
|
2814
|
-
await this.
|
|
3855
|
+
const users = await this.list();
|
|
3856
|
+
users.push(profile);
|
|
3857
|
+
await this.save(users);
|
|
3858
|
+
log(`created anonymous user ${id}`);
|
|
3859
|
+
return profile;
|
|
2815
3860
|
}
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
});
|
|
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];
|
|
2824
3868
|
}
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
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 {
|
|
2834
3899
|
}
|
|
2835
|
-
return aMajorMinor.minor - bMajorMinor.minor;
|
|
2836
3900
|
}
|
|
2837
3901
|
/**
|
|
2838
|
-
*
|
|
2839
|
-
*
|
|
2840
|
-
* "1.2.3" -> {major: 1, minor: 2}
|
|
3902
|
+
* Resolve the active profile for this tab: sessionStorage choice if it
|
|
3903
|
+
* still exists, else the most recently active profile, else null.
|
|
2841
3904
|
*/
|
|
2842
|
-
|
|
2843
|
-
const
|
|
2844
|
-
const
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
}
|
|
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;
|
|
2849
3916
|
}
|
|
3917
|
+
async touch(id) {
|
|
3918
|
+
await this.update(id, { lastActiveAt: Date.now() });
|
|
3919
|
+
}
|
|
3920
|
+
// -------------------------------------------------------------------
|
|
3921
|
+
// Legacy adoption
|
|
3922
|
+
// -------------------------------------------------------------------
|
|
2850
3923
|
/**
|
|
2851
|
-
*
|
|
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.
|
|
2852
3928
|
*/
|
|
2853
|
-
|
|
2854
|
-
this.
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
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;
|
|
2870
3958
|
}
|
|
2871
3959
|
};
|
|
2872
|
-
function getMigrations() {
|
|
2873
|
-
return [
|
|
2874
|
-
addMigrationTimestamp
|
|
2875
|
-
];
|
|
2876
|
-
}
|
|
2877
|
-
|
|
2878
|
-
// src/AuthContext.tsx
|
|
2879
|
-
init_network();
|
|
2880
3960
|
|
|
2881
3961
|
// src/utils/schema.ts
|
|
2882
|
-
var
|
|
3962
|
+
var import_schema2 = require("@basictech/schema");
|
|
2883
3963
|
init_config();
|
|
2884
3964
|
async function getSchemaStatus(schema) {
|
|
2885
3965
|
const projectId = schema.project_id;
|
|
2886
|
-
const valid = (0,
|
|
3966
|
+
const valid = (0, import_schema2.validateSchema)(schema);
|
|
2887
3967
|
if (!valid.valid) {
|
|
2888
3968
|
console.warn("BasicDB Error: your local schema is invalid. Please fix errors and try again - sync is disabled");
|
|
2889
3969
|
return {
|
|
@@ -2921,7 +4001,7 @@ async function getSchemaStatus(schema) {
|
|
|
2921
4001
|
latest: latestSchema
|
|
2922
4002
|
};
|
|
2923
4003
|
} else if (latestSchema.version === schema.version) {
|
|
2924
|
-
const changes = (0,
|
|
4004
|
+
const changes = (0, import_schema2.compareSchemas)(schema, latestSchema);
|
|
2925
4005
|
if (changes.valid) {
|
|
2926
4006
|
return {
|
|
2927
4007
|
valid: true,
|
|
@@ -2945,7 +4025,7 @@ async function getSchemaStatus(schema) {
|
|
|
2945
4025
|
}
|
|
2946
4026
|
}
|
|
2947
4027
|
async function validateAndCheckSchema(schema) {
|
|
2948
|
-
const valid = (0,
|
|
4028
|
+
const valid = (0, import_schema2.validateSchema)(schema);
|
|
2949
4029
|
if (!valid.valid) {
|
|
2950
4030
|
log("Basic Schema is invalid!", valid.errors);
|
|
2951
4031
|
console.group("Schema Errors");
|
|
@@ -2962,452 +4042,845 @@ async function validateAndCheckSchema(schema) {
|
|
|
2962
4042
|
errors: valid.errors
|
|
2963
4043
|
};
|
|
2964
4044
|
}
|
|
2965
|
-
let schemaStatus = { valid: false };
|
|
2966
|
-
if (schema.version !== 0) {
|
|
2967
|
-
schemaStatus = await getSchemaStatus(schema);
|
|
2968
|
-
log("schemaStatus", schemaStatus);
|
|
2969
|
-
} else {
|
|
2970
|
-
schemaStatus = { valid: false, status: "unpublished" };
|
|
2971
|
-
log("schema not published - at version 0");
|
|
4045
|
+
let schemaStatus = { valid: false };
|
|
4046
|
+
if (schema.version !== 0) {
|
|
4047
|
+
schemaStatus = await getSchemaStatus(schema);
|
|
4048
|
+
log("schemaStatus", schemaStatus);
|
|
4049
|
+
} else {
|
|
4050
|
+
schemaStatus = { valid: false, status: "unpublished" };
|
|
4051
|
+
log("schema not published - at version 0");
|
|
4052
|
+
}
|
|
4053
|
+
return {
|
|
4054
|
+
isValid: true,
|
|
4055
|
+
schemaStatus
|
|
4056
|
+
};
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
// src/updater/versionUpdater.ts
|
|
4060
|
+
init_config();
|
|
4061
|
+
var VersionUpdater = class {
|
|
4062
|
+
storage;
|
|
4063
|
+
currentVersion;
|
|
4064
|
+
migrations;
|
|
4065
|
+
versionKey = "basic_app_version";
|
|
4066
|
+
constructor(storage, currentVersion, migrations = []) {
|
|
4067
|
+
this.storage = storage;
|
|
4068
|
+
this.currentVersion = currentVersion;
|
|
4069
|
+
this.migrations = migrations.sort((a, b) => this.compareVersions(a.fromVersion, b.fromVersion));
|
|
4070
|
+
}
|
|
4071
|
+
/**
|
|
4072
|
+
* Check current stored version and run migrations if needed
|
|
4073
|
+
* Only compares major.minor versions, ignoring beta/prerelease parts
|
|
4074
|
+
* Example: "0.7.0-beta.1" and "0.7.0" are treated as the same version
|
|
4075
|
+
*/
|
|
4076
|
+
async checkAndUpdate() {
|
|
4077
|
+
const storedVersion = await this.getStoredVersion();
|
|
4078
|
+
if (!storedVersion) {
|
|
4079
|
+
await this.setStoredVersion(this.currentVersion);
|
|
4080
|
+
return { updated: false, toVersion: this.currentVersion };
|
|
4081
|
+
}
|
|
4082
|
+
if (storedVersion === this.currentVersion) {
|
|
4083
|
+
return { updated: false, toVersion: this.currentVersion };
|
|
4084
|
+
}
|
|
4085
|
+
const migrationsToRun = this.getMigrationsToRun(storedVersion, this.currentVersion);
|
|
4086
|
+
if (migrationsToRun.length === 0) {
|
|
4087
|
+
await this.setStoredVersion(this.currentVersion);
|
|
4088
|
+
return { updated: true, fromVersion: storedVersion, toVersion: this.currentVersion };
|
|
4089
|
+
}
|
|
4090
|
+
for (const migration of migrationsToRun) {
|
|
4091
|
+
try {
|
|
4092
|
+
log(`Running migration from ${migration.fromVersion} to ${migration.toVersion}`);
|
|
4093
|
+
await migration.migrate(this.storage);
|
|
4094
|
+
} catch (error) {
|
|
4095
|
+
console.error(`Migration failed from ${migration.fromVersion} to ${migration.toVersion}:`, error);
|
|
4096
|
+
throw new Error(`Migration failed: ${error}`);
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
await this.setStoredVersion(this.currentVersion);
|
|
4100
|
+
return { updated: true, fromVersion: storedVersion, toVersion: this.currentVersion };
|
|
4101
|
+
}
|
|
4102
|
+
async getStoredVersion() {
|
|
4103
|
+
try {
|
|
4104
|
+
const versionData = await this.storage.get(this.versionKey);
|
|
4105
|
+
if (!versionData) return null;
|
|
4106
|
+
const versionInfo = JSON.parse(versionData);
|
|
4107
|
+
return versionInfo.version;
|
|
4108
|
+
} catch (error) {
|
|
4109
|
+
console.warn("Failed to get stored version:", error);
|
|
4110
|
+
return null;
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
async setStoredVersion(version2) {
|
|
4114
|
+
const versionInfo = {
|
|
4115
|
+
version: version2,
|
|
4116
|
+
lastUpdated: Date.now()
|
|
4117
|
+
};
|
|
4118
|
+
await this.storage.set(this.versionKey, JSON.stringify(versionInfo));
|
|
4119
|
+
}
|
|
4120
|
+
getMigrationsToRun(fromVersion, toVersion) {
|
|
4121
|
+
return this.migrations.filter((migration) => {
|
|
4122
|
+
const storedLessThanMigrationTo = this.compareVersions(fromVersion, migration.toVersion) < 0;
|
|
4123
|
+
const currentGreaterThanOrEqualMigrationTo = this.compareVersions(toVersion, migration.toVersion) >= 0;
|
|
4124
|
+
const shouldRun = storedLessThanMigrationTo && currentGreaterThanOrEqualMigrationTo;
|
|
4125
|
+
log(`Migration ${migration.fromVersion} \u2192 ${migration.toVersion}: shouldRun=${shouldRun}`);
|
|
4126
|
+
return shouldRun;
|
|
4127
|
+
});
|
|
4128
|
+
}
|
|
4129
|
+
/**
|
|
4130
|
+
* Simple semantic version comparison (major.minor only, ignoring beta/prerelease)
|
|
4131
|
+
* Returns: -1 if a < b, 0 if a === b, 1 if a > b
|
|
4132
|
+
*/
|
|
4133
|
+
compareVersions(a, b) {
|
|
4134
|
+
const aMajorMinor = this.extractMajorMinor(a);
|
|
4135
|
+
const bMajorMinor = this.extractMajorMinor(b);
|
|
4136
|
+
if (aMajorMinor.major !== bMajorMinor.major) {
|
|
4137
|
+
return aMajorMinor.major - bMajorMinor.major;
|
|
4138
|
+
}
|
|
4139
|
+
return aMajorMinor.minor - bMajorMinor.minor;
|
|
4140
|
+
}
|
|
4141
|
+
/**
|
|
4142
|
+
* Extract major.minor from version string, ignoring beta/prerelease
|
|
4143
|
+
* Examples: "0.7.0-beta.1" -> {major: 0, minor: 7}
|
|
4144
|
+
* "1.2.3" -> {major: 1, minor: 2}
|
|
4145
|
+
*/
|
|
4146
|
+
extractMajorMinor(version2) {
|
|
4147
|
+
const cleanVersion = version2.split("-")[0]?.split("+")[0] || version2;
|
|
4148
|
+
const parts = cleanVersion.split(".").map(Number);
|
|
4149
|
+
return {
|
|
4150
|
+
major: parts[0] || 0,
|
|
4151
|
+
minor: parts[1] || 0
|
|
4152
|
+
};
|
|
4153
|
+
}
|
|
4154
|
+
/**
|
|
4155
|
+
* Add a migration to the updater
|
|
4156
|
+
*/
|
|
4157
|
+
addMigration(migration) {
|
|
4158
|
+
this.migrations.push(migration);
|
|
4159
|
+
this.migrations.sort((a, b) => this.compareVersions(a.fromVersion, b.fromVersion));
|
|
4160
|
+
}
|
|
4161
|
+
};
|
|
4162
|
+
function createVersionUpdater(storage, currentVersion, migrations = []) {
|
|
4163
|
+
return new VersionUpdater(storage, currentVersion, migrations);
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
// src/updater/updateMigrations.ts
|
|
4167
|
+
init_config();
|
|
4168
|
+
var addMigrationTimestamp = {
|
|
4169
|
+
fromVersion: "0.6.0",
|
|
4170
|
+
toVersion: "0.7.0",
|
|
4171
|
+
async migrate(storage) {
|
|
4172
|
+
log("Running migration 0.6.0 \u2192 0.7.0");
|
|
4173
|
+
storage.set("test_migration", "true");
|
|
4174
|
+
}
|
|
4175
|
+
};
|
|
4176
|
+
var dropLegacySyncDb = {
|
|
4177
|
+
fromVersion: "0.8.0",
|
|
4178
|
+
toVersion: "0.9.0",
|
|
4179
|
+
async migrate() {
|
|
4180
|
+
log("Running migration 0.8.0 \u2192 0.9.0: deleting legacy basicdb");
|
|
4181
|
+
try {
|
|
4182
|
+
const idb = globalThis.indexedDB;
|
|
4183
|
+
if (!idb) return;
|
|
4184
|
+
await new Promise((resolve) => {
|
|
4185
|
+
const req = idb.deleteDatabase("basicdb");
|
|
4186
|
+
req.onsuccess = req.onerror = req.onblocked = () => resolve();
|
|
4187
|
+
});
|
|
4188
|
+
} catch {
|
|
4189
|
+
}
|
|
2972
4190
|
}
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
4191
|
+
};
|
|
4192
|
+
function getMigrations() {
|
|
4193
|
+
return [
|
|
4194
|
+
addMigrationTimestamp,
|
|
4195
|
+
dropLegacySyncDb
|
|
4196
|
+
];
|
|
2977
4197
|
}
|
|
2978
4198
|
|
|
2979
|
-
// src/
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
var
|
|
2983
|
-
var BasicDevToolbar2 = (0, import_react3.lazy)(
|
|
2984
|
-
() => Promise.resolve().then(() => (init_BasicDevToolbar(), BasicDevToolbar_exports)).then((m) => ({ default: m.BasicDevToolbar }))
|
|
2985
|
-
);
|
|
2986
|
-
var DEFAULT_AUTH_CONFIG = {
|
|
4199
|
+
// src/core/BasicClient.ts
|
|
4200
|
+
init_config();
|
|
4201
|
+
init_package();
|
|
4202
|
+
var DEFAULTS = {
|
|
2987
4203
|
scopes: "profile,email,app:admin",
|
|
2988
4204
|
pds_url: "https://pds.basic.id",
|
|
2989
|
-
admin_url: "https://api.basic.tech"
|
|
2990
|
-
ws_url: "wss://pds.basic.id/ws"
|
|
4205
|
+
admin_url: "https://api.basic.tech"
|
|
2991
4206
|
};
|
|
2992
|
-
function
|
|
4207
|
+
function deriveSyncUrl(pdsUrl) {
|
|
4208
|
+
return pdsUrl.replace(/^http/, "ws").replace(/\/$/, "") + "/sync/";
|
|
4209
|
+
}
|
|
4210
|
+
function ephemeralLegacyProfile() {
|
|
4211
|
+
const now = Date.now();
|
|
2993
4212
|
return {
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
did: mgr.did,
|
|
3001
|
-
tokenScope: mgr.tokenScope
|
|
4213
|
+
id: "default",
|
|
4214
|
+
kind: "anon",
|
|
4215
|
+
keyspace: "",
|
|
4216
|
+
storagePrefix: "",
|
|
4217
|
+
createdAt: now,
|
|
4218
|
+
lastActiveAt: now
|
|
3002
4219
|
};
|
|
3003
4220
|
}
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
4221
|
+
var BasicClient = class {
|
|
4222
|
+
rest;
|
|
4223
|
+
mode;
|
|
4224
|
+
config;
|
|
4225
|
+
projectId;
|
|
4226
|
+
users;
|
|
4227
|
+
rawStorage;
|
|
4228
|
+
restDb;
|
|
4229
|
+
debug;
|
|
4230
|
+
anonymousEnabled;
|
|
4231
|
+
authConfig;
|
|
4232
|
+
syncUrl;
|
|
4233
|
+
binding;
|
|
4234
|
+
usersCache = [];
|
|
4235
|
+
devInfo = null;
|
|
4236
|
+
syncEnabled = false;
|
|
4237
|
+
schemaChecked = false;
|
|
4238
|
+
started = false;
|
|
4239
|
+
signOutInProgress = false;
|
|
4240
|
+
/** Serializes profile transitions (switch, dispose, sign-out fallthrough). */
|
|
4241
|
+
profileOps = Promise.resolve();
|
|
4242
|
+
mounts = /* @__PURE__ */ new Map();
|
|
4243
|
+
listeners = /* @__PURE__ */ new Set();
|
|
4244
|
+
snapshot;
|
|
4245
|
+
constructor(config) {
|
|
4246
|
+
this.config = config;
|
|
4247
|
+
this.debug = config.debug ?? false;
|
|
4248
|
+
this.mode = config.mode ?? "sync";
|
|
4249
|
+
this.projectId = config.schema?.project_id || config.project_id;
|
|
4250
|
+
this.anonymousEnabled = this.mode === "sync" && (config.anonymous ?? true);
|
|
4251
|
+
this.authConfig = {
|
|
4252
|
+
scopes: Array.isArray(config.auth?.scopes) ? config.auth.scopes.join(" ") : config.auth?.scopes || DEFAULTS.scopes,
|
|
4253
|
+
pds_url: config.auth?.pds_url || DEFAULTS.pds_url,
|
|
4254
|
+
admin_url: config.auth?.admin_url || DEFAULTS.admin_url
|
|
4255
|
+
};
|
|
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;
|
|
4259
|
+
this.rest = new RestClient({
|
|
4260
|
+
baseUrl: this.authConfig.pds_url,
|
|
4261
|
+
projectId: this.projectId ?? "",
|
|
4262
|
+
getToken: (opts) => this.auth.getToken(opts),
|
|
4263
|
+
log: this.debug ? log : void 0
|
|
4264
|
+
});
|
|
4265
|
+
this.restDb = new RestDb(this.rest, this.config.schema);
|
|
4266
|
+
this.binding = this.createBinding(ephemeralLegacyProfile());
|
|
4267
|
+
this.snapshot = this.buildSnapshot();
|
|
4268
|
+
}
|
|
4269
|
+
// -------------------------------------------------------------------
|
|
4270
|
+
// Public surface
|
|
4271
|
+
// -------------------------------------------------------------------
|
|
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. */
|
|
4279
|
+
get db() {
|
|
4280
|
+
if (this.mode === "sync" && this.binding.syncDb) return this.binding.syncDb;
|
|
4281
|
+
return this.restDb;
|
|
4282
|
+
}
|
|
4283
|
+
get activeUser() {
|
|
4284
|
+
return this.users ? this.binding.profile : null;
|
|
4285
|
+
}
|
|
4286
|
+
/** Bootstrap: version migrations, profile resolution, schema check, auth init. */
|
|
4287
|
+
async start() {
|
|
4288
|
+
if (this.started) return;
|
|
4289
|
+
this.started = true;
|
|
4290
|
+
try {
|
|
4291
|
+
const updater = createVersionUpdater(this.rawStorage, version, getMigrations());
|
|
4292
|
+
const result = await updater.checkAndUpdate();
|
|
4293
|
+
if (result.updated) log(`SDK storage migrated ${result.fromVersion} \u2192 ${result.toVersion}`);
|
|
4294
|
+
} catch (err) {
|
|
4295
|
+
log("version updater failed:", err);
|
|
4296
|
+
}
|
|
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
|
+
});
|
|
4311
|
+
}
|
|
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
|
+
*/
|
|
4317
|
+
async signOut() {
|
|
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
|
+
});
|
|
4381
|
+
}
|
|
4382
|
+
/** Stop connections and listeners; local data is kept. */
|
|
4383
|
+
stop() {
|
|
4384
|
+
this.started = false;
|
|
4385
|
+
this.binding.engine?.stop();
|
|
4386
|
+
for (const fn of this.binding.sessionCleanup) {
|
|
4387
|
+
try {
|
|
4388
|
+
fn();
|
|
4389
|
+
} catch {
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
this.binding.sessionCleanup = [];
|
|
4393
|
+
}
|
|
4394
|
+
/** Re-run the remote schema status check (dev toolbar). */
|
|
4395
|
+
async refreshSchemaStatus() {
|
|
4396
|
+
this.schemaChecked = false;
|
|
4397
|
+
await this.checkSchema();
|
|
4398
|
+
this.syncLifecycle();
|
|
4399
|
+
}
|
|
4400
|
+
async listRejected() {
|
|
4401
|
+
return this.engine?.listRejected() ?? [];
|
|
4402
|
+
}
|
|
4403
|
+
async clearRejected() {
|
|
4404
|
+
await this.engine?.clearRejected();
|
|
4405
|
+
this.publish();
|
|
4406
|
+
}
|
|
4407
|
+
// ---------------- shares ----------------
|
|
4408
|
+
/** Shares granted by / received by this user for this app. */
|
|
4409
|
+
async listShares() {
|
|
4410
|
+
return this.rest.listShares();
|
|
4411
|
+
}
|
|
4412
|
+
/** Mount a share: separate local keyspace + subscription. */
|
|
4413
|
+
async mountShare(shareId) {
|
|
4414
|
+
const engine = this.engine;
|
|
4415
|
+
if (!engine) throw new Error("shares require sync mode");
|
|
4416
|
+
const existing = this.mounts.get(shareId);
|
|
4417
|
+
if (existing) return existing;
|
|
4418
|
+
await engine.mountShare(shareId);
|
|
4419
|
+
const handle = {
|
|
4420
|
+
shareId,
|
|
4421
|
+
db: new SyncDb(engine, shareSubKey(shareId))
|
|
4422
|
+
};
|
|
4423
|
+
this.mounts.set(shareId, handle);
|
|
4424
|
+
this.publish();
|
|
4425
|
+
return handle;
|
|
4426
|
+
}
|
|
4427
|
+
async unmountShare(shareId, options) {
|
|
4428
|
+
if (!this.engine) return;
|
|
4429
|
+
await this.engine.unmountShare(shareId, options);
|
|
4430
|
+
this.mounts.delete(shareId);
|
|
4431
|
+
this.publish();
|
|
4432
|
+
}
|
|
4433
|
+
getMountedShare(shareId) {
|
|
4434
|
+
return this.mounts.get(shareId);
|
|
4435
|
+
}
|
|
4436
|
+
// ---------------- React subscription surface ----------------
|
|
4437
|
+
subscribe = (listener) => {
|
|
4438
|
+
this.listeners.add(listener);
|
|
4439
|
+
return () => this.listeners.delete(listener);
|
|
3023
4440
|
};
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
authErrorCode: null,
|
|
3035
|
-
user: null,
|
|
3036
|
-
did: null,
|
|
3037
|
-
tokenScope: null
|
|
3038
|
-
});
|
|
3039
|
-
const authRef = (0, import_react3.useRef)(null);
|
|
3040
|
-
if (!authRef.current) {
|
|
3041
|
-
authRef.current = new AuthManager(
|
|
4441
|
+
getSnapshot = () => {
|
|
4442
|
+
return this.snapshot;
|
|
4443
|
+
};
|
|
4444
|
+
// -------------------------------------------------------------------
|
|
4445
|
+
// Profile binding & transitions
|
|
4446
|
+
// -------------------------------------------------------------------
|
|
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(
|
|
3042
4451
|
{
|
|
3043
|
-
projectId:
|
|
3044
|
-
scopes:
|
|
3045
|
-
pdsUrl: authConfig.pds_url,
|
|
3046
|
-
adminUrl: authConfig.admin_url,
|
|
3047
|
-
debug
|
|
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
|
|
3048
4458
|
},
|
|
3049
|
-
|
|
3050
|
-
() =>
|
|
4459
|
+
storage,
|
|
4460
|
+
() => {
|
|
4461
|
+
if (this.binding === binding) this.handleAuthChange();
|
|
4462
|
+
}
|
|
3051
4463
|
);
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
}
|
|
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
|
+
})
|
|
3074
4492
|
);
|
|
3075
|
-
|
|
4493
|
+
} else {
|
|
4494
|
+
binding.engine = null;
|
|
4495
|
+
binding.syncDb = null;
|
|
3076
4496
|
}
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
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
|
+
}
|
|
3089
4517
|
}
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
storageAdapter,
|
|
3103
|
-
version,
|
|
3104
|
-
getMigrations()
|
|
3105
|
-
);
|
|
3106
|
-
const updateResult = await versionUpdater.checkAndUpdate();
|
|
3107
|
-
if (updateResult.updated) {
|
|
3108
|
-
log(
|
|
3109
|
-
`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`
|
|
3110
|
-
);
|
|
3111
|
-
} else {
|
|
3112
|
-
log(`App version ${updateResult.toVersion} is current`);
|
|
4518
|
+
await this.binding.auth.initialize();
|
|
4519
|
+
this.binding.sessionCleanup.push(this.binding.auth.setupNetworkListeners());
|
|
4520
|
+
this.mounts.clear();
|
|
4521
|
+
this.syncLifecycle();
|
|
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 {
|
|
3113
4530
|
}
|
|
3114
|
-
}
|
|
3115
|
-
|
|
4531
|
+
}, 50);
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
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();
|
|
3116
4544
|
}
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
if (newStatus === "ERROR_WILL_RETRY" /* ERROR_WILL_RETRY */) {
|
|
3132
|
-
log(
|
|
3133
|
-
"Sync entered ERROR_WILL_RETRY - reconciling auth session before retry"
|
|
3134
|
-
);
|
|
3135
|
-
authRef.current.reconcileSession("sync retry", {
|
|
3136
|
-
forceRefresh: true,
|
|
3137
|
-
throttleMs: 0
|
|
3138
|
-
}).catch(() => {
|
|
3139
|
-
});
|
|
3140
|
-
}
|
|
4545
|
+
if (next) this.users.setActiveId(next.id);
|
|
4546
|
+
}
|
|
4547
|
+
await this.activateProfile(next ?? ephemeralLegacyProfile());
|
|
4548
|
+
}
|
|
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" })
|
|
3141
4559
|
});
|
|
3142
|
-
if (options.shouldConnect) {
|
|
3143
|
-
setShouldConnect(true);
|
|
3144
|
-
} else {
|
|
3145
|
-
log("Sync is disabled");
|
|
3146
|
-
}
|
|
3147
|
-
setIsDbReady(true);
|
|
3148
4560
|
}
|
|
4561
|
+
} catch {
|
|
3149
4562
|
}
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
code: "missing_project_id",
|
|
3155
|
-
title: "Project ID Required",
|
|
3156
|
-
message: "Remote mode requires a project_id. Provide it via schema.project_id or the project_id prop."
|
|
3157
|
-
});
|
|
3158
|
-
setIsDbReady(true);
|
|
3159
|
-
return;
|
|
3160
|
-
}
|
|
3161
|
-
log("Initializing Basic Remote DB");
|
|
3162
|
-
remoteDbRef.current = new RemoteDB({
|
|
3163
|
-
serverUrl: authConfig.pds_url,
|
|
3164
|
-
projectId: project_id,
|
|
3165
|
-
getToken: (opts) => authRef.current.getToken(opts),
|
|
3166
|
-
schema,
|
|
3167
|
-
debug,
|
|
3168
|
-
onAuthError: (error2) => {
|
|
3169
|
-
log("RemoteDB auth error:", error2);
|
|
3170
|
-
if (error2.errorType === "forbidden") {
|
|
3171
|
-
log("403 Forbidden - user lacks required scope, not signing out");
|
|
3172
|
-
return;
|
|
3173
|
-
}
|
|
3174
|
-
authRef.current.reconcileSession(`remote db ${error2.errorType}`, {
|
|
3175
|
-
forceRefresh: error2.errorType !== "network",
|
|
3176
|
-
throttleMs: 0
|
|
3177
|
-
}).catch((reconcileError) => {
|
|
3178
|
-
log("RemoteDB auth recovery failed:", reconcileError);
|
|
3179
|
-
});
|
|
3180
|
-
}
|
|
3181
|
-
});
|
|
3182
|
-
setDbStatus("ONLINE" /* ONLINE */);
|
|
3183
|
-
setIsDbReady(true);
|
|
4563
|
+
for (const key of Object.values(STORAGE_KEYS)) {
|
|
4564
|
+
try {
|
|
4565
|
+
await storage.remove(key);
|
|
4566
|
+
} catch {
|
|
3184
4567
|
}
|
|
3185
4568
|
}
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
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);
|
|
3195
4582
|
}
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
lastCheckedAt: Date.now(),
|
|
3202
|
-
error: errorMessage.trim() || void 0
|
|
3203
|
-
});
|
|
3204
|
-
setError({
|
|
3205
|
-
code: "schema_invalid",
|
|
3206
|
-
title: "Basic Schema is invalid!",
|
|
3207
|
-
message: errorMessage
|
|
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();
|
|
3208
4588
|
});
|
|
3209
|
-
setIsDbReady(true);
|
|
3210
|
-
return null;
|
|
3211
|
-
}
|
|
3212
|
-
setSchemaDevInfo({
|
|
3213
|
-
projectId: schema?.project_id ?? null,
|
|
3214
|
-
localVersion: schema?.version,
|
|
3215
|
-
status: result.schemaStatus.status ?? "unknown",
|
|
3216
|
-
valid: result.schemaStatus.valid,
|
|
3217
|
-
lastCheckedAt: Date.now()
|
|
3218
|
-
});
|
|
3219
|
-
if (dbMode === "remote") {
|
|
3220
|
-
initRemoteDb();
|
|
3221
|
-
} else {
|
|
3222
|
-
if (result.schemaStatus.valid) {
|
|
3223
|
-
await initSyncDb({ shouldConnect: true });
|
|
3224
|
-
} else {
|
|
3225
|
-
if (result.schemaStatus.status === "unpublished") {
|
|
3226
|
-
log(
|
|
3227
|
-
"Schema not published yet (version 0) - sync is disabled. Publish your schema to enable sync."
|
|
3228
|
-
);
|
|
3229
|
-
} else {
|
|
3230
|
-
log("Schema is invalid!", result.schemaStatus);
|
|
3231
|
-
}
|
|
3232
|
-
await initSyncDb({ shouldConnect: false });
|
|
3233
|
-
}
|
|
3234
4589
|
}
|
|
3235
|
-
|
|
4590
|
+
} catch {
|
|
3236
4591
|
}
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
} : null
|
|
3248
|
-
);
|
|
3249
|
-
if (dbMode === "remote" && project_id) {
|
|
3250
|
-
initRemoteDb();
|
|
3251
|
-
} else {
|
|
3252
|
-
setIsDbReady(true);
|
|
3253
|
-
}
|
|
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();
|
|
3254
4602
|
}
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
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
|
+
}
|
|
3265
4621
|
}
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
4622
|
+
this.syncLifecycle();
|
|
4623
|
+
void this.maybeUpgradeProfile();
|
|
4624
|
+
this.publish();
|
|
4625
|
+
}
|
|
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();
|
|
3274
4639
|
return;
|
|
3275
4640
|
}
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
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()
|
|
4681
|
+
});
|
|
4682
|
+
if (updated) {
|
|
4683
|
+
this.binding.profile = updated;
|
|
3291
4684
|
}
|
|
4685
|
+
await this.refreshUsers();
|
|
4686
|
+
});
|
|
4687
|
+
}
|
|
4688
|
+
async refreshUsers() {
|
|
4689
|
+
if (this.users) {
|
|
4690
|
+
this.usersCache = await this.users.list();
|
|
3292
4691
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
4692
|
+
this.publish();
|
|
4693
|
+
}
|
|
4694
|
+
queueProfileOp(task) {
|
|
4695
|
+
this.profileOps = this.profileOps.then(task).catch((err) => {
|
|
4696
|
+
log("profile operation failed:", err);
|
|
4697
|
+
});
|
|
4698
|
+
return this.profileOps;
|
|
4699
|
+
}
|
|
4700
|
+
async checkSchema() {
|
|
4701
|
+
if (this.schemaChecked) return;
|
|
4702
|
+
const schema = this.config.schema;
|
|
4703
|
+
if (!schema) {
|
|
4704
|
+
this.devInfo = this.projectId ? {
|
|
4705
|
+
projectId: this.projectId,
|
|
4706
|
+
localVersion: void 0,
|
|
4707
|
+
status: "no_schema",
|
|
4708
|
+
valid: false,
|
|
4709
|
+
lastCheckedAt: Date.now()
|
|
4710
|
+
} : null;
|
|
4711
|
+
this.syncEnabled = false;
|
|
4712
|
+
this.publish();
|
|
4713
|
+
return;
|
|
3295
4714
|
}
|
|
3296
|
-
};
|
|
3297
|
-
const handleSignIn = async () => {
|
|
3298
4715
|
try {
|
|
3299
|
-
await
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
4716
|
+
const result = await validateAndCheckSchema(schema);
|
|
4717
|
+
if (!result.isValid) {
|
|
4718
|
+
const errText = result.errors?.map((e) => e.message || "").join("; ") || "invalid";
|
|
4719
|
+
this.devInfo = {
|
|
4720
|
+
projectId: schema.project_id ?? null,
|
|
4721
|
+
localVersion: schema.version,
|
|
4722
|
+
status: "invalid",
|
|
4723
|
+
valid: false,
|
|
4724
|
+
lastCheckedAt: Date.now(),
|
|
4725
|
+
error: errText
|
|
4726
|
+
};
|
|
4727
|
+
this.syncEnabled = false;
|
|
4728
|
+
} else {
|
|
4729
|
+
const status = result.schemaStatus.status ?? "unknown";
|
|
4730
|
+
this.devInfo = {
|
|
4731
|
+
projectId: schema.project_id ?? null,
|
|
4732
|
+
localVersion: schema.version,
|
|
4733
|
+
status,
|
|
4734
|
+
valid: result.schemaStatus.valid,
|
|
4735
|
+
lastCheckedAt: Date.now()
|
|
4736
|
+
};
|
|
4737
|
+
const locallyPublishable = typeof schema.version === "number" && schema.version > 0;
|
|
4738
|
+
const remoteCheckInconclusive = status === "error" || status === "unknown";
|
|
4739
|
+
this.syncEnabled = result.schemaStatus.valid || remoteCheckInconclusive && locallyPublishable;
|
|
4740
|
+
if (!result.schemaStatus.valid) {
|
|
4741
|
+
if (status === "unpublished") {
|
|
4742
|
+
log("Schema not published (version 0) \u2014 sync is disabled, local-only mode.");
|
|
4743
|
+
} else if (remoteCheckInconclusive && locallyPublishable) {
|
|
4744
|
+
log("Schema registry check failed \u2014 proceeding with the local schema (offline-first).");
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
3307
4747
|
}
|
|
3308
|
-
|
|
4748
|
+
} catch (err) {
|
|
4749
|
+
log("schema check failed:", err);
|
|
4750
|
+
this.syncEnabled = !!schema.version && schema.version > 0;
|
|
4751
|
+
this.devInfo = {
|
|
4752
|
+
projectId: schema.project_id ?? null,
|
|
4753
|
+
localVersion: schema.version,
|
|
4754
|
+
status: "unknown",
|
|
4755
|
+
valid: this.syncEnabled,
|
|
4756
|
+
lastCheckedAt: Date.now()
|
|
4757
|
+
};
|
|
3309
4758
|
}
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
}
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
4759
|
+
this.schemaChecked = true;
|
|
4760
|
+
this.publish();
|
|
4761
|
+
}
|
|
4762
|
+
buildSnapshot() {
|
|
4763
|
+
const { auth, engine, profile } = this.binding;
|
|
4764
|
+
return {
|
|
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,
|
|
4774
|
+
syncEnabled: this.syncEnabled,
|
|
4775
|
+
devInfo: this.devInfo,
|
|
4776
|
+
mode: this.mode,
|
|
4777
|
+
users: this.usersCache,
|
|
4778
|
+
activeUser: this.users ? profile : null,
|
|
4779
|
+
isAnonymous: this.users ? profile.kind === "anon" && !auth.isSignedIn : false
|
|
4780
|
+
};
|
|
4781
|
+
}
|
|
4782
|
+
publish() {
|
|
4783
|
+
this.snapshot = this.buildSnapshot();
|
|
4784
|
+
for (const listener of this.listeners) {
|
|
4785
|
+
try {
|
|
4786
|
+
listener();
|
|
4787
|
+
} catch {
|
|
3321
4788
|
}
|
|
3322
|
-
throw error2;
|
|
3323
4789
|
}
|
|
3324
|
-
}
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
}
|
|
3329
|
-
return syncRef.current || noDb;
|
|
3330
|
-
};
|
|
3331
|
-
const contextValue = {
|
|
3332
|
-
isReady: authState.isAuthReady,
|
|
3333
|
-
isSignedIn: authState.isSignedIn,
|
|
3334
|
-
authStatus: authState.authStatus,
|
|
3335
|
-
authErrorCode: authState.authErrorCode,
|
|
3336
|
-
user: authState.user,
|
|
3337
|
-
did: authState.did,
|
|
3338
|
-
scope: authState.tokenScope,
|
|
3339
|
-
hasScope: (s) => authRef.current.hasScope(s),
|
|
3340
|
-
missingScopes: () => authRef.current.missingScopes(),
|
|
3341
|
-
signIn: handleSignIn,
|
|
3342
|
-
signInWithHandle: handleSignInWithHandle,
|
|
3343
|
-
signOut: handleSignOut,
|
|
3344
|
-
signInWithCode: (code, state) => authRef.current.signInWithCode(code, state),
|
|
3345
|
-
getToken: (opts) => authRef.current.getToken(opts),
|
|
3346
|
-
getSignInUrl: (redirectUri) => authRef.current.getSignInUrl(redirectUri),
|
|
3347
|
-
db: getCurrentDb(),
|
|
3348
|
-
dbStatus,
|
|
3349
|
-
dbMode,
|
|
3350
|
-
devInfo: schemaDevInfo,
|
|
3351
|
-
refreshSchemaStatus,
|
|
3352
|
-
isAuthReady: authState.isAuthReady,
|
|
3353
|
-
signin: handleSignIn,
|
|
3354
|
-
signout: handleSignOut,
|
|
3355
|
-
signinWithCode: (code, state) => authRef.current.signInWithCode(code, state),
|
|
3356
|
-
getSignInLink: (redirectUri) => authRef.current.getSignInUrl(redirectUri)
|
|
3357
|
-
};
|
|
3358
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(BasicContext.Provider, { value: contextValue, children: [
|
|
3359
|
-
error && isDevMode() && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ErrorDisplay, { error }),
|
|
3360
|
-
devToolbar && isDevMode() && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react3.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BasicDevToolbar2, { debug }) }),
|
|
3361
|
-
isDbReady && authState.isAuthReady && children
|
|
3362
|
-
] });
|
|
4790
|
+
}
|
|
4791
|
+
};
|
|
4792
|
+
function createBasicClient(config) {
|
|
4793
|
+
return new BasicClient(config);
|
|
3363
4794
|
}
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
4795
|
+
|
|
4796
|
+
// src/react/BasicProvider.tsx
|
|
4797
|
+
init_network();
|
|
4798
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
4799
|
+
var BasicDevToolbar2 = (0, import_react4.lazy)(
|
|
4800
|
+
() => Promise.resolve().then(() => (init_BasicDevToolbar(), BasicDevToolbar_exports)).then((m) => ({ default: m.BasicDevToolbar }))
|
|
4801
|
+
);
|
|
4802
|
+
function BasicProvider({
|
|
4803
|
+
children,
|
|
4804
|
+
schema,
|
|
4805
|
+
project_id,
|
|
4806
|
+
auth,
|
|
4807
|
+
storage,
|
|
4808
|
+
debug = false,
|
|
4809
|
+
mode = "sync",
|
|
4810
|
+
anonymous = true,
|
|
4811
|
+
devToolbar = false,
|
|
4812
|
+
renderWhileLoading = false
|
|
4813
|
+
}) {
|
|
4814
|
+
const clientRef = (0, import_react4.useRef)(null);
|
|
4815
|
+
if (!clientRef.current) {
|
|
4816
|
+
clientRef.current = new BasicClient({
|
|
4817
|
+
schema,
|
|
4818
|
+
project_id,
|
|
4819
|
+
auth,
|
|
4820
|
+
storage,
|
|
4821
|
+
debug,
|
|
4822
|
+
mode,
|
|
4823
|
+
anonymous
|
|
4824
|
+
});
|
|
4825
|
+
}
|
|
4826
|
+
const client = clientRef.current;
|
|
4827
|
+
(0, import_react4.useEffect)(() => {
|
|
4828
|
+
void client.start();
|
|
4829
|
+
void checkForNewVersion();
|
|
4830
|
+
return () => client.stop();
|
|
4831
|
+
}, []);
|
|
4832
|
+
const snapshot = (0, import_react4.useSyncExternalStore)(client.subscribe, client.getSnapshot, client.getSnapshot);
|
|
4833
|
+
const showDevTools = devToolbar && isDevelopment(debug);
|
|
4834
|
+
const ready = snapshot.isReady;
|
|
4835
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(BasicClientContext.Provider, { value: client, children: [
|
|
4836
|
+
showDevTools && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react4.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BasicDevToolbar2, { debug }) }),
|
|
4837
|
+
(ready || renderWhileLoading) && children
|
|
4838
|
+
] });
|
|
3392
4839
|
}
|
|
3393
4840
|
|
|
3394
4841
|
// src/index.ts
|
|
3395
|
-
|
|
4842
|
+
init_hooks();
|
|
3396
4843
|
init_BasicDevToolbar();
|
|
3397
4844
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3398
4845
|
0 && (module.exports = {
|
|
4846
|
+
AuthManager,
|
|
4847
|
+
BasicClient,
|
|
3399
4848
|
BasicDevToolbar,
|
|
3400
4849
|
BasicProvider,
|
|
3401
|
-
|
|
4850
|
+
DEFAULT_LIMITS,
|
|
4851
|
+
LocalStorageAdapter,
|
|
3402
4852
|
NotAuthenticatedError,
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
4853
|
+
OWN_SUB,
|
|
4854
|
+
PROTOCOL_VERSION,
|
|
4855
|
+
PrefixedStorage,
|
|
4856
|
+
RestClient,
|
|
4857
|
+
RestDb,
|
|
4858
|
+
RestError,
|
|
3406
4859
|
STORAGE_KEYS,
|
|
4860
|
+
SyncConnection,
|
|
4861
|
+
SyncDb,
|
|
4862
|
+
SyncEngine,
|
|
4863
|
+
SyncStore,
|
|
4864
|
+
UserRegistry,
|
|
4865
|
+
applyOpToData,
|
|
4866
|
+
createBasicClient,
|
|
4867
|
+
isAuthError,
|
|
4868
|
+
isRebootstrapError,
|
|
4869
|
+
isRevocationError,
|
|
4870
|
+
isTerminalOpError,
|
|
4871
|
+
mintOpId,
|
|
3407
4872
|
resolveDid,
|
|
3408
4873
|
resolveDidWebUrl,
|
|
3409
4874
|
resolveHandle,
|
|
4875
|
+
shareSubKey,
|
|
4876
|
+
useAuth,
|
|
3410
4877
|
useBasic,
|
|
3411
|
-
|
|
4878
|
+
useBasicClient,
|
|
4879
|
+
useDb,
|
|
4880
|
+
useQuery,
|
|
4881
|
+
useShare,
|
|
4882
|
+
useShares,
|
|
4883
|
+
useSyncStatus,
|
|
4884
|
+
useUsers
|
|
3412
4885
|
});
|
|
3413
4886
|
//# sourceMappingURL=index.js.map
|