@effect-vfs/core 0.4.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 (143) hide show
  1. package/README.md +39 -32
  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 +15 -22
  15. package/dist/LiveVolume.d.ts.map +1 -1
  16. package/dist/LiveVolume.js +19 -15
  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 +450 -117
  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 +54 -0
  60. package/dist/internal/errors.d.ts.map +1 -0
  61. package/dist/internal/errors.js +91 -0
  62. package/dist/internal/{virtualFileSystem/fixture.d.ts → fixture.d.ts} +8 -8
  63. package/dist/internal/fixture.d.ts.map +1 -0
  64. package/dist/internal/fixture.js +133 -0
  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/{virtualFileSystem/overlayDiff.d.ts → overlayDiff.d.ts} +3 -3
  90. package/dist/internal/overlayDiff.d.ts.map +1 -0
  91. package/dist/internal/{virtualFileSystem/overlayDiff.js → overlayDiff.js} +1 -4
  92. package/dist/internal/{virtualFileSystem/path.d.ts → path.d.ts} +19 -7
  93. package/dist/internal/path.d.ts.map +1 -0
  94. package/dist/internal/{virtualFileSystem/path.js → path.js} +39 -25
  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 +18 -0
  101. package/dist/internal/stagedState.d.ts.map +1 -0
  102. package/dist/internal/stagedState.js +18 -0
  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 +41 -491
  113. package/dist/internal/virtualFileSystem.d.ts.map +1 -1
  114. package/dist/internal/virtualFileSystem.js +2118 -2471
  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 +25 -0
  119. package/dist/internal/watchHub.d.ts.map +1 -0
  120. package/dist/internal/watchHub.js +68 -0
  121. package/package.json +53 -9
  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/virtualFileSystem/errors.d.ts +0 -12
  126. package/dist/internal/virtualFileSystem/errors.d.ts.map +0 -1
  127. package/dist/internal/virtualFileSystem/errors.js +0 -47
  128. package/dist/internal/virtualFileSystem/fixture.d.ts.map +0 -1
  129. package/dist/internal/virtualFileSystem/fixture.js +0 -150
  130. package/dist/internal/virtualFileSystem/overlayContent.d.ts +0 -15
  131. package/dist/internal/virtualFileSystem/overlayContent.d.ts.map +0 -1
  132. package/dist/internal/virtualFileSystem/overlayContent.js +0 -29
  133. package/dist/internal/virtualFileSystem/overlayDiff.d.ts.map +0 -1
  134. package/dist/internal/virtualFileSystem/path.d.ts.map +0 -1
  135. package/dist/internal/virtualFileSystem/stagedState.d.ts +0 -22
  136. package/dist/internal/virtualFileSystem/stagedState.d.ts.map +0 -1
  137. package/dist/internal/virtualFileSystem/stagedState.js +0 -62
  138. package/dist/internal/virtualFileSystem/testHooks.d.ts +0 -20
  139. package/dist/internal/virtualFileSystem/testHooks.d.ts.map +0 -1
  140. package/dist/internal/virtualFileSystem/testHooks.js +0 -23
  141. package/dist/internal/virtualFileSystem/watchHub.d.ts +0 -16
  142. package/dist/internal/virtualFileSystem/watchHub.d.ts.map +0 -1
  143. package/dist/internal/virtualFileSystem/watchHub.js +0 -54
package/README.md CHANGED
@@ -13,11 +13,16 @@ for the exact permission, path, timestamp, quota, and atomicity rules.
13
13
  ## Install
14
14
 
15
15
  ```sh
16
- npm install @effect-vfs/core effect@4.0.0-rc.114
16
+ npm install @effect-vfs/core@latest
17
17
  ```
18
18
 
19
- The package currently requires the exact peer version `effect@4.0.0-rc.114`. The examples also need a `Crypto`
20
- service; on Node or Bun, install `@effect/platform-node-shared@4.0.0-rc.114` and provide `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:
22
+
23
+ ```sh
24
+ npm install "@effect/platform-node-shared@$(npm view @effect-vfs/core peerDependencies.effect)"
25
+ ```
21
26
 
22
27
  ## The mental model
23
28
 
@@ -38,7 +43,6 @@ lifetime are explicit values that can be composed in one Effect program.
38
43
 
39
44
  ```ts
40
45
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
41
- import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
42
46
  import { ByteSize, Effect } from "effect"
43
47
 
44
48
  const utf8 = new TextEncoder()
@@ -75,7 +79,7 @@ const program = Effect.scoped(Effect.gen(function*() {
75
79
  }
76
80
  }))
77
81
 
78
- const result = await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))
82
+ const result = await Effect.runPromise(program)
79
83
  console.log(result) // { config: { feature: "preview" }, mode: "640" }
80
84
  ```
81
85
 
@@ -93,7 +97,6 @@ contain names which are not valid UTF-8.
93
97
 
94
98
  ```ts
95
99
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
96
- import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
97
100
  import { Effect } from "effect"
98
101
 
99
102
  const program = Effect.gen(function*() {
@@ -107,18 +110,18 @@ const program = Effect.gen(function*() {
107
110
  create: "exclusive"
108
111
  })
109
112
 
110
- const names = yield* fs.readDirectoryBytes("/")
113
+ const listing = yield* fs.readDirectory("/")
111
114
  const roundTrip = yield* Vfs.pathToBytes(opaquePath)
112
- 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) }
113
116
  })
114
117
 
115
- console.log(await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))))
118
+ console.log(await Effect.runPromise(program))
116
119
  // { names: [[255]], roundTrip: [47, 255] }
117
120
  ```
118
121
 
119
122
  The constructors and byte-returning operations copy their buffers, so later mutation cannot change stored paths.
120
- String-returning operations fail with `FsError` code `UnrepresentableName` when a name is not valid UTF-8. Use the byte
121
- 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.
122
125
 
123
126
  ## Build fixtures and restore snapshots
124
127
 
@@ -128,7 +131,6 @@ resettable sandboxes.
128
131
 
129
132
  ```ts
130
133
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
131
- import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
132
134
  import { Effect } from "effect"
133
135
  import * as ByteSize from "effect/ByteSize"
134
136
 
@@ -166,7 +168,7 @@ const program = Effect.gen(function*() {
166
168
  return new TextDecoder().decode(yield* b.readFile("/project/settings.json"))
167
169
  })
168
170
 
169
- console.log(await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))) // {"theme":"dark"}
171
+ console.log(await Effect.runPromise(program)) // {"theme":"dark"}
170
172
  ```
171
173
 
172
174
  Fixture paths must be absolute and unique, and parent directories must be listed explicitly. Fixtures can also contain
@@ -194,7 +196,6 @@ interruption.
194
196
 
195
197
  ```ts
196
198
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
197
- import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
198
199
  import { Effect } from "effect"
199
200
 
200
201
  const program = Effect.scoped(Effect.gen(function*() {
@@ -209,10 +210,13 @@ const program = Effect.scoped(Effect.gen(function*() {
209
210
  const second = yield* file.read(4)
210
211
  const preview = yield* file.pread(3, 0n) // Does not move the cursor.
211
212
 
212
- 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]
213
217
  }))
214
218
 
215
- 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]
216
220
  ```
217
221
 
218
222
  Handles also expose an explicit `close` effect when early release matters. Calling explicit close twice fails, while
@@ -220,57 +224,60 @@ scope cleanup remains safe after an explicit close.
220
224
 
221
225
  ## Handle expected failures as data
222
226
 
223
- Filesystem failures are typed `FsError` values with a stable `code`, `operation`, and optional `path`. Configuration
224
- and snapshot failures use `ConfigurationError` and `ImageError`. Interruption and defects remain separate from these
225
- 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.
226
231
 
227
232
  ```ts
228
233
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
229
- import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
230
234
  import { Effect } from "effect"
231
235
 
232
236
  const program = Effect.gen(function*() {
233
237
  const fs = yield* (yield* Vfs.make()).caller()
234
238
 
235
239
  return yield* fs.readFile("/optional.json").pipe(
236
- Effect.catchTag("FsError", (error) =>
240
+ Effect.catchTag("VfsError", (error) =>
237
241
  error.code === "NotFound"
238
242
  ? Effect.succeed(new TextEncoder().encode("{}"))
239
243
  : Effect.fail(error))
240
244
  )
241
245
  })
242
246
 
243
- const bytes = await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))
247
+ const bytes = await Effect.runPromise(program)
244
248
  console.log(new TextDecoder().decode(bytes)) // {}
245
249
  ```
246
250
 
247
- ## Provide a caller as an Effect service
251
+ ## Provide a volume and a caller as Effect services
248
252
 
249
- `CurrentFileSystem` is an optional service for application code that should receive an existing caller through its
250
- Effect environment. The service owns no storage; the provided caller keeps its original volume, identity, umask, and
251
- 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.
252
258
 
253
259
  ```ts
254
260
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
255
- import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
256
261
  import { Effect } from "effect"
257
262
 
258
263
  const loadConfig = Effect.gen(function*() {
259
- const fs = yield* Vfs.CurrentFileSystem
264
+ const fs = yield* Vfs.Caller
260
265
  return yield* fs.readFile("/app/config.json")
261
266
  })
262
267
 
263
268
  const program = Effect.gen(function*() {
264
- const fs = yield* (yield* Vfs.make()).caller()
269
+ const fs = yield* Vfs.Caller
265
270
  yield* fs.mkdir("/app")
266
271
  yield* fs.writeFile("/app/config.json", new TextEncoder().encode("{}"), {
267
272
  access: "write",
268
273
  create: "exclusive"
269
274
  })
270
- return yield* loadConfig.pipe(Effect.provideService(Vfs.CurrentFileSystem, fs))
275
+ return yield* loadConfig
271
276
  })
272
277
 
273
- 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
+ )
274
281
  console.log(new TextDecoder().decode(bytes)) // {}
275
282
  ```
276
283
 
@@ -291,7 +298,7 @@ candidate.
291
298
  - Absolute paths ignore a supplied directory base. Relative paths can use a live, same-volume directory handle.
292
299
  - Operations coordinate through one permit per volume. At most 65 operations are admitted at once by
293
300
  default, including the active operation; set `maxPendingOperations` to change the waiting budget.
294
- Excess work fails with retryable `FsError` code `VolumeBusy`
301
+ Excess work fails with retryable `VfsError` code `VolumeBusy`
295
302
  before mutation or storage commit. Interruption while waiting makes no change; interruption after a commit
296
303
  does not roll it back.
297
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
+ };