@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.
@@ -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)) return Promise.resolve(this.childProviders.get(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
- * Limitation: at least one client must have the child doc open after an edit
10871
- * for the timestamp to propagate (eventually consistent).
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: Date.now()` to the root
10875
- * doc-tree entry for `childDocId` whenever the child doc receives a
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
- * @param treeMap The root doc's "doc-tree" Y.Map.
10879
- * @param childDocId The child document's UUID (key in treeMap).
10880
- * @param childDoc The child Y.Doc to observe.
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
- * @returns Cleanup function call on provider destroy.
10885
- */
10886
- function attachUpdatedAtObserver(treeMap, childDocId, childDoc, offlineStore) {
10887
- function handler(update, origin) {
10888
- if (offlineStore !== null && origin === offlineStore) return;
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: Date.now()
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 () => childDoc.off("update", handler);
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
- await childProvider.ready;
11231
- await this._waitForSynced(childProvider);
11232
- {
11233
- const treeEntry = this.rootProvider.document.getMap("doc-tree").get(docId);
11234
- this.emit("docSynced", {
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
- document: childProvider.document,
11237
- label: treeEntry?.label ?? "",
11238
- meta: treeEntry?.meta
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 = this.rootProvider.document.getMap("doc-tree").get(docId);
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({ type: "pair-approved" });
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
- export { AbracadabraBaseProvider, AbracadabraClient, AbracadabraProvider, AbracadabraWS, AbracadabraWebRTC, AuthMessageType, AwarenessError, BackgroundSyncManager, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, 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, 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 };
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