@abraca/dabra 2.16.0 → 2.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1440,6 +1440,32 @@ var AbracadabraWS = class extends EventEmitter {
1440
1440
  console.error(e);
1441
1441
  }
1442
1442
  }
1443
+ /**
1444
+ * Force a fresh socket on the SAME WS manager: drop the current connection
1445
+ * (if any) and reconnect immediately.
1446
+ *
1447
+ * Unlike {@link disconnect} (which clears `shouldConnect` and gives up) this
1448
+ * re-arms `shouldConnect`, so it also revives a socket that a permanent
1449
+ * permission-denial turned off (see `AbracadabraBaseProvider.permissionDeniedHandler`).
1450
+ * The WS manager and its `providerMap` are preserved, so every attached
1451
+ * provider — and every Y.Doc + observer riding this socket — survives
1452
+ * untouched; only the transport is recycled.
1453
+ *
1454
+ * The existing socket is torn down silently (handlers removed first), so no
1455
+ * `close` event fires and no second reconnect is scheduled — `connect()`
1456
+ * below is the single path that re-opens. Returns the `connect()` promise.
1457
+ */
1458
+ reconnect() {
1459
+ this.shouldConnect = true;
1460
+ if (this.cancelWebsocketRetry) {
1461
+ this.cancelWebsocketRetry();
1462
+ this.cancelWebsocketRetry = void 0;
1463
+ }
1464
+ if (this.webSocket) this.cleanupWebSocket();
1465
+ this.status = WebSocketStatus.Disconnected;
1466
+ this.emit("status", { status: WebSocketStatus.Disconnected });
1467
+ return this.connect();
1468
+ }
1443
1469
  send(message) {
1444
1470
  if (this.webSocket?.readyState === WsReadyStates.Open) this.webSocket.send(message);
1445
1471
  else this.messageQueue.push(message);
@@ -2430,6 +2456,7 @@ var AbracadabraBaseProvider = class extends EventEmitter {
2430
2456
  onAuthenticationFailed: () => null,
2431
2457
  onRateLimited: () => null,
2432
2458
  onOpen: () => null,
2459
+ onReconnected: () => null,
2433
2460
  onConnect: () => null,
2434
2461
  onMessage: () => null,
2435
2462
  onOutgoingMessage: () => null,
@@ -2448,6 +2475,7 @@ var AbracadabraBaseProvider = class extends EventEmitter {
2448
2475
  this.unsyncedChanges = 0;
2449
2476
  this.isAuthenticated = false;
2450
2477
  this._hasEverAuthenticated = false;
2478
+ this._hasOpenedBefore = false;
2451
2479
  this.authorizedScope = void 0;
2452
2480
  this.manageSocket = false;
2453
2481
  this._isAttached = false;
@@ -2468,6 +2496,7 @@ var AbracadabraBaseProvider = class extends EventEmitter {
2468
2496
  this.configuration.awareness = configuration.awareness !== void 0 ? configuration.awareness : new Awareness(this.document);
2469
2497
  if (this.awareness && this.awareness.getLocalState() === null) this.awareness.setLocalState({});
2470
2498
  this.on("open", this.configuration.onOpen);
2499
+ this.on("reconnected", this.configuration.onReconnected);
2471
2500
  this.on("message", this.configuration.onMessage);
2472
2501
  this.on("outgoingMessage", this.configuration.onOutgoingMessage);
2473
2502
  this.on("synced", this.configuration.onSynced);
@@ -2617,9 +2646,41 @@ var AbracadabraBaseProvider = class extends EventEmitter {
2617
2646
  if (this.manageSocket) return this.configuration.websocketProvider.disconnect();
2618
2647
  console.warn("AbracadabraBaseProvider::disconnect() is deprecated and does not do anything. Please connect/disconnect on the websocketProvider, or attach/deattach providers.");
2619
2648
  }
2649
+ /**
2650
+ * Force the underlying socket to drop and reconnect on the SAME Y.Doc.
2651
+ *
2652
+ * Unlike destroying and re-creating the provider, the document is never
2653
+ * replaced — so every `observe`/`observeDeep` listener attached to it (and
2654
+ * its subdocs) keeps firing once the socket comes back. This also revives a
2655
+ * socket that a permanent permission-denial gave up on (see
2656
+ * {@link permissionDeniedHandler}). Pair it with a freshly-minted token (the
2657
+ * `token` callback is re-invoked on every reconnect) when healing a drop that
2658
+ * was caused by an expired JWT.
2659
+ *
2660
+ * Every provider multiplexed onto this socket is marked unsynced so a
2661
+ * subsequent `waitForSync` actually waits for the post-reconnect handshake
2662
+ * instead of short-circuiting on the stale `isSynced` flag.
2663
+ *
2664
+ * No-op (with a warning) for a provider on a shared, externally-managed
2665
+ * socket — reconnect the owning `websocketProvider` directly in that case.
2666
+ */
2667
+ reconnect() {
2668
+ if (!this.manageSocket) {
2669
+ console.warn("AbracadabraBaseProvider::reconnect() — this provider shares an externally-managed socket; call reconnect() on the websocketProvider instead.");
2670
+ return;
2671
+ }
2672
+ for (const provider of this.configuration.websocketProvider.configuration.providerMap.values()) {
2673
+ provider.synced = false;
2674
+ provider.isAuthenticated = false;
2675
+ }
2676
+ return this.configuration.websocketProvider.reconnect();
2677
+ }
2620
2678
  async onOpen(event) {
2679
+ const isReopen = this._hasOpenedBefore;
2680
+ this._hasOpenedBefore = true;
2621
2681
  this.isAuthenticated = false;
2622
2682
  this.emit("open", { event });
2683
+ if (isReopen) this.emit("reconnected", { event });
2623
2684
  await this.sendToken();
2624
2685
  this.startSync();
2625
2686
  }
@@ -19117,6 +19178,125 @@ var TreeManager = class {
19117
19178
  }
19118
19179
  };
19119
19180
 
19181
+ //#endregion
19182
+ //#region packages/provider/src/CoverReconcile.ts
19183
+ /**
19184
+ * CoverReconcile — keep a document's cover-image metadata in sync with the
19185
+ * image/video file blocks that actually live in its content.
19186
+ *
19187
+ * The cover (`meta.coverUploadId` / `coverDocId` / `coverMimeType` on the
19188
+ * doc's own tree entry) is *persisted* metadata, not derived live — the
19189
+ * kanban/gallery/table views read it without loading each card's content.
19190
+ * Historically the only thing that reconciled it was cou-sh's TipTap editor
19191
+ * (`reconcileCover` in DocRenderer.vue), so any write that did NOT go through
19192
+ * that editor — MCP `write_document`, `@abraca/convert`, the CLI, another SDK
19193
+ * — could leave an orphaned cover pointing at an image the body no longer
19194
+ * contains. This is the shared, write-path-level reconciliation so every
19195
+ * content write through the SDK keeps the cover honest.
19196
+ *
19197
+ * Semantics mirror cou-sh's editor reconciler exactly:
19198
+ * - cover set + still references a present image/video → leave it (a
19199
+ * user-chosen cover is preserved as long as its image is in the doc);
19200
+ * - cover set + its image is gone, others remain → swap to the first;
19201
+ * - cover set + no image/video remains → clear the three cover keys;
19202
+ * - no cover + at least one image/video → adopt the first as the cover.
19203
+ */
19204
+ const EXT_MIME = {
19205
+ png: "image/png",
19206
+ jpg: "image/jpeg",
19207
+ jpeg: "image/jpeg",
19208
+ gif: "image/gif",
19209
+ webp: "image/webp",
19210
+ avif: "image/avif",
19211
+ svg: "image/svg+xml",
19212
+ bmp: "image/bmp",
19213
+ heic: "image/heic",
19214
+ heif: "image/heif",
19215
+ mp4: "video/mp4",
19216
+ webm: "video/webm",
19217
+ mov: "video/quicktime",
19218
+ m4v: "video/x-m4v",
19219
+ ogv: "video/ogg",
19220
+ mkv: "video/x-matroska"
19221
+ };
19222
+ function attr(node, name) {
19223
+ const v = node.getAttribute(name);
19224
+ return typeof v === "string" && v ? v : void 0;
19225
+ }
19226
+ /**
19227
+ * Resolve a file node's mime type. A `fileBlock` may carry the mime under
19228
+ * `mimeType` (cou-sh's TipTap/y-prosemirror node attr) or `mime` (the
19229
+ * `@abraca/convert` markdown path), and may carry neither — in which case we
19230
+ * infer it from the filename/src extension (the convert sidecar `src` is
19231
+ * `.abracadabra/files/<id>-<filename>`). Returns "" when it can't be
19232
+ * classified as image/video.
19233
+ */
19234
+ function resolveMime(node) {
19235
+ const explicit = attr(node, "mimeType") ?? attr(node, "mime");
19236
+ if (explicit) return explicit;
19237
+ return EXT_MIME[(attr(node, "filename") ?? attr(node, "src") ?? "").split(".").pop()?.toLowerCase() ?? ""] ?? "";
19238
+ }
19239
+ /**
19240
+ * Recursively collect image/video `fileBlock` nodes from a Y.XmlFragment (or
19241
+ * element subtree), in document order. Mirrors the TipTap
19242
+ * `doc.descendants()` walk used by cou-sh — fileBlocks may be nested inside
19243
+ * other block nodes, so this recurses rather than scanning only top level.
19244
+ */
19245
+ function collectMediaFileBlocks(root) {
19246
+ const out = [];
19247
+ const visit = (node) => {
19248
+ if (node instanceof Y.XmlElement && node.nodeName === "fileBlock") {
19249
+ const uploadId = attr(node, "uploadId") ?? attr(node, "upload-id");
19250
+ if (uploadId) {
19251
+ const mimeType = resolveMime(node);
19252
+ if (mimeType.startsWith("image/") || mimeType.startsWith("video/")) out.push({
19253
+ uploadId,
19254
+ docId: attr(node, "docId") ?? "",
19255
+ mimeType
19256
+ });
19257
+ }
19258
+ }
19259
+ if (node instanceof Y.XmlElement || node instanceof Y.XmlFragment) for (let i = 0; i < node.length; i++) visit(node.get(i));
19260
+ };
19261
+ visit(root);
19262
+ return out;
19263
+ }
19264
+ /**
19265
+ * Reconcile the cover metadata of `docId`'s tree entry against the media file
19266
+ * blocks currently present in `fragment`. No-op when the entry is missing or
19267
+ * the cover is already valid, so it is safe (and cheap) to call after every
19268
+ * content write. The `treeMap` is the root doc-tree Y.Map; the cover lives on
19269
+ * the doc's OWN self-entry (`treeMap.get(docId).meta`).
19270
+ */
19271
+ function reconcileDocCover(treeMap, docId, fragment) {
19272
+ const raw = treeMap.get(docId);
19273
+ if (!raw) return;
19274
+ const meta = toPlain(raw).meta ?? {};
19275
+ const currentCoverId = meta.coverUploadId;
19276
+ const media = collectMediaFileBlocks(fragment);
19277
+ if (currentCoverId && media.some((b) => b.uploadId === currentCoverId)) return;
19278
+ if (media.length > 0) {
19279
+ const best = media[0];
19280
+ if (currentCoverId === best.uploadId && meta.coverDocId === (best.docId || docId) && meta.coverMimeType === best.mimeType) return;
19281
+ patchEntry(treeMap, docId, {
19282
+ meta: {
19283
+ ...meta,
19284
+ coverUploadId: best.uploadId,
19285
+ coverDocId: best.docId || docId,
19286
+ coverMimeType: best.mimeType
19287
+ },
19288
+ updatedAt: Date.now()
19289
+ });
19290
+ return;
19291
+ }
19292
+ if (!currentCoverId && !meta.coverDocId && !meta.coverMimeType) return;
19293
+ const { coverUploadId: _cu, coverDocId: _cd, coverMimeType: _cm, ...rest } = meta;
19294
+ patchEntry(treeMap, docId, {
19295
+ meta: rest,
19296
+ updatedAt: Date.now()
19297
+ });
19298
+ }
19299
+
19120
19300
  //#endregion
19121
19301
  //#region packages/provider/src/DocConverters.ts
19122
19302
  /**
@@ -20518,6 +20698,8 @@ var ContentManager = class {
20518
20698
  while (fragment.length > 0) fragment.delete(0);
20519
20699
  });
20520
20700
  populateYDocFromMarkdown(fragment, body || markdown, title || "Untitled");
20701
+ const reconcileTree = this.dm.getTreeMap();
20702
+ if (reconcileTree) reconcileDocCover(reconcileTree, docId, fragment);
20521
20703
  }
20522
20704
  async _appendElements(docId, els) {
20523
20705
  const doc = (await this.dm.getChildProvider(docId)).document;
@@ -20908,5 +21090,5 @@ var DocumentManager = class {
20908
21090
  };
20909
21091
 
20910
21092
  //#endregion
20911
- export { AbracadabraBaseProvider, AbracadabraClient, AbracadabraProvider, AbracadabraWS, AbracadabraWebRTC, AuthMessageType, AwarenessError, BackgroundSyncManager, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, ChannelKeyResolver, ChatClient, ConnectionTimeout, ContentManager, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, DevicePairingChannel, DeviceRegistrationService, DocKeyManager, DocumentCache, DocumentManager, E2EAbracadabraProvider, E2EEChannel, E2EOfflineStore, EncryptedChatClient, EncryptedYMap, EncryptedYText, FileBlobStore, FileTransferChannel, FileTransferHandle, Forbidden, GEO_TYPE_META_SCHEMAS, HocuspocusProvider, HocuspocusProviderWebsocket, IdentityDocProvider, KEY_EXCHANGE_CHANNEL, Kind, LocalStorageDeviceSessionStorage, ManualSignaling, MessageTooBig, MessageType, MetaManager, MetaValidationError, NotificationsClient, OfflineStore, PAGE_TYPES, PeerConnection, QUERY_PREFIX, QueryClient, QueryError, RPC_PREFIX, ResetConnection, RpcClient, RpcError, SERVER_ROOT_ID, SearchIndex, SignalingSocket, SubdocMessage, TYPE_ALIASES, TokenManager, TreeManager, TypedDocTypeMismatchError, Unauthorized, WebSocketStatus, WsReadyStates, YjsDataChannel, attachUpdatedAtObserver, awarenessStatesToArray, wordlist as bip39Wordlist, buildBlockquoteElement, buildBlocksFromMarkdown, buildBulletListElement, buildCodeBlockElement, buildHeadingElement, buildHorizontalRuleElement, buildOrderedListElement, buildParagraphElement, buildTaskListElement, decryptChatContent, decryptField, deriveIdentityDocId, deriveSeedWrappingKey, encryptChatContent, encryptField, filenameToLabel, foldRecords, generateMnemonic, isEncryptedContent, isPlaceholderLabel, makeEncryptedYMap, makeEncryptedYText, makeEntryMap, mnemonicToEd25519Seed, mnemonicToKeyPair, normalizeRootId, parseFrontmatter, patchEntry, populateYDocFromMarkdown, readAuthMessage, readBlocksFromFragment, recordFromYAny, resolvePageType, toPlain, unwrapSeed, validateMnemonic, waitForSync, withTimeout, wrapSeed, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest, yjsToMarkdown };
21093
+ export { AbracadabraBaseProvider, AbracadabraClient, AbracadabraProvider, AbracadabraWS, AbracadabraWebRTC, AuthMessageType, AwarenessError, BackgroundSyncManager, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, ChannelKeyResolver, ChatClient, ConnectionTimeout, ContentManager, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, DevicePairingChannel, DeviceRegistrationService, DocKeyManager, DocumentCache, DocumentManager, E2EAbracadabraProvider, E2EEChannel, E2EOfflineStore, EncryptedChatClient, EncryptedYMap, EncryptedYText, FileBlobStore, FileTransferChannel, FileTransferHandle, Forbidden, GEO_TYPE_META_SCHEMAS, HocuspocusProvider, HocuspocusProviderWebsocket, IdentityDocProvider, KEY_EXCHANGE_CHANNEL, Kind, LocalStorageDeviceSessionStorage, ManualSignaling, MessageTooBig, MessageType, MetaManager, MetaValidationError, NotificationsClient, OfflineStore, PAGE_TYPES, PeerConnection, QUERY_PREFIX, QueryClient, QueryError, RPC_PREFIX, ResetConnection, RpcClient, RpcError, SERVER_ROOT_ID, SearchIndex, SignalingSocket, SubdocMessage, TYPE_ALIASES, TokenManager, TreeManager, TypedDocTypeMismatchError, Unauthorized, WebSocketStatus, WsReadyStates, YjsDataChannel, attachUpdatedAtObserver, awarenessStatesToArray, wordlist as bip39Wordlist, buildBlockquoteElement, buildBlocksFromMarkdown, buildBulletListElement, buildCodeBlockElement, buildHeadingElement, buildHorizontalRuleElement, buildOrderedListElement, buildParagraphElement, buildTaskListElement, collectMediaFileBlocks, decryptChatContent, decryptField, deriveIdentityDocId, deriveSeedWrappingKey, encryptChatContent, encryptField, filenameToLabel, foldRecords, generateMnemonic, isEncryptedContent, isPlaceholderLabel, makeEncryptedYMap, makeEncryptedYText, makeEntryMap, mnemonicToEd25519Seed, mnemonicToKeyPair, normalizeRootId, parseFrontmatter, patchEntry, populateYDocFromMarkdown, readAuthMessage, readBlocksFromFragment, reconcileDocCover, recordFromYAny, resolvePageType, toPlain, unwrapSeed, validateMnemonic, waitForSync, withTimeout, wrapSeed, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest, yjsToMarkdown };
20912
21094
  //# sourceMappingURL=abracadabra-provider.esm.js.map