@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2025 Michael
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # pure
2
+
3
+ Pure TypeScript utility library for browsers.
4
+
5
+ These are meant to be only used by my projects so they
6
+ are not stable by any means. However, feel free to use or reference them.
7
+
8
+ See [GitHub](https://github.com/Pistonite/pure) for more information.
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@pistonite/pure",
3
+ "version": "0.0.12",
4
+ "description": "Pure TypeScript libraries for my projects",
5
+ "homepage": "https://github.com/Pistonite/pure",
6
+ "bugs": {
7
+ "url": "https://github.com/Pistonite/pure/issues"
8
+ },
9
+ "license": "MIT",
10
+ "author": "Pistonight <pistonknight@outlook.com>",
11
+ "files": [
12
+ "src/**/*"
13
+ ],
14
+ "exports": {
15
+ "./fs": "./src/fs/index.ts",
16
+ "./log": "./src/log/index.ts",
17
+ "./result": "./src/result/index.ts",
18
+ "./sync": "./src/sync/index.ts",
19
+ "./pref": "./src/pref/index.ts"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/Pistonite/pure.git",
24
+ "directory": "packages/pure"
25
+ },
26
+ "dependencies": {
27
+ "denque": "2.1.0",
28
+ "file-saver": "2.0.5"
29
+ },
30
+ "devDependencies": {
31
+ "@types/file-saver": "^2.0.7"
32
+ }
33
+ }
@@ -0,0 +1,55 @@
1
+ import type { Result, Void } from "../result/index.ts";
2
+
3
+ /** Result type for file system operations */
4
+ export const FsErr = {
5
+ /** Generic error */
6
+ Fail: 1,
7
+ /** The operation does not apply to the root directory */
8
+ IsRoot: 2,
9
+ /** Invalid encoding */
10
+ InvalidEncoding: 3,
11
+ /** Not supported */
12
+ NotSupported: 4,
13
+ /** The operation does not apply to a file */
14
+ IsFile: 5,
15
+ /** The file was not modified since the last check */
16
+ NotModified: 6,
17
+ /** Permission error */
18
+ PermissionDenied: 7,
19
+ /** User abort */
20
+ UserAbort: 8,
21
+ /** Not found */
22
+ NotFound: 9,
23
+ /** Trying to do stuff to a closed file */
24
+ IsClosed: 10,
25
+ /** If the path is invalid, for example trying to get the parent of root */
26
+ InvalidPath: 11,
27
+ /** Trying to operate on a file that has been closed */
28
+ Closed: 12,
29
+ /** The operation does not apply to a directory */
30
+ IsDirectory: 5,
31
+ } as const;
32
+
33
+ /** Result type for file system operations */
34
+ export type FsErr = (typeof FsErr)[keyof typeof FsErr];
35
+
36
+ /** Fs error type with a code and message */
37
+ export type FsError = {
38
+ readonly code: FsErr;
39
+ readonly message: string;
40
+ };
41
+
42
+ /** Helper to create a FsError */
43
+ export function fsErr(code: FsErr, message: string): FsError {
44
+ return { code, message };
45
+ }
46
+
47
+ /** Helper to create a FsError with the code Fail */
48
+ export function fsFail(message: string): FsError {
49
+ return fsErr(FsErr.Fail, message);
50
+ }
51
+
52
+ /** Helper result type for FsError */
53
+ export type FsResult<T> = Result<T, FsError>;
54
+ /** Helper result type for FsError with no value */
55
+ export type FsVoid = Void<FsError>;
@@ -0,0 +1,67 @@
1
+ import type { FsResult, FsVoid } from "./FsError.ts";
2
+
3
+ /** Interface for operating on a file in the loaded file system */
4
+ export interface FsFile {
5
+ /** Path of the file relative to the root of the file system (the uploaded directory) */
6
+ readonly path: string;
7
+
8
+ /** Returns if the content of the file in memory is newer than the file on disk */
9
+ isDirty(): boolean;
10
+
11
+ /** Get the last modified time. May load it from file system if needed */
12
+ getLastModified(): Promise<FsResult<number>>;
13
+
14
+ /**
15
+ * Get the text content of the file
16
+ *
17
+ * If the file is not loaded, it will load it.
18
+ *
19
+ * If the file is not a text file, it will return InvalidEncoding
20
+ */
21
+ getText(): Promise<FsResult<string>>;
22
+
23
+ /** Get the content of the file */
24
+ getBytes(): Promise<FsResult<Uint8Array>>;
25
+
26
+ /**
27
+ * Set the content in memory. Does not save to disk.
28
+ * Does nothing if file is closed
29
+ */
30
+ setText(content: string): void;
31
+
32
+ /**
33
+ * Set the content in memory. Does not save to disk.
34
+ * Does nothing if file is closed
35
+ */
36
+ setBytes(content: Uint8Array): void;
37
+
38
+ /**
39
+ * Load the file's content if it's not newer than fs
40
+ *
41
+ * Returns Ok if the file is newer than fs
42
+ */
43
+ loadIfNotDirty(): Promise<FsVoid>;
44
+
45
+ /**
46
+ * Load the file's content from FS.
47
+ *
48
+ * Overwrites any unsaved changes in memory only if the file was modified
49
+ * at a later time than the last in memory modification.
50
+ *
51
+ * If it fails, the file's content in memory will not be changed
52
+ */
53
+ load(): Promise<FsVoid>;
54
+
55
+ /**
56
+ * Save the file's content to FS if it is dirty.
57
+ *
58
+ * If not dirty, returns Ok
59
+ */
60
+ writeIfNewer(): Promise<FsVoid>;
61
+
62
+ /**
63
+ * Close the file. In memory content will be lost.
64
+ * Further operations on the file will fail
65
+ */
66
+ close(): void;
67
+ }
@@ -0,0 +1,225 @@
1
+ import { tryAsync, errstr } from "../result/index.ts";
2
+
3
+ import type { FsFile } from "./FsFile.ts";
4
+ import type { FsFileSystemInternal } from "./FsFileSystemInternal.ts";
5
+ import { FsErr, type FsResult, type FsVoid, fsErr, fsFail } from "./FsError.ts";
6
+
7
+ /** Allocate a new file object */
8
+ export function fsFile(fs: FsFileSystemInternal, path: string): FsFile {
9
+ return new FsFileImpl(fs, path);
10
+ }
11
+
12
+ function errclosed() {
13
+ return { err: fsErr(FsErr.Closed, "File is closed") } as const;
14
+ }
15
+
16
+ class FsFileImpl implements FsFile {
17
+ /** The path of the file */
18
+ public path: string;
19
+
20
+ private closed: boolean;
21
+
22
+ /** Reference to the file system so we can read/write */
23
+ private fs: FsFileSystemInternal;
24
+ /** If the file is text */
25
+ private isText: boolean;
26
+ /** Bytes of the file */
27
+ private buffer: Uint8Array | undefined;
28
+ /** If the content in the buffer is different from the content on FS */
29
+ private isBufferDirty: boolean;
30
+ /** The content string of the file */
31
+ private content: string | undefined;
32
+ /** If the content string is newer than the bytes */
33
+ private isContentNewer: boolean;
34
+ /** The last modified time of the file */
35
+ private lastModified: number | undefined;
36
+
37
+ constructor(fs: FsFileSystemInternal, path: string) {
38
+ this.closed = false;
39
+ this.fs = fs;
40
+ this.path = path;
41
+ this.isText = false;
42
+ this.buffer = undefined;
43
+ this.isBufferDirty = false;
44
+ this.content = undefined;
45
+ this.isContentNewer = false;
46
+ this.lastModified = undefined;
47
+ }
48
+
49
+ public close(): void {
50
+ this.closed = true;
51
+ this.fs.closeFile(this.path);
52
+ }
53
+
54
+ public isDirty(): boolean {
55
+ return this.isBufferDirty || this.isContentNewer;
56
+ }
57
+
58
+ public async getLastModified(): Promise<FsResult<number>> {
59
+ if (this.closed) {
60
+ return errclosed();
61
+ }
62
+ if (this.lastModified === undefined) {
63
+ const r = await this.loadIfNotDirty();
64
+ if (r.err) {
65
+ return r;
66
+ }
67
+ }
68
+ return { val: this.lastModified ?? 0 };
69
+ }
70
+
71
+ public async getText(): Promise<FsResult<string>> {
72
+ if (this.closed) {
73
+ return errclosed();
74
+ }
75
+ if (this.buffer === undefined) {
76
+ const r = await this.load();
77
+ if (r.err) {
78
+ return r;
79
+ }
80
+ }
81
+ if (!this.isText) {
82
+ const err = fsFail("File is not valid UTF-8");
83
+ return { err };
84
+ }
85
+ return { val: this.content ?? "" };
86
+ }
87
+
88
+ public async getBytes(): Promise<FsResult<Uint8Array>> {
89
+ if (this.closed) {
90
+ return errclosed();
91
+ }
92
+ this.updateBuffer();
93
+ if (this.buffer === undefined) {
94
+ const r = await this.load();
95
+ if (r.err) {
96
+ return r;
97
+ }
98
+ }
99
+ if (this.buffer === undefined) {
100
+ const err = fsFail(
101
+ "Read was successful, but content was undefined",
102
+ );
103
+ return { err };
104
+ }
105
+ return { val: this.buffer };
106
+ }
107
+
108
+ public setText(content: string): void {
109
+ if (this.closed) {
110
+ return;
111
+ }
112
+ if (this.content === content) {
113
+ return;
114
+ }
115
+ this.content = content;
116
+ this.isContentNewer = true;
117
+ this.lastModified = new Date().getTime();
118
+ }
119
+
120
+ public setBytes(content: Uint8Array): void {
121
+ if (this.closed) {
122
+ return;
123
+ }
124
+ this.buffer = content;
125
+ this.isBufferDirty = true;
126
+ this.decodeBuffer();
127
+ this.isContentNewer = true;
128
+ this.lastModified = new Date().getTime();
129
+ }
130
+
131
+ public async loadIfNotDirty(): Promise<FsVoid> {
132
+ if (this.closed) {
133
+ return errclosed();
134
+ }
135
+ if (this.isDirty()) {
136
+ return {};
137
+ }
138
+ return await this.load();
139
+ }
140
+
141
+ public async load(): Promise<FsVoid> {
142
+ if (this.closed) {
143
+ return errclosed();
144
+ }
145
+ const { val: file, err } = await this.fs.read(this.path);
146
+ if (err) {
147
+ return { err };
148
+ }
149
+
150
+ // check if the file has been modified since last loaded
151
+ if (this.lastModified !== undefined) {
152
+ if (file.lastModified <= this.lastModified) {
153
+ return {};
154
+ }
155
+ }
156
+ this.lastModified = file.lastModified;
157
+ // load the buffer
158
+ const buffer = await tryAsync(
159
+ async () => new Uint8Array(await file.arrayBuffer()),
160
+ );
161
+ if ("err" in buffer) {
162
+ const err = fsFail(errstr(buffer.err));
163
+ return { err };
164
+ }
165
+ this.buffer = buffer.val;
166
+ this.isBufferDirty = false;
167
+ // Try decoding the buffer as text
168
+ this.decodeBuffer();
169
+ this.isContentNewer = false;
170
+ return {};
171
+ }
172
+
173
+ public async writeIfNewer(): Promise<FsVoid> {
174
+ if (this.closed) {
175
+ return errclosed();
176
+ }
177
+ if (!this.isDirty()) {
178
+ return {};
179
+ }
180
+ return await this.write();
181
+ }
182
+
183
+ /**
184
+ * Write the content without checking if it's dirty. Overwrites the file currently on FS
185
+ *
186
+ * This is private - outside code should only use writeIfDirty
187
+ */
188
+ private async write(): Promise<FsVoid> {
189
+ this.updateBuffer();
190
+ const buffer = this.buffer;
191
+ if (this.content === undefined || buffer === undefined) {
192
+ // file was never read or modified
193
+ return {};
194
+ }
195
+ const result = await this.fs.write(this.path, buffer);
196
+ if (result.err) {
197
+ return result;
198
+ }
199
+ this.isBufferDirty = false;
200
+ return {};
201
+ }
202
+
203
+ private decodeBuffer() {
204
+ try {
205
+ this.content = new TextDecoder("utf-8", { fatal: true }).decode(
206
+ this.buffer,
207
+ );
208
+ this.isText = true;
209
+ } catch (_) {
210
+ this.content = undefined;
211
+ this.isText = false;
212
+ }
213
+ }
214
+
215
+ /** Encode the content to buffer if it is newer */
216
+ private updateBuffer() {
217
+ if (!this.isContentNewer || this.content === undefined) {
218
+ return;
219
+ }
220
+ const encoder = new TextEncoder();
221
+ this.buffer = encoder.encode(this.content);
222
+ this.isBufferDirty = true;
223
+ this.isContentNewer = false;
224
+ }
225
+ }
@@ -0,0 +1,29 @@
1
+ import type { FsFile } from "./FsFile.ts";
2
+ import type { FsFileSystemInternal } from "./FsFileSystemInternal.ts";
3
+ import { fsFile } from "./FsFileImpl.ts";
4
+
5
+ /** Internal class to track opened files */
6
+ export class FsFileMgr {
7
+ private opened: { [path: string]: FsFile };
8
+
9
+ public constructor() {
10
+ this.opened = {};
11
+ }
12
+
13
+ public get(fs: FsFileSystemInternal, path: string): FsFile {
14
+ let file = this.opened[path];
15
+ if (!file) {
16
+ file = fsFile(fs, path);
17
+ this.opened[path] = file;
18
+ }
19
+ return file;
20
+ }
21
+
22
+ public close(path: string): void {
23
+ delete this.opened[path];
24
+ }
25
+
26
+ public getOpenedPaths(): string[] {
27
+ return Object.keys(this.opened);
28
+ }
29
+ }
@@ -0,0 +1,71 @@
1
+ import type { FsFile } from "./FsFile.ts";
2
+ import type { FsResult } from "./FsError.ts";
3
+
4
+ /**
5
+ * File system before it is initialized
6
+ *
7
+ * This is an internal type used inside fsOpen functions
8
+ */
9
+ export interface FsFileSystemUninit {
10
+ /// Initialize the file system
11
+ init(): Promise<FsResult<FsFileSystem>>;
12
+ }
13
+
14
+ /** Initialized file system */
15
+ export interface FsFileSystem {
16
+ /**
17
+ * Get the root path of the file system for display
18
+ *
19
+ * The returned string has no significance in the file system itself.
20
+ * It should only be used as an indicator to the user.
21
+ */
22
+ readonly root: string;
23
+
24
+ /**
25
+ * Capabilities of this file system implementation
26
+ * See README.md for more information
27
+ */
28
+ readonly capabilities: FsCapabilities;
29
+
30
+ /**
31
+ * List files in a directory
32
+ *
33
+ * The input path should be relative to the root (of the uploaded directory).
34
+ *
35
+ * Returns a list of file names in the directory (not full paths).
36
+ * Directory names end with a slash.
37
+ *
38
+ * Returns Fail if the underlying file system operation fails.
39
+ */
40
+ listDir: (path: string) => Promise<FsResult<string[]>>;
41
+
42
+ /**
43
+ * Get a file object for operations
44
+ *
45
+ * The returned object can store temporary state for the file, such
46
+ * as newer content. Calling openFile with the same path will
47
+ * return the same object.
48
+ *
49
+ * Note that opening a file doesn't actually block the file
50
+ * from being modified by programs other than the browser.
51
+ *
52
+ * You can make the FsFileSystem forget about the file by
53
+ * calling `close` on the file object.
54
+ */
55
+ getFile: (path: string) => FsFile;
56
+
57
+ /** Get all paths that `getFile` has been called with but not `close`d */
58
+ getOpenedPaths: () => string[];
59
+ }
60
+
61
+ /** Capabilities of the file system implementation */
62
+ export type FsCapabilities = {
63
+ /** Can the browser directly write to the file system */
64
+ write: boolean;
65
+ /**
66
+ * Can the browser detect live updates:
67
+ * - Change of modified time
68
+ * - Change of directory structure (new, renamed, deleted files)
69
+ */
70
+ live: boolean;
71
+ };
@@ -0,0 +1,30 @@
1
+ import type { FsResult, FsVoid } from "./FsError.ts";
2
+
3
+ /** Internal APIs for FsFileSystem */
4
+ export interface FsFileSystemInternal {
5
+ /**
6
+ * Read the file as a File object
7
+ *
8
+ * Returns Fail if the underlying file system operation fails.
9
+ */
10
+ read: (path: string) => Promise<FsResult<File>>;
11
+
12
+ /**
13
+ * Write content to a file on disk
14
+ *
15
+ * Writes the content to the path specified.
16
+ * If the content is a string, UTF-8 encoding is used.
17
+ *
18
+ * Will overwrite existing file.
19
+ *
20
+ * Returns Fail if the underlying file system operation fails.
21
+ * Returns NotSupported if the browser does not support this
22
+ * Returns PermissionDenied if the operation is supported, but permission is not given
23
+ */
24
+ write: (path: string, content: Uint8Array) => Promise<FsVoid>;
25
+
26
+ /**
27
+ * Forget about a file
28
+ */
29
+ closeFile: (path: string) => void;
30
+ }