@hydranium/core 1.0.0-next.39 → 1.0.0-next.40
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/lib/node/node-file-system-provider.d.ts.map +1 -1
- package/lib/node/node-file-system-provider.js +1 -40
- package/lib/node/node-file-system-provider.js.map +1 -1
- package/lib/node/rename-over-open-readers.d.ts +52 -0
- package/lib/node/rename-over-open-readers.d.ts.map +1 -0
- package/lib/node/rename-over-open-readers.js +66 -0
- package/lib/node/rename-over-open-readers.js.map +1 -0
- package/package.json +5 -5
- package/src/node/node-file-system-provider.ts +1 -41
- package/src/node/rename-over-open-readers.ts +78 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-file-system-provider.d.ts","sourceRoot":"","sources":["../../src/node/node-file-system-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,GAAG,EAAY,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAIjE,OAAO,EAAE,KAAK,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AACvF,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"node-file-system-provider.d.ts","sourceRoot":"","sources":["../../src/node/node-file-system-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,GAAG,EAAY,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAIjE,OAAO,EAAE,KAAK,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AACvF,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AAE3E,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAuB,KAAK,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAGtG;;;;;;;;;;;;GAYG;AACH,qBAAa,yBAA0B,SAAQ,sBAAuB,YAAW,0BAA0B;IAKrG,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE,2BAA2B;IAJ3D,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;IAC5C,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAGZ,QAAQ,EAAE,2BAA2B,EACxD,OAAO,GAAE,cAAmB;IAStB,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC;IAKnC,YAAY,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM;IAIvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6CG;IACG,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgCnD,OAAO,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAUpD;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,GAAG,SAAS;CAUrC;AA6BD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc;;CAE1B,CAAC"}
|
|
@@ -11,6 +11,7 @@ import { NodeFileSystemProvider } from '@hydranium/langium/node';
|
|
|
11
11
|
import { randomUUID } from 'node:crypto';
|
|
12
12
|
import { realpathSync } from 'node:fs';
|
|
13
13
|
import * as fsp from 'node:fs/promises';
|
|
14
|
+
import { renameOverOpenReaders } from './rename-over-open-readers.js';
|
|
14
15
|
import { serverSharedFactory } from '../langium/shared-services.js';
|
|
15
16
|
import { serveVirtualDocument } from '../langium/workspace/virtual-document.js';
|
|
16
17
|
/**
|
|
@@ -173,46 +174,6 @@ async function replacedFile(path) {
|
|
|
173
174
|
return undefined;
|
|
174
175
|
}
|
|
175
176
|
}
|
|
176
|
-
/**
|
|
177
|
-
* Errno values a rename reports on Windows when another handle holds the
|
|
178
|
-
* destination open. POSIX renames are unaffected by open handles, so this
|
|
179
|
-
* cannot fire there and the retry below costs nothing.
|
|
180
|
-
*/
|
|
181
|
-
const RENAME_CONTENTION_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']);
|
|
182
|
-
/**
|
|
183
|
-
* `rename` the staged content over `destination`, retrying while Windows
|
|
184
|
-
* reports the destination as held open.
|
|
185
|
-
*
|
|
186
|
-
* The staging file exists so a reader never sees a half-written file, and on
|
|
187
|
-
* Windows the very presence of that reader is what makes the replacing rename
|
|
188
|
-
* fail — so the indivisible write breaks under exactly the contention it was
|
|
189
|
-
* built for, and it breaks intermittently, which is worse than never working.
|
|
190
|
-
* A reader's handle is released in milliseconds, so waiting turns a spurious
|
|
191
|
-
* failure into a slightly later success.
|
|
192
|
-
*
|
|
193
|
-
* BOUNDED, not open-ended: a genuine permission fault raises the same errno and
|
|
194
|
-
* is distinguishable from contention only by never clearing, so an unbounded
|
|
195
|
-
* wait would convert a hard error into a hang. Exhausting the budget rethrows
|
|
196
|
-
* the last failure, leaving the caller's cleanup and error contract unchanged.
|
|
197
|
-
*/
|
|
198
|
-
async function renameOverOpenReaders(staging, destination) {
|
|
199
|
-
const deadline = Date.now() + 2_000;
|
|
200
|
-
for (let attempt = 0;; attempt++) {
|
|
201
|
-
try {
|
|
202
|
-
await fsp.rename(staging, destination);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
catch (err) {
|
|
206
|
-
const code = err instanceof Error && 'code' in err ? String(err.code) : undefined;
|
|
207
|
-
if (code === undefined || !RENAME_CONTENTION_CODES.has(code) || Date.now() >= deadline) {
|
|
208
|
-
throw err;
|
|
209
|
-
}
|
|
210
|
-
// Backs off to keep a long contention window from spinning, capped so
|
|
211
|
-
// a late attempt still lands promptly once the handle is released.
|
|
212
|
-
await new Promise(resolve => setTimeout(resolve, Math.min(2 ** attempt, 50)));
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
177
|
/**
|
|
217
178
|
* Module fragment that binds the writable Node {@link DefaultFileSystemProvider},
|
|
218
179
|
* spread into the services context on a Node host.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-file-system-provider.js","sourceRoot":"","sources":["../../src/node/node-file-system-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAGlF,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,GAAG,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"node-file-system-provider.js","sourceRoot":"","sources":["../../src/node/node-file-system-provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
/**
|
|
10
|
+
* Errnos Windows raises when a replacing `rename` meets a handle already on the
|
|
11
|
+
* destination open. POSIX renames are unaffected by open handles, so these
|
|
12
|
+
* cannot fire there and the retry below costs nothing.
|
|
13
|
+
*/
|
|
14
|
+
export declare const RENAME_CONTENTION_CODES: Set<string>;
|
|
15
|
+
/**
|
|
16
|
+
* How long a replacing rename may keep retrying before it gives up.
|
|
17
|
+
*
|
|
18
|
+
* BOUNDED, not open-ended: a genuine permission fault raises the same errno as
|
|
19
|
+
* contention and is distinguishable from it only by never clearing, so an
|
|
20
|
+
* unbounded wait would convert a hard error into a hang.
|
|
21
|
+
*
|
|
22
|
+
* The figure is a judgement, not a measurement, and the honest bound on it is
|
|
23
|
+
* that the previous 2s was observed EXHAUSTED on a loaded Windows CI runner
|
|
24
|
+
* while no upper bound has been established. What would settle it is recording
|
|
25
|
+
* how long a real contended rename takes to clear. graceful-fs tolerates 60s
|
|
26
|
+
* for Windows rename contention, but that is weaker support than it looks: it
|
|
27
|
+
* retries only where the destination has GONE, which is the opposite of
|
|
28
|
+
* replacing one a reader holds open.
|
|
29
|
+
*/
|
|
30
|
+
export declare const RENAME_BUDGET_MS = 10000;
|
|
31
|
+
/** Seams for the test, which cannot make a real rename fail on demand. */
|
|
32
|
+
export interface RenameOverOpenReadersOptions {
|
|
33
|
+
readonly rename?: (from: string, to: string) => Promise<void>;
|
|
34
|
+
readonly budgetMs?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* `rename` the staged content over `destination`, retrying while Windows
|
|
38
|
+
* reports the destination as held open.
|
|
39
|
+
*
|
|
40
|
+
* The staging file exists so a reader never sees a half-written file, and on
|
|
41
|
+
* Windows the very presence of that reader is what makes the replacing rename
|
|
42
|
+
* fail — so the indivisible write breaks under exactly the contention it was
|
|
43
|
+
* built for, and it breaks intermittently, which is worse than never working.
|
|
44
|
+
* A reader's handle is released in milliseconds, so waiting turns a spurious
|
|
45
|
+
* failure into a slightly later success.
|
|
46
|
+
*
|
|
47
|
+
* Exhausting the budget rethrows the last failure, leaving the caller's cleanup
|
|
48
|
+
* and error contract unchanged. An errno OUTSIDE the contention set is rethrown
|
|
49
|
+
* without waiting at all: retrying a fault that will not clear only delays it.
|
|
50
|
+
*/
|
|
51
|
+
export declare function renameOverOpenReaders(staging: string, destination: string, options?: RenameOverOpenReadersOptions): Promise<void>;
|
|
52
|
+
//# sourceMappingURL=rename-over-open-readers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rename-over-open-readers.d.ts","sourceRoot":"","sources":["../../src/node/rename-over-open-readers.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAIlF;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,aAAwC,CAAC;AAE7E;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAEvC,0EAA0E;AAC1E,MAAM,WAAW,4BAA4B;IAC1C,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,qBAAqB,CACxC,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,4BAAiC,GAC1C,OAAO,CAAC,IAAI,CAAC,CAiBf"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
import * as fsp from 'node:fs/promises';
|
|
10
|
+
/**
|
|
11
|
+
* Errnos Windows raises when a replacing `rename` meets a handle already on the
|
|
12
|
+
* destination open. POSIX renames are unaffected by open handles, so these
|
|
13
|
+
* cannot fire there and the retry below costs nothing.
|
|
14
|
+
*/
|
|
15
|
+
export const RENAME_CONTENTION_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']);
|
|
16
|
+
/**
|
|
17
|
+
* How long a replacing rename may keep retrying before it gives up.
|
|
18
|
+
*
|
|
19
|
+
* BOUNDED, not open-ended: a genuine permission fault raises the same errno as
|
|
20
|
+
* contention and is distinguishable from it only by never clearing, so an
|
|
21
|
+
* unbounded wait would convert a hard error into a hang.
|
|
22
|
+
*
|
|
23
|
+
* The figure is a judgement, not a measurement, and the honest bound on it is
|
|
24
|
+
* that the previous 2s was observed EXHAUSTED on a loaded Windows CI runner
|
|
25
|
+
* while no upper bound has been established. What would settle it is recording
|
|
26
|
+
* how long a real contended rename takes to clear. graceful-fs tolerates 60s
|
|
27
|
+
* for Windows rename contention, but that is weaker support than it looks: it
|
|
28
|
+
* retries only where the destination has GONE, which is the opposite of
|
|
29
|
+
* replacing one a reader holds open.
|
|
30
|
+
*/
|
|
31
|
+
export const RENAME_BUDGET_MS = 10_000;
|
|
32
|
+
/**
|
|
33
|
+
* `rename` the staged content over `destination`, retrying while Windows
|
|
34
|
+
* reports the destination as held open.
|
|
35
|
+
*
|
|
36
|
+
* The staging file exists so a reader never sees a half-written file, and on
|
|
37
|
+
* Windows the very presence of that reader is what makes the replacing rename
|
|
38
|
+
* fail — so the indivisible write breaks under exactly the contention it was
|
|
39
|
+
* built for, and it breaks intermittently, which is worse than never working.
|
|
40
|
+
* A reader's handle is released in milliseconds, so waiting turns a spurious
|
|
41
|
+
* failure into a slightly later success.
|
|
42
|
+
*
|
|
43
|
+
* Exhausting the budget rethrows the last failure, leaving the caller's cleanup
|
|
44
|
+
* and error contract unchanged. An errno OUTSIDE the contention set is rethrown
|
|
45
|
+
* without waiting at all: retrying a fault that will not clear only delays it.
|
|
46
|
+
*/
|
|
47
|
+
export async function renameOverOpenReaders(staging, destination, options = {}) {
|
|
48
|
+
const rename = options.rename ?? fsp.rename;
|
|
49
|
+
const deadline = Date.now() + (options.budgetMs ?? RENAME_BUDGET_MS);
|
|
50
|
+
for (let attempt = 0;; attempt++) {
|
|
51
|
+
try {
|
|
52
|
+
await rename(staging, destination);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
const code = err instanceof Error && 'code' in err ? String(err.code) : undefined;
|
|
57
|
+
if (code === undefined || !RENAME_CONTENTION_CODES.has(code) || Date.now() >= deadline) {
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
// Backs off to keep a long contention window from spinning, capped so
|
|
61
|
+
// a late attempt still lands promptly once the handle is released.
|
|
62
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(2 ** attempt, 50)));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=rename-over-open-readers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rename-over-open-readers.js","sourceRoot":"","sources":["../../src/node/rename-over-open-readers.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,KAAK,GAAG,MAAM,kBAAkB,CAAC;AAExC;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;AAE7E;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAQvC;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACxC,OAAe,EACf,WAAmB,EACnB,UAAwC,EAAE;IAE1C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC;IAC5C,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,QAAQ,IAAI,gBAAgB,CAAC,CAAC;IACrE,KAAK,IAAI,OAAO,GAAG,CAAC,GAAI,OAAO,EAAE,EAAE,CAAC;QACjC,IAAI,CAAC;YACF,MAAM,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;YACnC,OAAO;QACV,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACrB,MAAM,IAAI,GAAG,GAAG,YAAY,KAAK,IAAI,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,MAAM,CAAE,GAA6B,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAC7G,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,QAAQ,EAAE,CAAC;gBACtF,MAAM,GAAG,CAAC;YACb,CAAC;YACD,sEAAsE;YACtE,mEAAmE;YACnE,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QACjF,CAAC;IACJ,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hydranium/core",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.40",
|
|
4
4
|
"description": "Foundational runtime of the hydranium framework: AST coordination layer (services, integrity, AST extension, serialization, multi-client documents) plus the LSP textual head at the /lsp subpath. Consumed by protocol-head packages (@hydranium/data-server, @hydranium/glsp-server).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hydranium",
|
|
@@ -104,8 +104,8 @@
|
|
|
104
104
|
"diff": "^5.2.0"
|
|
105
105
|
},
|
|
106
106
|
"devDependencies": {
|
|
107
|
-
"@hydranium/langium": "1.0.0-next.
|
|
108
|
-
"@hydranium/protocol": "1.0.0-next.
|
|
107
|
+
"@hydranium/langium": "1.0.0-next.40",
|
|
108
|
+
"@hydranium/protocol": "1.0.0-next.40",
|
|
109
109
|
"@playwright/test": "^1.40.0",
|
|
110
110
|
"@types/diff": "^5.2.0",
|
|
111
111
|
"rimraf": "^5.0.0",
|
|
@@ -117,8 +117,8 @@
|
|
|
117
117
|
"vscode-languageserver-types": "^3.17.5"
|
|
118
118
|
},
|
|
119
119
|
"peerDependencies": {
|
|
120
|
-
"@hydranium/langium": "1.0.0-next.
|
|
121
|
-
"@hydranium/protocol": "1.0.0-next.
|
|
120
|
+
"@hydranium/langium": "1.0.0-next.40",
|
|
121
|
+
"@hydranium/protocol": "1.0.0-next.40",
|
|
122
122
|
"@playwright/test": "^1.40.0",
|
|
123
123
|
"vscode-jsonrpc": "9.0.1",
|
|
124
124
|
"vscode-languageserver": "~10.0.1",
|
|
@@ -15,6 +15,7 @@ import { realpathSync } from 'node:fs';
|
|
|
15
15
|
import * as fsp from 'node:fs/promises';
|
|
16
16
|
import { type WritableFileSystemProvider } from '../documents/ast-document-manager.js';
|
|
17
17
|
import { type SelfSaveRegistry } from '../documents/self-save-registry.js';
|
|
18
|
+
import { renameOverOpenReaders } from './rename-over-open-readers.js';
|
|
18
19
|
import { type LogNameOptions } from '../langium/diagnostics/logger.js';
|
|
19
20
|
import { serverSharedFactory, type ServerSharedServicesMinimal } from '../langium/shared-services.js';
|
|
20
21
|
import { serveVirtualDocument } from '../langium/workspace/virtual-document.js';
|
|
@@ -195,47 +196,6 @@ async function replacedFile(path: string): Promise<ReplacedFile | undefined> {
|
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
198
|
|
|
198
|
-
/**
|
|
199
|
-
* Errno values a rename reports on Windows when another handle holds the
|
|
200
|
-
* destination open. POSIX renames are unaffected by open handles, so this
|
|
201
|
-
* cannot fire there and the retry below costs nothing.
|
|
202
|
-
*/
|
|
203
|
-
const RENAME_CONTENTION_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']);
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* `rename` the staged content over `destination`, retrying while Windows
|
|
207
|
-
* reports the destination as held open.
|
|
208
|
-
*
|
|
209
|
-
* The staging file exists so a reader never sees a half-written file, and on
|
|
210
|
-
* Windows the very presence of that reader is what makes the replacing rename
|
|
211
|
-
* fail — so the indivisible write breaks under exactly the contention it was
|
|
212
|
-
* built for, and it breaks intermittently, which is worse than never working.
|
|
213
|
-
* A reader's handle is released in milliseconds, so waiting turns a spurious
|
|
214
|
-
* failure into a slightly later success.
|
|
215
|
-
*
|
|
216
|
-
* BOUNDED, not open-ended: a genuine permission fault raises the same errno and
|
|
217
|
-
* is distinguishable from contention only by never clearing, so an unbounded
|
|
218
|
-
* wait would convert a hard error into a hang. Exhausting the budget rethrows
|
|
219
|
-
* the last failure, leaving the caller's cleanup and error contract unchanged.
|
|
220
|
-
*/
|
|
221
|
-
async function renameOverOpenReaders(staging: string, destination: string): Promise<void> {
|
|
222
|
-
const deadline = Date.now() + 2_000;
|
|
223
|
-
for (let attempt = 0; ; attempt++) {
|
|
224
|
-
try {
|
|
225
|
-
await fsp.rename(staging, destination);
|
|
226
|
-
return;
|
|
227
|
-
} catch (err: unknown) {
|
|
228
|
-
const code = err instanceof Error && 'code' in err ? String((err as NodeJS.ErrnoException).code) : undefined;
|
|
229
|
-
if (code === undefined || !RENAME_CONTENTION_CODES.has(code) || Date.now() >= deadline) {
|
|
230
|
-
throw err;
|
|
231
|
-
}
|
|
232
|
-
// Backs off to keep a long contention window from spinning, capped so
|
|
233
|
-
// a late attempt still lands promptly once the handle is released.
|
|
234
|
-
await new Promise(resolve => setTimeout(resolve, Math.min(2 ** attempt, 50)));
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
199
|
/**
|
|
240
200
|
* Module fragment that binds the writable Node {@link DefaultFileSystemProvider},
|
|
241
201
|
* spread into the services context on a Node host.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import * as fsp from 'node:fs/promises';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Errnos Windows raises when a replacing `rename` meets a handle already on the
|
|
14
|
+
* destination open. POSIX renames are unaffected by open handles, so these
|
|
15
|
+
* cannot fire there and the retry below costs nothing.
|
|
16
|
+
*/
|
|
17
|
+
export const RENAME_CONTENTION_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* How long a replacing rename may keep retrying before it gives up.
|
|
21
|
+
*
|
|
22
|
+
* BOUNDED, not open-ended: a genuine permission fault raises the same errno as
|
|
23
|
+
* contention and is distinguishable from it only by never clearing, so an
|
|
24
|
+
* unbounded wait would convert a hard error into a hang.
|
|
25
|
+
*
|
|
26
|
+
* The figure is a judgement, not a measurement, and the honest bound on it is
|
|
27
|
+
* that the previous 2s was observed EXHAUSTED on a loaded Windows CI runner
|
|
28
|
+
* while no upper bound has been established. What would settle it is recording
|
|
29
|
+
* how long a real contended rename takes to clear. graceful-fs tolerates 60s
|
|
30
|
+
* for Windows rename contention, but that is weaker support than it looks: it
|
|
31
|
+
* retries only where the destination has GONE, which is the opposite of
|
|
32
|
+
* replacing one a reader holds open.
|
|
33
|
+
*/
|
|
34
|
+
export const RENAME_BUDGET_MS = 10_000;
|
|
35
|
+
|
|
36
|
+
/** Seams for the test, which cannot make a real rename fail on demand. */
|
|
37
|
+
export interface RenameOverOpenReadersOptions {
|
|
38
|
+
readonly rename?: (from: string, to: string) => Promise<void>;
|
|
39
|
+
readonly budgetMs?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `rename` the staged content over `destination`, retrying while Windows
|
|
44
|
+
* reports the destination as held open.
|
|
45
|
+
*
|
|
46
|
+
* The staging file exists so a reader never sees a half-written file, and on
|
|
47
|
+
* Windows the very presence of that reader is what makes the replacing rename
|
|
48
|
+
* fail — so the indivisible write breaks under exactly the contention it was
|
|
49
|
+
* built for, and it breaks intermittently, which is worse than never working.
|
|
50
|
+
* A reader's handle is released in milliseconds, so waiting turns a spurious
|
|
51
|
+
* failure into a slightly later success.
|
|
52
|
+
*
|
|
53
|
+
* Exhausting the budget rethrows the last failure, leaving the caller's cleanup
|
|
54
|
+
* and error contract unchanged. An errno OUTSIDE the contention set is rethrown
|
|
55
|
+
* without waiting at all: retrying a fault that will not clear only delays it.
|
|
56
|
+
*/
|
|
57
|
+
export async function renameOverOpenReaders(
|
|
58
|
+
staging: string,
|
|
59
|
+
destination: string,
|
|
60
|
+
options: RenameOverOpenReadersOptions = {}
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
const rename = options.rename ?? fsp.rename;
|
|
63
|
+
const deadline = Date.now() + (options.budgetMs ?? RENAME_BUDGET_MS);
|
|
64
|
+
for (let attempt = 0; ; attempt++) {
|
|
65
|
+
try {
|
|
66
|
+
await rename(staging, destination);
|
|
67
|
+
return;
|
|
68
|
+
} catch (err: unknown) {
|
|
69
|
+
const code = err instanceof Error && 'code' in err ? String((err as NodeJS.ErrnoException).code) : undefined;
|
|
70
|
+
if (code === undefined || !RENAME_CONTENTION_CODES.has(code) || Date.now() >= deadline) {
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
// Backs off to keep a long contention window from spinning, capped so
|
|
74
|
+
// a late attempt still lands promptly once the handle is released.
|
|
75
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(2 ** attempt, 50)));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|