@effect-vfs/core 0.5.0 → 0.7.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 +29 -21
  2. package/dist/BytePath.d.ts +115 -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 +13 -20
  15. package/dist/LiveVolume.d.ts.map +1 -1
  16. package/dist/LiveVolume.js +19 -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 +32 -53
  21. package/dist/Snapshot.d.ts.map +1 -1
  22. package/dist/Snapshot.js +27 -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 +132 -0
  30. package/dist/Testing.d.ts.map +1 -0
  31. package/dist/Testing.js +100 -0
  32. package/dist/VfsError.d.ts +200 -0
  33. package/dist/VfsError.d.ts.map +1 -0
  34. package/dist/VfsError.js +165 -0
  35. package/dist/VirtualFileSystem.d.ts +676 -353
  36. package/dist/VirtualFileSystem.d.ts.map +1 -1
  37. package/dist/VirtualFileSystem.js +487 -121
  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 +4 -15
  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 +605 -503
  98. package/dist/internal/snapshotDeltaModel.d.ts.map +1 -1
  99. package/dist/internal/snapshotDeltaModel.js +0 -1
  100. package/dist/internal/testSeams.d.ts +17 -0
  101. package/dist/internal/testSeams.d.ts.map +1 -0
  102. package/dist/internal/testSeams.js +20 -0
  103. package/dist/internal/tree.d.ts +225 -0
  104. package/dist/internal/tree.d.ts.map +1 -0
  105. package/dist/internal/tree.js +385 -0
  106. package/dist/internal/turnstile.d.ts +8 -0
  107. package/dist/internal/turnstile.d.ts.map +1 -0
  108. package/dist/internal/turnstile.js +43 -0
  109. package/dist/internal/virtualFileSystem.d.ts +44 -488
  110. package/dist/internal/virtualFileSystem.d.ts.map +1 -1
  111. package/dist/internal/virtualFileSystem.js +2111 -2466
  112. package/dist/internal/volumeState.d.ts +111 -0
  113. package/dist/internal/volumeState.d.ts.map +1 -0
  114. package/dist/internal/volumeState.js +202 -0
  115. package/dist/internal/watchHub.d.ts +15 -6
  116. package/dist/internal/watchHub.d.ts.map +1 -1
  117. package/dist/internal/watchHub.js +56 -42
  118. package/package.json +54 -10
  119. package/dist/VirtualFileSystemError.d.ts +0 -54
  120. package/dist/VirtualFileSystemError.d.ts.map +0 -1
  121. package/dist/VirtualFileSystemError.js +0 -49
  122. package/dist/internal/overlayContent.d.ts +0 -15
  123. package/dist/internal/overlayContent.d.ts.map +0 -1
  124. package/dist/internal/overlayContent.js +0 -29
  125. package/dist/internal/stagedState.d.ts +0 -22
  126. package/dist/internal/stagedState.d.ts.map +0 -1
  127. package/dist/internal/stagedState.js +0 -62
  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,8 @@ 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. Volume constructors and snapshot delta functions require Effect’s `Crypto` service. On Node, install the matching
20
+ provider and supply `NodeCrypto.layer` when running these effects:
21
21
 
22
22
  ```sh
23
23
  npm install "@effect/platform-node-shared@$(npm view @effect-vfs/core peerDependencies.effect)"
@@ -111,9 +111,9 @@ const program = Effect.gen(function*() {
111
111
  create: "exclusive"
112
112
  })
113
113
 
114
- const names = yield* fs.readDirectoryBytes("/")
114
+ const listing = yield* fs.readDirectory("/")
115
115
  const roundTrip = yield* Vfs.pathToBytes(opaquePath)
116
- return { names: names.map((name) => Array.from(name)), roundTrip: Array.from(roundTrip) }
116
+ return { names: listing.value.map((entry) => Array.from(entry.name)), roundTrip: Array.from(roundTrip) }
117
117
  })
118
118
 
119
119
  console.log(await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))))
@@ -121,8 +121,8 @@ console.log(await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer
121
121
  ```
122
122
 
123
123
  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.
124
+ Directory listings, symbolic-link targets, and resolved paths come back as bytes; decoding them to text is the caller's
125
+ choice, and a strict decoder reports a name that is not valid UTF-8.
126
126
 
127
127
  ## Build fixtures and restore snapshots
128
128
 
@@ -213,10 +213,13 @@ const program = Effect.scoped(Effect.gen(function*() {
213
213
  const second = yield* file.read(4)
214
214
  const preview = yield* file.pread(3, 0n) // Does not move the cursor.
215
215
 
216
- return [first, second, preview].map((bytes) => new TextDecoder().decode(bytes))
216
+ const decode = (bytes: Uint8Array) => new TextDecoder().decode(bytes)
217
+
218
+ // A positioned read also says whether it reached the end of the file.
219
+ return [decode(first), decode(second), decode(preview.bytes), preview.eof]
217
220
  }))
218
221
 
219
- console.log(await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))) // ["one\n", "two\n", "one"]
222
+ console.log(await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))) // ["one\n", "two\n", "one", false]
220
223
  ```
221
224
 
222
225
  Handles also expose an explicit `close` effect when early release matters. Calling explicit close twice fails, while
@@ -224,9 +227,10 @@ scope cleanup remains safe after an explicit close.
224
227
 
225
228
  ## Handle expected failures as data
226
229
 
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.
230
+ Every failure is a `VfsError` with a stable `code` and the `operation` that failed. A rejected option names its
231
+ `field`, a path-addressed failure names its `path` as bytes, and a classified failure keeps its `cause`. Codec and
232
+ store failures share the class with their own codes. Interruption and defects remain separate from these expected
233
+ failures.
230
234
 
231
235
  ```ts
232
236
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
@@ -237,7 +241,7 @@ const program = Effect.gen(function*() {
237
241
  const fs = yield* (yield* Vfs.make()).caller()
238
242
 
239
243
  return yield* fs.readFile("/optional.json").pipe(
240
- Effect.catchTag("FsError", (error) =>
244
+ Effect.catchTag("VfsError", (error) =>
241
245
  error.code === "NotFound"
242
246
  ? Effect.succeed(new TextEncoder().encode("{}"))
243
247
  : Effect.fail(error))
@@ -248,11 +252,13 @@ const bytes = await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.lay
248
252
  console.log(new TextDecoder().decode(bytes)) // {}
249
253
  ```
250
254
 
251
- ## Provide a caller as an Effect service
255
+ ## Provide a volume and a caller as Effect services
252
256
 
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.
257
+ `Volume` and `Caller` are services with static layers. `Volume.layer()` builds a fresh empty volume,
258
+ `Volume.layerFromSnapshot`, `Volume.layerFromFixture` and `Volume.layerOverlay` build one from existing state, and
259
+ `Volume.layerLive` opens a durable one through the `LiveImageStore` in context. `Caller.layer(options)` makes a root
260
+ caller on the volume in context. A caller supplied through the service keeps its original volume, identity, umask and
261
+ working directory.
256
262
 
257
263
  ```ts
258
264
  import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
@@ -260,21 +266,23 @@ import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
260
266
  import { Effect } from "effect"
261
267
 
262
268
  const loadConfig = Effect.gen(function*() {
263
- const fs = yield* Vfs.CurrentFileSystem
269
+ const fs = yield* Vfs.Caller
264
270
  return yield* fs.readFile("/app/config.json")
265
271
  })
266
272
 
267
273
  const program = Effect.gen(function*() {
268
- const fs = yield* (yield* Vfs.make()).caller()
274
+ const fs = yield* Vfs.Caller
269
275
  yield* fs.mkdir("/app")
270
276
  yield* fs.writeFile("/app/config.json", new TextEncoder().encode("{}"), {
271
277
  access: "write",
272
278
  create: "exclusive"
273
279
  })
274
- return yield* loadConfig.pipe(Effect.provideService(Vfs.CurrentFileSystem, fs))
280
+ return yield* loadConfig
275
281
  })
276
282
 
277
- const bytes = await Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer)))
283
+ const bytes = await Effect.runPromise(
284
+ program.pipe(Effect.provide(Vfs.Caller.layer()), Effect.provide(Vfs.Volume.layer()), Effect.provide(NodeCrypto.layer))
285
+ )
278
286
  console.log(new TextDecoder().decode(bytes)) // {}
279
287
  ```
280
288
 
@@ -295,7 +303,7 @@ candidate.
295
303
  - Absolute paths ignore a supplied directory base. Relative paths can use a live, same-volume directory handle.
296
304
  - Operations coordinate through one permit per volume. At most 65 operations are admitted at once by
297
305
  default, including the active operation; set `maxPendingOperations` to change the waiting budget.
298
- Excess work fails with retryable `FsError` code `VolumeBusy`
306
+ Excess work fails with retryable `VfsError` code `VolumeBusy`
299
307
  before mutation or storage commit. Interruption while waiting makes no change; interruption after a commit
300
308
  does not roll it back.
301
309
  - 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
  *
@@ -28,8 +33,8 @@ export type BytePathId = typeof BytePathId;
28
33
  *
29
34
  * @example
30
35
  * ```ts
36
+ * import * as BunCrypto from "@effect/platform-bun/BunCrypto"
31
37
  * import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
32
- * import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
33
38
  * import { Effect } from "effect"
34
39
  *
35
40
  * // A name that is not valid UTF-8 still round-trips exactly.
@@ -42,11 +47,11 @@ export type BytePathId = typeof BytePathId;
42
47
  * create: "exclusive"
43
48
  * })
44
49
  *
45
- * // `readDirectory` fails `UnrepresentableName` here; the byte variant keeps it.
46
- * return yield* caller.readDirectoryBytes("/")
50
+ * // A listing carries names as bytes, so nothing is lost to text decoding.
51
+ * return (yield* caller.readDirectory("/")).value.map((entry) => entry.name)
47
52
  * })
48
53
  *
49
- * Effect.runPromise(program.pipe(Effect.provide(NodeCrypto.layer))).then(console.log)
54
+ * Effect.runPromise(program.pipe(Effect.provide(BunCrypto.layer))).then(console.log)
50
55
  * // [ Uint8Array(2) [ 255, 254 ] ]
51
56
  * ```
52
57
  *
@@ -63,4 +68,110 @@ export interface BytePath extends Equal.Equal, Pipeable {
63
68
  * @since 0.1.0
64
69
  */
65
70
  export declare const BytePath: Schema.declare<BytePath, BytePath>;
71
+ /**
72
+ * Whether a value is a byte path.
73
+ *
74
+ * @category predicates
75
+ * @since 0.6.0
76
+ */
77
+ export declare const isBytePath: (value: unknown) => value is BytePath;
78
+ /**
79
+ * Orders raw byte strings, such as entry names, byte by byte and then by length.
80
+ *
81
+ * @category ordering
82
+ * @since 0.6.0
83
+ */
84
+ export declare const byteOrder: OrderModule.Order<Uint8Array>;
85
+ /**
86
+ * Orders byte paths by their bytes.
87
+ *
88
+ * @category ordering
89
+ * @since 0.6.0
90
+ */
91
+ export declare const Order: OrderModule.Order<BytePath>;
92
+ /**
93
+ * A byte path holding a copy of `bytes`, which must be non-empty and hold no NUL.
94
+ *
95
+ * @category constructors
96
+ * @since 0.6.0
97
+ */
98
+ export declare const fromBytes: (bytes: Uint8Array) => Effect.Effect<BytePath, FsFailure>;
99
+ /**
100
+ * A byte path holding the UTF-8 encoding of `text`.
101
+ *
102
+ * @category constructors
103
+ * @since 0.6.0
104
+ */
105
+ export declare const fromString: (text: string) => Effect.Effect<BytePath, FsFailure>;
106
+ /**
107
+ * A byte path for any path input: a byte path is returned as is, a string is encoded.
108
+ *
109
+ * @category constructors
110
+ * @since 0.6.0
111
+ */
112
+ export declare const fromInput: (input: PathInput) => Effect.Effect<BytePath, FsFailure>;
113
+ /**
114
+ * A copy of the bytes a byte path holds.
115
+ *
116
+ * @category getters
117
+ * @since 0.6.0
118
+ */
119
+ export declare const toBytes: (path: BytePath) => Effect.Effect<Uint8Array, FsFailure>;
120
+ /**
121
+ * The text of a byte path, failing as `UnrepresentableName` when its bytes are not valid UTF-8.
122
+ *
123
+ * @category getters
124
+ * @since 0.6.0
125
+ */
126
+ export declare const toString: (path: BytePath) => Effect.Effect<string, FsFailure>;
127
+ /**
128
+ * The text of a byte path, or none when its bytes are not valid UTF-8.
129
+ *
130
+ * @category getters
131
+ * @since 0.6.0
132
+ */
133
+ export declare const toStringOption: (path: BytePath) => Option.Option<string>;
134
+ /**
135
+ * The text of raw bytes such as an entry name, or none when they are not valid UTF-8.
136
+ *
137
+ * @category getters
138
+ * @since 0.6.0
139
+ */
140
+ export declare const decodeOption: (bytes: Uint8Array) => Option.Option<string>;
141
+ /**
142
+ * Whether a byte path is exactly `/`.
143
+ *
144
+ * @category predicates
145
+ * @since 0.6.0
146
+ */
147
+ export declare const isRoot: (path: BytePath) => boolean;
148
+ /**
149
+ * Whether a byte path starts with a slash.
150
+ *
151
+ * @category predicates
152
+ * @since 0.6.0
153
+ */
154
+ export declare const isAbsolute: (path: BytePath) => boolean;
155
+ /**
156
+ * The final component of a byte path as bytes; empty for the root and for a path that ends in a slash.
157
+ *
158
+ * @category getters
159
+ * @since 0.6.0
160
+ */
161
+ export declare const basename: (path: BytePath) => Uint8Array;
162
+ /**
163
+ * The byte path holding everything before the final component, or the path itself when it has no parent.
164
+ *
165
+ * @category getters
166
+ * @since 0.6.0
167
+ */
168
+ export declare const parent: (path: BytePath) => BytePath;
169
+ /**
170
+ * A byte path naming `name` under `path`. A string name is encoded as UTF-8 and not checked: a slash in it
171
+ * adds further components, and a NUL makes a path that fails where it is used.
172
+ *
173
+ * @category combinators
174
+ * @since 0.6.0
175
+ */
176
+ export declare const join: (path: BytePath, name: string | Uint8Array) => BytePath;
66
177
  //# 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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;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
+ };