@effect-vfs/core 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -9
- package/dist/BytePath.d.ts +21 -0
- package/dist/BytePath.d.ts.map +1 -0
- package/dist/BytePath.js +16 -0
- package/dist/Snapshot.d.ts +2 -2
- package/dist/Snapshot.js +2 -2
- package/dist/SnapshotDelta.d.ts +126 -0
- package/dist/SnapshotDelta.d.ts.map +1 -0
- package/dist/SnapshotDelta.js +112 -0
- package/dist/VirtualFileSystem.d.ts +232 -22
- package/dist/VirtualFileSystem.d.ts.map +1 -1
- package/dist/VirtualFileSystem.js +344 -95
- package/dist/internal/canonicalBase64.d.ts +7 -0
- package/dist/internal/canonicalBase64.d.ts.map +1 -0
- package/dist/internal/canonicalBase64.js +11 -0
- package/dist/internal/content.d.ts +25 -0
- package/dist/internal/content.d.ts.map +1 -0
- package/dist/internal/content.js +26 -0
- package/dist/internal/image.d.ts +7 -5
- package/dist/internal/image.d.ts.map +1 -1
- package/dist/internal/image.js +10 -14
- package/dist/internal/overlayChanges.d.ts +50 -0
- package/dist/internal/overlayChanges.d.ts.map +1 -0
- package/dist/internal/overlayChanges.js +146 -0
- package/dist/internal/overlayTesting.d.ts +12 -0
- package/dist/internal/overlayTesting.d.ts.map +1 -0
- package/dist/internal/overlayTesting.js +12 -0
- package/dist/internal/snapshotDelta.d.ts +88 -0
- package/dist/internal/snapshotDelta.d.ts.map +1 -0
- package/dist/internal/snapshotDelta.js +589 -0
- package/package.json +6 -5
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Strict padded base64 check, including canonical unused pad bits and no ignored whitespace. */
|
|
2
|
+
export declare const isCanonical: (value: string) => boolean;
|
|
3
|
+
/** Decoded length for an already canonical base64 string, without allocating its payload. */
|
|
4
|
+
export declare const decodedLength: (value: string) => number;
|
|
5
|
+
/** Decodes base64 previously accepted by `isCanonical`. */
|
|
6
|
+
export declare const decodeTrusted: (value: string) => Uint8Array;
|
|
7
|
+
//# sourceMappingURL=canonicalBase64.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canonicalBase64.d.ts","sourceRoot":"","sources":["../../src/internal/canonicalBase64.ts"],"names":[],"mappings":"AAKA,iGAAiG;AACjG,eAAO,MAAM,WAAW,UAAW,MAAM,KAAG,OAGP,CAAA;AAErC,6FAA6F;AAC7F,eAAO,MAAM,aAAa,UAAW,MAAM,KAAG,MACmC,CAAA;AAEjF,2DAA2D;AAC3D,eAAO,MAAM,aAAa,UAAW,MAAM,KAAG,UAC4D,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as Encoding from "effect/Encoding";
|
|
2
|
+
import * as Result from "effect/Result";
|
|
3
|
+
const canonicalTail = /^(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/][AQgw]==|[A-Za-z0-9+/]{2}[AEIMQUYcgkosw048]=)?(?![\s\S])/;
|
|
4
|
+
/** Strict padded base64 check, including canonical unused pad bits and no ignored whitespace. */
|
|
5
|
+
export const isCanonical = (value) => value.length % 4 === 0 &&
|
|
6
|
+
!/[^A-Za-z0-9+/]/.test(value.slice(0, -4)) &&
|
|
7
|
+
canonicalTail.test(value.slice(-4));
|
|
8
|
+
/** Decoded length for an already canonical base64 string, without allocating its payload. */
|
|
9
|
+
export const decodedLength = (value) => value.length / 4 * 3 - (value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0);
|
|
10
|
+
/** Decodes base64 previously accepted by `isCanonical`. */
|
|
11
|
+
export const decodeTrusted = (value) => Result.getOrThrowWith(Encoding.decodeBase64(value), () => new Error("Invalid trusted canonical base64"));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Immutable regular-file payloads shared by overlay workspaces.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
import type { Snapshot } from "../Snapshot.js";
|
|
7
|
+
import * as Image from "./image.js";
|
|
8
|
+
/** @internal */
|
|
9
|
+
export interface Content {
|
|
10
|
+
readonly bytes: Uint8Array;
|
|
11
|
+
}
|
|
12
|
+
/** @internal */
|
|
13
|
+
export declare const hasOverlayContents: (snapshot: Snapshot) => boolean;
|
|
14
|
+
/** @internal */
|
|
15
|
+
export declare const make: (bytes: Uint8Array) => Content;
|
|
16
|
+
/** @internal */
|
|
17
|
+
export declare const empty: () => Content;
|
|
18
|
+
/**
|
|
19
|
+
* Returns the immutable file payloads associated with the identity of a base
|
|
20
|
+
* snapshot. The cache is weak so it cannot extend the snapshot's lifetime.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export declare const forOverlay: (snapshot: Snapshot, image: Image.Document) => ReadonlyMap<string, Content>;
|
|
25
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/internal/content.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAEnC,gBAAgB;AAChB,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;CAC3B;AAID,gBAAgB;AAChB,eAAO,MAAM,kBAAkB,aAAc,QAAQ,KAAG,OAAwC,CAAA;AAEhG,gBAAgB;AAChB,eAAO,MAAM,IAAI,UAAW,UAAU,KAAG,OAAsB,CAAA;AAE/D,gBAAgB;AAChB,eAAO,MAAM,KAAK,QAAO,OAAkC,CAAA;AAE3D;;;;;GAKG;AACH,eAAO,MAAM,UAAU,aAAc,QAAQ,SAAS,KAAK,CAAC,QAAQ,KAAG,WAAW,CAAC,MAAM,EAAE,OAAO,CASjG,CAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as Image from "./image.js";
|
|
2
|
+
const overlayContents = new WeakMap();
|
|
3
|
+
/** @internal */
|
|
4
|
+
export const hasOverlayContents = (snapshot) => overlayContents.has(snapshot);
|
|
5
|
+
/** @internal */
|
|
6
|
+
export const make = (bytes) => ({ bytes });
|
|
7
|
+
/** @internal */
|
|
8
|
+
export const empty = () => make(new Uint8Array(0));
|
|
9
|
+
/**
|
|
10
|
+
* Returns the immutable file payloads associated with the identity of a base
|
|
11
|
+
* snapshot. The cache is weak so it cannot extend the snapshot's lifetime.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export const forOverlay = (snapshot, image) => {
|
|
16
|
+
const cached = overlayContents.get(snapshot);
|
|
17
|
+
if (cached !== undefined)
|
|
18
|
+
return cached;
|
|
19
|
+
const decoded = new Map();
|
|
20
|
+
for (const record of image.records) {
|
|
21
|
+
if (record.kind === "file")
|
|
22
|
+
decoded.set(record.id, make(Image.bytes(record.data)));
|
|
23
|
+
}
|
|
24
|
+
overlayContents.set(snapshot, decoded);
|
|
25
|
+
return decoded;
|
|
26
|
+
};
|
package/dist/internal/image.d.ts
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @internal
|
|
5
5
|
*/
|
|
6
|
+
import * as ByteSize from "effect/ByteSize";
|
|
6
7
|
import * as Effect from "effect/Effect";
|
|
7
8
|
import * as Schema from "effect/Schema";
|
|
8
9
|
import { ImageError, type Snapshot } from "../Snapshot.js";
|
|
10
|
+
import * as CanonicalBase64 from "./canonicalBase64.js";
|
|
9
11
|
/** @internal */
|
|
10
12
|
export declare const StoredMetadata: Schema.Struct<{
|
|
11
13
|
readonly uid: Schema.Finite;
|
|
@@ -116,7 +118,7 @@ export declare const Document: Schema.Struct<{
|
|
|
116
118
|
/** @internal */
|
|
117
119
|
export type Document = typeof Document.Type;
|
|
118
120
|
/** @internal */
|
|
119
|
-
export declare const decodedLength:
|
|
121
|
+
export declare const decodedLength: typeof CanonicalBase64.decodedLength;
|
|
120
122
|
/** @internal */
|
|
121
123
|
export declare const base64: (input: Uint8Array) => string;
|
|
122
124
|
/** @internal */
|
|
@@ -125,18 +127,18 @@ export declare const bytes: (value: string) => Uint8Array;
|
|
|
125
127
|
export declare const inspect: (snapshot: Snapshot) => Effect.Effect<Document, ImageError>;
|
|
126
128
|
/** @internal */
|
|
127
129
|
export declare const capture: (input: unknown, limits?: {
|
|
128
|
-
readonly maxEncodedBytes:
|
|
130
|
+
readonly maxEncodedBytes: ByteSize.ByteSize;
|
|
129
131
|
readonly maxRecords: number;
|
|
130
132
|
readonly maxEntries: number;
|
|
131
|
-
readonly maxDecodedBytes:
|
|
133
|
+
readonly maxDecodedBytes: ByteSize.ByteSize;
|
|
132
134
|
} | undefined) => Effect.Effect<Snapshot, ImageError, never>;
|
|
133
135
|
/** @internal */
|
|
134
136
|
export declare const encodeSnapshot: (snapshot: Snapshot) => Effect.Effect<Uint8Array<ArrayBuffer>, ImageError, never>;
|
|
135
137
|
/** @internal */
|
|
136
138
|
export declare const decodeSnapshot: (input: Uint8Array<ArrayBufferLike>, limits: {
|
|
137
|
-
readonly maxEncodedBytes:
|
|
139
|
+
readonly maxEncodedBytes: ByteSize.ByteSize;
|
|
138
140
|
readonly maxRecords: number;
|
|
139
141
|
readonly maxEntries: number;
|
|
140
|
-
readonly maxDecodedBytes:
|
|
142
|
+
readonly maxDecodedBytes: ByteSize.ByteSize;
|
|
141
143
|
}) => Effect.Effect<Snapshot, ImageError, never>;
|
|
142
144
|
//# sourceMappingURL=image.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../src/internal/image.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,EAAgB,UAAU,EAAE,KAAK,QAAQ,EAAkB,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../src/internal/image.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,EAAgB,UAAU,EAAE,KAAK,QAAQ,EAAkB,MAAM,gBAAgB,CAAA;AACxF,OAAO,KAAK,eAAe,MAAM,sBAAsB,CAAA;AAavD,gBAAgB;AAChB,eAAO,MAAM,cAAc;;;;;;;;EAQzB,CAAA;AACF,gBAAgB;AAChB,MAAM,MAAM,cAAc,GAAG,OAAO,cAAc,CAAC,IAAI,CAAA;AAEvD,gBAAgB;AAChB,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IASjB,CAAA;AACF,gBAAgB;AAChB,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,IAAI,CAAA;AACvC,gBAAgB;AAChB,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKnB,CAAA;AACF,gBAAgB;AAChB,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAE3C,gBAAgB;AAChB,eAAO,MAAM,aAAa,sCAAgC,CAAA;AAC1D,gBAAgB;AAChB,eAAO,MAAM,MAAM,UAAW,UAAU,KAAG,MAU1C,CAAA;AAGD,gBAAgB;AAChB,eAAO,MAAM,KAAK,UAAW,MAAM,KAAG,UAErC,CAAA;AACD,gBAAgB;AAChB,eAAO,MAAM,OAAO,aAAc,QAAQ,KAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,UAAU,CAI3E,CAAA;AAEJ,gBAAgB;AAChB,eAAO,MAAM,OAAO;;;;;4DAwElB,CAAA;AAEF,gBAAgB;AAChB,eAAO,MAAM,cAAc,mFAKzB,CAAA;AACF,gBAAgB;AAChB,eAAO,MAAM,cAAc;;;;;gDAqB1B,CAAA"}
|
package/dist/internal/image.js
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @internal
|
|
5
5
|
*/
|
|
6
|
+
import * as ByteSize from "effect/ByteSize";
|
|
6
7
|
import * as Effect from "effect/Effect";
|
|
7
8
|
import * as Encoding from "effect/Encoding";
|
|
8
9
|
import * as Result from "effect/Result";
|
|
9
10
|
import * as Schema from "effect/Schema";
|
|
10
11
|
import { DecodeLimits, ImageError, SnapshotTypeId } from "../Snapshot.js";
|
|
12
|
+
import * as CanonicalBase64 from "./canonicalBase64.js";
|
|
11
13
|
const natural = Schema.Finite.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(Number.MAX_SAFE_INTEGER));
|
|
12
14
|
class SnapshotImpl {
|
|
13
15
|
[SnapshotTypeId] = SnapshotTypeId;
|
|
@@ -43,12 +45,8 @@ export const Document = Schema.Struct({
|
|
|
43
45
|
records: Schema.Array(Record)
|
|
44
46
|
});
|
|
45
47
|
const snapshots = new WeakMap();
|
|
46
|
-
const canonicalBase64Tail = /^(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/][AQgw]==|[A-Za-z0-9+/]{2}[AEIMQUYcgkosw048]=)?(?![\s\S])/;
|
|
47
|
-
// Only the final quartet can contain padding. Scanning the alphabet separately avoids
|
|
48
|
-
// the regexp stack growth caused by repeating a four-character group over large files.
|
|
49
|
-
const canonicalBase64 = (value) => value.length % 4 === 0 && !/[^A-Za-z0-9+/]/.test(value.slice(0, -4)) && canonicalBase64Tail.test(value.slice(-4));
|
|
50
48
|
/** @internal */
|
|
51
|
-
export const decodedLength =
|
|
49
|
+
export const decodedLength = CanonicalBase64.decodedLength;
|
|
52
50
|
/** @internal */
|
|
53
51
|
export const base64 = (input) => {
|
|
54
52
|
// Effect's encoder concatenates individual characters. Join bounded chunks so snapshots
|
|
@@ -64,10 +62,7 @@ export const base64 = (input) => {
|
|
|
64
62
|
const error = (code, field) => new ImageError({ code, ...(field === undefined ? {} : { field }) });
|
|
65
63
|
/** @internal */
|
|
66
64
|
export const bytes = (value) => {
|
|
67
|
-
|
|
68
|
-
if (Result.isFailure(result))
|
|
69
|
-
throw new Error("Invalid trusted snapshot base64");
|
|
70
|
-
return result.success;
|
|
65
|
+
return CanonicalBase64.decodeTrusted(value);
|
|
71
66
|
};
|
|
72
67
|
/** @internal */
|
|
73
68
|
export const inspect = (snapshot) => Effect.suspend(() => {
|
|
@@ -82,7 +77,7 @@ export const capture = Effect.fnUntraced(function* (input, limits) {
|
|
|
82
77
|
const document = decoded.success;
|
|
83
78
|
const records = new Map();
|
|
84
79
|
let entries = 0;
|
|
85
|
-
let payload =
|
|
80
|
+
let payload = ByteSize.zero;
|
|
86
81
|
if (limits !== undefined && document.records.length > limits.maxRecords) {
|
|
87
82
|
return yield* error("LimitExceeded", "records");
|
|
88
83
|
}
|
|
@@ -101,10 +96,10 @@ export const capture = Effect.fnUntraced(function* (input, limits) {
|
|
|
101
96
|
? record.entries.map((entry) => entry.name)
|
|
102
97
|
: [record.kind === "file" ? record.data : record.target];
|
|
103
98
|
for (const value of values) {
|
|
104
|
-
if (!
|
|
99
|
+
if (!CanonicalBase64.isCanonical(value))
|
|
105
100
|
return yield* error("InvalidEncoding", record.id);
|
|
106
|
-
payload
|
|
107
|
-
if (
|
|
101
|
+
payload = ByteSize.sum(payload, ByteSize.bytes(decodedLength(value)));
|
|
102
|
+
if (limits !== undefined && ByteSize.isGreaterThan(payload, limits.maxDecodedBytes)) {
|
|
108
103
|
return yield* error("LimitExceeded", "bytes");
|
|
109
104
|
}
|
|
110
105
|
}
|
|
@@ -173,8 +168,9 @@ export const decodeSnapshot = Effect.fn("VirtualFileSystem.decodeSnapshot")(func
|
|
|
173
168
|
return yield* error("InvalidStructure", "limits");
|
|
174
169
|
if (!(input instanceof Uint8Array) || !(input.buffer instanceof ArrayBuffer))
|
|
175
170
|
return yield* error("InvalidEncoding");
|
|
176
|
-
if (input.byteLength
|
|
171
|
+
if (ByteSize.isGreaterThan(ByteSize.bytes(input.byteLength), checked.success.maxEncodedBytes)) {
|
|
177
172
|
return yield* error("LimitExceeded", "encodedBytes");
|
|
173
|
+
}
|
|
178
174
|
const text = yield* Effect.try({
|
|
179
175
|
try: () => new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array(input)),
|
|
180
176
|
catch: () => error("InvalidEncoding")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
export interface ObservationMetadata {
|
|
3
|
+
readonly uid: number;
|
|
4
|
+
readonly gid: number;
|
|
5
|
+
readonly mode: number;
|
|
6
|
+
readonly atimeNs: string;
|
|
7
|
+
readonly mtimeNs: string;
|
|
8
|
+
readonly ctimeNs: string;
|
|
9
|
+
readonly birthtimeNs: string;
|
|
10
|
+
}
|
|
11
|
+
/** @internal */
|
|
12
|
+
export interface ObservationEntry {
|
|
13
|
+
readonly path: Uint8Array;
|
|
14
|
+
readonly lineage: string | undefined;
|
|
15
|
+
readonly kind: "directory" | "file" | "symlink";
|
|
16
|
+
readonly content: Uint8Array | undefined;
|
|
17
|
+
readonly metadata: ObservationMetadata;
|
|
18
|
+
}
|
|
19
|
+
/** @internal */
|
|
20
|
+
export type OverlayDifference = "content" | "mode" | "uid" | "gid" | "atimeNs" | "mtimeNs" | "ctimeNs" | "birthtimeNs";
|
|
21
|
+
/** @internal */
|
|
22
|
+
export type RawOverlayChange = {
|
|
23
|
+
readonly _tag: "Added";
|
|
24
|
+
readonly path: Uint8Array;
|
|
25
|
+
readonly kind: ObservationEntry["kind"];
|
|
26
|
+
} | {
|
|
27
|
+
readonly _tag: "Removed";
|
|
28
|
+
readonly path: Uint8Array;
|
|
29
|
+
readonly kind: ObservationEntry["kind"];
|
|
30
|
+
} | {
|
|
31
|
+
readonly _tag: "Replaced";
|
|
32
|
+
readonly path: Uint8Array;
|
|
33
|
+
readonly beforeKind: ObservationEntry["kind"];
|
|
34
|
+
readonly afterKind: ObservationEntry["kind"];
|
|
35
|
+
readonly differences: ReadonlyArray<OverlayDifference>;
|
|
36
|
+
} | {
|
|
37
|
+
readonly _tag: "Renamed";
|
|
38
|
+
readonly from: Uint8Array;
|
|
39
|
+
readonly to: Uint8Array;
|
|
40
|
+
readonly kind: ObservationEntry["kind"];
|
|
41
|
+
readonly differences: ReadonlyArray<OverlayDifference>;
|
|
42
|
+
} | {
|
|
43
|
+
readonly _tag: "Updated";
|
|
44
|
+
readonly path: Uint8Array;
|
|
45
|
+
readonly kind: ObservationEntry["kind"];
|
|
46
|
+
readonly differences: ReadonlyArray<OverlayDifference>;
|
|
47
|
+
};
|
|
48
|
+
/** @internal */
|
|
49
|
+
export declare const compareOverlay: (base: ReadonlyArray<ObservationEntry>, current: ReadonlyArray<ObservationEntry>, includeTimestamps?: boolean) => ReadonlyArray<RawOverlayChange>;
|
|
50
|
+
//# sourceMappingURL=overlayChanges.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overlayChanges.d.ts","sourceRoot":"","sources":["../../src/internal/overlayChanges.ts"],"names":[],"mappings":"AAGA,gBAAgB;AAChB,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED,gBAAgB;AAChB,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAAA;IAC/C,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,SAAS,CAAA;IACxC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAA;CACvC;AAED,gBAAgB;AAChB,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,CAAA;AAEtH,gBAAgB;AAChB,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAA;CAAE,GAC9F;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAA;CAAE,GAChG;IACA,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAA;IAC7C,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAA;IAC5C,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAA;CACvD,GACC;IACA,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAA;IACvC,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAA;CACvD,GACC;IACA,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAA;IACvC,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAA;CACvD,CAAA;AA4EH,gBAAgB;AAChB,eAAO,MAAM,cAAc,SACnB,aAAa,CAAC,gBAAgB,CAAC,WAC5B,aAAa,CAAC,gBAAgB,CAAC,kCAEvC,aAAa,CAAC,gBAAgB,CAoEhC,CAAA"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import * as Encoding from "effect/Encoding";
|
|
2
|
+
import * as Equal from "effect/Equal";
|
|
3
|
+
const timestampFields = new Set(["atimeNs", "mtimeNs", "ctimeNs", "birthtimeNs"]);
|
|
4
|
+
const differenceOrder = [
|
|
5
|
+
"content",
|
|
6
|
+
"mode",
|
|
7
|
+
"uid",
|
|
8
|
+
"gid",
|
|
9
|
+
"atimeNs",
|
|
10
|
+
"mtimeNs",
|
|
11
|
+
"ctimeNs",
|
|
12
|
+
"birthtimeNs"
|
|
13
|
+
];
|
|
14
|
+
const tagOrder = {
|
|
15
|
+
Added: 0,
|
|
16
|
+
Removed: 1,
|
|
17
|
+
Replaced: 2,
|
|
18
|
+
Renamed: 3,
|
|
19
|
+
Updated: 4
|
|
20
|
+
};
|
|
21
|
+
const key = Encoding.encodeHex;
|
|
22
|
+
const comparePaths = (left, right) => {
|
|
23
|
+
const length = Math.min(left.length, right.length);
|
|
24
|
+
for (let index = 0; index < length; index++) {
|
|
25
|
+
const difference = left[index] - right[index];
|
|
26
|
+
if (difference !== 0)
|
|
27
|
+
return difference;
|
|
28
|
+
}
|
|
29
|
+
return left.length - right.length;
|
|
30
|
+
};
|
|
31
|
+
const sameBytes = (left, right) => {
|
|
32
|
+
return Equal.equals(left, right);
|
|
33
|
+
};
|
|
34
|
+
const differences = (before, after, includeTimestamps) => differenceOrder.filter((field) => {
|
|
35
|
+
if (!includeTimestamps && timestampFields.has(field))
|
|
36
|
+
return false;
|
|
37
|
+
if (field === "content")
|
|
38
|
+
return before.kind !== after.kind || !sameBytes(before.content, after.content);
|
|
39
|
+
return before.metadata[field] !== after.metadata[field];
|
|
40
|
+
});
|
|
41
|
+
const byLineage = (entries) => {
|
|
42
|
+
const result = new Map();
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (entry.lineage === undefined)
|
|
45
|
+
continue;
|
|
46
|
+
const group = result.get(entry.lineage);
|
|
47
|
+
if (group === undefined)
|
|
48
|
+
result.set(entry.lineage, new Map([[key(entry.path), entry]]));
|
|
49
|
+
else
|
|
50
|
+
group.set(key(entry.path), entry);
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
const copyChange = (change) => {
|
|
55
|
+
switch (change._tag) {
|
|
56
|
+
case "Added":
|
|
57
|
+
case "Removed":
|
|
58
|
+
return Object.freeze({ ...change, path: change.path.slice() });
|
|
59
|
+
case "Replaced":
|
|
60
|
+
case "Updated":
|
|
61
|
+
return Object.freeze({
|
|
62
|
+
...change,
|
|
63
|
+
path: change.path.slice(),
|
|
64
|
+
differences: Object.freeze([...change.differences])
|
|
65
|
+
});
|
|
66
|
+
case "Renamed":
|
|
67
|
+
return Object.freeze({
|
|
68
|
+
...change,
|
|
69
|
+
from: change.from.slice(),
|
|
70
|
+
to: change.to.slice(),
|
|
71
|
+
differences: Object.freeze([...change.differences])
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
/** @internal */
|
|
76
|
+
export const compareOverlay = (base, current, includeTimestamps = false) => {
|
|
77
|
+
const basePaths = new Map(base.map((entry) => [key(entry.path), entry]));
|
|
78
|
+
const currentPaths = new Map(current.map((entry) => [key(entry.path), entry]));
|
|
79
|
+
const baseLineages = byLineage(base);
|
|
80
|
+
const currentLineages = byLineage(current);
|
|
81
|
+
const consumedBase = new Set();
|
|
82
|
+
const consumedCurrent = new Set();
|
|
83
|
+
const changes = [];
|
|
84
|
+
for (const [lineage, beforeEntries] of baseLineages) {
|
|
85
|
+
const afterEntries = currentLineages.get(lineage);
|
|
86
|
+
if (afterEntries === undefined)
|
|
87
|
+
continue;
|
|
88
|
+
const removed = [...beforeEntries].filter(([pathKey]) => !afterEntries.has(pathKey)).map(([, entry]) => entry);
|
|
89
|
+
const added = [...afterEntries].filter(([pathKey]) => !beforeEntries.has(pathKey)).map(([, entry]) => entry);
|
|
90
|
+
if (removed.length !== 1 || added.length !== 1)
|
|
91
|
+
continue;
|
|
92
|
+
const before = removed[0];
|
|
93
|
+
const after = added[0];
|
|
94
|
+
consumedBase.add(key(before.path));
|
|
95
|
+
consumedCurrent.add(key(after.path));
|
|
96
|
+
changes.push({
|
|
97
|
+
_tag: "Renamed",
|
|
98
|
+
from: before.path,
|
|
99
|
+
to: after.path,
|
|
100
|
+
kind: after.kind,
|
|
101
|
+
differences: differences(before, after, includeTimestamps)
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
for (const before of base) {
|
|
105
|
+
const pathKey = key(before.path);
|
|
106
|
+
if (consumedBase.has(pathKey))
|
|
107
|
+
continue;
|
|
108
|
+
const after = currentPaths.get(pathKey);
|
|
109
|
+
if (after === undefined || consumedCurrent.has(pathKey)) {
|
|
110
|
+
changes.push({ _tag: "Removed", path: before.path, kind: before.kind });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
consumedCurrent.add(pathKey);
|
|
114
|
+
if (before.lineage === undefined || before.lineage !== after.lineage) {
|
|
115
|
+
changes.push({
|
|
116
|
+
_tag: "Replaced",
|
|
117
|
+
path: before.path,
|
|
118
|
+
beforeKind: before.kind,
|
|
119
|
+
afterKind: after.kind,
|
|
120
|
+
differences: differences(before, after, includeTimestamps)
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const changed = differences(before, after, includeTimestamps);
|
|
125
|
+
if (changed.length > 0)
|
|
126
|
+
changes.push({ _tag: "Updated", path: before.path, kind: after.kind, differences: changed });
|
|
127
|
+
}
|
|
128
|
+
for (const after of current) {
|
|
129
|
+
const pathKey = key(after.path);
|
|
130
|
+
if (!consumedCurrent.has(pathKey) && (!basePaths.has(pathKey) || consumedBase.has(pathKey))) {
|
|
131
|
+
changes.push({ _tag: "Added", path: after.path, kind: after.kind });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
changes.sort((left, right) => {
|
|
135
|
+
const leftPath = left._tag === "Renamed" ? left.from : left.path;
|
|
136
|
+
const rightPath = right._tag === "Renamed" ? right.from : right.path;
|
|
137
|
+
const pathOrder = comparePaths(leftPath, rightPath);
|
|
138
|
+
if (pathOrder !== 0)
|
|
139
|
+
return pathOrder;
|
|
140
|
+
const typeOrder = tagOrder[left._tag] - tagOrder[right._tag];
|
|
141
|
+
if (typeOrder !== 0)
|
|
142
|
+
return typeOrder;
|
|
143
|
+
return left._tag === "Renamed" && right._tag === "Renamed" ? comparePaths(left.to, right.to) : 0;
|
|
144
|
+
});
|
|
145
|
+
return Object.freeze(changes.map(copyChange));
|
|
146
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Package-internal synchronization hooks for deterministic overlay tests. @internal */
|
|
2
|
+
import type * as Effect from "effect/Effect";
|
|
3
|
+
import type { Snapshot } from "../Snapshot.js";
|
|
4
|
+
/** @internal */
|
|
5
|
+
export interface ObservationHook {
|
|
6
|
+
readonly betweenSnapshotAndSummary: Effect.Effect<void>;
|
|
7
|
+
}
|
|
8
|
+
/** @internal */
|
|
9
|
+
export declare const getObservationHook: (snapshot: Snapshot) => ObservationHook | undefined;
|
|
10
|
+
/** @internal */
|
|
11
|
+
export declare const setObservationHook: (snapshot: Snapshot, hook: ObservationHook) => () => void;
|
|
12
|
+
//# sourceMappingURL=overlayTesting.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overlayTesting.d.ts","sourceRoot":"","sources":["../../src/internal/overlayTesting.ts"],"names":[],"mappings":"AAAA,wFAAwF;AAExF,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAC5C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAE9C,gBAAgB;AAChB,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,yBAAyB,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CACxD;AAID,gBAAgB;AAChB,eAAO,MAAM,kBAAkB,aAAc,QAAQ,KAAG,eAAe,GAAG,SAA2C,CAAA;AAErH,gBAAgB;AAChB,eAAO,MAAM,kBAAkB,aAAc,QAAQ,QAAQ,eAAe,KAAG,MAAM,IAKpF,CAAA"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Package-internal synchronization hooks for deterministic overlay tests. @internal */
|
|
2
|
+
const observationHooks = new WeakMap();
|
|
3
|
+
/** @internal */
|
|
4
|
+
export const getObservationHook = (snapshot) => observationHooks.get(snapshot);
|
|
5
|
+
/** @internal */
|
|
6
|
+
export const setObservationHook = (snapshot, hook) => {
|
|
7
|
+
observationHooks.set(snapshot, hook);
|
|
8
|
+
return () => {
|
|
9
|
+
if (observationHooks.get(snapshot) === hook)
|
|
10
|
+
observationHooks.delete(snapshot);
|
|
11
|
+
};
|
|
12
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** Exact snapshot delta construction, inspection, serialization, and application. @internal */
|
|
2
|
+
import * as ByteSize from "effect/ByteSize";
|
|
3
|
+
import * as Crypto from "effect/Crypto";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import { ImageError, type Snapshot } from "../Snapshot.js";
|
|
6
|
+
import { type SnapshotDelta, SnapshotDeltaError } from "../SnapshotDelta.js";
|
|
7
|
+
/** @internal */
|
|
8
|
+
export declare const diffSnapshots: (base: Snapshot, target: Snapshot, limits: {
|
|
9
|
+
readonly maxEncodedBytes: ByteSize.ByteSize;
|
|
10
|
+
readonly maxIdentityBytes: ByteSize.ByteSize;
|
|
11
|
+
readonly maxDeltaRecords: number;
|
|
12
|
+
readonly maxDecodedDeltaBytes: ByteSize.ByteSize;
|
|
13
|
+
readonly maxBaseRecords: number;
|
|
14
|
+
readonly maxTargetRecords: number;
|
|
15
|
+
readonly maxEntries: number;
|
|
16
|
+
readonly maxOutputRecords: number;
|
|
17
|
+
readonly maxOutputBytes: ByteSize.ByteSize;
|
|
18
|
+
readonly maxInheritedRecords: number;
|
|
19
|
+
}) => Effect.Effect<SnapshotDelta, ImageError | import("effect/PlatformError").PlatformError, Crypto.Crypto>;
|
|
20
|
+
/** @internal */
|
|
21
|
+
export declare const inspectSnapshotDelta: (base: Snapshot, delta: SnapshotDelta, options: {
|
|
22
|
+
readonly includeTimestamps?: boolean;
|
|
23
|
+
}, limits: {
|
|
24
|
+
readonly maxEncodedBytes: ByteSize.ByteSize;
|
|
25
|
+
readonly maxIdentityBytes: ByteSize.ByteSize;
|
|
26
|
+
readonly maxDeltaRecords: number;
|
|
27
|
+
readonly maxDecodedDeltaBytes: ByteSize.ByteSize;
|
|
28
|
+
readonly maxBaseRecords: number;
|
|
29
|
+
readonly maxTargetRecords: number;
|
|
30
|
+
readonly maxEntries: number;
|
|
31
|
+
readonly maxOutputRecords: number;
|
|
32
|
+
readonly maxOutputBytes: ByteSize.ByteSize;
|
|
33
|
+
readonly maxInheritedRecords: number;
|
|
34
|
+
}) => Effect.Effect<readonly ({
|
|
35
|
+
readonly _tag: "Added";
|
|
36
|
+
readonly path: import("../BytePath.js").BytePath;
|
|
37
|
+
readonly kind: "directory" | "file" | "symlink";
|
|
38
|
+
} | {
|
|
39
|
+
readonly _tag: "Removed";
|
|
40
|
+
readonly path: import("../BytePath.js").BytePath;
|
|
41
|
+
readonly kind: "directory" | "file" | "symlink";
|
|
42
|
+
} | {
|
|
43
|
+
readonly _tag: "Updated";
|
|
44
|
+
readonly path: import("../BytePath.js").BytePath;
|
|
45
|
+
readonly beforeKind: "directory" | "file" | "symlink";
|
|
46
|
+
readonly afterKind: "directory" | "file" | "symlink";
|
|
47
|
+
readonly differences: readonly ("atimeNs" | "birthtimeNs" | "content" | "ctimeNs" | "gid" | "hardLinks" | "kind" | "mode" | "mtimeNs" | "target" | "uid")[];
|
|
48
|
+
})[], ImageError | import("effect/PlatformError").PlatformError | SnapshotDeltaError, Crypto.Crypto>;
|
|
49
|
+
/** @internal */
|
|
50
|
+
export declare const encodeSnapshotDelta: (delta: SnapshotDelta, limits: {
|
|
51
|
+
readonly maxEncodedBytes: ByteSize.ByteSize;
|
|
52
|
+
readonly maxIdentityBytes: ByteSize.ByteSize;
|
|
53
|
+
readonly maxDeltaRecords: number;
|
|
54
|
+
readonly maxDecodedDeltaBytes: ByteSize.ByteSize;
|
|
55
|
+
readonly maxBaseRecords: number;
|
|
56
|
+
readonly maxTargetRecords: number;
|
|
57
|
+
readonly maxEntries: number;
|
|
58
|
+
readonly maxOutputRecords: number;
|
|
59
|
+
readonly maxOutputBytes: ByteSize.ByteSize;
|
|
60
|
+
readonly maxInheritedRecords: number;
|
|
61
|
+
}) => Effect.Effect<Uint8Array<ArrayBuffer>, ImageError, never>;
|
|
62
|
+
/** @internal */
|
|
63
|
+
export declare const decodeSnapshotDelta: (input: Uint8Array<ArrayBufferLike>, limits: {
|
|
64
|
+
readonly maxEncodedBytes: ByteSize.ByteSize;
|
|
65
|
+
readonly maxIdentityBytes: ByteSize.ByteSize;
|
|
66
|
+
readonly maxDeltaRecords: number;
|
|
67
|
+
readonly maxDecodedDeltaBytes: ByteSize.ByteSize;
|
|
68
|
+
readonly maxBaseRecords: number;
|
|
69
|
+
readonly maxTargetRecords: number;
|
|
70
|
+
readonly maxEntries: number;
|
|
71
|
+
readonly maxOutputRecords: number;
|
|
72
|
+
readonly maxOutputBytes: ByteSize.ByteSize;
|
|
73
|
+
readonly maxInheritedRecords: number;
|
|
74
|
+
}) => Effect.Effect<SnapshotDelta, ImageError, never>;
|
|
75
|
+
/** @internal */
|
|
76
|
+
export declare const applySnapshotDelta: (base: Snapshot, delta: SnapshotDelta, limits: {
|
|
77
|
+
readonly maxEncodedBytes: ByteSize.ByteSize;
|
|
78
|
+
readonly maxIdentityBytes: ByteSize.ByteSize;
|
|
79
|
+
readonly maxDeltaRecords: number;
|
|
80
|
+
readonly maxDecodedDeltaBytes: ByteSize.ByteSize;
|
|
81
|
+
readonly maxBaseRecords: number;
|
|
82
|
+
readonly maxTargetRecords: number;
|
|
83
|
+
readonly maxEntries: number;
|
|
84
|
+
readonly maxOutputRecords: number;
|
|
85
|
+
readonly maxOutputBytes: ByteSize.ByteSize;
|
|
86
|
+
readonly maxInheritedRecords: number;
|
|
87
|
+
}) => Effect.Effect<Snapshot, ImageError | import("effect/PlatformError").PlatformError | SnapshotDeltaError, Crypto.Crypto>;
|
|
88
|
+
//# sourceMappingURL=snapshotDelta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshotDelta.d.ts","sourceRoot":"","sources":["../../src/internal/snapshotDelta.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAMvC,OAAO,EAAE,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAC1D,OAAO,EAIL,KAAK,aAAa,EAClB,kBAAkB,EAKnB,MAAM,qBAAqB,CAAA;AA6d5B,gBAAgB;AAChB,eAAO,MAAM,aAAa;;;;;;;;;;;4GAgDzB,CAAA;AAwBD,gBAAgB;AAChB,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;oGAgBhC,CAAA;AACD,gBAAgB;AAChB,eAAO,MAAM,mBAAmB;;;;;;;;;;;+DAS9B,CAAA;AACF,gBAAgB;AAChB,eAAO,MAAM,mBAAmB;;;;;;;;;;;qDAkB9B,CAAA;AACF,gBAAgB;AAChB,eAAO,MAAM,kBAAkB;;;;;;;;;;;4HAI9B,CAAA"}
|