@effect-vfs/core 0.5.0 → 0.6.0
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 +34 -31
- package/dist/BytePath.d.ts +114 -4
- package/dist/BytePath.d.ts.map +1 -1
- package/dist/BytePath.js +155 -0
- package/dist/Caller.d.ts +391 -0
- package/dist/Caller.d.ts.map +1 -0
- package/dist/Caller.js +199 -0
- package/dist/FileHandle.d.ts +50 -0
- package/dist/FileHandle.d.ts.map +1 -0
- package/dist/FileHandle.js +28 -0
- package/dist/Fixture.d.ts +155 -0
- package/dist/Fixture.d.ts.map +1 -0
- package/dist/Fixture.js +65 -0
- package/dist/LiveVolume.d.ts +11 -21
- package/dist/LiveVolume.d.ts.map +1 -1
- package/dist/LiveVolume.js +17 -14
- package/dist/Metadata.d.ts +186 -0
- package/dist/Metadata.d.ts.map +1 -0
- package/dist/Metadata.js +125 -0
- package/dist/Snapshot.d.ts +31 -53
- package/dist/Snapshot.d.ts.map +1 -1
- package/dist/Snapshot.js +26 -49
- package/dist/SnapshotDelta.d.ts +24 -39
- package/dist/SnapshotDelta.d.ts.map +1 -1
- package/dist/SnapshotDelta.js +14 -35
- package/dist/Target.d.ts +225 -0
- package/dist/Target.d.ts.map +1 -0
- package/dist/Target.js +92 -0
- package/dist/Testing.d.ts +128 -0
- package/dist/Testing.d.ts.map +1 -0
- package/dist/Testing.js +111 -0
- package/dist/VfsError.d.ts +199 -0
- package/dist/VfsError.d.ts.map +1 -0
- package/dist/VfsError.js +164 -0
- package/dist/VirtualFileSystem.d.ts +628 -347
- package/dist/VirtualFileSystem.d.ts.map +1 -1
- package/dist/VirtualFileSystem.js +448 -115
- package/dist/Volume.d.ts +211 -0
- package/dist/Volume.d.ts.map +1 -0
- package/dist/Volume.js +172 -0
- package/dist/Watch.d.ts +68 -0
- package/dist/Watch.d.ts.map +1 -0
- package/dist/Watch.js +46 -0
- package/dist/index.d.ts +56 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +56 -1
- package/dist/internal/budget.d.ts +90 -0
- package/dist/internal/budget.d.ts.map +1 -0
- package/dist/internal/budget.js +70 -0
- package/dist/internal/bytePath.d.ts +2 -0
- package/dist/internal/bytePath.d.ts.map +1 -1
- package/dist/internal/bytePath.js +3 -2
- package/dist/internal/bytes.d.ts +3 -0
- package/dist/internal/bytes.d.ts.map +1 -1
- package/dist/internal/bytes.js +4 -2
- package/dist/internal/canonicalBase64.d.ts +1 -4
- package/dist/internal/canonicalBase64.d.ts.map +1 -1
- package/dist/internal/canonicalBase64.js +9 -13
- package/dist/internal/errors.d.ts +47 -5
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +80 -36
- package/dist/internal/fixture.d.ts +2 -2
- package/dist/internal/fixture.d.ts.map +1 -1
- package/dist/internal/fixture.js +57 -74
- package/dist/internal/hex128.d.ts +12 -0
- package/dist/internal/hex128.d.ts.map +1 -0
- package/dist/internal/hex128.js +7 -0
- package/dist/internal/hmac.d.ts +5 -0
- package/dist/internal/hmac.d.ts.map +1 -0
- package/dist/internal/hmac.js +150 -0
- package/dist/internal/image.d.ts +14 -117
- package/dist/internal/image.d.ts.map +1 -1
- package/dist/internal/image.js +137 -167
- package/dist/internal/inodeTable.d.ts +20 -0
- package/dist/internal/inodeTable.d.ts.map +1 -0
- package/dist/internal/inodeTable.js +48 -0
- package/dist/internal/lines.d.ts +36 -0
- package/dist/internal/lines.d.ts.map +1 -0
- package/dist/internal/lines.js +115 -0
- package/dist/internal/liveImage.d.ts +19 -278
- package/dist/internal/liveImage.d.ts.map +1 -1
- package/dist/internal/liveImage.js +75 -166
- package/dist/internal/merkle.d.ts +45 -0
- package/dist/internal/merkle.d.ts.map +1 -0
- package/dist/internal/merkle.js +179 -0
- package/dist/internal/metadata.d.ts +2 -13
- package/dist/internal/metadata.d.ts.map +1 -1
- package/dist/internal/metadata.js +5 -15
- package/dist/internal/overlayDiff.d.ts +2 -2
- package/dist/internal/overlayDiff.d.ts.map +1 -1
- package/dist/internal/overlayDiff.js +0 -3
- package/dist/internal/path.d.ts +17 -5
- package/dist/internal/path.d.ts.map +1 -1
- package/dist/internal/path.js +38 -24
- package/dist/internal/snapshotDelta.d.ts +61 -60
- package/dist/internal/snapshotDelta.d.ts.map +1 -1
- package/dist/internal/snapshotDelta.js +612 -504
- package/dist/internal/snapshotDeltaModel.d.ts.map +1 -1
- package/dist/internal/snapshotDeltaModel.js +0 -1
- package/dist/internal/stagedState.d.ts +11 -15
- package/dist/internal/stagedState.d.ts.map +1 -1
- package/dist/internal/stagedState.js +15 -59
- package/dist/internal/testSeams.d.ts +17 -0
- package/dist/internal/testSeams.d.ts.map +1 -0
- package/dist/internal/testSeams.js +20 -0
- package/dist/internal/tree.d.ts +225 -0
- package/dist/internal/tree.d.ts.map +1 -0
- package/dist/internal/tree.js +385 -0
- package/dist/internal/turnstile.d.ts +8 -0
- package/dist/internal/turnstile.d.ts.map +1 -0
- package/dist/internal/turnstile.js +43 -0
- package/dist/internal/virtualFileSystem.d.ts +39 -489
- package/dist/internal/virtualFileSystem.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem.js +2112 -2465
- package/dist/internal/volumeState.d.ts +110 -0
- package/dist/internal/volumeState.d.ts.map +1 -0
- package/dist/internal/volumeState.js +204 -0
- package/dist/internal/watchHub.d.ts +15 -6
- package/dist/internal/watchHub.d.ts.map +1 -1
- package/dist/internal/watchHub.js +56 -42
- package/package.json +48 -4
- package/dist/VirtualFileSystemError.d.ts +0 -54
- package/dist/VirtualFileSystemError.d.ts.map +0 -1
- package/dist/VirtualFileSystemError.js +0 -49
- package/dist/internal/overlayContent.d.ts +0 -15
- package/dist/internal/overlayContent.d.ts.map +0 -1
- package/dist/internal/overlayContent.js +0 -29
- package/dist/internal/testHooks.d.ts +0 -20
- package/dist/internal/testHooks.d.ts.map +0 -1
- package/dist/internal/testHooks.js +0 -23
package/dist/internal/image.js
CHANGED
|
@@ -1,187 +1,157 @@
|
|
|
1
|
-
// Snapshot validation and serialization used by VirtualFileSystem.
|
|
2
|
-
import * as ByteSize from "effect/ByteSize";
|
|
3
1
|
import * as Effect from "effect/Effect";
|
|
4
|
-
import * as
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import * as Predicate from "effect/Predicate";
|
|
5
4
|
import * as Result from "effect/Result";
|
|
6
5
|
import * as Schema from "effect/Schema";
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import * as Stream from "effect/Stream";
|
|
7
|
+
import { decodeOption } from "../BytePath.js";
|
|
8
|
+
import { SnapshotTypeId } from "../Snapshot.js";
|
|
9
|
+
import { make as makeBytePath } from "./bytePath.js";
|
|
10
|
+
import { fsFailure, imageFailure } from "./errors.js";
|
|
11
|
+
import * as Lines from "./lines.js";
|
|
12
|
+
import { DOT_DOT_HEX, DOT_HEX, joinPath, MAX_SYMLINK_TRAVERSALS, nameBytes, ownedPath, preparePath } from "./path.js";
|
|
13
|
+
import * as Tree from "./tree.js";
|
|
14
|
+
import { assemble, byEntryName, getNode, ROOT_INO, storedMetadata } from "./volumeState.js";
|
|
15
|
+
// Volume values are immutable, so snapshots share them without exposing their state.
|
|
10
16
|
class SnapshotImpl {
|
|
11
17
|
[SnapshotTypeId] = SnapshotTypeId;
|
|
18
|
+
#value;
|
|
19
|
+
constructor(value) {
|
|
20
|
+
this.#value = value;
|
|
21
|
+
}
|
|
22
|
+
static valueOf(snapshot) {
|
|
23
|
+
return #value in snapshot ? snapshot.#value : undefined;
|
|
24
|
+
}
|
|
12
25
|
}
|
|
13
26
|
/** @internal */
|
|
14
|
-
export
|
|
15
|
-
const id = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(128));
|
|
27
|
+
export const make = (value) => Object.freeze(new SnapshotImpl(value));
|
|
16
28
|
/** @internal */
|
|
17
|
-
export const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
},
|
|
23
|
-
file: { id, metadata: StoredMetadata, data: CanonicalBase64.Encoded },
|
|
24
|
-
symlink: { id, metadata: StoredMetadata, target: CanonicalBase64.Encoded }
|
|
29
|
+
export const valueOf = (snapshot) => Effect.suspend(() => {
|
|
30
|
+
const value = SnapshotImpl.valueOf(snapshot);
|
|
31
|
+
return value === undefined
|
|
32
|
+
? imageFailure("decodeSnapshot", "InvalidStructure", { field: "snapshot" })
|
|
33
|
+
: Effect.succeed(value);
|
|
25
34
|
});
|
|
35
|
+
const HEADER = JSON.stringify(Tree.SnapshotHeader.make({ format: "effect-vfs", version: 1 }));
|
|
36
|
+
const nodeText = Schema.encodeResult(Schema.fromJsonString(Tree.TreeNode));
|
|
37
|
+
// Charge encoded lines by decoder rules so any successful output fits the same budget.
|
|
26
38
|
/** @internal */
|
|
27
|
-
export const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
file: { id, metadata: WireStoredMetadata, data: Schema.String },
|
|
40
|
-
symlink: { id, metadata: WireStoredMetadata, target: Schema.String }
|
|
41
|
-
});
|
|
42
|
-
const WireDocument = Schema.Struct({
|
|
43
|
-
format: Schema.Literal("effect-vfs"),
|
|
44
|
-
version: Schema.Literal(1),
|
|
45
|
-
root: id,
|
|
46
|
-
records: Schema.Array(WireRecord)
|
|
47
|
-
});
|
|
48
|
-
const VersionProbe = Schema.Struct({
|
|
49
|
-
format: Schema.Literal("effect-vfs"),
|
|
50
|
-
version: Schema.Unknown
|
|
51
|
-
});
|
|
52
|
-
const snapshots = new WeakMap();
|
|
39
|
+
export const encodeSnapshotStream = (snapshot, budget) => Stream.unwrap(Effect.gen(function* () {
|
|
40
|
+
const nodes = yield* Tree.treeNodes(yield* valueOf(snapshot));
|
|
41
|
+
const operation = "encodeSnapshot";
|
|
42
|
+
return Tree.writeTree({
|
|
43
|
+
operation,
|
|
44
|
+
documentField: "text",
|
|
45
|
+
header: HEADER,
|
|
46
|
+
node: Tree.treeNode,
|
|
47
|
+
text: nodeText,
|
|
48
|
+
meter: budget === undefined ? undefined : Tree.meter(budget, operation)
|
|
49
|
+
}, nodes);
|
|
50
|
+
}));
|
|
53
51
|
/** @internal */
|
|
54
|
-
export const
|
|
55
|
-
const document = snapshots.get(snapshot);
|
|
56
|
-
return document === undefined
|
|
57
|
-
? new ImageError({ code: "InvalidStructure", field: "snapshot" })
|
|
58
|
-
: Effect.succeed(document);
|
|
59
|
-
});
|
|
52
|
+
export const encodeSnapshot = (snapshot, budget) => Lines.collect(encodeSnapshotStream(snapshot, budget));
|
|
60
53
|
/** @internal */
|
|
61
|
-
export const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const values = Match.value(record).pipe(Match.tag("directory", (record) => record.entries.map((entry) => entry.name)), Match.tag("file", (record) => [record.data]), Match.tag("symlink", (record) => [record.target]), Match.exhaustive);
|
|
94
|
-
for (const value of values) {
|
|
95
|
-
if (!CanonicalBase64.is(value)) {
|
|
96
|
-
return yield* new ImageError({ code: "InvalidEncoding", field: record.id });
|
|
97
|
-
}
|
|
98
|
-
payload = ByteSize.sum(payload, ByteSize.bytes(CanonicalBase64.decodedLength(value)));
|
|
99
|
-
if (limits !== undefined && ByteSize.isGreaterThan(payload, limits.maxDecodedBytes)) {
|
|
100
|
-
return yield* new ImageError({ code: "LimitExceeded", field: "bytes" });
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
const root = records.get(document.root);
|
|
106
|
-
if (root === undefined || !Record.guards.directory(root)) {
|
|
107
|
-
return yield* new ImageError({ code: "InvalidStructure", field: "root" });
|
|
108
|
-
}
|
|
109
|
-
const parents = new Map();
|
|
110
|
-
for (const record of document.records) {
|
|
111
|
-
if (Record.guards.directory(record)) {
|
|
112
|
-
const names = new Set();
|
|
113
|
-
for (const entry of record.entries) {
|
|
114
|
-
if (names.has(entry.name) || CanonicalBase64.decodedLength(entry.name) < 1 ||
|
|
115
|
-
CanonicalBase64.decodedLength(entry.name) > 255)
|
|
116
|
-
return yield* new ImageError({ code: "InvalidStructure", field: "name" });
|
|
117
|
-
names.add(entry.name);
|
|
118
|
-
const name = yield* CanonicalBase64.decode(entry.name);
|
|
119
|
-
if (name.includes(0) || name.includes(47) || (name.length === 1 && name[0] === 46) ||
|
|
120
|
-
(name.length === 2 && name[0] === 46 && name[1] === 46))
|
|
121
|
-
return yield* new ImageError({ code: "InvalidStructure", field: "name" });
|
|
122
|
-
const target = records.get(entry.target);
|
|
123
|
-
if (target === undefined)
|
|
124
|
-
return yield* new ImageError({ code: "InvalidStructure", field: "target" });
|
|
125
|
-
parents.set(target.id, (parents.get(target.id) ?? 0) + 1);
|
|
126
|
-
}
|
|
54
|
+
export const decodeSnapshotSink = (budget) => {
|
|
55
|
+
const operation = "decodeSnapshot";
|
|
56
|
+
return Lines.sink({ operation, inputField: "input", textField: "text" }, () => {
|
|
57
|
+
const meter = Tree.meter(budget, operation);
|
|
58
|
+
const reader = Tree.snapshotReader({ operation, documentField: "document", meter });
|
|
59
|
+
return {
|
|
60
|
+
meter,
|
|
61
|
+
fold: { line: reader.line, end: () => Result.map(reader.end(), (read) => make(assemble(read.specs))) }
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
const OPERATION = "snapshotEntries";
|
|
66
|
+
const encoder = new TextEncoder();
|
|
67
|
+
// Match privileged lookup: follow intermediate links, honor trailing slashes, and leave the final link intact.
|
|
68
|
+
const resolve = (value, input) => {
|
|
69
|
+
const fail = (code) => Result.fail(fsFailure(code, OPERATION, { path: input }));
|
|
70
|
+
const prepared = preparePath(input, OPERATION, undefined);
|
|
71
|
+
if (Result.isFailure(prepared))
|
|
72
|
+
return Result.fail(prepared.failure);
|
|
73
|
+
const root = getNode(value, ROOT_INO);
|
|
74
|
+
let work = prepared.success;
|
|
75
|
+
let current = root;
|
|
76
|
+
let traversals = 0;
|
|
77
|
+
for (let index = 0; index < work.components.length; index++) {
|
|
78
|
+
if (current.kind !== "directory")
|
|
79
|
+
return fail("NotDirectory");
|
|
80
|
+
const component = work.components[index];
|
|
81
|
+
if (component === DOT_HEX)
|
|
82
|
+
continue;
|
|
83
|
+
if (component === DOT_DOT_HEX) {
|
|
84
|
+
current = getNode(value, current.parent);
|
|
85
|
+
continue;
|
|
127
86
|
}
|
|
128
|
-
|
|
129
|
-
|
|
87
|
+
const ino = current.entries.get(component);
|
|
88
|
+
const child = ino === undefined ? undefined : getNode(value, ino);
|
|
89
|
+
if (child === undefined)
|
|
90
|
+
return fail("NotFound");
|
|
91
|
+
if (child.kind !== "symlink" || (index === work.components.length - 1 && !work.trailingSlash)) {
|
|
92
|
+
current = child;
|
|
93
|
+
continue;
|
|
130
94
|
}
|
|
95
|
+
if (child.target.length === 0)
|
|
96
|
+
return fail("NotFound");
|
|
97
|
+
if (++traversals > MAX_SYMLINK_TRAVERSALS)
|
|
98
|
+
return fail("SymlinkLoop");
|
|
99
|
+
const suffix = work.suffixes[index] ?? new Uint8Array(0);
|
|
100
|
+
const expansion = new Uint8Array(child.target.length + suffix.length);
|
|
101
|
+
expansion.set(child.target);
|
|
102
|
+
expansion.set(suffix, child.target.length);
|
|
103
|
+
const expanded = preparePath(ownedPath(expansion), OPERATION, undefined);
|
|
104
|
+
if (Result.isFailure(expanded))
|
|
105
|
+
return fail(expanded.failure.code);
|
|
106
|
+
work = expanded.success;
|
|
107
|
+
if (work.absolute)
|
|
108
|
+
current = root;
|
|
109
|
+
index = -1;
|
|
131
110
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
111
|
+
return work.trailingSlash && current.kind !== "directory" ? fail("NotDirectory") : Result.succeed(current);
|
|
112
|
+
};
|
|
113
|
+
const text = (bytes) => Option.getOrUndefined(decodeOption(bytes));
|
|
114
|
+
const childPath = (parent, name) => {
|
|
115
|
+
const named = text(name);
|
|
116
|
+
if (Predicate.isString(parent) && named !== undefined)
|
|
117
|
+
return parent === "/" ? `/${named}` : `${parent}/${named}`;
|
|
118
|
+
return joinPath(Predicate.isString(parent) ? encoder.encode(parent) : parent, name);
|
|
119
|
+
};
|
|
120
|
+
const pathInput = (path) => (Predicate.isString(path) ? path : makeBytePath(path));
|
|
121
|
+
// Emit the first hard-link path as content and later paths as links; copy file bytes only on pull.
|
|
122
|
+
function* walk(value, node) {
|
|
123
|
+
const pending = [[node, "/"]];
|
|
124
|
+
const first = new Map();
|
|
125
|
+
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
|
126
|
+
const [current, walked] = next;
|
|
127
|
+
const path = pathInput(walked);
|
|
128
|
+
const metadata = storedMetadata(current.metadata);
|
|
129
|
+
if (current.kind === "directory") {
|
|
130
|
+
yield { kind: "directory", path, metadata };
|
|
131
|
+
const children = [...current.entries].sort(byEntryName).reverse();
|
|
132
|
+
for (const [name, ino] of children)
|
|
133
|
+
pending.push([getNode(value, ino), childPath(walked, nameBytes(name))]);
|
|
134
|
+
continue;
|
|
138
135
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
while (pending.length > 0) {
|
|
143
|
-
const next = pending.pop();
|
|
144
|
-
if (next === undefined || visited.has(next))
|
|
136
|
+
const alias = current.links.length > 1 ? first.get(current.ino) : undefined;
|
|
137
|
+
if (alias !== undefined) {
|
|
138
|
+
yield { kind: "hardLink", path, target: alias };
|
|
145
139
|
continue;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
140
|
+
}
|
|
141
|
+
if (current.links.length > 1)
|
|
142
|
+
first.set(current.ino, path);
|
|
143
|
+
if (current.kind === "file")
|
|
144
|
+
yield { kind: "file", path, bytes: current.data.slice(), metadata };
|
|
145
|
+
else {
|
|
146
|
+
const target = text(current.target);
|
|
147
|
+
yield { kind: "symlink", path, target: target ?? makeBytePath(current.target.slice()), metadata };
|
|
153
148
|
}
|
|
154
149
|
}
|
|
155
|
-
|
|
156
|
-
return yield* new ImageError({ code: "InvalidStructure", field: "reachability" });
|
|
157
|
-
const snapshot = Object.freeze(new SnapshotImpl());
|
|
158
|
-
snapshots.set(snapshot, document);
|
|
159
|
-
return snapshot;
|
|
160
|
-
});
|
|
161
|
-
/** @internal */
|
|
162
|
-
export const encodeSnapshot = Effect.fn("VirtualFileSystem.encodeSnapshot")(function* (snapshot) {
|
|
163
|
-
const text = yield* Schema.encodeEffect(Schema.fromJsonString(Document))(yield* inspect(snapshot)).pipe(Effect.mapError(() => new ImageError({ code: "InvalidStructure", field: "text" })));
|
|
164
|
-
return new TextEncoder().encode(text);
|
|
165
|
-
});
|
|
150
|
+
}
|
|
166
151
|
/** @internal */
|
|
167
|
-
export const
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
if (ByteSize.isGreaterThan(ByteSize.bytes(input.byteLength), checked.success.maxEncodedBytes)) {
|
|
175
|
-
return yield* new ImageError({ code: "LimitExceeded", field: "encodedBytes" });
|
|
176
|
-
}
|
|
177
|
-
const text = yield* Effect.try({
|
|
178
|
-
try: () => new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array(input)),
|
|
179
|
-
catch: () => new ImageError({ code: "InvalidEncoding", field: "text" })
|
|
180
|
-
});
|
|
181
|
-
const value = yield* Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown))(text).pipe(Effect.mapError(() => new ImageError({ code: "InvalidEncoding", field: "text" })));
|
|
182
|
-
const version = Schema.decodeUnknownResult(VersionProbe)(value);
|
|
183
|
-
if (Result.isSuccess(version) && version.success.version !== 1) {
|
|
184
|
-
return yield* new ImageError({ code: "UnsupportedVersion", field: "version" });
|
|
185
|
-
}
|
|
186
|
-
return yield* capture(value, checked.success);
|
|
187
|
-
});
|
|
152
|
+
export const snapshotEntries = (snapshot, root) => Stream.unwrap(Effect.gen(function* () {
|
|
153
|
+
const value = yield* valueOf(snapshot);
|
|
154
|
+
const node = yield* Effect.fromResult(resolve(value, root));
|
|
155
|
+
// Bound each pulled chunk to one file's copied bytes.
|
|
156
|
+
return Stream.fromIterable({ [Symbol.iterator]: () => walk(value, node) }, { chunkSize: 1 });
|
|
157
|
+
}));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
export type Owner = symbol;
|
|
3
|
+
interface Slots<A> {
|
|
4
|
+
readonly owner: Owner | undefined;
|
|
5
|
+
readonly items: Array<Slots<A> | A | undefined>;
|
|
6
|
+
}
|
|
7
|
+
/** @internal */
|
|
8
|
+
export interface InodeTable<A> {
|
|
9
|
+
readonly root: Slots<A>;
|
|
10
|
+
readonly levels: number;
|
|
11
|
+
readonly capacity: number;
|
|
12
|
+
}
|
|
13
|
+
/** @internal */
|
|
14
|
+
export declare const empty: <A>() => InodeTable<A>;
|
|
15
|
+
/** @internal */
|
|
16
|
+
export declare const get: <A>(table: InodeTable<A>, key: number) => A | undefined;
|
|
17
|
+
/** @internal */
|
|
18
|
+
export declare const set: <A>(table: InodeTable<A>, key: number, value: A | undefined, owner?: Owner) => InodeTable<A>;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=inodeTable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inodeTable.d.ts","sourceRoot":"","sources":["../../src/internal/inodeTable.ts"],"names":[],"mappings":"AAQA,gBAAgB;AAChB,MAAM,MAAM,KAAK,GAAG,MAAM,CAAA;AAE1B,UAAU,KAAK,CAAC,CAAC;IACf,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,CAAA;IACjC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,CAAA;CAChD;AAED,gBAAgB;AAChB,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED,gBAAgB;AAChB,eAAO,MAAM,KAAK,GAAI,CAAC,OAAK,UAAU,CAAC,CAAC,CAA4E,CAAA;AAKpH,gBAAgB;AAChB,eAAO,MAAM,GAAG,GAAI,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,OAAO,MAAM,KAAG,CAAC,GAAG,SAgB9D,CAAA;AAsBD,gBAAgB;AAChB,eAAO,MAAM,GAAG,GAAI,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,OAAO,MAAM,SAAS,CAAC,GAAG,SAAS,UAAU,KAAK,KAAG,UAAU,CAAC,CAAC,CAU3G,CAAA"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// A 32-way persistent trie. One owner may mutate paths it has already copied within a batch.
|
|
2
|
+
const BITS = 5;
|
|
3
|
+
const WIDTH = 1 << BITS;
|
|
4
|
+
const MASK = WIDTH - 1;
|
|
5
|
+
const SHIFTABLE = 0x100000000; // Shifts wrap keys at 2^32 and shift counts at 32.
|
|
6
|
+
const MAX_SHIFT = 32;
|
|
7
|
+
/** @internal */
|
|
8
|
+
export const empty = () => ({ root: { owner: undefined, items: [] }, levels: 1, capacity: WIDTH });
|
|
9
|
+
const index = (key, shift) => key < SHIFTABLE && shift < MAX_SHIFT ? (key >>> shift) & MASK : Math.floor(key / 2 ** shift) % WIDTH;
|
|
10
|
+
/** @internal */
|
|
11
|
+
export const get = (table, key) => {
|
|
12
|
+
if (key >= table.capacity)
|
|
13
|
+
return undefined;
|
|
14
|
+
let node = table.root;
|
|
15
|
+
let shift = (table.levels - 1) * BITS;
|
|
16
|
+
while (shift > 0) {
|
|
17
|
+
const next = node.items[index(key, shift)];
|
|
18
|
+
if (next === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
// SAFETY: every slot above the leaf level holds a Slots value or nothing.
|
|
21
|
+
node = next;
|
|
22
|
+
shift -= BITS;
|
|
23
|
+
}
|
|
24
|
+
// SAFETY: leaf level slots hold values or nothing.
|
|
25
|
+
return node.items[key & MASK];
|
|
26
|
+
};
|
|
27
|
+
const setIn = (node, key, value, shift, owner) => {
|
|
28
|
+
const target = owner !== undefined && node.owner === owner ? node : { owner, items: node.items.slice() };
|
|
29
|
+
if (shift === 0)
|
|
30
|
+
target.items[key & MASK] = value;
|
|
31
|
+
else {
|
|
32
|
+
const at = index(key, shift);
|
|
33
|
+
// SAFETY: every slot above the leaf level holds a Slots value or nothing.
|
|
34
|
+
const child = node.items[at] ?? { owner: undefined, items: [] };
|
|
35
|
+
target.items[at] = setIn(child, key, value, shift - BITS, owner);
|
|
36
|
+
}
|
|
37
|
+
return target;
|
|
38
|
+
};
|
|
39
|
+
/** @internal */
|
|
40
|
+
export const set = (table, key, value, owner) => {
|
|
41
|
+
let { capacity, levels, root } = table;
|
|
42
|
+
while (key >= capacity) {
|
|
43
|
+
root = { owner, items: [root] };
|
|
44
|
+
levels += 1;
|
|
45
|
+
capacity *= WIDTH;
|
|
46
|
+
}
|
|
47
|
+
return { root: setIn(root, key, value, (levels - 1) * BITS, owner), levels, capacity };
|
|
48
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as ByteSize from "effect/ByteSize";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Result from "effect/Result";
|
|
4
|
+
import type * as Schema from "effect/Schema";
|
|
5
|
+
import * as Sink from "effect/Sink";
|
|
6
|
+
import * as Stream from "effect/Stream";
|
|
7
|
+
import type { ImageFailure } from "../VfsError.js";
|
|
8
|
+
/** @internal */
|
|
9
|
+
export interface Framing {
|
|
10
|
+
readonly operation: string;
|
|
11
|
+
readonly inputField: string;
|
|
12
|
+
readonly textField: string;
|
|
13
|
+
}
|
|
14
|
+
/** @internal */
|
|
15
|
+
export interface FrameMeter {
|
|
16
|
+
readonly line: (bytes: number, ended: boolean) => Result.Result<void, ImageFailure>;
|
|
17
|
+
}
|
|
18
|
+
/** @internal */
|
|
19
|
+
export interface LineFold<A> {
|
|
20
|
+
readonly line: (value: typeof Schema.Unknown.Type, index: number) => Result.Result<void, ImageFailure>;
|
|
21
|
+
readonly end: () => Result.Result<A, ImageFailure>;
|
|
22
|
+
}
|
|
23
|
+
/** @internal */
|
|
24
|
+
export declare const byteLimit: (limit: ByteSize.ByteSize) => number;
|
|
25
|
+
/** @internal */
|
|
26
|
+
export declare const sink: <A>(framing: Framing, start: () => {
|
|
27
|
+
readonly meter: FrameMeter;
|
|
28
|
+
readonly fold: LineFold<A>;
|
|
29
|
+
}) => Sink.Sink<A, Uint8Array, never, ImageFailure>;
|
|
30
|
+
/** @internal */
|
|
31
|
+
export declare const frame: (lines: ReadonlyArray<Uint8Array>) => Uint8Array;
|
|
32
|
+
/** @internal */
|
|
33
|
+
export declare const encodeLine: (text: string) => Uint8Array;
|
|
34
|
+
/** @internal */
|
|
35
|
+
export declare const collect: <E, R>(stream: Stream.Stream<Uint8Array, E, R>) => Effect.Effect<Uint8Array, E, R>;
|
|
36
|
+
//# sourceMappingURL=lines.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lines.d.ts","sourceRoot":"","sources":["../../src/internal/lines.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAC5C,OAAO,KAAK,IAAI,MAAM,aAAa,CAAA;AACnC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAQlD,gBAAgB;AAChB,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,gBAAgB;AAChB,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;CACpF;AAED,gBAAgB;AAChB,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;IACtG,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,YAAY,CAAC,CAAA;CACnD;AAED,gBAAgB;AAChB,eAAO,MAAM,SAAS,UAAW,QAAQ,CAAC,QAAQ,KAAG,MAIpD,CAAA;AAkGD,gBAAgB;AAChB,eAAO,MAAM,IAAI,GAAI,CAAC,WACX,OAAO,SACT,MAAM;IAAE,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAA;CAAE,KACtE,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,YAAY,CAUsB,CAAA;AAIrE,gBAAgB;AAChB,eAAO,MAAM,KAAK,UAAW,aAAa,CAAC,UAAU,CAAC,KAAG,UAWxD,CAAA;AAED,gBAAgB;AAChB,eAAO,MAAM,UAAU,SAAU,MAAM,KAAG,UAAkC,CAAA;AAE5E,gBAAgB;AAChB,eAAO,MAAM,OAAO,GAAI,CAAC,EAAE,CAAC,UAAU,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC,KAAG,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,EAAE,CAAC,CAInG,CAAA"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Tree images require bounded, newline-terminated JSON lines with strict UTF-8. Text stream operators cannot
|
|
2
|
+
// enforce these byte limits or reject malformed UTF-8 and lone carriage returns.
|
|
3
|
+
import * as ByteSize from "effect/ByteSize";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Result from "effect/Result";
|
|
6
|
+
import * as Sink from "effect/Sink";
|
|
7
|
+
import * as Stream from "effect/Stream";
|
|
8
|
+
import { imageFailure } from "./errors.js";
|
|
9
|
+
import { isAttachedBytes } from "./path.js";
|
|
10
|
+
import { WALK_YIELD_INTERVAL } from "./volumeState.js";
|
|
11
|
+
const NEWLINE = 0x0a;
|
|
12
|
+
const CARRIAGE_RETURN = 0x0d;
|
|
13
|
+
/** @internal */
|
|
14
|
+
export const byteLimit = (limit) => {
|
|
15
|
+
const bytes = ByteSize.toBigInt(limit);
|
|
16
|
+
return bytes > BigInt(Number.MAX_SAFE_INTEGER) ? Number.POSITIVE_INFINITY : Number(bytes);
|
|
17
|
+
};
|
|
18
|
+
const concat = (pieces, length) => {
|
|
19
|
+
const out = new Uint8Array(length);
|
|
20
|
+
let offset = 0;
|
|
21
|
+
for (const piece of pieces) {
|
|
22
|
+
out.set(piece, offset);
|
|
23
|
+
offset += piece.length;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
};
|
|
27
|
+
const EMPTY = new Uint8Array(0);
|
|
28
|
+
const makeReader = (framing, meter, fold) => {
|
|
29
|
+
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
30
|
+
const invalid = (cause) => imageFailure(framing.operation, "InvalidEncoding", { field: framing.textField, cause });
|
|
31
|
+
// Chunks may be reused after reading, so an unfinished line owns its bytes.
|
|
32
|
+
let pending = EMPTY;
|
|
33
|
+
let pendingBytes = 0;
|
|
34
|
+
let lines = 0;
|
|
35
|
+
const hold = (piece, length) => {
|
|
36
|
+
if (length > pending.length) {
|
|
37
|
+
const grown = new Uint8Array(Math.max(length, pending.length * 2));
|
|
38
|
+
grown.set(pending.subarray(0, pendingBytes));
|
|
39
|
+
pending = grown;
|
|
40
|
+
}
|
|
41
|
+
pending.set(piece, pendingBytes);
|
|
42
|
+
pendingBytes = length;
|
|
43
|
+
};
|
|
44
|
+
const parse = (bytes) => {
|
|
45
|
+
if (bytes.length === 0 || bytes[bytes.length - 1] === CARRIAGE_RETURN)
|
|
46
|
+
return Result.fail(invalid());
|
|
47
|
+
try {
|
|
48
|
+
return Result.succeed(JSON.parse(decoder.decode(bytes)));
|
|
49
|
+
}
|
|
50
|
+
catch (cause) {
|
|
51
|
+
return Result.fail(invalid(cause));
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const scan = (chunk, from) => {
|
|
55
|
+
for (let start = from, batch = 0; start < chunk.length; batch++) {
|
|
56
|
+
if (batch === WALK_YIELD_INTERVAL)
|
|
57
|
+
return Result.succeed(start);
|
|
58
|
+
const newline = chunk.indexOf(NEWLINE, start);
|
|
59
|
+
const stop = newline < 0 ? chunk.length : newline;
|
|
60
|
+
const length = pendingBytes + stop - start;
|
|
61
|
+
const bounded = meter.line(length, newline >= 0);
|
|
62
|
+
if (Result.isFailure(bounded))
|
|
63
|
+
return Result.fail(bounded.failure);
|
|
64
|
+
if (newline < 0) {
|
|
65
|
+
hold(chunk.subarray(start), length);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
let bytes = chunk.subarray(start, stop);
|
|
69
|
+
if (pendingBytes > 0) {
|
|
70
|
+
hold(bytes, length);
|
|
71
|
+
bytes = pending.subarray(0, length);
|
|
72
|
+
pending = EMPTY;
|
|
73
|
+
pendingBytes = 0;
|
|
74
|
+
}
|
|
75
|
+
start = newline + 1;
|
|
76
|
+
const read = Result.flatMap(parse(bytes), (value) => fold.line(value, lines++));
|
|
77
|
+
if (Result.isFailure(read))
|
|
78
|
+
return Result.fail(read.failure);
|
|
79
|
+
}
|
|
80
|
+
return Result.succeed(chunk.length);
|
|
81
|
+
};
|
|
82
|
+
const push = Effect.fnUntraced(function* (chunk) {
|
|
83
|
+
if (!isAttachedBytes(chunk)) {
|
|
84
|
+
return yield* imageFailure(framing.operation, "InvalidEncoding", { field: framing.inputField });
|
|
85
|
+
}
|
|
86
|
+
// Yield during large chunks so other fibers run and interruption is observed.
|
|
87
|
+
for (let start = yield* Effect.fromResult(scan(chunk, 0)); start < chunk.length;) {
|
|
88
|
+
yield* Effect.yieldNow;
|
|
89
|
+
start = yield* Effect.fromResult(scan(chunk, start));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
const end = () => (pendingBytes > 0 ? Result.fail(invalid()) : fold.end());
|
|
93
|
+
return { push, end };
|
|
94
|
+
};
|
|
95
|
+
/** @internal */
|
|
96
|
+
export const sink = (framing, start) => Sink.foldArray(() => {
|
|
97
|
+
const { fold, meter } = start();
|
|
98
|
+
return makeReader(framing, meter, fold);
|
|
99
|
+
}, () => true, (reader, chunks) => Effect.as(Effect.forEach(chunks, (chunk) => reader.push(chunk), { discard: true }), reader)).pipe(Sink.mapEffect((reader) => Effect.fromResult(reader.end())));
|
|
100
|
+
const encoder = new TextEncoder();
|
|
101
|
+
/** @internal */
|
|
102
|
+
export const frame = (lines) => {
|
|
103
|
+
const out = new Uint8Array(lines.reduce((sum, line) => sum + line.length + 1, 0));
|
|
104
|
+
let offset = 0;
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
out.set(line, offset);
|
|
107
|
+
offset += line.length;
|
|
108
|
+
out[offset++] = NEWLINE;
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
};
|
|
112
|
+
/** @internal */
|
|
113
|
+
export const encodeLine = (text) => encoder.encode(text);
|
|
114
|
+
/** @internal */
|
|
115
|
+
export const collect = (stream) => Effect.map(Stream.runCollect(stream), (chunks) => concat(chunks, chunks.reduce((sum, chunk) => sum + chunk.length, 0)));
|