@poe-platform/safe-fs 0.1.695 → 0.1.697

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.
@@ -0,0 +1,23 @@
1
+ export interface ObjectIoQualificationBindings {
2
+ readonly QUALIFICATION_TOKEN?: string;
3
+ readonly QUALIFICATION_OWNER?: string;
4
+ readonly QUALIFICATION_EXPIRES_AT?: string;
5
+ }
6
+ export declare function authorizeObjectIoRequest(request: Request, bindings: ObjectIoQualificationBindings, now?: number): Response | undefined;
7
+ export interface ObjectIoCleanupBucket {
8
+ list(options: {
9
+ limit: number;
10
+ }): Promise<{
11
+ objects: readonly {
12
+ key: string;
13
+ }[];
14
+ truncated: boolean;
15
+ }>;
16
+ delete(keys: string[]): Promise<unknown>;
17
+ }
18
+ export declare function cleanupObjectIoBucket(bucket: ObjectIoCleanupBucket, maxPages?: number): Promise<{
19
+ removedObjects: number;
20
+ listedPages: number;
21
+ remainingObjects: 0;
22
+ truncated: false;
23
+ }>;
@@ -0,0 +1,39 @@
1
+ export function authorizeObjectIoRequest(request, bindings, now = Date.now()) {
2
+ const expires = Number(bindings.QUALIFICATION_EXPIRES_AT);
3
+ if (typeof bindings.QUALIFICATION_TOKEN !== "string" || bindings.QUALIFICATION_TOKEN.length < 32
4
+ || !bindings.QUALIFICATION_OWNER || bindings.QUALIFICATION_OWNER.length > 128 || !Number.isSafeInteger(expires) || expires <= 0) {
5
+ return new Response("Qualification bindings required", { status: 503 });
6
+ }
7
+ if (request.headers.get("Authorization") !== `Bearer ${bindings.QUALIFICATION_TOKEN}`)
8
+ return new Response("Unauthorized", { status: 401 });
9
+ const url = new URL(request.url);
10
+ if (expires <= now && url.pathname !== "/cleanup")
11
+ return new Response("Qualification expired", { status: 410 });
12
+ if (expires - now > 3600000)
13
+ return new Response("Expiry must be within one hour", { status: 503 });
14
+ if (request.method !== "POST")
15
+ return new Response("POST required", { status: 405 });
16
+ if (request.headers.get("Content-Length") !== "0" || request.headers.has("Transfer-Encoding"))
17
+ return new Response("Empty request required", { status: 400 });
18
+ if (!["/ready", "/cleanup", "/conformance", "/unhandled-errors", "/object-io-781"].includes(url.pathname))
19
+ return new Response("Not found", { status: 404 });
20
+ if (url.pathname !== "/object-io-781" && url.search)
21
+ return new Response("Control queries forbidden", { status: 400 });
22
+ return undefined;
23
+ }
24
+ export async function cleanupObjectIoBucket(bucket, maxPages = 32) {
25
+ if (!Number.isSafeInteger(maxPages) || maxPages < 1 || maxPages > 32)
26
+ throw new RangeError("Invalid cleanup page cap");
27
+ let removedObjects = 0;
28
+ for (let listedPages = 1; listedPages <= maxPages; listedPages++) {
29
+ const page = await bucket.list({ limit: 100 });
30
+ if (!page.objects.length) {
31
+ if (page.truncated)
32
+ throw new Error("Qualification cleanup returned a truncated empty page");
33
+ return { removedObjects, listedPages, remainingObjects: 0, truncated: false };
34
+ }
35
+ await bucket.delete(page.objects.map(object => object.key));
36
+ removedObjects += page.objects.length;
37
+ }
38
+ throw new Error("Qualification cleanup cap exhausted; bucket emptiness unverified");
39
+ }
@@ -0,0 +1,28 @@
1
+ export interface ObjectIoOperationMetrics {
2
+ count: number;
3
+ failed: number;
4
+ elapsedMs: number;
5
+ }
6
+ export interface ObjectIoPhaseMetrics {
7
+ elapsedMs: number;
8
+ operations: Record<string, ObjectIoOperationMetrics>;
9
+ }
10
+ export declare class ObjectIoMetrics {
11
+ private readonly now;
12
+ private readonly phases;
13
+ private current;
14
+ private started;
15
+ constructor(now?: () => number);
16
+ phase(name: string): void;
17
+ measure<Value>(category: string, name: string, operation: () => Value | Promise<Value>): Promise<Value>;
18
+ snapshot(): Record<string, ObjectIoPhaseMetrics>;
19
+ }
20
+ export declare function delayObjectIoBackend<Backend extends object>(backend: Backend, metrics: ObjectIoMetrics, delayMs: number, sleep?: (duration: number) => Promise<void>): Backend;
21
+ export interface ObjectIoReadbackOptions {
22
+ readonly body: ReadableStream<Uint8Array>;
23
+ readonly size: number;
24
+ readonly metrics: ObjectIoMetrics;
25
+ dispose(): Promise<void>;
26
+ summary(): Record<string, unknown>;
27
+ }
28
+ export declare function createObjectIoReadbackStream(options: ObjectIoReadbackOptions): ReadableStream<Uint8Array>;
@@ -0,0 +1,114 @@
1
+ export class ObjectIoMetrics {
2
+ now;
3
+ phases = {};
4
+ current = "setup";
5
+ started;
6
+ constructor(now = () => performance.now()) {
7
+ this.now = now;
8
+ this.started = now();
9
+ this.phases[this.current] = { elapsedMs: 0, operations: {} };
10
+ }
11
+ phase(name) {
12
+ const time = this.now();
13
+ this.phases[this.current].elapsedMs += time - this.started;
14
+ this.started = time;
15
+ this.current = name;
16
+ this.phases[name] ??= { elapsedMs: 0, operations: {} };
17
+ }
18
+ async measure(category, name, operation) {
19
+ const operations = this.phases[this.current].operations;
20
+ const entry = operations[`${category}.${name}`] ??= { count: 0, failed: 0, elapsedMs: 0 };
21
+ entry.count++;
22
+ const started = this.now();
23
+ try {
24
+ return await operation();
25
+ }
26
+ catch (error) {
27
+ entry.failed++;
28
+ throw error;
29
+ }
30
+ finally {
31
+ entry.elapsedMs += this.now() - started;
32
+ }
33
+ }
34
+ snapshot() {
35
+ const result = structuredClone(this.phases);
36
+ result[this.current].elapsedMs += this.now() - this.started;
37
+ return result;
38
+ }
39
+ }
40
+ export function delayObjectIoBackend(backend, metrics, delayMs, sleep = duration => new Promise(resolve => setTimeout(resolve, duration))) {
41
+ if (!Number.isSafeInteger(delayMs) || delayMs < 0)
42
+ throw new RangeError("Backend delay must be a nonnegative integer");
43
+ return new Proxy(backend, { get(target, property) {
44
+ const value = Reflect.get(target, property, target);
45
+ if (typeof value !== "function")
46
+ return value;
47
+ return (...args) => metrics.measure("backend", String(property), async () => {
48
+ if (delayMs)
49
+ await sleep(delayMs);
50
+ return value.apply(target, args);
51
+ });
52
+ } });
53
+ }
54
+ export function createObjectIoReadbackStream(options) {
55
+ const reader = options.body.getReader({ mode: "byob" });
56
+ const encoder = new TextEncoder();
57
+ let offset = 0;
58
+ let cleanup;
59
+ const dispose = () => cleanup ??= (async () => {
60
+ options.metrics.phase("fixtureCleanup");
61
+ await options.dispose();
62
+ options.metrics.phase("complete");
63
+ })();
64
+ options.metrics.phase("canonicalStream");
65
+ return new ReadableStream({
66
+ async pull(controller) {
67
+ try {
68
+ const chunk = await options.metrics.measure("stream", "read", () => reader.read(new Uint8Array(65536)));
69
+ if (chunk.done) {
70
+ if (offset !== options.size)
71
+ throw new Error("Canonical readback size mismatch");
72
+ await dispose();
73
+ controller.enqueue(encoder.encode(JSON.stringify({ ...options.summary(), type: "summary", completed: true,
74
+ canonicalBytes: offset, phases: options.metrics.snapshot() }) + "\n"));
75
+ controller.close();
76
+ reader.releaseLock();
77
+ return;
78
+ }
79
+ if (offset + chunk.value.length > options.size)
80
+ throw new Error("Canonical readback size mismatch");
81
+ const base64 = btoa(String.fromCharCode(...chunk.value));
82
+ controller.enqueue(encoder.encode(JSON.stringify({ type: "chunk", offset, base64 }) + "\n"));
83
+ offset += chunk.value.length;
84
+ }
85
+ catch (error) {
86
+ let failure = error;
87
+ try {
88
+ await reader.cancel(error);
89
+ }
90
+ catch (cancelError) {
91
+ if (cancelError !== error)
92
+ failure = new AggregateError([error, cancelError], "Readback and cancellation failed");
93
+ }
94
+ try {
95
+ await dispose();
96
+ }
97
+ catch (cleanupError) {
98
+ failure = new AggregateError([failure, cleanupError], "Readback and cleanup failed");
99
+ }
100
+ reader.releaseLock();
101
+ controller.error(failure);
102
+ }
103
+ },
104
+ async cancel(reason) {
105
+ try {
106
+ await reader.cancel(reason);
107
+ }
108
+ finally {
109
+ await dispose();
110
+ reader.releaseLock();
111
+ }
112
+ },
113
+ });
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.695",
3
+ "version": "0.1.697",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",