@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
|
@@ -2317,6 +2317,7 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2317
2317
|
onAwarenessUpdate: () => null,
|
|
2318
2318
|
onAwarenessChange: () => null,
|
|
2319
2319
|
onStateless: () => null,
|
|
2320
|
+
onServerError: () => null,
|
|
2320
2321
|
onUnsyncedChanges: () => null
|
|
2321
2322
|
};
|
|
2322
2323
|
this.isSynced = false;
|
|
@@ -2348,6 +2349,7 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2348
2349
|
this.on("awarenessUpdate", this.configuration.onAwarenessUpdate);
|
|
2349
2350
|
this.on("awarenessChange", this.configuration.onAwarenessChange);
|
|
2350
2351
|
this.on("stateless", this.configuration.onStateless);
|
|
2352
|
+
this.on("serverError", this.configuration.onServerError);
|
|
2351
2353
|
this.on("unsyncedChanges", this.configuration.onUnsyncedChanges);
|
|
2352
2354
|
this.on("authenticated", this.configuration.onAuthenticated);
|
|
2353
2355
|
this.on("authenticationFailed", this.configuration.onAuthenticationFailed);
|
|
@@ -2463,6 +2465,19 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2463
2465
|
if (state) this.emit("synced", { state });
|
|
2464
2466
|
}
|
|
2465
2467
|
receiveStateless(payload) {
|
|
2468
|
+
try {
|
|
2469
|
+
const parsed = JSON.parse(payload);
|
|
2470
|
+
if (parsed?.type === "error" && parsed.source && parsed.code) {
|
|
2471
|
+
const { source, code, message } = parsed;
|
|
2472
|
+
console.warn(`[Abracadabra] Server error: ${source} (${code}) — ${message}`);
|
|
2473
|
+
this.emit("serverError", {
|
|
2474
|
+
source,
|
|
2475
|
+
code,
|
|
2476
|
+
message: message ?? ""
|
|
2477
|
+
});
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
} catch {}
|
|
2466
2481
|
this.emit("stateless", { payload });
|
|
2467
2482
|
}
|
|
2468
2483
|
async connect() {
|
|
@@ -2808,6 +2823,9 @@ function isValidDocId(id) {
|
|
|
2808
2823
|
* refreshed from the server on every reconnect.
|
|
2809
2824
|
*/
|
|
2810
2825
|
var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
2826
|
+
static {
|
|
2827
|
+
this.MAX_CHILDREN = 20;
|
|
2828
|
+
}
|
|
2811
2829
|
constructor(configuration) {
|
|
2812
2830
|
const resolved = { ...configuration };
|
|
2813
2831
|
const client = configuration.client ?? null;
|
|
@@ -2819,6 +2837,8 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2819
2837
|
this.effectiveRole = null;
|
|
2820
2838
|
this.childProviders = /* @__PURE__ */ new Map();
|
|
2821
2839
|
this.pendingLoads = /* @__PURE__ */ new Map();
|
|
2840
|
+
this.childAccessTimes = /* @__PURE__ */ new Map();
|
|
2841
|
+
this.pinnedChildren = /* @__PURE__ */ new Set();
|
|
2822
2842
|
this.boundHandleYSubdocsChange = this.handleYSubdocsChange.bind(this);
|
|
2823
2843
|
this._client = client;
|
|
2824
2844
|
this.abracadabraConfig = configuration;
|
|
@@ -3013,7 +3033,10 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
3013
3033
|
*/
|
|
3014
3034
|
loadChild(childId) {
|
|
3015
3035
|
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.`));
|
|
3016
|
-
if (this.childProviders.has(childId))
|
|
3036
|
+
if (this.childProviders.has(childId)) {
|
|
3037
|
+
this.childAccessTimes.set(childId, Date.now());
|
|
3038
|
+
return Promise.resolve(this.childProviders.get(childId));
|
|
3039
|
+
}
|
|
3017
3040
|
if (this.pendingLoads.has(childId)) return this.pendingLoads.get(childId);
|
|
3018
3041
|
const load = this._doLoadChild(childId);
|
|
3019
3042
|
this.pendingLoads.set(childId, load);
|
|
@@ -3038,6 +3061,8 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
3038
3061
|
});
|
|
3039
3062
|
childProvider.attach();
|
|
3040
3063
|
this.childProviders.set(childId, childProvider);
|
|
3064
|
+
this.childAccessTimes.set(childId, Date.now());
|
|
3065
|
+
this.evictLRU();
|
|
3041
3066
|
this.emit("subdocLoaded", {
|
|
3042
3067
|
childId,
|
|
3043
3068
|
provider: childProvider
|
|
@@ -3049,6 +3074,44 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
3049
3074
|
if (provider) {
|
|
3050
3075
|
provider.destroy();
|
|
3051
3076
|
this.childProviders.delete(childId);
|
|
3077
|
+
this.childAccessTimes.delete(childId);
|
|
3078
|
+
this.pinnedChildren.delete(childId);
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* Mark a child as pinned so LRU eviction will not remove it.
|
|
3083
|
+
* Use this when a document is actively being viewed by the user.
|
|
3084
|
+
*/
|
|
3085
|
+
pinChild(childId) {
|
|
3086
|
+
this.pinnedChildren.add(childId);
|
|
3087
|
+
}
|
|
3088
|
+
/**
|
|
3089
|
+
* Unpin a child, allowing LRU eviction to reclaim it when capacity is exceeded.
|
|
3090
|
+
*/
|
|
3091
|
+
unpinChild(childId) {
|
|
3092
|
+
this.pinnedChildren.delete(childId);
|
|
3093
|
+
this.evictLRU();
|
|
3094
|
+
}
|
|
3095
|
+
/**
|
|
3096
|
+
* Evict least-recently-used unpinned child providers until the cache is
|
|
3097
|
+
* at or below MAX_CHILDREN.
|
|
3098
|
+
*/
|
|
3099
|
+
evictLRU() {
|
|
3100
|
+
if (this.childProviders.size <= AbracadabraProvider.MAX_CHILDREN) return;
|
|
3101
|
+
const evictable = [];
|
|
3102
|
+
for (const [id] of this.childProviders) {
|
|
3103
|
+
if (this.pinnedChildren.has(id)) continue;
|
|
3104
|
+
evictable.push({
|
|
3105
|
+
id,
|
|
3106
|
+
accessTime: this.childAccessTimes.get(id) ?? 0
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
evictable.sort((a, b) => a.accessTime - b.accessTime);
|
|
3110
|
+
let toEvict = this.childProviders.size - AbracadabraProvider.MAX_CHILDREN;
|
|
3111
|
+
for (const entry of evictable) {
|
|
3112
|
+
if (toEvict <= 0) break;
|
|
3113
|
+
this.unloadChild(entry.id);
|
|
3114
|
+
toEvict--;
|
|
3052
3115
|
}
|
|
3053
3116
|
}
|
|
3054
3117
|
/** Return all currently-loaded child providers. */
|
|
@@ -3108,6 +3171,8 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
3108
3171
|
const childIds = [...this.childProviders.keys()];
|
|
3109
3172
|
for (const provider of this.childProviders.values()) provider.destroy();
|
|
3110
3173
|
this.childProviders.clear();
|
|
3174
|
+
this.childAccessTimes.clear();
|
|
3175
|
+
this.pinnedChildren.clear();
|
|
3111
3176
|
const wsProviderMap = this.configuration.websocketProvider?.configuration?.providerMap;
|
|
3112
3177
|
if (wsProviderMap) for (const childId of childIds) wsProviderMap.delete(childId);
|
|
3113
3178
|
this.offlineStore?.destroy();
|
|
@@ -10906,35 +10971,57 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
|
|
|
10906
10971
|
* This propagates "last edited" timestamps to all peers via the root CRDT,
|
|
10907
10972
|
* without requiring any server-side changes.
|
|
10908
10973
|
*
|
|
10909
|
-
*
|
|
10910
|
-
*
|
|
10974
|
+
* A trailing-edge throttle (default 5 s) limits writes to avoid CRDT bloat
|
|
10975
|
+
* on the root doc during rapid typing.
|
|
10911
10976
|
*/
|
|
10912
10977
|
/**
|
|
10913
|
-
* Attach an observer that writes `updatedAt
|
|
10914
|
-
*
|
|
10915
|
-
* non-offline update.
|
|
10978
|
+
* Attach an observer that writes `updatedAt` to the root doc-tree entry for
|
|
10979
|
+
* `childDocId` whenever the child doc receives a non-offline update.
|
|
10916
10980
|
*
|
|
10917
|
-
*
|
|
10918
|
-
*
|
|
10919
|
-
*
|
|
10981
|
+
* Writes are throttled: the first qualifying update records the timestamp;
|
|
10982
|
+
* a trailing-edge timer flushes it to the tree map after `throttleMs`.
|
|
10983
|
+
*
|
|
10984
|
+
* @param treeMap The root doc's "doc-tree" Y.Map.
|
|
10985
|
+
* @param childDocId The child document's UUID (key in treeMap).
|
|
10986
|
+
* @param childDoc The child Y.Doc to observe.
|
|
10920
10987
|
* @param offlineStore The child provider's OfflineStore (used to detect
|
|
10921
10988
|
* offline-replay origins and skip them). Pass null when
|
|
10922
10989
|
* the offline store is disabled.
|
|
10923
|
-
* @
|
|
10924
|
-
|
|
10925
|
-
function
|
|
10926
|
-
|
|
10927
|
-
|
|
10990
|
+
* @param options Optional config. `throttleMs` controls the write
|
|
10991
|
+
* interval (default 5000).
|
|
10992
|
+
* @returns Cleanup function — call on provider destroy. Flushes
|
|
10993
|
+
* any pending write before detaching.
|
|
10994
|
+
*/
|
|
10995
|
+
function attachUpdatedAtObserver(treeMap, childDocId, childDoc, offlineStore, options) {
|
|
10996
|
+
const throttleMs = options?.throttleMs ?? 5e3;
|
|
10997
|
+
let latestTs = 0;
|
|
10998
|
+
let timer = null;
|
|
10999
|
+
function flush() {
|
|
11000
|
+
if (latestTs === 0) return;
|
|
11001
|
+
const ts = latestTs;
|
|
11002
|
+
latestTs = 0;
|
|
11003
|
+
timer = null;
|
|
10928
11004
|
const raw = treeMap.get(childDocId);
|
|
10929
11005
|
if (!raw) return;
|
|
10930
11006
|
const entry = raw instanceof yjs.Map ? raw.toJSON() : raw;
|
|
10931
11007
|
treeMap.set(childDocId, {
|
|
10932
11008
|
...entry,
|
|
10933
|
-
updatedAt:
|
|
11009
|
+
updatedAt: ts
|
|
10934
11010
|
});
|
|
10935
11011
|
}
|
|
11012
|
+
function handler(_update, origin) {
|
|
11013
|
+
if (offlineStore !== null && origin === offlineStore) return;
|
|
11014
|
+
latestTs = Date.now();
|
|
11015
|
+
if (timer === null) timer = setTimeout(flush, throttleMs);
|
|
11016
|
+
}
|
|
10936
11017
|
childDoc.on("update", handler);
|
|
10937
|
-
return () =>
|
|
11018
|
+
return () => {
|
|
11019
|
+
childDoc.off("update", handler);
|
|
11020
|
+
if (timer !== null) {
|
|
11021
|
+
clearTimeout(timer);
|
|
11022
|
+
flush();
|
|
11023
|
+
}
|
|
11024
|
+
};
|
|
10938
11025
|
}
|
|
10939
11026
|
|
|
10940
11027
|
//#endregion
|
|
@@ -11204,7 +11291,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11204
11291
|
async _syncWithSemaphore(docId, updatedAt) {
|
|
11205
11292
|
if (this._destroyed) return true;
|
|
11206
11293
|
const existing = this.syncStates.get(docId);
|
|
11207
|
-
if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced >= updatedAt) {
|
|
11294
|
+
if (existing && existing.status === "synced" && existing.lastSynced !== null && existing.lastSynced + 200 >= updatedAt) {
|
|
11208
11295
|
this.emit("stateChanged", {
|
|
11209
11296
|
docId,
|
|
11210
11297
|
state: existing
|
|
@@ -11256,6 +11343,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11256
11343
|
}
|
|
11257
11344
|
}
|
|
11258
11345
|
async _syncNonE2EDoc(docId) {
|
|
11346
|
+
const treeMap = this.rootProvider.document.getMap("doc-tree");
|
|
11259
11347
|
const alreadyCached = this.rootProvider.children.has(docId);
|
|
11260
11348
|
if (!alreadyCached) {
|
|
11261
11349
|
if ((this.rootProvider.configuration?.websocketProvider?.configuration?.providerMap)?.has(docId)) return {
|
|
@@ -11266,27 +11354,33 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11266
11354
|
};
|
|
11267
11355
|
}
|
|
11268
11356
|
const childProvider = await this.rootProvider.loadChild(docId);
|
|
11269
|
-
|
|
11270
|
-
|
|
11271
|
-
|
|
11272
|
-
|
|
11273
|
-
|
|
11357
|
+
try {
|
|
11358
|
+
await childProvider.ready;
|
|
11359
|
+
await this._waitForSynced(childProvider);
|
|
11360
|
+
{
|
|
11361
|
+
const treeEntry = treeMap.get(docId);
|
|
11362
|
+
this.emit("docSynced", {
|
|
11363
|
+
docId,
|
|
11364
|
+
document: childProvider.document,
|
|
11365
|
+
label: treeEntry?.label ?? "",
|
|
11366
|
+
meta: treeEntry?.meta
|
|
11367
|
+
});
|
|
11368
|
+
}
|
|
11369
|
+
if (this.opts.prefetchFiles && this.fileBlobStore) this._prefetchDocFiles(docId, childProvider.document).catch(() => null);
|
|
11370
|
+
const treeEntry = treeMap.get(docId);
|
|
11371
|
+
const treeUpdatedAt = treeEntry?.updatedAt ?? treeEntry?.createdAt ?? 0;
|
|
11372
|
+
return {
|
|
11274
11373
|
docId,
|
|
11275
|
-
|
|
11276
|
-
|
|
11277
|
-
|
|
11278
|
-
}
|
|
11374
|
+
status: "synced",
|
|
11375
|
+
lastSynced: Math.max(Date.now(), treeUpdatedAt),
|
|
11376
|
+
isE2E: false
|
|
11377
|
+
};
|
|
11378
|
+
} finally {
|
|
11379
|
+
if (!alreadyCached) this.rootProvider.unloadChild(docId);
|
|
11279
11380
|
}
|
|
11280
|
-
if (this.opts.prefetchFiles && this.fileBlobStore) this._prefetchDocFiles(docId, childProvider.document).catch(() => null);
|
|
11281
|
-
if (!alreadyCached) this.rootProvider.unloadChild(docId);
|
|
11282
|
-
return {
|
|
11283
|
-
docId,
|
|
11284
|
-
status: "synced",
|
|
11285
|
-
lastSynced: Date.now(),
|
|
11286
|
-
isE2E: false
|
|
11287
|
-
};
|
|
11288
11381
|
}
|
|
11289
11382
|
async _syncE2EDoc(docId) {
|
|
11383
|
+
const treeMap = this.rootProvider.document.getMap("doc-tree");
|
|
11290
11384
|
const docKeyManager = this.rootProvider.abracadabraConfig?.docKeyManager;
|
|
11291
11385
|
const keystore = this.rootProvider.abracadabraConfig?.keystore;
|
|
11292
11386
|
if (!docKeyManager || !keystore) return {
|
|
@@ -11307,7 +11401,7 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11307
11401
|
await childProvider.ready;
|
|
11308
11402
|
await this._waitForSynced(childProvider);
|
|
11309
11403
|
{
|
|
11310
|
-
const treeEntry =
|
|
11404
|
+
const treeEntry = treeMap.get(docId);
|
|
11311
11405
|
this.emit("docSynced", {
|
|
11312
11406
|
docId,
|
|
11313
11407
|
document: childDoc,
|
|
@@ -11316,10 +11410,12 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
11316
11410
|
});
|
|
11317
11411
|
}
|
|
11318
11412
|
if (this.opts.prefetchFiles && this.fileBlobStore) this._prefetchDocFiles(docId, childDoc).catch(() => null);
|
|
11413
|
+
const treeEntry = treeMap.get(docId);
|
|
11414
|
+
const treeUpdatedAt = treeEntry?.updatedAt ?? treeEntry?.createdAt ?? 0;
|
|
11319
11415
|
return {
|
|
11320
11416
|
docId,
|
|
11321
11417
|
status: "synced",
|
|
11322
|
-
lastSynced: Date.now(),
|
|
11418
|
+
lastSynced: Math.max(Date.now(), treeUpdatedAt),
|
|
11323
11419
|
isE2E: true
|
|
11324
11420
|
};
|
|
11325
11421
|
} finally {
|
|
@@ -13095,8 +13191,12 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
|
|
|
13095
13191
|
/**
|
|
13096
13192
|
* Approve the pending pairing request. Calls `client.addKey()` to
|
|
13097
13193
|
* register Device B's public key, then notifies Device B.
|
|
13194
|
+
*
|
|
13195
|
+
* @param client Authenticated REST client.
|
|
13196
|
+
* @param masterPublicKey Approver's account-level Ed25519 public key (base64url).
|
|
13197
|
+
* Sent to Device B so it can adopt the master's identity doc.
|
|
13098
13198
|
*/
|
|
13099
|
-
async approve(client) {
|
|
13199
|
+
async approve(client, masterPublicKey) {
|
|
13100
13200
|
if (this.role !== "approver") return {
|
|
13101
13201
|
success: false,
|
|
13102
13202
|
error: "Only the approver can approve"
|
|
@@ -13116,7 +13216,10 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
|
|
|
13116
13216
|
deviceName: req.deviceName,
|
|
13117
13217
|
x25519Key: req.x25519Key
|
|
13118
13218
|
});
|
|
13119
|
-
this.sendMessage({
|
|
13219
|
+
this.sendMessage({
|
|
13220
|
+
type: "pair-approved",
|
|
13221
|
+
masterPublicKey
|
|
13222
|
+
});
|
|
13120
13223
|
this._pendingRequest = null;
|
|
13121
13224
|
this.emit("pairingComplete", { success: true });
|
|
13122
13225
|
return { success: true };
|
|
@@ -13139,8 +13242,12 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
|
|
|
13139
13242
|
* Approve via server-side device invite. Creates a single-use invite code
|
|
13140
13243
|
* and sends it to Device B over the E2EE channel. Device B redeems it
|
|
13141
13244
|
* independently via HTTP — Device A can go offline after this.
|
|
13245
|
+
*
|
|
13246
|
+
* @param client Authenticated REST client.
|
|
13247
|
+
* @param masterPublicKey Approver's account-level Ed25519 public key (base64url).
|
|
13248
|
+
* Sent to Device B so it can adopt the master's identity doc.
|
|
13142
13249
|
*/
|
|
13143
|
-
async approveWithInvite(client) {
|
|
13250
|
+
async approveWithInvite(client, masterPublicKey) {
|
|
13144
13251
|
if (this.role !== "approver") return {
|
|
13145
13252
|
success: false,
|
|
13146
13253
|
error: "Only the approver can approve"
|
|
@@ -13157,7 +13264,8 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
|
|
|
13157
13264
|
const { code } = await client.createDeviceInvite();
|
|
13158
13265
|
this.sendMessage({
|
|
13159
13266
|
type: "pair-invite-code",
|
|
13160
|
-
code
|
|
13267
|
+
code,
|
|
13268
|
+
masterPublicKey
|
|
13161
13269
|
});
|
|
13162
13270
|
this._pendingRequest = null;
|
|
13163
13271
|
this.emit("pairingComplete", { success: true });
|
|
@@ -13328,7 +13436,7 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
|
|
|
13328
13436
|
break;
|
|
13329
13437
|
case "pair-approved":
|
|
13330
13438
|
if (this.role !== "requester") return;
|
|
13331
|
-
this.emit("approved");
|
|
13439
|
+
this.emit("approved", { masterPublicKey: msg.masterPublicKey });
|
|
13332
13440
|
this.emit("pairingComplete", { success: true });
|
|
13333
13441
|
break;
|
|
13334
13442
|
case "pair-rejected":
|
|
@@ -13341,7 +13449,7 @@ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
|
|
|
13341
13449
|
break;
|
|
13342
13450
|
case "pair-invite-code":
|
|
13343
13451
|
if (this.role !== "requester") return;
|
|
13344
|
-
this.emit("inviteCode", msg.code);
|
|
13452
|
+
this.emit("inviteCode", msg.code, msg.masterPublicKey);
|
|
13345
13453
|
break;
|
|
13346
13454
|
}
|
|
13347
13455
|
}
|
|
@@ -13868,6 +13976,19 @@ var IdentityDocProvider = class extends EventEmitter {
|
|
|
13868
13976
|
return this.profileMap.size === 0 && this.serversMap.size === 0;
|
|
13869
13977
|
}
|
|
13870
13978
|
/**
|
|
13979
|
+
* Enable WebRTC P2P sync at runtime.
|
|
13980
|
+
* Use this for claimed/passkey users where E2EE identity derivation
|
|
13981
|
+
* was deferred to avoid biometric prompts on page load.
|
|
13982
|
+
*/
|
|
13983
|
+
enableWebRTC(webrtcConfig) {
|
|
13984
|
+
if (this._destroyed || this.webrtc) return;
|
|
13985
|
+
this.config = {
|
|
13986
|
+
...this.config,
|
|
13987
|
+
webrtc: webrtcConfig
|
|
13988
|
+
};
|
|
13989
|
+
this._connectWebRTC();
|
|
13990
|
+
}
|
|
13991
|
+
/**
|
|
13871
13992
|
* Update the sync server URL at runtime (e.g. when user changes their
|
|
13872
13993
|
* designated sync server in settings).
|
|
13873
13994
|
*/
|
|
@@ -14054,6 +14175,319 @@ var DeviceRegistrationService = class {
|
|
|
14054
14175
|
}
|
|
14055
14176
|
};
|
|
14056
14177
|
|
|
14178
|
+
//#endregion
|
|
14179
|
+
//#region packages/provider/src/ChatClient.ts
|
|
14180
|
+
const DEFAULT_TIMEOUT_MS$1 = 1e4;
|
|
14181
|
+
/**
|
|
14182
|
+
* Typed client for the Abracadabra chat feature.
|
|
14183
|
+
*
|
|
14184
|
+
* Wraps a connected provider (or base provider) and translates JSON envelopes
|
|
14185
|
+
* on the stateless channel into typed method calls and events.
|
|
14186
|
+
*
|
|
14187
|
+
* Events emitted:
|
|
14188
|
+
* - `message` → ChatMessage (new message broadcast)
|
|
14189
|
+
* - `typing` → ChatTypingEvent (typing indicator broadcast)
|
|
14190
|
+
* - `readReceipt` → ChatReadReceipt (mark_read broadcast)
|
|
14191
|
+
*/
|
|
14192
|
+
var ChatClient = class extends EventEmitter {
|
|
14193
|
+
constructor(provider, options) {
|
|
14194
|
+
super();
|
|
14195
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
14196
|
+
this.provider = provider;
|
|
14197
|
+
this.responseTimeoutMs = options?.responseTimeoutMs ?? DEFAULT_TIMEOUT_MS$1;
|
|
14198
|
+
this.boundOnStateless = (data) => this.handleStateless(data.payload);
|
|
14199
|
+
this.provider.on("stateless", this.boundOnStateless);
|
|
14200
|
+
}
|
|
14201
|
+
/** Stop listening for chat messages. Does not disconnect the underlying provider. */
|
|
14202
|
+
destroy() {
|
|
14203
|
+
this.provider.off("stateless", this.boundOnStateless);
|
|
14204
|
+
for (const queue of this.pending.values()) for (const p of queue) {
|
|
14205
|
+
clearTimeout(p.timer);
|
|
14206
|
+
p.reject(/* @__PURE__ */ new Error("ChatClient destroyed"));
|
|
14207
|
+
}
|
|
14208
|
+
this.pending.clear();
|
|
14209
|
+
this.removeAllListeners();
|
|
14210
|
+
}
|
|
14211
|
+
/** Send a chat message on a channel. Fire-and-forget (the server broadcasts). */
|
|
14212
|
+
sendMessage(input) {
|
|
14213
|
+
this.provider.sendStateless(JSON.stringify({
|
|
14214
|
+
type: "chat:send",
|
|
14215
|
+
channel: input.channel,
|
|
14216
|
+
content: input.content,
|
|
14217
|
+
...input.sender_name !== void 0 ? { sender_name: input.sender_name } : {}
|
|
14218
|
+
}));
|
|
14219
|
+
}
|
|
14220
|
+
/** Fetch historical messages for a channel. Resolves with the server response. */
|
|
14221
|
+
getHistory(input) {
|
|
14222
|
+
const promise = this.enqueue("chat:history");
|
|
14223
|
+
this.provider.sendStateless(JSON.stringify({
|
|
14224
|
+
type: "chat:history",
|
|
14225
|
+
channel: input.channel,
|
|
14226
|
+
...input.before !== void 0 ? { before: input.before } : {},
|
|
14227
|
+
...input.limit !== void 0 ? { limit: input.limit } : {}
|
|
14228
|
+
}));
|
|
14229
|
+
return promise;
|
|
14230
|
+
}
|
|
14231
|
+
/** Broadcast a typing indicator on a channel. */
|
|
14232
|
+
sendTyping(channel) {
|
|
14233
|
+
this.provider.sendStateless(JSON.stringify({
|
|
14234
|
+
type: "chat:typing",
|
|
14235
|
+
channel
|
|
14236
|
+
}));
|
|
14237
|
+
}
|
|
14238
|
+
/** List the current user's channels (ordered by last activity). */
|
|
14239
|
+
listChannels() {
|
|
14240
|
+
const promise = this.enqueue("chat:channels");
|
|
14241
|
+
this.provider.sendStateless(JSON.stringify({ type: "chat:channels" }));
|
|
14242
|
+
return promise;
|
|
14243
|
+
}
|
|
14244
|
+
/** Mark a channel read up to `timestamp` (unix ms). */
|
|
14245
|
+
markRead(channel, timestamp) {
|
|
14246
|
+
this.provider.sendStateless(JSON.stringify({
|
|
14247
|
+
type: "chat:mark_read",
|
|
14248
|
+
channel,
|
|
14249
|
+
timestamp
|
|
14250
|
+
}));
|
|
14251
|
+
}
|
|
14252
|
+
/** Fetch per-user read cursors for a channel. */
|
|
14253
|
+
getReadCursors(channel) {
|
|
14254
|
+
const promise = this.enqueue("chat:read_cursors");
|
|
14255
|
+
this.provider.sendStateless(JSON.stringify({
|
|
14256
|
+
type: "chat:read_cursors",
|
|
14257
|
+
channel
|
|
14258
|
+
}));
|
|
14259
|
+
return promise;
|
|
14260
|
+
}
|
|
14261
|
+
onMessage(fn) {
|
|
14262
|
+
return this.on("message", fn);
|
|
14263
|
+
}
|
|
14264
|
+
onTyping(fn) {
|
|
14265
|
+
return this.on("typing", fn);
|
|
14266
|
+
}
|
|
14267
|
+
onReadReceipt(fn) {
|
|
14268
|
+
return this.on("readReceipt", fn);
|
|
14269
|
+
}
|
|
14270
|
+
enqueue(type) {
|
|
14271
|
+
return new Promise((resolve, reject) => {
|
|
14272
|
+
const entry = {
|
|
14273
|
+
resolve,
|
|
14274
|
+
reject,
|
|
14275
|
+
timer: setTimeout(() => {
|
|
14276
|
+
this.removePending(type, entry);
|
|
14277
|
+
reject(/* @__PURE__ */ new Error(`ChatClient: timeout waiting for ${type} response`));
|
|
14278
|
+
}, this.responseTimeoutMs)
|
|
14279
|
+
};
|
|
14280
|
+
const queue = this.pending.get(type) ?? [];
|
|
14281
|
+
queue.push(entry);
|
|
14282
|
+
this.pending.set(type, queue);
|
|
14283
|
+
});
|
|
14284
|
+
}
|
|
14285
|
+
removePending(type, entry) {
|
|
14286
|
+
const queue = this.pending.get(type);
|
|
14287
|
+
if (!queue) return;
|
|
14288
|
+
const idx = queue.indexOf(entry);
|
|
14289
|
+
if (idx >= 0) queue.splice(idx, 1);
|
|
14290
|
+
if (queue.length === 0) this.pending.delete(type);
|
|
14291
|
+
}
|
|
14292
|
+
resolveNext(type, value) {
|
|
14293
|
+
const queue = this.pending.get(type);
|
|
14294
|
+
if (!queue || queue.length === 0) return false;
|
|
14295
|
+
const next = queue.shift();
|
|
14296
|
+
if (queue.length === 0) this.pending.delete(type);
|
|
14297
|
+
clearTimeout(next.timer);
|
|
14298
|
+
next.resolve(value);
|
|
14299
|
+
return true;
|
|
14300
|
+
}
|
|
14301
|
+
handleStateless(payload) {
|
|
14302
|
+
let parsed;
|
|
14303
|
+
try {
|
|
14304
|
+
parsed = JSON.parse(payload);
|
|
14305
|
+
} catch {
|
|
14306
|
+
return;
|
|
14307
|
+
}
|
|
14308
|
+
const type = parsed?.type;
|
|
14309
|
+
if (typeof type !== "string" || !type.startsWith("chat:")) return;
|
|
14310
|
+
switch (type) {
|
|
14311
|
+
case "chat:message": {
|
|
14312
|
+
const { type: _t, ...rest } = parsed;
|
|
14313
|
+
this.emit("message", rest);
|
|
14314
|
+
break;
|
|
14315
|
+
}
|
|
14316
|
+
case "chat:history":
|
|
14317
|
+
this.resolveNext("chat:history", {
|
|
14318
|
+
channel: parsed.channel,
|
|
14319
|
+
messages: parsed.messages ?? []
|
|
14320
|
+
});
|
|
14321
|
+
break;
|
|
14322
|
+
case "chat:channels":
|
|
14323
|
+
this.resolveNext("chat:channels", { channels: parsed.channels ?? [] });
|
|
14324
|
+
break;
|
|
14325
|
+
case "chat:typing":
|
|
14326
|
+
this.emit("typing", {
|
|
14327
|
+
channel: parsed.channel,
|
|
14328
|
+
sender_id: parsed.sender_id,
|
|
14329
|
+
sender_name: parsed.sender_name ?? null
|
|
14330
|
+
});
|
|
14331
|
+
break;
|
|
14332
|
+
case "chat:read_receipt":
|
|
14333
|
+
this.emit("readReceipt", {
|
|
14334
|
+
channel: parsed.channel,
|
|
14335
|
+
user_id: parsed.user_id,
|
|
14336
|
+
last_read_at: parsed.last_read_at
|
|
14337
|
+
});
|
|
14338
|
+
break;
|
|
14339
|
+
case "chat:read_cursors":
|
|
14340
|
+
this.resolveNext("chat:read_cursors", {
|
|
14341
|
+
channel: parsed.channel,
|
|
14342
|
+
cursors: parsed.cursors ?? []
|
|
14343
|
+
});
|
|
14344
|
+
break;
|
|
14345
|
+
default: break;
|
|
14346
|
+
}
|
|
14347
|
+
}
|
|
14348
|
+
};
|
|
14349
|
+
|
|
14350
|
+
//#endregion
|
|
14351
|
+
//#region packages/provider/src/NotificationsClient.ts
|
|
14352
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
14353
|
+
/**
|
|
14354
|
+
* Typed client for the Abracadabra notifications feature.
|
|
14355
|
+
*
|
|
14356
|
+
* Emits:
|
|
14357
|
+
* - `new` → NotificationRecord (incoming notify:new broadcast)
|
|
14358
|
+
* - `readUpdate` → NotificationReadUpdate (broadcast after mark_read / mark_all_read)
|
|
14359
|
+
*/
|
|
14360
|
+
var NotificationsClient = class extends EventEmitter {
|
|
14361
|
+
constructor(provider, options) {
|
|
14362
|
+
super();
|
|
14363
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
14364
|
+
this.provider = provider;
|
|
14365
|
+
this.responseTimeoutMs = options?.responseTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
14366
|
+
this.boundOnStateless = (data) => this.handleStateless(data.payload);
|
|
14367
|
+
this.provider.on("stateless", this.boundOnStateless);
|
|
14368
|
+
}
|
|
14369
|
+
destroy() {
|
|
14370
|
+
this.provider.off("stateless", this.boundOnStateless);
|
|
14371
|
+
for (const queue of this.pending.values()) for (const p of queue) {
|
|
14372
|
+
clearTimeout(p.timer);
|
|
14373
|
+
p.reject(/* @__PURE__ */ new Error("NotificationsClient destroyed"));
|
|
14374
|
+
}
|
|
14375
|
+
this.pending.clear();
|
|
14376
|
+
this.removeAllListeners();
|
|
14377
|
+
}
|
|
14378
|
+
/**
|
|
14379
|
+
* Create a notification targeting a specific recipient. Requires elevated role
|
|
14380
|
+
* (service or admin); a `server:error` event with code `forbidden` is emitted
|
|
14381
|
+
* by the underlying provider if the caller lacks permission.
|
|
14382
|
+
*/
|
|
14383
|
+
create(input) {
|
|
14384
|
+
this.provider.sendStateless(JSON.stringify({
|
|
14385
|
+
type: "notify:create",
|
|
14386
|
+
recipient_id: input.recipient_id,
|
|
14387
|
+
...input.notification_type !== void 0 ? { notification_type: input.notification_type } : {},
|
|
14388
|
+
title: input.title,
|
|
14389
|
+
...input.body !== void 0 ? { body: input.body } : {},
|
|
14390
|
+
...input.icon !== void 0 ? { icon: input.icon } : {},
|
|
14391
|
+
...input.link !== void 0 ? { link: input.link } : {},
|
|
14392
|
+
...input.source_id !== void 0 ? { source_id: input.source_id } : {}
|
|
14393
|
+
}));
|
|
14394
|
+
}
|
|
14395
|
+
/** Fetch notification history for the current user. */
|
|
14396
|
+
fetch(input = {}) {
|
|
14397
|
+
const promise = this.enqueue("notify:history");
|
|
14398
|
+
this.provider.sendStateless(JSON.stringify({
|
|
14399
|
+
type: "notify:fetch",
|
|
14400
|
+
...input.before !== void 0 ? { before: input.before } : {},
|
|
14401
|
+
...input.limit !== void 0 ? { limit: input.limit } : {},
|
|
14402
|
+
...input.unread_only !== void 0 ? { unread_only: input.unread_only } : {}
|
|
14403
|
+
}));
|
|
14404
|
+
return promise;
|
|
14405
|
+
}
|
|
14406
|
+
/** Mark a single notification, or a batch, as read. */
|
|
14407
|
+
markRead(target) {
|
|
14408
|
+
const body = { type: "notify:mark_read" };
|
|
14409
|
+
if ("id" in target) body.id = target.id;
|
|
14410
|
+
else body.ids = target.ids;
|
|
14411
|
+
this.provider.sendStateless(JSON.stringify(body));
|
|
14412
|
+
}
|
|
14413
|
+
/** Mark every notification for the current user as read. */
|
|
14414
|
+
markAllRead() {
|
|
14415
|
+
this.provider.sendStateless(JSON.stringify({ type: "notify:mark_all_read" }));
|
|
14416
|
+
}
|
|
14417
|
+
/** Mark all notifications generated by the given source (e.g. a chat channel) as read. */
|
|
14418
|
+
markReadBySource(sourceId) {
|
|
14419
|
+
this.provider.sendStateless(JSON.stringify({
|
|
14420
|
+
type: "notify:mark_read_by_source",
|
|
14421
|
+
source_id: sourceId
|
|
14422
|
+
}));
|
|
14423
|
+
}
|
|
14424
|
+
onNew(fn) {
|
|
14425
|
+
return this.on("new", fn);
|
|
14426
|
+
}
|
|
14427
|
+
onReadUpdate(fn) {
|
|
14428
|
+
return this.on("readUpdate", fn);
|
|
14429
|
+
}
|
|
14430
|
+
enqueue(type) {
|
|
14431
|
+
return new Promise((resolve, reject) => {
|
|
14432
|
+
const entry = {
|
|
14433
|
+
resolve,
|
|
14434
|
+
reject,
|
|
14435
|
+
timer: setTimeout(() => {
|
|
14436
|
+
this.removePending(type, entry);
|
|
14437
|
+
reject(/* @__PURE__ */ new Error(`NotificationsClient: timeout waiting for ${type} response`));
|
|
14438
|
+
}, this.responseTimeoutMs)
|
|
14439
|
+
};
|
|
14440
|
+
const queue = this.pending.get(type) ?? [];
|
|
14441
|
+
queue.push(entry);
|
|
14442
|
+
this.pending.set(type, queue);
|
|
14443
|
+
});
|
|
14444
|
+
}
|
|
14445
|
+
removePending(type, entry) {
|
|
14446
|
+
const queue = this.pending.get(type);
|
|
14447
|
+
if (!queue) return;
|
|
14448
|
+
const idx = queue.indexOf(entry);
|
|
14449
|
+
if (idx >= 0) queue.splice(idx, 1);
|
|
14450
|
+
if (queue.length === 0) this.pending.delete(type);
|
|
14451
|
+
}
|
|
14452
|
+
resolveNext(type, value) {
|
|
14453
|
+
const queue = this.pending.get(type);
|
|
14454
|
+
if (!queue || queue.length === 0) return false;
|
|
14455
|
+
const next = queue.shift();
|
|
14456
|
+
if (queue.length === 0) this.pending.delete(type);
|
|
14457
|
+
clearTimeout(next.timer);
|
|
14458
|
+
next.resolve(value);
|
|
14459
|
+
return true;
|
|
14460
|
+
}
|
|
14461
|
+
handleStateless(payload) {
|
|
14462
|
+
let parsed;
|
|
14463
|
+
try {
|
|
14464
|
+
parsed = JSON.parse(payload);
|
|
14465
|
+
} catch {
|
|
14466
|
+
return;
|
|
14467
|
+
}
|
|
14468
|
+
const type = parsed?.type;
|
|
14469
|
+
if (typeof type !== "string" || !type.startsWith("notify:")) return;
|
|
14470
|
+
switch (type) {
|
|
14471
|
+
case "notify:new": {
|
|
14472
|
+
const { type: _t, ...rest } = parsed;
|
|
14473
|
+
this.emit("new", rest);
|
|
14474
|
+
break;
|
|
14475
|
+
}
|
|
14476
|
+
case "notify:history":
|
|
14477
|
+
this.resolveNext("notify:history", { notifications: parsed.notifications ?? [] });
|
|
14478
|
+
break;
|
|
14479
|
+
case "notify:read_update": {
|
|
14480
|
+
const update = { recipient_id: parsed.recipient_id };
|
|
14481
|
+
if (parsed.ids !== void 0) update.ids = parsed.ids;
|
|
14482
|
+
if (parsed.all !== void 0) update.all = parsed.all;
|
|
14483
|
+
this.emit("readUpdate", update);
|
|
14484
|
+
break;
|
|
14485
|
+
}
|
|
14486
|
+
default: break;
|
|
14487
|
+
}
|
|
14488
|
+
}
|
|
14489
|
+
};
|
|
14490
|
+
|
|
14057
14491
|
//#endregion
|
|
14058
14492
|
exports.AbracadabraBaseProvider = AbracadabraBaseProvider;
|
|
14059
14493
|
exports.AbracadabraClient = AbracadabraClient;
|
|
@@ -14066,6 +14500,7 @@ exports.BackgroundSyncManager = BackgroundSyncManager;
|
|
|
14066
14500
|
exports.BackgroundSyncPersistence = BackgroundSyncPersistence;
|
|
14067
14501
|
exports.BroadcastChannelSync = BroadcastChannelSync;
|
|
14068
14502
|
exports.CHANNEL_NAMES = CHANNEL_NAMES;
|
|
14503
|
+
exports.ChatClient = ChatClient;
|
|
14069
14504
|
exports.ConnectionTimeout = ConnectionTimeout;
|
|
14070
14505
|
exports.CryptoIdentityKeystore = CryptoIdentityKeystore;
|
|
14071
14506
|
exports.DEFAULT_FILE_CHUNK_SIZE = DEFAULT_FILE_CHUNK_SIZE;
|
|
@@ -14091,6 +14526,7 @@ exports.KEY_EXCHANGE_CHANNEL = KEY_EXCHANGE_CHANNEL;
|
|
|
14091
14526
|
exports.ManualSignaling = ManualSignaling;
|
|
14092
14527
|
exports.MessageTooBig = MessageTooBig;
|
|
14093
14528
|
exports.MessageType = MessageType;
|
|
14529
|
+
exports.NotificationsClient = NotificationsClient;
|
|
14094
14530
|
exports.OfflineStore = OfflineStore;
|
|
14095
14531
|
exports.PeerConnection = PeerConnection;
|
|
14096
14532
|
exports.ResetConnection = ResetConnection;
|