@effect-vfs/core 0.5.0 → 0.6.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 (130) hide show
  1. package/README.md +34 -31
  2. package/dist/BytePath.d.ts +114 -4
  3. package/dist/BytePath.d.ts.map +1 -1
  4. package/dist/BytePath.js +155 -0
  5. package/dist/Caller.d.ts +391 -0
  6. package/dist/Caller.d.ts.map +1 -0
  7. package/dist/Caller.js +199 -0
  8. package/dist/FileHandle.d.ts +50 -0
  9. package/dist/FileHandle.d.ts.map +1 -0
  10. package/dist/FileHandle.js +28 -0
  11. package/dist/Fixture.d.ts +155 -0
  12. package/dist/Fixture.d.ts.map +1 -0
  13. package/dist/Fixture.js +65 -0
  14. package/dist/LiveVolume.d.ts +11 -21
  15. package/dist/LiveVolume.d.ts.map +1 -1
  16. package/dist/LiveVolume.js +17 -14
  17. package/dist/Metadata.d.ts +186 -0
  18. package/dist/Metadata.d.ts.map +1 -0
  19. package/dist/Metadata.js +125 -0
  20. package/dist/Snapshot.d.ts +31 -53
  21. package/dist/Snapshot.d.ts.map +1 -1
  22. package/dist/Snapshot.js +26 -49
  23. package/dist/SnapshotDelta.d.ts +24 -39
  24. package/dist/SnapshotDelta.d.ts.map +1 -1
  25. package/dist/SnapshotDelta.js +14 -35
  26. package/dist/Target.d.ts +225 -0
  27. package/dist/Target.d.ts.map +1 -0
  28. package/dist/Target.js +92 -0
  29. package/dist/Testing.d.ts +128 -0
  30. package/dist/Testing.d.ts.map +1 -0
  31. package/dist/Testing.js +111 -0
  32. package/dist/VfsError.d.ts +199 -0
  33. package/dist/VfsError.d.ts.map +1 -0
  34. package/dist/VfsError.js +164 -0
  35. package/dist/VirtualFileSystem.d.ts +628 -347
  36. package/dist/VirtualFileSystem.d.ts.map +1 -1
  37. package/dist/VirtualFileSystem.js +448 -115
  38. package/dist/Volume.d.ts +211 -0
  39. package/dist/Volume.d.ts.map +1 -0
  40. package/dist/Volume.js +172 -0
  41. package/dist/Watch.d.ts +68 -0
  42. package/dist/Watch.d.ts.map +1 -0
  43. package/dist/Watch.js +46 -0
  44. package/dist/index.d.ts +56 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +56 -1
  47. package/dist/internal/budget.d.ts +90 -0
  48. package/dist/internal/budget.d.ts.map +1 -0
  49. package/dist/internal/budget.js +70 -0
  50. package/dist/internal/bytePath.d.ts +2 -0
  51. package/dist/internal/bytePath.d.ts.map +1 -1
  52. package/dist/internal/bytePath.js +3 -2
  53. package/dist/internal/bytes.d.ts +3 -0
  54. package/dist/internal/bytes.d.ts.map +1 -1
  55. package/dist/internal/bytes.js +4 -2
  56. package/dist/internal/canonicalBase64.d.ts +1 -4
  57. package/dist/internal/canonicalBase64.d.ts.map +1 -1
  58. package/dist/internal/canonicalBase64.js +9 -13
  59. package/dist/internal/errors.d.ts +47 -5
  60. package/dist/internal/errors.d.ts.map +1 -1
  61. package/dist/internal/errors.js +80 -36
  62. package/dist/internal/fixture.d.ts +2 -2
  63. package/dist/internal/fixture.d.ts.map +1 -1
  64. package/dist/internal/fixture.js +57 -74
  65. package/dist/internal/hex128.d.ts +12 -0
  66. package/dist/internal/hex128.d.ts.map +1 -0
  67. package/dist/internal/hex128.js +7 -0
  68. package/dist/internal/hmac.d.ts +5 -0
  69. package/dist/internal/hmac.d.ts.map +1 -0
  70. package/dist/internal/hmac.js +150 -0
  71. package/dist/internal/image.d.ts +14 -117
  72. package/dist/internal/image.d.ts.map +1 -1
  73. package/dist/internal/image.js +137 -167
  74. package/dist/internal/inodeTable.d.ts +20 -0
  75. package/dist/internal/inodeTable.d.ts.map +1 -0
  76. package/dist/internal/inodeTable.js +48 -0
  77. package/dist/internal/lines.d.ts +36 -0
  78. package/dist/internal/lines.d.ts.map +1 -0
  79. package/dist/internal/lines.js +115 -0
  80. package/dist/internal/liveImage.d.ts +19 -278
  81. package/dist/internal/liveImage.d.ts.map +1 -1
  82. package/dist/internal/liveImage.js +75 -166
  83. package/dist/internal/merkle.d.ts +45 -0
  84. package/dist/internal/merkle.d.ts.map +1 -0
  85. package/dist/internal/merkle.js +179 -0
  86. package/dist/internal/metadata.d.ts +2 -13
  87. package/dist/internal/metadata.d.ts.map +1 -1
  88. package/dist/internal/metadata.js +5 -15
  89. package/dist/internal/overlayDiff.d.ts +2 -2
  90. package/dist/internal/overlayDiff.d.ts.map +1 -1
  91. package/dist/internal/overlayDiff.js +0 -3
  92. package/dist/internal/path.d.ts +17 -5
  93. package/dist/internal/path.d.ts.map +1 -1
  94. package/dist/internal/path.js +38 -24
  95. package/dist/internal/snapshotDelta.d.ts +61 -60
  96. package/dist/internal/snapshotDelta.d.ts.map +1 -1
  97. package/dist/internal/snapshotDelta.js +612 -504
  98. package/dist/internal/snapshotDeltaModel.d.ts.map +1 -1
  99. package/dist/internal/snapshotDeltaModel.js +0 -1
  100. package/dist/internal/stagedState.d.ts +11 -15
  101. package/dist/internal/stagedState.d.ts.map +1 -1
  102. package/dist/internal/stagedState.js +15 -59
  103. package/dist/internal/testSeams.d.ts +17 -0
  104. package/dist/internal/testSeams.d.ts.map +1 -0
  105. package/dist/internal/testSeams.js +20 -0
  106. package/dist/internal/tree.d.ts +225 -0
  107. package/dist/internal/tree.d.ts.map +1 -0
  108. package/dist/internal/tree.js +385 -0
  109. package/dist/internal/turnstile.d.ts +8 -0
  110. package/dist/internal/turnstile.d.ts.map +1 -0
  111. package/dist/internal/turnstile.js +43 -0
  112. package/dist/internal/virtualFileSystem.d.ts +39 -489
  113. package/dist/internal/virtualFileSystem.d.ts.map +1 -1
  114. package/dist/internal/virtualFileSystem.js +2112 -2465
  115. package/dist/internal/volumeState.d.ts +110 -0
  116. package/dist/internal/volumeState.d.ts.map +1 -0
  117. package/dist/internal/volumeState.js +204 -0
  118. package/dist/internal/watchHub.d.ts +15 -6
  119. package/dist/internal/watchHub.d.ts.map +1 -1
  120. package/dist/internal/watchHub.js +56 -42
  121. package/package.json +48 -4
  122. package/dist/VirtualFileSystemError.d.ts +0 -54
  123. package/dist/VirtualFileSystemError.d.ts.map +0 -1
  124. package/dist/VirtualFileSystemError.js +0 -49
  125. package/dist/internal/overlayContent.d.ts +0 -15
  126. package/dist/internal/overlayContent.d.ts.map +0 -1
  127. package/dist/internal/overlayContent.js +0 -29
  128. package/dist/internal/testHooks.d.ts +0 -20
  129. package/dist/internal/testHooks.d.ts.map +0 -1
  130. package/dist/internal/testHooks.js +0 -23
package/README.md CHANGED
@@ -16,8 +16,9 @@ for the exact permission, path, timestamp, quota, and atomicity rules.
16
16
  npm install @effect-vfs/core@latest
17
17
  ```
18
18
 
19
- The package declares its exact Effect version as a peer dependency. The examples also need a `Crypto` service. On Node
20
- or Bun, install the matching provider and supply `NodeCrypto.layer`:
19
+ The package declares its exact Effect version as a peer dependency. An in-memory volume needs no platform service.
20
+ The snapshot delta functions hash with SHA-256, so they take a `Crypto` service; on Node or Bun, install the matching
21
+ provider and supply `NodeCrypto.layer` for those:
21
22
 
22
23
  ```sh
23
24
  npm install "@effect/platform-node-shared@$(npm view @effect-vfs/core peerDependencies.effect)"
@@ -42,7 +43,6 @@ lifetime are explicit values that can be composed in one Effect program.
42
43
 
43
44
  ```ts
44
45
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
45
- import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
46
46
  import { ByteSize, Effect } from "effect"
47
47
 
48
48
  const utf8 = new TextEncoder()
@@ -79,7 +79,7 @@ const program = Effect.scoped(Effect.gen(function*() {
79
79
  }
80
80
  }))
81
81
 
82
- const result = await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))
82
+ const result = await Effect.runPromise(program)
83
83
  console.log(result) // { config: { feature: "preview" }, mode: "640" }
84
84
  ```
85
85
 
@@ -97,7 +97,6 @@ contain names which are not valid UTF-8.
97
97
 
98
98
  ```ts
99
99
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
100
- import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
101
100
  import { Effect } from "effect"
102
101
 
103
102
  const program = Effect.gen(function*() {
@@ -111,18 +110,18 @@ const program = Effect.gen(function*() {
111
110
  create: "exclusive"
112
111
  })
113
112
 
114
- const names = yield* fs.readDirectoryBytes("/")
113
+ const listing = yield* fs.readDirectory("/")
115
114
  const roundTrip = yield* Vfs.pathToBytes(opaquePath)
116
- return { names: names.map((name) => Array.from(name)), roundTrip: Array.from(roundTrip) }
115
+ return { names: listing.value.map((entry) => Array.from(entry.name)), roundTrip: Array.from(roundTrip) }
117
116
  })
118
117
 
119
- console.log(await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))))
118
+ console.log(await Effect.runPromise(program))
120
119
  // { names: [[255]], roundTrip: [47, 255] }
121
120
  ```
122
121
 
123
122
  The constructors and byte-returning operations copy their buffers, so later mutation cannot change stored paths.
124
- String-returning operations fail with `FsError` code `UnrepresentableName` when a name is not valid UTF-8. Use the byte
125
- variants of directory enumeration, symbolic-link targets, and resolved paths when exact bytes matter.
123
+ Directory listings, symbolic-link targets, and resolved paths come back as bytes; decoding them to text is the caller's
124
+ choice, and a strict decoder reports a name that is not valid UTF-8.
126
125
 
127
126
  ## Build fixtures and restore snapshots
128
127
 
@@ -132,7 +131,6 @@ resettable sandboxes.
132
131
 
133
132
  ```ts
134
133
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
135
- import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
136
134
  import { Effect } from "effect"
137
135
  import * as ByteSize from "effect/ByteSize"
138
136
 
@@ -170,7 +168,7 @@ const program = Effect.gen(function*() {
170
168
  return new TextDecoder().decode(yield* b.readFile("/project/settings.json"))
171
169
  })
172
170
 
173
- console.log(await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))) // {"theme":"dark"}
171
+ console.log(await Effect.runPromise(program)) // {"theme":"dark"}
174
172
  ```
175
173
 
176
174
  Fixture paths must be absolute and unique, and parent directories must be listed explicitly. Fixtures can also contain
@@ -198,7 +196,6 @@ interruption.
198
196
 
199
197
  ```ts
200
198
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
201
- import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
202
199
  import { Effect } from "effect"
203
200
 
204
201
  const program = Effect.scoped(Effect.gen(function*() {
@@ -213,10 +210,13 @@ const program = Effect.scoped(Effect.gen(function*() {
213
210
  const second = yield* file.read(4)
214
211
  const preview = yield* file.pread(3, 0n) // Does not move the cursor.
215
212
 
216
- return [first, second, preview].map((bytes) => new TextDecoder().decode(bytes))
213
+ const decode = (bytes: Uint8Array) => new TextDecoder().decode(bytes)
214
+
215
+ // A positioned read also says whether it reached the end of the file.
216
+ return [decode(first), decode(second), decode(preview.bytes), preview.eof]
217
217
  }))
218
218
 
219
- console.log(await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))) // ["one\n", "two\n", "one"]
219
+ console.log(await Effect.runPromise(program)) // ["one\n", "two\n", "one", false]
220
220
  ```
221
221
 
222
222
  Handles also expose an explicit `close` effect when early release matters. Calling explicit close twice fails, while
@@ -224,57 +224,60 @@ scope cleanup remains safe after an explicit close.
224
224
 
225
225
  ## Handle expected failures as data
226
226
 
227
- Filesystem failures are typed `FsError` values with a stable `code`, `operation`, and optional `path`. Configuration
228
- and snapshot failures use `ConfigurationError` and `ImageError`. Interruption and defects remain separate from these
229
- expected failures.
227
+ Every failure is a `VfsError` with a stable `code` and the `operation` that failed. A rejected option names its
228
+ `field`, a path-addressed failure names its `path` as bytes, and a classified failure keeps its `cause`. Codec and
229
+ store failures share the class with their own codes. Interruption and defects remain separate from these expected
230
+ failures.
230
231
 
231
232
  ```ts
232
233
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
233
- import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
234
234
  import { Effect } from "effect"
235
235
 
236
236
  const program = Effect.gen(function*() {
237
237
  const fs = yield* (yield* Vfs.make()).caller()
238
238
 
239
239
  return yield* fs.readFile("/optional.json").pipe(
240
- Effect.catchTag("FsError", (error) =>
240
+ Effect.catchTag("VfsError", (error) =>
241
241
  error.code === "NotFound"
242
242
  ? Effect.succeed(new TextEncoder().encode("{}"))
243
243
  : Effect.fail(error))
244
244
  )
245
245
  })
246
246
 
247
- const bytes = await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))
247
+ const bytes = await Effect.runPromise(program)
248
248
  console.log(new TextDecoder().decode(bytes)) // {}
249
249
  ```
250
250
 
251
- ## Provide a caller as an Effect service
251
+ ## Provide a volume and a caller as Effect services
252
252
 
253
- `CurrentFileSystem` is an optional service for application code that should receive an existing caller through its
254
- Effect environment. The service owns no storage; the provided caller keeps its original volume, identity, umask, and
255
- current directory.
253
+ `Volume` and `Caller` are services with static layers. `Volume.layer()` builds a fresh empty volume,
254
+ `Volume.layerFromSnapshot`, `Volume.layerFromFixture` and `Volume.layerOverlay` build one from existing state, and
255
+ `Volume.layerLive` opens a durable one through the `LiveImageStore` in context. `Caller.layer(options)` makes a root
256
+ caller on the volume in context. A caller supplied through the service keeps its original volume, identity, umask and
257
+ working directory.
256
258
 
257
259
  ```ts
258
260
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
259
- import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
260
261
  import { Effect } from "effect"
261
262
 
262
263
  const loadConfig = Effect.gen(function*() {
263
- const fs = yield* Vfs.CurrentFileSystem
264
+ const fs = yield* Vfs.Caller
264
265
  return yield* fs.readFile("/app/config.json")
265
266
  })
266
267
 
267
268
  const program = Effect.gen(function*() {
268
- const fs = yield* (yield* Vfs.make()).caller()
269
+ const fs = yield* Vfs.Caller
269
270
  yield* fs.mkdir("/app")
270
271
  yield* fs.writeFile("/app/config.json", new TextEncoder().encode("{}"), {
271
272
  access: "write",
272
273
  create: "exclusive"
273
274
  })
274
- return yield* loadConfig.pipe(Effect.provideService(Vfs.CurrentFileSystem, fs))
275
+ return yield* loadConfig
275
276
  })
276
277
 
277
- const bytes = await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))
278
+ const bytes = await Effect.runPromise(
279
+ program.pipe(Effect.provide(Vfs.Caller.layer()), Effect.provide(Vfs.Volume.layer()))
280
+ )
278
281
  console.log(new TextDecoder().decode(bytes)) // {}
279
282
  ```
280
283
 
@@ -295,7 +298,7 @@ candidate.
295
298
  - Absolute paths ignore a supplied directory base. Relative paths can use a live, same-volume directory handle.
296
299
  - Operations coordinate through one permit per volume. At most 65 operations are admitted at once by
297
300
  default, including the active operation; set `maxPendingOperations` to change the waiting budget.
298
- Excess work fails with retryable `FsError` code `VolumeBusy`
301
+ Excess work fails with retryable `VfsError` code `VolumeBusy`
299
302
  before mutation or storage commit. Interruption while waiting makes no change; interruption after a commit
300
303
  does not roll it back.
301
304
  - Each watch subscriber retains at most 256 events by default; set `maxWatchEvents` to change this bound
@@ -6,9 +6,14 @@
6
6
  *
7
7
  * @since 0.1.0
8
8
  */
9
+ import * as Effect from "effect/Effect";
9
10
  import type * as Equal from "effect/Equal";
11
+ import * as Option from "effect/Option";
12
+ import * as OrderModule from "effect/Order";
10
13
  import type { Pipeable } from "effect/Pipeable";
11
14
  import * as Schema from "effect/Schema";
15
+ import type { FsFailure } from "./VfsError.js";
16
+ import type { PathInput } from "./VirtualFileSystem.js";
12
17
  /**
13
18
  * Type identifier for opaque byte-preserving filesystem paths.
14
19
  *
@@ -29,7 +34,6 @@ export type BytePathId = typeof BytePathId;
29
34
  * @example
30
35
  * ```ts
31
36
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
32
- * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
33
37
  * import { Effect } from "effect"
34
38
  *
35
39
  * // A name that is not valid UTF-8 still round-trips exactly.
@@ -42,11 +46,11 @@ export type BytePathId = typeof BytePathId;
42
46
  * create: "exclusive"
43
47
  * })
44
48
  *
45
- * // `readDirectory` fails `UnrepresentableName` here; the byte variant keeps it.
46
- * return yield* caller.readDirectoryBytes("/")
49
+ * // A listing carries names as bytes, so nothing is lost to text decoding.
50
+ * return (yield* caller.readDirectory("/")).value.map((entry) => entry.name)
47
51
  * })
48
52
  *
49
- * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
53
+ * Effect.runPromise(program).then(console.log)
50
54
  * // [ Uint8Array(2) [ 255, 254 ] ]
51
55
  * ```
52
56
  *
@@ -63,4 +67,110 @@ export interface BytePath extends Equal.Equal, Pipeable {
63
67
  * @since 0.1.0
64
68
  */
65
69
  export declare const BytePath: Schema.declare<BytePath, BytePath>;
70
+ /**
71
+ * Whether a value is a byte path.
72
+ *
73
+ * @category predicates
74
+ * @since 0.6.0
75
+ */
76
+ export declare const isBytePath: (value: unknown) => value is BytePath;
77
+ /**
78
+ * Orders raw byte strings, such as entry names, byte by byte and then by length.
79
+ *
80
+ * @category ordering
81
+ * @since 0.6.0
82
+ */
83
+ export declare const byteOrder: OrderModule.Order<Uint8Array>;
84
+ /**
85
+ * Orders byte paths by their bytes.
86
+ *
87
+ * @category ordering
88
+ * @since 0.6.0
89
+ */
90
+ export declare const Order: OrderModule.Order<BytePath>;
91
+ /**
92
+ * A byte path holding a copy of `bytes`, which must be non-empty and hold no NUL.
93
+ *
94
+ * @category constructors
95
+ * @since 0.6.0
96
+ */
97
+ export declare const fromBytes: (bytes: Uint8Array) => Effect.Effect<BytePath, FsFailure>;
98
+ /**
99
+ * A byte path holding the UTF-8 encoding of `text`.
100
+ *
101
+ * @category constructors
102
+ * @since 0.6.0
103
+ */
104
+ export declare const fromString: (text: string) => Effect.Effect<BytePath, FsFailure>;
105
+ /**
106
+ * A byte path for any path input: a byte path is returned as is, a string is encoded.
107
+ *
108
+ * @category constructors
109
+ * @since 0.6.0
110
+ */
111
+ export declare const fromInput: (input: PathInput) => Effect.Effect<BytePath, FsFailure>;
112
+ /**
113
+ * A copy of the bytes a byte path holds.
114
+ *
115
+ * @category getters
116
+ * @since 0.6.0
117
+ */
118
+ export declare const toBytes: (path: BytePath) => Effect.Effect<Uint8Array, FsFailure>;
119
+ /**
120
+ * The text of a byte path, failing as `UnrepresentableName` when its bytes are not valid UTF-8.
121
+ *
122
+ * @category getters
123
+ * @since 0.6.0
124
+ */
125
+ export declare const toString: (path: BytePath) => Effect.Effect<string, FsFailure>;
126
+ /**
127
+ * The text of a byte path, or none when its bytes are not valid UTF-8.
128
+ *
129
+ * @category getters
130
+ * @since 0.6.0
131
+ */
132
+ export declare const toStringOption: (path: BytePath) => Option.Option<string>;
133
+ /**
134
+ * The text of raw bytes such as an entry name, or none when they are not valid UTF-8.
135
+ *
136
+ * @category getters
137
+ * @since 0.6.0
138
+ */
139
+ export declare const decodeOption: (bytes: Uint8Array) => Option.Option<string>;
140
+ /**
141
+ * Whether a byte path is exactly `/`.
142
+ *
143
+ * @category predicates
144
+ * @since 0.6.0
145
+ */
146
+ export declare const isRoot: (path: BytePath) => boolean;
147
+ /**
148
+ * Whether a byte path starts with a slash.
149
+ *
150
+ * @category predicates
151
+ * @since 0.6.0
152
+ */
153
+ export declare const isAbsolute: (path: BytePath) => boolean;
154
+ /**
155
+ * The final component of a byte path as bytes; empty for the root and for a path that ends in a slash.
156
+ *
157
+ * @category getters
158
+ * @since 0.6.0
159
+ */
160
+ export declare const basename: (path: BytePath) => Uint8Array;
161
+ /**
162
+ * The byte path holding everything before the final component, or the path itself when it has no parent.
163
+ *
164
+ * @category getters
165
+ * @since 0.6.0
166
+ */
167
+ export declare const parent: (path: BytePath) => BytePath;
168
+ /**
169
+ * A byte path naming `name` under `path`. A string name is encoded as UTF-8 and not checked: a slash in it
170
+ * adds further components, and a NUL makes a path that fails where it is used.
171
+ *
172
+ * @category combinators
173
+ * @since 0.6.0
174
+ */
175
+ export declare const join: (path: BytePath, name: string | Uint8Array) => BytePath;
66
176
  //# sourceMappingURL=BytePath.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"BytePath.d.ts","sourceRoot":"","sources":["../src/BytePath.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,2BAAiD,CAAA;AAE1E;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAA;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,WAAW,QAAS,SAAQ,KAAK,CAAC,KAAK,EAAE,QAAQ;IACrD,QAAQ,CAAC,CAAC,UAAU,CAAC,EAAE,UAAU,CAAA;CAClC;AAED;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,oCAAgD,CAAA"}
1
+ {"version":3,"file":"BytePath.d.ts","sourceRoot":"","sources":["../src/BytePath.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,WAAW,MAAM,cAAc,CAAA;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAG/C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAKvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAEvD;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,2BAAiD,CAAA;AAE1E;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAA;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,WAAW,QAAS,SAAQ,KAAK,CAAC,KAAK,EAAE,QAAQ;IACrD,QAAQ,CAAC,CAAC,UAAU,CAAC,EAAE,UAAU,CAAA;CAClC;AAED;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,oCAAgD,CAAA;AAUrE;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,KAAK,IAAI,QAA8B,CAAA;AAEpF;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,WAAW,CAAC,KAAK,CAAC,UAAU,CAAoB,CAAA;AAExE;;;;;GAKG;AACH,eAAO,MAAM,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAmD,CAAA;AAMjG;;;;;GAKG;AACH,eAAO,MAAM,SAAS,UAAW,UAAU,KAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,SAAS,CACvB,CAAA;AAExD;;;;;GAKG;AACH,eAAO,MAAM,UAAU,SAAU,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,SAAS,CAM1E,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,SAAS,UAAW,SAAS,KAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,SAAS,CACN,CAAA;AAExE;;;;;GAKG;AACH,eAAO,MAAM,OAAO,SAAU,QAAQ,KAAG,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,SAAS,CACzB,CAAA;AAEnD;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,SAAU,QAAQ,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAItE,CAAA;AAEH;;;;;GAKG;AACH,eAAO,MAAM,cAAc,SAAU,QAAQ,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAgC,CAAA;AAEpG;;;;;GAKG;AACH,eAAO,MAAM,YAAY,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAErE,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,MAAM,SAAU,QAAQ,KAAG,OAIvC,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,UAAU,SAAU,QAAQ,KAAG,OAAqC,CAAA;AAEjF;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,SAAU,QAAQ,KAAG,UAIzC,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,MAAM,SAAU,QAAQ,KAAG,QAOvC,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,IAAI,SAAU,QAAQ,QAAQ,MAAM,GAAG,UAAU,KAAG,QAWhE,CAAA"}
package/dist/BytePath.js CHANGED
@@ -1,5 +1,21 @@
1
+ /**
2
+ * Opaque, byte-preserving filesystem paths.
3
+ *
4
+ * A `BytePath` stores the exact bytes of a path component so that names which
5
+ * are not valid UTF-8 survive a round trip through the virtual filesystem.
6
+ *
7
+ * @since 0.1.0
8
+ */
9
+ import * as Effect from "effect/Effect";
10
+ import * as Option from "effect/Option";
11
+ import * as OrderModule from "effect/Order";
12
+ import * as Predicate from "effect/Predicate";
13
+ import * as Result from "effect/Result";
1
14
  import * as Schema from "effect/Schema";
2
15
  import * as Internal from "./internal/bytePath.js";
16
+ import * as Bytes from "./internal/bytes.js";
17
+ import { fsFailure, retargetFailure } from "./internal/errors.js";
18
+ import * as Path from "./internal/path.js";
3
19
  /**
4
20
  * Type identifier for opaque byte-preserving filesystem paths.
5
21
  *
@@ -14,3 +30,142 @@ export const BytePathId = Internal.BytePathId;
14
30
  * @since 0.1.0
15
31
  */
16
32
  export const BytePath = Schema.declare(Internal.isBytePath);
33
+ const encoder = new TextEncoder();
34
+ const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
35
+ const SLASH = 0x2f;
36
+ const bytesOf = (path) => Internal.getBytes(path) ?? new Uint8Array();
37
+ /**
38
+ * Whether a value is a byte path.
39
+ *
40
+ * @category predicates
41
+ * @since 0.6.0
42
+ */
43
+ export const isBytePath = Internal.isBytePath;
44
+ /**
45
+ * Orders raw byte strings, such as entry names, byte by byte and then by length.
46
+ *
47
+ * @category ordering
48
+ * @since 0.6.0
49
+ */
50
+ export const byteOrder = Bytes.bytesOrder;
51
+ /**
52
+ * Orders byte paths by their bytes.
53
+ *
54
+ * @category ordering
55
+ * @since 0.6.0
56
+ */
57
+ export const Order = OrderModule.mapInput(Bytes.bytesOrder, bytesOf);
58
+ // The shared conversions fail under their own names; each toolkit function names itself instead.
59
+ const named = (operation, effect) => Effect.mapError(effect, (error) => retargetFailure(operation, error));
60
+ /**
61
+ * A byte path holding a copy of `bytes`, which must be non-empty and hold no NUL.
62
+ *
63
+ * @category constructors
64
+ * @since 0.6.0
65
+ */
66
+ export const fromBytes = (bytes) => named("BytePath.fromBytes", Path.pathFromBytes(bytes));
67
+ /**
68
+ * A byte path holding the UTF-8 encoding of `text`.
69
+ *
70
+ * @category constructors
71
+ * @since 0.6.0
72
+ */
73
+ export const fromString = (text) => {
74
+ const bytes = Path.inputBytes(text);
75
+ return Result.isFailure(bytes)
76
+ ? Effect.fail(fsFailure(bytes.failure, "BytePath.fromString"))
77
+ : named("BytePath.fromString", Path.pathFromBytes(new Uint8Array(bytes.success)));
78
+ };
79
+ /**
80
+ * A byte path for any path input: a byte path is returned as is, a string is encoded.
81
+ *
82
+ * @category constructors
83
+ * @since 0.6.0
84
+ */
85
+ export const fromInput = (input) => Internal.isBytePath(input) ? Effect.succeed(input) : fromString(input);
86
+ /**
87
+ * A copy of the bytes a byte path holds.
88
+ *
89
+ * @category getters
90
+ * @since 0.6.0
91
+ */
92
+ export const toBytes = (path) => named("BytePath.toBytes", Path.pathToBytes(path));
93
+ /**
94
+ * The text of a byte path, failing as `UnrepresentableName` when its bytes are not valid UTF-8.
95
+ *
96
+ * @category getters
97
+ * @since 0.6.0
98
+ */
99
+ export const toString = (path) => Effect.flatMap(named("BytePath.toString", Path.pathToBytes(path)), (bytes) => Path.strictString(bytes, "BytePath.toString"));
100
+ /**
101
+ * The text of a byte path, or none when its bytes are not valid UTF-8.
102
+ *
103
+ * @category getters
104
+ * @since 0.6.0
105
+ */
106
+ export const toStringOption = (path) => decodeOption(bytesOf(path));
107
+ /**
108
+ * The text of raw bytes such as an entry name, or none when they are not valid UTF-8.
109
+ *
110
+ * @category getters
111
+ * @since 0.6.0
112
+ */
113
+ export const decodeOption = Option.liftThrowable((bytes) => decoder.decode(bytes));
114
+ /**
115
+ * Whether a byte path is exactly `/`.
116
+ *
117
+ * @category predicates
118
+ * @since 0.6.0
119
+ */
120
+ export const isRoot = (path) => {
121
+ const bytes = bytesOf(path);
122
+ return bytes.length === 1 && bytes[0] === SLASH;
123
+ };
124
+ /**
125
+ * Whether a byte path starts with a slash.
126
+ *
127
+ * @category predicates
128
+ * @since 0.6.0
129
+ */
130
+ export const isAbsolute = (path) => bytesOf(path)[0] === SLASH;
131
+ /**
132
+ * The final component of a byte path as bytes; empty for the root and for a path that ends in a slash.
133
+ *
134
+ * @category getters
135
+ * @since 0.6.0
136
+ */
137
+ export const basename = (path) => {
138
+ const bytes = bytesOf(path);
139
+ return bytes.slice(bytes.lastIndexOf(SLASH) + 1);
140
+ };
141
+ /**
142
+ * The byte path holding everything before the final component, or the path itself when it has no parent.
143
+ *
144
+ * @category getters
145
+ * @since 0.6.0
146
+ */
147
+ export const parent = (path) => {
148
+ const bytes = bytesOf(path);
149
+ const slash = bytes.lastIndexOf(SLASH);
150
+ if (slash < 0)
151
+ return path;
152
+ return Internal.make(slash === 0 ? bytes.slice(0, 1) : bytes.slice(0, slash));
153
+ };
154
+ /**
155
+ * A byte path naming `name` under `path`. A string name is encoded as UTF-8 and not checked: a slash in it
156
+ * adds further components, and a NUL makes a path that fails where it is used.
157
+ *
158
+ * @category combinators
159
+ * @since 0.6.0
160
+ */
161
+ export const join = (path, name) => {
162
+ const prefix = bytesOf(path);
163
+ const suffix = Predicate.isString(name) ? encoder.encode(name) : name;
164
+ const separator = prefix.length > 0 && prefix[prefix.length - 1] === SLASH ? 0 : 1;
165
+ const output = new Uint8Array(prefix.length + separator + suffix.length);
166
+ output.set(prefix);
167
+ if (separator === 1)
168
+ output[prefix.length] = SLASH;
169
+ output.set(suffix, prefix.length + separator);
170
+ return Internal.make(output);
171
+ };