* Appends contents to a file-path for persistance. File contents are * encrypted before being saved to disk. Reads happen via the in-memory * lookup of the internal map. Appends to the same path are serialized. * * @param path The filepath to persist contents to * @param newContent A
(
path: string,
newContent: string,
shouldEncode: boolean,
maxEntries?: number,
)
| 34 | * @returns void |
| 35 | */ |
| 36 | public async append( |
| 37 | path: string, |
| 38 | newContent: string, |
| 39 | shouldEncode: boolean, |
| 40 | maxEntries?: number, |
| 41 | ): Promise<void> { |
| 42 | if ( |
| 43 | maxEntries !== undefined && |
| 44 | (!Number.isInteger(maxEntries) || maxEntries < 1) |
| 45 | ) { |
| 46 | throw new Error( |
| 47 | `maxEntries must be a positive integer when set, got "${maxEntries}"`, |
| 48 | ); |
| 49 | } |
| 50 | |
| 51 | const prior = this.writeChains.get(path) ?? Promise.resolve(); |
| 52 | const task = prior.then(async () => { |
| 53 | // Work on a copy — read() returns the live cached array, and the |
| 54 | // cache must only reflect the new entry once the disk write has |
| 55 | // succeeded, or a failed write leaves cache and file diverged. |
| 56 | const contents = [...(await this.read(path, shouldEncode))]; |
| 57 | |
| 58 | contents.push(newContent); |
| 59 | if (maxEntries && contents.length > maxEntries) { |
| 60 | contents.splice(0, contents.length - maxEntries); |
| 61 | } |
| 62 | |
| 63 | const encoded = shouldEncode |
| 64 | ? await encrypt(contents.join('\n'), this.currentAESKey) |
| 65 | : contents.join('\n'); |
| 66 | |
| 67 | await writeFile(path, encoded.toString()); |
| 68 | this.fsMap.set(path, contents); |
| 69 | await this.recordMtime(path); |
| 70 | }); |
| 71 | |
| 72 | // Keep the chain alive past failures so one bad write doesn't wedge |
| 73 | // every subsequent append to this path. |
| 74 | const chain = task.catch(() => undefined); |
| 75 | this.writeChains.set(path, chain); |
| 76 | // Reclaim the entry once this chain settles — unless a newer append for |
| 77 | // the same path has already replaced it — so the map doesn't retain one |
| 78 | // resolved promise per distinct path for the life of the process. |
| 79 | void chain.finally(() => { |
| 80 | if (this.writeChains.get(path) === chain) { |
| 81 | this.writeChains.delete(path); |
| 82 | } |
| 83 | }); |
| 84 | |
| 85 | return task; |
| 86 | } |
| 87 | |
| 88 | /** |
| 89 | * Reads contents from the local map, if any exist and the file hasn't |
no test coverage detected