@procedures/fs 0.27.3 → 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.
@@ -1,39 +1,165 @@
1
1
  /**
2
- * The repair of the end of an append-only NDJSON file after a crash (roadmap O51).
2
+ * The repair of the end of an append-only NDJSON file after a crash (roadmap O51 and O52).
3
3
  *
4
4
  * A crash during an append can leave a part of a line at the end of the file. The next append
5
5
  * then puts its line after that part, and the file has a bad line in its middle.
6
- * `healNdjsonTail()` repairs the end before the next append.
6
+ * `healNdjsonTail()` and `healNdjsonTailSync()` repair the end before the next append.
7
7
  */
8
+ import { closeSync, fstatSync, fsyncSync, ftruncateSync, openSync, readSync, writeSync } from "node:fs";
8
9
  import { open } from "node:fs/promises";
10
+ import { perform, runAsync, runSync } from "./steps.js";
11
+ /** The code of the error of a last line that is longer than `maxLineBytes`. */
12
+ export const NDJSON_LINE_TOO_LONG = "ERR_NDJSON_LINE_TOO_LONG";
9
13
  const NEWLINE = 0x0a;
10
- const NEWLINE_BYTES = Uint8Array.of(NEWLINE);
14
+ const CARRIAGE_RETURN = 0x0d;
11
15
  const CHUNK_BYTES = 64 * 1024;
12
16
  const DEFAULT_MAX_LINE_BYTES = 16 * 1024 * 1024;
13
17
  const BOM = [0xef, 0xbb, 0xbf];
18
+ /** The bytes of the file from `position`, `length` bytes or fewer. */
19
+ function read(position, length) {
20
+ return perform({ op: "read", position, length });
21
+ }
22
+ /**
23
+ * The `RangeError` of a last line that is longer than `maxLineBytes`. It tells the length, the
24
+ * limit and the option. It has the fields `code`, `path`, `lineBytes` and `maxLineBytes`.
25
+ */
26
+ function lineTooLong(path, lineBytes, maxLineBytes) {
27
+ const error = new RangeError(`A line at the end of ${path} has ${lineBytes} bytes, more than the limit maxLineBytes (${maxLineBytes} bytes). ` +
28
+ `Set the option maxLineBytes to ${lineBytes} or more, or to Infinity for no limit. The file did not change.`);
29
+ return Object.assign(error, { code: NDJSON_LINE_TOO_LONG, path, lineBytes, maxLineBytes });
30
+ }
14
31
  /**
15
- * The bytes of the file from `position`, `length` bytes or fewer. A read of a local file gives all
16
- * the bytes up to the end of the file.
32
+ * The position after the last newline of the bytes from `lowest` to `end`, or -1 without a newline.
33
+ * The function reads back from `end`, one chunk at a time. It adds each chunk after the newline to
34
+ * the start of `keep`, when `keep` is set.
17
35
  */
18
- async function readAt(handle, position, length) {
19
- const buffer = Buffer.alloc(length);
20
- const { bytesRead } = await handle.read(buffer, 0, length, position);
21
- return buffer.subarray(0, bytesRead);
36
+ function* lineStartBack(end, lowest, keep) {
37
+ for (let to = end; to > lowest;) {
38
+ const from = Math.max(lowest, to - CHUNK_BYTES);
39
+ const chunk = yield* read(from, to - from);
40
+ const at = chunk.lastIndexOf(NEWLINE);
41
+ keep?.unshift(at === -1 ? chunk : chunk.subarray(at + 1));
42
+ if (at !== -1)
43
+ return from + at + 1;
44
+ to = from;
45
+ }
46
+ return -1;
22
47
  }
23
48
  /**
24
- * True when the bytes hold one complete JSON value. No part of an object or of an array is valid
49
+ * The line that ends at `end`, from the newline before it. The function reads no more than
50
+ * `maxLineBytes` and one byte back from `end`. A longer line is an error.
51
+ */
52
+ function* lineBefore(path, end, maxLineBytes) {
53
+ const lowest = Math.max(0, end - maxLineBytes - 1);
54
+ const parts = [];
55
+ const start = yield* lineStartBack(end, lowest, parts);
56
+ if (start !== -1 || lowest === 0)
57
+ return { start: Math.max(start, 0), bytes: Buffer.concat(parts) };
58
+ // Only to give the length in the error: the bytes before the window, without a copy in memory
59
+ const found = yield* lineStartBack(lowest, 0, undefined);
60
+ throw lineTooLong(path, end - Math.max(found, 0), maxLineBytes);
61
+ }
62
+ /** The bytes of a line without the byte order mark at the start of the file. */
63
+ function withoutBom(line) {
64
+ const { bytes } = line;
65
+ return line.start === 0 && BOM.every((byte, index) => bytes[index] === byte) ? bytes.subarray(BOM.length) : bytes;
66
+ }
67
+ /**
68
+ * True when the line holds one complete JSON value. No part of an object or of an array is valid
25
69
  * JSON, so the function cannot keep a part of such a record.
26
70
  */
27
- function isCompleteJson(bytes, atStart) {
28
- const text = atStart && BOM.every((byte, index) => bytes[index] === byte) ? bytes.subarray(BOM.length) : bytes;
71
+ function isCompleteJson(line) {
29
72
  try {
30
- JSON.parse(text.toString("utf8"));
73
+ JSON.parse(withoutBom(line).toString("utf8"));
31
74
  return true;
32
75
  }
33
76
  catch {
34
77
  return false;
35
78
  }
36
79
  }
80
+ /** The text of a complete line for `lastLine`: without the byte order mark and without a `\r` at the end. */
81
+ function textOf(line) {
82
+ const bytes = withoutBom(line);
83
+ return (bytes.at(-1) === CARRIAGE_RETURN ? bytes.subarray(0, -1) : bytes).toString("utf8");
84
+ }
85
+ /** The steps of `healNdjsonTail()` and `healNdjsonTailSync()` on the open file. */
86
+ function* healSteps(path, options) {
87
+ const maxLineBytes = options.maxLineBytes ?? DEFAULT_MAX_LINE_BYTES;
88
+ const flush = options.fsync ?? true;
89
+ const wantLast = options.lastLine === true;
90
+ // The text of the complete line that ends at the newline at `newline`
91
+ const completeLine = function* (newline) {
92
+ return wantLast ? { lastLine: textOf(yield* lineBefore(path, newline, maxLineBytes)) } : {};
93
+ };
94
+ const noLine = wantLast ? { lastLine: undefined } : {};
95
+ const size = yield* perform({ op: "size" });
96
+ if (size === 0)
97
+ return { action: "none", bytes: 0, size, ...noLine };
98
+ const [last] = yield* read(size - 1, 1);
99
+ if (last === NEWLINE)
100
+ return { action: "none", bytes: 0, size, ...(yield* completeLine(size - 1)) };
101
+ const line = yield* lineBefore(path, size, maxLineBytes);
102
+ if (isCompleteJson(line)) {
103
+ yield { op: "newline", position: size };
104
+ if (flush)
105
+ yield { op: "sync" };
106
+ return { action: "newline", bytes: 1, size: size + 1, ...(wantLast ? { lastLine: textOf(line) } : {}) };
107
+ }
108
+ // The line before the cut is the last line after it. The function reads it first, so an error changes nothing.
109
+ const before = line.start === 0 ? noLine : yield* completeLine(line.start - 1);
110
+ yield { op: "truncate", length: line.start };
111
+ if (flush)
112
+ yield { op: "sync" };
113
+ return { action: "truncated", bytes: size - line.start, size: line.start, ...before };
114
+ }
115
+ /** The result for a file that does not exist. */
116
+ function missing(options) {
117
+ return { action: "none", bytes: 0, size: 0, ...(options.lastLine === true ? { lastLine: undefined } : {}) };
118
+ }
119
+ /** True for the error of a file that does not exist. */
120
+ function isMissing(error) {
121
+ return error.code === "ENOENT";
122
+ }
123
+ const NEWLINE_BYTES = Uint8Array.of(NEWLINE);
124
+ /** The async operations on the open file, with a `FileHandle`. */
125
+ function asyncOn(handle) {
126
+ return async (op) => {
127
+ switch (op.op) {
128
+ case "size":
129
+ return (await handle.stat()).size;
130
+ case "read": {
131
+ const buffer = Buffer.alloc(op.length);
132
+ const { bytesRead } = await handle.read(buffer, 0, op.length, op.position);
133
+ return buffer.subarray(0, bytesRead);
134
+ }
135
+ case "newline":
136
+ return handle.write(NEWLINE_BYTES, 0, 1, op.position);
137
+ case "truncate":
138
+ return handle.truncate(op.length);
139
+ case "sync":
140
+ return handle.sync();
141
+ }
142
+ };
143
+ }
144
+ /** The sync operations on the open file, with a file descriptor. */
145
+ function syncOn(fd) {
146
+ return (op) => {
147
+ switch (op.op) {
148
+ case "size":
149
+ return fstatSync(fd).size;
150
+ case "read": {
151
+ const buffer = Buffer.alloc(op.length);
152
+ return buffer.subarray(0, readSync(fd, buffer, 0, op.length, op.position));
153
+ }
154
+ case "newline":
155
+ return writeSync(fd, NEWLINE_BYTES, 0, 1, op.position);
156
+ case "truncate":
157
+ return ftruncateSync(fd, op.length);
158
+ case "sync":
159
+ return fsyncSync(fd);
160
+ }
161
+ };
162
+ }
37
163
  /**
38
164
  * This function repairs the end of an append-only NDJSON file after a crash. Then the file ends in a newline, or it is empty.
39
165
  *
@@ -41,64 +167,50 @@ function isCompleteJson(bytes, atStart) {
41
167
  * - A last line that is not valid JSON is a part of a record. The function cuts it off with `ftruncate`.
42
168
  * - A last line that is valid JSON is a complete record without its newline. The function adds the newline.
43
169
  * - The function reads only the last line, from the end of the file. It does not read the whole file.
170
+ * - With `options.lastLine`, the result also gives the last complete line after the repair.
171
+ *
172
+ * A last line that is longer than `maxLineBytes` (16 MiB by default) is a `RangeError` with the code
173
+ * `ERR_NDJSON_LINE_TOO_LONG`. The file then does not change. Set `maxLineBytes: Infinity` for files with very long records.
44
174
  *
45
175
  * The records must be objects or arrays. A part of a number or of a literal can be valid JSON too.
46
176
  * The function does not lock the file. Start it before an append, in the same lock as the append.
47
177
  */
48
178
  export async function healNdjsonTail(path, options = {}) {
49
- const maxLineBytes = options.maxLineBytes ?? DEFAULT_MAX_LINE_BYTES;
50
179
  let handle;
51
180
  try {
52
181
  handle = await open(path, "r+");
53
182
  }
54
183
  catch (error) {
55
- if (error.code === "ENOENT")
56
- return { action: "none", bytes: 0, size: 0 };
184
+ if (isMissing(error))
185
+ return missing(options);
57
186
  throw error;
58
187
  }
59
188
  try {
60
- const { size } = await handle.stat();
61
- if (size === 0)
62
- return { action: "none", bytes: 0, size };
63
- const [last] = await readAt(handle, size - 1, 1);
64
- if (last === NEWLINE)
65
- return { action: "none", bytes: 0, size };
66
- // Back from the end, one chunk at a time, to the newline before the last line
67
- const lowest = Math.max(0, size - maxLineBytes - 1);
68
- const parts = [];
69
- let lineStart = -1;
70
- for (let end = size; end > lowest && lineStart === -1;) {
71
- const start = Math.max(lowest, end - CHUNK_BYTES);
72
- const chunk = await readAt(handle, start, end - start);
73
- const at = chunk.lastIndexOf(NEWLINE);
74
- if (at === -1) {
75
- parts.unshift(chunk);
76
- }
77
- else {
78
- parts.unshift(chunk.subarray(at + 1));
79
- lineStart = start + at + 1;
80
- }
81
- end = start;
82
- }
83
- if (lineStart === -1) {
84
- if (lowest > 0)
85
- throw new RangeError(`The last line of ${path} is longer than ${maxLineBytes} bytes (maxLineBytes)`);
86
- lineStart = 0;
87
- }
88
- const flush = options.fsync ?? true;
89
- if (isCompleteJson(Buffer.concat(parts), lineStart === 0)) {
90
- await handle.write(NEWLINE_BYTES, 0, 1, size);
91
- if (flush)
92
- await handle.sync();
93
- return { action: "newline", bytes: 1, size: size + 1 };
94
- }
95
- await handle.truncate(lineStart);
96
- if (flush)
97
- await handle.sync();
98
- return { action: "truncated", bytes: size - lineStart, size: lineStart };
189
+ return await runAsync(healSteps(path, options), asyncOn(handle));
99
190
  }
100
191
  finally {
101
192
  await handle.close();
102
193
  }
103
194
  }
195
+ /**
196
+ * This function is the sync form of `healNdjsonTail()`. It makes the same repairs, with the same
197
+ * options, the same result and the same errors. It blocks the thread until the repair is complete.
198
+ */
199
+ export function healNdjsonTailSync(path, options = {}) {
200
+ let fd;
201
+ try {
202
+ fd = openSync(path, "r+");
203
+ }
204
+ catch (error) {
205
+ if (isMissing(error))
206
+ return missing(options);
207
+ throw error;
208
+ }
209
+ try {
210
+ return runSync(healSteps(path, options), syncOn(fd));
211
+ }
212
+ finally {
213
+ closeSync(fd);
214
+ }
215
+ }
104
216
  //# sourceMappingURL=ndjson-tail.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ndjson-tail.js","sourceRoot":"","sources":["../../src/files/ndjson-tail.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ndjson-tail.js","sourceRoot":"","sources":["../../src/files/ndjson-tail.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The runners of the steps of a file helper (roadmap O52). A helper is one generator that yields
3
+ * each operation of the file system. `runSync()` and `runAsync()` do the operations, so the sync
4
+ * form and the async form of a helper share all their decisions.
5
+ */
6
+ /**
7
+ * The step that gives the result of `op`. The cast is safe because each runner gives the value
8
+ * that the operation of the step gives.
9
+ */
10
+ export declare function perform<Op, T>(op: Op): Generator<Op, T, unknown>;
11
+ /**
12
+ * This function does the steps with a sync `does`. An error of an operation goes back into the
13
+ * steps, so their `try` and `finally` blocks apply.
14
+ */
15
+ export declare function runSync<Op, T>(steps: Generator<Op, T, unknown>, does: (op: Op) => unknown): T;
16
+ /** This function does the steps with an async `does`, in the same way as `runSync()`. */
17
+ export declare function runAsync<Op, T>(steps: Generator<Op, T, unknown>, does: (op: Op) => Promise<unknown>): Promise<T>;
18
+ //# sourceMappingURL=steps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"steps.d.ts","sourceRoot":"","sources":["../../src/files/steps.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;GAGG;AACH,wBAAiB,OAAO,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,GAAG,SAAS,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,CAAC,CAEjE;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,GAAG,CAAC,CAa7F;AAED,yFAAyF;AACzF,wBAAsB,QAAQ,CAAC,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAatH"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The runners of the steps of a file helper (roadmap O52). A helper is one generator that yields
3
+ * each operation of the file system. `runSync()` and `runAsync()` do the operations, so the sync
4
+ * form and the async form of a helper share all their decisions.
5
+ */
6
+ /**
7
+ * The step that gives the result of `op`. The cast is safe because each runner gives the value
8
+ * that the operation of the step gives.
9
+ */
10
+ export function* perform(op) {
11
+ return (yield op);
12
+ }
13
+ /**
14
+ * This function does the steps with a sync `does`. An error of an operation goes back into the
15
+ * steps, so their `try` and `finally` blocks apply.
16
+ */
17
+ export function runSync(steps, does) {
18
+ let step = steps.next();
19
+ while (!step.done) {
20
+ let value;
21
+ try {
22
+ value = does(step.value);
23
+ }
24
+ catch (error) {
25
+ step = steps.throw(error);
26
+ continue;
27
+ }
28
+ step = steps.next(value);
29
+ }
30
+ return step.value;
31
+ }
32
+ /** This function does the steps with an async `does`, in the same way as `runSync()`. */
33
+ export async function runAsync(steps, does) {
34
+ let step = steps.next();
35
+ while (!step.done) {
36
+ let value;
37
+ try {
38
+ value = await does(step.value);
39
+ }
40
+ catch (error) {
41
+ step = steps.throw(error);
42
+ continue;
43
+ }
44
+ step = steps.next(value);
45
+ }
46
+ return step.value;
47
+ }
48
+ //# sourceMappingURL=steps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"steps.js","sourceRoot":"","sources":["../../src/files/steps.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;GAGG;AACH,MAAM,SAAS,CAAC,CAAC,OAAO,CAAQ,EAAM;IACpC,OAAO,CAAC,MAAM,EAAE,CAAM,CAAC;AACzB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAQ,KAAgC,EAAE,IAAyB;IACxF,IAAI,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IACxB,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,IAAI,KAAc,CAAC;QACnB,IAAI,CAAC;YACH,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAC1B,SAAS;QACX,CAAC;QACD,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC;AACpB,CAAC;AAED,yFAAyF;AACzF,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAQ,KAAgC,EAAE,IAAkC;IACxG,IAAI,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IACxB,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,IAAI,KAAc,CAAC;QACnB,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAC1B,SAAS;QACX,CAAC;QACD,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC;AACpB,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@procedures/fs",
4
- "version": "0.27.3",
4
+ "version": "0.27.5",
5
5
  "description": "File system procedures: read, write, copy, move, glob, exists, mkdir and JSON read and write, callable in process, over RPC or from Claude through MCP.",
6
6
  "keywords": [
7
7
  "filesystem",
@@ -69,7 +69,7 @@
69
69
  "procedures": "./dist/register.js"
70
70
  },
71
71
  "dependencies": {
72
- "@procedures/client": "0.27.3",
72
+ "@procedures/client": "0.27.5",
73
73
  "zod": "^4.6.5"
74
74
  },
75
75
  "devDependencies": {