@pi-unipi/background-tasks 2.6.1
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/README.md +87 -0
- package/extensions/anthropic-attribution.ts +1 -0
- package/extensions/delegate-child.ts +1 -0
- package/extensions/fusion-child.ts +1 -0
- package/package.json +40 -0
- package/src/__tests__/anthropic-attribution.test.ts +195 -0
- package/src/__tests__/config.test.ts +137 -0
- package/src/__tests__/core.test.ts +493 -0
- package/src/__tests__/delegate-artifacts.test.ts +528 -0
- package/src/__tests__/delegate-budget.test.ts +456 -0
- package/src/__tests__/delegate-launch.test.ts +676 -0
- package/src/__tests__/delegate-result-package.test.ts +350 -0
- package/src/__tests__/delegate-seed.test.ts +392 -0
- package/src/__tests__/durable-fs.test.ts +559 -0
- package/src/__tests__/extension-api.test.ts +579 -0
- package/src/__tests__/fusion-artifacts.test.ts +1039 -0
- package/src/__tests__/fusion-budget.test.ts +1356 -0
- package/src/__tests__/fusion-claude-cache.test.ts +320 -0
- package/src/__tests__/fusion-config.test.ts +335 -0
- package/src/__tests__/fusion-context-prompts.test.ts +670 -0
- package/src/__tests__/fusion-evaluation.test.ts +315 -0
- package/src/__tests__/fusion-extraction-equivalence.test.ts +58 -0
- package/src/__tests__/fusion-golden-bytes.test.ts +35 -0
- package/src/__tests__/fusion-high-cardinality.test.ts +192 -0
- package/src/__tests__/fusion-model-selector.test.ts +205 -0
- package/src/__tests__/fusion-orchestrator.test.ts +1194 -0
- package/src/__tests__/fusion-rpc.test.ts +369 -0
- package/src/__tests__/fusion-sdk.test.ts +1226 -0
- package/src/__tests__/fusion-v5-core.test.ts +219 -0
- package/src/__tests__/fusion-validate-orchestrator.test.ts +240 -0
- package/src/__tests__/fusion-web-fetch.test.ts +485 -0
- package/src/__tests__/fusion-workflows.test.ts +59 -0
- package/src/__tests__/helpers/delegate-deterministic-seed.ts +109 -0
- package/src/__tests__/helpers/delegate-seed-subprocess.ts +10 -0
- package/src/__tests__/helpers/fusion-canonical-subprocess.ts +21 -0
- package/src/__tests__/helpers/fusion-canonical.ts +140 -0
- package/src/__tests__/helpers/fusion-fake-pi.ts +279 -0
- package/src/__tests__/helpers/fusion-golden-corpus.ts +500 -0
- package/src/__tests__/helpers/fusion-high-cardinality.ts +140 -0
- package/src/__tests__/helpers/normalize.ts +22 -0
- package/src/__tests__/helpers/pi-hook-contract-evidence.json +18 -0
- package/src/__tests__/pi-launch.test.ts +202 -0
- package/src/__tests__/registry.test.ts +1580 -0
- package/src/__tests__/scripted-provider/delegate-ambient-provider.test.ts +130 -0
- package/src/__tests__/scripted-provider/delegate-child-guard.test.ts +631 -0
- package/src/__tests__/scripted-provider/delegate-guard-provider.ts +403 -0
- package/src/__tests__/scripted-provider/follow-up.test.ts +448 -0
- package/src/__tests__/scripted-provider/fusion-output-recovery.test.ts +132 -0
- package/src/__tests__/scripted-provider/fusion-reason.test.ts +310 -0
- package/src/__tests__/scripted-provider/fusion-runtime-guard.test.ts +163 -0
- package/src/__tests__/scripted-provider/hook-contract-provider.ts +179 -0
- package/src/__tests__/scripted-provider/hook-probe-a.ts +3 -0
- package/src/__tests__/scripted-provider/hook-probe-b.ts +3 -0
- package/src/__tests__/scripted-provider/hook-probe-extension.ts +126 -0
- package/src/__tests__/scripted-provider/output-recovery-provider.ts +153 -0
- package/src/__tests__/scripted-provider/pi-hook-contract-evidence.json +18 -0
- package/src/__tests__/scripted-provider/pi-hook-contract.test.ts +477 -0
- package/src/__tests__/scripted-provider/runtime-guard-probe.ts +28 -0
- package/src/__tests__/scripted-provider/runtime-guard-provider.ts +49 -0
- package/src/__tests__/scripted-provider/scripted-provider-extension.ts +408 -0
- package/src/__tests__/task-manager.test.ts +479 -0
- package/src/__tests__/windows-taskkill.test.ts +161 -0
- package/src/anthropic-attribution-path.ts +21 -0
- package/src/anthropic-attribution.ts +1983 -0
- package/src/attested-pi-run.ts +612 -0
- package/src/child-process.ts +55 -0
- package/src/common.ts +8 -0
- package/src/config.ts +292 -0
- package/src/context-parent-snapshot.ts +142 -0
- package/src/context-token-budget.ts +903 -0
- package/src/context-visible-conversation-v2.ts +551 -0
- package/src/delegate/artifacts.ts +487 -0
- package/src/delegate/budget.ts +415 -0
- package/src/delegate/hook-contract-evidence.json +18 -0
- package/src/delegate/hook-contract.ts +154 -0
- package/src/delegate/launch.ts +497 -0
- package/src/delegate/result-package.ts +459 -0
- package/src/delegate/runner.ts +449 -0
- package/src/delegate/seed.ts +423 -0
- package/src/delegate/types.ts +323 -0
- package/src/delegate-child-extension.ts +978 -0
- package/src/delegate-extension.ts +806 -0
- package/src/durable-fs.ts +386 -0
- package/src/extension-api.ts +548 -0
- package/src/fixtures/delegate-context-incident.json +17 -0
- package/src/fixtures/fusion-golden-bytes.json +310 -0
- package/src/fixtures/fusion-validate-golden-bytes.json +282 -0
- package/src/fusion/artifacts.ts +967 -0
- package/src/fusion/budget.ts +1162 -0
- package/src/fusion/child-protocol.ts +305 -0
- package/src/fusion/claude-cache.ts +207 -0
- package/src/fusion/clean-context.ts +91 -0
- package/src/fusion/config.ts +449 -0
- package/src/fusion/context.ts +265 -0
- package/src/fusion/evaluation.ts +800 -0
- package/src/fusion/orchestrator.ts +1288 -0
- package/src/fusion/output-contract.ts +34 -0
- package/src/fusion/pi-child.ts +2373 -0
- package/src/fusion/prompts.ts +345 -0
- package/src/fusion/result-package.ts +959 -0
- package/src/fusion/source-policy.ts +257 -0
- package/src/fusion/types.ts +1139 -0
- package/src/fusion/web-fetch.ts +1060 -0
- package/src/fusion/workflows.ts +184 -0
- package/src/fusion-child-extension.ts +1052 -0
- package/src/fusion-extension.ts +1293 -0
- package/src/index.ts +295 -0
- package/src/pi-launch.ts +225 -0
- package/src/registry.ts +2424 -0
- package/src/settings-overlay.ts +208 -0
- package/src/task-manager.ts +774 -0
- package/src/tools.ts +530 -0
- package/src/turndown.d.ts +15 -0
- package/src/types.ts +963 -0
- package/src/ui/fusion-model-selector.ts +322 -0
- package/src/windows-taskkill.ts +250 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/background-tasks — Durable file operations
|
|
3
|
+
*
|
|
4
|
+
* Ported verbatim (behavior-preserving) from pi-background-tasks
|
|
5
|
+
* src/core/durable-fs.ts. ISC-licensed reference: Copyright Ismail
|
|
6
|
+
* <ismailsalikhodjaev@gmail.com>.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { randomBytes } from "node:crypto";
|
|
10
|
+
import { open as nodeOpen, rename as nodeRename, rm as nodeRm } from "node:fs/promises";
|
|
11
|
+
import { basename, dirname, join } from "node:path";
|
|
12
|
+
|
|
13
|
+
export type DurableData = Buffer | string;
|
|
14
|
+
export type DurableWriteFlag = "w" | "wx";
|
|
15
|
+
export type DurableOperation =
|
|
16
|
+
| "open_file"
|
|
17
|
+
| "write_file"
|
|
18
|
+
| "sync_file"
|
|
19
|
+
| "close_file"
|
|
20
|
+
| "rename_file"
|
|
21
|
+
| "remove_temp"
|
|
22
|
+
| "open_directory"
|
|
23
|
+
| "sync_directory"
|
|
24
|
+
| "close_directory";
|
|
25
|
+
|
|
26
|
+
export interface DurableFailure {
|
|
27
|
+
operation: DurableOperation;
|
|
28
|
+
path: string;
|
|
29
|
+
cause: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DurableWritableHandle {
|
|
33
|
+
writeFile(data: DurableData): Promise<void>;
|
|
34
|
+
sync(): Promise<void>;
|
|
35
|
+
close(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DurableDirectoryHandle {
|
|
39
|
+
sync(): Promise<void>;
|
|
40
|
+
close(): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DurableFileOperations {
|
|
44
|
+
platform: NodeJS.Platform;
|
|
45
|
+
openWritable(path: string, flag: DurableWriteFlag, mode?: number): Promise<DurableWritableHandle>;
|
|
46
|
+
openDirectory(path: string): Promise<DurableDirectoryHandle>;
|
|
47
|
+
rename(source: string, target: string): Promise<void>;
|
|
48
|
+
remove(path: string): Promise<void>;
|
|
49
|
+
temporaryPath(target: string): string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface DurableFileWriter {
|
|
53
|
+
write(path: string, data: DurableData): Promise<void>;
|
|
54
|
+
replace(path: string, data: DurableData): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface DurableErrorInput {
|
|
58
|
+
operation: DurableOperation;
|
|
59
|
+
path: string;
|
|
60
|
+
cause: unknown;
|
|
61
|
+
cleanupFailures: readonly DurableFailure[];
|
|
62
|
+
targetPath: string | undefined;
|
|
63
|
+
temporaryPath: string | undefined;
|
|
64
|
+
renameCompleted: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface WritableSequenceResult {
|
|
68
|
+
primaryFailure: DurableFailure | undefined;
|
|
69
|
+
cleanupFailures: DurableFailure[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class DurableFileError extends Error {
|
|
73
|
+
readonly operation: DurableOperation;
|
|
74
|
+
readonly path: string;
|
|
75
|
+
readonly nativeCode: string | undefined;
|
|
76
|
+
readonly primaryCause: unknown;
|
|
77
|
+
readonly cleanupFailures: readonly DurableFailure[];
|
|
78
|
+
readonly targetPath: string | undefined;
|
|
79
|
+
readonly temporaryPath: string | undefined;
|
|
80
|
+
readonly renameCompleted: boolean;
|
|
81
|
+
|
|
82
|
+
constructor(input: {
|
|
83
|
+
operation: DurableOperation;
|
|
84
|
+
path: string;
|
|
85
|
+
cause: unknown;
|
|
86
|
+
cleanupFailures: readonly DurableFailure[];
|
|
87
|
+
targetPath: string | undefined;
|
|
88
|
+
temporaryPath: string | undefined;
|
|
89
|
+
renameCompleted: boolean;
|
|
90
|
+
}) {
|
|
91
|
+
super(formatDurableMessage(input));
|
|
92
|
+
this.name = "DurableFileError";
|
|
93
|
+
this.operation = input.operation;
|
|
94
|
+
this.path = input.path;
|
|
95
|
+
this.nativeCode = nativeCodeForCause(input.cause);
|
|
96
|
+
this.primaryCause = input.cause;
|
|
97
|
+
this.cleanupFailures = [...input.cleanupFailures];
|
|
98
|
+
this.targetPath = input.targetPath;
|
|
99
|
+
this.temporaryPath = input.temporaryPath;
|
|
100
|
+
this.renameCompleted = input.renameCompleted;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function failure(operation: DurableOperation, path: string, cause: unknown): DurableFailure {
|
|
105
|
+
return { operation, path, cause };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function durableError(input: DurableErrorInput): DurableFileError {
|
|
109
|
+
return new DurableFileError(input);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function nativeCodeForCause(cause: unknown): string | undefined {
|
|
113
|
+
if (typeof cause !== "object" || cause === null) return undefined;
|
|
114
|
+
const code = Reflect.get(cause, "code");
|
|
115
|
+
return typeof code === "string" ? code : undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function describeCause(cause: unknown): string {
|
|
119
|
+
if (cause instanceof Error) {
|
|
120
|
+
const message = cause.message.length > 0 ? cause.message : String(cause);
|
|
121
|
+
return `${cause.name}: ${message}`;
|
|
122
|
+
}
|
|
123
|
+
return String(cause);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatFailureForMessage(entry: DurableFailure): string {
|
|
127
|
+
const code = nativeCodeForCause(entry.cause);
|
|
128
|
+
const codeText = code === undefined ? "" : ` (code ${code})`;
|
|
129
|
+
return `${entry.operation} ${entry.path}${codeText}: ${describeCause(entry.cause)}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function formatDurableMessage(input: DurableErrorInput): string {
|
|
133
|
+
const primary = formatFailureForMessage({
|
|
134
|
+
operation: input.operation,
|
|
135
|
+
path: input.path,
|
|
136
|
+
cause: input.cause,
|
|
137
|
+
});
|
|
138
|
+
const segments = [`Durable file operation failed: ${primary}`];
|
|
139
|
+
if (input.targetPath !== undefined) segments.push(`target: ${input.targetPath}`);
|
|
140
|
+
if (input.temporaryPath !== undefined) segments.push(`temporary: ${input.temporaryPath}`);
|
|
141
|
+
if (input.renameCompleted) {
|
|
142
|
+
const target = input.targetPath ?? input.path;
|
|
143
|
+
segments.push(`Replacement may already be visible at ${target}.`);
|
|
144
|
+
}
|
|
145
|
+
if (input.cleanupFailures.length > 0) {
|
|
146
|
+
const cleanupText = input.cleanupFailures.map(formatFailureForMessage).join("; ");
|
|
147
|
+
segments.push(`Cleanup failures: ${cleanupText}`);
|
|
148
|
+
}
|
|
149
|
+
return segments.join(" ");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function writeSyncClose(
|
|
153
|
+
handle: DurableWritableHandle,
|
|
154
|
+
path: string,
|
|
155
|
+
data: DurableData,
|
|
156
|
+
): Promise<WritableSequenceResult> {
|
|
157
|
+
const cleanupFailures: DurableFailure[] = [];
|
|
158
|
+
let primaryFailure: DurableFailure | undefined;
|
|
159
|
+
try {
|
|
160
|
+
await handle.writeFile(data);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
primaryFailure = failure("write_file", path, error);
|
|
163
|
+
}
|
|
164
|
+
if (primaryFailure === undefined) {
|
|
165
|
+
try {
|
|
166
|
+
await handle.sync();
|
|
167
|
+
} catch (error) {
|
|
168
|
+
primaryFailure = failure("sync_file", path, error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
await handle.close();
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const closeFailure = failure("close_file", path, error);
|
|
175
|
+
if (primaryFailure === undefined) primaryFailure = closeFailure;
|
|
176
|
+
else cleanupFailures.push(closeFailure);
|
|
177
|
+
}
|
|
178
|
+
return { primaryFailure, cleanupFailures };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function syncCloseDirectory(handle: DurableDirectoryHandle, path: string): Promise<WritableSequenceResult> {
|
|
182
|
+
const cleanupFailures: DurableFailure[] = [];
|
|
183
|
+
let primaryFailure: DurableFailure | undefined;
|
|
184
|
+
try {
|
|
185
|
+
await handle.sync();
|
|
186
|
+
} catch (error) {
|
|
187
|
+
primaryFailure = failure("sync_directory", path, error);
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
await handle.close();
|
|
191
|
+
} catch (error) {
|
|
192
|
+
const closeFailure = failure("close_directory", path, error);
|
|
193
|
+
if (primaryFailure === undefined) primaryFailure = closeFailure;
|
|
194
|
+
else cleanupFailures.push(closeFailure);
|
|
195
|
+
}
|
|
196
|
+
return { primaryFailure, cleanupFailures };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function throwDurable(
|
|
200
|
+
primaryFailure: DurableFailure,
|
|
201
|
+
cleanupFailures: readonly DurableFailure[],
|
|
202
|
+
targetPath: string | undefined,
|
|
203
|
+
temporaryPath: string | undefined,
|
|
204
|
+
renameCompleted: boolean,
|
|
205
|
+
): never {
|
|
206
|
+
throw durableError({
|
|
207
|
+
operation: primaryFailure.operation,
|
|
208
|
+
path: primaryFailure.path,
|
|
209
|
+
cause: primaryFailure.cause,
|
|
210
|
+
cleanupFailures,
|
|
211
|
+
targetPath,
|
|
212
|
+
temporaryPath,
|
|
213
|
+
renameCompleted,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function removeTemporary(
|
|
218
|
+
operations: DurableFileOperations,
|
|
219
|
+
temporaryPath: string,
|
|
220
|
+
cleanupFailures: DurableFailure[],
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
try {
|
|
223
|
+
await operations.remove(temporaryPath);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
cleanupFailures.push(failure("remove_temp", temporaryPath, error));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function writeWithOperations(
|
|
230
|
+
operations: DurableFileOperations,
|
|
231
|
+
path: string,
|
|
232
|
+
data: DurableData,
|
|
233
|
+
): Promise<void> {
|
|
234
|
+
let handle: DurableWritableHandle;
|
|
235
|
+
try {
|
|
236
|
+
handle = await operations.openWritable(path, "w");
|
|
237
|
+
} catch (error) {
|
|
238
|
+
throwDurable(failure("open_file", path, error), [], path, undefined, false);
|
|
239
|
+
}
|
|
240
|
+
const result = await writeSyncClose(handle, path, data);
|
|
241
|
+
if (result.primaryFailure !== undefined) {
|
|
242
|
+
throwDurable(result.primaryFailure, result.cleanupFailures, path, undefined, false);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function syncDirectoryAfterRename(
|
|
247
|
+
operations: DurableFileOperations,
|
|
248
|
+
targetPath: string,
|
|
249
|
+
temporaryPath: string,
|
|
250
|
+
): Promise<void> {
|
|
251
|
+
if (operations.platform === "win32") return;
|
|
252
|
+
const directoryPath = dirname(targetPath);
|
|
253
|
+
let handle: DurableDirectoryHandle;
|
|
254
|
+
try {
|
|
255
|
+
handle = await operations.openDirectory(directoryPath);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
throwDurable(failure("open_directory", directoryPath, error), [], targetPath, temporaryPath, true);
|
|
258
|
+
}
|
|
259
|
+
const result = await syncCloseDirectory(handle, directoryPath);
|
|
260
|
+
if (result.primaryFailure !== undefined) {
|
|
261
|
+
throwDurable(result.primaryFailure, result.cleanupFailures, targetPath, temporaryPath, true);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function replaceWithOperations(
|
|
266
|
+
operations: DurableFileOperations,
|
|
267
|
+
path: string,
|
|
268
|
+
data: DurableData,
|
|
269
|
+
): Promise<void> {
|
|
270
|
+
const temporaryPath = operations.temporaryPath(path);
|
|
271
|
+
let handle: DurableWritableHandle;
|
|
272
|
+
try {
|
|
273
|
+
handle = await operations.openWritable(temporaryPath, "wx", 0o600);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
throwDurable(failure("open_file", temporaryPath, error), [], path, temporaryPath, false);
|
|
276
|
+
}
|
|
277
|
+
const writeResult = await writeSyncClose(handle, temporaryPath, data);
|
|
278
|
+
if (writeResult.primaryFailure !== undefined) {
|
|
279
|
+
await removeTemporary(operations, temporaryPath, writeResult.cleanupFailures);
|
|
280
|
+
throwDurable(writeResult.primaryFailure, writeResult.cleanupFailures, path, temporaryPath, false);
|
|
281
|
+
}
|
|
282
|
+
const renameError = await renameWithWindowsContention(operations, temporaryPath, path);
|
|
283
|
+
if (renameError !== undefined) {
|
|
284
|
+
const cleanupFailures: DurableFailure[] = [];
|
|
285
|
+
await removeTemporary(operations, temporaryPath, cleanupFailures);
|
|
286
|
+
throwDurable(failure("rename_file", path, renameError), cleanupFailures, path, temporaryPath, false);
|
|
287
|
+
}
|
|
288
|
+
await syncDirectoryAfterRename(operations, path, temporaryPath);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Windows sharing-violation codes for a replace-rename.
|
|
293
|
+
*
|
|
294
|
+
* POSIX `rename(2)` atomically replaces the target. Windows `MoveFileEx` fails
|
|
295
|
+
* with a sharing violation when another process momentarily holds the target
|
|
296
|
+
* open, including transient scanners, indexers, and concurrent writers. The
|
|
297
|
+
* operation is legitimate and simply needs to be reattempted.
|
|
298
|
+
*/
|
|
299
|
+
const WINDOWS_RENAME_CONTENTION_CODES: ReadonlySet<string> = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
300
|
+
|
|
301
|
+
const WINDOWS_RENAME_ATTEMPTS = 10;
|
|
302
|
+
const WINDOWS_RENAME_RETRY_DELAY_MS = 20;
|
|
303
|
+
|
|
304
|
+
function delay(ms: number): Promise<void> {
|
|
305
|
+
return new Promise((resolve) => {
|
|
306
|
+
setTimeout(resolve, ms);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Rename the temporary file over the target, retrying only Windows sharing
|
|
312
|
+
* violations.
|
|
313
|
+
*
|
|
314
|
+
* This is a bounded retry of a transient OS condition, not a fallback: the
|
|
315
|
+
* final failure is still returned and raised loudly, no alternative write path
|
|
316
|
+
* is taken, and no other platform or error code is retried.
|
|
317
|
+
*
|
|
318
|
+
* Returns the failure cause, or undefined on success.
|
|
319
|
+
*/
|
|
320
|
+
async function renameWithWindowsContention(
|
|
321
|
+
operations: DurableFileOperations,
|
|
322
|
+
temporaryPath: string,
|
|
323
|
+
targetPath: string,
|
|
324
|
+
): Promise<unknown> {
|
|
325
|
+
const attempts = operations.platform === "win32" ? WINDOWS_RENAME_ATTEMPTS : 1;
|
|
326
|
+
let lastError: unknown;
|
|
327
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
328
|
+
try {
|
|
329
|
+
await operations.rename(temporaryPath, targetPath);
|
|
330
|
+
return undefined;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
lastError = error;
|
|
333
|
+
const code = nativeCodeForCause(error);
|
|
334
|
+
const retryable =
|
|
335
|
+
operations.platform === "win32" && code !== undefined && WINDOWS_RENAME_CONTENTION_CODES.has(code);
|
|
336
|
+
if (!retryable || attempt === attempts) return error;
|
|
337
|
+
await delay(WINDOWS_RENAME_RETRY_DELAY_MS * attempt);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return lastError;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function temporaryPathForTarget(target: string): string {
|
|
344
|
+
return join(dirname(target), `.${basename(target)}.${String(process.pid)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const nodeOperations: DurableFileOperations = {
|
|
348
|
+
platform: process.platform,
|
|
349
|
+
async openWritable(path: string, flag: DurableWriteFlag, mode?: number) {
|
|
350
|
+
if (mode === undefined) return nodeOpen(path, flag);
|
|
351
|
+
return nodeOpen(path, flag, mode);
|
|
352
|
+
},
|
|
353
|
+
async openDirectory(path: string) {
|
|
354
|
+
return nodeOpen(path, "r");
|
|
355
|
+
},
|
|
356
|
+
async rename(source: string, target: string) {
|
|
357
|
+
await nodeRename(source, target);
|
|
358
|
+
},
|
|
359
|
+
async remove(path: string) {
|
|
360
|
+
// `force` ignores a missing path only. Permission and other failures still
|
|
361
|
+
// throw and are surfaced as `remove_temp` cleanup failures.
|
|
362
|
+
await nodeRm(path, { force: true });
|
|
363
|
+
},
|
|
364
|
+
temporaryPath: temporaryPathForTarget,
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const defaultWriter = createDurableFileWriter(nodeOperations);
|
|
368
|
+
|
|
369
|
+
export function createDurableFileWriter(operations: DurableFileOperations): DurableFileWriter {
|
|
370
|
+
return {
|
|
371
|
+
async write(path: string, data: DurableData): Promise<void> {
|
|
372
|
+
await writeWithOperations(operations, path, data);
|
|
373
|
+
},
|
|
374
|
+
async replace(path: string, data: DurableData): Promise<void> {
|
|
375
|
+
await replaceWithOperations(operations, path, data);
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function writeFileDurable(path: string, data: DurableData): Promise<void> {
|
|
381
|
+
return defaultWriter.write(path, data);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function replaceFileDurable(path: string, data: DurableData): Promise<void> {
|
|
385
|
+
return defaultWriter.replace(path, data);
|
|
386
|
+
}
|