@poe-platform/safe-fs 0.1.727 → 0.1.729

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 (58) hide show
  1. package/README.md +15 -8
  2. package/dist/safe-fs/contracts/errors.js +8 -1
  3. package/dist/safe-fs/contracts/filesystem.d.ts +68 -2
  4. package/dist/safe-fs/contracts/io.js +210 -28
  5. package/dist/safe-fs/contracts/virtual-path.d.ts +3 -1
  6. package/dist/safe-fs/contracts/virtual-path.js +20 -1
  7. package/dist/safe-fs/core.d.ts +1 -0
  8. package/dist/safe-fs/core.js +1 -0
  9. package/dist/safe-fs/fs/capabilities.d.ts +1 -1
  10. package/dist/safe-fs/fs/capabilities.js +59 -21
  11. package/dist/safe-fs/fs/conditional-chmod.d.ts +7 -0
  12. package/dist/safe-fs/fs/conditional-chmod.js +19 -0
  13. package/dist/safe-fs/fs/descriptor.js +51 -25
  14. package/dist/safe-fs/fs/devices/index.d.ts +7 -2
  15. package/dist/safe-fs/fs/devices/index.js +210 -29
  16. package/dist/safe-fs/fs/devices/path.d.ts +3 -1
  17. package/dist/safe-fs/fs/devices/path.js +70 -12
  18. package/dist/safe-fs/fs/memory/index.d.ts +70 -48
  19. package/dist/safe-fs/fs/memory/index.js +1595 -244
  20. package/dist/safe-fs/fs/memory/ledger.d.ts +8 -1
  21. package/dist/safe-fs/fs/memory/ledger.js +46 -6
  22. package/dist/safe-fs/fs/memory/missing-target.js +21 -1
  23. package/dist/safe-fs/fs/mount/index.d.ts +11 -3
  24. package/dist/safe-fs/fs/mount/index.js +326 -15
  25. package/dist/safe-fs/fs/object-publication/index.js +79 -3
  26. package/dist/safe-fs/fs/overlay/index.d.ts +2 -2
  27. package/dist/safe-fs/fs/overlay/index.js +27 -5
  28. package/dist/safe-fs/fs/overlay/memory-publication.d.ts +2 -1
  29. package/dist/safe-fs/fs/overlay/memory-publication.js +39 -2
  30. package/dist/safe-fs/fs/quota/index.js +5 -5
  31. package/dist/safe-fs/fs/readonly/index.d.ts +1 -0
  32. package/dist/safe-fs/fs/readonly/index.js +13 -0
  33. package/dist/safe-fs/fs/real/index.d.ts +6 -2
  34. package/dist/safe-fs/fs/real/index.js +78 -8
  35. package/dist/safe-fs/fs/s3/filesystem.d.ts +3 -0
  36. package/dist/safe-fs/fs/s3/filesystem.js +57 -25
  37. package/dist/safe-fs/fs/s3/http/transport.js +20 -3
  38. package/dist/safe-fs/fs/s3/mock.js +19 -2
  39. package/dist/safe-fs/fs/s3/namespace.js +8 -2
  40. package/dist/safe-fs/fs/s3/registry.js +2 -0
  41. package/dist/safe-fs/fs/scoped.d.ts +1 -0
  42. package/dist/safe-fs/fs/scoped.js +261 -74
  43. package/dist/safe-fs/fs/staging-ancestry.d.ts +8 -0
  44. package/dist/safe-fs/fs/staging-ancestry.js +115 -0
  45. package/dist/safe-fs/fs/staging-cleanup.d.ts +5 -0
  46. package/dist/safe-fs/fs/staging-cleanup.js +63 -0
  47. package/dist/safe-fs/fs/webdav/webdav.js +7 -5
  48. package/dist/safe-fs/platform/browser.d.ts +5 -0
  49. package/dist/safe-fs/platform/browser.js +17 -0
  50. package/dist/safe-fs/platform/node.d.ts +5 -0
  51. package/dist/safe-fs/platform/node.js +28 -0
  52. package/dist/safe-fs/platform/transport-budget.d.ts +7 -0
  53. package/dist/safe-fs/platform/transport-budget.js +4 -0
  54. package/dist/safe-fs/testing/object-io-metrics.d.ts +3 -0
  55. package/dist/safe-fs/testing/object-io-metrics.js +29 -0
  56. package/dist/safe-fs/testing/object-publication.d.ts +3 -1
  57. package/dist/safe-fs/testing/object-publication.js +2 -2
  58. package/package.json +1 -1
@@ -0,0 +1,115 @@
1
+ import { FsError, toFsError } from "../contracts/errors.js";
2
+ import { compareIdentity } from "./mount/identity.js";
3
+ import { normalizePath, validatePath } from "../contracts/virtual-path.js";
4
+ export function directoryAncestryPaths(directory) {
5
+ validatePath(directory);
6
+ if (!directory.startsWith("/") || normalizePath(directory) !== directory)
7
+ throw new FsError("EINVAL", { path: directory });
8
+ let path = "";
9
+ return ["/", ...directory.split("/").filter(Boolean).map(component => path += `/${component}`)];
10
+ }
11
+ export function snapshotDirectoryAncestry(ancestors) {
12
+ const entries = ancestors.map(entry => Object.freeze({ path: entry.path, stat: Object.freeze({ ...entry.stat }) }));
13
+ const paths = directoryAncestryPaths(entries.at(-1)?.path ?? "");
14
+ if (entries.length !== paths.length || entries.some((entry, index) => entry.path !== paths[index])) {
15
+ throw new FsError("EINVAL", { message: "directory ancestry requires a complete ordered root-to-directory list" });
16
+ }
17
+ for (const entry of entries) {
18
+ if (entry.stat.type !== "directory")
19
+ throw new FsError("EINVAL", { path: entry.path });
20
+ if (compareIdentity(entry.stat, entry.stat) !== "same")
21
+ throw new FsError("ENOTSUP", { path: entry.path });
22
+ }
23
+ return Object.freeze(entries);
24
+ }
25
+ export function runStagingGuard(guard) {
26
+ const result = guard();
27
+ if (result !== true) {
28
+ // A void callback would accept async implementations. Require a literal
29
+ // success value and observe a misdeclared asynchronous rejection.
30
+ void Promise.resolve(result).catch(() => { });
31
+ throw new FsError("ENOTSUP", { message: "staging validation must return true synchronously" });
32
+ }
33
+ }
34
+ export function snapshotStagingResolution(resolution) {
35
+ const path = resolution.path;
36
+ validatePath(path);
37
+ const ancestors = snapshotDirectoryAncestry(resolution.ancestors);
38
+ const validate = resolution.validate;
39
+ if (normalizePath(path) !== path || !path.startsWith("/") || path === "/"
40
+ || ancestors.at(-1).path !== (path.slice(0, path.lastIndexOf("/")) || "/")
41
+ || typeof validate !== "function")
42
+ throw new FsError("EINVAL", { path });
43
+ const parent = Object.freeze({ ...resolution.parent });
44
+ if (parent.type !== "directory")
45
+ throw new FsError("EINVAL", { path });
46
+ if (compareIdentity(parent, ancestors.at(-1).stat) !== "same")
47
+ throw new FsError("ENOTSUP", { path });
48
+ const supplied = resolution.destination;
49
+ const destination = supplied === null ? null : Object.freeze({ ...supplied });
50
+ const requireSnapshot = (stat) => {
51
+ if (compareIdentity(stat, stat) !== "same" || stat.type !== "directory" && (!Number.isSafeInteger(stat.revision) || stat.revision < 0)) {
52
+ throw new FsError("ENOTSUP", { path });
53
+ }
54
+ };
55
+ if (destination !== null) {
56
+ if (destination.type !== "file")
57
+ throw new FsError("EINVAL", { path });
58
+ requireSnapshot(destination);
59
+ }
60
+ if (!resolution.traversed.length)
61
+ throw new FsError("EINVAL", { path });
62
+ if (resolution.traversed.length > 4096)
63
+ throw new FsError("EFBIG", { path });
64
+ let recordedUnits = 0;
65
+ const traversed = resolution.traversed.map(entry => {
66
+ const entryPath = entry.path, stat = Object.freeze({ ...entry.stat }), linkTarget = entry.linkTarget;
67
+ validatePath(entryPath);
68
+ if (!entryPath.startsWith("/") || normalizePath(entryPath) !== entryPath
69
+ || !["directory", "file", "symlink"].includes(stat.type)
70
+ || stat.type === "symlink" && typeof linkTarget !== "string"
71
+ || stat.type !== "symlink" && linkTarget !== undefined)
72
+ throw new FsError("EINVAL", { path: entryPath });
73
+ requireSnapshot(stat);
74
+ recordedUnits += entryPath.length + (linkTarget?.length ?? 0);
75
+ if (recordedUnits > 1_048_576)
76
+ throw new FsError("EFBIG", { path });
77
+ return Object.freeze({ path: entryPath, stat, ...(linkTarget === undefined ? {} : { linkTarget }) });
78
+ });
79
+ return Object.freeze({ path, parent, destination, ancestors, traversed: Object.freeze(traversed), validate });
80
+ }
81
+ /** Diagnose failed admission against supplied receipts; this is never a commit
82
+ * guard. Successful publication still requires the backend's atomic checks. */
83
+ export async function inspectStagingBindings(read, destination, options) {
84
+ options.signal?.throwIfAborted();
85
+ const ancestors = snapshotDirectoryAncestry(options.ancestors);
86
+ const parent = destination.slice(0, destination.lastIndexOf("/")) || "/";
87
+ if (normalizePath(destination) !== destination || ancestors.at(-1)?.path !== parent)
88
+ throw new FsError("EINVAL", { path: destination });
89
+ const expected = options.destination === null ? null : { ...options.destination };
90
+ if (expected !== null && (compareIdentity(expected, expected) !== "same"
91
+ || expected.type === "file" && (!Number.isSafeInteger(expected.revision) || expected.revision < 0)))
92
+ throw new FsError("ENOTSUP", { path: destination });
93
+ const entries = [...ancestors.map(entry => ({ ...entry, directory: true })), { path: destination, stat: expected, directory: false }];
94
+ for (const entry of entries) {
95
+ let current;
96
+ try {
97
+ current = await read(entry.path);
98
+ }
99
+ catch (error) {
100
+ options.signal?.throwIfAborted();
101
+ const code = toFsError(error).code;
102
+ if (code === "ENOENT" && entry.stat === null)
103
+ continue;
104
+ if (["ENOENT", "ENOTDIR", "ELOOP"].includes(code))
105
+ throw new FsError("EAGAIN", { path: entry.path, cause: error });
106
+ throw error;
107
+ }
108
+ options.signal?.throwIfAborted();
109
+ if (entry.stat === null || current.type !== entry.stat.type || compareIdentity(current, entry.stat) !== "same")
110
+ throw new FsError("EAGAIN", { path: entry.path });
111
+ if (!entry.directory && ["revision", "size", "mode", "nlink", "mtimeMs", "ctimeMs"].some(field => current[field] !== entry.stat[field])) {
112
+ throw new FsError("EAGAIN", { path: entry.path });
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,5 @@
1
+ import type { CreateStagedFileOptions, FileStagingCleanup, FsOptions } from "../contracts/filesystem.js";
2
+ /** Capture creation controls before awaits or allocation can split their meaning. */
3
+ export declare function snapshotStagingCreation(options: CreateStagedFileOptions, path: string): CreateStagedFileOptions;
4
+ /** Own admission and release outside wrappers that can fail before dispatch. */
5
+ export declare function createStagingCleanup(path: string, remove: (options: FsOptions) => void | Promise<void>, release: () => void | Promise<void>): FileStagingCleanup;
@@ -0,0 +1,63 @@
1
+ import { FsError } from "../contracts/errors.js";
2
+ import { finishCleanup } from "../contracts/cleanup.js";
3
+ /** Capture creation controls before awaits or allocation can split their meaning. */
4
+ export function snapshotStagingCreation(options, path) {
5
+ const captured = { ...options };
6
+ for (const key of ["signal", "retainCleanup", "parent", "mode", "atimeMs", "mtimeMs"]) {
7
+ if (!Object.hasOwn(captured, key))
8
+ Object.defineProperty(captured, key, { value: options[key], enumerable: true });
9
+ }
10
+ const { signal, retainCleanup, parent, mode, atimeMs, mtimeMs, ...extra } = captured;
11
+ signal?.throwIfAborted();
12
+ if (retainCleanup !== undefined && typeof retainCleanup !== "boolean")
13
+ throw new FsError("EINVAL", { syscall: "createStagedFile", path });
14
+ return {
15
+ ...extra, parent,
16
+ ...(signal === undefined ? {} : { signal }),
17
+ ...(retainCleanup === undefined ? {} : { retainCleanup }),
18
+ ...(mode === undefined ? {} : { mode }),
19
+ ...(atimeMs === undefined ? {} : { atimeMs }),
20
+ ...(mtimeMs === undefined ? {} : { mtimeMs }),
21
+ };
22
+ }
23
+ /** Own admission and release outside wrappers that can fail before dispatch. */
24
+ export function createStagingCleanup(path, remove, release) {
25
+ let accepting = true;
26
+ let removal;
27
+ let closing;
28
+ let released;
29
+ const dispose = () => released ??= Promise.resolve().then(release);
30
+ return Object.freeze({
31
+ remove(options = {}) {
32
+ if (removal)
33
+ return removal;
34
+ if (!accepting) {
35
+ const rejected = Promise.reject(new FsError("EBADF", { syscall: "removeStagedFile", path }));
36
+ void rejected.catch(() => { });
37
+ return rejected;
38
+ }
39
+ accepting = false;
40
+ removal = Promise.resolve().then(async () => {
41
+ let failed = true;
42
+ try {
43
+ await remove(options);
44
+ failed = false;
45
+ }
46
+ finally {
47
+ await finishCleanup(dispose, failed);
48
+ }
49
+ });
50
+ void removal.catch(() => { });
51
+ return removal;
52
+ },
53
+ close() {
54
+ accepting = false;
55
+ closing ??= (async () => {
56
+ await removal?.catch(() => { });
57
+ await dispose();
58
+ })();
59
+ void closing.catch(() => { });
60
+ return closing;
61
+ },
62
+ });
63
+ }
@@ -86,7 +86,9 @@ const resourceIdBody = '<?xml version="1.0" encoding="utf-8"?>'
86
86
  function fail(code, syscall, path, message) {
87
87
  throw new FsError(code, { syscall, path, ...(message === undefined ? {} : { message }) });
88
88
  }
89
- function positive(value, name, zero = false) {
89
+ function positive(value, name, zero = false, unlimited = false) {
90
+ if (unlimited && value === Infinity)
91
+ return value;
90
92
  if (!Number.isSafeInteger(value) || value < (zero ? 0 : 1)) {
91
93
  throw new FsError("EINVAL", { message: `${name} must be a ${zero ? "nonnegative" : "positive"} safe integer` });
92
94
  }
@@ -286,9 +288,9 @@ export class WebDavFileSystem {
286
288
  removeDirectory: this.atomicEmptyDirectory !== undefined,
287
289
  streamingAppend: this.requestStreamSupport !== false,
288
290
  });
289
- this.maxResponseBytes = options.maxResponseBytes === undefined ? 16 * 1024 * 1024 : positive(options.maxResponseBytes, "maxResponseBytes");
290
- this.maxXmlBytes = options.maxXmlBytes === undefined ? 1024 * 1024 : positive(options.maxXmlBytes, "maxXmlBytes");
291
- this.maxEntries = options.maxEntries === undefined ? Infinity : positive(options.maxEntries, "maxEntries");
291
+ this.maxResponseBytes = options.maxResponseBytes === undefined ? 16 * 1024 * 1024 : positive(options.maxResponseBytes, "maxResponseBytes", false, true);
292
+ this.maxXmlBytes = options.maxXmlBytes === undefined ? 1024 * 1024 : positive(options.maxXmlBytes, "maxXmlBytes", false, true);
293
+ this.maxEntries = options.maxEntries === undefined ? Infinity : positive(options.maxEntries, "maxEntries", false, true);
292
294
  this.timeoutMs = options.timeoutMs === undefined ? undefined : positive(options.timeoutMs, "timeoutMs");
293
295
  this.overwritePolicy = options.overwritePolicy ?? "lock";
294
296
  this.configuredComparison = options.compareEntry !== undefined;
@@ -867,7 +869,7 @@ export class WebDavFileSystem {
867
869
  }
868
870
  async readFile(path, options = {}) {
869
871
  const normalized = normalize(path);
870
- const limit = Math.min(this.maxResponseBytes, options.maxBytes === undefined ? this.maxResponseBytes : positive(options.maxBytes, "maxBytes", true));
872
+ const limit = Math.min(this.maxResponseBytes, options.maxBytes === undefined ? this.maxResponseBytes : positive(options.maxBytes, "maxBytes", true, true));
871
873
  if ((await this.stat(path, options)).type === "directory")
872
874
  fail("EISDIR", "readFile", path);
873
875
  return this.request("GET", normalized, options, { headers: { "Accept-Encoding": "identity" } }, async (response, signal) => {
@@ -1,4 +1,5 @@
1
1
  import type { FsOptions } from "../contracts/filesystem.js";
2
+ import type { ScopedTransportBudgetFrame } from "./transport-budget.js";
2
3
  export type PlatformErrno = number | undefined;
3
4
  export type PlatformComparisonCallback<Callback> = Callback & never;
4
5
  export declare const platform: Readonly<{
@@ -11,3 +12,7 @@ export declare const comparisonContext: Readonly<{
11
12
  active(options: FsOptions): boolean;
12
13
  run<Result>(options: FsOptions, action: (nested: FsOptions) => Promise<Result>): Promise<Result>;
13
14
  }>;
15
+ export declare function getScopedTransportBudget(): readonly ScopedTransportBudgetFrame[] | undefined;
16
+ export declare function withScopedTransportBudget<Result>(frames: readonly ScopedTransportBudgetFrame[] | undefined, action: () => Result): Result;
17
+ export declare function runScopedTransportBudget<Result>(_admit: (options?: FsOptions) => void, action: () => Result, _credit?: number): Result;
18
+ export declare function chargeScopedTransportCall(options?: FsOptions): never;
@@ -38,3 +38,20 @@ export const comparisonContext = Object.freeze({
38
38
  }
39
39
  }
40
40
  });
41
+ // Scoped browser filesystems charge logical operations directly. S3 remains
42
+ // Node-only; never pretend to provide its async transport budget context here.
43
+ export function getScopedTransportBudget() {
44
+ return undefined;
45
+ }
46
+ export function withScopedTransportBudget(frames, action) {
47
+ if (frames !== undefined)
48
+ throw new FsError("ENOTSUP", { message: "S3 transport budget contexts require Node" });
49
+ return action();
50
+ }
51
+ export function runScopedTransportBudget(_admit, action, _credit = 1) {
52
+ return action();
53
+ }
54
+ export function chargeScopedTransportCall(options) {
55
+ options?.signal?.throwIfAborted();
56
+ throw new FsError("ENOTSUP", { message: "S3 transport budget contexts require Node" });
57
+ }
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { FsOptions } from "../contracts/filesystem.js";
3
+ import { type ScopedTransportBudgetFrame } from "./transport-budget.js";
3
4
  export type PlatformErrno = number;
4
5
  export type PlatformComparisonCallback<Callback> = Callback;
5
6
  export declare const platform: Readonly<{
@@ -12,3 +13,7 @@ export declare const comparisonContext: Readonly<{
12
13
  active(_options: FsOptions): boolean;
13
14
  run<Result>(options: FsOptions, action: (nested: FsOptions) => Promise<Result>): Promise<Result>;
14
15
  }>;
16
+ export declare function getScopedTransportBudget(): readonly ScopedTransportBudgetFrame[] | undefined;
17
+ export declare function withScopedTransportBudget<Result>(frames: readonly ScopedTransportBudgetFrame[] | undefined, action: () => Result): Result;
18
+ export declare function runScopedTransportBudget<Result>(admit: (options?: FsOptions) => void, action: () => Result, credit?: number): Result;
19
+ export declare function chargeScopedTransportCall(options?: FsOptions): void;
@@ -2,6 +2,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as util from "node:util";
4
4
  import { constants } from "node:os";
5
+ import { hasRegisteredS3FileSystem } from "./transport-budget.js";
5
6
  const systemErrnos = (() => {
6
7
  const fallback = new Map(Object.entries(constants.errno).map(([name, errno]) => [name, -Math.abs(errno)]));
7
8
  if (typeof util.getSystemErrorMap !== "function")
@@ -33,3 +34,30 @@ export const comparisonContext = Object.freeze({
33
34
  return negotiating.run(true, () => action(options));
34
35
  }
35
36
  });
37
+ const transportBudgets = new AsyncLocalStorage();
38
+ export function getScopedTransportBudget() {
39
+ return hasRegisteredS3FileSystem ? transportBudgets.getStore() : undefined;
40
+ }
41
+ export function withScopedTransportBudget(frames, action) {
42
+ return frames ? transportBudgets.run(frames, action) : action();
43
+ }
44
+ export function runScopedTransportBudget(admit, action, credit = 1) {
45
+ if (!hasRegisteredS3FileSystem)
46
+ return action();
47
+ const parent = transportBudgets.getStore() ?? [];
48
+ const frame = { credit, admit };
49
+ return transportBudgets.run([...parent, frame], action);
50
+ }
51
+ export function chargeScopedTransportCall(options) {
52
+ if (!hasRegisteredS3FileSystem)
53
+ return;
54
+ const frames = transportBudgets.getStore();
55
+ if (!frames)
56
+ return;
57
+ for (const frame of frames) {
58
+ if (frame.credit > 0)
59
+ frame.credit--;
60
+ else
61
+ frame.admit(options);
62
+ }
63
+ }
@@ -0,0 +1,7 @@
1
+ import type { FsOptions } from "../contracts/filesystem.js";
2
+ export interface ScopedTransportBudgetFrame {
3
+ credit: number;
4
+ readonly admit: (options?: FsOptions) => void;
5
+ }
6
+ export declare let hasRegisteredS3FileSystem: boolean;
7
+ export declare function enableS3TransportBudget(): void;
@@ -0,0 +1,4 @@
1
+ export let hasRegisteredS3FileSystem = false;
2
+ export function enableS3TransportBudget() {
3
+ hasRegisteredS3FileSystem = true;
4
+ }
@@ -1,3 +1,4 @@
1
+ import type { ObjectFilePublicationStore } from "../fs/object-publication/index.js";
1
2
  export interface ObjectIoOperationMetrics {
2
3
  count: number;
3
4
  failed: number;
@@ -17,6 +18,8 @@ export declare class ObjectIoMetrics {
17
18
  measure<Value>(category: string, name: string, operation: () => Value | Promise<Value>): Promise<Value>;
18
19
  snapshot(): Record<string, ObjectIoPhaseMetrics>;
19
20
  }
21
+ /** Timings are inclusive: store publication can contain staging reads and backend I/O. */
22
+ export declare function measureObjectIoStore(store: ObjectFilePublicationStore, metrics: ObjectIoMetrics): ObjectFilePublicationStore;
20
23
  export declare function delayObjectIoBackend<Backend extends object>(backend: Backend, metrics: ObjectIoMetrics, delayMs: number, sleep?: (duration: number) => Promise<void>): Backend;
21
24
  export interface ObjectIoReadbackOptions {
22
25
  readonly body: ReadableStream<Uint8Array>;
@@ -37,6 +37,35 @@ export class ObjectIoMetrics {
37
37
  return result;
38
38
  }
39
39
  }
40
+ /** Timings are inclusive: store publication can contain staging reads and backend I/O. */
41
+ export function measureObjectIoStore(store, metrics) {
42
+ const version = (head) => ({
43
+ revision: head.revision, stat: head.stat,
44
+ ...(head.publicationToken === undefined ? {} : { publicationToken: head.publicationToken }),
45
+ read: (...args) => metrics.measure("version", "read", () => head.read(...args)),
46
+ close: () => metrics.measure("version", "close", () => head.close()),
47
+ });
48
+ const publish = store.publish;
49
+ const createStaging = store.createStaging;
50
+ return {
51
+ async acquire(...args) {
52
+ const head = await metrics.measure("store", "acquire", () => store.acquire(...args));
53
+ return head && version(head);
54
+ },
55
+ ...(publish ? { async publish(...args) {
56
+ return version(await metrics.measure("store", "publish", () => publish.apply(store, args)));
57
+ } } : {}),
58
+ ...(createStaging ? { async createStaging(...args) {
59
+ const staging = await metrics.measure("store", "createStaging", () => createStaging.apply(store, args));
60
+ return {
61
+ readPage: (...values) => metrics.measure("staging", "readPage", () => staging.readPage(...values)),
62
+ writePage: (...values) => metrics.measure("staging", "writePage", () => staging.writePage(...values)),
63
+ truncate: (...values) => metrics.measure("staging", "truncate", () => staging.truncate(...values)),
64
+ close: () => metrics.measure("staging", "close", () => staging.close()),
65
+ };
66
+ } } : {}),
67
+ };
68
+ }
40
69
  export function delayObjectIoBackend(backend, metrics, delayMs, sleep = duration => new Promise(resolve => setTimeout(resolve, duration))) {
41
70
  if (!Number.isSafeInteger(delayMs) || delayMs < 0)
42
71
  throw new RangeError("Backend delay must be a nonnegative integer");
@@ -1,14 +1,16 @@
1
1
  import type { FileSystem } from "../contracts/filesystem.js";
2
- import type { ObjectFilePublicationStore } from "../fs/object-publication/index.js";
2
+ import type { ObjectFileDescriptorOptions, ObjectFilePublicationStore } from "../fs/object-publication/index.js";
3
3
  export interface ObjectFilePublicationConformanceFixture {
4
4
  readonly fs: FileSystem;
5
5
  readonly store: ObjectFilePublicationStore;
6
6
  readonly root: string;
7
+ readonly descriptorOptions?: ObjectFileDescriptorOptions;
7
8
  dispose(): void | Promise<void>;
8
9
  }
9
10
  export interface ObjectFilePublicationConformanceOptions {
10
11
  readonly createFixture: () => ObjectFilePublicationConformanceFixture | Promise<ObjectFilePublicationConformanceFixture>;
11
12
  readonly requireStaging?: boolean;
13
+ readonly descriptorOptions?: ObjectFileDescriptorOptions;
12
14
  }
13
15
  export interface ObjectFilePublicationConformanceCase {
14
16
  readonly name: string;
@@ -92,7 +92,7 @@ export function createObjectFilePublicationConformanceCases(options) {
92
92
  async run({ fixture, path, track, publish }) {
93
93
  const name = path("descriptor");
94
94
  await publish(name, null, new Uint8Array([1, 2, 3, 4]));
95
- const fs = withObjectFileDescriptors(fixture.fs, fixture.store, { chunkBytes: 4, maxStagedBytes: 16 });
95
+ const fs = withObjectFileDescriptors(fixture.fs, fixture.store, { chunkBytes: 4, maxStagedBytes: 16, maxFileBytes: 16, ...options.descriptorOptions, ...fixture.descriptorOptions });
96
96
  const descriptor = track(await fs.open(name, { access: "readwrite" }));
97
97
  check(descriptor.capabilities.publication === "conditional", "descriptor publication profile was lost");
98
98
  await descriptor.write(new Uint8Array([7]), 1);
@@ -156,7 +156,7 @@ export function createObjectFilePublicationConformanceCases(options) {
156
156
  async run({ fixture, path, track, publish }) {
157
157
  const name = path("stage-descriptor");
158
158
  await publish(name, null, new Uint8Array([1, 2, 3, 4]));
159
- const fs = withObjectFileDescriptors(fixture.fs, fixture.store, { chunkBytes: 4, maxStagedBytes: 4, maxStagedPages: 1, maxFileBytes: 16 });
159
+ const fs = withObjectFileDescriptors(fixture.fs, fixture.store, { chunkBytes: 4, maxStagedBytes: 4, maxStagedPages: 1, maxFileBytes: 16, ...options.descriptorOptions, ...fixture.descriptorOptions });
160
160
  const descriptor = track(await fs.open(name, { access: "readwrite" }));
161
161
  const content = new Uint8Array([5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
162
162
  await descriptor.write(content, 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.727",
3
+ "version": "0.1.729",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",