@zenfs/core 2.5.8 → 2.6.1

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 (66) hide show
  1. package/dist/backends/cow.d.ts +17 -0
  2. package/dist/backends/cow.js +72 -25
  3. package/dist/backends/fetch.d.ts +4 -0
  4. package/dist/backends/fetch.js +7 -3
  5. package/dist/backends/single_buffer.d.ts +2 -2
  6. package/dist/backends/store/fs.d.ts +6 -6
  7. package/dist/backends/store/fs.js +10 -2
  8. package/dist/internal/devices.d.ts +6 -6
  9. package/dist/internal/index_fs.d.ts +4 -4
  10. package/dist/node/async.js +1 -1
  11. package/dist/node/dir.d.ts +3 -1
  12. package/dist/node/dir.js +23 -5
  13. package/dist/node/promises.d.ts +3 -1
  14. package/dist/node/promises.js +128 -89
  15. package/dist/node/streams.d.ts +4 -0
  16. package/dist/node/streams.js +9 -10
  17. package/dist/node/sync.js +71 -40
  18. package/dist/utils.d.ts +8 -1
  19. package/dist/utils.js +26 -7
  20. package/dist/vfs/async.js +224 -85
  21. package/dist/vfs/file.d.ts +13 -7
  22. package/dist/vfs/file.js +661 -357
  23. package/dist/vfs/shared.d.ts +1 -0
  24. package/dist/vfs/shared.js +18 -1
  25. package/dist/vfs/sync.js +214 -85
  26. package/dist/vfs/vcache.d.ts +66 -0
  27. package/dist/vfs/vcache.js +249 -0
  28. package/dist/vfs/vnode.d.ts +72 -0
  29. package/dist/vfs/vnode.js +215 -0
  30. package/dist/vfs/watchers.js +4 -2
  31. package/dist/vfs/xattr.js +2 -2
  32. package/eslint.shared.js +1 -1
  33. package/package.json +3 -3
  34. package/scripts/make-index.js +43 -16
  35. package/scripts/test.js +28 -21
  36. package/tests/backend/fetch.test.ts +24 -1
  37. package/tests/backend/port.test.ts +3 -3
  38. package/tests/backend/store.test.ts +49 -0
  39. package/tests/common.ts +60 -0
  40. package/tests/fetch/fetch.ts +6 -0
  41. package/tests/fetch/run.sh +2 -2
  42. package/tests/fetch/server.js +3 -3
  43. package/tests/fs/append.test.ts +5 -5
  44. package/tests/fs/concurrency.test.ts +90 -0
  45. package/tests/fs/dir.test.ts +13 -15
  46. package/tests/fs/directory.test.ts +32 -31
  47. package/tests/fs/errors.test.ts +151 -31
  48. package/tests/fs/exists.test.ts +4 -4
  49. package/tests/fs/links.test.ts +4 -11
  50. package/tests/fs/open.test.ts +4 -4
  51. package/tests/fs/permissions.test.ts +15 -15
  52. package/tests/fs/read.test.ts +9 -9
  53. package/tests/fs/readFile.test.ts +9 -9
  54. package/tests/fs/rename.test.ts +8 -8
  55. package/tests/fs/scaling.test.ts +4 -4
  56. package/tests/fs/stat.test.ts +20 -20
  57. package/tests/fs/streams.test.ts +9 -6
  58. package/tests/fs/tempdir.test.ts +33 -10
  59. package/tests/fs/times.test.ts +11 -11
  60. package/tests/fs/truncate.test.ts +6 -6
  61. package/tests/fs/watch.test.ts +107 -53
  62. package/tests/fs/write.test.ts +10 -10
  63. package/tests/fs/xattr.test.ts +3 -3
  64. package/tests/native/run.sh +3 -0
  65. package/tests/native/setup.ts +230 -0
  66. package/types/readable-stream.d.ts +2 -2
@@ -23,6 +23,7 @@ export declare function mount(this: V_Context, mountPoint: string, fs: FileSyste
23
23
  /**
24
24
  * Unmounts the file system at `mountPoint`.
25
25
  * @category Backends and Configuration
26
+ * @todo [BREAKING] make this `async` and `await` vcache sync
26
27
  */
27
28
  export declare function umount(this: V_Context, mountPoint: string): void;
28
29
  /**
@@ -9,6 +9,7 @@ import { credentialsAllowRoot } from '../internal/credentials.js';
9
9
  import { withExceptionContext } from '../internal/error.js';
10
10
  import { join, resolve } from '../path.js';
11
11
  import { normalizePath } from '../utils.js';
12
+ import { caches, VCache } from './vcache.js';
12
13
  /**
13
14
  * The map of mount points
14
15
  * @category Backends and Configuration
@@ -30,21 +31,37 @@ export function mount(mountPoint, fs) {
30
31
  throw err(withErrno('EINVAL', 'Mount point is already in use: ' + mountPoint));
31
32
  fs._mountPoint = mountPoint;
32
33
  mounts.set(mountPoint, fs);
34
+ if (!caches.has(fs.uuid))
35
+ caches.set(fs.uuid, new VCache(fs));
33
36
  info(`Mounted ${fs.name} on ${mountPoint}`);
34
37
  debug(`${fs.name} attributes: ${[...fs.attributes].map(([k, v]) => (v !== undefined && v !== null ? k + '=' + v : k)).join(', ')}`);
35
38
  }
36
39
  /**
37
40
  * Unmounts the file system at `mountPoint`.
38
41
  * @category Backends and Configuration
42
+ * @todo [BREAKING] make this `async` and `await` vcache sync
39
43
  */
40
44
  export function umount(mountPoint) {
41
45
  if (mountPoint[0] != '/')
42
46
  mountPoint = '/' + mountPoint;
43
47
  mountPoint = resolve.call(this, mountPoint);
44
- if (!mounts.has(mountPoint)) {
48
+ const fs = mounts.get(mountPoint);
49
+ if (!fs) {
45
50
  warn(mountPoint + ' is already unmounted');
46
51
  return;
47
52
  }
53
+ const _done = () => {
54
+ caches.delete(fs.uuid);
55
+ info('Synced all changes to ' + mountPoint);
56
+ };
57
+ try {
58
+ caches.get(fs.uuid)?.syncSync();
59
+ _done();
60
+ }
61
+ catch {
62
+ warn(`Could not flush to ${mountPoint} synchronously.`);
63
+ void caches.get(fs.uuid)?.sync().then(_done);
64
+ }
48
65
  mounts.delete(mountPoint);
49
66
  notice('Unmounted ' + mountPoint);
50
67
  }
package/dist/vfs/sync.js CHANGED
@@ -1,10 +1,64 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
1
53
  import { setUVMessage, UV } from 'kerium';
2
54
  import { decodeUTF8 } from 'utilium';
3
55
  import * as constants from '../constants.js';
4
56
  import { contextOf } from '../internal/contexts.js';
57
+ import { wrap } from '../internal/error.js';
5
58
  import { hasAccess, isDirectory, isSymbolicLink } from '../internal/inode.js';
6
59
  import { basename, dirname, join, parse, resolve as resolvePath } from '../path.js';
7
60
  import { normalizeMode, normalizePath } from '../utils.js';
61
+ import { cacheOf, lockPathSync } from './vcache.js';
8
62
  import { checkAccess } from './config.js';
9
63
  import { Dirent, ifToDt } from './dir.js';
10
64
  import { Handle } from './file.js';
@@ -23,8 +77,8 @@ export function resolve($, path, preserveSymlinks, extra) {
23
77
  that means we don't need to perform any resolution for parent directories. */
24
78
  try {
25
79
  const resolved = resolveMount(path, $);
26
- // Stat it to make sure it exists
27
- const stats = resolved.fs.statSync(resolved.path);
80
+ // Stat it to make sure it exists. The vnode cache takes precedence since it may have unsynced changes
81
+ const stats = cacheOf(resolved.fs).get(resolved.path)?.inode ?? resolved.fs.statSync(resolved.path);
28
82
  if (!isSymbolicLink(stats) || preserveSymlinks) {
29
83
  return { ...resolved, fullPath: path, stats };
30
84
  }
@@ -42,7 +96,7 @@ export function resolve($, path, preserveSymlinks, extra) {
42
96
  const resolved = resolveMount(maybePath, $);
43
97
  let stats;
44
98
  try {
45
- stats = resolved.fs.statSync(resolved.path);
99
+ stats = cacheOf(resolved.fs).get(resolved.path)?.inode ?? resolved.fs.statSync(resolved.path);
46
100
  }
47
101
  catch (e) {
48
102
  if (e.code === 'ENOENT')
@@ -72,45 +126,57 @@ export function open(path, opt) {
72
126
  // nothing
73
127
  }
74
128
  if (!stats) {
75
- if (!(flag & constants.O_CREAT)) {
76
- throw UV('ENOENT', 'open', path);
77
- }
78
- // Create the file
79
- const parentStats = fs.statSync(dirname(resolved));
80
- if (checkAccess && !hasAccess(this, parentStats, constants.W_OK)) {
81
- throw UV('EACCES', 'open', path);
129
+ const env_1 = { stack: [], error: void 0, hasError: false };
130
+ try {
131
+ if (!(flag & constants.O_CREAT)) {
132
+ throw UV('ENOENT', 'open', path);
133
+ }
134
+ // Create the file
135
+ const parentPath = dirname(resolved);
136
+ const parentStats = fs.statSync(parentPath);
137
+ if (checkAccess && !hasAccess(this, parentStats, constants.W_OK)) {
138
+ throw UV('EACCES', 'open', path);
139
+ }
140
+ if (!isDirectory(parentStats)) {
141
+ throw UV('ENOTDIR', 'open', path);
142
+ }
143
+ if (!opt.allowDirectory && mode & constants.S_IFDIR)
144
+ throw UV('EISDIR', 'open', path);
145
+ // Serialize entry creation with other operations on the parent directory
146
+ const _ = __addDisposableResource(env_1, lockPathSync(fs, parentPath, 'rw', parentStats), false);
147
+ const { euid: uid, egid: gid } = contextOf(this).credentials;
148
+ const inode = fs.createFileSync(resolved, {
149
+ mode,
150
+ uid: parentStats.mode & constants.S_ISUID ? parentStats.uid : uid,
151
+ gid: parentStats.mode & constants.S_ISGID ? parentStats.gid : gid,
152
+ });
153
+ // A new entry in the parent directory, which is a 'rename' event
154
+ emitChange(this, 'rename', path);
155
+ return new Handle(this, path, resolved, flag, cacheOf(fs).ref(resolved, inode));
82
156
  }
83
- if (!isDirectory(parentStats)) {
84
- throw UV('ENOTDIR', 'open', path);
157
+ catch (e_1) {
158
+ env_1.error = e_1;
159
+ env_1.hasError = true;
85
160
  }
86
- if (!opt.allowDirectory && mode & constants.S_IFDIR)
87
- throw UV('EISDIR', 'open', path);
88
- if (checkAccess && !hasAccess(this, parentStats, constants.W_OK)) {
89
- throw UV('EACCES', 'open', path);
161
+ finally {
162
+ __disposeResources(env_1);
90
163
  }
91
- const { euid: uid, egid: gid } = contextOf(this).credentials;
92
- const inode = fs.createFileSync(resolved, {
93
- mode,
94
- uid: parentStats.mode & constants.S_ISUID ? parentStats.uid : uid,
95
- gid: parentStats.mode & constants.S_ISGID ? parentStats.gid : gid,
96
- });
97
- return new Handle(this, path, fs, resolved, flag, inode);
98
164
  }
99
165
  if (checkAccess && (!hasAccess(this, stats, mode) || !hasAccess(this, stats, flags.toMode(flag)))) {
100
166
  throw UV('EACCES', 'open', path);
101
167
  }
102
168
  if (flag & constants.O_EXCL)
103
169
  throw UV('EEXIST', 'open', path);
104
- const file = new Handle(this, path, fs, resolved, flag, stats);
105
170
  if (!opt.allowDirectory && stats.mode & constants.S_IFDIR)
106
171
  throw UV('EISDIR', 'open', path);
172
+ const file = new Handle(this, path, resolved, flag, cacheOf(fs).ref(resolved, stats));
107
173
  if (flag & constants.O_TRUNC)
108
174
  file.truncateSync(0);
109
175
  return file;
110
176
  }
111
177
  export function readlink(path) {
112
178
  path = normalizePath(path);
113
- const { fs, stats, path: resolved } = resolve(this, path, true);
179
+ const { fs, stats, path: resolved } = resolve(this, path, true, { syscall: 'readlink' });
114
180
  if (!stats)
115
181
  throw UV('ENOENT', 'readlink', path);
116
182
  if (checkAccess && !hasAccess(this, stats, constants.R_OK))
@@ -128,15 +194,26 @@ export function mkdir(path, options = {}) {
128
194
  const { euid: uid, egid: gid } = contextOf(this).credentials;
129
195
  const { mode = 0o777, recursive } = options;
130
196
  const __create = (path, resolved, parent) => {
131
- if (checkAccess && !hasAccess(this, parent, constants.W_OK))
132
- throw UV('EACCES', 'mkdir', dirname(path));
133
- const inode = fs.mkdirSync(resolved, {
134
- mode,
135
- uid: parent.mode & constants.S_ISUID ? parent.uid : uid,
136
- gid: parent.mode & constants.S_ISGID ? parent.gid : gid,
137
- });
138
- emitChange(this, 'rename', path);
139
- return inode;
197
+ const env_2 = { stack: [], error: void 0, hasError: false };
198
+ try {
199
+ if (checkAccess && !hasAccess(this, parent, constants.W_OK))
200
+ throw UV('EACCES', 'mkdir', dirname(path));
201
+ const _ = __addDisposableResource(env_2, lockPathSync(fs, dirname(resolved), 'rw', parent), false);
202
+ const inode = wrap(fs, 'mkdirSync', { path, syscall: 'mkdir' })(resolved, {
203
+ mode,
204
+ uid: parent.mode & constants.S_ISUID ? parent.uid : uid,
205
+ gid: parent.mode & constants.S_ISGID ? parent.gid : gid,
206
+ });
207
+ emitChange(this, 'rename', path);
208
+ return inode;
209
+ }
210
+ catch (e_2) {
211
+ env_2.error = e_2;
212
+ env_2.hasError = true;
213
+ }
214
+ finally {
215
+ __disposeResources(env_2);
216
+ }
140
217
  };
141
218
  if (!recursive) {
142
219
  __create(path, resolved, fs.statSync(dirname(resolved)));
@@ -157,12 +234,27 @@ export function mkdir(path, options = {}) {
157
234
  export function readdir(path, options = {}) {
158
235
  path = normalizePath(path);
159
236
  const { fs, path: resolved } = resolve(this, path);
160
- const stats = fs.statSync(resolved);
237
+ // Node reports `readdir` failures as `scandir`
238
+ const stats = cacheOf(fs).get(resolved)?.inode ?? wrap(fs, 'statSync', { path, syscall: 'scandir' })(resolved);
161
239
  if (checkAccess && !hasAccess(this, stats, constants.R_OK))
162
- throw UV('EACCES', 'readdir', path);
240
+ throw UV('EACCES', 'scandir', path);
163
241
  if (!isDirectory(stats))
164
- throw UV('ENOTDIR', 'readdir', path);
165
- const entries = fs.readdirSync(resolved);
242
+ throw UV('ENOTDIR', 'scandir', path);
243
+ let entries;
244
+ {
245
+ const env_3 = { stack: [], error: void 0, hasError: false };
246
+ try {
247
+ const _ = __addDisposableResource(env_3, lockPathSync(fs, resolved, 'ro', stats), false);
248
+ entries = fs.readdirSync(resolved);
249
+ }
250
+ catch (e_3) {
251
+ env_3.error = e_3;
252
+ env_3.hasError = true;
253
+ }
254
+ finally {
255
+ __disposeResources(env_3);
256
+ }
257
+ }
166
258
  // Iterate over entries and handle recursive case if needed
167
259
  const values = [];
168
260
  const addEntry = (entry) => {
@@ -192,58 +284,94 @@ export function readdir(path, options = {}) {
192
284
  return values;
193
285
  }
194
286
  export function rename(oldPath, newPath) {
195
- oldPath = normalizePath(oldPath);
196
- newPath = normalizePath(newPath);
197
- const $ex = { syscall: 'rename', path: oldPath, dest: newPath };
198
- const src = resolve(this, oldPath, true, $ex);
199
- const dst = resolveMount(newPath, this, $ex);
200
- if (src.fs.uuid !== dst.fs.uuid)
201
- throw UV('EXDEV', $ex);
202
- if (dst.path.startsWith(src.path + '/'))
203
- throw UV('EBUSY', $ex);
204
- if (!src.stats)
205
- throw UV('ENOENT', $ex);
206
- const fs = src.fs;
207
- const oldParent = fs.statSync(dirname(src.path));
208
- const newParent = fs.statSync(dirname(dst.path));
209
- let newStats;
287
+ const env_4 = { stack: [], error: void 0, hasError: false };
210
288
  try {
211
- newStats = fs.statSync(dst.path);
289
+ oldPath = normalizePath(oldPath);
290
+ newPath = normalizePath(newPath);
291
+ const $ex = { syscall: 'rename', path: oldPath, dest: newPath };
292
+ const src = resolve(this, oldPath, true, $ex);
293
+ const dst = resolveMount(newPath, this, $ex);
294
+ if (src.fs.uuid !== dst.fs.uuid)
295
+ throw UV('EXDEV', $ex);
296
+ // A directory can not be moved inside itself
297
+ if (dst.path.startsWith(src.path + '/'))
298
+ throw UV('EINVAL', $ex);
299
+ if (!src.stats)
300
+ throw UV('ENOENT', $ex);
301
+ const fs = src.fs;
302
+ const oldParent = fs.statSync(dirname(src.path));
303
+ const newParent = fs.statSync(dirname(dst.path));
304
+ let newStats;
305
+ try {
306
+ newStats = fs.statSync(dst.path);
307
+ }
308
+ catch (e) {
309
+ if (e.code != 'ENOENT')
310
+ throw e;
311
+ }
312
+ if (checkAccess && (!hasAccess(this, oldParent, constants.R_OK) || !hasAccess(this, newParent, constants.W_OK)))
313
+ throw UV('EACCES', $ex);
314
+ if (newStats && !isDirectory(src.stats) && isDirectory(newStats))
315
+ throw UV('EISDIR', $ex);
316
+ if (newStats && isDirectory(src.stats) && !isDirectory(newStats))
317
+ throw UV('ENOTDIR', $ex);
318
+ const srcDir = dirname(src.path);
319
+ const dstDir = dirname(dst.path);
320
+ // Lock both parent directories, ordered by inode number to avoid ABBA deadlocks (like Linux's `lock_two_nondirectories`)
321
+ const parents = [
322
+ [srcDir, oldParent],
323
+ [dstDir, newParent],
324
+ ];
325
+ if (oldParent.ino > newParent.ino)
326
+ parents.reverse();
327
+ const _first = __addDisposableResource(env_4, lockPathSync(fs, parents[0][0], 'rw', parents[0][1]), false);
328
+ const _second = __addDisposableResource(env_4, oldParent.ino == newParent.ino ? null : lockPathSync(fs, parents[1][0], 'rw', parents[1][1]), false);
329
+ src.fs.renameSync(src.path, dst.path);
330
+ cacheOf(fs).rename(src.path, dst.path);
331
+ // Both names change which entries exist, so both are 'rename' events
332
+ emitChange(this, 'rename', oldPath);
333
+ emitChange(this, 'rename', newPath);
212
334
  }
213
- catch (e) {
214
- if (e.code != 'ENOENT')
215
- throw e;
335
+ catch (e_4) {
336
+ env_4.error = e_4;
337
+ env_4.hasError = true;
338
+ }
339
+ finally {
340
+ __disposeResources(env_4);
216
341
  }
217
- if (checkAccess && (!hasAccess(this, oldParent, constants.R_OK) || !hasAccess(this, newParent, constants.W_OK)))
218
- throw UV('EACCES', $ex);
219
- if (newStats && !isDirectory(src.stats) && isDirectory(newStats))
220
- throw UV('EISDIR', $ex);
221
- if (newStats && isDirectory(src.stats) && !isDirectory(newStats))
222
- throw UV('ENOTDIR', $ex);
223
- src.fs.renameSync(src.path, dst.path);
224
- emitChange(this, 'rename', oldPath);
225
- emitChange(this, 'change', newPath);
226
342
  }
227
343
  export function link(target, link) {
228
- target = normalizePath(target);
229
- link = normalizePath(link);
230
- const $ex = { syscall: 'link', path: link, dest: target };
231
- const { fs, path: resolved } = resolveMount(target, this, $ex);
232
- const dst = resolveMount(link, this, $ex);
233
- if (fs.uuid !== dst.fs.uuid)
234
- throw UV('EXDEV', $ex);
235
- const stats = fs.statSync(resolved);
236
- if (checkAccess) {
237
- if (!hasAccess(this, stats, constants.R_OK))
238
- throw UV('EACCES', $ex);
239
- const dirStats = fs.statSync(dirname(resolved));
240
- if (!hasAccess(this, dirStats, constants.R_OK))
241
- throw UV('EACCES', $ex);
242
- const destStats = fs.statSync(dirname(dst.path));
243
- if (!hasAccess(this, destStats, constants.W_OK))
244
- throw UV('EACCES', $ex);
344
+ const env_5 = { stack: [], error: void 0, hasError: false };
345
+ try {
346
+ target = normalizePath(target);
347
+ link = normalizePath(link);
348
+ const $ex = { syscall: 'link', path: target, dest: link };
349
+ const { fs, path: resolved } = resolveMount(target, this, $ex);
350
+ const dst = resolveMount(link, this, $ex);
351
+ if (fs.uuid !== dst.fs.uuid)
352
+ throw UV('EXDEV', $ex);
353
+ const stats = fs.statSync(resolved);
354
+ if (checkAccess) {
355
+ if (!hasAccess(this, stats, constants.R_OK))
356
+ throw UV('EACCES', $ex);
357
+ const dirStats = fs.statSync(dirname(resolved));
358
+ if (!hasAccess(this, dirStats, constants.R_OK))
359
+ throw UV('EACCES', $ex);
360
+ const destStats = fs.statSync(dirname(dst.path));
361
+ if (!hasAccess(this, destStats, constants.W_OK))
362
+ throw UV('EACCES', $ex);
363
+ }
364
+ const _ = __addDisposableResource(env_5, lockPathSync(fs, dirname(dst.path), 'rw'), false);
365
+ fs.linkSync(resolved, dst.path);
366
+ cacheOf(fs).link(resolved, dst.path);
367
+ }
368
+ catch (e_5) {
369
+ env_5.error = e_5;
370
+ env_5.hasError = true;
371
+ }
372
+ finally {
373
+ __disposeResources(env_5);
245
374
  }
246
- return fs.linkSync(resolved, dst.path);
247
375
  }
248
376
  export function stat(path, lstat) {
249
377
  path = normalizePath.call(this, path);
@@ -255,7 +383,8 @@ export function stat(path, lstat) {
255
383
  const { base, dir } = parse(path);
256
384
  const { fs, path: parent } = resolve(this, dir, false, extra);
257
385
  try {
258
- stats = fs.statSync(base ? join(parent, base) : parent);
386
+ const target = base ? join(parent, base) : parent;
387
+ stats = cacheOf(fs).get(target)?.inode ?? fs.statSync(target);
259
388
  }
260
389
  catch (e) {
261
390
  setUVMessage(Object.assign(e, extra));
@@ -0,0 +1,66 @@
1
+ import type { LockMode, LockRelease } from 'kerium/locks';
2
+ import type { FileSystem } from '../internal/filesystem.js';
3
+ import type { InodeLike } from '../internal/inode.js';
4
+ import { VNode } from './vnode.js';
5
+ /**
6
+ * The VFS-level cache of vnodes for a single file system. Modeled after Linux's dcache/icache.
7
+ *
8
+ * Vnodes are keyed by inode number, so hard links share a vnode.
9
+ * A vnode stays cached while it is referenced or dirty, and is evicted otherwise.
10
+ * @category VFS
11
+ * @todo Add LRU or something to limit memory usage
12
+ */
13
+ export declare class VCache {
14
+ readonly fs: FileSystem;
15
+ protected byIno: Map<number, VNode>;
16
+ protected byPath: Map<string, VNode>;
17
+ constructor(fs: FileSystem);
18
+ /** Get the vnode for a path, if one is cached. Note paths are relative to the FS root, not the VFS root */
19
+ get(path: string): VNode | undefined;
20
+ /**
21
+ * Get or create the vnode for `path`, incrementing its reference count.
22
+ * Callers must `unref` the vnode when done with it.
23
+ */
24
+ ref(path: string, inode: InodeLike): VNode;
25
+ /** Release a reference to a vnode, evicting it if it is unreferenced and clean */
26
+ unref(node: VNode): void;
27
+ protected evict(node: VNode): void;
28
+ /** Update cached paths for a rename. Handles directories, whose descendants' paths change too */
29
+ rename(oldPath: string, newPath: string): void;
30
+ /** Add a path for a newly created hard link, if the target is cached */
31
+ link(targetPath: string, linkPath: string): void;
32
+ /** Remove a path for an unlink/rmdir. The vnode is kept if other paths (hard links) refer to it */
33
+ remove(path: string): void;
34
+ /** Sync all dirty vnodes to the backend */
35
+ sync(): Promise<void>;
36
+ /** Sync all dirty vnodes to the backend synchronously */
37
+ syncSync(): void;
38
+ }
39
+ /**
40
+ * All vnode caches, keyed by file system UUID.
41
+ * Keyed by UUID rather than the `FileSystem` object because `resolveMount`
42
+ * wraps file systems in a new exception-context proxy on every call.
43
+ * @category VFS
44
+ * @internal
45
+ */
46
+ export declare const caches: Map<`${string}-${string}-${string}-${string}-${string}`, VCache>;
47
+ /**
48
+ * Get the vnode cache for a file system, creating it if needed.
49
+ * @category VFS
50
+ * @internal
51
+ */
52
+ export declare function cacheOf(fs: FileSystem): VCache;
53
+ /**
54
+ * Ref and lock the vnode for a path, usually a parent directory during a namespace operation.
55
+ * The returned release function unlocks and unrefs the vnode.
56
+ * If `inode` is not provided and the vnode is not cached, the backend is `stat`ed.
57
+ * @category VFS
58
+ * @internal
59
+ */
60
+ export declare function lockPath(fs: FileSystem, path: string, mode: LockMode, inode?: InodeLike): Promise<LockRelease>;
61
+ /**
62
+ * Synchronous version of `lockPath`.
63
+ * @category VFS
64
+ * @internal
65
+ */
66
+ export declare function lockPathSync(fs: FileSystem, path: string, mode: LockMode, inode?: InodeLike): LockRelease;