@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.
- package/dist/abracadabra-provider.cjs +184 -0
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +183 -1
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +78 -1
- package/package.json +2 -2
- package/src/AbracadabraBaseProvider.ts +57 -0
- package/src/AbracadabraWS.ts +39 -0
- package/src/ContentManager.ts +8 -0
- package/src/CoverReconcile.ts +168 -0
- package/src/index.ts +5 -0
- package/src/types.ts +13 -0
|
@@ -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
|