@effect-vfs/core 0.2.0 → 0.3.1

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 (67) hide show
  1. package/dist/BytePath.d.ts +49 -5
  2. package/dist/BytePath.d.ts.map +1 -1
  3. package/dist/BytePath.js +9 -9
  4. package/dist/Snapshot.d.ts +61 -5
  5. package/dist/Snapshot.d.ts.map +1 -1
  6. package/dist/Snapshot.js +60 -5
  7. package/dist/SnapshotDelta.d.ts +208 -31
  8. package/dist/SnapshotDelta.d.ts.map +1 -1
  9. package/dist/SnapshotDelta.js +162 -25
  10. package/dist/VirtualFileSystem.d.ts +1035 -348
  11. package/dist/VirtualFileSystem.d.ts.map +1 -1
  12. package/dist/VirtualFileSystem.js +620 -1772
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/internal/bytePath.d.ts +10 -0
  16. package/dist/internal/bytePath.d.ts.map +1 -0
  17. package/dist/internal/bytePath.js +41 -0
  18. package/dist/internal/bytes.d.ts +6 -0
  19. package/dist/internal/bytes.d.ts.map +1 -0
  20. package/dist/internal/bytes.js +17 -0
  21. package/dist/internal/canonicalBase64.d.ts +14 -6
  22. package/dist/internal/canonicalBase64.d.ts.map +1 -1
  23. package/dist/internal/canonicalBase64.js +29 -7
  24. package/dist/internal/image.d.ts +85 -104
  25. package/dist/internal/image.d.ts.map +1 -1
  26. package/dist/internal/image.js +99 -95
  27. package/dist/internal/metadata.d.ts +28 -0
  28. package/dist/internal/metadata.d.ts.map +1 -0
  29. package/dist/internal/metadata.js +29 -0
  30. package/dist/internal/snapshotDelta.d.ts +0 -1
  31. package/dist/internal/snapshotDelta.d.ts.map +1 -1
  32. package/dist/internal/snapshotDelta.js +358 -325
  33. package/dist/internal/snapshotDeltaModel.d.ts +10 -0
  34. package/dist/internal/snapshotDeltaModel.d.ts.map +1 -0
  35. package/dist/internal/snapshotDeltaModel.js +15 -0
  36. package/dist/internal/virtualFileSystem/errors.d.ts +31 -0
  37. package/dist/internal/virtualFileSystem/errors.d.ts.map +1 -0
  38. package/dist/internal/virtualFileSystem/errors.js +51 -0
  39. package/dist/internal/virtualFileSystem/fixture.d.ts +63 -0
  40. package/dist/internal/virtualFileSystem/fixture.d.ts.map +1 -0
  41. package/dist/internal/virtualFileSystem/fixture.js +144 -0
  42. package/dist/internal/{content.d.ts → virtualFileSystem/overlayContent.d.ts} +4 -14
  43. package/dist/internal/virtualFileSystem/overlayContent.d.ts.map +1 -0
  44. package/dist/internal/virtualFileSystem/overlayContent.js +29 -0
  45. package/dist/internal/{overlayChanges.d.ts → virtualFileSystem/overlayDiff.d.ts} +3 -12
  46. package/dist/internal/virtualFileSystem/overlayDiff.d.ts.map +1 -0
  47. package/dist/internal/virtualFileSystem/overlayDiff.js +120 -0
  48. package/dist/internal/virtualFileSystem/path.d.ts +44 -0
  49. package/dist/internal/virtualFileSystem/path.d.ts.map +1 -0
  50. package/dist/internal/virtualFileSystem/path.js +127 -0
  51. package/dist/internal/virtualFileSystem/testHooks.d.ts +20 -0
  52. package/dist/internal/virtualFileSystem/testHooks.d.ts.map +1 -0
  53. package/dist/internal/virtualFileSystem/testHooks.js +23 -0
  54. package/dist/internal/virtualFileSystem/watchHub.d.ts +16 -0
  55. package/dist/internal/virtualFileSystem/watchHub.d.ts.map +1 -0
  56. package/dist/internal/virtualFileSystem/watchHub.js +33 -0
  57. package/dist/internal/virtualFileSystem.d.ts +366 -0
  58. package/dist/internal/virtualFileSystem.d.ts.map +1 -0
  59. package/dist/internal/virtualFileSystem.js +1755 -0
  60. package/package.json +15 -5
  61. package/dist/internal/content.d.ts.map +0 -1
  62. package/dist/internal/content.js +0 -26
  63. package/dist/internal/overlayChanges.d.ts.map +0 -1
  64. package/dist/internal/overlayChanges.js +0 -146
  65. package/dist/internal/overlayTesting.d.ts +0 -12
  66. package/dist/internal/overlayTesting.d.ts.map +0 -1
  67. package/dist/internal/overlayTesting.js +0 -12
@@ -1,22 +1,34 @@
1
- /** Exact snapshot delta construction, inspection, serialization, and application. @internal */
1
+ // Exact snapshot delta construction, inspection, serialization, and application.
2
2
  import * as ByteSize from "effect/ByteSize";
3
3
  import * as Crypto from "effect/Crypto";
4
4
  import * as Effect from "effect/Effect";
5
5
  import * as Encoding from "effect/Encoding";
6
- import * as Equal from "effect/Equal";
6
+ import * as Fn from "effect/Function";
7
+ import * as Match from "effect/Match";
8
+ import * as Predicate from "effect/Predicate";
7
9
  import * as Result from "effect/Result";
8
10
  import * as Schema from "effect/Schema";
9
- import { make as makeBytePath } from "../BytePath.js";
10
11
  import { ImageError } from "../Snapshot.js";
11
- import { makeSnapshotDelta, SnapshotDeltaError, snapshotDeltaValue } from "../SnapshotDelta.js";
12
- import * as CanonicalBase64 from "./canonicalBase64.js";
12
+ import { SnapshotDeltaError, SnapshotDifference, SnapshotNodeKind } from "../SnapshotDelta.js";
13
+ import { make as makeBytePath } from "./bytePath.js";
14
+ import { bytesOrder, sameBytes } from "./bytes.js";
15
+ import { CanonicalBase64 } from "./canonicalBase64.js";
13
16
  import * as Image from "./image.js";
14
- const Path = Schema.String;
15
- const Payload = Schema.Union([
16
- Schema.TaggedStruct("Inline", { bytes: Schema.String }),
17
- Schema.TaggedStruct("Base", { path: Path })
18
- ]);
19
- const Record = Schema.Union([
17
+ import { WireStoredMetadata } from "./metadata.js";
18
+ import * as SnapshotDeltaModel from "./snapshotDeltaModel.js";
19
+ const FORMAT = "effect-vfs-delta";
20
+ const ALGORITHM = "effect-vfs-semantic-sha256-v1";
21
+ const SLASH_BYTE = 47;
22
+ const DOT_BYTE = 46;
23
+ const MAX_NAME_BYTES = 255;
24
+ const NUL_BYTE = 0;
25
+ const SHA256_BYTES = 32;
26
+ const ROOT_PATH = new Uint8Array([SLASH_BYTE]);
27
+ const Path = CanonicalBase64.Encoded;
28
+ const InlinePayload = Schema.TaggedStruct("Inline", { bytes: CanonicalBase64.Encoded });
29
+ const BasePayload = Schema.TaggedStruct("Base", { path: Path });
30
+ const Payload = Schema.Union([InlinePayload, BasePayload]);
31
+ const DeltaRecord = Schema.Union([
20
32
  Schema.Struct({ kind: Schema.Literal("directory"), paths: Schema.Array(Path), metadata: Image.StoredMetadata }),
21
33
  Schema.Struct({
22
34
  kind: Schema.Literal("file"),
@@ -31,241 +43,256 @@ const Record = Schema.Union([
31
43
  payload: Payload
32
44
  })
33
45
  ]);
34
- const Kind = Schema.Literals(["directory", "file", "symlink"]);
35
- const Difference = Schema.Literals([
36
- "kind",
37
- "content",
38
- "target",
39
- "hardLinks",
40
- "mode",
41
- "uid",
42
- "gid",
43
- "atimeNs",
44
- "mtimeNs",
45
- "ctimeNs",
46
- "birthtimeNs"
46
+ const AddedChange = Schema.TaggedStruct("Added", { path: Path, kind: SnapshotNodeKind });
47
+ const RemovedChange = Schema.TaggedStruct("Removed", { path: Path, kind: SnapshotNodeKind });
48
+ const UpdatedChange = Schema.TaggedStruct("Updated", {
49
+ path: Path,
50
+ beforeKind: SnapshotNodeKind,
51
+ afterKind: SnapshotNodeKind,
52
+ differences: Schema.Array(SnapshotDifference).check(Schema.isMinLength(1))
53
+ });
54
+ const Change = Schema.Union([AddedChange, RemovedChange, UpdatedChange]);
55
+ const WirePayload = Schema.Union([
56
+ Schema.TaggedStruct("Inline", { bytes: Schema.String }),
57
+ Schema.TaggedStruct("Base", { path: Schema.String })
47
58
  ]);
48
- const Change = Schema.Union([
49
- Schema.TaggedStruct("Added", { path: Path, kind: Kind }),
50
- Schema.TaggedStruct("Removed", { path: Path, kind: Kind }),
59
+ const WireDeltaRecord = Schema.Union([
60
+ Schema.Struct({
61
+ kind: Schema.Literal("directory"),
62
+ paths: Schema.Array(Schema.String),
63
+ metadata: WireStoredMetadata
64
+ }),
65
+ Schema.Struct({
66
+ kind: Schema.Literal("file"),
67
+ paths: Schema.Array(Schema.String),
68
+ metadata: WireStoredMetadata,
69
+ payload: WirePayload
70
+ }),
71
+ Schema.Struct({
72
+ kind: Schema.Literal("symlink"),
73
+ paths: Schema.Array(Schema.String),
74
+ metadata: WireStoredMetadata,
75
+ payload: WirePayload
76
+ })
77
+ ]);
78
+ const WireChange = Schema.Union([
79
+ Schema.TaggedStruct("Added", { path: Schema.String, kind: SnapshotNodeKind }),
80
+ Schema.TaggedStruct("Removed", { path: Schema.String, kind: SnapshotNodeKind }),
51
81
  Schema.TaggedStruct("Updated", {
52
- path: Path,
53
- beforeKind: Kind,
54
- afterKind: Kind,
55
- differences: Schema.Array(Difference).check(Schema.isMinLength(1))
82
+ path: Schema.String,
83
+ beforeKind: SnapshotNodeKind,
84
+ afterKind: SnapshotNodeKind,
85
+ differences: Schema.Array(SnapshotDifference).check(Schema.isMinLength(1))
56
86
  })
57
87
  ]);
88
+ const sameChanges = Schema.toEquivalence(Schema.Array(Change));
89
+ const VersionProbe = Schema.Struct({
90
+ format: Schema.Literal(FORMAT),
91
+ version: Schema.Unknown
92
+ });
58
93
  const Document = Schema.Struct({
59
- format: Schema.Literal("effect-vfs-delta"),
94
+ format: Schema.Literal(FORMAT),
60
95
  version: Schema.Literal(1),
61
- base: Schema.Struct({ algorithm: Schema.Literal("effect-vfs-semantic-sha256-v1"), digest: Schema.String }),
62
- records: Schema.Array(Record),
96
+ base: Schema.Struct({ algorithm: Schema.Literal(ALGORITHM), digest: CanonicalBase64.Encoded }),
97
+ records: Schema.Array(DeltaRecord),
63
98
  changes: Schema.Array(Change)
64
99
  });
100
+ const WireDocument = Schema.Struct({
101
+ format: Schema.Literal(FORMAT),
102
+ version: Schema.Literal(1),
103
+ base: Schema.Struct({ algorithm: Schema.Literal(ALGORITHM), digest: Schema.String }),
104
+ records: Schema.Array(WireDeltaRecord),
105
+ changes: Schema.Array(WireChange)
106
+ });
65
107
  const timestampFields = new Set(["atimeNs", "mtimeNs", "ctimeNs", "birthtimeNs"]);
66
- const differenceOrder = [
67
- "kind",
68
- "content",
69
- "target",
70
- "hardLinks",
71
- "mode",
72
- "uid",
73
- "gid",
74
- "atimeNs",
75
- "mtimeNs",
76
- "ctimeNs",
77
- "birthtimeNs"
78
- ];
108
+ const differenceOrder = SnapshotDifference.literals;
79
109
  const encoder = new TextEncoder();
80
110
  const Json = Schema.fromJsonString(Schema.Unknown);
81
- const failure = (code, field) => new ImageError({ code, ...(field === undefined ? {} : { field }) });
82
- const sameBytes = (a, b) => Equal.equals(a, b);
83
- const compareBytes = (a, b) => {
84
- for (let i = 0; i < Math.min(a.length, b.length); i++) {
85
- const d = a[i] - b[i];
86
- if (d !== 0)
87
- return d;
88
- }
89
- return a.length - b.length;
90
- };
111
+ const JsonDocument = Schema.fromJsonString(Document);
91
112
  const key = Encoding.encodeHex;
92
113
  const join = (parent, name) => {
93
114
  const out = new Uint8Array(parent.length + (parent.length === 1 ? 0 : 1) + name.length);
94
115
  out.set(parent);
95
116
  let offset = parent.length;
96
117
  if (parent.length !== 1)
97
- out[offset++] = 47;
118
+ out[offset++] = SLASH_BYTE;
98
119
  out.set(name, offset);
99
120
  return out;
100
121
  };
101
122
  const parentKey = (path) => {
102
123
  let slash = path.length - 1;
103
- while (slash > 0 && path[slash] !== 47)
124
+ while (slash > 0 && path[slash] !== SLASH_BYTE)
104
125
  slash--;
105
- return key(slash === 0 ? new Uint8Array([47]) : path.subarray(0, slash));
126
+ return key(slash === 0 ? ROOT_PATH : path.subarray(0, slash));
106
127
  };
107
128
  const basename = (path) => {
108
129
  let slash = path.length - 1;
109
- while (slash > 0 && path[slash] !== 47)
130
+ while (slash > 0 && path[slash] !== SLASH_BYTE)
110
131
  slash--;
111
132
  return path.subarray(slash + 1);
112
133
  };
113
- const decode64 = (s, field) => CanonicalBase64.isCanonical(s) ? Effect.succeed(Image.bytes(s)) : Effect.fail(failure("InvalidEncoding", field));
114
134
  const safeAdd = (a, b) => {
115
135
  const n = a + b;
116
136
  return Number.isSafeInteger(n) ? n : undefined;
117
137
  };
118
- const encodedPayloadLength = (value) => value.length / 4 * 3 - (value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0);
119
- const exceedsByteLimit = (value, limit) => BigInt(value) > BigInt(limit);
138
+ const exceeds = (value, limit) => ByteSize.isGreaterThan(ByteSize.bytes(value), limit);
120
139
  const validPath = (path) => {
121
- if (path.length === 0 || path[0] !== 47 || path.includes(0) || (path.length > 1 && path.at(-1) === 47))
140
+ if (path.length === 0 || path[0] !== SLASH_BYTE || path.includes(NUL_BYTE) ||
141
+ (path.length > 1 && path.at(-1) === SLASH_BYTE))
122
142
  return false;
123
143
  if (path.length === 1)
124
144
  return true;
125
145
  let start = 1;
126
146
  for (let i = 1; i <= path.length; i++) {
127
- if (i === path.length || path[i] === 47) {
147
+ if (i === path.length || path[i] === SLASH_BYTE) {
128
148
  const length = i - start;
129
- if (length < 1 || length > 255 || (length === 1 && path[start] === 46) ||
130
- (length === 2 && path[start] === 46 && path[start + 1] === 46))
149
+ if (length < 1 || length > MAX_NAME_BYTES || (length === 1 && path[start] === DOT_BYTE) ||
150
+ (length === 2 && path[start] === DOT_BYTE && path[start + 1] === DOT_BYTE))
131
151
  return false;
132
152
  start = i + 1;
133
153
  }
134
154
  }
135
155
  return true;
136
156
  };
157
+ const roleBudget = (limits, role) => role === "base"
158
+ ? {
159
+ records: limits.maxBaseRecords,
160
+ recordsField: "baseRecords",
161
+ pathBytes: limits.maxIdentityBytes,
162
+ pathBytesField: "identityBytes",
163
+ payloadBytes: limits.maxIdentityBytes,
164
+ payloadBytesField: "identityBytes"
165
+ }
166
+ : {
167
+ records: limits.maxTargetRecords,
168
+ recordsField: "targetRecords",
169
+ pathBytes: limits.maxDecodedDeltaBytes,
170
+ pathBytesField: "decodedDeltaBytes",
171
+ payloadBytes: limits.maxOutputBytes,
172
+ payloadBytesField: "outputBytes"
173
+ };
174
+ const encodedPayload = (record) => Match.value(record).pipe(Match.tag("directory", () => undefined), Match.tag("file", (record) => record.data), Match.tag("symlink", (record) => record.target), Match.exhaustive);
137
175
  const normalize = Effect.fnUntraced(function* (snapshot, limits, role) {
138
176
  const doc = yield* Image.inspect(snapshot);
139
- if (doc.records.length > (role === "base" ? limits.maxBaseRecords : limits.maxTargetRecords)) {
140
- return yield* failure("LimitExceeded", `${role}Records`);
177
+ const budget = roleBudget(limits, role);
178
+ if (doc.records.length > budget.records) {
179
+ return yield* new ImageError({ code: "LimitExceeded", field: budget.recordsField });
141
180
  }
142
181
  const records = new Map(doc.records.map((record) => [record.id, record]));
143
182
  const pathsById = new Map();
144
- const pending = [[doc.root, new Uint8Array([47])]];
183
+ const pending = [[doc.root, ROOT_PATH]];
145
184
  let entries = 0;
146
- let pathBytes = 1;
185
+ let pathBytes = ByteSize.bytes(ROOT_PATH.length);
186
+ // Index loop: `pending` grows while it is being walked.
147
187
  for (let i = 0; i < pending.length; i++) {
148
188
  const [recordId, path] = pending[i];
149
189
  const record = records.get(recordId);
150
190
  if (record === undefined)
151
- return yield* failure("InvalidStructure", role);
191
+ return yield* new ImageError({ code: "InvalidStructure", field: role });
152
192
  const paths = pathsById.get(recordId);
153
193
  if (paths === undefined)
154
194
  pathsById.set(recordId, [path]);
155
195
  else
156
196
  paths.push(path);
157
- if (record.kind === "directory") {
197
+ if (Image.Record.guards.directory(record)) {
158
198
  for (const entry of record.entries) {
159
199
  if (++entries > limits.maxEntries)
160
- return yield* failure("LimitExceeded", "entries");
161
- const nameLength = encodedPayloadLength(entry.name);
162
- const pathLength = safeAdd(path.length + (path.length === 1 ? 0 : 1), nameLength);
163
- const nextPathBytes = pathLength === undefined ? undefined : safeAdd(pathBytes, pathLength);
164
- const pathLimit = role === "base" ? limits.maxIdentityBytes : limits.maxDecodedDeltaBytes;
165
- if (nextPathBytes === undefined || exceedsByteLimit(nextPathBytes, pathLimit)) {
166
- return yield* failure("LimitExceeded", role === "base" ? "identityBytes" : "decodedDeltaBytes");
200
+ return yield* new ImageError({ code: "LimitExceeded", field: "entries" });
201
+ const separator = path.length === ROOT_PATH.length ? 0 : 1;
202
+ const pathLength = path.length + separator + CanonicalBase64.decodedLength(entry.name);
203
+ pathBytes = ByteSize.sum(pathBytes, ByteSize.bytes(pathLength));
204
+ if (ByteSize.isGreaterThan(pathBytes, budget.pathBytes)) {
205
+ return yield* new ImageError({ code: "LimitExceeded", field: budget.pathBytesField });
167
206
  }
168
- pathBytes = nextPathBytes;
169
- pending.push([entry.target, join(path, Image.bytes(entry.name))]);
207
+ pending.push([entry.target, join(path, yield* CanonicalBase64.decode(entry.name))]);
170
208
  }
171
209
  }
172
210
  }
173
211
  const objects = [];
174
212
  const byPath = new Map();
175
- let payloadBytes = 0;
176
- let basePayloadBytes = 0;
213
+ let payloadBytes = ByteSize.zero;
177
214
  for (const record of doc.records) {
178
215
  const paths = pathsById.get(record.id);
179
216
  if (paths === undefined)
180
- return yield* failure("InvalidStructure", role);
181
- paths.sort(compareBytes);
182
- const encodedPayload = record.kind === "directory"
183
- ? undefined
184
- : record.kind === "file"
185
- ? record.data
186
- : record.target;
187
- if (encodedPayload !== undefined) {
188
- const payloadLength = encodedPayloadLength(encodedPayload);
189
- const next = safeAdd(role === "base" ? basePayloadBytes : payloadBytes, payloadLength);
190
- const limit = role === "base" ? limits.maxIdentityBytes : limits.maxOutputBytes;
191
- if (next === undefined || exceedsByteLimit(next, limit)) {
192
- return yield* failure("LimitExceeded", role === "base" ? "identityBytes" : "outputBytes");
217
+ return yield* new ImageError({ code: "InvalidStructure", field: role });
218
+ paths.sort(bytesOrder);
219
+ const encoded = encodedPayload(record);
220
+ if (encoded !== undefined) {
221
+ payloadBytes = ByteSize.sum(payloadBytes, ByteSize.bytes(CanonicalBase64.decodedLength(encoded)));
222
+ if (ByteSize.isGreaterThan(payloadBytes, budget.payloadBytes)) {
223
+ return yield* new ImageError({ code: "LimitExceeded", field: budget.payloadBytesField });
193
224
  }
194
- if (role === "base")
195
- basePayloadBytes = next;
196
- else
197
- payloadBytes = next;
198
225
  }
199
- const payload = encodedPayload === undefined ? undefined : Image.bytes(encodedPayload);
200
226
  const object = {
201
- kind: record.kind,
227
+ kind: record._tag,
202
228
  metadata: record.metadata,
203
229
  paths,
204
230
  pathIdentity: paths.map(key).join("/"),
205
- payload
231
+ payload: encoded === undefined ? undefined : yield* CanonicalBase64.decode(encoded)
206
232
  };
207
233
  objects.push(object);
208
234
  for (const path of paths)
209
235
  byPath.set(key(path), { object, path });
210
236
  }
211
- objects.sort((a, b) => compareBytes(a.paths[0], b.paths[0]));
237
+ objects.sort((a, b) => bytesOrder(a.paths[0], b.paths[0]));
212
238
  return { objects, byPath };
213
239
  });
240
+ const U64_BYTES = 8;
241
+ const EMPTY = new Uint8Array();
242
+ const IDENTITY_PREFIX = encoder.encode(`${ALGORITHM}\0`);
243
+ const KIND_BYTE = { directory: 0, file: 1, symlink: 2 };
214
244
  const u64 = (n) => {
215
- const out = new Uint8Array(8);
245
+ const out = new Uint8Array(U64_BYTES);
216
246
  new DataView(out.buffer).setBigUint64(0, BigInt(n), false);
217
247
  return out;
218
248
  };
249
+ const framedLength = (bytes) => U64_BYTES + bytes.length;
250
+ const timestampBytes = (metadata) => [metadata.atimeNs, metadata.mtimeNs, metadata.ctimeNs, metadata.birthtimeNs].map((value) => encoder.encode(String(value)));
219
251
  // This is the versioned semantic identity encoding, not a generic byte builder.
220
252
  // Field framing, ordering, and the domain prefix are part of the persisted delta contract.
221
- const identityBytes = Effect.fnUntraced(function* (view, limits) {
222
- let length = 0;
223
- const initialCapacity = ByteSize.isGreaterThanOrEqualTo(limits.maxIdentityBytes, ByteSize.kibibytes(1))
224
- ? 1024
225
- : Number(ByteSize.toBigInt(limits.maxIdentityBytes));
226
- let output = new Uint8Array(initialCapacity);
227
- const append = (bytes) => {
228
- const next = safeAdd(length, bytes.length);
229
- if (next === undefined || BigInt(next) > ByteSize.toBigInt(limits.maxIdentityBytes)) {
230
- return Effect.fail(failure("LimitExceeded", "identityBytes"));
231
- }
232
- if (next > output.length) {
233
- let capacity = Math.max(1, output.length);
234
- while (capacity < next)
235
- capacity = Math.min(next, capacity * 2);
236
- const expanded = new Uint8Array(capacity);
237
- expanded.set(output);
238
- output = expanded;
239
- }
240
- output.set(bytes, length);
241
- length = next;
242
- return Effect.void;
253
+ const identityBytes = (view, limits) => {
254
+ const timestamps = view.objects.map((object) => timestampBytes(object.metadata));
255
+ let total = ByteSize.bytes(IDENTITY_PREFIX.length + U64_BYTES);
256
+ for (const [index, object] of view.objects.entries()) {
257
+ const objectBytes = U64_BYTES +
258
+ object.paths.reduce((sum, path) => sum + framedLength(path), 0) +
259
+ 1 + 3 * U64_BYTES +
260
+ timestamps[index].reduce((sum, stamp) => sum + framedLength(stamp), 0) +
261
+ framedLength(object.payload ?? EMPTY);
262
+ total = ByteSize.sum(total, ByteSize.bytes(objectBytes));
263
+ }
264
+ if (ByteSize.isGreaterThan(total, limits.maxIdentityBytes)) {
265
+ return Result.fail(new ImageError({ code: "LimitExceeded", field: "identityBytes" }));
266
+ }
267
+ const output = new Uint8Array(Number(ByteSize.toBigInt(total)));
268
+ let offset = 0;
269
+ const put = (bytes) => {
270
+ output.set(bytes, offset);
271
+ offset += bytes.length;
243
272
  };
244
- const frame = Effect.fnUntraced(function* (bytes) {
245
- yield* append(u64(bytes.length));
246
- yield* append(bytes);
247
- });
248
- yield* append(encoder.encode("effect-vfs-semantic-sha256-v1\0"));
249
- yield* append(u64(view.objects.length));
250
- for (const object of view.objects) {
251
- yield* append(u64(object.paths.length));
273
+ const frame = (bytes) => {
274
+ put(u64(bytes.length));
275
+ put(bytes);
276
+ };
277
+ put(IDENTITY_PREFIX);
278
+ put(u64(view.objects.length));
279
+ for (const [index, object] of view.objects.entries()) {
280
+ put(u64(object.paths.length));
252
281
  for (const path of object.paths)
253
- yield* frame(path);
254
- yield* append(new Uint8Array([object.kind === "directory" ? 0 : object.kind === "file" ? 1 : 2]));
255
- yield* append(u64(object.metadata.uid));
256
- yield* append(u64(object.metadata.gid));
257
- yield* append(u64(object.metadata.mode));
258
- yield* frame(encoder.encode(object.metadata.atimeNs));
259
- yield* frame(encoder.encode(object.metadata.mtimeNs));
260
- yield* frame(encoder.encode(object.metadata.ctimeNs));
261
- yield* frame(encoder.encode(object.metadata.birthtimeNs));
262
- yield* frame(object.payload ?? new Uint8Array());
282
+ frame(path);
283
+ put(new Uint8Array([KIND_BYTE[object.kind]]));
284
+ put(u64(object.metadata.uid));
285
+ put(u64(object.metadata.gid));
286
+ put(u64(object.metadata.mode));
287
+ for (const stamp of timestamps[index])
288
+ frame(stamp);
289
+ frame(object.payload ?? EMPTY);
263
290
  }
264
- return output.slice(0, length);
265
- });
291
+ return Result.succeed(output);
292
+ };
266
293
  const digest = Effect.fnUntraced(function* (view, limits) {
267
294
  const crypto = yield* Crypto.Crypto;
268
- return yield* crypto.digest("SHA-256", yield* identityBytes(view, limits));
295
+ return yield* crypto.digest("SHA-256", yield* Effect.fromResult(identityBytes(view, limits)));
269
296
  });
270
297
  const samePaths = (a, b) => a.pathIdentity === b.pathIdentity;
271
298
  const differences = (before, after) => differenceOrder.filter((field) => {
@@ -284,264 +311,265 @@ const differences = (before, after) => differenceOrder.filter((field) => {
284
311
  const compare = (base, target) => {
285
312
  const changes = [];
286
313
  const add = (change, path) => changes.push({ change, path });
287
- const differencesByPair = new WeakMap();
288
- const cachedDifferences = (before, after) => {
289
- let byAfter = differencesByPair.get(before);
290
- if (byAfter === undefined) {
291
- byAfter = new WeakMap();
292
- differencesByPair.set(before, byAfter);
293
- }
294
- const cached = byAfter.get(after);
295
- if (cached !== undefined)
296
- return cached;
297
- const value = differences(before, after);
298
- byAfter.set(after, value);
299
- return value;
300
- };
314
+ // Memoized per object pair so hard-link groups compare each pair once.
315
+ const cachedDifferences = Fn.memoize((before) => Fn.memoize((after) => differences(before, after)));
301
316
  for (const [pathKey, before] of base.byPath) {
302
317
  const after = target.byPath.get(pathKey);
303
318
  if (after === undefined) {
304
- add({ _tag: "Removed", path: Image.base64(before.path), kind: before.object.kind }, before.path);
319
+ add(RemovedChange.make({ path: CanonicalBase64.encode(before.path), kind: before.object.kind }), before.path);
305
320
  }
306
321
  else {
307
- const diff = cachedDifferences(before.object, after.object);
322
+ const diff = cachedDifferences(before.object)(after.object);
308
323
  if (diff.length > 0) {
309
- add({
310
- _tag: "Updated",
311
- path: Image.base64(before.path),
324
+ add(UpdatedChange.make({
325
+ path: CanonicalBase64.encode(before.path),
312
326
  beforeKind: before.object.kind,
313
327
  afterKind: after.object.kind,
314
328
  differences: diff
315
- }, before.path);
329
+ }), before.path);
316
330
  }
317
331
  }
318
332
  }
319
333
  for (const [pathKey, after] of target.byPath) {
320
334
  if (!base.byPath.has(pathKey)) {
321
- add({ _tag: "Added", path: Image.base64(after.path), kind: after.object.kind }, after.path);
335
+ add(AddedChange.make({ path: CanonicalBase64.encode(after.path), kind: after.object.kind }), after.path);
322
336
  }
323
337
  }
324
- changes.sort((a, b) => compareBytes(a.path, b.path));
338
+ changes.sort((a, b) => bytesOrder(a.path, b.path));
325
339
  return changes.map(({ change }) => change);
326
340
  };
327
341
  const getDocument = (delta) => Effect.suspend(() => {
328
- const value = snapshotDeltaValue(delta);
342
+ const value = SnapshotDeltaModel.value(delta);
329
343
  return value !== undefined && Schema.is(Document)(value)
330
344
  ? Effect.succeed(value)
331
- : Effect.fail(failure("InvalidStructure", "delta"));
345
+ : Effect.fail(new ImageError({ code: "InvalidStructure", field: "delta" }));
346
+ });
347
+ const byEntryName = (a, b) => bytesOrder(a.name, b.name);
348
+ // Every recorded path must be the root directory or the child of a recorded directory.
349
+ // Together with `validPath` and the per-record path rules in `validate`, this subsumes the
350
+ // structural checks `Image.capture` would otherwise perform on the applied image.
351
+ const linkTree = (paths) => {
352
+ const rootKey = key(ROOT_PATH);
353
+ const root = paths.get(rootKey);
354
+ if (root?.kind !== "directory")
355
+ return Result.fail(new ImageError({ code: "InvalidStructure", field: "root" }));
356
+ const entriesById = new Map();
357
+ for (const entry of paths.values()) {
358
+ if (entry.kind === "directory")
359
+ entriesById.set(entry.id, []);
360
+ }
361
+ for (const [pathKey, child] of paths) {
362
+ if (pathKey === rootKey)
363
+ continue;
364
+ const parent = paths.get(parentKey(child.bytes));
365
+ const entries = parent === undefined ? undefined : entriesById.get(parent.id);
366
+ if (entries === undefined)
367
+ return Result.fail(new ImageError({ code: "InvalidStructure", field: "parent" }));
368
+ entries.push({ name: basename(child.bytes), target: child.id });
369
+ }
370
+ for (const entries of entriesById.values())
371
+ entries.sort(byEntryName);
372
+ return Result.succeed({ root: root.id, entriesById });
373
+ };
374
+ const inheritedPayload = Effect.fnUntraced(function* (base, kind, encodedPath) {
375
+ const source = base.byPath.get(key(yield* CanonicalBase64.decode(encodedPath)))?.object;
376
+ return source?.kind === kind ? source.payload : undefined;
332
377
  });
378
+ // Expects a document that already passed `validate`.
333
379
  const buildImage = Effect.fnUntraced(function* (document, base, limits) {
334
380
  const paths = new Map();
335
- const records = [];
336
- const entriesById = new Map();
337
- let bytes = 0;
338
- for (let i = 0; i < document.records.length; i++) {
339
- const record = document.records[i];
340
- const recordId = String(i);
381
+ for (const [index, record] of document.records.entries()) {
341
382
  for (const encodedPath of record.paths) {
342
- const path = yield* decode64(encodedPath, "path");
343
- paths.set(key(path), { id: recordId, kind: record.kind, bytes: path });
383
+ const bytes = yield* CanonicalBase64.decode(encodedPath);
384
+ paths.set(key(bytes), { id: String(index), kind: record.kind, bytes });
344
385
  }
386
+ }
387
+ const tree = yield* Effect.fromResult(linkTree(paths));
388
+ const records = [];
389
+ let outputBytes = ByteSize.zero;
390
+ for (const [index, record] of document.records.entries()) {
391
+ const id = String(index);
345
392
  if (record.kind === "directory") {
346
- const entries = [];
347
- entriesById.set(recordId, entries);
348
- records.push({ id: recordId, kind: "directory", metadata: record.metadata, entries });
393
+ records.push(Image.Record.cases.directory.make({
394
+ id,
395
+ metadata: record.metadata,
396
+ entries: (tree.entriesById.get(id) ?? []).map((entry) => ({
397
+ name: CanonicalBase64.encode(entry.name),
398
+ target: entry.target
399
+ }))
400
+ }));
349
401
  continue;
350
402
  }
351
- const inherited = record.payload._tag === "Base"
352
- ? base?.byPath.get(key(yield* decode64(record.payload.path, "basePath")))?.object
353
- : undefined;
354
- const inlineLength = record.payload._tag === "Inline"
355
- ? encodedPayloadLength(record.payload.bytes)
356
- : undefined;
357
- const payloadLength = inlineLength ?? (base === undefined
358
- ? 0
359
- : inherited?.kind === record.kind
360
- ? inherited.payload?.length
361
- : undefined);
362
- const next = payloadLength === undefined ? undefined : safeAdd(bytes, payloadLength);
363
- if (next !== undefined && exceedsByteLimit(next, limits.maxOutputBytes)) {
364
- return yield* failure("LimitExceeded", "outputBytes");
403
+ const payload = Predicate.isTagged("Inline")(record.payload)
404
+ ? yield* CanonicalBase64.decode(record.payload.bytes)
405
+ : yield* inheritedPayload(base, record.kind, record.payload.path);
406
+ if (payload === undefined)
407
+ return yield* new ImageError({ code: "InvalidStructure", field: "baseReference" });
408
+ outputBytes = ByteSize.sum(outputBytes, ByteSize.bytes(payload.length));
409
+ if (ByteSize.isGreaterThan(outputBytes, limits.maxOutputBytes)) {
410
+ return yield* new ImageError({ code: "LimitExceeded", field: "outputBytes" });
365
411
  }
366
- const payload = record.payload._tag === "Inline"
367
- ? yield* decode64(record.payload.bytes, "payload")
368
- : inherited?.kind === record.kind
369
- ? inherited.payload
370
- : undefined;
371
- if (payload === undefined && base !== undefined)
372
- return yield* failure("InvalidStructure", "baseReference");
373
- const actual = payload ?? new Uint8Array();
374
- if (next === undefined)
375
- return yield* failure("LimitExceeded", "outputBytes");
376
- bytes = next;
377
412
  records.push(record.kind === "file"
378
- ? { id: recordId, kind: "file", metadata: record.metadata, data: Image.base64(actual) }
379
- : { id: recordId, kind: "symlink", metadata: record.metadata, target: Image.base64(actual) });
413
+ ? Image.Record.cases.file.make({ id, metadata: record.metadata, data: CanonicalBase64.encode(payload) })
414
+ : Image.Record.cases.symlink.make({
415
+ id,
416
+ metadata: record.metadata,
417
+ target: CanonicalBase64.encode(payload)
418
+ }));
380
419
  }
381
- const root = paths.get(key(new Uint8Array([47])));
382
- if (root?.kind !== "directory")
383
- return yield* failure("InvalidStructure", "root");
384
- for (const [pathKey, child] of paths) {
385
- if (pathKey !== key(new Uint8Array([47]))) {
386
- const parent = paths.get(parentKey(child.bytes));
387
- const entries = parent === undefined ? undefined : entriesById.get(parent.id);
388
- if (entries === undefined)
389
- return yield* failure("InvalidStructure", "parent");
390
- entries.push({ name: Image.base64(basename(child.bytes)), target: child.id });
391
- }
392
- }
393
- for (const entries of entriesById.values())
394
- entries.sort((a, b) => a.name.localeCompare(b.name));
395
- return yield* Image.capture({ format: "effect-vfs", version: 1, root: root.id, records });
420
+ return yield* Image.capture({ format: "effect-vfs", version: 1, root: tree.root, records }, undefined, true);
396
421
  });
397
422
  const validate = Effect.fnUntraced(function* (document, limits) {
398
423
  const deltaRecords = safeAdd(document.records.length, document.changes.length);
399
424
  if (deltaRecords === undefined || deltaRecords > limits.maxDeltaRecords) {
400
- return yield* failure("LimitExceeded", "deltaRecords");
425
+ return yield* new ImageError({ code: "LimitExceeded", field: "deltaRecords" });
426
+ }
427
+ if (document.records.length > limits.maxOutputRecords) {
428
+ return yield* new ImageError({ code: "LimitExceeded", field: "outputRecords" });
401
429
  }
402
- if (document.records.length > limits.maxOutputRecords)
403
- return yield* failure("LimitExceeded", "outputRecords");
404
- if (!CanonicalBase64.isCanonical(document.base.digest) || CanonicalBase64.decodedLength(document.base.digest) !== 32) {
405
- return yield* failure("InvalidEncoding", "digest");
430
+ if (!CanonicalBase64.is(document.base.digest) ||
431
+ CanonicalBase64.decodedLength(document.base.digest) !== SHA256_BYTES) {
432
+ return yield* new ImageError({ code: "InvalidEncoding", field: "digest" });
406
433
  }
407
- let decoded = 32;
434
+ let decoded = ByteSize.bytes(SHA256_BYTES);
435
+ let outputBytes = ByteSize.zero;
408
436
  let entries = -1;
409
437
  let inherited = 0;
410
- const seen = new Set();
411
- for (const record of document.records) {
438
+ const paths = new Map();
439
+ const charge = (encoded) => {
440
+ decoded = ByteSize.sum(decoded, ByteSize.bytes(CanonicalBase64.decodedLength(encoded)));
441
+ return ByteSize.isGreaterThan(decoded, limits.maxDecodedDeltaBytes)
442
+ ? Effect.fail(new ImageError({ code: "LimitExceeded", field: "decodedDeltaBytes" }))
443
+ : Effect.void;
444
+ };
445
+ for (const [index, record] of document.records.entries()) {
412
446
  if (record.paths.length < 1 || (record.kind === "directory" && record.paths.length !== 1)) {
413
- return yield* failure("InvalidStructure", "paths");
447
+ return yield* new ImageError({ code: "InvalidStructure", field: "paths" });
414
448
  }
415
449
  for (const encodedPath of record.paths) {
416
- if (!CanonicalBase64.isCanonical(encodedPath))
417
- return yield* failure("InvalidEncoding", "path");
418
- decoded = safeAdd(decoded, CanonicalBase64.decodedLength(encodedPath)) ?? Number.POSITIVE_INFINITY;
419
- if (BigInt(decoded) > ByteSize.toBigInt(limits.maxDecodedDeltaBytes)) {
420
- return yield* failure("LimitExceeded", "decodedDeltaBytes");
450
+ if (!CanonicalBase64.is(encodedPath)) {
451
+ return yield* new ImageError({ code: "InvalidEncoding", field: "path" });
421
452
  }
453
+ yield* charge(encodedPath);
422
454
  entries++;
423
455
  if (entries > limits.maxEntries)
424
- return yield* failure("LimitExceeded", "entries");
425
- const path = yield* decode64(encodedPath, "path");
426
- if (!validPath(path) || seen.has(key(path)))
427
- return yield* failure("InvalidStructure", "path");
428
- seen.add(key(path));
456
+ return yield* new ImageError({ code: "LimitExceeded", field: "entries" });
457
+ const bytes = yield* CanonicalBase64.decode(encodedPath);
458
+ const pathKey = key(bytes);
459
+ if (!validPath(bytes) || paths.has(pathKey)) {
460
+ return yield* new ImageError({ code: "InvalidStructure", field: "path" });
461
+ }
462
+ paths.set(pathKey, { id: String(index), kind: record.kind, bytes });
429
463
  }
430
- if (record.kind !== "directory") {
431
- if (record.payload._tag === "Inline") {
432
- if (!CanonicalBase64.isCanonical(record.payload.bytes))
433
- return yield* failure("InvalidEncoding", "payload");
434
- decoded = safeAdd(decoded, CanonicalBase64.decodedLength(record.payload.bytes)) ?? Number.POSITIVE_INFINITY;
435
- if (BigInt(decoded) > ByteSize.toBigInt(limits.maxDecodedDeltaBytes)) {
436
- return yield* failure("LimitExceeded", "decodedDeltaBytes");
437
- }
464
+ if (record.kind === "directory")
465
+ continue;
466
+ if (Predicate.isTagged("Inline")(record.payload)) {
467
+ if (!CanonicalBase64.is(record.payload.bytes)) {
468
+ return yield* new ImageError({ code: "InvalidEncoding", field: "payload" });
438
469
  }
439
- else {
440
- if (!CanonicalBase64.isCanonical(record.payload.path))
441
- return yield* failure("InvalidEncoding", "basePath");
442
- decoded = safeAdd(decoded, CanonicalBase64.decodedLength(record.payload.path)) ?? Number.POSITIVE_INFINITY;
443
- if (BigInt(decoded) > ByteSize.toBigInt(limits.maxDecodedDeltaBytes)) {
444
- return yield* failure("LimitExceeded", "decodedDeltaBytes");
445
- }
446
- inherited++;
447
- if (inherited > limits.maxInheritedRecords) {
448
- return yield* failure("LimitExceeded", "inheritedRecords");
449
- }
450
- const path = yield* decode64(record.payload.path, "basePath");
451
- if (!validPath(path) || !record.paths.includes(record.payload.path)) {
452
- return yield* failure("InvalidStructure", "basePath");
453
- }
470
+ yield* charge(record.payload.bytes);
471
+ outputBytes = ByteSize.sum(outputBytes, ByteSize.bytes(CanonicalBase64.decodedLength(record.payload.bytes)));
472
+ }
473
+ else {
474
+ if (!CanonicalBase64.is(record.payload.path)) {
475
+ return yield* new ImageError({ code: "InvalidEncoding", field: "basePath" });
476
+ }
477
+ yield* charge(record.payload.path);
478
+ inherited++;
479
+ if (inherited > limits.maxInheritedRecords) {
480
+ return yield* new ImageError({ code: "LimitExceeded", field: "inheritedRecords" });
481
+ }
482
+ if (!validPath(yield* CanonicalBase64.decode(record.payload.path)) || !record.paths.includes(record.payload.path)) {
483
+ return yield* new ImageError({ code: "InvalidStructure", field: "basePath" });
454
484
  }
455
485
  }
456
486
  }
457
487
  let previous;
458
488
  for (const change of document.changes) {
459
- if (!CanonicalBase64.isCanonical(change.path))
460
- return yield* failure("InvalidEncoding", "changePath");
461
- decoded = safeAdd(decoded, CanonicalBase64.decodedLength(change.path)) ?? Number.POSITIVE_INFINITY;
462
- if (BigInt(decoded) > ByteSize.toBigInt(limits.maxDecodedDeltaBytes)) {
463
- return yield* failure("LimitExceeded", "decodedDeltaBytes");
489
+ if (!CanonicalBase64.is(change.path)) {
490
+ return yield* new ImageError({ code: "InvalidEncoding", field: "changePath" });
464
491
  }
465
- const path = yield* decode64(change.path, "changePath");
466
- if (!validPath(path) || (previous !== undefined && compareBytes(previous, path) >= 0)) {
467
- return yield* failure("InvalidStructure", "changes");
492
+ yield* charge(change.path);
493
+ const path = yield* CanonicalBase64.decode(change.path);
494
+ if (!validPath(path) || (previous !== undefined && bytesOrder(previous, path) >= 0)) {
495
+ return yield* new ImageError({ code: "InvalidStructure", field: "changes" });
468
496
  }
469
497
  previous = path;
470
498
  }
471
- yield* buildImage(document, undefined, limits);
499
+ if (ByteSize.isGreaterThan(outputBytes, limits.maxOutputBytes)) {
500
+ return yield* new ImageError({ code: "LimitExceeded", field: "outputBytes" });
501
+ }
502
+ yield* Effect.fromResult(linkTree(paths));
503
+ // Inline symlink targets are the only applied payloads a base snapshot has not already vetted.
504
+ for (const record of document.records) {
505
+ if (record.kind !== "symlink" || !Predicate.isTagged("Inline")(record.payload))
506
+ continue;
507
+ if (!CanonicalBase64.is(record.payload.bytes)) {
508
+ return yield* new ImageError({ code: "InvalidEncoding", field: "payload" });
509
+ }
510
+ if ((yield* CanonicalBase64.decode(record.payload.bytes)).includes(NUL_BYTE)) {
511
+ return yield* new ImageError({ code: "InvalidStructure", field: "symlink" });
512
+ }
513
+ }
472
514
  });
473
515
  /** @internal */
474
- export const diffSnapshots = Effect.fn("VirtualFileSystem.diffSnapshots")(function* (base, target, limits) {
516
+ export const diffSnapshots = Effect.fnUntraced(function* (base, target, limits) {
475
517
  const before = yield* normalize(base, limits, "base");
476
518
  const after = yield* normalize(target, limits, "target");
477
519
  const changes = [...compare(before, after)];
478
520
  const deltaRecords = safeAdd(after.objects.length, changes.length);
479
521
  if (deltaRecords === undefined || deltaRecords > limits.maxDeltaRecords) {
480
- return yield* failure("LimitExceeded", "deltaRecords");
522
+ return yield* new ImageError({ code: "LimitExceeded", field: "deltaRecords" });
481
523
  }
482
- const payloadEquality = new WeakMap();
483
- const samePayload = (source, target) => {
484
- let byTarget = payloadEquality.get(source);
485
- if (byTarget === undefined) {
486
- byTarget = new WeakMap();
487
- payloadEquality.set(source, byTarget);
488
- }
489
- const cached = byTarget.get(target);
490
- if (cached !== undefined)
491
- return cached;
492
- const value = sameBytes(source.payload, target.payload);
493
- byTarget.set(target, value);
494
- return value;
495
- };
524
+ const samePayload = Fn.memoize((source) => Fn.memoize((candidate) => sameBytes(source.payload, candidate.payload)));
496
525
  const records = after.objects.map((object) => {
497
- const paths = object.paths.map(Image.base64);
526
+ const paths = object.paths.map(CanonicalBase64.encode);
498
527
  if (object.kind === "directory")
499
528
  return { kind: object.kind, paths, metadata: object.metadata };
500
529
  const inherited = object.paths.find((path) => {
501
530
  const source = before.byPath.get(key(path));
502
- return source?.object.kind === object.kind && samePayload(source.object, object);
531
+ return source?.object.kind === object.kind && samePayload(source.object)(object);
503
532
  });
504
533
  return {
505
534
  kind: object.kind,
506
535
  paths,
507
536
  metadata: object.metadata,
508
537
  payload: inherited === undefined
509
- ? { _tag: "Inline", bytes: Image.base64(object.payload ?? new Uint8Array()) }
510
- : { _tag: "Base", path: Image.base64(inherited) }
538
+ ? InlinePayload.make({ bytes: CanonicalBase64.encode(object.payload ?? new Uint8Array()) })
539
+ : BasePayload.make({ path: CanonicalBase64.encode(inherited) })
511
540
  };
512
541
  });
513
542
  const document = {
514
- format: "effect-vfs-delta",
543
+ format: FORMAT,
515
544
  version: 1,
516
- base: { algorithm: "effect-vfs-semantic-sha256-v1", digest: Image.base64(yield* digest(before, limits)) },
545
+ base: { algorithm: ALGORITHM, digest: CanonicalBase64.encode(yield* digest(before, limits)) },
517
546
  records,
518
547
  changes
519
548
  };
520
549
  yield* validate(document, limits);
521
- return makeSnapshotDelta(document);
550
+ return SnapshotDeltaModel.make(document);
522
551
  });
523
552
  /** @internal */
524
553
  const verify = Effect.fnUntraced(function* (base, document, limits) {
525
554
  yield* validate(document, limits);
526
555
  const before = yield* normalize(base, limits, "base");
527
- if (!sameBytes(yield* digest(before, limits), Image.bytes(document.base.digest))) {
556
+ if (!sameBytes(yield* digest(before, limits), yield* CanonicalBase64.decode(document.base.digest))) {
528
557
  return yield* new SnapshotDeltaError({ code: "BaseMismatch" });
529
558
  }
530
559
  const target = yield* buildImage(document, before, limits);
531
560
  const changes = compare(before, yield* normalize(target, limits, "target"));
532
- const actualChanges = yield* Schema.encodeEffect(Json)(changes).pipe(Effect.mapError(() => failure("InvalidStructure", "changes")));
533
- const expectedChanges = yield* Schema.encodeEffect(Json)(document.changes).pipe(Effect.mapError(() => failure("InvalidStructure", "changes")));
534
- if (actualChanges !== expectedChanges)
535
- return yield* failure("InvalidStructure", "changes");
561
+ if (!sameChanges(changes, document.changes)) {
562
+ return yield* new ImageError({ code: "InvalidStructure", field: "changes" });
563
+ }
536
564
  return { changes, target };
537
565
  });
538
566
  /** @internal */
539
- export const inspectSnapshotDelta = Effect.fn("VirtualFileSystem.inspectSnapshotDelta")(function* (base, delta, options, limits) {
567
+ export const inspectSnapshotDelta = Effect.fnUntraced(function* (base, delta, options, limits) {
540
568
  const output = [];
541
569
  const { changes } = yield* verify(base, yield* getDocument(delta), limits);
542
570
  for (const change of changes) {
543
- const path = makeBytePath(new Uint8Array(yield* decode64(change.path, "changePath")));
544
- if (change._tag !== "Updated")
571
+ const path = makeBytePath(yield* CanonicalBase64.decode(change.path));
572
+ if (!Predicate.isTagged("Updated")(change))
545
573
  output.push(Object.freeze({ ...change, path }));
546
574
  else {
547
575
  const fields = change.differences.filter((field) => options.includeTimestamps === true || !timestampFields.has(field));
@@ -555,35 +583,40 @@ export const inspectSnapshotDelta = Effect.fn("VirtualFileSystem.inspectSnapshot
555
583
  export const encodeSnapshotDelta = Effect.fnUntraced(function* (delta, limits) {
556
584
  const document = yield* getDocument(delta);
557
585
  yield* validate(document, limits);
558
- const text = yield* Schema.encodeEffect(Json)(document).pipe(Effect.mapError(() => failure("InvalidStructure")));
586
+ const text = yield* Schema.encodeEffect(JsonDocument)(document).pipe(Effect.mapError(() => new ImageError({ code: "InvalidStructure" })));
559
587
  const bytes = encoder.encode(text);
560
- if (BigInt(bytes.length) > ByteSize.toBigInt(limits.maxEncodedBytes)) {
561
- return yield* failure("LimitExceeded", "encodedBytes");
588
+ if (exceeds(bytes.length, limits.maxEncodedBytes)) {
589
+ return yield* new ImageError({ code: "LimitExceeded", field: "encodedBytes" });
562
590
  }
563
591
  return bytes;
564
592
  });
565
593
  /** @internal */
566
594
  export const decodeSnapshotDelta = Effect.fnUntraced(function* (input, limits) {
567
- if (!(input instanceof Uint8Array) || !(input.buffer instanceof ArrayBuffer))
568
- return yield* failure("InvalidEncoding");
569
- if (BigInt(input.byteLength) > ByteSize.toBigInt(limits.maxEncodedBytes)) {
570
- return yield* failure("LimitExceeded", "encodedBytes");
595
+ if (!(input instanceof Uint8Array) || !(input.buffer instanceof ArrayBuffer)) {
596
+ return yield* new ImageError({ code: "InvalidEncoding" });
597
+ }
598
+ if (exceeds(input.byteLength, limits.maxEncodedBytes)) {
599
+ return yield* new ImageError({ code: "LimitExceeded", field: "encodedBytes" });
571
600
  }
572
601
  const text = yield* Effect.try({
573
602
  try: () => new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array(input)),
574
- catch: () => failure("InvalidEncoding")
603
+ catch: () => new ImageError({ code: "InvalidEncoding" })
575
604
  });
576
- const value = yield* Schema.decodeEffect(Json)(text).pipe(Effect.mapError(() => failure("InvalidEncoding")));
577
- if (typeof value === "object" && value !== null && "format" in value && value.format === "effect-vfs-delta" &&
578
- "version" in value && value.version !== 1)
579
- return yield* failure("UnsupportedVersion");
605
+ const value = yield* Schema.decodeEffect(Json)(text).pipe(Effect.mapError(() => new ImageError({ code: "InvalidEncoding" })));
606
+ const version = Schema.decodeUnknownResult(VersionProbe)(value);
607
+ if (Result.isSuccess(version) && version.success.version !== 1) {
608
+ return yield* new ImageError({ code: "UnsupportedVersion" });
609
+ }
610
+ const wire = Schema.decodeUnknownResult(WireDocument, { onExcessProperty: "error" })(value);
611
+ if (Result.isFailure(wire))
612
+ return yield* new ImageError({ code: "InvalidStructure" });
613
+ yield* validate(wire.success, limits);
580
614
  const parsed = Schema.decodeUnknownResult(Document, { onExcessProperty: "error" })(value);
581
615
  if (Result.isFailure(parsed))
582
- return yield* failure("InvalidStructure");
583
- yield* validate(parsed.success, limits);
584
- return makeSnapshotDelta(parsed.success);
616
+ return yield* new ImageError({ code: "InvalidEncoding" });
617
+ return SnapshotDeltaModel.make(parsed.success);
585
618
  });
586
619
  /** @internal */
587
- export const applySnapshotDelta = Effect.fn("VirtualFileSystem.applySnapshotDelta")(function* (base, delta, limits) {
620
+ export const applySnapshotDelta = Effect.fnUntraced(function* (base, delta, limits) {
588
621
  return (yield* verify(base, yield* getDocument(delta), limits)).target;
589
622
  });