pi-background-tasks 2.5.0 → 2.6.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/BACKGROUND-TASKS-INSTRUCTIONS.md +1 -1
- package/README.md +70 -25
- package/TESTING.md +42 -2
- package/TEST_PLAN.md +7 -4
- package/dist/extensions/anthropic-attribution-child.js +4 -0
- package/dist/extensions/anthropic-attribution-child.js.map +1 -0
- package/dist/extensions/anthropic-attribution.js +107 -0
- package/dist/extensions/anthropic-attribution.js.map +1 -0
- package/dist/extensions/background-tasks.js +2 -0
- package/dist/extensions/background-tasks.js.map +1 -0
- package/dist/extensions/delegate-child.js +2 -0
- package/dist/extensions/delegate-child.js.map +1 -0
- package/dist/extensions/fusion-child.js +2 -0
- package/dist/extensions/fusion-child.js.map +1 -0
- package/dist/package.json +5 -0
- package/dist/src/core/anthropic-attribution-path.js +24 -0
- package/dist/src/core/anthropic-attribution-path.js.map +1 -0
- package/dist/src/core/anthropic-attribution.js +2247 -0
- package/dist/src/core/anthropic-attribution.js.map +1 -0
- package/dist/src/core/attested-pi-contract.js +5 -0
- package/dist/src/core/attested-pi-contract.js.map +1 -0
- package/dist/src/core/attested-pi-run.js +749 -0
- package/dist/src/core/attested-pi-run.js.map +1 -0
- package/dist/src/core/canonical-json.js +19 -0
- package/dist/src/core/canonical-json.js.map +1 -0
- package/dist/src/core/common.js +787 -0
- package/dist/src/core/common.js.map +1 -0
- package/dist/src/core/config.js +78 -0
- package/dist/src/core/config.js.map +1 -0
- package/dist/src/core/context/parent-snapshot.js +75 -0
- package/dist/src/core/context/parent-snapshot.js.map +1 -0
- package/dist/src/core/context/token-budget.js +631 -0
- package/dist/src/core/context/token-budget.js.map +1 -0
- package/dist/src/core/context/visible-conversation-v2.js +390 -0
- package/dist/src/core/context/visible-conversation-v2.js.map +1 -0
- package/dist/src/core/delegate/artifacts.js +357 -0
- package/dist/src/core/delegate/artifacts.js.map +1 -0
- package/dist/src/core/delegate/budget.js +266 -0
- package/dist/src/core/delegate/budget.js.map +1 -0
- package/dist/src/core/delegate/facade-contract.js +7 -0
- package/dist/src/core/delegate/facade-contract.js.map +1 -0
- package/dist/src/core/delegate/hook-contract-evidence.json +18 -0
- package/dist/src/core/delegate/hook-contract.js +124 -0
- package/dist/src/core/delegate/hook-contract.js.map +1 -0
- package/dist/src/core/delegate/launch.js +366 -0
- package/dist/src/core/delegate/launch.js.map +1 -0
- package/dist/src/core/delegate/result-package.js +293 -0
- package/dist/src/core/delegate/result-package.js.map +1 -0
- package/dist/src/core/delegate/runner.js +398 -0
- package/dist/src/core/delegate/runner.js.map +1 -0
- package/dist/src/core/delegate/seed.js +334 -0
- package/dist/src/core/delegate/seed.js.map +1 -0
- package/dist/src/core/delegate/types.js +103 -0
- package/dist/src/core/delegate/types.js.map +1 -0
- package/dist/src/core/durable-fs.js +372 -0
- package/dist/src/core/durable-fs.js.map +1 -0
- package/dist/src/core/extension-api.js +382 -0
- package/dist/src/core/extension-api.js.map +1 -0
- package/dist/src/core/fusion/artifacts.js +654 -0
- package/dist/src/core/fusion/artifacts.js.map +1 -0
- package/dist/src/core/fusion/budget.js +949 -0
- package/dist/src/core/fusion/budget.js.map +1 -0
- package/dist/src/core/fusion/child-protocol.js +176 -0
- package/dist/src/core/fusion/child-protocol.js.map +1 -0
- package/dist/src/core/fusion/claude-cache.js +156 -0
- package/dist/src/core/fusion/claude-cache.js.map +1 -0
- package/dist/src/core/fusion/clean-context.js +64 -0
- package/dist/src/core/fusion/clean-context.js.map +1 -0
- package/dist/src/core/fusion/config.js +349 -0
- package/dist/src/core/fusion/config.js.map +1 -0
- package/dist/src/core/fusion/context.js +184 -0
- package/dist/src/core/fusion/context.js.map +1 -0
- package/dist/src/core/fusion/evaluation.js +696 -0
- package/dist/src/core/fusion/evaluation.js.map +1 -0
- package/dist/src/core/fusion/facade-contract.js +3 -0
- package/dist/src/core/fusion/facade-contract.js.map +1 -0
- package/dist/src/core/fusion/orchestrator.js +867 -0
- package/dist/src/core/fusion/orchestrator.js.map +1 -0
- package/dist/src/core/fusion/output-contract.js +27 -0
- package/dist/src/core/fusion/output-contract.js.map +1 -0
- package/dist/src/core/fusion/pi-child.js +1741 -0
- package/dist/src/core/fusion/pi-child.js.map +1 -0
- package/dist/src/core/fusion/prompts.js +260 -0
- package/dist/src/core/fusion/prompts.js.map +1 -0
- package/dist/src/core/fusion/result-package.js +857 -0
- package/dist/src/core/fusion/result-package.js.map +1 -0
- package/dist/src/core/fusion/source-policy.js +259 -0
- package/dist/src/core/fusion/source-policy.js.map +1 -0
- package/dist/src/core/fusion/types.js +207 -0
- package/dist/src/core/fusion/types.js.map +1 -0
- package/dist/src/core/fusion/web-fetch.js +777 -0
- package/dist/src/core/fusion/web-fetch.js.map +1 -0
- package/dist/src/core/fusion/workflows.js +126 -0
- package/dist/src/core/fusion/workflows.js.map +1 -0
- package/dist/src/core/lazy-module.js +181 -0
- package/dist/src/core/lazy-module.js.map +1 -0
- package/dist/src/core/pi-launch.js +431 -0
- package/dist/src/core/pi-launch.js.map +1 -0
- package/dist/src/core/registry.js +3331 -0
- package/dist/src/core/registry.js.map +1 -0
- package/dist/src/core/reload-shell-owner.js +1396 -0
- package/dist/src/core/reload-shell-owner.js.map +1 -0
- package/dist/src/core/shell-policy.js +80 -0
- package/dist/src/core/shell-policy.js.map +1 -0
- package/dist/src/core/task-durable.js +51 -0
- package/dist/src/core/task-durable.js.map +1 -0
- package/dist/src/core/update-check.js +92 -0
- package/dist/src/core/update-check.js.map +1 -0
- package/dist/src/core/windows-taskkill.js +185 -0
- package/dist/src/core/windows-taskkill.js.map +1 -0
- package/dist/src/delegate-child-extension.js +776 -0
- package/dist/src/delegate-child-extension.js.map +1 -0
- package/dist/src/delegate-extension.js +746 -0
- package/dist/src/delegate-extension.js.map +1 -0
- package/dist/src/extension.js +953 -0
- package/dist/src/extension.js.map +1 -0
- package/dist/src/fusion-child-extension.js +760 -0
- package/dist/src/fusion-child-extension.js.map +1 -0
- package/dist/src/fusion-extension.js +1030 -0
- package/dist/src/fusion-extension.js.map +1 -0
- package/dist/src/ui/background-tasks-manager.js +689 -0
- package/dist/src/ui/background-tasks-manager.js.map +1 -0
- package/dist/src/ui/fusion-model-selector.js +277 -0
- package/dist/src/ui/fusion-model-selector.js.map +1 -0
- package/docs/INDEX.md +35 -33
- package/docs/api/eventbus-v1.md +21 -5
- package/docs/choose-a-workflow.md +2 -0
- package/docs/commands/bg-clear.md +3 -3
- package/docs/commands/bg-update.md +3 -3
- package/docs/commands/bg.md +18 -6
- package/docs/commands/claude-cache.md +4 -4
- package/docs/commands/fusion-models.md +49 -4
- package/docs/commands/fusion.md +3 -3
- package/docs/commands/jobs.md +3 -3
- package/docs/commands/kill.md +3 -3
- package/docs/commands/logs.md +3 -3
- package/docs/commands/task-manager.md +5 -5
- package/docs/concepts/completion-delivery.md +6 -2
- package/docs/getting-started.md +7 -1
- package/docs/manifest.json +228 -42
- package/docs/operations/configuration.md +58 -5
- package/docs/operations/releasing.md +4 -2
- package/docs/operations/testing.md +22 -0
- package/docs/operations/troubleshooting.md +6 -2
- package/docs/read-before-edit.md +44 -31
- package/docs/reference/runtime-contracts.md +87 -51
- package/docs/reference/shortcuts-and-dock.md +28 -14
- package/docs/subsystems/anthropic-attribution.md +23 -11
- package/docs/subsystems/attested-pi-runs.md +12 -7
- package/docs/subsystems/background-task-runtime.md +53 -11
- package/docs/subsystems/child-launch-durability-and-safety.md +28 -9
- package/docs/subsystems/delegation.md +16 -3
- package/docs/subsystems/docs-freshness-gate.md +14 -7
- package/docs/subsystems/fusion.md +36 -11
- package/docs/subsystems/host-ui-and-telemetry.md +22 -9
- package/docs/tools/bg_delegate.md +3 -1
- package/docs/tools/bg_kill.md +3 -1
- package/docs/tools/bg_logs.md +3 -1
- package/docs/tools/bg_result.md +5 -3
- package/docs/tools/bg_run.md +31 -5
- package/docs/tools/bg_run_pi_attested.md +3 -1
- package/docs/tools/bg_status.md +3 -1
- package/docs/tools/fusion_investigate.md +3 -1
- package/docs/tools/fusion_reason.md +3 -1
- package/docs/tools/fusion_research.md +3 -1
- package/docs/tools/fusion_validate.md +3 -1
- package/extensions/anthropic-attribution-child.ts +3 -0
- package/extensions/anthropic-attribution.ts +166 -1
- package/package.json +12 -10
- package/src/core/anthropic-attribution-path.ts +5 -4
- package/src/core/anthropic-attribution.ts +293 -113
- package/src/core/attested-pi-contract.ts +4 -0
- package/src/core/attested-pi-run.ts +468 -81
- package/src/core/canonical-json.ts +21 -0
- package/src/core/common.ts +450 -40
- package/src/core/config.ts +121 -0
- package/src/core/context/visible-conversation-v2.ts +3 -6
- package/src/core/delegate/artifacts.ts +13 -9
- package/src/core/delegate/budget.ts +13 -6
- package/src/core/delegate/facade-contract.ts +6 -0
- package/src/core/delegate/result-package.ts +32 -14
- package/src/core/delegate/runner.ts +129 -58
- package/src/core/delegate/seed.ts +5 -7
- package/src/core/durable-fs.ts +178 -30
- package/src/core/extension-api.ts +36 -8
- package/src/core/fusion/artifacts.ts +17 -16
- package/src/core/fusion/clean-context.ts +8 -5
- package/src/core/fusion/config.ts +2 -1
- package/src/core/fusion/context.ts +2 -6
- package/src/core/fusion/facade-contract.ts +2 -0
- package/src/core/fusion/orchestrator.ts +3 -3
- package/src/core/fusion/prompts.ts +1 -1
- package/src/core/fusion/result-package.ts +259 -72
- package/src/core/fusion/source-policy.ts +43 -17
- package/src/core/lazy-module.ts +215 -0
- package/src/core/pi-launch.ts +390 -65
- package/src/core/registry.ts +2010 -381
- package/src/core/reload-shell-owner.ts +1662 -0
- package/src/core/shell-policy.ts +134 -0
- package/src/core/task-durable.ts +67 -0
- package/src/delegate-extension.ts +404 -144
- package/src/extension.ts +387 -114
- package/src/fusion-extension.ts +202 -96
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { open as nodeOpen, rename as nodeRename, rm as nodeRm } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, join } from 'node:path';
|
|
4
|
+
export class DurableFileCancellationError extends Error {
|
|
5
|
+
code = 'durable_file_cancelled';
|
|
6
|
+
path;
|
|
7
|
+
reason;
|
|
8
|
+
cleanupFailures;
|
|
9
|
+
targetPath;
|
|
10
|
+
temporaryPath;
|
|
11
|
+
renameCompleted;
|
|
12
|
+
constructor(input) {
|
|
13
|
+
const reasonText = describeCause(input.reason);
|
|
14
|
+
const cleanupText = input.cleanupFailures.length === 0
|
|
15
|
+
? ''
|
|
16
|
+
: ` Cleanup failures: ${input.cleanupFailures.map(formatFailureForMessage).join('; ')}`;
|
|
17
|
+
const commitText = input.renameCompleted
|
|
18
|
+
? ` Replacement may already be visible at ${input.targetPath ?? input.path}.`
|
|
19
|
+
: '';
|
|
20
|
+
super(`Durable file operation cancelled for ${input.path}: ${reasonText}.${commitText}${cleanupText}`);
|
|
21
|
+
this.name = 'DurableFileCancellationError';
|
|
22
|
+
this.path = input.path;
|
|
23
|
+
this.reason = input.reason;
|
|
24
|
+
this.cleanupFailures = [...input.cleanupFailures];
|
|
25
|
+
this.targetPath = input.targetPath;
|
|
26
|
+
this.temporaryPath = input.temporaryPath;
|
|
27
|
+
this.renameCompleted = input.renameCompleted;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class DurableFileError extends Error {
|
|
31
|
+
operation;
|
|
32
|
+
path;
|
|
33
|
+
nativeCode;
|
|
34
|
+
primaryCause;
|
|
35
|
+
cleanupFailures;
|
|
36
|
+
targetPath;
|
|
37
|
+
temporaryPath;
|
|
38
|
+
renameCompleted;
|
|
39
|
+
constructor(input) {
|
|
40
|
+
super(formatDurableMessage(input));
|
|
41
|
+
this.name = 'DurableFileError';
|
|
42
|
+
this.operation = input.operation;
|
|
43
|
+
this.path = input.path;
|
|
44
|
+
this.nativeCode = nativeCodeForCause(input.cause);
|
|
45
|
+
this.primaryCause = input.cause;
|
|
46
|
+
this.cleanupFailures = [...input.cleanupFailures];
|
|
47
|
+
this.targetPath = input.targetPath;
|
|
48
|
+
this.temporaryPath = input.temporaryPath;
|
|
49
|
+
this.renameCompleted = input.renameCompleted;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function failure(operation, path, cause) {
|
|
53
|
+
return { operation, path, cause };
|
|
54
|
+
}
|
|
55
|
+
function durableError(input) {
|
|
56
|
+
return new DurableFileError(input);
|
|
57
|
+
}
|
|
58
|
+
function nativeCodeForCause(cause) {
|
|
59
|
+
if (typeof cause !== 'object' || cause === null)
|
|
60
|
+
return undefined;
|
|
61
|
+
const code = Reflect.get(cause, 'code');
|
|
62
|
+
return typeof code === 'string' ? code : undefined;
|
|
63
|
+
}
|
|
64
|
+
function describeCause(cause) {
|
|
65
|
+
if (cause instanceof Error) {
|
|
66
|
+
const message = cause.message.length > 0 ? cause.message : String(cause);
|
|
67
|
+
return `${cause.name}: ${message}`;
|
|
68
|
+
}
|
|
69
|
+
return String(cause);
|
|
70
|
+
}
|
|
71
|
+
function formatFailureForMessage(entry) {
|
|
72
|
+
const code = nativeCodeForCause(entry.cause);
|
|
73
|
+
const codeText = code === undefined ? '' : ` (code ${code})`;
|
|
74
|
+
return `${entry.operation} ${entry.path}${codeText}: ${describeCause(entry.cause)}`;
|
|
75
|
+
}
|
|
76
|
+
function formatDurableMessage(input) {
|
|
77
|
+
const primary = formatFailureForMessage({
|
|
78
|
+
operation: input.operation,
|
|
79
|
+
path: input.path,
|
|
80
|
+
cause: input.cause,
|
|
81
|
+
});
|
|
82
|
+
const segments = [`Durable file operation failed: ${primary}`];
|
|
83
|
+
if (input.targetPath !== undefined)
|
|
84
|
+
segments.push(`target: ${input.targetPath}`);
|
|
85
|
+
if (input.temporaryPath !== undefined)
|
|
86
|
+
segments.push(`temporary: ${input.temporaryPath}`);
|
|
87
|
+
if (input.renameCompleted) {
|
|
88
|
+
const target = input.targetPath ?? input.path;
|
|
89
|
+
segments.push(`Replacement may already be visible at ${target}.`);
|
|
90
|
+
}
|
|
91
|
+
if (input.cleanupFailures.length > 0) {
|
|
92
|
+
const cleanupText = input.cleanupFailures.map(formatFailureForMessage).join('; ');
|
|
93
|
+
segments.push(`Cleanup failures: ${cleanupText}`);
|
|
94
|
+
}
|
|
95
|
+
return segments.join(' ');
|
|
96
|
+
}
|
|
97
|
+
async function writeSyncClose(handle, path, data, signal) {
|
|
98
|
+
const cleanupFailures = [];
|
|
99
|
+
let primaryFailure;
|
|
100
|
+
let cancelled = signal?.aborted === true;
|
|
101
|
+
if (!cancelled) {
|
|
102
|
+
try {
|
|
103
|
+
await handle.writeFile(data);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
primaryFailure = failure('write_file', path, error);
|
|
107
|
+
}
|
|
108
|
+
cancelled = signal?.aborted === true;
|
|
109
|
+
}
|
|
110
|
+
if (primaryFailure === undefined && !cancelled) {
|
|
111
|
+
try {
|
|
112
|
+
await handle.sync();
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
primaryFailure = failure('sync_file', path, error);
|
|
116
|
+
}
|
|
117
|
+
cancelled = signal?.aborted === true;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
await handle.close();
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
const closeFailure = failure('close_file', path, error);
|
|
124
|
+
if (primaryFailure === undefined)
|
|
125
|
+
primaryFailure = closeFailure;
|
|
126
|
+
else
|
|
127
|
+
cleanupFailures.push(closeFailure);
|
|
128
|
+
}
|
|
129
|
+
cancelled ||= signal?.aborted === true;
|
|
130
|
+
return { primaryFailure, cleanupFailures, cancelled };
|
|
131
|
+
}
|
|
132
|
+
async function syncCloseDirectory(handle, path) {
|
|
133
|
+
const cleanupFailures = [];
|
|
134
|
+
let primaryFailure;
|
|
135
|
+
try {
|
|
136
|
+
await handle.sync();
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
primaryFailure = failure('sync_directory', path, error);
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
await handle.close();
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
const closeFailure = failure('close_directory', path, error);
|
|
146
|
+
if (primaryFailure === undefined)
|
|
147
|
+
primaryFailure = closeFailure;
|
|
148
|
+
else
|
|
149
|
+
cleanupFailures.push(closeFailure);
|
|
150
|
+
}
|
|
151
|
+
return { primaryFailure, cleanupFailures, cancelled: false };
|
|
152
|
+
}
|
|
153
|
+
function cancellationReason(signal) {
|
|
154
|
+
return signal.reason ?? new Error('operation aborted');
|
|
155
|
+
}
|
|
156
|
+
function throwCancellation(signal, path, cleanupFailures, targetPath, temporaryPath, renameCompleted) {
|
|
157
|
+
const reason = cancellationReason(signal);
|
|
158
|
+
if (cleanupFailures.length === 0 && !renameCompleted && reason instanceof Error)
|
|
159
|
+
throw reason;
|
|
160
|
+
throw new DurableFileCancellationError({
|
|
161
|
+
path,
|
|
162
|
+
reason,
|
|
163
|
+
cleanupFailures,
|
|
164
|
+
targetPath,
|
|
165
|
+
temporaryPath,
|
|
166
|
+
renameCompleted,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function throwDurable(primaryFailure, cleanupFailures, targetPath, temporaryPath, renameCompleted) {
|
|
170
|
+
throw durableError({
|
|
171
|
+
operation: primaryFailure.operation,
|
|
172
|
+
path: primaryFailure.path,
|
|
173
|
+
cause: primaryFailure.cause,
|
|
174
|
+
cleanupFailures,
|
|
175
|
+
targetPath,
|
|
176
|
+
temporaryPath,
|
|
177
|
+
renameCompleted,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
async function removeTemporary(operations, temporaryPath, cleanupFailures) {
|
|
181
|
+
try {
|
|
182
|
+
await operations.remove(temporaryPath);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
cleanupFailures.push(failure('remove_temp', temporaryPath, error));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function isAborted(signal) {
|
|
189
|
+
return signal?.aborted === true;
|
|
190
|
+
}
|
|
191
|
+
async function writeWithOperations(operations, path, data, options) {
|
|
192
|
+
const signal = options.signal;
|
|
193
|
+
if (signal !== undefined && isAborted(signal))
|
|
194
|
+
throwCancellation(signal, path, [], path, undefined, false);
|
|
195
|
+
let handle;
|
|
196
|
+
try {
|
|
197
|
+
handle = await operations.openWritable(path, 'w');
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (signal !== undefined && isAborted(signal))
|
|
201
|
+
throwCancellation(signal, path, [], path, undefined, false);
|
|
202
|
+
throwDurable(failure('open_file', path, error), [], path, undefined, false);
|
|
203
|
+
}
|
|
204
|
+
const result = await writeSyncClose(handle, path, data, signal);
|
|
205
|
+
if (result.primaryFailure !== undefined) {
|
|
206
|
+
throwDurable(result.primaryFailure, result.cleanupFailures, path, undefined, false);
|
|
207
|
+
}
|
|
208
|
+
if (result.cancelled && signal !== undefined) {
|
|
209
|
+
throwCancellation(signal, path, result.cleanupFailures, path, undefined, false);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function syncDirectoryAfterRename(operations, targetPath, temporaryPath) {
|
|
213
|
+
if (operations.platform === 'win32')
|
|
214
|
+
return;
|
|
215
|
+
const directoryPath = dirname(targetPath);
|
|
216
|
+
let handle;
|
|
217
|
+
try {
|
|
218
|
+
handle = await operations.openDirectory(directoryPath);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
throwDurable(failure('open_directory', directoryPath, error), [], targetPath, temporaryPath, true);
|
|
222
|
+
}
|
|
223
|
+
const result = await syncCloseDirectory(handle, directoryPath);
|
|
224
|
+
if (result.primaryFailure !== undefined) {
|
|
225
|
+
throwDurable(result.primaryFailure, result.cleanupFailures, targetPath, temporaryPath, true);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function replaceWithOperations(operations, path, data, options) {
|
|
229
|
+
const signal = options.signal;
|
|
230
|
+
if (signal !== undefined && isAborted(signal))
|
|
231
|
+
throwCancellation(signal, path, [], path, undefined, false);
|
|
232
|
+
const temporaryPath = operations.temporaryPath(path);
|
|
233
|
+
let handle;
|
|
234
|
+
try {
|
|
235
|
+
handle = await operations.openWritable(temporaryPath, 'wx', 0o600);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
if (signal !== undefined && isAborted(signal))
|
|
239
|
+
throwCancellation(signal, temporaryPath, [], path, temporaryPath, false);
|
|
240
|
+
throwDurable(failure('open_file', temporaryPath, error), [], path, temporaryPath, false);
|
|
241
|
+
}
|
|
242
|
+
const writeResult = await writeSyncClose(handle, temporaryPath, data, signal);
|
|
243
|
+
if (writeResult.primaryFailure !== undefined) {
|
|
244
|
+
await removeTemporary(operations, temporaryPath, writeResult.cleanupFailures);
|
|
245
|
+
throwDurable(writeResult.primaryFailure, writeResult.cleanupFailures, path, temporaryPath, false);
|
|
246
|
+
}
|
|
247
|
+
if (writeResult.cancelled && signal !== undefined) {
|
|
248
|
+
await removeTemporary(operations, temporaryPath, writeResult.cleanupFailures);
|
|
249
|
+
throwCancellation(signal, temporaryPath, writeResult.cleanupFailures, path, temporaryPath, false);
|
|
250
|
+
}
|
|
251
|
+
const renameResult = await renameWithWindowsContention(operations, temporaryPath, path, signal);
|
|
252
|
+
if (renameResult.cancelled && signal !== undefined) {
|
|
253
|
+
const cleanupFailures = [];
|
|
254
|
+
await removeTemporary(operations, temporaryPath, cleanupFailures);
|
|
255
|
+
throwCancellation(signal, temporaryPath, cleanupFailures, path, temporaryPath, false);
|
|
256
|
+
}
|
|
257
|
+
if (renameResult.error !== undefined) {
|
|
258
|
+
const cleanupFailures = [];
|
|
259
|
+
await removeTemporary(operations, temporaryPath, cleanupFailures);
|
|
260
|
+
throwDurable(failure('rename_file', path, renameResult.error), cleanupFailures, path, temporaryPath, false);
|
|
261
|
+
}
|
|
262
|
+
await syncDirectoryAfterRename(operations, path, temporaryPath);
|
|
263
|
+
if (signal !== undefined && isAborted(signal)) {
|
|
264
|
+
throwCancellation(signal, path, [], path, temporaryPath, true);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Windows sharing-violation codes for a replace-rename.
|
|
269
|
+
*
|
|
270
|
+
* POSIX `rename(2)` atomically replaces the target. Windows `MoveFileEx` fails
|
|
271
|
+
* with a sharing violation when another process momentarily holds the target
|
|
272
|
+
* open, including transient scanners, indexers, and concurrent writers. The
|
|
273
|
+
* operation is legitimate and simply needs to be reattempted.
|
|
274
|
+
*/
|
|
275
|
+
const WINDOWS_RENAME_CONTENTION_CODES = new Set([
|
|
276
|
+
'EPERM',
|
|
277
|
+
'EACCES',
|
|
278
|
+
'EBUSY',
|
|
279
|
+
]);
|
|
280
|
+
const WINDOWS_RENAME_ATTEMPTS = 10;
|
|
281
|
+
const WINDOWS_RENAME_RETRY_DELAY_MS = 20;
|
|
282
|
+
function delay(ms, signal) {
|
|
283
|
+
if (isAborted(signal))
|
|
284
|
+
return Promise.resolve();
|
|
285
|
+
return new Promise((resolve) => {
|
|
286
|
+
const timer = setTimeout(() => {
|
|
287
|
+
signal?.removeEventListener('abort', aborted);
|
|
288
|
+
resolve();
|
|
289
|
+
}, ms);
|
|
290
|
+
const aborted = () => {
|
|
291
|
+
clearTimeout(timer);
|
|
292
|
+
resolve();
|
|
293
|
+
};
|
|
294
|
+
signal?.addEventListener('abort', aborted, { once: true });
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Rename the temporary file over the target, retrying only Windows sharing
|
|
299
|
+
* violations.
|
|
300
|
+
*
|
|
301
|
+
* This is a bounded retry of a transient OS condition, not a fallback: the
|
|
302
|
+
* final failure is still returned and raised loudly, no alternative write path
|
|
303
|
+
* is taken, and no other platform or error code is retried.
|
|
304
|
+
*
|
|
305
|
+
* Returns an explicit success/error/cancellation sequence result.
|
|
306
|
+
*/
|
|
307
|
+
async function renameWithWindowsContention(operations, temporaryPath, targetPath, signal) {
|
|
308
|
+
const attempts = operations.platform === 'win32' ? WINDOWS_RENAME_ATTEMPTS : 1;
|
|
309
|
+
let lastError;
|
|
310
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
311
|
+
if (isAborted(signal))
|
|
312
|
+
return { error: undefined, cancelled: true };
|
|
313
|
+
try {
|
|
314
|
+
await operations.rename(temporaryPath, targetPath);
|
|
315
|
+
return { error: undefined, cancelled: false };
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
lastError = error;
|
|
319
|
+
if (isAborted(signal))
|
|
320
|
+
return { error: undefined, cancelled: true };
|
|
321
|
+
const code = nativeCodeForCause(error);
|
|
322
|
+
const retryable = operations.platform === 'win32' &&
|
|
323
|
+
code !== undefined &&
|
|
324
|
+
WINDOWS_RENAME_CONTENTION_CODES.has(code);
|
|
325
|
+
if (!retryable || attempt === attempts)
|
|
326
|
+
return { error, cancelled: false };
|
|
327
|
+
await delay(WINDOWS_RENAME_RETRY_DELAY_MS * attempt, signal);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return { error: lastError, cancelled: false };
|
|
331
|
+
}
|
|
332
|
+
function temporaryPathForTarget(target) {
|
|
333
|
+
return join(dirname(target), `.${basename(target)}.${String(process.pid)}.${randomBytes(6).toString('hex')}.tmp`);
|
|
334
|
+
}
|
|
335
|
+
const nodeOperations = {
|
|
336
|
+
platform: process.platform,
|
|
337
|
+
async openWritable(path, flag, mode) {
|
|
338
|
+
if (mode === undefined)
|
|
339
|
+
return nodeOpen(path, flag);
|
|
340
|
+
return nodeOpen(path, flag, mode);
|
|
341
|
+
},
|
|
342
|
+
async openDirectory(path) {
|
|
343
|
+
return nodeOpen(path, 'r');
|
|
344
|
+
},
|
|
345
|
+
async rename(source, target) {
|
|
346
|
+
await nodeRename(source, target);
|
|
347
|
+
},
|
|
348
|
+
async remove(path) {
|
|
349
|
+
// `force` ignores a missing path only. Permission and other failures still
|
|
350
|
+
// throw and are surfaced as `remove_temp` cleanup failures.
|
|
351
|
+
await nodeRm(path, { force: true });
|
|
352
|
+
},
|
|
353
|
+
temporaryPath: temporaryPathForTarget,
|
|
354
|
+
};
|
|
355
|
+
const defaultWriter = createDurableFileWriter(nodeOperations);
|
|
356
|
+
export function createDurableFileWriter(operations) {
|
|
357
|
+
return {
|
|
358
|
+
async write(path, data, options = {}) {
|
|
359
|
+
await writeWithOperations(operations, path, data, options);
|
|
360
|
+
},
|
|
361
|
+
async replace(path, data, options = {}) {
|
|
362
|
+
await replaceWithOperations(operations, path, data, options);
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
export async function writeFileDurable(path, data, options = {}) {
|
|
367
|
+
await defaultWriter.write(path, data, options);
|
|
368
|
+
}
|
|
369
|
+
export async function replaceFileDurable(path, data, options = {}) {
|
|
370
|
+
await defaultWriter.replace(path, data, options);
|
|
371
|
+
}
|
|
372
|
+
//# sourceMappingURL=durable-fs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable-fs.js","sourceRoot":"","sources":["../../../src/core/durable-fs.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { open as nodeOpen, rename as nodeRename, rm as nodeRm } from 'node:fs/promises';\nimport { basename, dirname, join } from 'node:path';\n\nexport type DurableData = Buffer | string;\nexport type DurableWriteFlag = 'w' | 'wx';\nexport type DurableOperation =\n | 'open_file'\n | 'write_file'\n | 'sync_file'\n | 'close_file'\n | 'rename_file'\n | 'remove_temp'\n | 'open_directory'\n | 'sync_directory'\n | 'close_directory';\n\nexport interface DurableFailure {\n operation: DurableOperation;\n path: string;\n cause: unknown;\n}\n\nexport interface DurableWritableHandle {\n writeFile(data: DurableData): Promise<void>;\n sync(): Promise<void>;\n close(): Promise<void>;\n}\n\nexport interface DurableDirectoryHandle {\n sync(): Promise<void>;\n close(): Promise<void>;\n}\n\nexport interface DurableFileOperations {\n platform: NodeJS.Platform;\n openWritable(\n path: string,\n flag: DurableWriteFlag,\n mode?: number,\n ): Promise<DurableWritableHandle>;\n openDirectory(path: string): Promise<DurableDirectoryHandle>;\n rename(source: string, target: string): Promise<void>;\n remove(path: string): Promise<void>;\n temporaryPath(target: string): string;\n}\n\nexport interface DurableWriteOptions {\n readonly signal?: AbortSignal | undefined;\n}\n\nexport interface DurableFileWriter {\n write(path: string, data: DurableData, options?: DurableWriteOptions): Promise<void>;\n replace(path: string, data: DurableData, options?: DurableWriteOptions): Promise<void>;\n}\n\ninterface DurableErrorInput {\n operation: DurableOperation;\n path: string;\n cause: unknown;\n cleanupFailures: readonly DurableFailure[];\n targetPath: string | undefined;\n temporaryPath: string | undefined;\n renameCompleted: boolean;\n}\n\ninterface WritableSequenceResult {\n primaryFailure: DurableFailure | undefined;\n cleanupFailures: DurableFailure[];\n cancelled: boolean;\n}\n\ninterface RenameSequenceResult {\n error: unknown | undefined;\n cancelled: boolean;\n}\n\nexport class DurableFileCancellationError extends Error {\n readonly code = 'durable_file_cancelled';\n readonly path: string;\n readonly reason: unknown;\n readonly cleanupFailures: readonly DurableFailure[];\n readonly targetPath: string | undefined;\n readonly temporaryPath: string | undefined;\n readonly renameCompleted: boolean;\n\n constructor(input: {\n path: string;\n reason: unknown;\n cleanupFailures: readonly DurableFailure[];\n targetPath: string | undefined;\n temporaryPath: string | undefined;\n renameCompleted: boolean;\n }) {\n const reasonText = describeCause(input.reason);\n const cleanupText =\n input.cleanupFailures.length === 0\n ? ''\n : ` Cleanup failures: ${input.cleanupFailures.map(formatFailureForMessage).join('; ')}`;\n const commitText = input.renameCompleted\n ? ` Replacement may already be visible at ${input.targetPath ?? input.path}.`\n : '';\n super(`Durable file operation cancelled for ${input.path}: ${reasonText}.${commitText}${cleanupText}`);\n this.name = 'DurableFileCancellationError';\n this.path = input.path;\n this.reason = input.reason;\n this.cleanupFailures = [...input.cleanupFailures];\n this.targetPath = input.targetPath;\n this.temporaryPath = input.temporaryPath;\n this.renameCompleted = input.renameCompleted;\n }\n}\n\nexport class DurableFileError extends Error {\n readonly operation: DurableOperation;\n readonly path: string;\n readonly nativeCode: string | undefined;\n readonly primaryCause: unknown;\n readonly cleanupFailures: readonly DurableFailure[];\n readonly targetPath: string | undefined;\n readonly temporaryPath: string | undefined;\n readonly renameCompleted: boolean;\n\n constructor(input: {\n operation: DurableOperation;\n path: string;\n cause: unknown;\n cleanupFailures: readonly DurableFailure[];\n targetPath: string | undefined;\n temporaryPath: string | undefined;\n renameCompleted: boolean;\n }) {\n super(formatDurableMessage(input));\n this.name = 'DurableFileError';\n this.operation = input.operation;\n this.path = input.path;\n this.nativeCode = nativeCodeForCause(input.cause);\n this.primaryCause = input.cause;\n this.cleanupFailures = [...input.cleanupFailures];\n this.targetPath = input.targetPath;\n this.temporaryPath = input.temporaryPath;\n this.renameCompleted = input.renameCompleted;\n }\n}\n\nfunction failure(operation: DurableOperation, path: string, cause: unknown): DurableFailure {\n return { operation, path, cause };\n}\n\nfunction durableError(input: DurableErrorInput): DurableFileError {\n return new DurableFileError(input);\n}\n\nfunction nativeCodeForCause(cause: unknown): string | undefined {\n if (typeof cause !== 'object' || cause === null) return undefined;\n const code = Reflect.get(cause, 'code');\n return typeof code === 'string' ? code : undefined;\n}\n\nfunction describeCause(cause: unknown): string {\n if (cause instanceof Error) {\n const message = cause.message.length > 0 ? cause.message : String(cause);\n return `${cause.name}: ${message}`;\n }\n return String(cause);\n}\n\nfunction formatFailureForMessage(entry: DurableFailure): string {\n const code = nativeCodeForCause(entry.cause);\n const codeText = code === undefined ? '' : ` (code ${code})`;\n return `${entry.operation} ${entry.path}${codeText}: ${describeCause(entry.cause)}`;\n}\n\nfunction formatDurableMessage(input: DurableErrorInput): string {\n const primary = formatFailureForMessage({\n operation: input.operation,\n path: input.path,\n cause: input.cause,\n });\n const segments = [`Durable file operation failed: ${primary}`];\n if (input.targetPath !== undefined) segments.push(`target: ${input.targetPath}`);\n if (input.temporaryPath !== undefined) segments.push(`temporary: ${input.temporaryPath}`);\n if (input.renameCompleted) {\n const target = input.targetPath ?? input.path;\n segments.push(`Replacement may already be visible at ${target}.`);\n }\n if (input.cleanupFailures.length > 0) {\n const cleanupText = input.cleanupFailures.map(formatFailureForMessage).join('; ');\n segments.push(`Cleanup failures: ${cleanupText}`);\n }\n return segments.join(' ');\n}\n\nasync function writeSyncClose(\n handle: DurableWritableHandle,\n path: string,\n data: DurableData,\n signal?: AbortSignal,\n): Promise<WritableSequenceResult> {\n const cleanupFailures: DurableFailure[] = [];\n let primaryFailure: DurableFailure | undefined;\n let cancelled = signal?.aborted === true;\n if (!cancelled) {\n try {\n await handle.writeFile(data);\n } catch (error) {\n primaryFailure = failure('write_file', path, error);\n }\n cancelled = signal?.aborted === true;\n }\n if (primaryFailure === undefined && !cancelled) {\n try {\n await handle.sync();\n } catch (error) {\n primaryFailure = failure('sync_file', path, error);\n }\n cancelled = signal?.aborted === true;\n }\n try {\n await handle.close();\n } catch (error) {\n const closeFailure = failure('close_file', path, error);\n if (primaryFailure === undefined) primaryFailure = closeFailure;\n else cleanupFailures.push(closeFailure);\n }\n cancelled ||= signal?.aborted === true;\n return { primaryFailure, cleanupFailures, cancelled };\n}\n\nasync function syncCloseDirectory(\n handle: DurableDirectoryHandle,\n path: string,\n): Promise<WritableSequenceResult> {\n const cleanupFailures: DurableFailure[] = [];\n let primaryFailure: DurableFailure | undefined;\n try {\n await handle.sync();\n } catch (error) {\n primaryFailure = failure('sync_directory', path, error);\n }\n try {\n await handle.close();\n } catch (error) {\n const closeFailure = failure('close_directory', path, error);\n if (primaryFailure === undefined) primaryFailure = closeFailure;\n else cleanupFailures.push(closeFailure);\n }\n return { primaryFailure, cleanupFailures, cancelled: false };\n}\n\nfunction cancellationReason(signal: AbortSignal): unknown {\n return signal.reason ?? new Error('operation aborted');\n}\n\nfunction throwCancellation(\n signal: AbortSignal,\n path: string,\n cleanupFailures: readonly DurableFailure[],\n targetPath: string | undefined,\n temporaryPath: string | undefined,\n renameCompleted: boolean,\n): never {\n const reason = cancellationReason(signal);\n if (cleanupFailures.length === 0 && !renameCompleted && reason instanceof Error) throw reason;\n throw new DurableFileCancellationError({\n path,\n reason,\n cleanupFailures,\n targetPath,\n temporaryPath,\n renameCompleted,\n });\n}\n\nfunction throwDurable(\n primaryFailure: DurableFailure,\n cleanupFailures: readonly DurableFailure[],\n targetPath: string | undefined,\n temporaryPath: string | undefined,\n renameCompleted: boolean,\n): never {\n throw durableError({\n operation: primaryFailure.operation,\n path: primaryFailure.path,\n cause: primaryFailure.cause,\n cleanupFailures,\n targetPath,\n temporaryPath,\n renameCompleted,\n });\n}\n\nasync function removeTemporary(\n operations: DurableFileOperations,\n temporaryPath: string,\n cleanupFailures: DurableFailure[],\n): Promise<void> {\n try {\n await operations.remove(temporaryPath);\n } catch (error) {\n cleanupFailures.push(failure('remove_temp', temporaryPath, error));\n }\n}\n\nfunction isAborted(signal: AbortSignal | undefined): boolean {\n return signal?.aborted === true;\n}\n\nasync function writeWithOperations(\n operations: DurableFileOperations,\n path: string,\n data: DurableData,\n options: DurableWriteOptions,\n): Promise<void> {\n const signal = options.signal;\n if (signal !== undefined && isAborted(signal))\n throwCancellation(signal, path, [], path, undefined, false);\n let handle: DurableWritableHandle;\n try {\n handle = await operations.openWritable(path, 'w');\n } catch (error) {\n if (signal !== undefined && isAborted(signal))\n throwCancellation(signal, path, [], path, undefined, false);\n throwDurable(failure('open_file', path, error), [], path, undefined, false);\n }\n const result = await writeSyncClose(handle, path, data, signal);\n if (result.primaryFailure !== undefined) {\n throwDurable(result.primaryFailure, result.cleanupFailures, path, undefined, false);\n }\n if (result.cancelled && signal !== undefined) {\n throwCancellation(signal, path, result.cleanupFailures, path, undefined, false);\n }\n}\n\nasync function syncDirectoryAfterRename(\n operations: DurableFileOperations,\n targetPath: string,\n temporaryPath: string,\n): Promise<void> {\n if (operations.platform === 'win32') return;\n const directoryPath = dirname(targetPath);\n let handle: DurableDirectoryHandle;\n try {\n handle = await operations.openDirectory(directoryPath);\n } catch (error) {\n throwDurable(failure('open_directory', directoryPath, error), [], targetPath, temporaryPath, true);\n }\n const result = await syncCloseDirectory(handle, directoryPath);\n if (result.primaryFailure !== undefined) {\n throwDurable(result.primaryFailure, result.cleanupFailures, targetPath, temporaryPath, true);\n }\n}\n\nasync function replaceWithOperations(\n operations: DurableFileOperations,\n path: string,\n data: DurableData,\n options: DurableWriteOptions,\n): Promise<void> {\n const signal = options.signal;\n if (signal !== undefined && isAborted(signal))\n throwCancellation(signal, path, [], path, undefined, false);\n const temporaryPath = operations.temporaryPath(path);\n let handle: DurableWritableHandle;\n try {\n handle = await operations.openWritable(temporaryPath, 'wx', 0o600);\n } catch (error) {\n if (signal !== undefined && isAborted(signal))\n throwCancellation(signal, temporaryPath, [], path, temporaryPath, false);\n throwDurable(failure('open_file', temporaryPath, error), [], path, temporaryPath, false);\n }\n const writeResult = await writeSyncClose(handle, temporaryPath, data, signal);\n if (writeResult.primaryFailure !== undefined) {\n await removeTemporary(operations, temporaryPath, writeResult.cleanupFailures);\n throwDurable(writeResult.primaryFailure, writeResult.cleanupFailures, path, temporaryPath, false);\n }\n if (writeResult.cancelled && signal !== undefined) {\n await removeTemporary(operations, temporaryPath, writeResult.cleanupFailures);\n throwCancellation(\n signal,\n temporaryPath,\n writeResult.cleanupFailures,\n path,\n temporaryPath,\n false,\n );\n }\n const renameResult = await renameWithWindowsContention(\n operations,\n temporaryPath,\n path,\n signal,\n );\n if (renameResult.cancelled && signal !== undefined) {\n const cleanupFailures: DurableFailure[] = [];\n await removeTemporary(operations, temporaryPath, cleanupFailures);\n throwCancellation(signal, temporaryPath, cleanupFailures, path, temporaryPath, false);\n }\n if (renameResult.error !== undefined) {\n const cleanupFailures: DurableFailure[] = [];\n await removeTemporary(operations, temporaryPath, cleanupFailures);\n throwDurable(\n failure('rename_file', path, renameResult.error),\n cleanupFailures,\n path,\n temporaryPath,\n false,\n );\n }\n await syncDirectoryAfterRename(operations, path, temporaryPath);\n if (signal !== undefined && isAborted(signal)) {\n throwCancellation(signal, path, [], path, temporaryPath, true);\n }\n}\n\n/**\n * Windows sharing-violation codes for a replace-rename.\n *\n * POSIX `rename(2)` atomically replaces the target. Windows `MoveFileEx` fails\n * with a sharing violation when another process momentarily holds the target\n * open, including transient scanners, indexers, and concurrent writers. The\n * operation is legitimate and simply needs to be reattempted.\n */\nconst WINDOWS_RENAME_CONTENTION_CODES: ReadonlySet<string> = new Set([\n 'EPERM',\n 'EACCES',\n 'EBUSY',\n]);\n\nconst WINDOWS_RENAME_ATTEMPTS = 10;\nconst WINDOWS_RENAME_RETRY_DELAY_MS = 20;\n\nfunction delay(ms: number, signal?: AbortSignal): Promise<void> {\n if (isAborted(signal)) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', aborted);\n resolve();\n }, ms);\n const aborted = () => {\n clearTimeout(timer);\n resolve();\n };\n signal?.addEventListener('abort', aborted, { once: true });\n });\n}\n\n/**\n * Rename the temporary file over the target, retrying only Windows sharing\n * violations.\n *\n * This is a bounded retry of a transient OS condition, not a fallback: the\n * final failure is still returned and raised loudly, no alternative write path\n * is taken, and no other platform or error code is retried.\n *\n * Returns an explicit success/error/cancellation sequence result.\n */\nasync function renameWithWindowsContention(\n operations: DurableFileOperations,\n temporaryPath: string,\n targetPath: string,\n signal?: AbortSignal,\n): Promise<RenameSequenceResult> {\n const attempts = operations.platform === 'win32' ? WINDOWS_RENAME_ATTEMPTS : 1;\n let lastError: unknown;\n for (let attempt = 1; attempt <= attempts; attempt++) {\n if (isAborted(signal)) return { error: undefined, cancelled: true };\n try {\n await operations.rename(temporaryPath, targetPath);\n return { error: undefined, cancelled: false };\n } catch (error) {\n lastError = error;\n if (isAborted(signal)) return { error: undefined, cancelled: true };\n const code = nativeCodeForCause(error);\n const retryable =\n operations.platform === 'win32' &&\n code !== undefined &&\n WINDOWS_RENAME_CONTENTION_CODES.has(code);\n if (!retryable || attempt === attempts) return { error, cancelled: false };\n await delay(WINDOWS_RENAME_RETRY_DELAY_MS * attempt, signal);\n }\n }\n return { error: lastError, cancelled: false };\n}\n\nfunction temporaryPathForTarget(target: string): string {\n return join(\n dirname(target),\n `.${basename(target)}.${String(process.pid)}.${randomBytes(6).toString('hex')}.tmp`,\n );\n}\n\nconst nodeOperations: DurableFileOperations = {\n platform: process.platform,\n async openWritable(path: string, flag: DurableWriteFlag, mode?: number) {\n if (mode === undefined) return nodeOpen(path, flag);\n return nodeOpen(path, flag, mode);\n },\n async openDirectory(path: string) {\n return nodeOpen(path, 'r');\n },\n async rename(source: string, target: string) {\n await nodeRename(source, target);\n },\n async remove(path: string) {\n // `force` ignores a missing path only. Permission and other failures still\n // throw and are surfaced as `remove_temp` cleanup failures.\n await nodeRm(path, { force: true });\n },\n temporaryPath: temporaryPathForTarget,\n};\n\nconst defaultWriter = createDurableFileWriter(nodeOperations);\n\nexport function createDurableFileWriter(operations: DurableFileOperations): DurableFileWriter {\n return {\n async write(\n path: string,\n data: DurableData,\n options: DurableWriteOptions = {},\n ): Promise<void> {\n await writeWithOperations(operations, path, data, options);\n },\n async replace(\n path: string,\n data: DurableData,\n options: DurableWriteOptions = {},\n ): Promise<void> {\n await replaceWithOperations(operations, path, data, options);\n },\n };\n}\n\nexport async function writeFileDurable(\n path: string,\n data: DurableData,\n options: DurableWriteOptions = {},\n): Promise<void> {\n await defaultWriter.write(path, data, options);\n}\n\nexport async function replaceFileDurable(\n path: string,\n data: DurableData,\n options: DurableWriteOptions = {},\n): Promise<void> {\n await defaultWriter.replace(path, data, options);\n}\n"]}
|