@abraca/dabra 2.7.0 → 2.9.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 +85 -5
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +85 -6
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +37 -2
- package/package.json +2 -2
- package/src/AbracadabraWS.ts +72 -2
- package/src/DocUtils.ts +62 -3
- package/src/index.ts +1 -0
|
@@ -1233,6 +1233,8 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1233
1233
|
this.identifier = 0;
|
|
1234
1234
|
this.intervals = { connectionChecker: null };
|
|
1235
1235
|
this.connectionAttempt = null;
|
|
1236
|
+
this.onlineListener = null;
|
|
1237
|
+
this.offlineListener = null;
|
|
1236
1238
|
this.receivedOnOpenPayload = void 0;
|
|
1237
1239
|
this.closeTries = 0;
|
|
1238
1240
|
this.setConfiguration(configuration);
|
|
@@ -1251,8 +1253,39 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1251
1253
|
this.on("close", this.onClose.bind(this));
|
|
1252
1254
|
this.on("message", this.onMessage.bind(this));
|
|
1253
1255
|
this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
|
|
1256
|
+
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
1257
|
+
this.onlineListener = this.handleOnline.bind(this);
|
|
1258
|
+
this.offlineListener = this.handleOffline.bind(this);
|
|
1259
|
+
window.addEventListener("online", this.onlineListener);
|
|
1260
|
+
window.addEventListener("offline", this.offlineListener);
|
|
1261
|
+
}
|
|
1254
1262
|
if (this.shouldConnect) this.connect();
|
|
1255
1263
|
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Whether the device currently believes it has network connectivity.
|
|
1266
|
+
*
|
|
1267
|
+
* Treats "unknown" as online: in Node and other non-browser environments
|
|
1268
|
+
* `navigator` (or `navigator.onLine`) is absent, and we must not gate
|
|
1269
|
+
* reconnection there — only the browser exposes a trustworthy signal.
|
|
1270
|
+
*/
|
|
1271
|
+
get isOnline() {
|
|
1272
|
+
return typeof navigator === "undefined" || navigator.onLine !== false;
|
|
1273
|
+
}
|
|
1274
|
+
handleOnline() {
|
|
1275
|
+
if (this.shouldConnect && this.status !== WebSocketStatus.Connected) this.connect();
|
|
1276
|
+
}
|
|
1277
|
+
handleOffline() {
|
|
1278
|
+
if (this.cancelWebsocketRetry) {
|
|
1279
|
+
this.cancelWebsocketRetry();
|
|
1280
|
+
this.cancelWebsocketRetry = void 0;
|
|
1281
|
+
}
|
|
1282
|
+
try {
|
|
1283
|
+
this.webSocket?.close();
|
|
1284
|
+
this.messageQueue = [];
|
|
1285
|
+
} catch (e) {
|
|
1286
|
+
console.error(e);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1256
1289
|
async onOpen(event) {
|
|
1257
1290
|
this.status = WebSocketStatus.Connected;
|
|
1258
1291
|
this.emit("status", { status: WebSocketStatus.Connected });
|
|
@@ -1282,6 +1315,10 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1282
1315
|
}
|
|
1283
1316
|
async connect() {
|
|
1284
1317
|
if (this.status === WebSocketStatus.Connected) return;
|
|
1318
|
+
if (!this.isOnline) {
|
|
1319
|
+
this.shouldConnect = true;
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1285
1322
|
if (this.cancelWebsocketRetry) {
|
|
1286
1323
|
this.cancelWebsocketRetry();
|
|
1287
1324
|
this.cancelWebsocketRetry = void 0;
|
|
@@ -1446,7 +1483,7 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1446
1483
|
const isRateLimited = event?.code === 4429 || event === 4429;
|
|
1447
1484
|
this.emit("disconnect", { event });
|
|
1448
1485
|
if (isRateLimited) this.emit("rateLimited");
|
|
1449
|
-
if (!this.cancelWebsocketRetry && this.shouldConnect) {
|
|
1486
|
+
if (!this.cancelWebsocketRetry && this.shouldConnect && this.isOnline) {
|
|
1450
1487
|
const delay = isRateLimited ? 6e4 : this.configuration.delay;
|
|
1451
1488
|
setTimeout(() => {
|
|
1452
1489
|
this.connect();
|
|
@@ -1456,6 +1493,12 @@ var AbracadabraWS = class extends EventEmitter {
|
|
|
1456
1493
|
destroy() {
|
|
1457
1494
|
this.shouldConnect = false;
|
|
1458
1495
|
this.emit("destroy");
|
|
1496
|
+
if (typeof window !== "undefined" && typeof window.removeEventListener === "function") {
|
|
1497
|
+
if (this.onlineListener) window.removeEventListener("online", this.onlineListener);
|
|
1498
|
+
if (this.offlineListener) window.removeEventListener("offline", this.offlineListener);
|
|
1499
|
+
}
|
|
1500
|
+
this.onlineListener = null;
|
|
1501
|
+
this.offlineListener = null;
|
|
1459
1502
|
clearInterval(this.intervals.connectionChecker);
|
|
1460
1503
|
this.stopConnectionAttempt();
|
|
1461
1504
|
this.disconnect();
|
|
@@ -15715,6 +15758,19 @@ function makeEntryMap(fields) {
|
|
|
15715
15758
|
return m;
|
|
15716
15759
|
}
|
|
15717
15760
|
/**
|
|
15761
|
+
* A label is a "placeholder" — i.e. carries no real user title — when it is
|
|
15762
|
+
* empty/whitespace, null/undefined, or the literal `"Untitled"` sentinel
|
|
15763
|
+
* (case-insensitive, so `"untitled"` from a `labelToFilename` round-trip is
|
|
15764
|
+
* caught too). The whole tree-label corruption class boils down to a
|
|
15765
|
+
* placeholder being allowed to overwrite a real title; `patchEntry` refuses
|
|
15766
|
+
* exactly that. Mirrors cou-sh's `isEmptyTreeLabel`.
|
|
15767
|
+
*/
|
|
15768
|
+
function isPlaceholderLabel(label) {
|
|
15769
|
+
if (typeof label !== "string") return label == null;
|
|
15770
|
+
const t = label.trim();
|
|
15771
|
+
return t === "" || t.toLowerCase() === "untitled";
|
|
15772
|
+
}
|
|
15773
|
+
/**
|
|
15718
15774
|
* Patch an EXISTING entry's fields per-key on its nested `Y.Map`, so a
|
|
15719
15775
|
* concurrent edit to a *different* field by a peer is preserved instead
|
|
15720
15776
|
* of being clobbered by a whole-entry write — the whole-entry-LWW fix
|
|
@@ -15729,21 +15785,44 @@ function makeEntryMap(fields) {
|
|
|
15729
15785
|
* Self-transacting: it batches its writes in one `Y.Doc` transaction
|
|
15730
15786
|
* (a safe reentrant no-op join when already inside one), so callers
|
|
15731
15787
|
* don't need to pass or own a transaction.
|
|
15788
|
+
*
|
|
15789
|
+
* ── NO-DESTROY LABEL INVARIANT ──────────────────────────────────────────
|
|
15790
|
+
* A `label` patch that is a placeholder (empty/whitespace/"Untitled") is
|
|
15791
|
+
* DROPPED when the entry already holds a real (non-placeholder) label —
|
|
15792
|
+
* regardless of which consumer (cou-sh title-sync, fs-sync rename
|
|
15793
|
+
* detection, MCP, table renderers, a stale snapshot) tried it. This is the
|
|
15794
|
+
* source-of-truth guard against the "card title silently becomes Untitled
|
|
15795
|
+
* / files renamed to untitled.md" corruption: a placeholder must never win
|
|
15796
|
+
* over a real title. Creating a brand-new entry with an empty label (e.g.
|
|
15797
|
+
* a fresh kanban card) is still allowed — the guard only fires when a real
|
|
15798
|
+
* label already exists. Pass `{ allowLabelClear: true }` to override (the
|
|
15799
|
+
* single legitimate "user explicitly cleared it" path).
|
|
15732
15800
|
*/
|
|
15733
|
-
function patchEntry(treeMap, id, patch, removeKeys = []) {
|
|
15801
|
+
function patchEntry(treeMap, id, patch, removeKeys = [], opts = {}) {
|
|
15734
15802
|
const apply = () => {
|
|
15735
15803
|
const raw = treeMap.get(id);
|
|
15804
|
+
let effectivePatch = patch;
|
|
15805
|
+
if (!opts.allowLabelClear && Object.hasOwn(patch, "label") && isPlaceholderLabel(patch.label)) {
|
|
15806
|
+
let existingLabel;
|
|
15807
|
+
if (raw != null && typeof raw.get === "function") existingLabel = raw.get("label");
|
|
15808
|
+
else if (raw != null && typeof raw.toJSON === "function") existingLabel = raw.toJSON()?.label;
|
|
15809
|
+
else if (raw != null && typeof raw === "object") existingLabel = raw.label;
|
|
15810
|
+
if (!isPlaceholderLabel(existingLabel)) {
|
|
15811
|
+
const { label: _dropped, ...rest } = patch;
|
|
15812
|
+
effectivePatch = rest;
|
|
15813
|
+
}
|
|
15814
|
+
}
|
|
15736
15815
|
if (raw instanceof yjs.Map) {
|
|
15737
|
-
for (const [k, v] of Object.entries(
|
|
15816
|
+
for (const [k, v] of Object.entries(effectivePatch)) if (v === void 0) raw.delete(k);
|
|
15738
15817
|
else raw.set(k, v);
|
|
15739
15818
|
for (const k of removeKeys) raw.delete(k);
|
|
15740
15819
|
return;
|
|
15741
15820
|
}
|
|
15742
15821
|
const merged = {
|
|
15743
15822
|
...raw == null ? {} : toPlain(raw),
|
|
15744
|
-
...
|
|
15823
|
+
...effectivePatch
|
|
15745
15824
|
};
|
|
15746
|
-
for (const [k, v] of Object.entries(
|
|
15825
|
+
for (const [k, v] of Object.entries(effectivePatch)) if (v === void 0) delete merged[k];
|
|
15747
15826
|
for (const k of removeKeys) delete merged[k];
|
|
15748
15827
|
treeMap.set(id, makeEntryMap(merged));
|
|
15749
15828
|
};
|
|
@@ -20900,6 +20979,7 @@ exports.filenameToLabel = filenameToLabel;
|
|
|
20900
20979
|
exports.foldRecords = foldRecords;
|
|
20901
20980
|
exports.generateMnemonic = generateMnemonic;
|
|
20902
20981
|
exports.isEncryptedContent = isEncryptedContent;
|
|
20982
|
+
exports.isPlaceholderLabel = isPlaceholderLabel;
|
|
20903
20983
|
exports.makeEncryptedYMap = makeEncryptedYMap;
|
|
20904
20984
|
exports.makeEncryptedYText = makeEncryptedYText;
|
|
20905
20985
|
exports.makeEntryMap = makeEntryMap;
|