@pistonite/pure 0.0.12

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.
@@ -0,0 +1,307 @@
1
+ import { tryCatch, tryAsync, errstr } from "../result/index.ts";
2
+
3
+ import type { FsFileSystem, FsFileSystemUninit } from "./FsFileSystem.ts";
4
+ import {
5
+ FsErr,
6
+ type FsError,
7
+ type FsResult,
8
+ fsErr,
9
+ fsFail,
10
+ } from "./FsError.ts";
11
+ import { fsGetSupportStatus } from "./FsSupportStatus.ts";
12
+ import { FsImplFileAPI } from "./FsImplFileAPI.ts";
13
+ import { FsImplEntryAPI } from "./FsImplEntryAPI.ts";
14
+ import { FsImplHandleAPI } from "./FsImplHandleAPI.ts";
15
+
16
+ /** Handle for handling top level open errors, and decide if the operation should be retried */
17
+ export type FsOpenRetryHandler = (
18
+ error: FsError,
19
+ attempt: number,
20
+ ) => Promise<FsResult<boolean>>;
21
+
22
+ const MAX_RETRY = 10;
23
+
24
+ /** Open a file system for read-only access with a directory picker dialog */
25
+ export async function fsOpenRead(
26
+ retryHandler?: FsOpenRetryHandler,
27
+ ): Promise<FsResult<FsFileSystem>> {
28
+ const fs = await createWithPicker(false, retryHandler);
29
+ if (fs.err) {
30
+ return fs;
31
+ }
32
+ return await init(fs.val, retryHandler);
33
+ }
34
+
35
+ /** Open a file system for read-write access with a directory picker dialog */
36
+ export async function fsOpenReadWrite(
37
+ retryHandler?: FsOpenRetryHandler,
38
+ ): Promise<FsResult<FsFileSystem>> {
39
+ const fs = await createWithPicker(true, retryHandler);
40
+ if (fs.err) {
41
+ return fs;
42
+ }
43
+ return await init(fs.val, retryHandler);
44
+ }
45
+
46
+ /** Open a file system for read-only access from a DataTransferItem from a drag and drop event */
47
+ export async function fsOpenReadFrom(
48
+ item: DataTransferItem,
49
+ retryHandler?: FsOpenRetryHandler,
50
+ ): Promise<FsResult<FsFileSystem>> {
51
+ const fs = await createFromDataTransferItem(item, false, retryHandler);
52
+ if (fs.err) {
53
+ return fs;
54
+ }
55
+ return await init(fs.val, retryHandler);
56
+ }
57
+
58
+ /** Open a file system for read-write access from a DataTransferItem from a drag and drop event */
59
+ export async function fsOpenReadWriteFrom(
60
+ item: DataTransferItem,
61
+ retryHandler?: FsOpenRetryHandler,
62
+ ): Promise<FsResult<FsFileSystem>> {
63
+ const fs = await createFromDataTransferItem(item, true, retryHandler);
64
+ if (fs.err) {
65
+ return fs;
66
+ }
67
+ return await init(fs.val, retryHandler);
68
+ }
69
+
70
+ async function createWithPicker(
71
+ write: boolean,
72
+ retryHandler: FsOpenRetryHandler | undefined,
73
+ ): Promise<FsResult<FsFileSystemUninit>> {
74
+ for (let attempt = 1; attempt <= MAX_RETRY; attempt++) {
75
+ const { implementation } = fsGetSupportStatus();
76
+ if (implementation === "FileSystemAccess") {
77
+ const handle = await tryAsync(() => showDirectoryPicker(write));
78
+ if (handle.val) {
79
+ return createFromFileSystemHandle(handle.val, write);
80
+ }
81
+ if (retryHandler) {
82
+ const error = isAbortError(handle.err)
83
+ ? fsErr(FsErr.UserAbort, "User cancelled the operation")
84
+ : fsFail(errstr(handle.err));
85
+ const shouldRetry = await retryHandler(error, attempt);
86
+ if (shouldRetry.err) {
87
+ // retry handler failed
88
+ return shouldRetry;
89
+ }
90
+ if (!shouldRetry.val) {
91
+ // don't retry
92
+ return { err: error };
93
+ }
94
+ }
95
+ // Retry with FileSystemAccess API
96
+ continue;
97
+ }
98
+
99
+ // FileEntry API only supported through drag and drop
100
+ // so fallback to File API
101
+ const inputElement = document.createElement("input");
102
+ inputElement.id = "temp";
103
+ inputElement.style.display = "none";
104
+ document.body.appendChild(inputElement);
105
+ inputElement.type = "file";
106
+ inputElement.webkitdirectory = true;
107
+
108
+ const fsUninit = await new Promise<FsResult<FsFileSystemUninit>>(
109
+ (resolve) => {
110
+ inputElement.addEventListener("change", (event) => {
111
+ const files = (event.target as HTMLInputElement).files;
112
+ if (!files) {
113
+ const err = fsFail(
114
+ "Failed to get files from input element",
115
+ );
116
+ return resolve({ err });
117
+ }
118
+ resolve(createFromFileList(files));
119
+ });
120
+ inputElement.click();
121
+ },
122
+ );
123
+ inputElement.remove();
124
+
125
+ if (fsUninit.val) {
126
+ return fsUninit;
127
+ }
128
+
129
+ if (retryHandler) {
130
+ const shouldRetry = await retryHandler(fsUninit.err, attempt);
131
+ if (shouldRetry.err) {
132
+ // retry handler failed
133
+ return shouldRetry;
134
+ }
135
+ if (!shouldRetry.val) {
136
+ // don't retry
137
+ return fsUninit;
138
+ }
139
+ // fall through to retry
140
+ }
141
+ }
142
+ return { err: fsFail("Max retry count reached") };
143
+ }
144
+
145
+ async function createFromDataTransferItem(
146
+ item: DataTransferItem,
147
+ write: boolean,
148
+ retryHandler: FsOpenRetryHandler | undefined,
149
+ ): Promise<FsResult<FsFileSystemUninit>> {
150
+ for (let attempt = 1; attempt <= MAX_RETRY; attempt++) {
151
+ let error: FsError | undefined = undefined;
152
+ const { implementation } = fsGetSupportStatus();
153
+ // Prefer File System Access API since it supports writing
154
+ if (
155
+ "getAsFileSystemHandle" in item &&
156
+ implementation === "FileSystemAccess"
157
+ ) {
158
+ const handle = await tryAsync(() => getAsFileSystemHandle(item));
159
+ if (handle.val) {
160
+ return createFromFileSystemHandle(handle.val, write);
161
+ }
162
+ error = fsFail(
163
+ "Failed to get handle from DataTransferItem: " +
164
+ errstr(handle.err),
165
+ );
166
+ } else if (
167
+ "webkitGetAsEntry" in item &&
168
+ implementation === "FileEntry"
169
+ ) {
170
+ const entry = tryCatch(() => webkitGetAsEntry(item));
171
+ if (entry.val) {
172
+ return createFromFileSystemEntry(entry.val);
173
+ }
174
+ error = fsFail(
175
+ "Failed to get entry from DataTransferItem: " +
176
+ errstr(entry.err),
177
+ );
178
+ }
179
+ if (!error) {
180
+ const err = fsErr(
181
+ FsErr.NotSupported,
182
+ "No supported API found on the DataTransferItem",
183
+ );
184
+ return { err };
185
+ }
186
+ // handle error
187
+ if (retryHandler) {
188
+ const shouldRetry = await retryHandler(error, attempt);
189
+ if (shouldRetry.err) {
190
+ // retry handler failed
191
+ return shouldRetry;
192
+ }
193
+ if (!shouldRetry.val) {
194
+ // don't retry
195
+ return { err: error };
196
+ }
197
+ // fall through to retry
198
+ }
199
+ }
200
+ return { err: fsFail("Max retry count reached") };
201
+ }
202
+
203
+ async function init(
204
+ fs: FsFileSystemUninit,
205
+ retryHandler: FsOpenRetryHandler | undefined,
206
+ ): Promise<FsResult<FsFileSystem>> {
207
+ let attempt = -1;
208
+ while (true) {
209
+ attempt++;
210
+ const inited = await fs.init();
211
+ if (!inited.err) {
212
+ return inited;
213
+ }
214
+ if (!retryHandler) {
215
+ return inited;
216
+ }
217
+ const shouldRetry = await retryHandler(inited.err, attempt);
218
+ if (shouldRetry.err) {
219
+ // retry handler failed
220
+ return shouldRetry;
221
+ }
222
+ if (!shouldRetry.val) {
223
+ // should not retry
224
+ return inited;
225
+ }
226
+ }
227
+ }
228
+
229
+ /** Wrapper for window.showDirectoryPicker */
230
+ function showDirectoryPicker(write: boolean): Promise<FileSystemHandle> {
231
+ // @ts-expect-error showDirectoryPicker is not in the TS lib
232
+ return globalThis.showDirectoryPicker({
233
+ mode: write ? "readwrite" : "read",
234
+ });
235
+ }
236
+
237
+ /**
238
+ * Check if an error is an AbortError from Chromium
239
+ *
240
+ * @param e The error to check
241
+ */
242
+ function isAbortError(e: unknown): boolean {
243
+ return (
244
+ !!e && typeof e === "object" && "name" in e && e.name === "AbortError"
245
+ );
246
+ }
247
+
248
+ /** Wrapper for DataTransferItem.getAsFileSystemHandle */
249
+ async function getAsFileSystemHandle(
250
+ item: DataTransferItem,
251
+ ): Promise<FileSystemHandle> {
252
+ // @ts-expect-error getAsFileSystemHandle is not in the TS lib
253
+ const handle = await item.getAsFileSystemHandle();
254
+ if (!handle) {
255
+ throw new Error("handle is null");
256
+ }
257
+ return handle;
258
+ }
259
+
260
+ /** Wrapper for DataTransferItem.webkitGetAsEntry */
261
+ function webkitGetAsEntry(item: DataTransferItem): FileSystemEntry {
262
+ const entry = item.webkitGetAsEntry();
263
+ if (!entry) {
264
+ throw new Error("entry is null");
265
+ }
266
+ return entry;
267
+ }
268
+
269
+ function createFromFileSystemHandle(
270
+ handle: FileSystemHandle,
271
+ write: boolean,
272
+ ): FsResult<FsFileSystemUninit> {
273
+ if (handle.kind !== "directory") {
274
+ const err = fsErr(FsErr.IsFile, "Expected directory");
275
+ return { err };
276
+ }
277
+
278
+ const fs = new FsImplHandleAPI(
279
+ handle.name,
280
+ handle as FileSystemDirectoryHandle,
281
+ write,
282
+ );
283
+
284
+ return { val: fs };
285
+ }
286
+
287
+ function createFromFileSystemEntry(
288
+ entry: FileSystemEntry,
289
+ ): FsResult<FsFileSystemUninit> {
290
+ if (entry.isFile || !entry.isDirectory) {
291
+ const err = fsErr(FsErr.IsFile, "Expected directory");
292
+ return { err };
293
+ }
294
+ const fs = new FsImplEntryAPI(
295
+ entry.name,
296
+ entry as FileSystemDirectoryEntry,
297
+ );
298
+ return { val: fs };
299
+ }
300
+
301
+ function createFromFileList(files: FileList): FsResult<FsFileSystemUninit> {
302
+ if (!files.length) {
303
+ const err = fsFail("Expected at least one file");
304
+ return { err };
305
+ }
306
+ return { val: new FsImplFileAPI(files) };
307
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Path utilities
3
+ *
4
+ * The library has the following path standard:
5
+ * - All paths are relative (without leading /) to the root
6
+ * of the file system (i.e. the uploaded directory)
7
+ * - Paths are always separated by /
8
+ * - Empty string denotes root
9
+ * - Paths cannot lead outside of root
10
+ *
11
+ * @module
12
+ */
13
+
14
+ import { FsErr, type FsResult, fsErr } from "./FsError.ts";
15
+
16
+ /** Get the root path. Current implementation is empty string. */
17
+ export function fsRoot(): string {
18
+ return "";
19
+ }
20
+
21
+ /** Check if a path is the root directory, also handles badly formatted paths like ".///../" */
22
+ export function fsIsRoot(p: string): boolean {
23
+ if (!p) {
24
+ return true;
25
+ }
26
+ for (let i = 0; i < p.length; i++) {
27
+ if (p[i] !== "/" || p[i] !== "." || p[i] !== "\\") {
28
+ return false;
29
+ }
30
+ }
31
+ return true;
32
+ }
33
+
34
+ /**
35
+ * Get the base name of a path (i.e. remove the last component)
36
+ *
37
+ * If this path is the root directory, return InvalidPath.
38
+ */
39
+ export function fsGetBase(p: string): FsResult<string> {
40
+ if (fsIsRoot(p)) {
41
+ const err = fsErr(FsErr.InvalidPath, "Trying to get the base of root");
42
+ return { err };
43
+ }
44
+ const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
45
+ if (i < 0) {
46
+ return { val: fsRoot() };
47
+ }
48
+ return { val: p.substring(0, i) };
49
+ }
50
+
51
+ /**
52
+ * Get the name of a path (i.e. the last component)
53
+ *
54
+ * Returns the last component of the path.
55
+ * Does not include leading or trailing slashes.
56
+ *
57
+ * If this path is the root directory, return IsRoot.
58
+ */
59
+ export function fsGetName(p: string): FsResult<string> {
60
+ p = stripTrailingSlashes(p);
61
+ if (fsIsRoot(p)) {
62
+ const err = fsErr(FsErr.IsRoot, "Root directory has no name");
63
+ return { err };
64
+ }
65
+ const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
66
+ if (i < 0) {
67
+ return { val: p };
68
+ }
69
+ return { val: p.substring(i + 1) };
70
+ }
71
+
72
+ /**
73
+ * Normalize .. and . in a path
74
+ *
75
+ * Returns InvalidPath if the path tries to escape the root directory.
76
+ */
77
+ export function fsNormalize(p: string): FsResult<string> {
78
+ let s = fsRoot();
79
+ for (const comp of fsComponents(p)) {
80
+ if (comp === "..") {
81
+ const base = fsGetBase(s);
82
+ if (base.err) {
83
+ return base;
84
+ }
85
+ s = base.val;
86
+ continue;
87
+ }
88
+ s = fsJoin(s, comp);
89
+ }
90
+ return { val: s };
91
+ }
92
+
93
+ /** Join two paths */
94
+ export function fsJoin(p1: string, p2: string): string {
95
+ if (fsIsRoot(p1)) {
96
+ return p2;
97
+ }
98
+ return p1 + "/" + p2;
99
+ }
100
+
101
+ /** Iterate through the components of a path. Empty components and . are skipped */
102
+ export function* fsComponents(p: string): Iterable<string> {
103
+ let i = 0;
104
+ while (i < p.length) {
105
+ let nextSlash = p.indexOf("/", i);
106
+ if (nextSlash < 0) {
107
+ nextSlash = p.length;
108
+ }
109
+ let nextBackslash = p.indexOf("\\", i);
110
+ if (nextBackslash < 0) {
111
+ nextBackslash = p.length;
112
+ }
113
+ let j = Math.min(nextSlash, nextBackslash);
114
+ if (j < 0) {
115
+ j = p.length;
116
+ }
117
+ const c = p.substring(i, j);
118
+ if (c && c !== ".") {
119
+ yield c;
120
+ }
121
+ i = j + 1;
122
+ }
123
+ }
124
+
125
+ /** Remove trailing slashes from a path */
126
+ function stripTrailingSlashes(p: string): string {
127
+ let i = p.length - 1;
128
+ for (; i >= 0; i--) {
129
+ if (p[i] !== "/" && p[i] !== "\\") {
130
+ break;
131
+ }
132
+ }
133
+ if (i === p.length - 1) {
134
+ return p;
135
+ }
136
+ return p.substring(0, i + 1);
137
+ }
@@ -0,0 +1,12 @@
1
+ // workaround for CommonJS
2
+ import pkg from "file-saver";
3
+ const { saveAs } = pkg;
4
+
5
+ /** Save (download) a file using Blob */
6
+ export function fsSave(content: string | Uint8Array, filename: string) {
7
+ const blob = new Blob([content], {
8
+ // maybe lying, but should be fine
9
+ type: "text/plain;charset=utf-8",
10
+ });
11
+ saveAs(blob, filename);
12
+ }
@@ -0,0 +1,91 @@
1
+ /** What is supported by the current environment */
2
+ export type FsSupportStatus = {
3
+ /** Returned by window.isSecureContext */
4
+ isSecureContext: boolean;
5
+
6
+ /**
7
+ * The implementation for FsFileSystem used
8
+ *
9
+ * See README.md for more information
10
+ */
11
+ implementation: "File" | "FileSystemAccess" | "FileEntry";
12
+ };
13
+
14
+ /** Get which implementation will be used for the current environment */
15
+ export function fsGetSupportStatus(): FsSupportStatus {
16
+ if (isFileSystemAccessSupported()) {
17
+ return {
18
+ isSecureContext: globalThis.isSecureContext,
19
+ implementation: "FileSystemAccess",
20
+ };
21
+ }
22
+ if (isFileEntrySupported()) {
23
+ return {
24
+ isSecureContext: globalThis.isSecureContext,
25
+ implementation: "FileEntry",
26
+ };
27
+ }
28
+
29
+ return {
30
+ isSecureContext: !!globalThis && globalThis.isSecureContext,
31
+ implementation: "File",
32
+ };
33
+ }
34
+
35
+ function isFileSystemAccessSupported() {
36
+ if (!globalThis) {
37
+ return false;
38
+ }
39
+ if (!globalThis.isSecureContext) {
40
+ // In Chrome, you can still access the APIs but they just crash the page entirely
41
+ return false;
42
+ }
43
+ if (!globalThis.FileSystemDirectoryHandle) {
44
+ return false;
45
+ }
46
+
47
+ if (!globalThis.FileSystemFileHandle) {
48
+ return false;
49
+ }
50
+
51
+ // since TSlib doesn't have these, let's check here
52
+
53
+ // @ts-expect-error FileSystemDirectoryHandle should have a values() method
54
+ if (!globalThis.FileSystemDirectoryHandle.prototype.values) {
55
+ return false;
56
+ }
57
+
58
+ // @ts-expect-error window should have showDirectoryPicker
59
+ if (!globalThis.showDirectoryPicker) {
60
+ return false;
61
+ }
62
+
63
+ return true;
64
+ }
65
+
66
+ function isFileEntrySupported(): boolean {
67
+ if (!globalThis) {
68
+ return false;
69
+ }
70
+
71
+ // Chrome/Edge has this but it's named DirectoryEntry
72
+ // AND, they don't work (I forgot how exactly they don't work)
73
+
74
+ if (
75
+ navigator &&
76
+ navigator.userAgent &&
77
+ navigator.userAgent.includes("Chrome")
78
+ ) {
79
+ return false;
80
+ }
81
+
82
+ if (!globalThis.FileSystemDirectoryEntry) {
83
+ return false;
84
+ }
85
+
86
+ if (!globalThis.FileSystemFileEntry) {
87
+ return false;
88
+ }
89
+
90
+ return true;
91
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * High level browser to file system integration library.
3
+ *
4
+ * This library integrates the `File`, `FileEntry` and `FileSystemAccess` API
5
+ * to provide different levels of integration with file system in web apps.
6
+ *
7
+ * Basically, user can select a directory as a mount point, and browser can access
8
+ * read and sometimes write in the directory.
9
+ *
10
+ * ## Support
11
+ * Use `fsGetSupportStatus()` to inspect which implementation will be used.
12
+ *
13
+ * ```typescript
14
+ * import { fsGetSupportStatus } from "@pistonite/pure/fs";
15
+ *
16
+ * const { implementation, isSecureContext } = fsGetSupportStatus();
17
+ * ```
18
+ *
19
+ * `implementation` can be 3 values:
20
+ * 1. `FileSystemAccess`: This is used for Google Chrome and Edge, and possibly other browsers, under secure context.
21
+ * 2. `FileEntry`: This is used for Firefox when the FS is mounted through a drag-and-drop interface.
22
+ * 3. `File`: This is used for Firefox when the FS is mounted by a directory picker dialog
23
+ *
24
+ * The implementation is also chosen in this order and the first supported one is selected. If you are on Chrome/Edge and `FileSystemAccess` is not used, you can use `isSecureContext` to narrow down the reason.
25
+ *
26
+ * If you are wondering why Safari is not mentioned, it's because Apple made it so I have to buy a Mac to test, which I didn't.
27
+ *
28
+ * After you get an instance of `FsFileSystem`, you can use `capabilities` to inspect
29
+ * what is and is not supported.
30
+ *
31
+ * See `FsCapabilities` for more info. This is the support matrix:
32
+ * |Implementation|`write`?|`live`?|
33
+ * |--------------|--------|-------|
34
+ * |`FileSystemAccess`|Yes*|Yes |
35
+ * |`FileEntry` |No |Yes |
36
+ * |`File` |No |No |
37
+ *
38
+ * `*` = Need to request permission from user.
39
+ *
40
+ *
41
+ * ## Usage
42
+ * First you need to get an instance of `FsFileSystem`. You can:
43
+ * 1. Call `fsOpenRead()` or `fsOpenReadWrite()` to show a directory picker,
44
+ * 2. Call `fsOpenReadFrom` or `fsOpenReadWriteFrom()` and pass in a `DataTransferItem` from a drag-and-drop interface.
45
+ *
46
+ * NOTE: `fsOpenReadWrite` does not guarantee the implementation supports writing. You should check
47
+ * with `capabilities` afterward.
48
+ *
49
+ * This is an example drop zone implementation in TypeScript
50
+ * ```typescript
51
+ * import { fsOpenReadWriteFrom } from "@pistonite/pure/fs";
52
+ *
53
+ * const div = document.createElement("div");
54
+ *
55
+ * div.addEventListener("dragover", (e) => {
56
+ * if (e.dataTransfer) {
57
+ * // setting this will allow dropping
58
+ * e.dataTransfer.dropEffect = "link";
59
+ * }
60
+ * });
61
+ *
62
+ * div.addEventListener("drop", async (e) => {
63
+ * const item = e.dataTransfer?.items[0];
64
+ * if (!item) {
65
+ * console.error("no item");
66
+ * return;
67
+ * }
68
+ *
69
+ * const result = await fsOpenReadWriteFrom(item);
70
+ * if (result.err) {
71
+ * console.error(result.err);
72
+ * return;
73
+ * }
74
+ *
75
+ * const fs = result.val;
76
+ * const { write, live } = fs.capabilities;
77
+ * // check capabilities and use fs
78
+ * // ...
79
+ * });
80
+ * ```
81
+ *
82
+ * ## Retry open
83
+ * You can pass in a retry handler and return true to retry, when opening fails.
84
+ * The handler is async so you can ask user.
85
+ *
86
+ * ```typescript
87
+ * import { FsError, FsResult } from "@pistonite/pure/fs";
88
+ *
89
+ * async function shouldRetry(error: FsError, attempt: number): Promise<FsResult<boolean>> {
90
+ * if (attempt < 10 && error === FsError.PermissionDenied) {
91
+ * alert("you must give permission to use this feature!");
92
+ * return { val: true };
93
+ * }
94
+ * return { val: false };
95
+ * }
96
+ *
97
+ * const result = await fsOpenReadWrite(shouldRetry);
98
+ * ```
99
+ *
100
+ * @module
101
+ */
102
+ export { fsSave } from "./FsSave.ts";
103
+ export {
104
+ fsOpenRead,
105
+ fsOpenReadWrite,
106
+ fsOpenReadFrom,
107
+ fsOpenReadWriteFrom,
108
+ } from "./FsOpen.ts";
109
+ export { fsGetSupportStatus } from "./FsSupportStatus.ts";
110
+ export {
111
+ fsRoot,
112
+ fsIsRoot,
113
+ fsGetBase,
114
+ fsGetName,
115
+ fsNormalize,
116
+ fsJoin,
117
+ fsComponents,
118
+ } from "./FsPath.ts";
119
+ export { FsErr, fsErr, fsFail } from "./FsError.ts";
120
+
121
+ export type { FsOpenRetryHandler } from "./FsOpen.ts";
122
+ export type { FsSupportStatus } from "./FsSupportStatus.ts";
123
+ export type {
124
+ FsFileSystem,
125
+ FsFileSystemUninit,
126
+ FsCapabilities,
127
+ } from "./FsFileSystem.ts";
128
+ export type { FsFile } from "./FsFile.ts";
129
+ export type { FsError, FsResult, FsVoid } from "./FsError.ts";