@effect-vfs/core 0.4.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 +39 -32
- 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 +15 -22
- package/dist/LiveVolume.d.ts.map +1 -1
- package/dist/LiveVolume.js +19 -15
- 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 +450 -117
- 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 +54 -0
- package/dist/internal/errors.d.ts.map +1 -0
- package/dist/internal/errors.js +91 -0
- package/dist/internal/{virtualFileSystem/fixture.d.ts → fixture.d.ts} +8 -8
- package/dist/internal/fixture.d.ts.map +1 -0
- package/dist/internal/fixture.js +133 -0
- 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/{virtualFileSystem/overlayDiff.d.ts → overlayDiff.d.ts} +3 -3
- package/dist/internal/overlayDiff.d.ts.map +1 -0
- package/dist/internal/{virtualFileSystem/overlayDiff.js → overlayDiff.js} +1 -4
- package/dist/internal/{virtualFileSystem/path.d.ts → path.d.ts} +19 -7
- package/dist/internal/path.d.ts.map +1 -0
- package/dist/internal/{virtualFileSystem/path.js → path.js} +39 -25
- 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 +18 -0
- package/dist/internal/stagedState.d.ts.map +1 -0
- package/dist/internal/stagedState.js +18 -0
- 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 +41 -491
- package/dist/internal/virtualFileSystem.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem.js +2118 -2471
- 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 +25 -0
- package/dist/internal/watchHub.d.ts.map +1 -0
- package/dist/internal/watchHub.js +68 -0
- package/package.json +53 -9
- package/dist/VirtualFileSystemError.d.ts +0 -54
- package/dist/VirtualFileSystemError.d.ts.map +0 -1
- package/dist/VirtualFileSystemError.js +0 -49
- package/dist/internal/virtualFileSystem/errors.d.ts +0 -12
- package/dist/internal/virtualFileSystem/errors.d.ts.map +0 -1
- package/dist/internal/virtualFileSystem/errors.js +0 -47
- package/dist/internal/virtualFileSystem/fixture.d.ts.map +0 -1
- package/dist/internal/virtualFileSystem/fixture.js +0 -150
- package/dist/internal/virtualFileSystem/overlayContent.d.ts +0 -15
- package/dist/internal/virtualFileSystem/overlayContent.d.ts.map +0 -1
- package/dist/internal/virtualFileSystem/overlayContent.js +0 -29
- package/dist/internal/virtualFileSystem/overlayDiff.d.ts.map +0 -1
- package/dist/internal/virtualFileSystem/path.d.ts.map +0 -1
- package/dist/internal/virtualFileSystem/stagedState.d.ts +0 -22
- package/dist/internal/virtualFileSystem/stagedState.d.ts.map +0 -1
- package/dist/internal/virtualFileSystem/stagedState.js +0 -62
- package/dist/internal/virtualFileSystem/testHooks.d.ts +0 -20
- package/dist/internal/virtualFileSystem/testHooks.d.ts.map +0 -1
- package/dist/internal/virtualFileSystem/testHooks.js +0 -23
- package/dist/internal/virtualFileSystem/watchHub.d.ts +0 -16
- package/dist/internal/virtualFileSystem/watchHub.d.ts.map +0 -1
- package/dist/internal/virtualFileSystem/watchHub.js +0 -54
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
// Validate each tree line as it arrives; check cross-node graph rules after the final line.
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Encoding from "effect/Encoding";
|
|
4
|
+
import * as Predicate from "effect/Predicate";
|
|
5
|
+
import * as Result from "effect/Result";
|
|
6
|
+
import * as Schema from "effect/Schema";
|
|
7
|
+
import * as SchemaTransformation from "effect/SchemaTransformation";
|
|
8
|
+
import * as Stream from "effect/Stream";
|
|
9
|
+
import { VolumeIdentity } from "../Volume.js";
|
|
10
|
+
import { CanonicalBase64 } from "./canonicalBase64.js";
|
|
11
|
+
import { ENCODING_CHECK, imageFailure, isEncodingIssue, issueSite } from "./errors.js";
|
|
12
|
+
import { KeySecret, VolumeEpoch } from "./hex128.js";
|
|
13
|
+
import { byteLimit, encodeLine, frame } from "./lines.js";
|
|
14
|
+
import { StoredMetadata } from "./metadata.js";
|
|
15
|
+
import { isNameBytes, MAX_NAME_BYTES, nameBytes, NUL_BYTE } from "./path.js";
|
|
16
|
+
import { byIno, Ino, MAX_INO, reachableNodes, ROOT_INO, storedMetadata, WALK_YIELD_INTERVAL } from "./volumeState.js";
|
|
17
|
+
const TreeIno = Schema.Int.check(Schema.isBetween({ minimum: ROOT_INO, maximum: MAX_INO }));
|
|
18
|
+
const NaturalBigInt = Schema.String.check(Schema.isPattern(/^(0|[1-9][0-9]{0,127})$/, { [ENCODING_CHECK]: true })).pipe(Schema.decodeTo(Schema.BigInt, SchemaTransformation.bigintFromString));
|
|
19
|
+
const TreeLink = Schema.Struct({ parent: TreeIno, name: CanonicalBase64.Encoded });
|
|
20
|
+
// Ref is reserved for future content-addressed storage and rejected by this reader.
|
|
21
|
+
const TreeContent = Schema.TaggedUnion({
|
|
22
|
+
Inline: { bytes: CanonicalBase64.Encoded },
|
|
23
|
+
Ref: { hash: Schema.String, size: Schema.Natural }
|
|
24
|
+
});
|
|
25
|
+
const inlineBytes = (content) => Predicate.isTagged(content, "Inline") ? content.bytes : undefined;
|
|
26
|
+
const directory = { ino: TreeIno, parent: TreeIno, name: CanonicalBase64.Encoded, metadata: StoredMetadata };
|
|
27
|
+
const file = { ino: TreeIno, links: Schema.Array(TreeLink), content: TreeContent, metadata: StoredMetadata };
|
|
28
|
+
const symlink = {
|
|
29
|
+
ino: TreeIno,
|
|
30
|
+
links: Schema.Array(TreeLink),
|
|
31
|
+
target: CanonicalBase64.Encoded,
|
|
32
|
+
metadata: StoredMetadata
|
|
33
|
+
};
|
|
34
|
+
// Snapshots omit revisions; restoring starts them afresh.
|
|
35
|
+
/** @internal */
|
|
36
|
+
export const TreeNode = Schema.TaggedUnion({ directory, file, symlink });
|
|
37
|
+
// Avoid validating each decoded node again.
|
|
38
|
+
const isDirectory = Predicate.isTagged("directory");
|
|
39
|
+
const isFile = Predicate.isTagged("file");
|
|
40
|
+
const isSymlink = Predicate.isTagged("symlink");
|
|
41
|
+
// Reopening must retain revisions for existing references and caches.
|
|
42
|
+
const rev = { rev: NaturalBigInt };
|
|
43
|
+
/** @internal */
|
|
44
|
+
export const LiveTreeNode = Schema.TaggedUnion({
|
|
45
|
+
directory: { ...directory, ...rev },
|
|
46
|
+
file: { ...file, ...rev },
|
|
47
|
+
symlink: { ...symlink, ...rev }
|
|
48
|
+
});
|
|
49
|
+
/** @internal */
|
|
50
|
+
export const SnapshotHeader = Schema.Struct({ format: Schema.Literal("effect-vfs"), version: Schema.Literal(1) });
|
|
51
|
+
const Runtime = Schema.Struct({
|
|
52
|
+
identity: VolumeIdentity,
|
|
53
|
+
epoch: VolumeEpoch,
|
|
54
|
+
keySecret: KeySecret,
|
|
55
|
+
// Inodes are number keys, so the allocator must remain exactly representable.
|
|
56
|
+
nextInode: Schema.Int.check(Schema.isBetween({ minimum: ROOT_INO + 1, maximum: Number.MAX_SAFE_INTEGER })),
|
|
57
|
+
revision: NaturalBigInt,
|
|
58
|
+
limits: Schema.Struct({
|
|
59
|
+
maxEntries: Schema.optionalKey(Schema.Natural),
|
|
60
|
+
maxBytes: Schema.optionalKey(NaturalBigInt),
|
|
61
|
+
maxFileBytes: Schema.optionalKey(NaturalBigInt),
|
|
62
|
+
maxPathBytes: Schema.optionalKey(NaturalBigInt)
|
|
63
|
+
}),
|
|
64
|
+
usage: Schema.Struct({ entries: Schema.Natural, usedBytes: NaturalBigInt })
|
|
65
|
+
});
|
|
66
|
+
/** @internal */
|
|
67
|
+
export const LiveHeader = Schema.Struct({
|
|
68
|
+
format: Schema.Literal("effect-vfs-live"),
|
|
69
|
+
version: Schema.Literal(1),
|
|
70
|
+
runtime: Runtime
|
|
71
|
+
});
|
|
72
|
+
const VersionProbe = Schema.Struct({ format: Schema.Literal("effect-vfs"), version: Schema.Unknown });
|
|
73
|
+
const STRICT = { onExcessProperty: "error" };
|
|
74
|
+
/** @internal */
|
|
75
|
+
export const meter = (budget, operation) => {
|
|
76
|
+
const fail = (field) => Result.fail(imageFailure(operation, "LimitExceeded", { field }));
|
|
77
|
+
const encodedBytes = byteLimit(budget.encodedBytes);
|
|
78
|
+
const lineBytes = byteLimit(budget.lineBytes);
|
|
79
|
+
const decodedBytes = byteLimit(budget.decodedBytes);
|
|
80
|
+
let encoded = 0;
|
|
81
|
+
let records = 0;
|
|
82
|
+
let entries = 0;
|
|
83
|
+
let decoded = 0;
|
|
84
|
+
const charge = (value) => {
|
|
85
|
+
if (value !== undefined)
|
|
86
|
+
decoded += CanonicalBase64.decodedLength(value);
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
// At a shared threshold, report the input bound regardless of chunking or read/write direction.
|
|
90
|
+
line: (bytes, ended) => {
|
|
91
|
+
const read = encoded + bytes + (ended ? 1 : 0);
|
|
92
|
+
const pastLine = bytes > lineBytes ? lineBytes + 1 : Number.POSITIVE_INFINITY;
|
|
93
|
+
const pastInput = read > encodedBytes ? encodedBytes - encoded + 1 : Number.POSITIVE_INFINITY;
|
|
94
|
+
if (pastInput !== Number.POSITIVE_INFINITY && pastInput <= pastLine)
|
|
95
|
+
return fail("encodedBytes");
|
|
96
|
+
if (pastLine !== Number.POSITIVE_INFINITY)
|
|
97
|
+
return fail("lineBytes");
|
|
98
|
+
if (ended)
|
|
99
|
+
encoded = read;
|
|
100
|
+
return Result.void;
|
|
101
|
+
},
|
|
102
|
+
node: (node) => {
|
|
103
|
+
if (++records > budget.records)
|
|
104
|
+
return fail("records");
|
|
105
|
+
if (isDirectory(node)) {
|
|
106
|
+
if (node.ino !== ROOT_INO) {
|
|
107
|
+
entries++;
|
|
108
|
+
charge(node.name);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
entries += node.links.length;
|
|
113
|
+
for (const link of node.links)
|
|
114
|
+
charge(link.name);
|
|
115
|
+
charge(isFile(node) ? inlineBytes(node.content) : node.target);
|
|
116
|
+
}
|
|
117
|
+
if (entries > budget.entries)
|
|
118
|
+
return fail("entries");
|
|
119
|
+
return decoded > decodedBytes ? fail("bytes") : Result.void;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
const at = (path, issue) => ({ path: ["nodes", ...path], issue });
|
|
124
|
+
const ROOT_RULE = "the first node is the root directory, its own parent with an empty name";
|
|
125
|
+
const malformedLine = (options) => (error) => imageFailure(options.operation, isEncodingIssue(issueSite(error.issue)) ? "InvalidEncoding" : "InvalidStructure", {
|
|
126
|
+
field: options.documentField,
|
|
127
|
+
cause: error
|
|
128
|
+
});
|
|
129
|
+
const makeReader = (options, decodeHeader, decodeNode, runtimeOf) => {
|
|
130
|
+
const { operation } = options;
|
|
131
|
+
const malformed = malformedLine(options);
|
|
132
|
+
const specs = [];
|
|
133
|
+
const names = [];
|
|
134
|
+
const parents = new Map();
|
|
135
|
+
const positions = new Map();
|
|
136
|
+
let header;
|
|
137
|
+
let runtime;
|
|
138
|
+
let count = 0;
|
|
139
|
+
let previous = 0;
|
|
140
|
+
let entries = 0;
|
|
141
|
+
let usedBytes = 0n;
|
|
142
|
+
const broken = (issue) => Result.fail(imageFailure(operation, "InvalidStructure", { field: issue.path.map(String).join("."), cause: issue.issue }));
|
|
143
|
+
const named = (parent, name, path, directory) => {
|
|
144
|
+
const length = CanonicalBase64.decodedLength(name);
|
|
145
|
+
// Reject overlong names before decoding base64.
|
|
146
|
+
const bytes = length < 1 || length > MAX_NAME_BYTES ? undefined : CanonicalBase64.toBytes(name);
|
|
147
|
+
if (bytes === undefined || !isNameBytes(bytes)) {
|
|
148
|
+
return broken(at([...path, "name"], "a name is 1 to 255 bytes without a NUL or a slash, and neither . nor .."));
|
|
149
|
+
}
|
|
150
|
+
const key = Encoding.encodeHex(bytes);
|
|
151
|
+
names.push({ parent, key, bytes: bytes.length, path, directory });
|
|
152
|
+
entries++;
|
|
153
|
+
return Result.succeed(key);
|
|
154
|
+
};
|
|
155
|
+
const read = (value, index) => {
|
|
156
|
+
const decoded = decodeNode(value);
|
|
157
|
+
if (Result.isFailure(decoded))
|
|
158
|
+
return Result.fail(malformed(decoded.failure));
|
|
159
|
+
const node = decoded.success;
|
|
160
|
+
const charged = options.meter?.node(node) ?? Result.void;
|
|
161
|
+
if (Result.isFailure(charged))
|
|
162
|
+
return charged;
|
|
163
|
+
const revision = node.rev ?? 1n;
|
|
164
|
+
if (index === 0) {
|
|
165
|
+
if (!isDirectory(node) || node.ino !== ROOT_INO || node.parent !== ROOT_INO || node.name !== "") {
|
|
166
|
+
return broken(at([0], ROOT_RULE));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else if (node.ino <= previous)
|
|
170
|
+
return broken(at([index, "ino"], "nodes ascend by inode number without repeats"));
|
|
171
|
+
if (runtime !== undefined && node.ino >= runtime.nextInode) {
|
|
172
|
+
return broken(at([index, "ino"], "every inode lies below the allocator"));
|
|
173
|
+
}
|
|
174
|
+
if (runtime !== undefined && (revision < 1n || revision > runtime.revision)) {
|
|
175
|
+
return broken(at([index, "rev"], "every revision lies between 1 and the counter"));
|
|
176
|
+
}
|
|
177
|
+
previous = node.ino;
|
|
178
|
+
count++;
|
|
179
|
+
const common = { ino: Ino(node.ino), metadata: node.metadata, revision };
|
|
180
|
+
if (isDirectory(node)) {
|
|
181
|
+
parents.set(node.ino, node.parent);
|
|
182
|
+
positions.set(node.ino, index);
|
|
183
|
+
const key = index === 0 ? Result.succeed("") : named(node.parent, node.name, [index], node.ino);
|
|
184
|
+
return Result.map(key, (name) => {
|
|
185
|
+
specs.push({ ...common, kind: "directory", parent: Ino(node.parent), name });
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// Reopening reclaims unlinked files, but their bytes still count toward stored usage.
|
|
189
|
+
const retained = runtime !== undefined && isFile(node) && node.links.length === 0;
|
|
190
|
+
if (node.links.length === 0 && !retained) {
|
|
191
|
+
return broken(at([index, "links"], "every file and symbolic link has a name"));
|
|
192
|
+
}
|
|
193
|
+
const links = [];
|
|
194
|
+
for (const [position, link] of node.links.entries()) {
|
|
195
|
+
const key = named(link.parent, link.name, [index, "links", position]);
|
|
196
|
+
if (Result.isFailure(key))
|
|
197
|
+
return Result.fail(key.failure);
|
|
198
|
+
links.push({ parent: Ino(link.parent), name: key.success });
|
|
199
|
+
}
|
|
200
|
+
if (isSymlink(node)) {
|
|
201
|
+
const target = CanonicalBase64.toBytes(node.target);
|
|
202
|
+
if (target.includes(NUL_BYTE))
|
|
203
|
+
return broken(at([index, "target"], "a symbolic link's target holds no NUL"));
|
|
204
|
+
usedBytes += BigInt(target.length);
|
|
205
|
+
specs.push({ ...common, kind: "symlink", links, target });
|
|
206
|
+
return Result.void;
|
|
207
|
+
}
|
|
208
|
+
const bytes = inlineBytes(node.content);
|
|
209
|
+
if (bytes === undefined) {
|
|
210
|
+
return Result.fail(imageFailure(operation, "UnsupportedVersion", { field: `nodes.${index}.content` }));
|
|
211
|
+
}
|
|
212
|
+
const size = BigInt(CanonicalBase64.decodedLength(bytes));
|
|
213
|
+
usedBytes += size;
|
|
214
|
+
if (runtime?.limits.maxFileBytes !== undefined && size > runtime.limits.maxFileBytes) {
|
|
215
|
+
return broken(at([index, "content"], "every file fits maxFileBytes"));
|
|
216
|
+
}
|
|
217
|
+
if (!retained)
|
|
218
|
+
specs.push({ ...common, kind: "file", links, data: CanonicalBase64.toBytes(bytes) });
|
|
219
|
+
return Result.void;
|
|
220
|
+
};
|
|
221
|
+
const graphIssue = () => {
|
|
222
|
+
const held = new Set();
|
|
223
|
+
for (const name of names) {
|
|
224
|
+
if (!parents.has(name.parent))
|
|
225
|
+
return at([...name.path, "parent"], "a name's parent is a directory in the tree");
|
|
226
|
+
const key = `${name.parent}/${name.key}`;
|
|
227
|
+
if (held.has(key))
|
|
228
|
+
return at([...name.path, "name"], "a directory holds each name once");
|
|
229
|
+
held.add(key);
|
|
230
|
+
}
|
|
231
|
+
const reachesRoot = new Set([ROOT_INO]);
|
|
232
|
+
for (const ino of parents.keys()) {
|
|
233
|
+
const chain = new Set();
|
|
234
|
+
let current = ino;
|
|
235
|
+
while (current !== undefined && !reachesRoot.has(current)) {
|
|
236
|
+
if (chain.has(current))
|
|
237
|
+
return at([positions.get(ino), "parent"], "every directory reaches the root");
|
|
238
|
+
chain.add(current);
|
|
239
|
+
current = parents.get(current);
|
|
240
|
+
}
|
|
241
|
+
for (const member of chain)
|
|
242
|
+
reachesRoot.add(member);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
// Graph validation makes this a tree, so one root walk can measure every path.
|
|
246
|
+
const pathIssue = (maxPathBytes) => {
|
|
247
|
+
const below = new Map();
|
|
248
|
+
for (const name of names) {
|
|
249
|
+
const listed = below.get(name.parent);
|
|
250
|
+
if (listed === undefined)
|
|
251
|
+
below.set(name.parent, [name]);
|
|
252
|
+
else
|
|
253
|
+
listed.push(name);
|
|
254
|
+
}
|
|
255
|
+
const pending = [[ROOT_INO, 1n]];
|
|
256
|
+
for (let index = 0; index < pending.length; index++) {
|
|
257
|
+
const [parent, parentBytes] = pending[index];
|
|
258
|
+
for (const name of below.get(parent) ?? []) {
|
|
259
|
+
const bytes = (parent === ROOT_INO ? parentBytes : parentBytes + 1n) + BigInt(name.bytes);
|
|
260
|
+
if (bytes > maxPathBytes)
|
|
261
|
+
return at([...name.path, "name"], "every path fits maxPathBytes");
|
|
262
|
+
if (name.directory !== undefined)
|
|
263
|
+
pending.push([name.directory, bytes]);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
const runtimeIssue = ({ limits, usage }) => {
|
|
268
|
+
const issue = (path, issue) => ({ path: ["runtime", ...path], issue });
|
|
269
|
+
if (entries !== usage.entries)
|
|
270
|
+
return issue(["usage", "entries"], "the stored entry count matches the nodes");
|
|
271
|
+
if (usedBytes !== usage.usedBytes)
|
|
272
|
+
return issue(["usage", "usedBytes"], "the stored byte count matches the nodes");
|
|
273
|
+
if (limits.maxEntries !== undefined && entries > limits.maxEntries) {
|
|
274
|
+
return issue(["limits", "maxEntries"], "the entries fit maxEntries");
|
|
275
|
+
}
|
|
276
|
+
if (limits.maxBytes !== undefined && usedBytes > limits.maxBytes) {
|
|
277
|
+
return issue(["limits", "maxBytes"], "the bytes fit maxBytes");
|
|
278
|
+
}
|
|
279
|
+
if (limits.maxPathBytes !== undefined) {
|
|
280
|
+
return limits.maxPathBytes < 1n
|
|
281
|
+
? issue(["limits", "maxPathBytes"], "the path limit admits the root")
|
|
282
|
+
: pathIssue(limits.maxPathBytes);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
return {
|
|
286
|
+
line: (value, index) => {
|
|
287
|
+
if (index > 0)
|
|
288
|
+
return read(value, index - 1);
|
|
289
|
+
return Result.map(decodeHeader(value), (decoded) => {
|
|
290
|
+
header = decoded;
|
|
291
|
+
runtime = runtimeOf(decoded);
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
end: () => {
|
|
295
|
+
if (header === undefined) {
|
|
296
|
+
return Result.fail(imageFailure(operation, "InvalidStructure", { field: options.documentField }));
|
|
297
|
+
}
|
|
298
|
+
if (count === 0)
|
|
299
|
+
return broken(at([0], ROOT_RULE));
|
|
300
|
+
const issue = graphIssue() ?? (runtime === undefined ? undefined : runtimeIssue(runtime));
|
|
301
|
+
return issue === undefined ? Result.succeed({ header, specs }) : broken(issue);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
};
|
|
305
|
+
// Reject later snapshot versions before checking their shape, including fields this version does not know.
|
|
306
|
+
/** @internal */
|
|
307
|
+
export const snapshotReader = (options) => makeReader(options, (value) => {
|
|
308
|
+
const version = Schema.decodeUnknownResult(VersionProbe)(value);
|
|
309
|
+
if (Result.isSuccess(version) && version.success.version !== 1) {
|
|
310
|
+
return Result.fail(imageFailure(options.operation, "UnsupportedVersion", { field: "version" }));
|
|
311
|
+
}
|
|
312
|
+
return Result.mapError(Schema.decodeUnknownResult(SnapshotHeader, STRICT)(value), malformedLine(options));
|
|
313
|
+
}, Schema.decodeUnknownResult(TreeNode, STRICT), () => undefined);
|
|
314
|
+
// Live images are private; a different version is malformed.
|
|
315
|
+
/** @internal */
|
|
316
|
+
export const liveReader = (options) => makeReader(options, (value) => Result.flatMap(Result.mapError(Schema.decodeUnknownResult(LiveHeader, STRICT)(value), malformedLine(options)), (header) => header.runtime.revision < 1n
|
|
317
|
+
? Result.fail(imageFailure(options.operation, "InvalidStructure", { field: "runtime.revision" }))
|
|
318
|
+
: Result.succeed(header)), Schema.decodeUnknownResult(LiveTreeNode, STRICT), (header) => header.runtime);
|
|
319
|
+
const encodeName = (name) => CanonicalBase64.encode(nameBytes(name));
|
|
320
|
+
// Value nodes have already passed validation.
|
|
321
|
+
/** @internal */
|
|
322
|
+
export const UNCHECKED = { disableChecks: true };
|
|
323
|
+
/** @internal */
|
|
324
|
+
export const treeNode = (node) => {
|
|
325
|
+
const metadata = storedMetadata(node.metadata);
|
|
326
|
+
if (node.kind === "directory") {
|
|
327
|
+
return TreeNode.cases.directory.make({ ino: node.ino, parent: node.parent, name: encodeName(node.name), metadata }, UNCHECKED);
|
|
328
|
+
}
|
|
329
|
+
const named = node.links.map((link) => ({ parent: link.parent, name: encodeName(link.name) }));
|
|
330
|
+
return node.kind === "file"
|
|
331
|
+
? TreeNode.cases.file.make({
|
|
332
|
+
ino: node.ino,
|
|
333
|
+
links: named,
|
|
334
|
+
content: TreeContent.cases.Inline.make({ bytes: CanonicalBase64.encode(node.data) }, UNCHECKED),
|
|
335
|
+
metadata
|
|
336
|
+
}, UNCHECKED)
|
|
337
|
+
: TreeNode.cases.symlink.make({ ino: node.ino, links: named, target: CanonicalBase64.encode(node.target), metadata }, UNCHECKED);
|
|
338
|
+
};
|
|
339
|
+
/** @internal */
|
|
340
|
+
export const treeNodes = Effect.fnUntraced(function* (value, extra = []) {
|
|
341
|
+
return [...(yield* reachableNodes(value)), ...extra].sort(byIno);
|
|
342
|
+
});
|
|
343
|
+
/** @internal */
|
|
344
|
+
export const ENCODED_CHUNK_BYTES = 64 * 1024;
|
|
345
|
+
/** @internal */
|
|
346
|
+
export const encodeFailure = (operation, field, cause) => imageFailure(operation, "InvalidStructure", { field, cause });
|
|
347
|
+
// Encode on pull. A chunk may hold one line past the byte cap when that line alone exceeds it.
|
|
348
|
+
/** @internal */
|
|
349
|
+
export const writeTree = (writer, nodes) => {
|
|
350
|
+
const charge = (line) => writer.meter === undefined ? Result.void : writer.meter.line(line.length, true);
|
|
351
|
+
// Match the reader's charge order: line, then node.
|
|
352
|
+
const encode = (source) => {
|
|
353
|
+
const node = writer.node(source);
|
|
354
|
+
const text = writer.text(node);
|
|
355
|
+
if (Result.isFailure(text)) {
|
|
356
|
+
return Result.fail(encodeFailure(writer.operation, writer.documentField, text.failure));
|
|
357
|
+
}
|
|
358
|
+
const line = encodeLine(text.success);
|
|
359
|
+
return Result.map(Result.flatMap(charge(line), () => writer.meter?.node(node) ?? Result.void), () => line);
|
|
360
|
+
};
|
|
361
|
+
const chunk = ({ carried, index }) => {
|
|
362
|
+
const lines = carried === undefined ? [] : [carried];
|
|
363
|
+
let bytes = carried === undefined ? 0 : carried.length + 1;
|
|
364
|
+
let next = index;
|
|
365
|
+
while (lines.length < WALK_YIELD_INTERVAL) {
|
|
366
|
+
const source = nodes[next];
|
|
367
|
+
if (source === undefined)
|
|
368
|
+
break;
|
|
369
|
+
next++;
|
|
370
|
+
const line = encode(source);
|
|
371
|
+
if (Result.isFailure(line))
|
|
372
|
+
return Result.fail(line.failure);
|
|
373
|
+
if (lines.length > 0 && bytes + line.success.length + 1 > ENCODED_CHUNK_BYTES) {
|
|
374
|
+
return Result.succeed([frame(lines), { index: next, carried: line.success }]);
|
|
375
|
+
}
|
|
376
|
+
lines.push(line.success);
|
|
377
|
+
bytes += line.success.length + 1;
|
|
378
|
+
}
|
|
379
|
+
return lines.length === 0
|
|
380
|
+
? Result.succeed(undefined)
|
|
381
|
+
: Result.succeed([frame(lines), { index: next, carried: undefined }]);
|
|
382
|
+
};
|
|
383
|
+
const header = encodeLine(writer.header);
|
|
384
|
+
return Stream.fromEffect(Effect.fromResult(Result.map(charge(header), () => frame([header])))).pipe(Stream.concat(Stream.unfold({ index: 0, carried: undefined }, (next) => Effect.fromResult(chunk(next)))));
|
|
385
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
/** @internal */
|
|
3
|
+
export interface Turnstile {
|
|
4
|
+
readonly withTurn: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
5
|
+
}
|
|
6
|
+
/** @internal */
|
|
7
|
+
export declare const makeTurnstile: () => Turnstile;
|
|
8
|
+
//# sourceMappingURL=turnstile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turnstile.d.ts","sourceRoot":"","sources":["../../src/internal/turnstile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAIvC,gBAAgB;AAChB,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;CACvF;AASD,gBAAgB;AAChB,eAAO,MAAM,aAAa,QAAO,SAkDhC,CAAA"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Exit from "effect/Exit";
|
|
3
|
+
// A Semaphore can let a newcomer overtake a scheduled waiter. Transfer ownership before scheduling the oldest waiter.
|
|
4
|
+
/** @internal */
|
|
5
|
+
export const makeTurnstile = () => {
|
|
6
|
+
let held = false;
|
|
7
|
+
const waiting = [];
|
|
8
|
+
const release = () => {
|
|
9
|
+
const next = waiting.shift();
|
|
10
|
+
if (next === undefined) {
|
|
11
|
+
held = false;
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
next.granted = true;
|
|
15
|
+
next.dispatcher.scheduleTask(() => next.wake?.(), 0);
|
|
16
|
+
};
|
|
17
|
+
const waitFor = (ticket) => Effect.callback((resume) => {
|
|
18
|
+
if (ticket.granted)
|
|
19
|
+
return resume(Effect.void);
|
|
20
|
+
ticket.wake = () => resume(Effect.void);
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
// Check ticket ownership after interruption so a granted turn is released rather than stranded.
|
|
24
|
+
withTurn: (effect) => Effect.uninterruptibleMask((restore) => Effect.withFiber((fiber) => {
|
|
25
|
+
const run = Effect.ensuring(restore(effect), Effect.sync(release));
|
|
26
|
+
if (!held) {
|
|
27
|
+
held = true;
|
|
28
|
+
return run;
|
|
29
|
+
}
|
|
30
|
+
const ticket = { dispatcher: fiber.currentDispatcher, granted: false, wake: undefined };
|
|
31
|
+
waiting.push(ticket);
|
|
32
|
+
return Effect.flatMap(Effect.exit(restore(waitFor(ticket))), (waited) => {
|
|
33
|
+
if (Exit.isSuccess(waited))
|
|
34
|
+
return run;
|
|
35
|
+
if (ticket.granted)
|
|
36
|
+
release();
|
|
37
|
+
else
|
|
38
|
+
waiting.splice(waiting.indexOf(ticket), 1);
|
|
39
|
+
return Effect.failCause(waited.cause);
|
|
40
|
+
});
|
|
41
|
+
}))
|
|
42
|
+
};
|
|
43
|
+
};
|