@effect-vfs/core 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +21 -235
  2. package/dist/Caller.d.ts +2 -0
  3. package/dist/Caller.d.ts.map +1 -1
  4. package/dist/Caller.js +3 -1
  5. package/dist/LiveVolume.d.ts +69 -1
  6. package/dist/LiveVolume.d.ts.map +1 -1
  7. package/dist/LiveVolume.js +89 -12
  8. package/dist/Search.d.ts +730 -0
  9. package/dist/Search.d.ts.map +1 -0
  10. package/dist/Search.js +576 -0
  11. package/dist/SnapshotDelta.d.ts +234 -0
  12. package/dist/SnapshotDelta.d.ts.map +1 -1
  13. package/dist/SnapshotDelta.js +107 -0
  14. package/dist/VfsError.d.ts +3 -1
  15. package/dist/VfsError.d.ts.map +1 -1
  16. package/dist/VfsError.js +3 -1
  17. package/dist/VirtualFileSystem.d.ts +78 -2
  18. package/dist/VirtualFileSystem.d.ts.map +1 -1
  19. package/dist/VirtualFileSystem.js +68 -4
  20. package/dist/Watch.d.ts +18 -0
  21. package/dist/Watch.d.ts.map +1 -1
  22. package/dist/Watch.js +12 -0
  23. package/dist/index.d.ts +7 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +7 -0
  26. package/dist/internal/callerWatch.d.ts +32 -0
  27. package/dist/internal/callerWatch.d.ts.map +1 -0
  28. package/dist/internal/callerWatch.js +184 -0
  29. package/dist/internal/confinement.d.ts +16 -0
  30. package/dist/internal/confinement.d.ts.map +1 -0
  31. package/dist/internal/confinement.js +79 -0
  32. package/dist/internal/directoryEntries.d.ts +6 -0
  33. package/dist/internal/directoryEntries.d.ts.map +1 -0
  34. package/dist/internal/directoryEntries.js +79 -0
  35. package/dist/internal/fixture.js +4 -4
  36. package/dist/internal/glob.d.ts +66 -0
  37. package/dist/internal/glob.d.ts.map +1 -0
  38. package/dist/internal/glob.js +248 -0
  39. package/dist/internal/image.d.ts +7 -1
  40. package/dist/internal/image.d.ts.map +1 -1
  41. package/dist/internal/image.js +9 -7
  42. package/dist/internal/liveImage.d.ts +1 -1
  43. package/dist/internal/liveImage.d.ts.map +1 -1
  44. package/dist/internal/liveImage.js +2 -10
  45. package/dist/internal/path.js +2 -2
  46. package/dist/internal/resolution.d.ts +139 -0
  47. package/dist/internal/resolution.d.ts.map +1 -0
  48. package/dist/internal/resolution.js +332 -0
  49. package/dist/internal/search.d.ts +24 -0
  50. package/dist/internal/search.d.ts.map +1 -0
  51. package/dist/internal/search.js +78 -0
  52. package/dist/internal/searchContent.d.ts +77 -0
  53. package/dist/internal/searchContent.d.ts.map +1 -0
  54. package/dist/internal/searchContent.js +223 -0
  55. package/dist/internal/searchModel.d.ts +83 -0
  56. package/dist/internal/searchModel.d.ts.map +1 -0
  57. package/dist/internal/searchModel.js +23 -0
  58. package/dist/internal/snapshotDelta.d.ts +470 -1
  59. package/dist/internal/snapshotDelta.d.ts.map +1 -1
  60. package/dist/internal/snapshotDelta.js +59 -25
  61. package/dist/internal/snapshotMerge.d.ts +54 -0
  62. package/dist/internal/snapshotMerge.d.ts.map +1 -0
  63. package/dist/internal/snapshotMerge.js +959 -0
  64. package/dist/internal/tokenRegistry.d.ts +83 -0
  65. package/dist/internal/tokenRegistry.d.ts.map +1 -0
  66. package/dist/internal/tokenRegistry.js +146 -0
  67. package/dist/internal/virtualFileSystem.d.ts +32 -17
  68. package/dist/internal/virtualFileSystem.d.ts.map +1 -1
  69. package/dist/internal/virtualFileSystem.js +503 -736
  70. package/dist/internal/volumeLimits.d.ts +53 -0
  71. package/dist/internal/volumeLimits.d.ts.map +1 -0
  72. package/dist/internal/volumeLimits.js +63 -0
  73. package/dist/internal/volumeState.d.ts +2 -0
  74. package/dist/internal/volumeState.d.ts.map +1 -1
  75. package/dist/internal/volumeState.js +9 -0
  76. package/dist/internal/watchHub.d.ts +6 -2
  77. package/dist/internal/watchHub.d.ts.map +1 -1
  78. package/dist/internal/watchHub.js +31 -6
  79. package/package.json +9 -3
@@ -0,0 +1,223 @@
1
+ import * as ByteSize from "effect/ByteSize";
2
+ import * as Effect from "effect/Effect";
3
+ import * as RegExp from "effect/RegExp";
4
+ import * as Schema from "effect/Schema";
5
+ import { BudgetExceeded, ContentQuery, defaultLimits, Pattern, QueryFailure } from "../Search.js";
6
+ import * as Glob from "./glob.js";
7
+ import * as Visitor from "./search.js";
8
+ import { makeMeter } from "./searchModel.js";
9
+ const stepBytes = 4_096;
10
+ const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
11
+ // Valid UTF-8 has one scalar per leading byte; classification proves these widths.
12
+ const scalarWidth = (byte) => byte < 0x80 ? 1 : byte < 0xe0 ? 2 : byte < 0xf0 ? 3 : 4;
13
+ const classify = Effect.fnUntraced(function* (data) {
14
+ let binary = false;
15
+ let checkpoint = stepBytes;
16
+ for (let index = 0; index < data.length;) {
17
+ if (index >= checkpoint) {
18
+ yield* Effect.yieldNow;
19
+ checkpoint = index + stepBytes;
20
+ }
21
+ const first = data[index];
22
+ if (first === 0)
23
+ binary = true;
24
+ if (first < 0x80) {
25
+ index++;
26
+ continue;
27
+ }
28
+ const width = scalarWidth(first);
29
+ if (first < 0xc2 || first > 0xf4 || index + width > data.length)
30
+ return "invalid";
31
+ const second = data[index + 1];
32
+ if (second < 0x80 || second > 0xbf)
33
+ return "invalid";
34
+ if (first === 0xe0 && second < 0xa0 || first === 0xed && second > 0x9f)
35
+ return "invalid";
36
+ if (first === 0xf0 && second < 0x90 || first === 0xf4 && second > 0x8f)
37
+ return "invalid";
38
+ for (let offset = 2; offset < width; offset++) {
39
+ const byte = data[index + offset];
40
+ if (byte < 0x80 || byte > 0xbf)
41
+ return "invalid";
42
+ }
43
+ index += width;
44
+ }
45
+ return binary ? "binary" : "text";
46
+ });
47
+ const offsets = Effect.fnUntraced(function* (data, start, matchStart, matchEnd) {
48
+ let index = start;
49
+ let units = 0;
50
+ let rangeStart = start;
51
+ let checkpoint = start + stepBytes;
52
+ while (units < matchEnd) {
53
+ if (index >= checkpoint) {
54
+ yield* Effect.yieldNow;
55
+ checkpoint = index + stepBytes;
56
+ }
57
+ if (units === matchStart)
58
+ rangeStart = index;
59
+ const width = scalarWidth(data[index]);
60
+ units += width === 4 ? 2 : 1;
61
+ index += width;
62
+ }
63
+ if (matchStart === matchEnd)
64
+ rangeStart = index;
65
+ return { start: rangeStart, end: index };
66
+ });
67
+ /** @internal */
68
+ export const evaluate = Effect.fnUntraced(function* (snapshot, input, mode) {
69
+ const query = yield* Schema.decodeEffect(ContentQuery, { onExcessProperty: "error" })(input).pipe(Effect.mapError((error) => new QueryFailure({ field: "query", message: error.message })));
70
+ const limits = { ...defaultLimits, ...query.limits };
71
+ const source = query.pattern.pattern;
72
+ let sourceBytes = 0n;
73
+ let sourceCheckpoint = stepBytes;
74
+ // Count source bytes before constructing a regex or an escaped literal source.
75
+ for (let index = 0; index < source.length;) {
76
+ const scalar = source.codePointAt(index);
77
+ sourceBytes += scalar < 0x80 ? 1n : scalar < 0x800 ? 2n : scalar < 0x10000 ? 3n : 4n;
78
+ if (sourceBytes > limits.maxPatternBytes) {
79
+ return yield* new QueryFailure({
80
+ field: "pattern",
81
+ message: "Pattern source exceeds maxPatternBytes",
82
+ limit: "maxPatternBytes"
83
+ });
84
+ }
85
+ index += scalar > 0xffff ? 2 : 1;
86
+ if (index >= sourceCheckpoint) {
87
+ yield* Effect.yieldNow;
88
+ sourceCheckpoint = index + stepBytes;
89
+ }
90
+ }
91
+ const pattern = yield* Effect.try({
92
+ try: () => new globalThis.RegExp(Pattern.match(query.pattern, {
93
+ Literal: ({ pattern }) => RegExp.escape(pattern),
94
+ Regex: ({ pattern }) => pattern
95
+ }), query.ignoreCase ? "ui" : "u"),
96
+ catch: (error) => new QueryFailure({ field: "pattern", pattern: source, message: String(error) })
97
+ });
98
+ const selectors = yield* Glob.compile(query, limits);
99
+ const meter = makeMeter(limits);
100
+ const visitor = yield* Visitor.makeVisitor(snapshot, query.root, meter);
101
+ let current;
102
+ let rows = 0;
103
+ let stopped = limits.maxResults === 0
104
+ ? new BudgetExceeded({ limit: "maxResults", path: "" })
105
+ : undefined;
106
+ const select = Effect.fnUntraced(function* (entry) {
107
+ const match = yield* Glob.matches(selectors, entry.parts, entry.kind, meter);
108
+ return { selected: entry.kind === "file" && !match.excluded && match.included, prune: match.excluded };
109
+ });
110
+ const reserve = Effect.fnUntraced(function* (size, path) {
111
+ if (size > limits.maxResultBytes - meter.work.resultBytes) {
112
+ return yield* new BudgetExceeded({ limit: "maxResultBytes", path });
113
+ }
114
+ meter.work.resultBytes = ByteSize.bytes(meter.work.resultBytes + size);
115
+ rows++;
116
+ if (rows === limits.maxResults)
117
+ stopped = new BudgetExceeded({ limit: "maxResults", path });
118
+ });
119
+ const next = Effect.fnUntraced(function* () {
120
+ if (stopped !== undefined)
121
+ return yield* stopped;
122
+ while (true) {
123
+ if (current === undefined) {
124
+ const entry = yield* visitor.next(select);
125
+ if (entry === undefined)
126
+ return undefined;
127
+ if (entry.node.kind !== "file")
128
+ continue;
129
+ const data = entry.node.data;
130
+ const size = BigInt(data.length);
131
+ if (size > limits.maxFileBytes) {
132
+ meter.skips.oversizedFiles++;
133
+ continue;
134
+ }
135
+ if (size > limits.maxScannedBytes - meter.work.scannedBytes) {
136
+ return yield* new BudgetExceeded({ limit: "maxScannedBytes", path: entry.path });
137
+ }
138
+ meter.work.scannedBytes = ByteSize.bytes(meter.work.scannedBytes + size);
139
+ const classification = yield* classify(data);
140
+ if (classification === "invalid") {
141
+ meter.skips.invalidUtf8Files++;
142
+ continue;
143
+ }
144
+ if (classification === "binary") {
145
+ meter.skips.binaryFiles++;
146
+ continue;
147
+ }
148
+ current = { entry, data, offset: 0, lineNumber: 0, count: 0 };
149
+ }
150
+ const cursor = current;
151
+ if (cursor.offset === cursor.data.length) {
152
+ current = undefined;
153
+ if (mode === "countLines" && cursor.count > 0) {
154
+ yield* reserve(BigInt(cursor.entry.pathBytes) + 8n, cursor.entry.path);
155
+ // SAFETY: The countLines branch selects the corresponding generic row type.
156
+ return { path: cursor.entry.path, count: cursor.count };
157
+ }
158
+ continue;
159
+ }
160
+ if (meter.work.lineEvaluations === limits.maxLineEvaluations) {
161
+ return yield* new BudgetExceeded({ limit: "maxLineEvaluations", path: cursor.entry.path });
162
+ }
163
+ const start = cursor.offset;
164
+ let end = start;
165
+ let checkpoint = start + stepBytes;
166
+ while (end < cursor.data.length && cursor.data[end] !== 10) {
167
+ end++;
168
+ if (end >= checkpoint) {
169
+ yield* Effect.yieldNow;
170
+ checkpoint = end + stepBytes;
171
+ }
172
+ }
173
+ const hasLf = end < cursor.data.length;
174
+ cursor.offset = hasLf ? end + 1 : end;
175
+ if (hasLf && end > start && cursor.data[end - 1] === 13)
176
+ end--;
177
+ cursor.lineNumber++;
178
+ const line = decoder.decode(cursor.data.subarray(start, end));
179
+ meter.work.lineEvaluations++;
180
+ const match = mode === "lines" ? pattern.exec(line) : undefined;
181
+ const matched = mode === "lines" ? match !== null : pattern.test(line);
182
+ // Regex calls themselves are synchronous; only owned work is interruptible.
183
+ yield* Effect.yieldNow;
184
+ if (!matched)
185
+ continue;
186
+ cursor.count++;
187
+ if (mode === "countLines")
188
+ continue;
189
+ if (mode === "files") {
190
+ yield* reserve(BigInt(cursor.entry.pathBytes), cursor.entry.path);
191
+ current = undefined;
192
+ // SAFETY: The files branch selects the corresponding generic row type.
193
+ return cursor.entry.path;
194
+ }
195
+ const range = yield* offsets(cursor.data, start, match.index, match.index + match[0].length);
196
+ let excerptEnd = range.start;
197
+ checkpoint = excerptEnd + stepBytes;
198
+ while (excerptEnd < end) {
199
+ const width = scalarWidth(cursor.data[excerptEnd]);
200
+ if (BigInt(excerptEnd + width - range.start) > limits.maxExcerptBytes)
201
+ break;
202
+ excerptEnd += width;
203
+ if (excerptEnd >= checkpoint) {
204
+ yield* Effect.yieldNow;
205
+ checkpoint = excerptEnd + stepBytes;
206
+ }
207
+ }
208
+ yield* reserve(BigInt(cursor.entry.pathBytes + 16 + excerptEnd - range.start), cursor.entry.path);
209
+ // SAFETY: The files and countLines branches returned or continued above; this is a lines row.
210
+ return {
211
+ path: cursor.entry.path,
212
+ lineNumber: cursor.lineNumber,
213
+ range,
214
+ excerpt: {
215
+ text: decoder.decode(cursor.data.subarray(range.start, excerptEnd)),
216
+ start: range.start,
217
+ truncated: range.start > start || excerptEnd < end
218
+ }
219
+ };
220
+ }
221
+ });
222
+ return { query, meter, next };
223
+ });
@@ -0,0 +1,83 @@
1
+ import * as ByteSize from "effect/ByteSize";
2
+ import * as Effect from "effect/Effect";
3
+ import { type Limits, QueryFailure, type Skips, type Work } from "../Search.js";
4
+ /** @internal */
5
+ export { BudgetExceeded, defaultLimits as defaults, GlobQuery, Kind, Limits, QueryFailure } from "../Search.js";
6
+ /** @internal */
7
+ export interface Meter {
8
+ readonly limits: Limits;
9
+ readonly work: {
10
+ -readonly [K in keyof Work]: Work[K];
11
+ };
12
+ readonly skips: {
13
+ -readonly [K in keyof Skips]: Skips[K];
14
+ };
15
+ }
16
+ /** @internal */
17
+ export declare const makeMeter: (limits: Limits) => Meter;
18
+ /** @internal */
19
+ export declare const query: (input: {
20
+ readonly root: string | import("../BytePath.js").BytePath;
21
+ readonly include: readonly [string, ...string[]];
22
+ readonly exclude?: readonly string[];
23
+ readonly kinds?: readonly ("directory" | "file" | "symlink")[];
24
+ readonly limits?: {
25
+ readonly maxEntries?: number;
26
+ readonly maxDepth?: number;
27
+ readonly maxPathBytes?: ByteSize.ByteSize;
28
+ readonly maxResults?: number;
29
+ readonly maxResultBytes?: ByteSize.ByteSize;
30
+ readonly maxPatternBytes?: ByteSize.ByteSize;
31
+ readonly maxPatterns?: number;
32
+ readonly maxPatternListBytes?: ByteSize.ByteSize;
33
+ readonly maxExpansions?: number;
34
+ readonly maxTokens?: number;
35
+ readonly maxMatchingWork?: number;
36
+ readonly maxScannedBytes?: ByteSize.ByteSize;
37
+ readonly maxFileBytes?: ByteSize.ByteSize;
38
+ readonly maxLineEvaluations?: number;
39
+ readonly maxExcerptBytes?: ByteSize.ByteSize;
40
+ };
41
+ }) => Effect.Effect<{
42
+ query: {
43
+ readonly root: string | import("../BytePath.js").BytePath;
44
+ readonly include: readonly [string, ...string[]];
45
+ readonly exclude?: readonly string[];
46
+ readonly kinds?: readonly ("directory" | "file" | "symlink")[];
47
+ readonly limits?: {
48
+ readonly maxEntries?: number;
49
+ readonly maxDepth?: number;
50
+ readonly maxPathBytes?: ByteSize.ByteSize;
51
+ readonly maxResults?: number;
52
+ readonly maxResultBytes?: ByteSize.ByteSize;
53
+ readonly maxPatternBytes?: ByteSize.ByteSize;
54
+ readonly maxPatterns?: number;
55
+ readonly maxPatternListBytes?: ByteSize.ByteSize;
56
+ readonly maxExpansions?: number;
57
+ readonly maxTokens?: number;
58
+ readonly maxMatchingWork?: number;
59
+ readonly maxScannedBytes?: ByteSize.ByteSize;
60
+ readonly maxFileBytes?: ByteSize.ByteSize;
61
+ readonly maxLineEvaluations?: number;
62
+ readonly maxExcerptBytes?: ByteSize.ByteSize;
63
+ };
64
+ };
65
+ limits: {
66
+ maxEntries: number;
67
+ maxDepth: number;
68
+ maxPathBytes: ByteSize.ByteSize;
69
+ maxResults: number;
70
+ maxResultBytes: ByteSize.ByteSize;
71
+ maxPatternBytes: ByteSize.ByteSize;
72
+ maxPatterns: number;
73
+ maxPatternListBytes: ByteSize.ByteSize;
74
+ maxExpansions: number;
75
+ maxTokens: number;
76
+ maxMatchingWork: number;
77
+ maxScannedBytes: ByteSize.ByteSize;
78
+ maxFileBytes: ByteSize.ByteSize;
79
+ maxLineEvaluations: number;
80
+ maxExcerptBytes: ByteSize.ByteSize;
81
+ };
82
+ }, QueryFailure, never>;
83
+ //# sourceMappingURL=searchModel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"searchModel.d.ts","sourceRoot":"","sources":["../../src/internal/searchModel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,EAAwC,KAAK,MAAM,EAAE,YAAY,EAAE,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,MAAM,cAAc,CAAA;AAErH,gBAAgB;AAChB,OAAO,EAAE,cAAc,EAAE,aAAa,IAAI,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAE/G,gBAAgB;AAChB,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE;QAAE,CAAC,UAAU,CAAC,IAAI,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC;KAAE,CAAA;IACvD,QAAQ,CAAC,KAAK,EAAE;QAAE,CAAC,UAAU,CAAC,IAAI,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC;KAAE,CAAA;CAC3D;AAED,gBAAgB;AAChB,eAAO,MAAM,SAAS,WAAY,MAAM,KAAG,KAUzC,CAAA;AAEF,gBAAgB;AAChB,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAMhB,CAAA"}
@@ -0,0 +1,23 @@
1
+ import * as ByteSize from "effect/ByteSize";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Schema from "effect/Schema";
4
+ import { defaultLimits as defaults, GlobQuery, QueryFailure } from "../Search.js";
5
+ /** @internal */
6
+ export { BudgetExceeded, defaultLimits as defaults, GlobQuery, Kind, Limits, QueryFailure } from "../Search.js";
7
+ /** @internal */
8
+ export const makeMeter = (limits) => ({
9
+ limits,
10
+ work: {
11
+ entries: 0,
12
+ matchingWork: 0,
13
+ resultBytes: ByteSize.bytes(0),
14
+ scannedBytes: ByteSize.bytes(0),
15
+ lineEvaluations: 0
16
+ },
17
+ skips: { invalidNames: 0, invalidNameSubtrees: 0, oversizedFiles: 0, invalidUtf8Files: 0, binaryFiles: 0 }
18
+ });
19
+ /** @internal */
20
+ export const query = Effect.fnUntraced(function* (input) {
21
+ const decoded = yield* Schema.decodeEffect(GlobQuery, { onExcessProperty: "error" })(input).pipe(Effect.mapError((error) => new QueryFailure({ field: "query", message: error.message })));
22
+ return { query: decoded, limits: { ...defaults, ...decoded.limits } };
23
+ });