@effect-vfs/core 0.3.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 (53) hide show
  1. package/dist/BytePath.d.ts +33 -2
  2. package/dist/BytePath.d.ts.map +1 -1
  3. package/dist/BytePath.js +1 -1
  4. package/dist/Snapshot.d.ts +58 -2
  5. package/dist/Snapshot.d.ts.map +1 -1
  6. package/dist/Snapshot.js +57 -1
  7. package/dist/SnapshotDelta.d.ts +113 -3
  8. package/dist/SnapshotDelta.d.ts.map +1 -1
  9. package/dist/SnapshotDelta.js +112 -2
  10. package/dist/VirtualFileSystem.d.ts +919 -10
  11. package/dist/VirtualFileSystem.d.ts.map +1 -1
  12. package/dist/VirtualFileSystem.js +575 -7
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/internal/bytePath.d.ts.map +1 -1
  16. package/dist/internal/bytePath.js +5 -10
  17. package/dist/internal/bytes.d.ts +6 -0
  18. package/dist/internal/bytes.d.ts.map +1 -0
  19. package/dist/internal/bytes.js +17 -0
  20. package/dist/internal/canonicalBase64.d.ts +13 -5
  21. package/dist/internal/canonicalBase64.d.ts.map +1 -1
  22. package/dist/internal/canonicalBase64.js +28 -6
  23. package/dist/internal/image.d.ts +76 -90
  24. package/dist/internal/image.d.ts.map +1 -1
  25. package/dist/internal/image.js +98 -89
  26. package/dist/internal/metadata.d.ts +28 -0
  27. package/dist/internal/metadata.d.ts.map +1 -0
  28. package/dist/internal/metadata.js +29 -0
  29. package/dist/internal/snapshotDelta.d.ts.map +1 -1
  30. package/dist/internal/snapshotDelta.js +346 -300
  31. package/dist/internal/snapshotDeltaModel.d.ts +2 -2
  32. package/dist/internal/snapshotDeltaModel.d.ts.map +1 -1
  33. package/dist/internal/snapshotDeltaModel.js +3 -1
  34. package/dist/internal/virtualFileSystem/errors.d.ts +5 -0
  35. package/dist/internal/virtualFileSystem/errors.d.ts.map +1 -1
  36. package/dist/internal/virtualFileSystem/errors.js +19 -0
  37. package/dist/internal/virtualFileSystem/fixture.d.ts.map +1 -1
  38. package/dist/internal/virtualFileSystem/fixture.js +74 -74
  39. package/dist/internal/virtualFileSystem/overlayContent.d.ts.map +1 -1
  40. package/dist/internal/virtualFileSystem/overlayContent.js +9 -2
  41. package/dist/internal/virtualFileSystem/overlayDiff.d.ts +2 -11
  42. package/dist/internal/virtualFileSystem/overlayDiff.d.ts.map +1 -1
  43. package/dist/internal/virtualFileSystem/overlayDiff.js +45 -71
  44. package/dist/internal/virtualFileSystem/path.d.ts +15 -14
  45. package/dist/internal/virtualFileSystem/path.d.ts.map +1 -1
  46. package/dist/internal/virtualFileSystem/path.js +57 -54
  47. package/dist/internal/virtualFileSystem/testHooks.d.ts.map +1 -1
  48. package/dist/internal/virtualFileSystem/watchHub.d.ts.map +1 -1
  49. package/dist/internal/virtualFileSystem/watchHub.js +13 -13
  50. package/dist/internal/virtualFileSystem.d.ts +91 -15
  51. package/dist/internal/virtualFileSystem.d.ts.map +1 -1
  52. package/dist/internal/virtualFileSystem.js +470 -392
  53. package/package.json +15 -5
@@ -3,21 +3,32 @@ 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
11
  import { ImageError } from "../Snapshot.js";
10
12
  import { SnapshotDeltaError, SnapshotDifference, SnapshotNodeKind } from "../SnapshotDelta.js";
11
13
  import { make as makeBytePath } from "./bytePath.js";
12
- import * as CanonicalBase64 from "./canonicalBase64.js";
14
+ import { bytesOrder, sameBytes } from "./bytes.js";
15
+ import { CanonicalBase64 } from "./canonicalBase64.js";
13
16
  import * as Image from "./image.js";
17
+ import { WireStoredMetadata } from "./metadata.js";
14
18
  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([
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([
21
32
  Schema.Struct({ kind: Schema.Literal("directory"), paths: Schema.Array(Path), metadata: Image.StoredMetadata }),
22
33
  Schema.Struct({
23
34
  kind: Schema.Literal("file"),
@@ -32,227 +43,256 @@ const Record = Schema.Union([
32
43
  payload: Payload
33
44
  })
34
45
  ]);
35
- const Change = Schema.Union([
36
- Schema.TaggedStruct("Added", { path: Path, kind: SnapshotNodeKind }),
37
- Schema.TaggedStruct("Removed", { path: Path, kind: SnapshotNodeKind }),
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 })
58
+ ]);
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 }),
38
81
  Schema.TaggedStruct("Updated", {
39
- path: Path,
82
+ path: Schema.String,
40
83
  beforeKind: SnapshotNodeKind,
41
84
  afterKind: SnapshotNodeKind,
42
85
  differences: Schema.Array(SnapshotDifference).check(Schema.isMinLength(1))
43
86
  })
44
87
  ]);
88
+ const sameChanges = Schema.toEquivalence(Schema.Array(Change));
89
+ const VersionProbe = Schema.Struct({
90
+ format: Schema.Literal(FORMAT),
91
+ version: Schema.Unknown
92
+ });
45
93
  const Document = Schema.Struct({
46
- format: Schema.Literal("effect-vfs-delta"),
94
+ format: Schema.Literal(FORMAT),
47
95
  version: Schema.Literal(1),
48
- base: Schema.Struct({ algorithm: Schema.Literal("effect-vfs-semantic-sha256-v1"), digest: Schema.String }),
49
- records: Schema.Array(Record),
96
+ base: Schema.Struct({ algorithm: Schema.Literal(ALGORITHM), digest: CanonicalBase64.Encoded }),
97
+ records: Schema.Array(DeltaRecord),
50
98
  changes: Schema.Array(Change)
51
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
+ });
52
107
  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
- ];
108
+ const differenceOrder = SnapshotDifference.literals;
66
109
  const encoder = new TextEncoder();
67
110
  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
- };
111
+ const JsonDocument = Schema.fromJsonString(Document);
78
112
  const key = Encoding.encodeHex;
79
113
  const join = (parent, name) => {
80
114
  const out = new Uint8Array(parent.length + (parent.length === 1 ? 0 : 1) + name.length);
81
115
  out.set(parent);
82
116
  let offset = parent.length;
83
117
  if (parent.length !== 1)
84
- out[offset++] = 47;
118
+ out[offset++] = SLASH_BYTE;
85
119
  out.set(name, offset);
86
120
  return out;
87
121
  };
88
122
  const parentKey = (path) => {
89
123
  let slash = path.length - 1;
90
- while (slash > 0 && path[slash] !== 47)
124
+ while (slash > 0 && path[slash] !== SLASH_BYTE)
91
125
  slash--;
92
- return key(slash === 0 ? new Uint8Array([47]) : path.subarray(0, slash));
126
+ return key(slash === 0 ? ROOT_PATH : path.subarray(0, slash));
93
127
  };
94
128
  const basename = (path) => {
95
129
  let slash = path.length - 1;
96
- while (slash > 0 && path[slash] !== 47)
130
+ while (slash > 0 && path[slash] !== SLASH_BYTE)
97
131
  slash--;
98
132
  return path.subarray(slash + 1);
99
133
  };
100
- const decode64 = (s, field) => CanonicalBase64.isCanonical(s) ? Effect.succeed(Image.bytes(s)) : Effect.fail(failure("InvalidEncoding", field));
101
134
  const safeAdd = (a, b) => {
102
135
  const n = a + b;
103
136
  return Number.isSafeInteger(n) ? n : undefined;
104
137
  };
105
- const encodedPayloadLength = (value) => value.length / 4 * 3 - (value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0);
106
- const exceedsByteLimit = (value, limit) => BigInt(value) > BigInt(limit);
138
+ const exceeds = (value, limit) => ByteSize.isGreaterThan(ByteSize.bytes(value), limit);
107
139
  const validPath = (path) => {
108
- 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))
109
142
  return false;
110
143
  if (path.length === 1)
111
144
  return true;
112
145
  let start = 1;
113
146
  for (let i = 1; i <= path.length; i++) {
114
- if (i === path.length || path[i] === 47) {
147
+ if (i === path.length || path[i] === SLASH_BYTE) {
115
148
  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))
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))
118
151
  return false;
119
152
  start = i + 1;
120
153
  }
121
154
  }
122
155
  return true;
123
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);
124
175
  const normalize = Effect.fnUntraced(function* (snapshot, limits, role) {
125
176
  const doc = yield* Image.inspect(snapshot);
126
- if (doc.records.length > (role === "base" ? limits.maxBaseRecords : limits.maxTargetRecords)) {
127
- 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 });
128
180
  }
129
181
  const records = new Map(doc.records.map((record) => [record.id, record]));
130
182
  const pathsById = new Map();
131
- const pending = [[doc.root, new Uint8Array([47])]];
183
+ const pending = [[doc.root, ROOT_PATH]];
132
184
  let entries = 0;
133
- let pathBytes = 1;
185
+ let pathBytes = ByteSize.bytes(ROOT_PATH.length);
186
+ // Index loop: `pending` grows while it is being walked.
134
187
  for (let i = 0; i < pending.length; i++) {
135
188
  const [recordId, path] = pending[i];
136
189
  const record = records.get(recordId);
137
190
  if (record === undefined)
138
- return yield* failure("InvalidStructure", role);
191
+ return yield* new ImageError({ code: "InvalidStructure", field: role });
139
192
  const paths = pathsById.get(recordId);
140
193
  if (paths === undefined)
141
194
  pathsById.set(recordId, [path]);
142
195
  else
143
196
  paths.push(path);
144
- if (record.kind === "directory") {
197
+ if (Image.Record.guards.directory(record)) {
145
198
  for (const entry of record.entries) {
146
199
  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");
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 });
154
206
  }
155
- pathBytes = nextPathBytes;
156
- pending.push([entry.target, join(path, Image.bytes(entry.name))]);
207
+ pending.push([entry.target, join(path, yield* CanonicalBase64.decode(entry.name))]);
157
208
  }
158
209
  }
159
210
  }
160
211
  const objects = [];
161
212
  const byPath = new Map();
162
- let payloadBytes = 0;
163
- let basePayloadBytes = 0;
213
+ let payloadBytes = ByteSize.zero;
164
214
  for (const record of doc.records) {
165
215
  const paths = pathsById.get(record.id);
166
216
  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");
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 });
180
224
  }
181
- if (role === "base")
182
- basePayloadBytes = next;
183
- else
184
- payloadBytes = next;
185
225
  }
186
- const payload = encodedPayload === undefined ? undefined : Image.bytes(encodedPayload);
187
226
  const object = {
188
- kind: record.kind,
227
+ kind: record._tag,
189
228
  metadata: record.metadata,
190
229
  paths,
191
230
  pathIdentity: paths.map(key).join("/"),
192
- payload
231
+ payload: encoded === undefined ? undefined : yield* CanonicalBase64.decode(encoded)
193
232
  };
194
233
  objects.push(object);
195
234
  for (const path of paths)
196
235
  byPath.set(key(path), { object, path });
197
236
  }
198
- objects.sort((a, b) => compareBytes(a.paths[0], b.paths[0]));
237
+ objects.sort((a, b) => bytesOrder(a.paths[0], b.paths[0]));
199
238
  return { objects, byPath };
200
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 };
201
244
  const u64 = (n) => {
202
- const out = new Uint8Array(8);
245
+ const out = new Uint8Array(U64_BYTES);
203
246
  new DataView(out.buffer).setBigUint64(0, BigInt(n), false);
204
247
  return out;
205
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)));
206
251
  // This is the versioned semantic identity encoding, not a generic byte builder.
207
252
  // 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;
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;
230
272
  };
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));
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));
239
281
  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());
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);
250
290
  }
251
- return output.slice(0, length);
252
- });
291
+ return Result.succeed(output);
292
+ };
253
293
  const digest = Effect.fnUntraced(function* (view, limits) {
254
294
  const crypto = yield* Crypto.Crypto;
255
- return yield* crypto.digest("SHA-256", yield* identityBytes(view, limits));
295
+ return yield* crypto.digest("SHA-256", yield* Effect.fromResult(identityBytes(view, limits)));
256
296
  });
257
297
  const samePaths = (a, b) => a.pathIdentity === b.pathIdentity;
258
298
  const differences = (before, after) => differenceOrder.filter((field) => {
@@ -271,191 +311,206 @@ const differences = (before, after) => differenceOrder.filter((field) => {
271
311
  const compare = (base, target) => {
272
312
  const changes = [];
273
313
  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
- };
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)));
288
316
  for (const [pathKey, before] of base.byPath) {
289
317
  const after = target.byPath.get(pathKey);
290
318
  if (after === undefined) {
291
- 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);
292
320
  }
293
321
  else {
294
- const diff = cachedDifferences(before.object, after.object);
322
+ const diff = cachedDifferences(before.object)(after.object);
295
323
  if (diff.length > 0) {
296
- add({
297
- _tag: "Updated",
298
- path: Image.base64(before.path),
324
+ add(UpdatedChange.make({
325
+ path: CanonicalBase64.encode(before.path),
299
326
  beforeKind: before.object.kind,
300
327
  afterKind: after.object.kind,
301
328
  differences: diff
302
- }, before.path);
329
+ }), before.path);
303
330
  }
304
331
  }
305
332
  }
306
333
  for (const [pathKey, after] of target.byPath) {
307
334
  if (!base.byPath.has(pathKey)) {
308
- 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);
309
336
  }
310
337
  }
311
- changes.sort((a, b) => compareBytes(a.path, b.path));
338
+ changes.sort((a, b) => bytesOrder(a.path, b.path));
312
339
  return changes.map(({ change }) => change);
313
340
  };
314
341
  const getDocument = (delta) => Effect.suspend(() => {
315
342
  const value = SnapshotDeltaModel.value(delta);
316
343
  return value !== undefined && Schema.is(Document)(value)
317
344
  ? Effect.succeed(value)
318
- : Effect.fail(failure("InvalidStructure", "delta"));
345
+ : Effect.fail(new ImageError({ code: "InvalidStructure", field: "delta" }));
319
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;
377
+ });
378
+ // Expects a document that already passed `validate`.
320
379
  const buildImage = Effect.fnUntraced(function* (document, base, limits) {
321
380
  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);
381
+ for (const [index, record] of document.records.entries()) {
328
382
  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 });
383
+ const bytes = yield* CanonicalBase64.decode(encodedPath);
384
+ paths.set(key(bytes), { id: String(index), kind: record.kind, bytes });
331
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);
332
392
  if (record.kind === "directory") {
333
- const entries = [];
334
- entriesById.set(recordId, entries);
335
- 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
+ }));
336
401
  continue;
337
402
  }
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");
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" });
352
411
  }
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
412
  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) });
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
+ }));
367
419
  }
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 });
420
+ return yield* Image.capture({ format: "effect-vfs", version: 1, root: tree.root, records }, undefined, true);
383
421
  });
384
422
  const validate = Effect.fnUntraced(function* (document, limits) {
385
423
  const deltaRecords = safeAdd(document.records.length, document.changes.length);
386
424
  if (deltaRecords === undefined || deltaRecords > limits.maxDeltaRecords) {
387
- 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" });
388
429
  }
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");
430
+ if (!CanonicalBase64.is(document.base.digest) ||
431
+ CanonicalBase64.decodedLength(document.base.digest) !== SHA256_BYTES) {
432
+ return yield* new ImageError({ code: "InvalidEncoding", field: "digest" });
393
433
  }
394
- let decoded = 32;
434
+ let decoded = ByteSize.bytes(SHA256_BYTES);
435
+ let outputBytes = ByteSize.zero;
395
436
  let entries = -1;
396
437
  let inherited = 0;
397
- const seen = new Set();
398
- 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()) {
399
446
  if (record.paths.length < 1 || (record.kind === "directory" && record.paths.length !== 1)) {
400
- return yield* failure("InvalidStructure", "paths");
447
+ return yield* new ImageError({ code: "InvalidStructure", field: "paths" });
401
448
  }
402
449
  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");
450
+ if (!CanonicalBase64.is(encodedPath)) {
451
+ return yield* new ImageError({ code: "InvalidEncoding", field: "path" });
408
452
  }
453
+ yield* charge(encodedPath);
409
454
  entries++;
410
455
  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));
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 });
416
463
  }
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
- }
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" });
425
469
  }
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
- }
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" });
441
484
  }
442
485
  }
443
486
  }
444
487
  let previous;
445
488
  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");
489
+ if (!CanonicalBase64.is(change.path)) {
490
+ return yield* new ImageError({ code: "InvalidEncoding", field: "changePath" });
451
491
  }
452
- const path = yield* decode64(change.path, "changePath");
453
- if (!validPath(path) || (previous !== undefined && compareBytes(previous, path) >= 0)) {
454
- 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" });
455
496
  }
456
497
  previous = path;
457
498
  }
458
- 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
+ }
459
514
  });
460
515
  /** @internal */
461
516
  export const diffSnapshots = Effect.fnUntraced(function* (base, target, limits) {
@@ -464,43 +519,30 @@ export const diffSnapshots = Effect.fnUntraced(function* (base, target, limits)
464
519
  const changes = [...compare(before, after)];
465
520
  const deltaRecords = safeAdd(after.objects.length, changes.length);
466
521
  if (deltaRecords === undefined || deltaRecords > limits.maxDeltaRecords) {
467
- return yield* failure("LimitExceeded", "deltaRecords");
522
+ return yield* new ImageError({ code: "LimitExceeded", field: "deltaRecords" });
468
523
  }
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
- };
524
+ const samePayload = Fn.memoize((source) => Fn.memoize((candidate) => sameBytes(source.payload, candidate.payload)));
483
525
  const records = after.objects.map((object) => {
484
- const paths = object.paths.map(Image.base64);
526
+ const paths = object.paths.map(CanonicalBase64.encode);
485
527
  if (object.kind === "directory")
486
528
  return { kind: object.kind, paths, metadata: object.metadata };
487
529
  const inherited = object.paths.find((path) => {
488
530
  const source = before.byPath.get(key(path));
489
- return source?.object.kind === object.kind && samePayload(source.object, object);
531
+ return source?.object.kind === object.kind && samePayload(source.object)(object);
490
532
  });
491
533
  return {
492
534
  kind: object.kind,
493
535
  paths,
494
536
  metadata: object.metadata,
495
537
  payload: inherited === undefined
496
- ? { _tag: "Inline", bytes: Image.base64(object.payload ?? new Uint8Array()) }
497
- : { _tag: "Base", path: Image.base64(inherited) }
538
+ ? InlinePayload.make({ bytes: CanonicalBase64.encode(object.payload ?? new Uint8Array()) })
539
+ : BasePayload.make({ path: CanonicalBase64.encode(inherited) })
498
540
  };
499
541
  });
500
542
  const document = {
501
- format: "effect-vfs-delta",
543
+ format: FORMAT,
502
544
  version: 1,
503
- 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)) },
504
546
  records,
505
547
  changes
506
548
  };
@@ -511,15 +553,14 @@ export const diffSnapshots = Effect.fnUntraced(function* (base, target, limits)
511
553
  const verify = Effect.fnUntraced(function* (base, document, limits) {
512
554
  yield* validate(document, limits);
513
555
  const before = yield* normalize(base, limits, "base");
514
- if (!sameBytes(yield* digest(before, limits), Image.bytes(document.base.digest))) {
556
+ if (!sameBytes(yield* digest(before, limits), yield* CanonicalBase64.decode(document.base.digest))) {
515
557
  return yield* new SnapshotDeltaError({ code: "BaseMismatch" });
516
558
  }
517
559
  const target = yield* buildImage(document, before, limits);
518
560
  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");
561
+ if (!sameChanges(changes, document.changes)) {
562
+ return yield* new ImageError({ code: "InvalidStructure", field: "changes" });
563
+ }
523
564
  return { changes, target };
524
565
  });
525
566
  /** @internal */
@@ -527,8 +568,8 @@ export const inspectSnapshotDelta = Effect.fnUntraced(function* (base, delta, op
527
568
  const output = [];
528
569
  const { changes } = yield* verify(base, yield* getDocument(delta), limits);
529
570
  for (const change of changes) {
530
- const path = makeBytePath(new Uint8Array(yield* decode64(change.path, "changePath")));
531
- if (change._tag !== "Updated")
571
+ const path = makeBytePath(yield* CanonicalBase64.decode(change.path));
572
+ if (!Predicate.isTagged("Updated")(change))
532
573
  output.push(Object.freeze({ ...change, path }));
533
574
  else {
534
575
  const fields = change.differences.filter((field) => options.includeTimestamps === true || !timestampFields.has(field));
@@ -542,32 +583,37 @@ export const inspectSnapshotDelta = Effect.fnUntraced(function* (base, delta, op
542
583
  export const encodeSnapshotDelta = Effect.fnUntraced(function* (delta, limits) {
543
584
  const document = yield* getDocument(delta);
544
585
  yield* validate(document, limits);
545
- 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" })));
546
587
  const bytes = encoder.encode(text);
547
- if (BigInt(bytes.length) > ByteSize.toBigInt(limits.maxEncodedBytes)) {
548
- return yield* failure("LimitExceeded", "encodedBytes");
588
+ if (exceeds(bytes.length, limits.maxEncodedBytes)) {
589
+ return yield* new ImageError({ code: "LimitExceeded", field: "encodedBytes" });
549
590
  }
550
591
  return bytes;
551
592
  });
552
593
  /** @internal */
553
594
  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");
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" });
558
600
  }
559
601
  const text = yield* Effect.try({
560
602
  try: () => new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array(input)),
561
- catch: () => failure("InvalidEncoding")
603
+ catch: () => new ImageError({ code: "InvalidEncoding" })
562
604
  });
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");
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);
567
614
  const parsed = Schema.decodeUnknownResult(Document, { onExcessProperty: "error" })(value);
568
615
  if (Result.isFailure(parsed))
569
- return yield* failure("InvalidStructure");
570
- yield* validate(parsed.success, limits);
616
+ return yield* new ImageError({ code: "InvalidEncoding" });
571
617
  return SnapshotDeltaModel.make(parsed.success);
572
618
  });
573
619
  /** @internal */