@rljson/fs-agent 0.0.34 → 0.0.36

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.
@@ -157,6 +157,15 @@ export declare const AGENT_STATE_FILE = ".fsagent-state.json";
157
157
  * that a node joining an idle network is not left waiting.
158
158
  */
159
159
  export declare const REFUSAL_ANSWER_COOLDOWN_MS = 5000;
160
+ /**
161
+ * How many tree nodes are fetched at once while walking a tree.
162
+ *
163
+ * The walk is latency-bound, so the whole point is to stop waiting for one node
164
+ * before asking for the next. Bounded because "the whole level at once" on a
165
+ * 184 000-file catalogue would be tens of thousands of simultaneous requests,
166
+ * which trades a latency problem for a queueing one.
167
+ */
168
+ export declare const TREE_FETCH_CONCURRENCY = 64;
160
169
  export declare const MASS_DELETE_MIN_FILES = 100;
161
170
  /**
162
171
  * Above this share of the folder, a prune is treated as suspicious rather than
@@ -622,6 +631,18 @@ export declare class FsAgent {
622
631
  * @param connector - Connector to broadcast on.
623
632
  */
624
633
  private _readvertiseAfterRefusal;
634
+ /**
635
+ * Whether a tree describes a folder holding nothing whatsoever.
636
+ *
637
+ * Deliberately conservative: an empty DIRECTORY counts as content. A user who
638
+ * creates a folder and puts a directory in it has made a statement about what
639
+ * should be there, and the silence this gates is only correct for an agent
640
+ * that has established nothing at all. Being wrong in that direction would
641
+ * mean a folder that never advertises until something else happens to change.
642
+ * @param tree - The tree to inspect.
643
+ * @returns `true` when the tree carries no entries at all.
644
+ */
645
+ private _treeIsEmpty;
625
646
  private _adoptAppliedRef;
626
647
  /**
627
648
  * Derives a deterministic content key from an FsTree.
package/dist/fs-agent.js CHANGED
@@ -1028,6 +1028,7 @@ const SYNC_ERROR_FILE = ".sync-errors.log";
1028
1028
  const ATOMIC_TMP_PREFIX = ".fsagent-tmp-";
1029
1029
  const AGENT_STATE_FILE = ".fsagent-state.json";
1030
1030
  const REFUSAL_ANSWER_COOLDOWN_MS = 5e3;
1031
+ const TREE_FETCH_CONCURRENCY = 64;
1031
1032
  const MASS_DELETE_MIN_FILES = 100;
1032
1033
  const MASS_DELETE_MAX_RATIO = 0.3;
1033
1034
  class RestoreIncompleteError extends Error {
@@ -1690,52 +1691,56 @@ ${err.stack}` : String(err);
1690
1691
  */
1691
1692
  async _fetchTreeRecursively(db, route, treeKey, rootHash) {
1692
1693
  const fetchedNodes = /* @__PURE__ */ new Map();
1693
- const nodesToFetch = /* @__PURE__ */ new Set([rootHash]);
1694
- const processed = /* @__PURE__ */ new Set();
1695
- while (nodesToFetch.size > 0) {
1696
- const currentHash = Array.from(nodesToFetch)[0];
1697
- nodesToFetch.delete(currentHash);
1698
- if (processed.has(currentHash)) {
1699
- continue;
1700
- }
1701
- processed.add(currentHash);
1702
- let result;
1703
- try {
1704
- result = await FsAgent._withTimeout(
1705
- db.get(route, { _hash: currentHash }),
1706
- this._timeouts.dbQuery,
1707
- `db.get(${treeKey}, _hash=${currentHash.slice(0, 8)}…)`
1708
- );
1709
- } catch (error) {
1710
- if (error instanceof Error && error.message.startsWith("Timeout")) {
1711
- throw error;
1712
- }
1713
- const errMsg = error instanceof Error ? error.message : String(error);
1714
- console.warn(
1715
- `[FsAgent] _fetchTreeRecursively: db.get failed for hash=${currentHash.slice(0, 8)}…: ${errMsg}`
1716
- );
1717
- this._writeSyncError(
1718
- `fetchTree/db.get(${currentHash.slice(0, 8)}…)`,
1719
- error
1694
+ const seen = /* @__PURE__ */ new Set([rootHash]);
1695
+ let frontier = [rootHash];
1696
+ while (frontier.length > 0) {
1697
+ const next = [];
1698
+ for (let i = 0; i < frontier.length; i += TREE_FETCH_CONCURRENCY) {
1699
+ const batch = frontier.slice(i, i + TREE_FETCH_CONCURRENCY);
1700
+ const results = await Promise.all(
1701
+ batch.map(async (hash) => {
1702
+ try {
1703
+ return await FsAgent._withTimeout(
1704
+ db.get(route, { _hash: hash }),
1705
+ this._timeouts.dbQuery,
1706
+ `db.get(${treeKey}, _hash=${hash.slice(0, 8)})`
1707
+ );
1708
+ } catch (error) {
1709
+ if (error instanceof Error && error.message.startsWith("Timeout")) {
1710
+ throw error;
1711
+ }
1712
+ const errMsg = error instanceof Error ? error.message : String(error);
1713
+ console.warn(
1714
+ `[FsAgent] _fetchTreeRecursively: db.get failed for hash=${hash.slice(0, 8)}…: ${errMsg}`
1715
+ );
1716
+ this._writeSyncError(
1717
+ `fetchTree/db.get(${hash.slice(0, 8)}…)`,
1718
+ error
1719
+ );
1720
+ return null;
1721
+ }
1722
+ })
1720
1723
  );
1721
- continue;
1722
- }
1723
- const treeData = result?.rljson?.[treeKey];
1724
- if (!treeData || !treeData._data) {
1725
- continue;
1726
- }
1727
- const dataArray = Array.isArray(treeData._data) ? treeData._data : Object.values(treeData._data);
1728
- for (const node of dataArray) {
1729
- if (!node._hash) continue;
1730
- fetchedNodes.set(node._hash, node);
1731
- if (node.children && Array.isArray(node.children)) {
1732
- for (const childHash of node.children) {
1733
- if (typeof childHash === "string" && !processed.has(childHash)) {
1734
- nodesToFetch.add(childHash);
1724
+ for (const result of results) {
1725
+ const treeData = result?.rljson?.[treeKey];
1726
+ if (!treeData || !treeData._data) continue;
1727
+ const dataArray = Array.isArray(treeData._data) ? treeData._data : Object.values(treeData._data);
1728
+ for (const node of dataArray) {
1729
+ if (!node._hash) continue;
1730
+ fetchedNodes.set(node._hash, node);
1731
+ if (node.children && Array.isArray(node.children)) {
1732
+ for (const childHash of node.children) {
1733
+ if (typeof childHash !== "string" || seen.has(childHash)) {
1734
+ continue;
1735
+ }
1736
+ seen.add(childHash);
1737
+ next.push(childHash);
1738
+ }
1735
1739
  }
1736
1740
  }
1737
1741
  }
1738
1742
  }
1743
+ frontier = next;
1739
1744
  }
1740
1745
  return Array.from(fetchedNodes.values());
1741
1746
  }
@@ -1883,7 +1888,16 @@ ${err.stack}` : String(err);
1883
1888
  this._timeouts.fetchTree,
1884
1889
  `syncToDb → initial storeFsTree(${treeKey})`
1885
1890
  );
1886
- if (initialRef) {
1891
+ const isSilentJoiner = initialParentRef === void 0 && this._treeIsEmpty(initialTree);
1892
+ if (isSilentJoiner) {
1893
+ console.warn(
1894
+ `[FsAgent] ${this._rootPath} is empty and has no remembered state — joining quietly rather than announcing emptiness.`
1895
+ );
1896
+ this._currentRef = initialRef;
1897
+ this._persistCurrentRef(initialRef);
1898
+ this._lastSentContentKey = this._contentKeyFromTree(initialTree);
1899
+ }
1900
+ if (initialRef && !isSilentJoiner) {
1887
1901
  this._lastSentRef = initialRef;
1888
1902
  this._currentRef = initialRef;
1889
1903
  this._persistCurrentRef(initialRef);
@@ -2136,6 +2150,20 @@ ${err.stack}` : String(err);
2136
2150
  console.warn(`[FsAgent] re-announcement failed: ${String(err)}`);
2137
2151
  }
2138
2152
  }
2153
+ /**
2154
+ * Whether a tree describes a folder holding nothing whatsoever.
2155
+ *
2156
+ * Deliberately conservative: an empty DIRECTORY counts as content. A user who
2157
+ * creates a folder and puts a directory in it has made a statement about what
2158
+ * should be there, and the silence this gates is only correct for an agent
2159
+ * that has established nothing at all. Being wrong in that direction would
2160
+ * mean a folder that never advertises until something else happens to change.
2161
+ * @param tree - The tree to inspect.
2162
+ * @returns `true` when the tree carries no entries at all.
2163
+ */
2164
+ _treeIsEmpty(tree) {
2165
+ return this._getFileContentMap(tree).size === 0;
2166
+ }
2139
2167
  _adoptAppliedRef(connector, treeRef) {
2140
2168
  if (this._lastAppliedRef && this._lastAppliedRef !== treeRef) {
2141
2169
  connector.invalidateSent?.(this._lastAppliedRef);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rljson/fs-agent",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
4
4
  "description": "Rljson fs-agent description",
5
5
  "homepage": "https://github.com/rljson/fs-agent",
6
6
  "bugs": "https://github.com/rljson/fs-agent/issues",