@procedures/fs 0.27.4 → 0.27.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 +17 -0
- package/README.md +31 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/files/atomic.d.ts +22 -3
- package/dist/files/atomic.d.ts.map +1 -1
- package/dist/files/atomic.js +152 -51
- package/dist/files/atomic.js.map +1 -1
- package/dist/files/edit.d.ts +2 -1
- package/dist/files/edit.d.ts.map +1 -1
- package/dist/files/edit.js.map +1 -1
- package/dist/files/index.d.ts +6 -6
- package/dist/files/index.d.ts.map +1 -1
- package/dist/files/index.js +6 -6
- package/dist/files/index.js.map +1 -1
- package/dist/files/ndjson-tail.d.ts +32 -6
- package/dist/files/ndjson-tail.d.ts.map +1 -1
- package/dist/files/ndjson-tail.js +167 -55
- package/dist/files/ndjson-tail.js.map +1 -1
- package/dist/files/steps.d.ts +18 -0
- package/dist/files/steps.d.ts.map +1 -0
- package/dist/files/steps.js +48 -0
- package/dist/files/steps.js.map +1 -0
- package/package.json +2 -2
- package/src/files/atomic.ts +203 -58
- package/src/files/edit.ts +2 -1
- package/src/files/index.ts +13 -6
- package/src/files/ndjson-tail.ts +210 -54
- package/src/files/steps.ts +48 -0
package/src/files/atomic.ts
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The atomic write of a file (roadmap O51).
|
|
2
|
+
* The atomic write of a file (roadmap O51 and O52).
|
|
3
3
|
*
|
|
4
4
|
* `writeFileAtomic()` writes the data to a new temporary file in the folder of the target. Then it
|
|
5
5
|
* renames the temporary file over the target. Thus a reader sees the old file or the whole new
|
|
6
|
-
* file, also after a crash.
|
|
6
|
+
* file, also after a crash. `writeFileAtomicSync()` does the same steps without a promise.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { randomBytes } from "node:crypto";
|
|
10
|
+
import {
|
|
11
|
+
chmodSync,
|
|
12
|
+
closeSync,
|
|
13
|
+
fchmodSync,
|
|
14
|
+
fsyncSync,
|
|
15
|
+
openSync,
|
|
16
|
+
realpathSync,
|
|
17
|
+
renameSync,
|
|
18
|
+
statSync,
|
|
19
|
+
unlinkSync,
|
|
20
|
+
writeFileSync,
|
|
21
|
+
} from "node:fs";
|
|
10
22
|
import { chmod, open, realpath, rename, stat, unlink, type FileHandle } from "node:fs/promises";
|
|
11
23
|
import { basename, dirname, join, resolve } from "node:path";
|
|
12
24
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
25
|
+
import { perform, runAsync, runSync } from "./steps.js";
|
|
13
26
|
|
|
14
|
-
/** The options of `writeFileAtomic()`. */
|
|
27
|
+
/** The options of `writeFileAtomic()` and `writeFileAtomicSync()`. */
|
|
15
28
|
export interface WriteFileAtomicOptions {
|
|
16
29
|
/** The encoding of a string `data`. The default is `utf8`. */
|
|
17
30
|
encoding?: BufferEncoding | undefined;
|
|
@@ -27,6 +40,15 @@ export interface WriteFileAtomicOptions {
|
|
|
27
40
|
* is 2000 on Windows and 0 on the other platforms.
|
|
28
41
|
*/
|
|
29
42
|
renameRetryMs?: number | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* The function that gives the name of the temporary file. It gets the absolute path of the target,
|
|
45
|
+
* after the function follows a symlink.
|
|
46
|
+
*
|
|
47
|
+
* The temporary file is in the folder of the target, so a name with `/` or `\` is an error. The
|
|
48
|
+
* default name is `.<name>.<16 hex digits>.tmp`, where `<name>` is the first 32 characters of the
|
|
49
|
+
* name of the target.
|
|
50
|
+
*/
|
|
51
|
+
tempName?: ((target: string) => string) | undefined;
|
|
30
52
|
}
|
|
31
53
|
|
|
32
54
|
/** On Windows, an antivirus or an indexer that holds the target open gives these errors for a short time. */
|
|
@@ -35,6 +57,30 @@ const LOCKED = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
|
35
57
|
/** A file system that cannot flush a folder gives these errors. The write is complete without the flush. */
|
|
36
58
|
const NO_FOLDER_SYNC = new Set(["EINVAL", "ENOTSUP", "EISDIR", "EBADF", "EPERM", "EACCES"]);
|
|
37
59
|
|
|
60
|
+
/** The permission bits that let the group and the others read a file. */
|
|
61
|
+
const OTHERS_READ = 0o044;
|
|
62
|
+
|
|
63
|
+
/** A file that the steps open: a `FileHandle` for the async form, a file descriptor for the sync form. */
|
|
64
|
+
type Handle = FileHandle | number;
|
|
65
|
+
|
|
66
|
+
/** One operation of the file system that the steps of a write need. */
|
|
67
|
+
type FileOp =
|
|
68
|
+
| { readonly op: "realpath"; readonly path: string }
|
|
69
|
+
| { readonly op: "mode"; readonly path: string }
|
|
70
|
+
| { readonly op: "create"; readonly path: string; readonly mode: number }
|
|
71
|
+
| { readonly op: "openFolder"; readonly path: string }
|
|
72
|
+
| { readonly op: "write"; readonly handle: Handle; readonly data: string | Uint8Array; readonly encoding: BufferEncoding }
|
|
73
|
+
| { readonly op: "fchmod"; readonly handle: Handle; readonly mode: number }
|
|
74
|
+
| { readonly op: "fsync"; readonly handle: Handle }
|
|
75
|
+
| { readonly op: "close"; readonly handle: Handle }
|
|
76
|
+
| { readonly op: "rename"; readonly from: string; readonly to: string }
|
|
77
|
+
| { readonly op: "unlink"; readonly path: string }
|
|
78
|
+
| { readonly op: "chmod"; readonly path: string; readonly mode: number }
|
|
79
|
+
| { readonly op: "sleep"; readonly ms: number };
|
|
80
|
+
|
|
81
|
+
/** The steps of a write. Each step yields one operation, and the runner gives back its result. */
|
|
82
|
+
type Steps<T> = Generator<FileOp, T, unknown>;
|
|
83
|
+
|
|
38
84
|
/** True on Windows. The function reads the platform at each call, so a test can change it. */
|
|
39
85
|
function isWindows(): boolean {
|
|
40
86
|
return process.platform === "win32";
|
|
@@ -52,10 +98,10 @@ function isMissing(error: unknown): boolean {
|
|
|
52
98
|
}
|
|
53
99
|
|
|
54
100
|
/** The absolute path of the file to replace. The function follows a symlink, so the link stays a link. */
|
|
55
|
-
|
|
101
|
+
function* targetOf(path: string): Steps<string> {
|
|
56
102
|
const absolute = resolve(path);
|
|
57
103
|
try {
|
|
58
|
-
return
|
|
104
|
+
return yield* perform<FileOp, string>({ op: "realpath", path: absolute });
|
|
59
105
|
} catch (error) {
|
|
60
106
|
if (isMissing(error)) return absolute;
|
|
61
107
|
throw error;
|
|
@@ -63,53 +109,75 @@ async function targetOf(path: string): Promise<string> {
|
|
|
63
109
|
}
|
|
64
110
|
|
|
65
111
|
/** The permission bits of the target, or undefined when the target does not exist. */
|
|
66
|
-
|
|
112
|
+
function* modeOf(target: string): Steps<number | undefined> {
|
|
67
113
|
try {
|
|
68
|
-
return (
|
|
114
|
+
return yield* perform<FileOp, number>({ op: "mode", path: target });
|
|
69
115
|
} catch (error) {
|
|
70
116
|
if (isMissing(error)) return undefined;
|
|
71
117
|
throw error;
|
|
72
118
|
}
|
|
73
119
|
}
|
|
74
120
|
|
|
121
|
+
/** The `ERR_INVALID_ARG_VALUE` error of a name of a temporary file that is not in the folder of the target. */
|
|
122
|
+
function badTempName(name: unknown): TypeError {
|
|
123
|
+
const error = new TypeError(
|
|
124
|
+
`The option tempName gave ${JSON.stringify(name)}. Give the name of a new file in the folder of the target, without "/" or "\\".`,
|
|
125
|
+
);
|
|
126
|
+
return Object.assign(error, { code: "ERR_INVALID_ARG_VALUE" });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A new path next to the target. The default name starts with a dot and ends in `.tmp`. Thus a
|
|
131
|
+
* reader that looks for the extension of the target does not see it. The name keeps 32 characters
|
|
132
|
+
* of the target, so it does not become too long.
|
|
133
|
+
*/
|
|
134
|
+
function tempPathOf(target: string, tempName: WriteFileAtomicOptions["tempName"]): string {
|
|
135
|
+
const own = basename(target);
|
|
136
|
+
if (!tempName) return join(dirname(target), `.${[...own].slice(0, 32).join("")}.${randomBytes(8).toString("hex")}.tmp`);
|
|
137
|
+
const name: unknown = tempName(target);
|
|
138
|
+
if (typeof name !== "string" || name === "" || name === "." || name === ".." || name === own || /[\\/]/.test(name)) {
|
|
139
|
+
throw badTempName(name);
|
|
140
|
+
}
|
|
141
|
+
return join(dirname(target), name);
|
|
142
|
+
}
|
|
143
|
+
|
|
75
144
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* target, so it does not become too long.
|
|
145
|
+
* The permissions of the new temporary file. When the final file hides its data from the group and
|
|
146
|
+
* the others, the temporary file gets `0o600` from the start. Then no other user can read it.
|
|
79
147
|
*/
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
return
|
|
148
|
+
function createModeOf(mode: number | undefined): number {
|
|
149
|
+
if (mode === undefined) return 0o666;
|
|
150
|
+
return (mode & OTHERS_READ) === 0 ? 0o600 : mode;
|
|
83
151
|
}
|
|
84
152
|
|
|
85
153
|
/**
|
|
86
154
|
* The rename, tried again with a longer wait each time until `retryMs` is over. A random part of
|
|
87
155
|
* each wait keeps two writers of one target out of step.
|
|
88
156
|
*/
|
|
89
|
-
|
|
157
|
+
function* renameWithRetry(from: string, to: string, retryMs: number): Steps<void> {
|
|
90
158
|
const deadline = Date.now() + retryMs;
|
|
91
159
|
for (let step = 10; ; step = Math.min(step * 2, 100)) {
|
|
92
160
|
try {
|
|
93
|
-
|
|
161
|
+
yield { op: "rename", from, to };
|
|
94
162
|
return;
|
|
95
163
|
} catch (error) {
|
|
96
164
|
const delay = Math.round(step * (0.5 + Math.random()));
|
|
97
165
|
if (!LOCKED.has(codeOf(error)) || Date.now() + delay > deadline) throw error;
|
|
98
|
-
|
|
166
|
+
yield { op: "sleep", ms: delay };
|
|
99
167
|
}
|
|
100
168
|
}
|
|
101
169
|
}
|
|
102
170
|
|
|
103
171
|
/** The removal of the temporary file after a failure. The original error is more important than an error here. */
|
|
104
|
-
|
|
172
|
+
function* removeTemp(temp: string): Steps<void> {
|
|
105
173
|
try {
|
|
106
|
-
|
|
174
|
+
yield { op: "unlink", path: temp };
|
|
107
175
|
} catch (error) {
|
|
108
176
|
if (codeOf(error) !== "EPERM") return;
|
|
109
177
|
// Windows does not remove a read-only file, so the function clears the flag and tries again
|
|
110
178
|
try {
|
|
111
|
-
|
|
112
|
-
|
|
179
|
+
yield { op: "chmod", path: temp, mode: 0o666 };
|
|
180
|
+
yield { op: "unlink", path: temp };
|
|
113
181
|
} catch {
|
|
114
182
|
// The temporary file stays. The error of the write is the error that the caller gets.
|
|
115
183
|
}
|
|
@@ -117,63 +185,140 @@ async function removeTemp(temp: string): Promise<void> {
|
|
|
117
185
|
}
|
|
118
186
|
|
|
119
187
|
/** The flush of the folder, so that the rename is on the disk. Windows cannot open a folder for it. */
|
|
120
|
-
|
|
188
|
+
function* syncFolder(folder: string): Steps<void> {
|
|
121
189
|
if (isWindows()) return;
|
|
122
|
-
let handle:
|
|
190
|
+
let handle: Handle | undefined;
|
|
123
191
|
try {
|
|
124
|
-
handle =
|
|
125
|
-
|
|
192
|
+
handle = yield* perform<FileOp, Handle>({ op: "openFolder", path: folder });
|
|
193
|
+
yield { op: "fsync", handle };
|
|
126
194
|
} catch (error) {
|
|
127
195
|
if (!NO_FOLDER_SYNC.has(codeOf(error))) throw error;
|
|
128
196
|
} finally {
|
|
129
|
-
|
|
197
|
+
if (handle !== undefined) yield { op: "close", handle };
|
|
130
198
|
}
|
|
131
199
|
}
|
|
132
200
|
|
|
133
|
-
/**
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
*
|
|
137
|
-
* - The function writes a new temporary file in the folder of the target and flushes it to the disk.
|
|
138
|
-
* - Then it renames the temporary file over the target and flushes the folder. Windows gets no flush of the folder.
|
|
139
|
-
* - On Windows, an open handle of the target blocks the rename. Then the function tries the rename again for `renameRetryMs`.
|
|
140
|
-
* - After a failure, the function removes the temporary file and throws the error.
|
|
141
|
-
* - The file keeps the permissions of the target, unless `options.mode` is set. It does not keep the owner.
|
|
142
|
-
* - When `path` is a symlink, the function replaces the target of the link.
|
|
143
|
-
*
|
|
144
|
-
* Two writes at the same time do not mix: the file gets the data of one of them. The folder must exist.
|
|
145
|
-
*/
|
|
146
|
-
export async function writeFileAtomic(
|
|
147
|
-
path: string,
|
|
148
|
-
data: string | Uint8Array,
|
|
149
|
-
options: WriteFileAtomicOptions = {},
|
|
150
|
-
): Promise<void> {
|
|
151
|
-
const target = await targetOf(path);
|
|
152
|
-
const mode = options.mode ?? (await modeOf(target));
|
|
201
|
+
/** The steps of `writeFileAtomic()` and `writeFileAtomicSync()`. */
|
|
202
|
+
function* writeSteps(path: string, data: string | Uint8Array, options: WriteFileAtomicOptions): Steps<void> {
|
|
203
|
+
const target = yield* targetOf(path);
|
|
204
|
+
const mode = options.mode ?? (yield* modeOf(target));
|
|
153
205
|
const flush = options.fsync ?? true;
|
|
154
|
-
const temp = tempPathOf(target);
|
|
206
|
+
const temp = tempPathOf(target, options.tempName);
|
|
155
207
|
let created = false;
|
|
156
|
-
let handle:
|
|
208
|
+
let handle: Handle | undefined;
|
|
157
209
|
try {
|
|
158
|
-
handle =
|
|
210
|
+
handle = yield* perform<FileOp, Handle>({ op: "create", path: temp, mode: createModeOf(mode) });
|
|
159
211
|
created = true;
|
|
160
|
-
|
|
161
|
-
// The umask changes the mode of open
|
|
162
|
-
if (mode !== undefined)
|
|
163
|
-
if (flush)
|
|
164
|
-
|
|
212
|
+
yield { op: "write", handle, data, encoding: options.encoding ?? "utf8" };
|
|
213
|
+
// The umask changes the mode of the open, and a secret file starts at 0o600, so the function sets the final mode
|
|
214
|
+
if (mode !== undefined) yield { op: "fchmod", handle, mode };
|
|
215
|
+
if (flush) yield { op: "fsync", handle };
|
|
216
|
+
yield { op: "close", handle };
|
|
165
217
|
handle = undefined;
|
|
166
|
-
|
|
218
|
+
yield* renameWithRetry(temp, target, options.renameRetryMs ?? (isWindows() ? 2000 : 0));
|
|
167
219
|
} catch (error) {
|
|
168
|
-
if (handle) {
|
|
220
|
+
if (handle !== undefined) {
|
|
169
221
|
try {
|
|
170
|
-
|
|
222
|
+
yield { op: "close", handle };
|
|
171
223
|
} catch {
|
|
172
224
|
// The error of the write is the error that the caller gets
|
|
173
225
|
}
|
|
174
226
|
}
|
|
175
|
-
if (created)
|
|
227
|
+
if (created) yield* removeTemp(temp);
|
|
176
228
|
throw error;
|
|
177
229
|
}
|
|
178
|
-
if (flush)
|
|
230
|
+
if (flush) yield* syncFolder(dirname(target));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The async operations of the file system, with `node:fs/promises`. */
|
|
234
|
+
async function doAsync(op: FileOp): Promise<unknown> {
|
|
235
|
+
switch (op.op) {
|
|
236
|
+
case "realpath":
|
|
237
|
+
return realpath(op.path);
|
|
238
|
+
case "mode":
|
|
239
|
+
return (await stat(op.path)).mode & 0o7777;
|
|
240
|
+
case "create":
|
|
241
|
+
return open(op.path, "wx", op.mode);
|
|
242
|
+
case "openFolder":
|
|
243
|
+
return open(op.path, "r");
|
|
244
|
+
case "write":
|
|
245
|
+
return (op.handle as FileHandle).writeFile(op.data, { encoding: op.encoding });
|
|
246
|
+
case "fchmod":
|
|
247
|
+
return (op.handle as FileHandle).chmod(op.mode);
|
|
248
|
+
case "fsync":
|
|
249
|
+
return (op.handle as FileHandle).sync();
|
|
250
|
+
case "close":
|
|
251
|
+
return (op.handle as FileHandle).close();
|
|
252
|
+
case "rename":
|
|
253
|
+
return rename(op.from, op.to);
|
|
254
|
+
case "unlink":
|
|
255
|
+
return unlink(op.path);
|
|
256
|
+
case "chmod":
|
|
257
|
+
return chmod(op.path, op.mode);
|
|
258
|
+
case "sleep":
|
|
259
|
+
return sleep(op.ms);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let sleeper: Int32Array | undefined;
|
|
264
|
+
|
|
265
|
+
/** The sync operations of the file system, with `node:fs`. A wait blocks the thread with `Atomics.wait()`. */
|
|
266
|
+
function doSync(op: FileOp): unknown {
|
|
267
|
+
switch (op.op) {
|
|
268
|
+
case "realpath":
|
|
269
|
+
return realpathSync.native(op.path);
|
|
270
|
+
case "mode":
|
|
271
|
+
return statSync(op.path).mode & 0o7777;
|
|
272
|
+
case "create":
|
|
273
|
+
return openSync(op.path, "wx", op.mode);
|
|
274
|
+
case "openFolder":
|
|
275
|
+
return openSync(op.path, "r");
|
|
276
|
+
case "write":
|
|
277
|
+
return writeFileSync(op.handle as number, op.data, { encoding: op.encoding });
|
|
278
|
+
case "fchmod":
|
|
279
|
+
return fchmodSync(op.handle as number, op.mode);
|
|
280
|
+
case "fsync":
|
|
281
|
+
return fsyncSync(op.handle as number);
|
|
282
|
+
case "close":
|
|
283
|
+
return closeSync(op.handle as number);
|
|
284
|
+
case "rename":
|
|
285
|
+
return renameSync(op.from, op.to);
|
|
286
|
+
case "unlink":
|
|
287
|
+
return unlinkSync(op.path);
|
|
288
|
+
case "chmod":
|
|
289
|
+
return chmodSync(op.path, op.mode);
|
|
290
|
+
case "sleep":
|
|
291
|
+
sleeper ??= new Int32Array(new SharedArrayBuffer(4));
|
|
292
|
+
return Atomics.wait(sleeper, 0, 0, op.ms);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* This function writes `data` to `path` in one step for each reader. A reader sees the old file or
|
|
298
|
+
* the whole new file. It does not see a part of a file at any time.
|
|
299
|
+
*
|
|
300
|
+
* - The function writes a new temporary file in the folder of the target and flushes it to the disk.
|
|
301
|
+
* - Then it renames the temporary file over the target and flushes the folder. Windows gets no flush of the folder.
|
|
302
|
+
* - On Windows, an open handle of the target blocks the rename. Then the function tries the rename again for `renameRetryMs`.
|
|
303
|
+
* - After a failure, the function removes the temporary file and throws the error.
|
|
304
|
+
* - The file keeps the permissions of the target, unless `options.mode` is set. It does not keep the owner.
|
|
305
|
+
* - When the group and the others cannot read the final file, the temporary file gets `0o600` before the write of the data.
|
|
306
|
+
* - When `path` is a symlink, the function replaces the target of the link.
|
|
307
|
+
*
|
|
308
|
+
* Two writes at the same time do not mix: the file gets the data of one of them. The folder must exist.
|
|
309
|
+
* The name of the temporary file is `.<name>.<16 hex digits>.tmp`, unless `options.tempName` gives a name.
|
|
310
|
+
*/
|
|
311
|
+
export function writeFileAtomic(path: string, data: string | Uint8Array, options: WriteFileAtomicOptions = {}): Promise<void> {
|
|
312
|
+
return runAsync(writeSteps(path, data, options), doAsync);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* This function is the sync form of `writeFileAtomic()`. It does the same steps, with the same
|
|
317
|
+
* options and the same errors.
|
|
318
|
+
*
|
|
319
|
+
* - It blocks the thread until the file is in place.
|
|
320
|
+
* - On Windows, a locked target can block the thread for `renameRetryMs` (2000 by default) while the function tries the rename again.
|
|
321
|
+
*/
|
|
322
|
+
export function writeFileAtomicSync(path: string, data: string | Uint8Array, options: WriteFileAtomicOptions = {}): void {
|
|
323
|
+
runSync(writeSteps(path, data, options), doSync);
|
|
179
324
|
}
|
package/src/files/edit.ts
CHANGED
|
@@ -62,7 +62,8 @@ async function readOrUndefined(path: string, encoding: BufferEncoding): Promise<
|
|
|
62
62
|
* - A transform that gives the same text, or `undefined`, writes nothing.
|
|
63
63
|
* - Two paths of the same file share the lock, also when the case of the letters is not the same on Windows.
|
|
64
64
|
*
|
|
65
|
-
* The lock is in the process. It does not stop another process. The options of `writeFileAtomic()` apply to the write
|
|
65
|
+
* The lock is in the process. It does not stop another process. The options of `writeFileAtomic()` apply to the write,
|
|
66
|
+
* also `tempName` and the `0o600` temporary file of a secret file.
|
|
66
67
|
*/
|
|
67
68
|
export function editFile(
|
|
68
69
|
path: string,
|
package/src/files/index.ts
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The file helpers of the package (roadmap O51): the `./files` entry. They are functions,
|
|
3
|
-
* procedures, and an import of this entry registers nothing.
|
|
2
|
+
* The file helpers of the package (roadmap O51 and O52): the `./files` entry. They are functions,
|
|
3
|
+
* not procedures, and an import of this entry registers nothing.
|
|
4
4
|
*
|
|
5
|
-
* - `writeFileAtomic()`
|
|
6
|
-
* - `healNdjsonTail()`
|
|
5
|
+
* - `writeFileAtomic()` and `writeFileAtomicSync()` write a file in one step for each reader.
|
|
6
|
+
* - `healNdjsonTail()` and `healNdjsonTailSync()` repair the end of an append-only NDJSON file after a crash.
|
|
7
7
|
* - `KeyedLock` and `withLock()` let one caller at a time hold a key.
|
|
8
8
|
* - `editFile()` changes a file, one edit at a time for each path.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
export { writeFileAtomic, type WriteFileAtomicOptions } from "./atomic.js";
|
|
12
|
-
export {
|
|
11
|
+
export { writeFileAtomic, writeFileAtomicSync, type WriteFileAtomicOptions } from "./atomic.js";
|
|
12
|
+
export {
|
|
13
|
+
healNdjsonTail,
|
|
14
|
+
healNdjsonTailSync,
|
|
15
|
+
NDJSON_LINE_TOO_LONG,
|
|
16
|
+
type HealNdjsonTailOptions,
|
|
17
|
+
type NdjsonTailAction,
|
|
18
|
+
type NdjsonTailResult,
|
|
19
|
+
} from "./ndjson-tail.js";
|
|
13
20
|
export {
|
|
14
21
|
KeyedLock,
|
|
15
22
|
withLock,
|