@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
@@ -23,10 +23,11 @@ import * as FixtureInternal from "./internal/fixture.js";
23
23
  import * as Image from "./internal/image.js";
24
24
  import * as Path from "./internal/path.js";
25
25
  import * as SnapshotDeltaInternal from "./internal/snapshotDelta.js";
26
+ import * as SnapshotMergeInternal from "./internal/snapshotMerge.js";
26
27
  import * as VfsModel from "./internal/virtualFileSystem.js";
27
28
  import { open as openLiveVolume } from "./LiveVolume.js";
28
29
  import * as SnapshotDeltaModel from "./SnapshotDelta.js";
29
- export { SnapshotChange, SnapshotChangesOptions, SnapshotDeltaLimits, SnapshotDeltaTypeId, SnapshotDifference, SnapshotNodeKind } from "./SnapshotDelta.js";
30
+ export { MergeConflict, MergeConflictReason, MergeOptions, MergeResolution, MergeResult, MergeSideChange, MergeTake, SnapshotChange, SnapshotChangesOptions, SnapshotDeltaLimits, SnapshotDeltaTypeId, SnapshotDifference, SnapshotNodeKind } from "./SnapshotDelta.js";
30
31
  /**
31
32
  * Schema for portable virtual filesystem error codes.
32
33
  *
@@ -572,6 +573,12 @@ export const Change = WatchModule.Change;
572
573
  * @since 0.6.0
573
574
  */
574
575
  export const WatchOptions = WatchModule.WatchOptions;
576
+ /**
577
+ * Caller watch publication and alias options.
578
+ * @category schemas
579
+ * @since 0.9.0
580
+ */
581
+ export const CallerWatchOptions = WatchModule.CallerWatchOptions;
575
582
  /**
576
583
  * Schema for the filesystem entry kinds reported by overlay summaries.
577
584
  *
@@ -895,7 +902,7 @@ const deltaLimits = (operation, limits) => Effect.fromResult(decodeConfiguration
895
902
  * @category snapshots
896
903
  * @since 0.1.0
897
904
  */
898
- export const diffSnapshots = Effect.fn("VirtualFileSystem.diffSnapshots")(function* (base, target, limits) {
905
+ export const diffSnapshots = Effect.fnUntraced(function* (base, target, limits) {
899
906
  // The delta codec fails under its own name; the public entry point names itself.
900
907
  return yield* SnapshotDeltaInternal.diffSnapshots(base, target, yield* deltaLimits("diffSnapshots", limits)).pipe(Effect.mapError((error) => retargetFailure("diffSnapshots", error)));
901
908
  });
@@ -934,7 +941,7 @@ export const diffSnapshots = Effect.fn("VirtualFileSystem.diffSnapshots")(functi
934
941
  * @category snapshots
935
942
  * @since 0.1.0
936
943
  */
937
- export const inspectSnapshotDelta = Effect.fn("VirtualFileSystem.inspectSnapshotDelta")(function* (base, delta, options, limits) {
944
+ export const inspectSnapshotDelta = Effect.fnUntraced(function* (base, delta, options, limits) {
938
945
  const decoded = yield* Effect.fromResult(decodeConfiguration(SnapshotDeltaModel.SnapshotChangesOptions, options ?? {}, "inspectSnapshotDelta"));
939
946
  return yield* SnapshotDeltaInternal.inspectSnapshotDelta(base, delta, decoded, yield* deltaLimits("inspectSnapshotDelta", limits)).pipe(Effect.mapError((error) => retargetFailure("inspectSnapshotDelta", error)));
940
947
  });
@@ -978,9 +985,66 @@ export const inspectSnapshotDelta = Effect.fn("VirtualFileSystem.inspectSnapshot
978
985
  * @category snapshots
979
986
  * @since 0.1.0
980
987
  */
981
- export const applySnapshotDelta = Effect.fn("VirtualFileSystem.applySnapshotDelta")(function* (base, delta, limits) {
988
+ export const applySnapshotDelta = Effect.fnUntraced(function* (base, delta, limits) {
982
989
  return yield* SnapshotDeltaInternal.applySnapshotDelta(base, delta, yield* deltaLimits("applySnapshotDelta", limits)).pipe(Effect.mapError((error) => retargetFailure("applySnapshotDelta", error)));
983
990
  });
991
+ /**
992
+ * Merges two snapshot deltas that share one base into a single delta, reporting path-level conflicts as data.
993
+ *
994
+ * Changes only one side made are taken as they are. Where both sides changed one path the merge keeps the change
995
+ * when both agree, combines metadata fields that only one side changed, and otherwise reports a conflict and leaves
996
+ * the base node in place. Timestamps never conflict. The merged delta is bound to the same base as its inputs and
997
+ * applies with {@link applySnapshotDelta}. Pass `resolutions` to take one side at paths an earlier merge reported.
998
+ * Requires the platform-neutral `Crypto.Crypto` service for base identity. Provide
999
+ * `NodeCrypto.layer`, `BunCrypto.layer`, or your own via `Crypto.make`.
1000
+ *
1001
+ * @example
1002
+ * ```ts
1003
+ * import * as BunCrypto from "@effect/platform-bun/BunCrypto"
1004
+ * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
1005
+ * import { Effect } from "effect"
1006
+ *
1007
+ * const encoder = new TextEncoder()
1008
+ *
1009
+ * const program = Effect.gen(function*() {
1010
+ * const source = yield* Vfs.fromFixture({
1011
+ * entries: [{ kind: "file", path: "/shared.txt", bytes: encoder.encode("v0") }]
1012
+ * })
1013
+ * const base = yield* source.snapshot
1014
+ *
1015
+ * // Two agents work on separate overlays of one base.
1016
+ * const ours = yield* Vfs.makeOverlay(base)
1017
+ * const theirs = yield* Vfs.makeOverlay(base)
1018
+ * yield* (yield* ours.caller()).writeFile("/ours.txt", encoder.encode("a"), { access: "write", create: "exclusive" })
1019
+ * yield* (yield* theirs.caller()).writeFile("/shared.txt", encoder.encode("v1"), { access: "write", truncate: true })
1020
+ *
1021
+ * const result = yield* Vfs.mergeSnapshotDeltas(
1022
+ * base,
1023
+ * yield* Vfs.diffSnapshots(base, (yield* ours.capture()).snapshot),
1024
+ * yield* Vfs.diffSnapshots(base, (yield* theirs.capture()).snapshot)
1025
+ * )
1026
+ *
1027
+ * // Disjoint changes merge without conflicts; the delta applies to the shared base.
1028
+ * const merged = yield* Vfs.fromSnapshot(yield* Vfs.applySnapshotDelta(base, result.delta))
1029
+ * const reader = yield* merged.caller()
1030
+ *
1031
+ * return [result.conflicts.length, new TextDecoder().decode(yield* reader.readFile("/shared.txt"))]
1032
+ * })
1033
+ *
1034
+ * Effect.runPromise(program.pipe(Effect.provide(BunCrypto.layer))).then(console.log)
1035
+ * // [ 0, 'v1' ]
1036
+ * ```
1037
+ *
1038
+ * @see {@link diffSnapshots} to build each side's delta, {@link applySnapshotDelta} to apply the merged one.
1039
+ * @category snapshots
1040
+ * @since 0.8.0
1041
+ */
1042
+ export const mergeSnapshotDeltas = Effect.fnUntraced(function* (base, ours, theirs, options) {
1043
+ // Limits decode first so a bad limit names its own field; the rest of the options decode as one.
1044
+ const limits = yield* deltaLimits("mergeSnapshotDeltas", options?.limits);
1045
+ const decoded = yield* Effect.fromResult(decodeConfiguration(SnapshotDeltaModel.MergeOptions, options ?? {}, "mergeSnapshotDeltas"));
1046
+ return yield* SnapshotMergeInternal.mergeSnapshotDeltas(base, ours, theirs, decoded.resolutions ?? [], limits).pipe(Effect.mapError((error) => retargetFailure("mergeSnapshotDeltas", error)));
1047
+ });
984
1048
  const deltaSchemaIssue = (cause, input, options) => new SchemaIssue.InvalidValue({ message: `Snapshot delta ${cause.code}${cause.field === undefined ? "" : ` at ${cause.field}`}` }, input, options);
985
1049
  /**
986
1050
  * Creates an Effect Schema codec between owned bytes and opaque snapshot deltas.
package/dist/Watch.d.ts CHANGED
@@ -65,4 +65,22 @@ export declare const WatchOptions: Schema.Struct<{
65
65
  * @since 0.6.0
66
66
  */
67
67
  export type WatchOptions = typeof WatchOptions.Type;
68
+ /**
69
+ * Options for a caller watch. `resolved` follows the selected path alias
70
+ * through rename and ends when that name is unlinked. `all` reports every
71
+ * currently authorized alias of the object.
72
+ *
73
+ * @category schemas
74
+ * @since 0.9.0
75
+ */
76
+ export declare const CallerWatchOptions: Schema.Struct<{
77
+ readonly recursive: Schema.optionalKey<Schema.Boolean>;
78
+ readonly alias: Schema.optionalKey<Schema.Literals<readonly ["all", "resolved"]>>;
79
+ }>;
80
+ /**
81
+ * Options for a caller watch.
82
+ * @category models
83
+ * @since 0.9.0
84
+ */
85
+ export type CallerWatchOptions = typeof CallerWatchOptions.Type;
68
86
  //# sourceMappingURL=Watch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Watch.d.ts","sourceRoot":"","sources":["../src/Watch.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAExC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAE7D;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;EAKjB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,IAAI,CAAA;AAEvC;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,YAAY;;;EAIvB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC,IAAI,CAAA"}
1
+ {"version":3,"file":"Watch.d.ts","sourceRoot":"","sources":["../src/Watch.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAExC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAE7D;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;EAKjB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,IAAI,CAAA;AAEvC;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,YAAY;;;EAIvB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC,IAAI,CAAA;AAEnD;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB;;;EAG7B,CAAA;AAEF;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,OAAO,kBAAkB,CAAC,IAAI,CAAA"}
package/dist/Watch.js CHANGED
@@ -44,3 +44,15 @@ export const WatchOptions = Schema.Struct({
44
44
  scope: Schema.optionalKey(ObjectReferenceSchema),
45
45
  recursive: Schema.optionalKey(Schema.Boolean)
46
46
  });
47
+ /**
48
+ * Options for a caller watch. `resolved` follows the selected path alias
49
+ * through rename and ends when that name is unlinked. `all` reports every
50
+ * currently authorized alias of the object.
51
+ *
52
+ * @category schemas
53
+ * @since 0.9.0
54
+ */
55
+ export const CallerWatchOptions = Schema.Struct({
56
+ recursive: Schema.optionalKey(Schema.Boolean),
57
+ alias: Schema.optionalKey(Schema.Literals(["all", "resolved"]))
58
+ });
package/dist/index.d.ts CHANGED
@@ -79,4 +79,11 @@ export * as Volume from "./Volume.js";
79
79
  * @category re-exports
80
80
  */
81
81
  export * as Watch from "./Watch.js";
82
+ /**
83
+ * Bounded filename search over immutable snapshots.
84
+ *
85
+ * @category re-exports
86
+ * @since 0.8.0
87
+ */
88
+ export * as Search from "./Search.js";
82
89
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;GAKG;AACH,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAA;AAE3D;;;;;GAKG;AACH,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AAEzC;;;;;GAKG;AACH,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAA;AAE7C;;;GAGG;AACH,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AAEzC;;;GAGG;AACH,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AAErC;;;GAGG;AACH,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAA;AAE7C;;;GAGG;AACH,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAEvC;;;GAGG;AACH,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AAEzC;;;GAGG;AACH,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AAEzC;;;GAGG;AACH,OAAO,KAAK,aAAa,MAAM,oBAAoB,CAAA;AAEnD;;;GAGG;AACH,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AAErC;;;GAGG;AACH,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAEvC;;;GAGG;AACH,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AAErC;;;GAGG;AACH,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;GAKG;AACH,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAA;AAE3D;;;;;GAKG;AACH,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AAEzC;;;;;GAKG;AACH,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAA;AAE7C;;;GAGG;AACH,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AAEzC;;;GAGG;AACH,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AAErC;;;GAGG;AACH,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAA;AAE7C;;;GAGG;AACH,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAEvC;;;GAGG;AACH,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AAEzC;;;GAGG;AACH,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AAEzC;;;GAGG;AACH,OAAO,KAAK,aAAa,MAAM,oBAAoB,CAAA;AAEnD;;;GAGG;AACH,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AAErC;;;GAGG;AACH,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAEvC;;;GAGG;AACH,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AAErC;;;GAGG;AACH,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAEnC;;;;;GAKG;AACH,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA"}
package/dist/index.js CHANGED
@@ -79,3 +79,10 @@ export * as Volume from "./Volume.js";
79
79
  * @category re-exports
80
80
  */
81
81
  export * as Watch from "./Watch.js";
82
+ /**
83
+ * Bounded filename search over immutable snapshots.
84
+ *
85
+ * @category re-exports
86
+ * @since 0.8.0
87
+ */
88
+ export * as Search from "./Search.js";
@@ -0,0 +1,32 @@
1
+ import type { FsFailure } from "../VfsError.js";
2
+ import { Change } from "../Watch.js";
3
+ import { type Ino, type Link, type VolumeState } from "./volumeState.js";
4
+ import type { Selection } from "./watchHub.js";
5
+ /** @internal */
6
+ export interface WatchEvent {
7
+ readonly change: Change;
8
+ readonly parent: Ino;
9
+ readonly ino: Ino;
10
+ readonly name?: string;
11
+ }
12
+ /** @internal */
13
+ export interface Rename {
14
+ readonly ino: Ino;
15
+ readonly from: Link;
16
+ readonly to: Link;
17
+ }
18
+ /** @internal */
19
+ export interface Installation {
20
+ readonly before: VolumeState;
21
+ readonly after: VolumeState;
22
+ readonly renames?: ReadonlyArray<Rename>;
23
+ }
24
+ /** Select and rebase names while publication holds both installed namespace states. @internal */
25
+ export declare const make: (options: {
26
+ readonly roots: ReadonlyArray<Ino>;
27
+ readonly additionalRoots?: ReadonlyArray<ReadonlyArray<Ino>>;
28
+ readonly target: Ino;
29
+ readonly recursive: boolean;
30
+ readonly selected?: Link;
31
+ }) => Selection<WatchEvent, Installation, FsFailure>;
32
+ //# sourceMappingURL=callerWatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callerWatch.d.ts","sourceRoot":"","sources":["../../src/internal/callerWatch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAIpC,OAAO,EAAW,KAAK,GAAG,EAAE,KAAK,IAAI,EAAY,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC3F,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAE9C,gBAAgB;AAChB,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAA;IACpB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAA;IACjB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,gBAAgB;AAChB,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAA;IACjB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;IACnB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;CAClB;AAED,gBAAgB;AAChB,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAA;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CACzC;AA6ED,iGAAiG;AACjG,eAAO,MAAM,IAAI,YAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,GAAG,CAAC,CAAA;IAClC,QAAQ,CAAC,eAAe,CAAC,EAAE,aAAa,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAA;IAC5D,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAA;IACpB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAA;CACzB,KAAG,SAAS,CAAC,UAAU,EAAE,YAAY,EAAE,SAAS,CA6IhD,CAAA"}
@@ -0,0 +1,184 @@
1
+ import * as Predicate from "effect/Predicate";
2
+ import { Change } from "../Watch.js";
3
+ import { getBytes } from "./bytePath.js";
4
+ import { fsFailure } from "./errors.js";
5
+ import { nameBytes, ownedPath, SLASH_HEX } from "./path.js";
6
+ import { getNode, ROOT_INO } from "./volumeState.js";
7
+ const directoryPath = (state, ino) => {
8
+ const names = [];
9
+ const visited = new Set();
10
+ let at = ino;
11
+ for (;;) {
12
+ if (visited.has(at))
13
+ return undefined;
14
+ visited.add(at);
15
+ const directory = getNode(state, at);
16
+ if (directory?.kind !== "directory" || directory.metadata.nlink === 0)
17
+ return undefined;
18
+ if (at === ROOT_INO)
19
+ return SLASH_HEX + names.reverse().join(SLASH_HEX);
20
+ names.push(directory.name);
21
+ at = directory.parent;
22
+ }
23
+ };
24
+ const below = (path, root) => root === SLASH_HEX || path === root || path.startsWith(root + SLASH_HEX);
25
+ const rebase = (path, root) => root === SLASH_HEX ? path : path === root ? SLASH_HEX : path.slice(root.length);
26
+ const entryPath = (state, link) => {
27
+ const parent = directoryPath(state, link.parent);
28
+ return parent === undefined ? undefined : parent + (parent === SLASH_HEX ? "" : SLASH_HEX) + link.name;
29
+ };
30
+ const paths = (state, target) => {
31
+ const node = getNode(state, target);
32
+ if (node === undefined || node.metadata.nlink === 0)
33
+ return [];
34
+ if (node.kind === "directory") {
35
+ const path = directoryPath(state, target);
36
+ return path === undefined ? [] : [path];
37
+ }
38
+ return node.links.flatMap((link) => {
39
+ const directory = getNode(state, link.parent);
40
+ const path = directory?.kind === "directory" && directory.entries.get(link.name) === target
41
+ ? entryPath(state, link)
42
+ : undefined;
43
+ return path === undefined ? [] : [path];
44
+ });
45
+ };
46
+ const rootPath = (state, roots) => {
47
+ let previous = SLASH_HEX;
48
+ for (const root of roots) {
49
+ const path = directoryPath(state, root);
50
+ if (path === undefined)
51
+ return fsFailure("ClosedCaller", "watch");
52
+ if (!below(path, previous))
53
+ return fsFailure("AccessDenied", "watch");
54
+ previous = path;
55
+ }
56
+ return previous;
57
+ };
58
+ const eventName = (event) => {
59
+ if (event.name !== undefined)
60
+ return event.name;
61
+ const bytes = getBytes(event.change.path);
62
+ const last = bytes.lastIndexOf(47);
63
+ return Array.from(bytes.subarray(last + 1), (byte) => byte.toString(16).padStart(2, "0")).join("");
64
+ };
65
+ /** Select and rebase names while publication holds both installed namespace states. @internal */
66
+ export const make = (options) => {
67
+ let selected = options.selected;
68
+ let oldSelected = selected;
69
+ let failure;
70
+ let previousRoot = SLASH_HEX;
71
+ let currentRoot = SLASH_HEX;
72
+ let terminal;
73
+ let rescanPath = SLASH_HEX;
74
+ const visible = (state, root, selection) => {
75
+ const aliases = paths(state, options.target);
76
+ for (const chain of options.additionalRoots ?? []) {
77
+ const boundary = rootPath(state, chain);
78
+ if (!Predicate.isString(boundary) || !aliases.some((path) => below(path, boundary)))
79
+ return [];
80
+ }
81
+ return aliases.filter((path) => below(path, root) && (selection === undefined || path === entryPath(state, selection)));
82
+ };
83
+ return {
84
+ boundedTerminal: true,
85
+ prepare: ({ before, after, renames }) => {
86
+ terminal = undefined;
87
+ oldSelected = selected;
88
+ const beforeRoot = rootPath(before, options.roots);
89
+ const afterRoot = rootPath(after, options.roots);
90
+ for (const chain of options.additionalRoots ?? []) {
91
+ const boundary = rootPath(after, chain);
92
+ if (!Predicate.isString(boundary)) {
93
+ failure = boundary;
94
+ return;
95
+ }
96
+ }
97
+ if (!Predicate.isString(afterRoot)) {
98
+ failure = afterRoot;
99
+ return;
100
+ }
101
+ if (!Predicate.isString(beforeRoot))
102
+ return;
103
+ previousRoot = beforeRoot;
104
+ currentRoot = afterRoot;
105
+ for (const rename of renames ?? []) {
106
+ if (rename.ino === options.target && selected !== undefined && selected.parent === rename.from.parent &&
107
+ selected.name === rename.from.name)
108
+ selected = rename.to;
109
+ }
110
+ const oldPaths = visible(before, previousRoot, oldSelected);
111
+ const nextPaths = visible(after, currentRoot, selected);
112
+ rescanPath = rebase(nextPaths[0] ?? oldPaths[0] ?? currentRoot, nextPaths.length > 0 ? currentRoot : previousRoot);
113
+ if (nextPaths.length === 0)
114
+ terminal = oldPaths.map((path) => rebase(path, previousRoot));
115
+ },
116
+ includes: () => failure === undefined,
117
+ project: (event, { before, after }) => {
118
+ if (failure !== undefined)
119
+ return undefined;
120
+ if (terminal === undefined && event.ino !== options.target && Predicate.isTagged(event.change, "Create")) {
121
+ const name = eventName(event);
122
+ const parent = getNode(before, event.parent);
123
+ const replacedPath = entryPath(before, { parent: event.parent, name });
124
+ // Rename publishes the incoming object; its replaced alias needs the old target's removal.
125
+ if (parent?.kind === "directory" && parent.entries.get(name) === options.target &&
126
+ replacedPath !== undefined && visible(before, previousRoot, oldSelected).includes(replacedPath)) {
127
+ return {
128
+ ...event,
129
+ ino: options.target,
130
+ change: Change.cases.Remove.make({ path: ownedPath(nameBytes(rebase(replacedPath, previousRoot))) })
131
+ };
132
+ }
133
+ }
134
+ const removed = Predicate.isTagged(event.change, "Remove");
135
+ const installed = removed ? before : after;
136
+ const root = removed ? previousRoot : currentRoot;
137
+ const selection = removed ? oldSelected : selected;
138
+ let path;
139
+ const node = getNode(installed, event.ino);
140
+ if (event.ino === options.target) {
141
+ // Settlement emits the terminal removals once, behind already queued changes.
142
+ if (removed && terminal !== undefined)
143
+ return undefined;
144
+ path = node?.kind === "directory"
145
+ ? directoryPath(installed, event.ino)
146
+ : entryPath(installed, { parent: event.parent, name: eventName(event) });
147
+ if (selection !== undefined && path !== entryPath(installed, selection))
148
+ return undefined;
149
+ }
150
+ else {
151
+ const scopes = visible(installed, root, selection);
152
+ path = entryPath(installed, { parent: event.parent, name: eventName(event) });
153
+ if (path === undefined || !scopes.some((scope) => {
154
+ if (!below(path, scope))
155
+ return false;
156
+ if (options.recursive)
157
+ return true;
158
+ return directoryPath(installed, event.parent) === scope;
159
+ }))
160
+ return undefined;
161
+ }
162
+ if (path === undefined || !below(path, root))
163
+ return undefined;
164
+ // A confinement root's outside parent is never reported as a rename.
165
+ if (event.ino === options.roots.at(-1) && !Predicate.isTagged(event.change, "Update"))
166
+ return undefined;
167
+ return {
168
+ ...event,
169
+ change: Change.cases[event.change._tag].make({ path: ownedPath(nameBytes(rebase(path, root))) })
170
+ };
171
+ },
172
+ rescan: () => ({
173
+ change: Change.cases.Rescan.make({ path: ownedPath(nameBytes(rescanPath)) }),
174
+ parent: options.target,
175
+ ino: options.target
176
+ }),
177
+ failure: () => failure,
178
+ settle: () => terminal?.map((path) => ({
179
+ change: Change.cases.Remove.make({ path: ownedPath(nameBytes(path)) }),
180
+ parent: options.target,
181
+ ino: options.target
182
+ }))
183
+ };
184
+ };
@@ -0,0 +1,16 @@
1
+ import * as Effect from "effect/Effect";
2
+ import type { FsFailure } from "../VfsError.js";
3
+ import type { OpContext } from "./errors.js";
4
+ import { type Ino, type Node } from "./volumeState.js";
5
+ /** @internal */
6
+ export declare const make: (view: Effect.Effect<(ino: Ino) => Node | undefined>, roots: ReadonlyArray<Ino>) => {
7
+ roots: readonly Ino[];
8
+ root: Ino;
9
+ alive: (op: OpContext, handle?: boolean) => Effect.Effect<undefined, FsFailure, never>;
10
+ contains: (node: Node) => Effect.Effect<boolean, never, never>;
11
+ check: (node: Node, op: OpContext, handle?: boolean | undefined) => Effect.Effect<void, FsFailure, never>;
12
+ path: (ino: Ino) => Effect.Effect<string | undefined, never, never>;
13
+ };
14
+ /** @internal */
15
+ export type Confinement = ReturnType<typeof make>;
16
+ //# sourceMappingURL=confinement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confinement.d.ts","sourceRoot":"","sources":["../../src/internal/confinement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,EAAE,KAAK,GAAG,EAAE,KAAK,IAAI,EAAiC,MAAM,kBAAkB,CAAA;AAErF,gBAAgB;AAChB,eAAO,MAAM,IAAI,SAAU,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,GAAG,SAAS,CAAC,SAAS,aAAa,CAAC,GAAG,CAAC;;;gBAoD9E,SAAS,WAAU,OAAO;qBAErB,IAAI;;;CAuC7B,CAAA;AAED,gBAAgB;AAChB,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAA"}
@@ -0,0 +1,79 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { ROOT_INO, WALK_YIELD_INTERVAL } from "./volumeState.js";
3
+ /** @internal */
4
+ export const make = (view, roots) => {
5
+ const descends = Effect.fnUntraced(function* (get, ino, root) {
6
+ let node = get(ino);
7
+ let steps = 0;
8
+ while (node?.kind === "directory" && node.metadata.nlink > 0) {
9
+ if (node.ino === root)
10
+ return true;
11
+ if (node.ino === ROOT_INO)
12
+ return false;
13
+ const parent = get(node.parent);
14
+ if (parent?.kind !== "directory" || parent.entries.get(node.name) !== node.ino)
15
+ return false;
16
+ node = parent;
17
+ if (++steps % WALK_YIELD_INTERVAL === 0)
18
+ yield* Effect.yieldNow;
19
+ }
20
+ return false;
21
+ });
22
+ const aliveIn = Effect.fnUntraced(function* (get, op, handle) {
23
+ for (let index = 0; index < roots.length; index++) {
24
+ const root = roots[index];
25
+ const node = get(root);
26
+ if (node?.kind !== "directory" || node.metadata.nlink === 0) {
27
+ return yield* op.fail(handle ? "InvalidHandle" : "ClosedCaller");
28
+ }
29
+ if (index > 0 && !(yield* descends(get, root, roots[index - 1])))
30
+ return yield* op.fail("AccessDenied");
31
+ }
32
+ });
33
+ const containsIn = Effect.fnUntraced(function* (get, node) {
34
+ const root = roots.at(-1);
35
+ if (root === undefined)
36
+ return true;
37
+ if (node.kind === "directory")
38
+ return yield* descends(get, node.ino, root);
39
+ for (const link of node.links) {
40
+ const parent = get(link.parent);
41
+ if (parent?.kind === "directory" && parent.entries.get(link.name) === node.ino &&
42
+ (yield* descends(get, parent.ino, root)))
43
+ return true;
44
+ }
45
+ return false;
46
+ });
47
+ const alive = (op, handle = false) => Effect.flatMap(view, (get) => aliveIn(get, op, handle));
48
+ const contains = (node) => Effect.flatMap(view, (get) => containsIn(get, node));
49
+ const check = Effect.fnUntraced(function* (node, op, handle = false) {
50
+ // Share the current lookup within authorization, never its result between operations.
51
+ const get = yield* view;
52
+ yield* aliveIn(get, op, handle);
53
+ if (!(yield* containsIn(get, node)))
54
+ return yield* op.fail("AccessDenied");
55
+ });
56
+ const path = Effect.fnUntraced(function* (ino) {
57
+ const names = [];
58
+ const get = yield* view;
59
+ let node = get(ino);
60
+ let steps = 0;
61
+ if (node?.kind !== "directory" || node.metadata.nlink === 0)
62
+ return undefined;
63
+ const root = roots.at(-1) ?? ROOT_INO;
64
+ while (node.ino !== root) {
65
+ if (node.ino === ROOT_INO)
66
+ return undefined;
67
+ names.push(node.name);
68
+ const parent = get(node.parent);
69
+ if (parent?.kind !== "directory" || parent.metadata.nlink === 0 || parent.entries.get(node.name) !== node.ino) {
70
+ return undefined;
71
+ }
72
+ node = parent;
73
+ if (++steps % WALK_YIELD_INTERVAL === 0)
74
+ yield* Effect.yieldNow;
75
+ }
76
+ return "2f" + names.reverse().join("2f");
77
+ });
78
+ return { roots, root: roots.at(-1) ?? ROOT_INO, alive, contains, check, path };
79
+ };
@@ -0,0 +1,6 @@
1
+ import type { Ino } from "./volumeState.js";
2
+ /** @internal */
3
+ export declare const set: (entries: ReadonlyMap<string, Ino>, name: string, ino: Ino) => ReadonlyMap<string, Ino>;
4
+ /** @internal */
5
+ export declare const remove: (entries: ReadonlyMap<string, Ino>, name: string) => ReadonlyMap<string, Ino>;
6
+ //# sourceMappingURL=directoryEntries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directoryEntries.d.ts","sourceRoot":"","sources":["../../src/internal/directoryEntries.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAA;AA2E3C,gBAAgB;AAChB,eAAO,MAAM,GAAG,YAAa,WAAW,CAAC,MAAM,EAAE,GAAG,CAAC,QAAQ,MAAM,OAAO,GAAG,KAAG,WAAW,CAAC,MAAM,EAAE,GAAG,CAYtG,CAAA;AAED,gBAAgB;AAChB,eAAO,MAAM,MAAM,YAAa,WAAW,CAAC,MAAM,EAAE,GAAG,CAAC,QAAQ,MAAM,KAAG,WAAW,CAAC,MAAM,EAAE,GAAG,CAa/F,CAAA"}
@@ -0,0 +1,79 @@
1
+ import * as HashMap from "effect/HashMap";
2
+ import * as Option from "effect/Option";
3
+ import * as InodeTable from "./inodeTable.js";
4
+ // Name lookup and insertion order share immutable trees. Deleted slots are compacted once they outnumber live entries.
5
+ class DirectoryEntries {
6
+ names;
7
+ slots;
8
+ length;
9
+ [Symbol.toStringTag] = "Map";
10
+ constructor(names, slots, length) {
11
+ this.names = names;
12
+ this.slots = slots;
13
+ this.length = length;
14
+ }
15
+ get size() {
16
+ return HashMap.size(this.names);
17
+ }
18
+ get(name) {
19
+ const slot = HashMap.get(this.names, name);
20
+ return Option.isSome(slot) ? slot.value.ino : undefined;
21
+ }
22
+ has(name) {
23
+ return HashMap.has(this.names, name);
24
+ }
25
+ *entries() {
26
+ for (let index = 0; index < this.length; index++) {
27
+ const entry = InodeTable.get(this.slots, index);
28
+ if (entry !== undefined)
29
+ yield [entry.name, entry.ino];
30
+ }
31
+ }
32
+ *keys() {
33
+ for (const [name] of this)
34
+ yield name;
35
+ }
36
+ *values() {
37
+ for (const [, ino] of this)
38
+ yield ino;
39
+ }
40
+ [Symbol.iterator]() {
41
+ return this.entries();
42
+ }
43
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- ReadonlyMap forwards the opaque callback receiver without inspecting it.
44
+ forEach(callback, thisArg) {
45
+ for (const [name, ino] of this)
46
+ callback.call(thisArg, ino, name, this);
47
+ }
48
+ }
49
+ const fromEntries = (entries) => {
50
+ let slots = InodeTable.empty();
51
+ let length = 0;
52
+ const owner = Symbol();
53
+ const names = HashMap.mutate(HashMap.empty(), (mutable) => {
54
+ for (const [name, ino] of entries) {
55
+ const entry = { name, ino, index: length };
56
+ HashMap.set(mutable, name, entry);
57
+ slots = InodeTable.set(slots, length, entry, owner);
58
+ length++;
59
+ }
60
+ });
61
+ return new DirectoryEntries(names, slots, length);
62
+ };
63
+ /** @internal */
64
+ export const set = (entries, name, ino) => {
65
+ const current = entries instanceof DirectoryEntries ? entries : fromEntries(entries);
66
+ const existing = HashMap.get(current.names, name);
67
+ const index = Option.isSome(existing) ? existing.value.index : current.length;
68
+ const entry = { name, ino, index };
69
+ return new DirectoryEntries(HashMap.set(current.names, name, entry), InodeTable.set(current.slots, index, entry), Option.isSome(existing) ? current.length : current.length + 1);
70
+ };
71
+ /** @internal */
72
+ export const remove = (entries, name) => {
73
+ const current = entries instanceof DirectoryEntries ? entries : fromEntries(entries);
74
+ const existing = HashMap.get(current.names, name);
75
+ if (Option.isNone(existing))
76
+ return current;
77
+ const removed = new DirectoryEntries(HashMap.remove(current.names, name), InodeTable.set(current.slots, existing.value.index, undefined), current.length);
78
+ return removed.length > removed.size * 2 ? fromEntries(removed) : removed;
79
+ };
@@ -5,12 +5,12 @@ import { Fixture as FixtureSchema, FixtureEntry } from "../Fixture.js";
5
5
  import { VolumeIdentity, VolumeOptions as VolumeOptionsSchema } from "../Volume.js";
6
6
  import { decodeConfiguration, imageFailure, OpContext, VfsError } from "./errors.js";
7
7
  import { inputBytes, isAttachedBytes, isDotComponent, preparePath } from "./path.js";
8
- import { makeVolume, VolumeSource } from "./virtualFileSystem.js";
8
+ import { makeVolume, memoryCommitProvider, VolumeSource } from "./virtualFileSystem.js";
9
9
  import { assemble, Ino, ROOT_INO } from "./volumeState.js";
10
10
  const DEFAULT_MODE = { directory: 0o755, file: 0o644, symlink: 0o777 };
11
11
  const EPOCH_NS = 0n;
12
12
  /** @internal */
13
- export const fromFixture = Effect.fn("VirtualFileSystem.fromFixture")(function* (fixture, options) {
13
+ export const fromFixture = Effect.fnUntraced(function* (fixture, options) {
14
14
  const config = yield* Effect.fromResult(decodeConfiguration(VolumeOptionsSchema, options ?? {}, "fromFixture"));
15
15
  const source = yield* Schema.decodeEffect(FixtureSchema, { onExcessProperty: "error" })(fixture).pipe(Effect.mapError((cause) => imageFailure("fromFixture", "InvalidStructure", { field: "fixture", cause })));
16
16
  if (source.entries.some((entry) => FixtureEntry.match(entry, {
@@ -124,8 +124,8 @@ export const fromFixture = Effect.fn("VirtualFileSystem.fromFixture")(function*
124
124
  const restored = VolumeSource.Restored({ value: assemble(specs) });
125
125
  const { identity, ...volumeOptions } = config;
126
126
  if (identity === undefined)
127
- return (yield* makeVolume(restored, volumeOptions)).volume;
128
- const { volume } = yield* makeVolume(restored, {
127
+ return (yield* makeVolume(restored, memoryCommitProvider, volumeOptions)).volume;
128
+ const { volume } = yield* makeVolume(restored, memoryCommitProvider, {
129
129
  ...volumeOptions,
130
130
  identity: VolumeIdentity.make(identity)
131
131
  });