@hachej/boring-files 0.4.45 → 0.4.47

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.
Files changed (2) hide show
  1. package/dist/workspace.js +37 -5
  2. package/package.json +1 -1
package/dist/workspace.js CHANGED
@@ -8,6 +8,7 @@ import { journalConnectionOf, sqliteBatchOf } from './sqlite-batch.js';
8
8
  import { isTemporary, mediaTypeOf, temporaryFileName } from './file-types.js';
9
9
  export { isTemporary } from './file-types.js';
10
10
  const sameBytes = (left, right) => left.length === right.length && left.every((byte, index) => byte === right[index]);
11
+ const ASIDE_FOLDER_MAX_AGE_MS = 60 * 60 * 1000;
11
12
  const queues = new WeakMap();
12
13
  /** One queue per file system object: two providers over the same `fs` share it. */
13
14
  function queueFor(fs) {
@@ -158,16 +159,40 @@ export function createWorkspaceProvider(options) {
158
159
  return { kind: 'file', bytes, revision: await blobRevision(bytes) };
159
160
  }
160
161
  /**
161
- * A folder exists through its files: the one a move or delete left empty goes, up to the first that still holds something. Best effort and
162
- * never recursive, so a file landing meanwhile stays; a file system that cannot remove a directory that way (Pi's Node one) keeps the folder.
162
+ * A folder exists through its files: the one a move or delete left empty goes, up to the first that still holds something. Best effort, and
163
+ * never recursive on the folder itself, so a file landing meanwhile stays. A file system that cannot remove a directory that way (Pi's Node one)
164
+ * has it renamed aside first: no new file can land there, and it is only deleted if it is still empty, otherwise it goes back.
163
165
  */
166
+ const removeAside = async (folder) => {
167
+ const aside = `${folder.slice(0, folder.lastIndexOf('/'))}/${temporaryFileName(randomUUID())}`;
168
+ const moved = await fs.renameFile(folder, aside, fsContext()).catch(() => undefined);
169
+ if (!moved?.ok)
170
+ return false;
171
+ const left = await fs.listDir(aside, fsContext()).catch(() => undefined);
172
+ if (left?.ok && !left.value.length)
173
+ return (await fs.remove(aside, { recursive: true }, fsContext()).catch(() => undefined))?.ok === true;
174
+ if ((await fs.renameFile(aside, folder, fsContext()).catch(() => undefined))?.ok)
175
+ return false;
176
+ // Another writer recreated the folder meanwhile: what landed joins it, entry by entry, and a name already taken there stays aside rather than overwrite.
177
+ const present = await fs.listDir(folder, fsContext()).catch(() => undefined);
178
+ if (!present?.ok || !left?.ok)
179
+ return false;
180
+ const taken = new Set(present.value.map(entry => entry.name));
181
+ for (const entry of left.value)
182
+ if (!taken.has(entry.name))
183
+ await fs.renameFile(entry.path, `${folder}/${entry.name}`, fsContext()).catch(() => undefined);
184
+ const rest = await fs.listDir(aside, fsContext()).catch(() => undefined);
185
+ if (rest?.ok && !rest.value.length)
186
+ await fs.remove(aside, { recursive: true }, fsContext()).catch(() => undefined);
187
+ return false;
188
+ };
164
189
  const pruneEmptied = async (path) => {
165
190
  for (let folder = path.slice(0, Math.max(0, path.lastIndexOf('/'))); folder; folder = folder.slice(0, Math.max(0, folder.lastIndexOf('/')))) {
166
191
  const listed = await fs.listDir(absolute(folder), fsContext()).catch(() => undefined);
167
192
  if (!listed?.ok || listed.value.length)
168
193
  return;
169
194
  const gone = await fs.remove(absolute(folder), { recursive: false }, fsContext()).catch(() => undefined);
170
- if (!gone?.ok)
195
+ if (!gone?.ok && !await removeAside(absolute(folder)))
171
196
  return;
172
197
  }
173
198
  };
@@ -180,8 +205,15 @@ export function createWorkspaceProvider(options) {
180
205
  return;
181
206
  for (const entry of listing.value) {
182
207
  if (entry.kind === 'directory') {
183
- if (entry.name !== '.git' && entry.name !== 'node_modules')
184
- await walk(entry.path);
208
+ if (entry.name === '.git' || entry.name === 'node_modules')
209
+ continue;
210
+ await walk(entry.path);
211
+ // A folder set aside by a process that died: removed only if nothing landed in it. Old enough that no running provider is still pruning it.
212
+ if (isTemporary(entry.path) && entry.mtimeMs < startedAt - ASIDE_FOLDER_MAX_AGE_MS) {
213
+ const left = await fs.listDir(entry.path, fsContext());
214
+ if (left.ok && !left.value.length)
215
+ await fs.remove(entry.path, { recursive: true }, fsContext());
216
+ }
185
217
  }
186
218
  else if (entry.kind === 'file' && isTemporary(entry.path) && entry.mtimeMs < startedAt)
187
219
  await fs.remove(entry.path, { force: true }, fsContext());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-files",
3
- "version": "0.4.45",
3
+ "version": "0.4.47",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "type": "module",