@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.
@@ -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(patch)) if (v === void 0) raw.delete(k);
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
- ...patch
15823
+ ...effectivePatch
15745
15824
  };
15746
- for (const [k, v] of Object.entries(patch)) if (v === void 0) delete merged[k];
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;