@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.
- package/README.md +15 -8
- package/dist/safe-fs/contracts/errors.js +8 -1
- package/dist/safe-fs/contracts/filesystem.d.ts +68 -2
- package/dist/safe-fs/contracts/io.js +210 -28
- package/dist/safe-fs/contracts/virtual-path.d.ts +3 -1
- package/dist/safe-fs/contracts/virtual-path.js +20 -1
- package/dist/safe-fs/core.d.ts +1 -0
- package/dist/safe-fs/core.js +1 -0
- package/dist/safe-fs/fs/capabilities.d.ts +1 -1
- package/dist/safe-fs/fs/capabilities.js +59 -21
- package/dist/safe-fs/fs/conditional-chmod.d.ts +7 -0
- package/dist/safe-fs/fs/conditional-chmod.js +19 -0
- package/dist/safe-fs/fs/descriptor.js +51 -25
- package/dist/safe-fs/fs/devices/index.d.ts +7 -2
- package/dist/safe-fs/fs/devices/index.js +210 -29
- package/dist/safe-fs/fs/devices/path.d.ts +3 -1
- package/dist/safe-fs/fs/devices/path.js +70 -12
- package/dist/safe-fs/fs/memory/index.d.ts +70 -48
- package/dist/safe-fs/fs/memory/index.js +1595 -244
- package/dist/safe-fs/fs/memory/ledger.d.ts +8 -1
- package/dist/safe-fs/fs/memory/ledger.js +46 -6
- package/dist/safe-fs/fs/memory/missing-target.js +21 -1
- package/dist/safe-fs/fs/mount/index.d.ts +11 -3
- package/dist/safe-fs/fs/mount/index.js +326 -15
- package/dist/safe-fs/fs/object-publication/index.js +79 -3
- package/dist/safe-fs/fs/overlay/index.d.ts +2 -2
- package/dist/safe-fs/fs/overlay/index.js +27 -5
- package/dist/safe-fs/fs/overlay/memory-publication.d.ts +2 -1
- package/dist/safe-fs/fs/overlay/memory-publication.js +39 -2
- package/dist/safe-fs/fs/quota/index.js +5 -5
- package/dist/safe-fs/fs/readonly/index.d.ts +1 -0
- package/dist/safe-fs/fs/readonly/index.js +13 -0
- package/dist/safe-fs/fs/real/index.d.ts +6 -2
- package/dist/safe-fs/fs/real/index.js +78 -8
- package/dist/safe-fs/fs/s3/filesystem.d.ts +3 -0
- package/dist/safe-fs/fs/s3/filesystem.js +57 -25
- package/dist/safe-fs/fs/s3/http/transport.js +20 -3
- package/dist/safe-fs/fs/s3/mock.js +19 -2
- package/dist/safe-fs/fs/s3/namespace.js +8 -2
- package/dist/safe-fs/fs/s3/registry.js +2 -0
- package/dist/safe-fs/fs/scoped.d.ts +1 -0
- package/dist/safe-fs/fs/scoped.js +261 -74
- package/dist/safe-fs/fs/staging-ancestry.d.ts +8 -0
- package/dist/safe-fs/fs/staging-ancestry.js +115 -0
- package/dist/safe-fs/fs/staging-cleanup.d.ts +5 -0
- package/dist/safe-fs/fs/staging-cleanup.js +63 -0
- package/dist/safe-fs/fs/webdav/webdav.js +7 -5
- package/dist/safe-fs/platform/browser.d.ts +5 -0
- package/dist/safe-fs/platform/browser.js +17 -0
- package/dist/safe-fs/platform/node.d.ts +5 -0
- package/dist/safe-fs/platform/node.js +28 -0
- package/dist/safe-fs/platform/transport-budget.d.ts +7 -0
- package/dist/safe-fs/platform/transport-budget.js +4 -0
- package/dist/safe-fs/testing/object-io-metrics.d.ts +3 -0
- package/dist/safe-fs/testing/object-io-metrics.js +29 -0
- package/dist/safe-fs/testing/object-publication.d.ts +3 -1
- package/dist/safe-fs/testing/object-publication.js +2 -2
- 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;
|
|
@@ -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);
|