@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
|
@@ -1470,6 +1470,32 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1470
1470
|
console.error(e);
|
|
1471
1471
|
}
|
|
1472
1472
|
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Force a fresh socket on the SAME WS manager: drop the current connection
|
|
1475
|
+
* (if any) and reconnect immediately.
|
|
1476
|
+
*
|
|
1477
|
+
* Unlike {@link disconnect} (which clears `shouldConnect` and gives up) this
|
|
1478
|
+
* re-arms `shouldConnect`, so it also revives a socket that a permanent
|
|
1479
|
+
* permission-denial turned off (see `AbracadabraBaseProvider.permissionDeniedHandler`).
|
|
1480
|
+
* The WS manager and its `providerMap` are preserved, so every attached
|
|
1481
|
+
* provider — and every Y.Doc + observer riding this socket — survives
|
|
1482
|
+
* untouched; only the transport is recycled.
|
|
1483
|
+
*
|
|
1484
|
+
* The existing socket is torn down silently (handlers removed first), so no
|
|
1485
|
+
* `close` event fires and no second reconnect is scheduled — `connect()`
|
|
1486
|
+
* below is the single path that re-opens. Returns the `connect()` promise.
|
|
1487
|
+
*/
|
|
1488
|
+
reconnect() {
|
|
1489
|
+
this.shouldConnect = true;
|
|
1490
|
+
if (this.cancelWebsocketRetry) {
|
|
1491
|
+
this.cancelWebsocketRetry();
|
|
1492
|
+
this.cancelWebsocketRetry = void 0;
|
|
1493
|
+
}
|
|
1494
|
+
if (this.webSocket) this.cleanupWebSocket();
|
|
1495
|
+
this.status = WebSocketStatus.Disconnected;
|
|
1496
|
+
this.emit("status", { status: WebSocketStatus.Disconnected });
|
|
1497
|
+
return this.connect();
|
|
1498
|
+
}
|
|
1473
1499
|
send(message) {
|
|
1474
1500
|
if (this.webSocket?.readyState === WsReadyStates.Open) this.webSocket.send(message);
|
|
1475
1501
|
else this.messageQueue.push(message);
|
|
@@ -2460,6 +2486,7 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2460
2486
|
onAuthenticationFailed: () => null,
|
|
2461
2487
|
onRateLimited: () => null,
|
|
2462
2488
|
onOpen: () => null,
|
|
2489
|
+
onReconnected: () => null,
|
|
2463
2490
|
onConnect: () => null,
|
|
2464
2491
|
onMessage: () => null,
|
|
2465
2492
|
onOutgoingMessage: () => null,
|
|
@@ -2478,6 +2505,7 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2478
2505
|
this.unsyncedChanges = 0;
|
|
2479
2506
|
this.isAuthenticated = false;
|
|
2480
2507
|
this._hasEverAuthenticated = false;
|
|
2508
|
+
this._hasOpenedBefore = false;
|
|
2481
2509
|
this.authorizedScope = void 0;
|
|
2482
2510
|
this.manageSocket = false;
|
|
2483
2511
|
this._isAttached = false;
|
|
@@ -2498,6 +2526,7 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2498
2526
|
this.configuration.awareness = configuration.awareness !== void 0 ? configuration.awareness : new Awareness(this.document);
|
|
2499
2527
|
if (this.awareness && this.awareness.getLocalState() === null) this.awareness.setLocalState({});
|
|
2500
2528
|
this.on("open", this.configuration.onOpen);
|
|
2529
|
+
this.on("reconnected", this.configuration.onReconnected);
|
|
2501
2530
|
this.on("message", this.configuration.onMessage);
|
|
2502
2531
|
this.on("outgoingMessage", this.configuration.onOutgoingMessage);
|
|
2503
2532
|
this.on("synced", this.configuration.onSynced);
|
|
@@ -2647,9 +2676,41 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2647
2676
|
if (this.manageSocket) return this.configuration.websocketProvider.disconnect();
|
|
2648
2677
|
console.warn("AbracadabraBaseProvider::disconnect() is deprecated and does not do anything. Please connect/disconnect on the websocketProvider, or attach/deattach providers.");
|
|
2649
2678
|
}
|
|
2679
|
+
/**
|
|
2680
|
+
* Force the underlying socket to drop and reconnect on the SAME Y.Doc.
|
|
2681
|
+
*
|
|
2682
|
+
* Unlike destroying and re-creating the provider, the document is never
|
|
2683
|
+
* replaced — so every `observe`/`observeDeep` listener attached to it (and
|
|
2684
|
+
* its subdocs) keeps firing once the socket comes back. This also revives a
|
|
2685
|
+
* socket that a permanent permission-denial gave up on (see
|
|
2686
|
+
* {@link permissionDeniedHandler}). Pair it with a freshly-minted token (the
|
|
2687
|
+
* `token` callback is re-invoked on every reconnect) when healing a drop that
|
|
2688
|
+
* was caused by an expired JWT.
|
|
2689
|
+
*
|
|
2690
|
+
* Every provider multiplexed onto this socket is marked unsynced so a
|
|
2691
|
+
* subsequent `waitForSync` actually waits for the post-reconnect handshake
|
|
2692
|
+
* instead of short-circuiting on the stale `isSynced` flag.
|
|
2693
|
+
*
|
|
2694
|
+
* No-op (with a warning) for a provider on a shared, externally-managed
|
|
2695
|
+
* socket — reconnect the owning `websocketProvider` directly in that case.
|
|
2696
|
+
*/
|
|
2697
|
+
reconnect() {
|
|
2698
|
+
if (!this.manageSocket) {
|
|
2699
|
+
console.warn("AbracadabraBaseProvider::reconnect() — this provider shares an externally-managed socket; call reconnect() on the websocketProvider instead.");
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
for (const provider of this.configuration.websocketProvider.configuration.providerMap.values()) {
|
|
2703
|
+
provider.synced = false;
|
|
2704
|
+
provider.isAuthenticated = false;
|
|
2705
|
+
}
|
|
2706
|
+
return this.configuration.websocketProvider.reconnect();
|
|
2707
|
+
}
|
|
2650
2708
|
async onOpen(event) {
|
|
2709
|
+
const isReopen = this._hasOpenedBefore;
|
|
2710
|
+
this._hasOpenedBefore = true;
|
|
2651
2711
|
this.isAuthenticated = false;
|
|
2652
2712
|
this.emit("open", { event });
|
|
2713
|
+
if (isReopen) this.emit("reconnected", { event });
|
|
2653
2714
|
await this.sendToken();
|
|
2654
2715
|
this.startSync();
|
|
2655
2716
|
}
|
|
@@ -19177,6 +19238,125 @@ var TreeManager = class {
|
|
|
19177
19238
|
}
|
|
19178
19239
|
};
|
|
19179
19240
|
|
|
19241
|
+
//#endregion
|
|
19242
|
+
//#region packages/provider/src/CoverReconcile.ts
|
|
19243
|
+
/**
|
|
19244
|
+
* CoverReconcile — keep a document's cover-image metadata in sync with the
|
|
19245
|
+
* image/video file blocks that actually live in its content.
|
|
19246
|
+
*
|
|
19247
|
+
* The cover (`meta.coverUploadId` / `coverDocId` / `coverMimeType` on the
|
|
19248
|
+
* doc's own tree entry) is *persisted* metadata, not derived live — the
|
|
19249
|
+
* kanban/gallery/table views read it without loading each card's content.
|
|
19250
|
+
* Historically the only thing that reconciled it was cou-sh's TipTap editor
|
|
19251
|
+
* (`reconcileCover` in DocRenderer.vue), so any write that did NOT go through
|
|
19252
|
+
* that editor — MCP `write_document`, `@abraca/convert`, the CLI, another SDK
|
|
19253
|
+
* — could leave an orphaned cover pointing at an image the body no longer
|
|
19254
|
+
* contains. This is the shared, write-path-level reconciliation so every
|
|
19255
|
+
* content write through the SDK keeps the cover honest.
|
|
19256
|
+
*
|
|
19257
|
+
* Semantics mirror cou-sh's editor reconciler exactly:
|
|
19258
|
+
* - cover set + still references a present image/video → leave it (a
|
|
19259
|
+
* user-chosen cover is preserved as long as its image is in the doc);
|
|
19260
|
+
* - cover set + its image is gone, others remain → swap to the first;
|
|
19261
|
+
* - cover set + no image/video remains → clear the three cover keys;
|
|
19262
|
+
* - no cover + at least one image/video → adopt the first as the cover.
|
|
19263
|
+
*/
|
|
19264
|
+
const EXT_MIME = {
|
|
19265
|
+
png: "image/png",
|
|
19266
|
+
jpg: "image/jpeg",
|
|
19267
|
+
jpeg: "image/jpeg",
|
|
19268
|
+
gif: "image/gif",
|
|
19269
|
+
webp: "image/webp",
|
|
19270
|
+
avif: "image/avif",
|
|
19271
|
+
svg: "image/svg+xml",
|
|
19272
|
+
bmp: "image/bmp",
|
|
19273
|
+
heic: "image/heic",
|
|
19274
|
+
heif: "image/heif",
|
|
19275
|
+
mp4: "video/mp4",
|
|
19276
|
+
webm: "video/webm",
|
|
19277
|
+
mov: "video/quicktime",
|
|
19278
|
+
m4v: "video/x-m4v",
|
|
19279
|
+
ogv: "video/ogg",
|
|
19280
|
+
mkv: "video/x-matroska"
|
|
19281
|
+
};
|
|
19282
|
+
function attr(node, name) {
|
|
19283
|
+
const v = node.getAttribute(name);
|
|
19284
|
+
return typeof v === "string" && v ? v : void 0;
|
|
19285
|
+
}
|
|
19286
|
+
/**
|
|
19287
|
+
* Resolve a file node's mime type. A `fileBlock` may carry the mime under
|
|
19288
|
+
* `mimeType` (cou-sh's TipTap/y-prosemirror node attr) or `mime` (the
|
|
19289
|
+
* `@abraca/convert` markdown path), and may carry neither — in which case we
|
|
19290
|
+
* infer it from the filename/src extension (the convert sidecar `src` is
|
|
19291
|
+
* `.abracadabra/files/<id>-<filename>`). Returns "" when it can't be
|
|
19292
|
+
* classified as image/video.
|
|
19293
|
+
*/
|
|
19294
|
+
function resolveMime(node) {
|
|
19295
|
+
const explicit = attr(node, "mimeType") ?? attr(node, "mime");
|
|
19296
|
+
if (explicit) return explicit;
|
|
19297
|
+
return EXT_MIME[(attr(node, "filename") ?? attr(node, "src") ?? "").split(".").pop()?.toLowerCase() ?? ""] ?? "";
|
|
19298
|
+
}
|
|
19299
|
+
/**
|
|
19300
|
+
* Recursively collect image/video `fileBlock` nodes from a Y.XmlFragment (or
|
|
19301
|
+
* element subtree), in document order. Mirrors the TipTap
|
|
19302
|
+
* `doc.descendants()` walk used by cou-sh — fileBlocks may be nested inside
|
|
19303
|
+
* other block nodes, so this recurses rather than scanning only top level.
|
|
19304
|
+
*/
|
|
19305
|
+
function collectMediaFileBlocks(root) {
|
|
19306
|
+
const out = [];
|
|
19307
|
+
const visit = (node) => {
|
|
19308
|
+
if (node instanceof yjs.XmlElement && node.nodeName === "fileBlock") {
|
|
19309
|
+
const uploadId = attr(node, "uploadId") ?? attr(node, "upload-id");
|
|
19310
|
+
if (uploadId) {
|
|
19311
|
+
const mimeType = resolveMime(node);
|
|
19312
|
+
if (mimeType.startsWith("image/") || mimeType.startsWith("video/")) out.push({
|
|
19313
|
+
uploadId,
|
|
19314
|
+
docId: attr(node, "docId") ?? "",
|
|
19315
|
+
mimeType
|
|
19316
|
+
});
|
|
19317
|
+
}
|
|
19318
|
+
}
|
|
19319
|
+
if (node instanceof yjs.XmlElement || node instanceof yjs.XmlFragment) for (let i = 0; i < node.length; i++) visit(node.get(i));
|
|
19320
|
+
};
|
|
19321
|
+
visit(root);
|
|
19322
|
+
return out;
|
|
19323
|
+
}
|
|
19324
|
+
/**
|
|
19325
|
+
* Reconcile the cover metadata of `docId`'s tree entry against the media file
|
|
19326
|
+
* blocks currently present in `fragment`. No-op when the entry is missing or
|
|
19327
|
+
* the cover is already valid, so it is safe (and cheap) to call after every
|
|
19328
|
+
* content write. The `treeMap` is the root doc-tree Y.Map; the cover lives on
|
|
19329
|
+
* the doc's OWN self-entry (`treeMap.get(docId).meta`).
|
|
19330
|
+
*/
|
|
19331
|
+
function reconcileDocCover(treeMap, docId, fragment) {
|
|
19332
|
+
const raw = treeMap.get(docId);
|
|
19333
|
+
if (!raw) return;
|
|
19334
|
+
const meta = toPlain(raw).meta ?? {};
|
|
19335
|
+
const currentCoverId = meta.coverUploadId;
|
|
19336
|
+
const media = collectMediaFileBlocks(fragment);
|
|
19337
|
+
if (currentCoverId && media.some((b) => b.uploadId === currentCoverId)) return;
|
|
19338
|
+
if (media.length > 0) {
|
|
19339
|
+
const best = media[0];
|
|
19340
|
+
if (currentCoverId === best.uploadId && meta.coverDocId === (best.docId || docId) && meta.coverMimeType === best.mimeType) return;
|
|
19341
|
+
patchEntry(treeMap, docId, {
|
|
19342
|
+
meta: {
|
|
19343
|
+
...meta,
|
|
19344
|
+
coverUploadId: best.uploadId,
|
|
19345
|
+
coverDocId: best.docId || docId,
|
|
19346
|
+
coverMimeType: best.mimeType
|
|
19347
|
+
},
|
|
19348
|
+
updatedAt: Date.now()
|
|
19349
|
+
});
|
|
19350
|
+
return;
|
|
19351
|
+
}
|
|
19352
|
+
if (!currentCoverId && !meta.coverDocId && !meta.coverMimeType) return;
|
|
19353
|
+
const { coverUploadId: _cu, coverDocId: _cd, coverMimeType: _cm, ...rest } = meta;
|
|
19354
|
+
patchEntry(treeMap, docId, {
|
|
19355
|
+
meta: rest,
|
|
19356
|
+
updatedAt: Date.now()
|
|
19357
|
+
});
|
|
19358
|
+
}
|
|
19359
|
+
|
|
19180
19360
|
//#endregion
|
|
19181
19361
|
//#region packages/provider/src/DocConverters.ts
|
|
19182
19362
|
/**
|
|
@@ -20583,6 +20763,8 @@ var ContentManager = class {
|
|
|
20583
20763
|
while (fragment.length > 0) fragment.delete(0);
|
|
20584
20764
|
});
|
|
20585
20765
|
populateYDocFromMarkdown(fragment, body || markdown, title || "Untitled");
|
|
20766
|
+
const reconcileTree = this.dm.getTreeMap();
|
|
20767
|
+
if (reconcileTree) reconcileDocCover(reconcileTree, docId, fragment);
|
|
20586
20768
|
}
|
|
20587
20769
|
async _appendElements(docId, els) {
|
|
20588
20770
|
const doc = (await this.dm.getChildProvider(docId)).document;
|
|
@@ -21054,6 +21236,7 @@ exports.buildHorizontalRuleElement = buildHorizontalRuleElement;
|
|
|
21054
21236
|
exports.buildOrderedListElement = buildOrderedListElement;
|
|
21055
21237
|
exports.buildParagraphElement = buildParagraphElement;
|
|
21056
21238
|
exports.buildTaskListElement = buildTaskListElement;
|
|
21239
|
+
exports.collectMediaFileBlocks = collectMediaFileBlocks;
|
|
21057
21240
|
exports.decryptChatContent = decryptChatContent;
|
|
21058
21241
|
exports.decryptField = decryptField;
|
|
21059
21242
|
exports.deriveIdentityDocId = deriveIdentityDocId;
|
|
@@ -21076,6 +21259,7 @@ exports.patchEntry = patchEntry;
|
|
|
21076
21259
|
exports.populateYDocFromMarkdown = populateYDocFromMarkdown;
|
|
21077
21260
|
exports.readAuthMessage = readAuthMessage;
|
|
21078
21261
|
exports.readBlocksFromFragment = readBlocksFromFragment;
|
|
21262
|
+
exports.reconcileDocCover = reconcileDocCover;
|
|
21079
21263
|
exports.recordFromYAny = recordFromYAny;
|
|
21080
21264
|
exports.resolvePageType = resolvePageType;
|
|
21081
21265
|
exports.toPlain = toPlain;
|