@uipath/filesystem 1.1.0 → 1.196.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/browser.d.ts CHANGED
@@ -19,6 +19,9 @@ export declare class BrowserFileSystem implements IFileSystem {
19
19
  stat(path: string): Promise<FileSystemStats | null>;
20
20
  exists(path: string): Promise<boolean>;
21
21
  mkdir(path: string): Promise<void>;
22
+ acquireLock(lockPath: string): Promise<() => Promise<void>>;
23
+ private acquireWebLock;
24
+ private acquireInMemoryLock;
22
25
  rm(path: string): Promise<void>;
23
26
  rename(oldPath: string, newPath: string): Promise<void>;
24
27
  realpath(filePath: string): Promise<string>;
@@ -1389,6 +1389,23 @@ var require_src = __commonJS((exports, module) => {
1389
1389
 
1390
1390
  // src/browser.ts
1391
1391
  var import_lightning_fs = __toESM(require_src(), 1);
1392
+ var LOCK_MAX_WAIT_MS = 20000;
1393
+ var IN_MEMORY_LOCK_TAILS_KEY = Symbol.for("@uipath/filesystem/lock-tails");
1394
+ var getInMemoryLockTails = () => {
1395
+ const globalsHost = globalThis;
1396
+ const existing = globalsHost[IN_MEMORY_LOCK_TAILS_KEY];
1397
+ if (existing && typeof existing.get === "function" && typeof existing.set === "function" && typeof existing.delete === "function") {
1398
+ return existing;
1399
+ }
1400
+ const created = new Map;
1401
+ globalsHost[IN_MEMORY_LOCK_TAILS_KEY] = created;
1402
+ return created;
1403
+ };
1404
+ var getWebLocks = () => {
1405
+ if (typeof navigator === "undefined")
1406
+ return;
1407
+ return navigator.locks;
1408
+ };
1392
1409
 
1393
1410
  class BrowserFileSystem {
1394
1411
  initialized = false;
@@ -1571,6 +1588,69 @@ class BrowserFileSystem {
1571
1588
  throw error;
1572
1589
  }
1573
1590
  }
1591
+ async acquireLock(lockPath) {
1592
+ const canonical = this.normalizePath(this.path.isAbsolute(lockPath) ? lockPath : `/${lockPath}`);
1593
+ const name = `@uipath/filesystem:lock:${canonical}`;
1594
+ const webLocks = getWebLocks();
1595
+ if (webLocks) {
1596
+ return await this.acquireWebLock(webLocks, name);
1597
+ }
1598
+ return this.acquireInMemoryLock(name);
1599
+ }
1600
+ async acquireWebLock(webLocks, name) {
1601
+ const abortController = new AbortController;
1602
+ const timeoutHandle = setTimeout(() => {
1603
+ abortController.abort();
1604
+ }, LOCK_MAX_WAIT_MS);
1605
+ return await new Promise((resolveAcquire, rejectAcquire) => {
1606
+ let releaseHeld;
1607
+ const requested = webLocks.request(name, { signal: abortController.signal }, () => new Promise((resolveHeld) => {
1608
+ clearTimeout(timeoutHandle);
1609
+ releaseHeld = resolveHeld;
1610
+ resolveAcquire(async () => {
1611
+ resolveHeld();
1612
+ });
1613
+ }));
1614
+ requested.catch((error) => {
1615
+ clearTimeout(timeoutHandle);
1616
+ if (releaseHeld) {
1617
+ return;
1618
+ }
1619
+ if (error?.name === "AbortError") {
1620
+ rejectAcquire(new Error(`ELOCKED: timed out waiting for lock on ${name}`));
1621
+ return;
1622
+ }
1623
+ rejectAcquire(error);
1624
+ });
1625
+ });
1626
+ }
1627
+ acquireInMemoryLock(name) {
1628
+ const tails = getInMemoryLockTails();
1629
+ const prior = tails.get(name) ?? Promise.resolve();
1630
+ let releaseHeld;
1631
+ const held = new Promise((resolve) => {
1632
+ releaseHeld = resolve;
1633
+ });
1634
+ const chained = prior.then(() => held);
1635
+ tails.set(name, chained);
1636
+ chained.then(() => {
1637
+ if (tails.get(name) === chained) {
1638
+ tails.delete(name);
1639
+ }
1640
+ });
1641
+ return new Promise((resolveAcquire, rejectAcquire) => {
1642
+ const timer = setTimeout(() => {
1643
+ releaseHeld();
1644
+ rejectAcquire(new Error(`ELOCKED: timed out waiting for lock on ${name}`));
1645
+ }, LOCK_MAX_WAIT_MS);
1646
+ prior.then(() => {
1647
+ clearTimeout(timer);
1648
+ resolveAcquire(async () => {
1649
+ releaseHeld();
1650
+ });
1651
+ });
1652
+ });
1653
+ }
1574
1654
  async rm(path) {
1575
1655
  await this.init();
1576
1656
  const fs = this.getFs();
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/node.ts
2
+ import { randomUUID } from "node:crypto";
2
3
  import { existsSync } from "node:fs";
3
4
  import * as fs6 from "node:fs/promises";
4
5
  import * as os2 from "node:os";
@@ -615,6 +616,13 @@ defineLazyProperty(apps, "safari", () => detectPlatformBinary({
615
616
  var open_default = open;
616
617
 
617
618
  // src/node.ts
619
+ var LOCK_HEARTBEAT_MS = 5000;
620
+ var LOCK_STALE_MS = 15000;
621
+ var LOCK_MAX_WAIT_MS = 20000;
622
+ var LOCK_MAX_HOLD_MS = 60000;
623
+ var LOCK_RETRY_MIN_MS = 100;
624
+ var LOCK_RETRY_JITTER_MS = 200;
625
+
618
626
  class NodeFileSystem {
619
627
  path = {
620
628
  join: path2.join,
@@ -691,6 +699,90 @@ class NodeFileSystem {
691
699
  async mkdir(dirPath) {
692
700
  await fs6.mkdir(dirPath, { recursive: true });
693
701
  }
702
+ async acquireLock(lockPath) {
703
+ const canonicalPath = await this.canonicalizeLockTarget(lockPath);
704
+ const lockFile = `${canonicalPath}.lock`;
705
+ const ownerId = randomUUID();
706
+ const start = Date.now();
707
+ while (true) {
708
+ try {
709
+ await fs6.writeFile(lockFile, ownerId, { flag: "wx" });
710
+ return this.createLockRelease(lockFile, ownerId);
711
+ } catch (error) {
712
+ if (!this.hasErrnoCode(error, "EEXIST")) {
713
+ throw error;
714
+ }
715
+ const stats = await fs6.stat(lockFile).catch(() => null);
716
+ if (stats && Date.now() - stats.mtimeMs > LOCK_STALE_MS) {
717
+ const reclaimed = await fs6.rm(lockFile, { force: true }).then(() => true).catch(() => false);
718
+ if (reclaimed)
719
+ continue;
720
+ }
721
+ if (Date.now() - start > LOCK_MAX_WAIT_MS) {
722
+ throw new Error(`ELOCKED: timed out waiting for lock on ${canonicalPath}`);
723
+ }
724
+ await new Promise((resolve2) => setTimeout(resolve2, LOCK_RETRY_MIN_MS + Math.random() * LOCK_RETRY_JITTER_MS));
725
+ }
726
+ }
727
+ }
728
+ async canonicalizeLockTarget(lockPath) {
729
+ const absolute = path2.resolve(lockPath);
730
+ const fullReal = await fs6.realpath(absolute).catch(() => null);
731
+ if (fullReal)
732
+ return fullReal;
733
+ const parent = path2.dirname(absolute);
734
+ const base = path2.basename(absolute);
735
+ const canonicalParent = await fs6.realpath(parent).catch(() => parent);
736
+ return path2.join(canonicalParent, base);
737
+ }
738
+ createLockRelease(lockFile, ownerId) {
739
+ const heartbeatStart = Date.now();
740
+ let heartbeatTimer;
741
+ let stopped = false;
742
+ const stopHeartbeat = () => {
743
+ stopped = true;
744
+ if (heartbeatTimer)
745
+ clearTimeout(heartbeatTimer);
746
+ };
747
+ const scheduleNextHeartbeat = () => {
748
+ if (stopped)
749
+ return;
750
+ if (Date.now() - heartbeatStart >= LOCK_MAX_HOLD_MS) {
751
+ stopped = true;
752
+ return;
753
+ }
754
+ heartbeatTimer = setTimeout(() => {
755
+ runHeartbeat();
756
+ }, LOCK_HEARTBEAT_MS);
757
+ heartbeatTimer.unref?.();
758
+ };
759
+ const runHeartbeat = async () => {
760
+ if (stopped)
761
+ return;
762
+ const current = await fs6.readFile(lockFile, "utf-8").catch(() => null);
763
+ if (stopped)
764
+ return;
765
+ if (current !== ownerId) {
766
+ stopped = true;
767
+ return;
768
+ }
769
+ const now = Date.now() / 1000;
770
+ await fs6.utimes(lockFile, now, now).catch(() => {});
771
+ scheduleNextHeartbeat();
772
+ };
773
+ scheduleNextHeartbeat();
774
+ let released = false;
775
+ return async () => {
776
+ if (released)
777
+ return;
778
+ released = true;
779
+ stopHeartbeat();
780
+ const current = await fs6.readFile(lockFile, "utf-8").catch(() => null);
781
+ if (current === ownerId) {
782
+ await fs6.rm(lockFile, { force: true });
783
+ }
784
+ };
785
+ }
694
786
  async rm(filePath) {
695
787
  await fs6.rm(filePath, { recursive: true, force: true });
696
788
  }
@@ -736,7 +828,10 @@ class NodeFileSystem {
736
828
  }
737
829
  }
738
830
  isEnoent(error) {
739
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
831
+ return this.hasErrnoCode(error, "ENOENT");
832
+ }
833
+ hasErrnoCode(error, code) {
834
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
740
835
  }
741
836
  }
742
837
 
@@ -32,6 +32,41 @@ export interface IFileSystem {
32
32
  stat(path: string): Promise<FileSystemStats | null>;
33
33
  exists(path: string): Promise<boolean>;
34
34
  mkdir(path: string): Promise<void>;
35
+ /**
36
+ * Acquire a cross-process advisory lock for `path`, serializing work
37
+ * across concurrent processes that would otherwise race on the same
38
+ * file (e.g. one-time-use auth refresh tokens). Resolves to a release
39
+ * function — call it in a `finally` (wrapped in try/catch) to free the
40
+ * lock.
41
+ *
42
+ * Node claims a sibling `<path>.lock` file via an atomic create-or-fail
43
+ * write (`O_CREAT | O_EXCL`, fail-on-`EEXIST`) carrying a random owner
44
+ * id, retrying with jitter; while held, a self-rescheduling heartbeat
45
+ * keeps the lock fresh so a live holder is never reclaimed, and a
46
+ * holder that stops heartbeating (crashed/hung, or held longer than
47
+ * the heartbeat-lifetime cap) is reclaimed as stale. Acquisition
48
+ * rejects with an `ELOCKED` error after a timeout; release removes the
49
+ * lock only if this process still owns it, and may itself reject if
50
+ * that removal fails (e.g. `EACCES`).
51
+ *
52
+ * Browser prefers the Web Locks API (`navigator.locks`), which
53
+ * coordinates across same-origin tabs, workers, and the service
54
+ * worker. When `navigator.locks` is unavailable (older runtimes,
55
+ * non-window contexts) it falls back to an in-tab promise chain —
56
+ * serialization within the tab only. LightningFS is per-tab anyway,
57
+ * so a stored lock file could not coordinate cross-tab either.
58
+ *
59
+ * `path` is normalized before use so two callers passing distinct
60
+ * strings that resolve to the same file share a single lock — Node
61
+ * absolutizes via CWD and realpaths the full target (collapsing
62
+ * symlinks on both parent and basename), falling back to
63
+ * realpath-of-parent + lexical basename when the target file does
64
+ * not exist yet (e.g. first-run auth file). Browser collapses
65
+ * `.`/`..` segments and treats relative paths as rooted at `/`.
66
+ * Pass the same logical path consistently across callers; do NOT
67
+ * rely on case-insensitive collapsing on Windows or macOS.
68
+ */
69
+ acquireLock(path: string): Promise<() => Promise<void>>;
35
70
  rm(path: string): Promise<void>;
36
71
  rename(oldPath: string, newPath: string): Promise<void>;
37
72
  /**
package/dist/node.d.ts CHANGED
@@ -13,10 +13,14 @@ export declare class NodeFileSystem implements IFileSystem {
13
13
  stat(filePath: string): Promise<FileSystemStats | null>;
14
14
  exists(filePath: string): Promise<boolean>;
15
15
  mkdir(dirPath: string): Promise<void>;
16
+ acquireLock(lockPath: string): Promise<() => Promise<void>>;
17
+ private canonicalizeLockTarget;
18
+ private createLockRelease;
16
19
  rm(filePath: string): Promise<void>;
17
20
  rename(oldPath: string, newPath: string): Promise<void>;
18
21
  realpath(filePath: string): Promise<string>;
19
22
  getTempDir(): Promise<string>;
20
23
  copyDirectory(sourcePath: string, destPath: string): Promise<void>;
21
24
  private isEnoent;
25
+ private hasErrnoCode;
22
26
  }
package/package.json CHANGED
@@ -1,47 +1,41 @@
1
1
  {
2
- "name": "@uipath/filesystem",
3
- "version": "1.1.0",
4
- "repository": {
5
- "type": "git",
6
- "url": "https://github.com/UiPath/cli.git",
7
- "directory": "packages/filesystem"
2
+ "name": "@uipath/filesystem",
3
+ "license": "MIT",
4
+ "version": "1.196.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/UiPath/cli.git",
8
+ "directory": "packages/filesystem"
9
+ },
10
+ "publishConfig": {
11
+ "registry": "https://registry.npmjs.org/"
12
+ },
13
+ "type": "module",
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "browser": {
19
+ "types": "./dist/index.browser.d.ts",
20
+ "default": "./dist/index.browser.js"
21
+ },
22
+ "default": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ }
8
26
  },
9
- "publishConfig": {
10
- "registry": "https://registry.npmjs.org/"
11
- },
12
- "type": "module",
13
- "main": "./dist/index.js",
14
- "types": "./dist/index.d.ts",
15
- "exports": {
16
- ".": {
17
- "browser": {
18
- "types": "./dist/index.browser.d.ts",
19
- "default": "./dist/index.browser.js"
20
- },
21
- "default": {
22
- "types": "./dist/index.d.ts",
23
- "default": "./dist/index.js"
24
- }
25
- },
26
- "./browser": {
27
- "types": "./dist/index.browser.d.ts",
28
- "default": "./dist/index.browser.js"
29
- }
30
- },
31
- "files": [
32
- "dist"
33
- ],
34
- "maintainers": [
35
- "aoltean16",
36
- "mihaigirleanu",
37
- "vlad-uipath"
38
- ],
39
- "devDependencies": {
40
- "@isomorphic-git/lightning-fs": "^4.6.2",
41
- "@types/node": "^25.5.2",
42
- "fake-indexeddb": "^6.2.4",
43
- "open": "^11.0.0",
44
- "typescript": "^6.0.2"
45
- },
46
- "gitHead": "06e8c8f566df4b87da4a008635483c62f64f33f0"
27
+ "./browser": {
28
+ "types": "./dist/index.browser.d.ts",
29
+ "default": "./dist/index.browser.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "maintainers": [
36
+ "aoltean16",
37
+ "mihaigirleanu",
38
+ "vlad-uipath"
39
+ ],
40
+ "gitHead": "94d71f9c52214980a1f0ae62b3f5372095788553"
47
41
  }