@pnpm/fs.graceful-fs 1100.2.3 → 1100.2.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @pnpm/fs.graceful-fs
2
2
 
3
+ ## 1100.2.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies:
8
+ - @pnpm/error@1100.2.2
9
+
10
+ ## 1100.2.4
11
+
12
+ ### Patch Changes
13
+
14
+ - Fixed concurrent installs failing when replacing the same stale hoisted dependency link.
15
+
16
+ - `pnpm install` now works in StackBlitz WebContainers on projects without a lockfile. It used to fail there with `ENOENT ... pnpm-lock.yaml`, because pnpm did not recognize the errors that WebContainers return from asynchronous file system calls [#15649](https://github.com/pnpm/pnpm/issues/15649).
17
+
18
+ - Updated dependencies:
19
+ - @pnpm/error@1100.2.1
20
+
3
21
  ## 1100.2.3
4
22
 
5
23
  ### Patch Changes
package/lib/index.d.ts CHANGED
@@ -55,4 +55,10 @@ export declare function unlinkWithRetry(target: string): void;
55
55
  * Runs a filesystem operation with the retry policy of
56
56
  * {@link renameFileWithRetry}.
57
57
  */
58
- export declare function withFileLockRetry<T>(operation: () => T): T;
58
+ export declare function withFileLockRetry<Result>(operation: () => Result): Result;
59
+ /**
60
+ * Asynchronous {@link withFileLockRetry}, which waits between attempts
61
+ * without blocking the event loop.
62
+ */
63
+ export declare function withFileLockRetryAsync<Result>(operation: () => Promise<Result>): Promise<Result>;
64
+ export declare function isTransientFileLockError(err: unknown): err is NodeJS.ErrnoException;
package/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
- import util, { promisify } from 'node:util';
3
+ import { promisify } from 'node:util';
4
+ import { isError } from '@pnpm/error';
4
5
  import gfs from 'graceful-fs';
5
6
  const FILE_LOCK_RETRY_BUDGET_MS = 60_000;
6
7
  const PERMISSION_DENIED_RETRY_BUDGET_MS = 1_000;
@@ -30,23 +31,33 @@ function withEagainRetry(fn, maxRetries = 15) {
30
31
  return (...args) => {
31
32
  let attempts = 0;
32
33
  while (attempts <= maxRetries) {
33
- try {
34
- return fn(...args);
35
- }
36
- catch (err) {
37
- if (util.types.isNativeError(err) && 'code' in err && err.code === 'EAGAIN' && attempts < maxRetries) {
38
- attempts++;
39
- // Exponential backoff: wait 2^attempts milliseconds, max 300ms
40
- const delay = Math.min(Math.pow(2, attempts), 300);
41
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay);
42
- continue;
43
- }
44
- throw err;
45
- }
34
+ const outcome = tryCallWithEagain(fn, args, attempts, maxRetries);
35
+ if (outcome.success)
36
+ return outcome.value;
37
+ attempts = outcome.attempts;
46
38
  }
47
39
  throw new Error('Unreachable');
48
40
  };
49
41
  }
42
+ function tryCallWithEagain(fn, args, attempts, maxRetries) {
43
+ try {
44
+ return { success: true, value: fn(...args) };
45
+ }
46
+ catch (err) {
47
+ if (!isEagainError(err) || attempts >= maxRetries)
48
+ throw err;
49
+ const nextAttempts = attempts + 1;
50
+ waitEagainBackoff(nextAttempts);
51
+ return { success: false, attempts: nextAttempts };
52
+ }
53
+ }
54
+ function isEagainError(err) {
55
+ return isError(err) && 'code' in err && err.code === 'EAGAIN';
56
+ }
57
+ function waitEagainBackoff(attempts) {
58
+ const delay = Math.min(Math.pow(2, attempts), 300);
59
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay);
60
+ }
50
61
  /**
51
62
  * Renames `src` over `dest`, retrying Windows EBUSY errors for up to a minute.
52
63
  * Windows drives mounted into WSL get the same retries.
@@ -108,23 +119,30 @@ export function withFileLockRetry(operation) {
108
119
  }
109
120
  }
110
121
  }
111
- async function withFileLockRetryAsync(operation) {
122
+ /**
123
+ * Asynchronous {@link withFileLockRetry}, which waits between attempts
124
+ * without blocking the event loop.
125
+ */
126
+ export async function withFileLockRetryAsync(operation) {
112
127
  const retry = createFileLockRetry();
113
128
  for (;;) {
114
129
  try {
115
- // eslint-disable-next-line no-await-in-loop
130
+ // eslint-disable-next-line no-await-in-loop -- the next attempt runs only after this one fails
116
131
  return await operation();
117
132
  }
118
133
  catch (err) {
119
- const delayMs = retry.delayBeforeNextAttempt(err);
120
- if (delayMs > 0) {
121
- // eslint-disable-next-line no-await-in-loop
122
- await new Promise((resolve) => setTimeout(resolve, delayMs));
123
- }
124
- retry.checkBudgetAfterDelay(err);
134
+ // eslint-disable-next-line no-await-in-loop -- backs off before the next attempt
135
+ await backOffWithFileLockRetryAsync(retry, err);
125
136
  }
126
137
  }
127
138
  }
139
+ async function backOffWithFileLockRetryAsync(retry, err) {
140
+ const delayMs = retry.delayBeforeNextAttempt(err);
141
+ if (delayMs > 0) {
142
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
143
+ }
144
+ retry.checkBudgetAfterDelay(err);
145
+ }
128
146
  function createFileLockRetry() {
129
147
  const startedAt = Date.now();
130
148
  let backoffMs = 0;
@@ -147,9 +165,9 @@ function createFileLockRetry() {
147
165
  },
148
166
  };
149
167
  }
150
- function isTransientFileLockError(err) {
168
+ export function isTransientFileLockError(err) {
151
169
  return (process.platform === 'win32' || isWsl()) &&
152
- util.types.isNativeError(err) &&
170
+ isError(err) &&
153
171
  'code' in err &&
154
172
  (err.code === 'EPERM' || err.code === 'EACCES' || err.code === 'EBUSY');
155
173
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/fs.graceful-fs",
3
- "version": "1100.2.3",
3
+ "version": "1100.2.5",
4
4
  "description": "Promisified graceful-fs",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -27,11 +27,12 @@
27
27
  "!*.map"
28
28
  ],
29
29
  "dependencies": {
30
+ "@pnpm/error": "1100.2.2",
30
31
  "graceful-fs": "^4.2.11"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@jest/globals": "30.4.1",
34
- "@pnpm/fs.graceful-fs": "1100.2.3",
35
+ "@pnpm/fs.graceful-fs": "1100.2.5",
35
36
  "@types/graceful-fs": "^4.1.9"
36
37
  },
37
38
  "engines": {