@abraca/dabra 1.5.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abracadabra-provider.cjs +478 -42
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +477 -43
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +234 -11
- package/package.json +1 -1
- package/src/AbracadabraBaseProvider.ts +15 -0
- package/src/AbracadabraProvider.ts +56 -0
- package/src/BackgroundSyncManager.ts +44 -30
- package/src/ChatClient.ts +234 -0
- package/src/IdentityDoc.ts +11 -0
- package/src/NotificationsClient.ts +185 -0
- package/src/TreeTimestamps.ts +47 -16
- package/src/index.ts +16 -0
- package/src/types.ts +92 -0
- package/src/webrtc/DevicePairingChannel.ts +18 -6
|
@@ -2287,6 +2287,7 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2287
2287
|
onAwarenessUpdate: () => null,
|
|
2288
2288
|
onAwarenessChange: () => null,
|
|
2289
2289
|
onStateless: () => null,
|
|
2290
|
+
onServerError: () => null,
|
|
2290
2291
|
onUnsyncedChanges: () => null
|
|
2291
2292
|
};
|
|
2292
2293
|
this.isSynced = false;
|
|
@@ -2318,6 +2319,7 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2318
2319
|
this.on("awarenessUpdate", this.configuration.onAwarenessUpdate);
|
|
2319
2320
|
this.on("awarenessChange", this.configuration.onAwarenessChange);
|
|
2320
2321
|
this.on("stateless", this.configuration.onStateless);
|
|
2322
|
+
this.on("serverError", this.configuration.onServerError);
|
|
2321
2323
|
this.on("unsyncedChanges", this.configuration.onUnsyncedChanges);
|
|
2322
2324
|
this.on("authenticated", this.configuration.onAuthenticated);
|
|
2323
2325
|
this.on("authenticationFailed", this.configuration.onAuthenticationFailed);
|
|
@@ -2433,6 +2435,19 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2433
2435
|
if (state) this.emit("synced", { state });
|
|
2434
2436
|
}
|
|
2435
2437
|
receiveStateless(payload) {
|
|
2438
|
+
try {
|
|
2439
|
+
const parsed = JSON.parse(payload);
|
|
2440
|
+
if (parsed?.type === "error" && parsed.source && parsed.code) {
|
|
2441
|
+
const { source, code, message } = parsed;
|
|
2442
|
+
console.warn(`[Abracadabra] Server error: ${source} (${code}) — ${message}`);
|
|
2443
|
+
this.emit("serverError", {
|
|
2444
|
+
source,
|
|
2445
|
+
code,
|
|
2446
|
+
message: message ?? ""
|
|
2447
|
+
});
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
} catch {}
|
|
2436
2451
|
this.emit("stateless", { payload });
|
|
2437
2452
|
}
|
|
2438
2453
|
async connect() {
|
|
@@ -2778,6 +2793,9 @@ function isValidDocId(id) {
|
|
|
2778
2793
|
* refreshed from the server on every reconnect.
|
|
2779
2794
|
*/
|
|
2780
2795
|
var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
2796
|
+
static {
|
|
2797
|
+
this.MAX_CHILDREN = 20;
|
|
2798
|
+
}
|
|
2781
2799
|
constructor(configuration) {
|
|
2782
2800
|
const resolved = { ...configuration };
|
|
2783
2801
|
const client = configuration.client ?? null;
|
|
@@ -2789,6 +2807,8 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2789
2807
|
this.effectiveRole = null;
|
|
2790
2808
|
this.childProviders = /* @__PURE__ */ new Map();
|
|
2791
2809
|
this.pendingLoads = /* @__PURE__ */ new Map();
|
|
2810
|
+
this.childAccessTimes = /* @__PURE__ */ new Map();
|
|
2811
|
+
this.pinnedChildren = /* @__PURE__ */ new Set();
|
|
2792
2812
|
this.boundHandleYSubdocsChange = this.handleYSubdocsChange.bind(this);
|
|
2793
2813
|
this._client = client;
|
|
2794
2814
|
this.abracadabraConfig = configuration;
|
|
@@ -2983,7 +3003,10 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2983
3003
|
*/
|
|
2984
3004
|
loadChild(childId) {
|
|
2985
3005
|
if (!isValidDocId(childId)) return Promise.reject(/* @__PURE__ */ new Error(`loadChild: "${childId}" is not a valid document ID (must be a UUID). If this node was created with an older version of the app, delete it and recreate it.`));
|
|
2986
|
-
if (this.childProviders.has(childId))
|
|
3006
|
+
if (this.childProviders.has(childId)) {
|
|
3007
|
+
this.childAccessTimes.set(childId, Date.now());
|
|
3008
|
+
return Promise.resolve(this.childProviders.get(childId));
|
|
3009
|
+
}
|
|
2987
3010
|
if (this.pendingLoads.has(childId)) return this.pendingLoads.get(childId);
|
|
2988
3011
|
const load = this._doLoadChild(childId);
|
|
2989
3012
|
this.pendingLoads.set(childId, load);
|
|
@@ -3008,6 +3031,8 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
3008
3031
|
});
|
|
3009
3032
|
childProvider.attach();
|
|
3010
3033
|
this.childProviders.set(childId, childProvider);
|
|
3034
|
+
this.childAccessTimes.set(childId, Date.now());
|
|
3035
|
+
this.evictLRU();
|
|
3011
3036
|
this.emit("subdocLoaded", {
|
|
3012
3037
|
childId,
|
|
3013
3038
|
provider: childProvider
|
|
@@ -3019,6 +3044,44 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
3019
3044
|
if (provider) {
|
|
3020
3045
|
provider.destroy();
|
|
3021
3046
|
this.childProviders.delete(childId);
|
|
3047
|
+
this.childAccessTimes.delete(childId);
|
|
3048
|
+
this.pinnedChildren.delete(childId);
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* Mark a child as pinned so LRU eviction will not remove it.
|
|
3053
|
+
* Use this when a document is actively being viewed by the user.
|
|
3054
|
+
*/
|
|
3055
|
+
pinChild(childId) {
|
|
3056
|
+
this.pinnedChildren.add(childId);
|
|
3057
|
+
}
|
|
3058
|
+
/**
|
|
3059
|
+
* Unpin a child, allowing LRU eviction to reclaim it when capacity is exceeded.
|
|
3060
|
+
*/
|
|
3061
|
+
unpinChild(childId) {
|
|
3062
|
+
this.pinnedChildren.delete(childId);
|
|
3063
|
+
this.evictLRU();
|
|
3064
|
+
}
|
|
3065
|
+
/**
|
|
3066
|
+
* Evict least-recently-used unpinned child providers until the cache is
|
|
3067
|
+
* at or below MAX_CHILDREN.
|
|
3068
|
+
*/
|
|
3069
|
+
evictLRU() {
|
|
3070
|
+
if (this.childProviders.size <= AbracadabraProvider.MAX_CHILDREN) return;
|
|
3071
|
+
const evictable = [];
|
|
3072
|
+
for (const [id] of this.childProviders) {
|
|
3073
|
+
if (this.pinnedChildren.has(id)) continue;
|
|
3074
|
+
evictable.push({
|
|
3075
|
+
id,
|
|
3076
|
+
accessTime: this.childAccessTimes.get(id) ?? 0
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
3079
|
+
evictable.sort((a, b) => a.accessTime - b.accessTime);
|
|
3080
|
+
let toEvict = this.childProviders.size - AbracadabraProvider.MAX_CHILDREN;
|
|
3081
|
+
for (const entry of evictable) {
|
|
3082
|
+
if (toEvict <= 0) break;
|
|
3083
|
+
this.unloadChild(entry.id);
|
|
3084
|
+
toEvict--;
|
|
3022
3085
|
}
|
|
3023
3086
|
}
|
|
3024
3087
|
/** Return all currently-loaded child providers. */
|
|
@@ -3078,6 +3141,8 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
3078
3141
|
const childIds = [...this.childProviders.keys()];
|
|
3079
3142
|
for (const provider of this.childProviders.values()) provider.destroy();
|
|
3080
3143
|
this.childProviders.clear();
|
|
3144
|
+
this.childAccessTimes.clear();
|
|
3145
|
+
this.pinnedChildren.clear();
|
|
3081
3146
|
const wsProviderMap = this.configuration.websocketProvider?.configuration?.providerMap;
|
|
3082
3147
|
if (wsProviderMap) for (const childId of childIds) wsProviderMap.delete(childId);
|
|
3083
3148
|
this.offlineStore?.destroy();
|
|
@@ -10867,35 +10932,57 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
|
|
|
10867
10932
|
* This propagates "last edited" timestamps to all peers via the root CRDT,
|
|
10868
10933
|
* without requiring any server-side changes.
|
|
10869
10934
|
*
|
|
10870
|
-
*
|
|
10871
|
-
*
|
|
10935
|
+
* A trailing-edge throttle (default 5 s) limits writes to avoid CRDT bloat
|
|
10936
|
+
* on the root doc during rapid typing.
|
|
10872
10937
|
*/
|
|
10873
10938
|
/**
|
|
10874
|
-
* Attach an observer that writes `updatedAt
|
|
10875
|
-
*
|
|
10876
|
-
* non-offline update.
|
|
10939
|
+
* Attach an observer that writes `updatedAt` to the root doc-tree entry for
|
|
10940
|
+
* `childDocId` whenever the child doc receives a non-offline update.
|
|
10877
10941
|
*
|
|
10878
|
-
*
|
|
10879
|
-
*
|
|
10880
|
-
*
|
|
10942
|
+
* Writes are throttled: the first qualifying update records the timestamp;
|
|
10943
|
+
* a trailing-edge timer flushes it to the tree map after `throttleMs`.
|
|
10944
|
+
*
|
|
10945
|
+
* @param treeMap The root doc's "doc-tree" Y.Map.
|
|
10946
|
+
* @param childDocId The child document's UUID (key in treeMap).
|
|
10947
|
+
* @param childDoc The child Y.Doc to observe.
|
|
10881
10948
|
* @param offlineStore The child provider's OfflineStore (used to detect
|
|
10882
10949
|
* offline-replay origins and skip them). Pass null when
|
|
10883
10950
|
* the offline store is disabled.
|
|
10884
|
-
* @
|
|
10885
|
-
|
|
10886
|
-
function
|
|
10887
|
-
|
|
10888
|
-
|
|
10951
|
+
* @param options Optional config. `throttleMs` controls the write
|
|
10952
|
+
* interval (default 5000).
|
|
10953
|
+
* @returns Cleanup function — call on provider destroy. Flushes
|
|
10954
|
+
* any pending write before detaching.
|
|
10955
|
+
*/
|
|
10956
|
+
function attachUpdatedAtObserver(treeMap, childDocId, childDoc, offlineStore, options) {
|
|
10957
|
+
const throttleMs = options?.throttleMs ?? 5e3;
|
|
10958
|
+
let latestTs = 0;
|
|
10959
|
+
let timer = null;
|
|
10960
|
+
function flush() {
|
|
10961
|
+
if (latestTs === 0) return;
|
|
10962
|
+
const ts = latestTs;
|
|
10963
|
+
latestTs = 0;
|
|
10964
|
+
timer = null;
|
|
10889
10965
|
const raw = treeMap.get(childDocId);
|
|
10890
10966
|
if (!raw) return;
|
|
10891
10967
|
const entry = raw instanceof Y.Map ? raw.toJSON() : raw;
|
|
10892
10968
|
treeMap.set(childDocId, {
|
|
10893
10969
|
...entry,
|
|
10894
|
-
updatedAt:
|
|
10970
|
+
updatedAt: ts
|
|
10895
10971
|
});
|
|
10896
10972
|
}
|
|
10973
|
+
function handler(_update, origin) {
|
|
10974
|
+
if (offlineStore !== null && origin === offlineStore) return;
|
|
10975
|
+
latestTs = Date.now();
|
|
10976
|
+
if (timer === null) timer = setTimeout(flush, throttleMs);
|
|
10977
|
+
}
|
|
10897
10978
|
childDoc.on("update", handler);
|
|
10898
|
-
return () =>
|
|
10979
|
+
return () => {
|
|
10980
|
+
childDoc.off("update", handler);
|
|
10981
|
+
if (timer !== null) {
|
|
10982
|
+
clearTimeout(timer);
|
|
10983
|
+
flush();
|
|
10984
|
+
}
|
|
10985
|
+
};
|
|
10899
10986
|
}
|
|
10900
10987
|
|
|
10901
10988
|
//#endregion
|
|
@@ -11165,7 +11252,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11165
11252
|
async _syncWithSemaphore(docId, updatedAt) {
|
|
11166
11253
|
if (this._destroyed) return true;
|
|
11167
11254
|
const existing = this.syncStates.get(docId);
|
|
11168
|
-
if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced >= updatedAt) {
|
|
11255
|
+
if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced + 200 >= updatedAt) {
|
|
11169
11256
|
this.emit("stateChanged", {
|
|
11170
11257
|
docId,
|
|
11171
11258
|
state: existing
|
|
@@ -11217,6 +11304,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11217
11304
|
}
|
|
11218
11305
|
}
|
|
11219
11306
|
async _syncNonE2EDoc(docId) {
|
|
11307
|
+
const treeMap = this.rootProvider.document.getMap("doc-tree");
|
|
11220
11308
|
const alreadyCached = this.rootProvider.children.has(docId);
|
|
11221
11309
|
if (!alreadyCached) {
|
|
11222
11310
|
if ((this.rootProvider.configuration?.websocketProvider?.configuration?.providerMap)?.has(docId)) return {
|
|
@@ -11227,27 +11315,33 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11227
11315
|
};
|
|
11228
11316
|
}
|
|
11229
11317
|
const childProvider = await this.rootProvider.loadChild(docId);
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11318
|
+
try {
|
|
11319
|
+
await childProvider.ready;
|
|
11320
|
+
await this._waitForSynced(childProvider);
|
|
11321
|
+
{
|
|
11322
|
+
const treeEntry = treeMap.get(docId);
|
|
11323
|
+
this.emit("docSynced", {
|
|
11324
|
+
docId,
|
|
11325
|
+
document: childProvider.document,
|
|
11326
|
+
label: treeEntry?.label ?? "",
|
|
11327
|
+
meta: treeEntry?.meta
|
|
11328
|
+
});
|
|
11329
|
+
}
|
|
11330
|
+
if (this.opts.prefetchFiles && this.fileBlobStore) this._prefetchDocFiles(docId, childProvider.document).catch(() => null);
|
|
11331
|
+
const treeEntry = treeMap.get(docId);
|
|
11332
|
+
const treeUpdatedAt = treeEntry?.updatedAt ?? treeEntry?.createdAt ?? 0;
|
|
11333
|
+
return {
|
|
11235
11334
|
docId,
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11239
|
-
}
|
|
11335
|
+
status: "synced",
|
|
11336
|
+
lastSynced: Math.max(Date.now(), treeUpdatedAt),
|
|
11337
|
+
isE2E: false
|
|
11338
|
+
};
|
|
11339
|
+
} finally {
|
|
11340
|
+
if (!alreadyCached) this.rootProvider.unloadChild(docId);
|
|
11240
11341
|
}
|
|
11241
|
-
if (this.opts.prefetchFiles && this.fileBlobStore) this._prefetchDocFiles(docId, childProvider.document).catch(() => null);
|
|
11242
|
-
if (!alreadyCached) this.rootProvider.unloadChild(docId);
|
|
11243
|
-
return {
|
|
11244
|
-
docId,
|
|
11245
|
-
status: "synced",
|
|
11246
|
-
lastSynced: Date.now(),
|
|
11247
|
-
isE2E: false
|
|
11248
|
-
};
|
|
11249
11342
|
}
|
|
11250
11343
|
async _syncE2EDoc(docId) {
|
|
11344
|
+
const treeMap = this.rootProvider.document.getMap("doc-tree");
|
|
11251
11345
|
const docKeyManager = this.rootProvider.abracadabraConfig?.docKeyManager;
|
|
11252
11346
|
const keystore = this.rootProvider.abracadabraConfig?.keystore;
|
|
11253
11347
|
if (!docKeyManager || !keystore) return {
|
|
@@ -11268,7 +11362,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11268
11362
|
await childProvider.ready;
|
|
11269
11363
|
await this._waitForSynced(childProvider);
|
|
11270
11364
|
{
|
|
11271
|
-
const treeEntry =
|
|
11365
|
+
const treeEntry = treeMap.get(docId);
|
|
11272
11366
|
this.emit("docSynced", {
|
|
11273
11367
|
docId,
|
|
11274
11368
|
document: childDoc,
|
|
@@ -11277,10 +11371,12 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11277
11371
|
});
|
|
11278
11372
|
}
|
|
11279
11373
|
if (this.opts.prefetchFiles && this.fileBlobStore) this._prefetchDocFiles(docId, childDoc).catch(() => null);
|
|
11374
|
+
const treeEntry = treeMap.get(docId);
|
|
11375
|
+
const treeUpdatedAt = treeEntry?.updatedAt ?? treeEntry?.createdAt ?? 0;
|
|
11280
11376
|
return {
|
|
11281
11377
|
docId,
|
|
11282
11378
|
status: "synced",
|
|
11283
|
-
lastSynced: Date.now(),
|
|
11379
|
+
lastSynced: Math.max(Date.now(), treeUpdatedAt),
|
|
11284
11380
|
isE2E: true
|
|
11285
11381
|
};
|
|
11286
11382
|
} finally {
|
|
@@ -13056,8 +13152,12 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
|
|
|
13056
13152
|
/**
|
|
13057
13153
|
* Approve the pending pairing request. Calls `client.addKey()` to
|
|
13058
13154
|
* register Device B's public key, then notifies Device B.
|
|
13155
|
+
*
|
|
13156
|
+
* @param client Authenticated REST client.
|
|
13157
|
+
* @param masterPublicKey Approver's account-level Ed25519 public key (base64url).
|
|
13158
|
+
* Sent to Device B so it can adopt the master's identity doc.
|
|
13059
13159
|
*/
|
|
13060
|
-
async approve(client) {
|
|
13160
|
+
async approve(client, masterPublicKey) {
|
|
13061
13161
|
if (this.role !== "approver") return {
|
|
13062
13162
|
success: false,
|
|
13063
13163
|
error: "Only the approver can approve"
|
|
@@ -13077,7 +13177,10 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
|
|
|
13077
13177
|
deviceName: req.deviceName,
|
|
13078
13178
|
x25519Key: req.x25519Key
|
|
13079
13179
|
});
|
|
13080
|
-
this.sendMessage({
|
|
13180
|
+
this.sendMessage({
|
|
13181
|
+
type: "pair-approved",
|
|
13182
|
+
masterPublicKey
|
|
13183
|
+
});
|
|
13081
13184
|
this._pendingRequest = null;
|
|
13082
13185
|
this.emit("pairingComplete", { success: true });
|
|
13083
13186
|
return { success: true };
|
|
@@ -13100,8 +13203,12 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
|
|
|
13100
13203
|
* Approve via server-side device invite. Creates a single-use invite code
|
|
13101
13204
|
* and sends it to Device B over the E2EE channel. Device B redeems it
|
|
13102
13205
|
* independently via HTTP — Device A can go offline after this.
|
|
13206
|
+
*
|
|
13207
|
+
* @param client Authenticated REST client.
|
|
13208
|
+
* @param masterPublicKey Approver's account-level Ed25519 public key (base64url).
|
|
13209
|
+
* Sent to Device B so it can adopt the master's identity doc.
|
|
13103
13210
|
*/
|
|
13104
|
-
async approveWithInvite(client) {
|
|
13211
|
+
async approveWithInvite(client, masterPublicKey) {
|
|
13105
13212
|
if (this.role !== "approver") return {
|
|
13106
13213
|
success: false,
|
|
13107
13214
|
error: "Only the approver can approve"
|
|
@@ -13118,7 +13225,8 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
|
|
|
13118
13225
|
const { code } = await client.createDeviceInvite();
|
|
13119
13226
|
this.sendMessage({
|
|
13120
13227
|
type: "pair-invite-code",
|
|
13121
|
-
code
|
|
13228
|
+
code,
|
|
13229
|
+
masterPublicKey
|
|
13122
13230
|
});
|
|
13123
13231
|
this._pendingRequest = null;
|
|
13124
13232
|
this.emit("pairingComplete", { success: true });
|
|
@@ -13289,7 +13397,7 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
|
|
|
13289
13397
|
break;
|
|
13290
13398
|
case "pair-approved":
|
|
13291
13399
|
if (this.role !== "requester") return;
|
|
13292
|
-
this.emit("approved");
|
|
13400
|
+
this.emit("approved", { masterPublicKey: msg.masterPublicKey });
|
|
13293
13401
|
this.emit("pairingComplete", { success: true });
|
|
13294
13402
|
break;
|
|
13295
13403
|
case "pair-rejected":
|
|
@@ -13302,7 +13410,7 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
|
|
|
13302
13410
|
break;
|
|
13303
13411
|
case "pair-invite-code":
|
|
13304
13412
|
if (this.role !== "requester") return;
|
|
13305
|
-
this.emit("inviteCode", msg.code);
|
|
13413
|
+
this.emit("inviteCode", msg.code, msg.masterPublicKey);
|
|
13306
13414
|
break;
|
|
13307
13415
|
}
|
|
13308
13416
|
}
|
|
@@ -13829,6 +13937,19 @@ var IdentityDocProvider = class extends EventEmitter {
|
|
|
13829
13937
|
return this.profileMap.size === 0 && this.serversMap.size === 0;
|
|
13830
13938
|
}
|
|
13831
13939
|
/**
|
|
13940
|
+
* Enable WebRTC P2P sync at runtime.
|
|
13941
|
+
* Use this for claimed/passkey users where E2EE identity derivation
|
|
13942
|
+
* was deferred to avoid biometric prompts on page load.
|
|
13943
|
+
*/
|
|
13944
|
+
enableWebRTC(webrtcConfig) {
|
|
13945
|
+
if (this._destroyed || this.webrtc) return;
|
|
13946
|
+
this.config = {
|
|
13947
|
+
...this.config,
|
|
13948
|
+
webrtc: webrtcConfig
|
|
13949
|
+
};
|
|
13950
|
+
this._connectWebRTC();
|
|
13951
|
+
}
|
|
13952
|
+
/**
|
|
13832
13953
|
* Update the sync server URL at runtime (e.g. when user changes their
|
|
13833
13954
|
* designated sync server in settings).
|
|
13834
13955
|
*/
|
|
@@ -14016,5 +14137,318 @@ var DeviceRegistrationService = class {
|
|
|
14016
14137
|
};
|
|
14017
14138
|
|
|
14018
14139
|
//#endregion
|
|
14019
|
-
|
|
14140
|
+
//#region packages/provider/src/ChatClient.ts
|
|
14141
|
+
const DEFAULT_TIMEOUT_MS$1 = 1e4;
|
|
14142
|
+
/**
|
|
14143
|
+
* Typed client for the Abracadabra chat feature.
|
|
14144
|
+
*
|
|
14145
|
+
* Wraps a connected provider (or base provider) and translates JSON envelopes
|
|
14146
|
+
* on the stateless channel into typed method calls and events.
|
|
14147
|
+
*
|
|
14148
|
+
* Events emitted:
|
|
14149
|
+
* - `message` → ChatMessage (new message broadcast)
|
|
14150
|
+
* - `typing` → ChatTypingEvent (typing indicator broadcast)
|
|
14151
|
+
* - `readReceipt` → ChatReadReceipt (mark_read broadcast)
|
|
14152
|
+
*/
|
|
14153
|
+
var ChatClient = class extends EventEmitter {
|
|
14154
|
+
constructor(provider, options) {
|
|
14155
|
+
super();
|
|
14156
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
14157
|
+
this.provider = provider;
|
|
14158
|
+
this.responseTimeoutMs = options?.responseTimeoutMs ?? DEFAULT_TIMEOUT_MS$1;
|
|
14159
|
+
this.boundOnStateless = (data) => this.handleStateless(data.payload);
|
|
14160
|
+
this.provider.on("stateless", this.boundOnStateless);
|
|
14161
|
+
}
|
|
14162
|
+
/** Stop listening for chat messages. Does not disconnect the underlying provider. */
|
|
14163
|
+
destroy() {
|
|
14164
|
+
this.provider.off("stateless", this.boundOnStateless);
|
|
14165
|
+
for (const queue of this.pending.values()) for (const p of queue) {
|
|
14166
|
+
clearTimeout(p.timer);
|
|
14167
|
+
p.reject(/* @__PURE__ */ new Error("ChatClient destroyed"));
|
|
14168
|
+
}
|
|
14169
|
+
this.pending.clear();
|
|
14170
|
+
this.removeAllListeners();
|
|
14171
|
+
}
|
|
14172
|
+
/** Send a chat message on a channel. Fire-and-forget (the server broadcasts). */
|
|
14173
|
+
sendMessage(input) {
|
|
14174
|
+
this.provider.sendStateless(JSON.stringify({
|
|
14175
|
+
type: "chat:send",
|
|
14176
|
+
channel: input.channel,
|
|
14177
|
+
content: input.content,
|
|
14178
|
+
...input.sender_name !== void 0 ? { sender_name: input.sender_name } : {}
|
|
14179
|
+
}));
|
|
14180
|
+
}
|
|
14181
|
+
/** Fetch historical messages for a channel. Resolves with the server response. */
|
|
14182
|
+
getHistory(input) {
|
|
14183
|
+
const promise = this.enqueue("chat:history");
|
|
14184
|
+
this.provider.sendStateless(JSON.stringify({
|
|
14185
|
+
type: "chat:history",
|
|
14186
|
+
channel: input.channel,
|
|
14187
|
+
...input.before !== void 0 ? { before: input.before } : {},
|
|
14188
|
+
...input.limit !== void 0 ? { limit: input.limit } : {}
|
|
14189
|
+
}));
|
|
14190
|
+
return promise;
|
|
14191
|
+
}
|
|
14192
|
+
/** Broadcast a typing indicator on a channel. */
|
|
14193
|
+
sendTyping(channel) {
|
|
14194
|
+
this.provider.sendStateless(JSON.stringify({
|
|
14195
|
+
type: "chat:typing",
|
|
14196
|
+
channel
|
|
14197
|
+
}));
|
|
14198
|
+
}
|
|
14199
|
+
/** List the current user's channels (ordered by last activity). */
|
|
14200
|
+
listChannels() {
|
|
14201
|
+
const promise = this.enqueue("chat:channels");
|
|
14202
|
+
this.provider.sendStateless(JSON.stringify({ type: "chat:channels" }));
|
|
14203
|
+
return promise;
|
|
14204
|
+
}
|
|
14205
|
+
/** Mark a channel read up to `timestamp` (unix ms). */
|
|
14206
|
+
markRead(channel, timestamp) {
|
|
14207
|
+
this.provider.sendStateless(JSON.stringify({
|
|
14208
|
+
type: "chat:mark_read",
|
|
14209
|
+
channel,
|
|
14210
|
+
timestamp
|
|
14211
|
+
}));
|
|
14212
|
+
}
|
|
14213
|
+
/** Fetch per-user read cursors for a channel. */
|
|
14214
|
+
getReadCursors(channel) {
|
|
14215
|
+
const promise = this.enqueue("chat:read_cursors");
|
|
14216
|
+
this.provider.sendStateless(JSON.stringify({
|
|
14217
|
+
type: "chat:read_cursors",
|
|
14218
|
+
channel
|
|
14219
|
+
}));
|
|
14220
|
+
return promise;
|
|
14221
|
+
}
|
|
14222
|
+
onMessage(fn) {
|
|
14223
|
+
return this.on("message", fn);
|
|
14224
|
+
}
|
|
14225
|
+
onTyping(fn) {
|
|
14226
|
+
return this.on("typing", fn);
|
|
14227
|
+
}
|
|
14228
|
+
onReadReceipt(fn) {
|
|
14229
|
+
return this.on("readReceipt", fn);
|
|
14230
|
+
}
|
|
14231
|
+
enqueue(type) {
|
|
14232
|
+
return new Promise((resolve, reject) => {
|
|
14233
|
+
const entry = {
|
|
14234
|
+
resolve,
|
|
14235
|
+
reject,
|
|
14236
|
+
timer: setTimeout(() => {
|
|
14237
|
+
this.removePending(type, entry);
|
|
14238
|
+
reject(/* @__PURE__ */ new Error(`ChatClient: timeout waiting for ${type} response`));
|
|
14239
|
+
}, this.responseTimeoutMs)
|
|
14240
|
+
};
|
|
14241
|
+
const queue = this.pending.get(type) ?? [];
|
|
14242
|
+
queue.push(entry);
|
|
14243
|
+
this.pending.set(type, queue);
|
|
14244
|
+
});
|
|
14245
|
+
}
|
|
14246
|
+
removePending(type, entry) {
|
|
14247
|
+
const queue = this.pending.get(type);
|
|
14248
|
+
if (!queue) return;
|
|
14249
|
+
const idx = queue.indexOf(entry);
|
|
14250
|
+
if (idx >= 0) queue.splice(idx, 1);
|
|
14251
|
+
if (queue.length === 0) this.pending.delete(type);
|
|
14252
|
+
}
|
|
14253
|
+
resolveNext(type, value) {
|
|
14254
|
+
const queue = this.pending.get(type);
|
|
14255
|
+
if (!queue || queue.length === 0) return false;
|
|
14256
|
+
const next = queue.shift();
|
|
14257
|
+
if (queue.length === 0) this.pending.delete(type);
|
|
14258
|
+
clearTimeout(next.timer);
|
|
14259
|
+
next.resolve(value);
|
|
14260
|
+
return true;
|
|
14261
|
+
}
|
|
14262
|
+
handleStateless(payload) {
|
|
14263
|
+
let parsed;
|
|
14264
|
+
try {
|
|
14265
|
+
parsed = JSON.parse(payload);
|
|
14266
|
+
} catch {
|
|
14267
|
+
return;
|
|
14268
|
+
}
|
|
14269
|
+
const type = parsed?.type;
|
|
14270
|
+
if (typeof type !== "string" || !type.startsWith("chat:")) return;
|
|
14271
|
+
switch (type) {
|
|
14272
|
+
case "chat:message": {
|
|
14273
|
+
const { type: _t, ...rest } = parsed;
|
|
14274
|
+
this.emit("message", rest);
|
|
14275
|
+
break;
|
|
14276
|
+
}
|
|
14277
|
+
case "chat:history":
|
|
14278
|
+
this.resolveNext("chat:history", {
|
|
14279
|
+
channel: parsed.channel,
|
|
14280
|
+
messages: parsed.messages ?? []
|
|
14281
|
+
});
|
|
14282
|
+
break;
|
|
14283
|
+
case "chat:channels":
|
|
14284
|
+
this.resolveNext("chat:channels", { channels: parsed.channels ?? [] });
|
|
14285
|
+
break;
|
|
14286
|
+
case "chat:typing":
|
|
14287
|
+
this.emit("typing", {
|
|
14288
|
+
channel: parsed.channel,
|
|
14289
|
+
sender_id: parsed.sender_id,
|
|
14290
|
+
sender_name: parsed.sender_name ?? null
|
|
14291
|
+
});
|
|
14292
|
+
break;
|
|
14293
|
+
case "chat:read_receipt":
|
|
14294
|
+
this.emit("readReceipt", {
|
|
14295
|
+
channel: parsed.channel,
|
|
14296
|
+
user_id: parsed.user_id,
|
|
14297
|
+
last_read_at: parsed.last_read_at
|
|
14298
|
+
});
|
|
14299
|
+
break;
|
|
14300
|
+
case "chat:read_cursors":
|
|
14301
|
+
this.resolveNext("chat:read_cursors", {
|
|
14302
|
+
channel: parsed.channel,
|
|
14303
|
+
cursors: parsed.cursors ?? []
|
|
14304
|
+
});
|
|
14305
|
+
break;
|
|
14306
|
+
default: break;
|
|
14307
|
+
}
|
|
14308
|
+
}
|
|
14309
|
+
};
|
|
14310
|
+
|
|
14311
|
+
//#endregion
|
|
14312
|
+
//#region packages/provider/src/NotificationsClient.ts
|
|
14313
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
14314
|
+
/**
|
|
14315
|
+
* Typed client for the Abracadabra notifications feature.
|
|
14316
|
+
*
|
|
14317
|
+
* Emits:
|
|
14318
|
+
* - `new` → NotificationRecord (incoming notify:new broadcast)
|
|
14319
|
+
* - `readUpdate` → NotificationReadUpdate (broadcast after mark_read / mark_all_read)
|
|
14320
|
+
*/
|
|
14321
|
+
var NotificationsClient = class extends EventEmitter {
|
|
14322
|
+
constructor(provider, options) {
|
|
14323
|
+
super();
|
|
14324
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
14325
|
+
this.provider = provider;
|
|
14326
|
+
this.responseTimeoutMs = options?.responseTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
14327
|
+
this.boundOnStateless = (data) => this.handleStateless(data.payload);
|
|
14328
|
+
this.provider.on("stateless", this.boundOnStateless);
|
|
14329
|
+
}
|
|
14330
|
+
destroy() {
|
|
14331
|
+
this.provider.off("stateless", this.boundOnStateless);
|
|
14332
|
+
for (const queue of this.pending.values()) for (const p of queue) {
|
|
14333
|
+
clearTimeout(p.timer);
|
|
14334
|
+
p.reject(/* @__PURE__ */ new Error("NotificationsClient destroyed"));
|
|
14335
|
+
}
|
|
14336
|
+
this.pending.clear();
|
|
14337
|
+
this.removeAllListeners();
|
|
14338
|
+
}
|
|
14339
|
+
/**
|
|
14340
|
+
* Create a notification targeting a specific recipient. Requires elevated role
|
|
14341
|
+
* (service or admin); a `server:error` event with code `forbidden` is emitted
|
|
14342
|
+
* by the underlying provider if the caller lacks permission.
|
|
14343
|
+
*/
|
|
14344
|
+
create(input) {
|
|
14345
|
+
this.provider.sendStateless(JSON.stringify({
|
|
14346
|
+
type: "notify:create",
|
|
14347
|
+
recipient_id: input.recipient_id,
|
|
14348
|
+
...input.notification_type !== void 0 ? { notification_type: input.notification_type } : {},
|
|
14349
|
+
title: input.title,
|
|
14350
|
+
...input.body !== void 0 ? { body: input.body } : {},
|
|
14351
|
+
...input.icon !== void 0 ? { icon: input.icon } : {},
|
|
14352
|
+
...input.link !== void 0 ? { link: input.link } : {},
|
|
14353
|
+
...input.source_id !== void 0 ? { source_id: input.source_id } : {}
|
|
14354
|
+
}));
|
|
14355
|
+
}
|
|
14356
|
+
/** Fetch notification history for the current user. */
|
|
14357
|
+
fetch(input = {}) {
|
|
14358
|
+
const promise = this.enqueue("notify:history");
|
|
14359
|
+
this.provider.sendStateless(JSON.stringify({
|
|
14360
|
+
type: "notify:fetch",
|
|
14361
|
+
...input.before !== void 0 ? { before: input.before } : {},
|
|
14362
|
+
...input.limit !== void 0 ? { limit: input.limit } : {},
|
|
14363
|
+
...input.unread_only !== void 0 ? { unread_only: input.unread_only } : {}
|
|
14364
|
+
}));
|
|
14365
|
+
return promise;
|
|
14366
|
+
}
|
|
14367
|
+
/** Mark a single notification, or a batch, as read. */
|
|
14368
|
+
markRead(target) {
|
|
14369
|
+
const body = { type: "notify:mark_read" };
|
|
14370
|
+
if ("id" in target) body.id = target.id;
|
|
14371
|
+
else body.ids = target.ids;
|
|
14372
|
+
this.provider.sendStateless(JSON.stringify(body));
|
|
14373
|
+
}
|
|
14374
|
+
/** Mark every notification for the current user as read. */
|
|
14375
|
+
markAllRead() {
|
|
14376
|
+
this.provider.sendStateless(JSON.stringify({ type: "notify:mark_all_read" }));
|
|
14377
|
+
}
|
|
14378
|
+
/** Mark all notifications generated by the given source (e.g. a chat channel) as read. */
|
|
14379
|
+
markReadBySource(sourceId) {
|
|
14380
|
+
this.provider.sendStateless(JSON.stringify({
|
|
14381
|
+
type: "notify:mark_read_by_source",
|
|
14382
|
+
source_id: sourceId
|
|
14383
|
+
}));
|
|
14384
|
+
}
|
|
14385
|
+
onNew(fn) {
|
|
14386
|
+
return this.on("new", fn);
|
|
14387
|
+
}
|
|
14388
|
+
onReadUpdate(fn) {
|
|
14389
|
+
return this.on("readUpdate", fn);
|
|
14390
|
+
}
|
|
14391
|
+
enqueue(type) {
|
|
14392
|
+
return new Promise((resolve, reject) => {
|
|
14393
|
+
const entry = {
|
|
14394
|
+
resolve,
|
|
14395
|
+
reject,
|
|
14396
|
+
timer: setTimeout(() => {
|
|
14397
|
+
this.removePending(type, entry);
|
|
14398
|
+
reject(/* @__PURE__ */ new Error(`NotificationsClient: timeout waiting for ${type} response`));
|
|
14399
|
+
}, this.responseTimeoutMs)
|
|
14400
|
+
};
|
|
14401
|
+
const queue = this.pending.get(type) ?? [];
|
|
14402
|
+
queue.push(entry);
|
|
14403
|
+
this.pending.set(type, queue);
|
|
14404
|
+
});
|
|
14405
|
+
}
|
|
14406
|
+
removePending(type, entry) {
|
|
14407
|
+
const queue = this.pending.get(type);
|
|
14408
|
+
if (!queue) return;
|
|
14409
|
+
const idx = queue.indexOf(entry);
|
|
14410
|
+
if (idx >= 0) queue.splice(idx, 1);
|
|
14411
|
+
if (queue.length === 0) this.pending.delete(type);
|
|
14412
|
+
}
|
|
14413
|
+
resolveNext(type, value) {
|
|
14414
|
+
const queue = this.pending.get(type);
|
|
14415
|
+
if (!queue || queue.length === 0) return false;
|
|
14416
|
+
const next = queue.shift();
|
|
14417
|
+
if (queue.length === 0) this.pending.delete(type);
|
|
14418
|
+
clearTimeout(next.timer);
|
|
14419
|
+
next.resolve(value);
|
|
14420
|
+
return true;
|
|
14421
|
+
}
|
|
14422
|
+
handleStateless(payload) {
|
|
14423
|
+
let parsed;
|
|
14424
|
+
try {
|
|
14425
|
+
parsed = JSON.parse(payload);
|
|
14426
|
+
} catch {
|
|
14427
|
+
return;
|
|
14428
|
+
}
|
|
14429
|
+
const type = parsed?.type;
|
|
14430
|
+
if (typeof type !== "string" || !type.startsWith("notify:")) return;
|
|
14431
|
+
switch (type) {
|
|
14432
|
+
case "notify:new": {
|
|
14433
|
+
const { type: _t, ...rest } = parsed;
|
|
14434
|
+
this.emit("new", rest);
|
|
14435
|
+
break;
|
|
14436
|
+
}
|
|
14437
|
+
case "notify:history":
|
|
14438
|
+
this.resolveNext("notify:history", { notifications: parsed.notifications ?? [] });
|
|
14439
|
+
break;
|
|
14440
|
+
case "notify:read_update": {
|
|
14441
|
+
const update = { recipient_id: parsed.recipient_id };
|
|
14442
|
+
if (parsed.ids !== void 0) update.ids = parsed.ids;
|
|
14443
|
+
if (parsed.all !== void 0) update.all = parsed.all;
|
|
14444
|
+
this.emit("readUpdate", update);
|
|
14445
|
+
break;
|
|
14446
|
+
}
|
|
14447
|
+
default: break;
|
|
14448
|
+
}
|
|
14449
|
+
}
|
|
14450
|
+
};
|
|
14451
|
+
|
|
14452
|
+
//#endregion
|
|
14453
|
+
export { AbracadabraBaseProvider, AbracadabraClient, AbracadabraProvider, AbracadabraWS, AbracadabraWebRTC, AuthMessageType, AwarenessError, BackgroundSyncManager, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, ChatClient, ConnectionTimeout, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, DevicePairingChannel, DeviceRegistrationService, DocKeyManager, DocumentCache, E2EAbracadabraProvider, E2EEChannel, E2EOfflineStore, EncryptedYMap, EncryptedYText, FileBlobStore, FileTransferChannel, FileTransferHandle, Forbidden, HocuspocusProvider, HocuspocusProviderWebsocket, IdentityDocProvider, KEY_EXCHANGE_CHANNEL, ManualSignaling, MessageTooBig, MessageType, NotificationsClient, OfflineStore, PeerConnection, ResetConnection, SearchIndex, SignalingSocket, SubdocMessage, Unauthorized, WebSocketStatus, WsReadyStates, YjsDataChannel, attachUpdatedAtObserver, awarenessStatesToArray, wordlist as bip39Wordlist, decryptField, deriveIdentityDocId, deriveSeedWrappingKey, encryptField, generateMnemonic, makeEncryptedYMap, makeEncryptedYText, mnemonicToEd25519Seed, mnemonicToKeyPair, readAuthMessage, unwrapSeed, validateMnemonic, wrapSeed, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest };
|
|
14020
14454
|
//# sourceMappingURL=abracadabra-provider.esm.js.map
|