@effect-vfs/core 0.1.0 → 0.3.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.
Files changed (58) hide show
  1. package/README.md +97 -8
  2. package/dist/BytePath.d.ts +34 -0
  3. package/dist/BytePath.d.ts.map +1 -0
  4. package/dist/BytePath.js +16 -0
  5. package/dist/Snapshot.d.ts +5 -5
  6. package/dist/Snapshot.d.ts.map +1 -1
  7. package/dist/Snapshot.js +5 -6
  8. package/dist/SnapshotDelta.d.ts +193 -0
  9. package/dist/SnapshotDelta.d.ts.map +1 -0
  10. package/dist/SnapshotDelta.js +139 -0
  11. package/dist/VirtualFileSystem.d.ts +193 -342
  12. package/dist/VirtualFileSystem.d.ts.map +1 -1
  13. package/dist/VirtualFileSystem.js +104 -1774
  14. package/dist/internal/bytePath.d.ts +10 -0
  15. package/dist/internal/bytePath.d.ts.map +1 -0
  16. package/dist/internal/bytePath.js +46 -0
  17. package/dist/internal/canonicalBase64.d.ts +7 -0
  18. package/dist/internal/canonicalBase64.d.ts.map +1 -0
  19. package/dist/internal/canonicalBase64.js +11 -0
  20. package/dist/internal/image.d.ts +28 -31
  21. package/dist/internal/image.d.ts.map +1 -1
  22. package/dist/internal/image.js +14 -23
  23. package/dist/internal/snapshotDelta.d.ts +87 -0
  24. package/dist/internal/snapshotDelta.d.ts.map +1 -0
  25. package/dist/internal/snapshotDelta.js +576 -0
  26. package/dist/internal/snapshotDeltaModel.d.ts +10 -0
  27. package/dist/internal/snapshotDeltaModel.d.ts.map +1 -0
  28. package/dist/internal/snapshotDeltaModel.js +13 -0
  29. package/dist/internal/virtualFileSystem/errors.d.ts +26 -0
  30. package/dist/internal/virtualFileSystem/errors.d.ts.map +1 -0
  31. package/dist/internal/virtualFileSystem/errors.js +32 -0
  32. package/dist/internal/virtualFileSystem/fixture.d.ts +63 -0
  33. package/dist/internal/virtualFileSystem/fixture.d.ts.map +1 -0
  34. package/dist/internal/virtualFileSystem/fixture.js +144 -0
  35. package/dist/internal/{content.d.ts → virtualFileSystem/overlayContent.d.ts} +4 -14
  36. package/dist/internal/virtualFileSystem/overlayContent.d.ts.map +1 -0
  37. package/dist/internal/{content.js → virtualFileSystem/overlayContent.js} +3 -7
  38. package/dist/internal/{overlayChanges.d.ts → virtualFileSystem/overlayDiff.d.ts} +1 -1
  39. package/dist/internal/virtualFileSystem/overlayDiff.d.ts.map +1 -0
  40. package/dist/internal/{overlayChanges.js → virtualFileSystem/overlayDiff.js} +4 -15
  41. package/dist/internal/virtualFileSystem/path.d.ts +43 -0
  42. package/dist/internal/virtualFileSystem/path.d.ts.map +1 -0
  43. package/dist/internal/virtualFileSystem/path.js +124 -0
  44. package/dist/internal/virtualFileSystem/testHooks.d.ts +20 -0
  45. package/dist/internal/virtualFileSystem/testHooks.d.ts.map +1 -0
  46. package/dist/internal/virtualFileSystem/testHooks.js +23 -0
  47. package/dist/internal/virtualFileSystem/watchHub.d.ts +16 -0
  48. package/dist/internal/virtualFileSystem/watchHub.d.ts.map +1 -0
  49. package/dist/internal/virtualFileSystem/watchHub.js +33 -0
  50. package/dist/internal/virtualFileSystem.d.ts +290 -0
  51. package/dist/internal/virtualFileSystem.d.ts.map +1 -0
  52. package/dist/internal/virtualFileSystem.js +1677 -0
  53. package/package.json +5 -4
  54. package/dist/internal/content.d.ts.map +0 -1
  55. package/dist/internal/overlayChanges.d.ts.map +0 -1
  56. package/dist/internal/overlayTesting.d.ts +0 -12
  57. package/dist/internal/overlayTesting.d.ts.map +0 -1
  58. package/dist/internal/overlayTesting.js +0 -12
@@ -0,0 +1,576 @@
1
+ // Exact snapshot delta construction, inspection, serialization, and application.
2
+ import * as ByteSize from "effect/ByteSize";
3
+ import * as Crypto from "effect/Crypto";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Encoding from "effect/Encoding";
6
+ import * as Equal from "effect/Equal";
7
+ import * as Result from "effect/Result";
8
+ import * as Schema from "effect/Schema";
9
+ import { ImageError } from "../Snapshot.js";
10
+ import { SnapshotDeltaError, SnapshotDifference, SnapshotNodeKind } from "../SnapshotDelta.js";
11
+ import { make as makeBytePath } from "./bytePath.js";
12
+ import * as CanonicalBase64 from "./canonicalBase64.js";
13
+ import * as Image from "./image.js";
14
+ import * as SnapshotDeltaModel from "./snapshotDeltaModel.js";
15
+ const Path = Schema.String;
16
+ const Payload = Schema.Union([
17
+ Schema.TaggedStruct("Inline", { bytes: Schema.String }),
18
+ Schema.TaggedStruct("Base", { path: Path })
19
+ ]);
20
+ const Record = Schema.Union([
21
+ Schema.Struct({ kind: Schema.Literal("directory"), paths: Schema.Array(Path), metadata: Image.StoredMetadata }),
22
+ Schema.Struct({
23
+ kind: Schema.Literal("file"),
24
+ paths: Schema.Array(Path),
25
+ metadata: Image.StoredMetadata,
26
+ payload: Payload
27
+ }),
28
+ Schema.Struct({
29
+ kind: Schema.Literal("symlink"),
30
+ paths: Schema.Array(Path),
31
+ metadata: Image.StoredMetadata,
32
+ payload: Payload
33
+ })
34
+ ]);
35
+ const Change = Schema.Union([
36
+ Schema.TaggedStruct("Added", { path: Path, kind: SnapshotNodeKind }),
37
+ Schema.TaggedStruct("Removed", { path: Path, kind: SnapshotNodeKind }),
38
+ Schema.TaggedStruct("Updated", {
39
+ path: Path,
40
+ beforeKind: SnapshotNodeKind,
41
+ afterKind: SnapshotNodeKind,
42
+ differences: Schema.Array(SnapshotDifference).check(Schema.isMinLength(1))
43
+ })
44
+ ]);
45
+ const Document = Schema.Struct({
46
+ format: Schema.Literal("effect-vfs-delta"),
47
+ version: Schema.Literal(1),
48
+ base: Schema.Struct({ algorithm: Schema.Literal("effect-vfs-semantic-sha256-v1"), digest: Schema.String }),
49
+ records: Schema.Array(Record),
50
+ changes: Schema.Array(Change)
51
+ });
52
+ const timestampFields = new Set(["atimeNs", "mtimeNs", "ctimeNs", "birthtimeNs"]);
53
+ const differenceOrder = [
54
+ "kind",
55
+ "content",
56
+ "target",
57
+ "hardLinks",
58
+ "mode",
59
+ "uid",
60
+ "gid",
61
+ "atimeNs",
62
+ "mtimeNs",
63
+ "ctimeNs",
64
+ "birthtimeNs"
65
+ ];
66
+ const encoder = new TextEncoder();
67
+ const Json = Schema.fromJsonString(Schema.Unknown);
68
+ const failure = (code, field) => new ImageError({ code, ...(field === undefined ? {} : { field }) });
69
+ const sameBytes = (a, b) => Equal.equals(a, b);
70
+ const compareBytes = (a, b) => {
71
+ for (let i = 0; i < Math.min(a.length, b.length); i++) {
72
+ const d = a[i] - b[i];
73
+ if (d !== 0)
74
+ return d;
75
+ }
76
+ return a.length - b.length;
77
+ };
78
+ const key = Encoding.encodeHex;
79
+ const join = (parent, name) => {
80
+ const out = new Uint8Array(parent.length + (parent.length === 1 ? 0 : 1) + name.length);
81
+ out.set(parent);
82
+ let offset = parent.length;
83
+ if (parent.length !== 1)
84
+ out[offset++] = 47;
85
+ out.set(name, offset);
86
+ return out;
87
+ };
88
+ const parentKey = (path) => {
89
+ let slash = path.length - 1;
90
+ while (slash > 0 && path[slash] !== 47)
91
+ slash--;
92
+ return key(slash === 0 ? new Uint8Array([47]) : path.subarray(0, slash));
93
+ };
94
+ const basename = (path) => {
95
+ let slash = path.length - 1;
96
+ while (slash > 0 && path[slash] !== 47)
97
+ slash--;
98
+ return path.subarray(slash + 1);
99
+ };
100
+ const decode64 = (s, field) => CanonicalBase64.isCanonical(s) ? Effect.succeed(Image.bytes(s)) : Effect.fail(failure("InvalidEncoding", field));
101
+ const safeAdd = (a, b) => {
102
+ const n = a + b;
103
+ return Number.isSafeInteger(n) ? n : undefined;
104
+ };
105
+ const encodedPayloadLength = (value) => value.length / 4 * 3 - (value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0);
106
+ const exceedsByteLimit = (value, limit) => BigInt(value) > BigInt(limit);
107
+ const validPath = (path) => {
108
+ if (path.length === 0 || path[0] !== 47 || path.includes(0) || (path.length > 1 && path.at(-1) === 47))
109
+ return false;
110
+ if (path.length === 1)
111
+ return true;
112
+ let start = 1;
113
+ for (let i = 1; i <= path.length; i++) {
114
+ if (i === path.length || path[i] === 47) {
115
+ const length = i - start;
116
+ if (length < 1 || length > 255 || (length === 1 && path[start] === 46) ||
117
+ (length === 2 && path[start] === 46 && path[start + 1] === 46))
118
+ return false;
119
+ start = i + 1;
120
+ }
121
+ }
122
+ return true;
123
+ };
124
+ const normalize = Effect.fnUntraced(function* (snapshot, limits, role) {
125
+ const doc = yield* Image.inspect(snapshot);
126
+ if (doc.records.length > (role === "base" ? limits.maxBaseRecords : limits.maxTargetRecords)) {
127
+ return yield* failure("LimitExceeded", `${role}Records`);
128
+ }
129
+ const records = new Map(doc.records.map((record) => [record.id, record]));
130
+ const pathsById = new Map();
131
+ const pending = [[doc.root, new Uint8Array([47])]];
132
+ let entries = 0;
133
+ let pathBytes = 1;
134
+ for (let i = 0; i < pending.length; i++) {
135
+ const [recordId, path] = pending[i];
136
+ const record = records.get(recordId);
137
+ if (record === undefined)
138
+ return yield* failure("InvalidStructure", role);
139
+ const paths = pathsById.get(recordId);
140
+ if (paths === undefined)
141
+ pathsById.set(recordId, [path]);
142
+ else
143
+ paths.push(path);
144
+ if (record.kind === "directory") {
145
+ for (const entry of record.entries) {
146
+ if (++entries > limits.maxEntries)
147
+ return yield* failure("LimitExceeded", "entries");
148
+ const nameLength = encodedPayloadLength(entry.name);
149
+ const pathLength = safeAdd(path.length + (path.length === 1 ? 0 : 1), nameLength);
150
+ const nextPathBytes = pathLength === undefined ? undefined : safeAdd(pathBytes, pathLength);
151
+ const pathLimit = role === "base" ? limits.maxIdentityBytes : limits.maxDecodedDeltaBytes;
152
+ if (nextPathBytes === undefined || exceedsByteLimit(nextPathBytes, pathLimit)) {
153
+ return yield* failure("LimitExceeded", role === "base" ? "identityBytes" : "decodedDeltaBytes");
154
+ }
155
+ pathBytes = nextPathBytes;
156
+ pending.push([entry.target, join(path, Image.bytes(entry.name))]);
157
+ }
158
+ }
159
+ }
160
+ const objects = [];
161
+ const byPath = new Map();
162
+ let payloadBytes = 0;
163
+ let basePayloadBytes = 0;
164
+ for (const record of doc.records) {
165
+ const paths = pathsById.get(record.id);
166
+ if (paths === undefined)
167
+ return yield* failure("InvalidStructure", role);
168
+ paths.sort(compareBytes);
169
+ const encodedPayload = record.kind === "directory"
170
+ ? undefined
171
+ : record.kind === "file"
172
+ ? record.data
173
+ : record.target;
174
+ if (encodedPayload !== undefined) {
175
+ const payloadLength = encodedPayloadLength(encodedPayload);
176
+ const next = safeAdd(role === "base" ? basePayloadBytes : payloadBytes, payloadLength);
177
+ const limit = role === "base" ? limits.maxIdentityBytes : limits.maxOutputBytes;
178
+ if (next === undefined || exceedsByteLimit(next, limit)) {
179
+ return yield* failure("LimitExceeded", role === "base" ? "identityBytes" : "outputBytes");
180
+ }
181
+ if (role === "base")
182
+ basePayloadBytes = next;
183
+ else
184
+ payloadBytes = next;
185
+ }
186
+ const payload = encodedPayload === undefined ? undefined : Image.bytes(encodedPayload);
187
+ const object = {
188
+ kind: record.kind,
189
+ metadata: record.metadata,
190
+ paths,
191
+ pathIdentity: paths.map(key).join("/"),
192
+ payload
193
+ };
194
+ objects.push(object);
195
+ for (const path of paths)
196
+ byPath.set(key(path), { object, path });
197
+ }
198
+ objects.sort((a, b) => compareBytes(a.paths[0], b.paths[0]));
199
+ return { objects, byPath };
200
+ });
201
+ const u64 = (n) => {
202
+ const out = new Uint8Array(8);
203
+ new DataView(out.buffer).setBigUint64(0, BigInt(n), false);
204
+ return out;
205
+ };
206
+ // This is the versioned semantic identity encoding, not a generic byte builder.
207
+ // Field framing, ordering, and the domain prefix are part of the persisted delta contract.
208
+ const identityBytes = Effect.fnUntraced(function* (view, limits) {
209
+ let length = 0;
210
+ const initialCapacity = ByteSize.isGreaterThanOrEqualTo(limits.maxIdentityBytes, ByteSize.kibibytes(1))
211
+ ? 1024
212
+ : Number(ByteSize.toBigInt(limits.maxIdentityBytes));
213
+ let output = new Uint8Array(initialCapacity);
214
+ const append = (bytes) => {
215
+ const next = safeAdd(length, bytes.length);
216
+ if (next === undefined || BigInt(next) > ByteSize.toBigInt(limits.maxIdentityBytes)) {
217
+ return Effect.fail(failure("LimitExceeded", "identityBytes"));
218
+ }
219
+ if (next > output.length) {
220
+ let capacity = Math.max(1, output.length);
221
+ while (capacity < next)
222
+ capacity = Math.min(next, capacity * 2);
223
+ const expanded = new Uint8Array(capacity);
224
+ expanded.set(output);
225
+ output = expanded;
226
+ }
227
+ output.set(bytes, length);
228
+ length = next;
229
+ return Effect.void;
230
+ };
231
+ const frame = Effect.fnUntraced(function* (bytes) {
232
+ yield* append(u64(bytes.length));
233
+ yield* append(bytes);
234
+ });
235
+ yield* append(encoder.encode("effect-vfs-semantic-sha256-v1\0"));
236
+ yield* append(u64(view.objects.length));
237
+ for (const object of view.objects) {
238
+ yield* append(u64(object.paths.length));
239
+ for (const path of object.paths)
240
+ yield* frame(path);
241
+ yield* append(new Uint8Array([object.kind === "directory" ? 0 : object.kind === "file" ? 1 : 2]));
242
+ yield* append(u64(object.metadata.uid));
243
+ yield* append(u64(object.metadata.gid));
244
+ yield* append(u64(object.metadata.mode));
245
+ yield* frame(encoder.encode(object.metadata.atimeNs));
246
+ yield* frame(encoder.encode(object.metadata.mtimeNs));
247
+ yield* frame(encoder.encode(object.metadata.ctimeNs));
248
+ yield* frame(encoder.encode(object.metadata.birthtimeNs));
249
+ yield* frame(object.payload ?? new Uint8Array());
250
+ }
251
+ return output.slice(0, length);
252
+ });
253
+ const digest = Effect.fnUntraced(function* (view, limits) {
254
+ const crypto = yield* Crypto.Crypto;
255
+ return yield* crypto.digest("SHA-256", yield* identityBytes(view, limits));
256
+ });
257
+ const samePaths = (a, b) => a.pathIdentity === b.pathIdentity;
258
+ const differences = (before, after) => differenceOrder.filter((field) => {
259
+ if (field === "kind")
260
+ return before.kind !== after.kind;
261
+ if (field === "content") {
262
+ return before.kind === after.kind && after.kind === "file" && !sameBytes(before.payload, after.payload);
263
+ }
264
+ if (field === "target") {
265
+ return before.kind === after.kind && after.kind === "symlink" && !sameBytes(before.payload, after.payload);
266
+ }
267
+ if (field === "hardLinks")
268
+ return !samePaths(before, after);
269
+ return before.metadata[field] !== after.metadata[field];
270
+ });
271
+ const compare = (base, target) => {
272
+ const changes = [];
273
+ const add = (change, path) => changes.push({ change, path });
274
+ const differencesByPair = new WeakMap();
275
+ const cachedDifferences = (before, after) => {
276
+ let byAfter = differencesByPair.get(before);
277
+ if (byAfter === undefined) {
278
+ byAfter = new WeakMap();
279
+ differencesByPair.set(before, byAfter);
280
+ }
281
+ const cached = byAfter.get(after);
282
+ if (cached !== undefined)
283
+ return cached;
284
+ const value = differences(before, after);
285
+ byAfter.set(after, value);
286
+ return value;
287
+ };
288
+ for (const [pathKey, before] of base.byPath) {
289
+ const after = target.byPath.get(pathKey);
290
+ if (after === undefined) {
291
+ add({ _tag: "Removed", path: Image.base64(before.path), kind: before.object.kind }, before.path);
292
+ }
293
+ else {
294
+ const diff = cachedDifferences(before.object, after.object);
295
+ if (diff.length > 0) {
296
+ add({
297
+ _tag: "Updated",
298
+ path: Image.base64(before.path),
299
+ beforeKind: before.object.kind,
300
+ afterKind: after.object.kind,
301
+ differences: diff
302
+ }, before.path);
303
+ }
304
+ }
305
+ }
306
+ for (const [pathKey, after] of target.byPath) {
307
+ if (!base.byPath.has(pathKey)) {
308
+ add({ _tag: "Added", path: Image.base64(after.path), kind: after.object.kind }, after.path);
309
+ }
310
+ }
311
+ changes.sort((a, b) => compareBytes(a.path, b.path));
312
+ return changes.map(({ change }) => change);
313
+ };
314
+ const getDocument = (delta) => Effect.suspend(() => {
315
+ const value = SnapshotDeltaModel.value(delta);
316
+ return value !== undefined && Schema.is(Document)(value)
317
+ ? Effect.succeed(value)
318
+ : Effect.fail(failure("InvalidStructure", "delta"));
319
+ });
320
+ const buildImage = Effect.fnUntraced(function* (document, base, limits) {
321
+ const paths = new Map();
322
+ const records = [];
323
+ const entriesById = new Map();
324
+ let bytes = 0;
325
+ for (let i = 0; i < document.records.length; i++) {
326
+ const record = document.records[i];
327
+ const recordId = String(i);
328
+ for (const encodedPath of record.paths) {
329
+ const path = yield* decode64(encodedPath, "path");
330
+ paths.set(key(path), { id: recordId, kind: record.kind, bytes: path });
331
+ }
332
+ if (record.kind === "directory") {
333
+ const entries = [];
334
+ entriesById.set(recordId, entries);
335
+ records.push({ id: recordId, kind: "directory", metadata: record.metadata, entries });
336
+ continue;
337
+ }
338
+ const inherited = record.payload._tag === "Base"
339
+ ? base?.byPath.get(key(yield* decode64(record.payload.path, "basePath")))?.object
340
+ : undefined;
341
+ const inlineLength = record.payload._tag === "Inline"
342
+ ? encodedPayloadLength(record.payload.bytes)
343
+ : undefined;
344
+ const payloadLength = inlineLength ?? (base === undefined
345
+ ? 0
346
+ : inherited?.kind === record.kind
347
+ ? inherited.payload?.length
348
+ : undefined);
349
+ const next = payloadLength === undefined ? undefined : safeAdd(bytes, payloadLength);
350
+ if (next !== undefined && exceedsByteLimit(next, limits.maxOutputBytes)) {
351
+ return yield* failure("LimitExceeded", "outputBytes");
352
+ }
353
+ const payload = record.payload._tag === "Inline"
354
+ ? yield* decode64(record.payload.bytes, "payload")
355
+ : inherited?.kind === record.kind
356
+ ? inherited.payload
357
+ : undefined;
358
+ if (payload === undefined && base !== undefined)
359
+ return yield* failure("InvalidStructure", "baseReference");
360
+ const actual = payload ?? new Uint8Array();
361
+ if (next === undefined)
362
+ return yield* failure("LimitExceeded", "outputBytes");
363
+ bytes = next;
364
+ records.push(record.kind === "file"
365
+ ? { id: recordId, kind: "file", metadata: record.metadata, data: Image.base64(actual) }
366
+ : { id: recordId, kind: "symlink", metadata: record.metadata, target: Image.base64(actual) });
367
+ }
368
+ const root = paths.get(key(new Uint8Array([47])));
369
+ if (root?.kind !== "directory")
370
+ return yield* failure("InvalidStructure", "root");
371
+ for (const [pathKey, child] of paths) {
372
+ if (pathKey !== key(new Uint8Array([47]))) {
373
+ const parent = paths.get(parentKey(child.bytes));
374
+ const entries = parent === undefined ? undefined : entriesById.get(parent.id);
375
+ if (entries === undefined)
376
+ return yield* failure("InvalidStructure", "parent");
377
+ entries.push({ name: Image.base64(basename(child.bytes)), target: child.id });
378
+ }
379
+ }
380
+ for (const entries of entriesById.values())
381
+ entries.sort((a, b) => a.name.localeCompare(b.name));
382
+ return yield* Image.capture({ format: "effect-vfs", version: 1, root: root.id, records });
383
+ });
384
+ const validate = Effect.fnUntraced(function* (document, limits) {
385
+ const deltaRecords = safeAdd(document.records.length, document.changes.length);
386
+ if (deltaRecords === undefined || deltaRecords > limits.maxDeltaRecords) {
387
+ return yield* failure("LimitExceeded", "deltaRecords");
388
+ }
389
+ if (document.records.length > limits.maxOutputRecords)
390
+ return yield* failure("LimitExceeded", "outputRecords");
391
+ if (!CanonicalBase64.isCanonical(document.base.digest) || CanonicalBase64.decodedLength(document.base.digest) !== 32) {
392
+ return yield* failure("InvalidEncoding", "digest");
393
+ }
394
+ let decoded = 32;
395
+ let entries = -1;
396
+ let inherited = 0;
397
+ const seen = new Set();
398
+ for (const record of document.records) {
399
+ if (record.paths.length < 1 || (record.kind === "directory" && record.paths.length !== 1)) {
400
+ return yield* failure("InvalidStructure", "paths");
401
+ }
402
+ for (const encodedPath of record.paths) {
403
+ if (!CanonicalBase64.isCanonical(encodedPath))
404
+ return yield* failure("InvalidEncoding", "path");
405
+ decoded = safeAdd(decoded, CanonicalBase64.decodedLength(encodedPath)) ?? Number.POSITIVE_INFINITY;
406
+ if (BigInt(decoded) > ByteSize.toBigInt(limits.maxDecodedDeltaBytes)) {
407
+ return yield* failure("LimitExceeded", "decodedDeltaBytes");
408
+ }
409
+ entries++;
410
+ if (entries > limits.maxEntries)
411
+ return yield* failure("LimitExceeded", "entries");
412
+ const path = yield* decode64(encodedPath, "path");
413
+ if (!validPath(path) || seen.has(key(path)))
414
+ return yield* failure("InvalidStructure", "path");
415
+ seen.add(key(path));
416
+ }
417
+ if (record.kind !== "directory") {
418
+ if (record.payload._tag === "Inline") {
419
+ if (!CanonicalBase64.isCanonical(record.payload.bytes))
420
+ return yield* failure("InvalidEncoding", "payload");
421
+ decoded = safeAdd(decoded, CanonicalBase64.decodedLength(record.payload.bytes)) ?? Number.POSITIVE_INFINITY;
422
+ if (BigInt(decoded) > ByteSize.toBigInt(limits.maxDecodedDeltaBytes)) {
423
+ return yield* failure("LimitExceeded", "decodedDeltaBytes");
424
+ }
425
+ }
426
+ else {
427
+ if (!CanonicalBase64.isCanonical(record.payload.path))
428
+ return yield* failure("InvalidEncoding", "basePath");
429
+ decoded = safeAdd(decoded, CanonicalBase64.decodedLength(record.payload.path)) ?? Number.POSITIVE_INFINITY;
430
+ if (BigInt(decoded) > ByteSize.toBigInt(limits.maxDecodedDeltaBytes)) {
431
+ return yield* failure("LimitExceeded", "decodedDeltaBytes");
432
+ }
433
+ inherited++;
434
+ if (inherited > limits.maxInheritedRecords) {
435
+ return yield* failure("LimitExceeded", "inheritedRecords");
436
+ }
437
+ const path = yield* decode64(record.payload.path, "basePath");
438
+ if (!validPath(path) || !record.paths.includes(record.payload.path)) {
439
+ return yield* failure("InvalidStructure", "basePath");
440
+ }
441
+ }
442
+ }
443
+ }
444
+ let previous;
445
+ for (const change of document.changes) {
446
+ if (!CanonicalBase64.isCanonical(change.path))
447
+ return yield* failure("InvalidEncoding", "changePath");
448
+ decoded = safeAdd(decoded, CanonicalBase64.decodedLength(change.path)) ?? Number.POSITIVE_INFINITY;
449
+ if (BigInt(decoded) > ByteSize.toBigInt(limits.maxDecodedDeltaBytes)) {
450
+ return yield* failure("LimitExceeded", "decodedDeltaBytes");
451
+ }
452
+ const path = yield* decode64(change.path, "changePath");
453
+ if (!validPath(path) || (previous !== undefined && compareBytes(previous, path) >= 0)) {
454
+ return yield* failure("InvalidStructure", "changes");
455
+ }
456
+ previous = path;
457
+ }
458
+ yield* buildImage(document, undefined, limits);
459
+ });
460
+ /** @internal */
461
+ export const diffSnapshots = Effect.fnUntraced(function* (base, target, limits) {
462
+ const before = yield* normalize(base, limits, "base");
463
+ const after = yield* normalize(target, limits, "target");
464
+ const changes = [...compare(before, after)];
465
+ const deltaRecords = safeAdd(after.objects.length, changes.length);
466
+ if (deltaRecords === undefined || deltaRecords > limits.maxDeltaRecords) {
467
+ return yield* failure("LimitExceeded", "deltaRecords");
468
+ }
469
+ const payloadEquality = new WeakMap();
470
+ const samePayload = (source, target) => {
471
+ let byTarget = payloadEquality.get(source);
472
+ if (byTarget === undefined) {
473
+ byTarget = new WeakMap();
474
+ payloadEquality.set(source, byTarget);
475
+ }
476
+ const cached = byTarget.get(target);
477
+ if (cached !== undefined)
478
+ return cached;
479
+ const value = sameBytes(source.payload, target.payload);
480
+ byTarget.set(target, value);
481
+ return value;
482
+ };
483
+ const records = after.objects.map((object) => {
484
+ const paths = object.paths.map(Image.base64);
485
+ if (object.kind === "directory")
486
+ return { kind: object.kind, paths, metadata: object.metadata };
487
+ const inherited = object.paths.find((path) => {
488
+ const source = before.byPath.get(key(path));
489
+ return source?.object.kind === object.kind && samePayload(source.object, object);
490
+ });
491
+ return {
492
+ kind: object.kind,
493
+ paths,
494
+ metadata: object.metadata,
495
+ payload: inherited === undefined
496
+ ? { _tag: "Inline", bytes: Image.base64(object.payload ?? new Uint8Array()) }
497
+ : { _tag: "Base", path: Image.base64(inherited) }
498
+ };
499
+ });
500
+ const document = {
501
+ format: "effect-vfs-delta",
502
+ version: 1,
503
+ base: { algorithm: "effect-vfs-semantic-sha256-v1", digest: Image.base64(yield* digest(before, limits)) },
504
+ records,
505
+ changes
506
+ };
507
+ yield* validate(document, limits);
508
+ return SnapshotDeltaModel.make(document);
509
+ });
510
+ /** @internal */
511
+ const verify = Effect.fnUntraced(function* (base, document, limits) {
512
+ yield* validate(document, limits);
513
+ const before = yield* normalize(base, limits, "base");
514
+ if (!sameBytes(yield* digest(before, limits), Image.bytes(document.base.digest))) {
515
+ return yield* new SnapshotDeltaError({ code: "BaseMismatch" });
516
+ }
517
+ const target = yield* buildImage(document, before, limits);
518
+ const changes = compare(before, yield* normalize(target, limits, "target"));
519
+ const actualChanges = yield* Schema.encodeEffect(Json)(changes).pipe(Effect.mapError(() => failure("InvalidStructure", "changes")));
520
+ const expectedChanges = yield* Schema.encodeEffect(Json)(document.changes).pipe(Effect.mapError(() => failure("InvalidStructure", "changes")));
521
+ if (actualChanges !== expectedChanges)
522
+ return yield* failure("InvalidStructure", "changes");
523
+ return { changes, target };
524
+ });
525
+ /** @internal */
526
+ export const inspectSnapshotDelta = Effect.fnUntraced(function* (base, delta, options, limits) {
527
+ const output = [];
528
+ const { changes } = yield* verify(base, yield* getDocument(delta), limits);
529
+ for (const change of changes) {
530
+ const path = makeBytePath(new Uint8Array(yield* decode64(change.path, "changePath")));
531
+ if (change._tag !== "Updated")
532
+ output.push(Object.freeze({ ...change, path }));
533
+ else {
534
+ const fields = change.differences.filter((field) => options.includeTimestamps === true || !timestampFields.has(field));
535
+ if (fields.length > 0)
536
+ output.push(Object.freeze({ ...change, path, differences: Object.freeze([...fields]) }));
537
+ }
538
+ }
539
+ return Object.freeze(output);
540
+ });
541
+ /** @internal */
542
+ export const encodeSnapshotDelta = Effect.fnUntraced(function* (delta, limits) {
543
+ const document = yield* getDocument(delta);
544
+ yield* validate(document, limits);
545
+ const text = yield* Schema.encodeEffect(Json)(document).pipe(Effect.mapError(() => failure("InvalidStructure")));
546
+ const bytes = encoder.encode(text);
547
+ if (BigInt(bytes.length) > ByteSize.toBigInt(limits.maxEncodedBytes)) {
548
+ return yield* failure("LimitExceeded", "encodedBytes");
549
+ }
550
+ return bytes;
551
+ });
552
+ /** @internal */
553
+ export const decodeSnapshotDelta = Effect.fnUntraced(function* (input, limits) {
554
+ if (!(input instanceof Uint8Array) || !(input.buffer instanceof ArrayBuffer))
555
+ return yield* failure("InvalidEncoding");
556
+ if (BigInt(input.byteLength) > ByteSize.toBigInt(limits.maxEncodedBytes)) {
557
+ return yield* failure("LimitExceeded", "encodedBytes");
558
+ }
559
+ const text = yield* Effect.try({
560
+ try: () => new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array(input)),
561
+ catch: () => failure("InvalidEncoding")
562
+ });
563
+ const value = yield* Schema.decodeEffect(Json)(text).pipe(Effect.mapError(() => failure("InvalidEncoding")));
564
+ if (typeof value === "object" && value !== null && "format" in value && value.format === "effect-vfs-delta" &&
565
+ "version" in value && value.version !== 1)
566
+ return yield* failure("UnsupportedVersion");
567
+ const parsed = Schema.decodeUnknownResult(Document, { onExcessProperty: "error" })(value);
568
+ if (Result.isFailure(parsed))
569
+ return yield* failure("InvalidStructure");
570
+ yield* validate(parsed.success, limits);
571
+ return SnapshotDeltaModel.make(parsed.success);
572
+ });
573
+ /** @internal */
574
+ export const applySnapshotDelta = Effect.fnUntraced(function* (base, delta, limits) {
575
+ return (yield* verify(base, yield* getDocument(delta), limits)).target;
576
+ });
@@ -0,0 +1,10 @@
1
+ import type { SnapshotDelta } from "../SnapshotDelta.js";
2
+ /** @internal */
3
+ export declare const SnapshotDeltaTypeId: "@effect-vfs/core/SnapshotDelta";
4
+ /** @internal */
5
+ export declare const make: (value: object) => SnapshotDelta;
6
+ /** @internal */
7
+ export declare const value: (delta: SnapshotDelta) => object | undefined;
8
+ /** @internal */
9
+ export declare const isSnapshotDelta: (input: unknown) => input is SnapshotDelta;
10
+ //# sourceMappingURL=snapshotDeltaModel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshotDeltaModel.d.ts","sourceRoot":"","sources":["../../src/internal/snapshotDeltaModel.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAExD,gBAAgB;AAChB,eAAO,MAAM,mBAAmB,EAAG,gCAAyC,CAAA;AAI5E,gBAAgB;AAChB,eAAO,MAAM,IAAI,UAAW,MAAM,KAAG,aAIpC,CAAA;AAED,gBAAgB;AAChB,eAAO,MAAM,KAAK,UAAW,aAAa,KAAG,MAAM,GAAG,SAAsC,CAAA;AAE5F,gBAAgB;AAChB,eAAO,MAAM,eAAe,UAAW,OAAO,KAAG,KAAK,IAAI,aACiC,CAAA"}
@@ -0,0 +1,13 @@
1
+ /** @internal */
2
+ export const SnapshotDeltaTypeId = "@effect-vfs/core/SnapshotDelta";
3
+ const snapshotDeltas = new WeakMap();
4
+ /** @internal */
5
+ export const make = (value) => {
6
+ const delta = Object.freeze({ [SnapshotDeltaTypeId]: SnapshotDeltaTypeId });
7
+ snapshotDeltas.set(delta, value);
8
+ return delta;
9
+ };
10
+ /** @internal */
11
+ export const value = (delta) => snapshotDeltas.get(delta);
12
+ /** @internal */
13
+ export const isSnapshotDelta = (input) => typeof input === "object" && input !== null && snapshotDeltas.has(input);
@@ -0,0 +1,26 @@
1
+ import * as Schema from "effect/Schema";
2
+ import type { PathInput } from "../../VirtualFileSystem.js";
3
+ /** @internal */
4
+ export declare const FsCode: Schema.Literals<readonly ["NotFound", "AlreadyExists", "NotEmpty", "NotDirectory", "AccessDenied", "InvalidHandle", "ForeignHandle", "InvalidReference", "ForeignReference", "StaleReference", "ClosedCaller", "InvalidArgument", "InvalidPathEncoding", "PathTooLong", "NoSpace", "IsDirectory", "FileTooLarge", "NoData", "SymlinkLoop", "UnrepresentableName"]>;
5
+ /** @internal */
6
+ export type FsCode = typeof FsCode.Type;
7
+ declare const FsError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
8
+ readonly _tag: "FsError";
9
+ } & Readonly<A>;
10
+ /** @internal */
11
+ export declare class FsError extends FsError_base<{
12
+ readonly code: FsCode;
13
+ readonly operation: string;
14
+ readonly path?: PathInput;
15
+ }> {
16
+ }
17
+ declare const ConfigurationError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
18
+ readonly _tag: "ConfigurationError";
19
+ } & Readonly<A>;
20
+ /** @internal */
21
+ export declare class ConfigurationError extends ConfigurationError_base<{
22
+ readonly field: string;
23
+ }> {
24
+ }
25
+ export {};
26
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/internal/virtualFileSystem/errors.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAE3D,gBAAgB;AAChB,eAAO,MAAM,MAAM,oWAqBjB,CAAA;AAEF,gBAAgB;AAChB,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,IAAI,CAAA;;;;AAEvC,gBAAgB;AAChB,qBAAa,OAAQ,SAAQ,aAA4B;IACvD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAA;CAC1B,CAAC;CAAG;;;;AAEL,gBAAgB;AAChB,qBAAa,kBAAmB,SAAQ,wBAAuC;IAC7E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB,CAAC;CAAG"}
@@ -0,0 +1,32 @@
1
+ // Virtual filesystem failure schemas and identities shared by the runtime implementation.
2
+ import * as Data from "effect/Data";
3
+ import * as Schema from "effect/Schema";
4
+ /** @internal */
5
+ export const FsCode = Schema.Literals([
6
+ "NotFound",
7
+ "AlreadyExists",
8
+ "NotEmpty",
9
+ "NotDirectory",
10
+ "AccessDenied",
11
+ "InvalidHandle",
12
+ "ForeignHandle",
13
+ "InvalidReference",
14
+ "ForeignReference",
15
+ "StaleReference",
16
+ "ClosedCaller",
17
+ "InvalidArgument",
18
+ "InvalidPathEncoding",
19
+ "PathTooLong",
20
+ "NoSpace",
21
+ "IsDirectory",
22
+ "FileTooLarge",
23
+ "NoData",
24
+ "SymlinkLoop",
25
+ "UnrepresentableName"
26
+ ]);
27
+ /** @internal */
28
+ export class FsError extends Data.TaggedError("FsError") {
29
+ }
30
+ /** @internal */
31
+ export class ConfigurationError extends Data.TaggedError("ConfigurationError") {
32
+ }