@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 +21 -0
- package/README.md +8 -0
- package/package.json +33 -0
- package/src/fs/FsError.ts +55 -0
- package/src/fs/FsFile.ts +67 -0
- package/src/fs/FsFileImpl.ts +225 -0
- package/src/fs/FsFileMgr.ts +29 -0
- package/src/fs/FsFileSystem.ts +71 -0
- package/src/fs/FsFileSystemInternal.ts +30 -0
- package/src/fs/FsImplEntryAPI.ts +188 -0
- package/src/fs/FsImplFileAPI.ts +126 -0
- package/src/fs/FsImplHandleAPI.ts +237 -0
- package/src/fs/FsOpen.ts +307 -0
- package/src/fs/FsPath.ts +137 -0
- package/src/fs/FsSave.ts +12 -0
- package/src/fs/FsSupportStatus.ts +91 -0
- package/src/fs/index.ts +129 -0
- package/src/log/index.ts +56 -0
- package/src/pref/dark.ts +184 -0
- package/src/pref/index.ts +12 -0
- package/src/pref/injectStyle.ts +22 -0
- package/src/pref/locale.ts +341 -0
- package/src/result/index.ts +215 -0
- package/src/sync/Debounce.ts +35 -0
- package/src/sync/Latest.ts +75 -0
- package/src/sync/RwLock.ts +95 -0
- package/src/sync/Serial.ts +170 -0
- package/src/sync/index.ts +12 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **I once had a fancy error object with TypeScript magic that tries
|
|
3
|
+
* to reduce allocation while maintaining Result-safety. It turns out
|
|
4
|
+
* that was slower than allocating plain objects for every return, because
|
|
5
|
+
* of how V8 optimizes things.**
|
|
6
|
+
*
|
|
7
|
+
* Don't even use `isErr()` helper functions to abstract. They are slower than
|
|
8
|
+
* directly property access in my testing.
|
|
9
|
+
*
|
|
10
|
+
* ## Function that can fail
|
|
11
|
+
* Instead of having functions `throw`, make it `return` instead.
|
|
12
|
+
* ```typescript
|
|
13
|
+
* // Instead of
|
|
14
|
+
* function doSomethingCanFail() {
|
|
15
|
+
* if (Math.random() < 0.5) {
|
|
16
|
+
* return 42;
|
|
17
|
+
* }
|
|
18
|
+
* throw "oops";
|
|
19
|
+
* }
|
|
20
|
+
* // Do this
|
|
21
|
+
* import type { Result } from "pure/result";
|
|
22
|
+
*
|
|
23
|
+
* function doSomethingCanFail(): Result<number, string> {
|
|
24
|
+
* if (Math.random() < 0.5) {
|
|
25
|
+
* return { val: 42 };
|
|
26
|
+
* }
|
|
27
|
+
* return { err: "oops" };
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
* This is similar to Rust:
|
|
31
|
+
* ```rust
|
|
32
|
+
* fn do_something_can_fail() -> Result<u32, String> {
|
|
33
|
+
* if ... {
|
|
34
|
+
* return Ok(42);
|
|
35
|
+
* }
|
|
36
|
+
*
|
|
37
|
+
* Err("oops".to_string())
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* ## Calling function that can fail
|
|
42
|
+
* The recommended pattern is
|
|
43
|
+
* ```typescript
|
|
44
|
+
* const x = doTheCall(); // x is Result<T, E>;
|
|
45
|
+
* if (x.err) {
|
|
46
|
+
* // x.err is E, handle it
|
|
47
|
+
* return ...
|
|
48
|
+
* }
|
|
49
|
+
* // x.val is T
|
|
50
|
+
* // ...
|
|
51
|
+
* ```
|
|
52
|
+
* If your `E` type covers falsy values that are valid, use `"err" in x` instead of `x.err`.
|
|
53
|
+
* A well-known case is `Result<T, unknown>`. `if(r.err)` cannot narrow the else case to `Ok`,
|
|
54
|
+
* but `if("err" in r)` can.
|
|
55
|
+
*
|
|
56
|
+
* A full example:
|
|
57
|
+
* ```typescript
|
|
58
|
+
* function getParam(name: string): Result<number, Error> {
|
|
59
|
+
* if (name === "a") {
|
|
60
|
+
* return { val: 13 };
|
|
61
|
+
* }
|
|
62
|
+
* if (name === "b") {
|
|
63
|
+
* return { val: 42 };
|
|
64
|
+
* }
|
|
65
|
+
* return { err: new Error("bad name") };
|
|
66
|
+
* }
|
|
67
|
+
*
|
|
68
|
+
* function multiplyFormat(
|
|
69
|
+
* name1: string,
|
|
70
|
+
* name2: string,
|
|
71
|
+
* prefix: string
|
|
72
|
+
* ): Result<string, Error> {
|
|
73
|
+
* const v1 = getParam(name1);
|
|
74
|
+
* if (v1.err) {
|
|
75
|
+
* console.error(v1.err);
|
|
76
|
+
* return v1;
|
|
77
|
+
* }
|
|
78
|
+
* const v2 = getParam(name1);
|
|
79
|
+
* if (v2.err) {
|
|
80
|
+
* console.error(v2.err);
|
|
81
|
+
* return v2;
|
|
82
|
+
* }
|
|
83
|
+
*
|
|
84
|
+
* const formatted = `${prefix}${v1.val * v2.val}`;
|
|
85
|
+
* return { val: formatted };
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* ## Interop with throwing functions
|
|
90
|
+
* This library also has `tryCatch` to interop with throwing functions,
|
|
91
|
+
* and `tryAsync` for async functions.
|
|
92
|
+
*
|
|
93
|
+
* ```typescript
|
|
94
|
+
* import { tryCatch, tryAsync } from "pure/result";
|
|
95
|
+
*
|
|
96
|
+
* // synchronous
|
|
97
|
+
* const result1: Result<MyData, unknown> = tryCatch(() => JSON.parse<MyData>(...));
|
|
98
|
+
* // or you can specify the error type:
|
|
99
|
+
* const result2 = tryCatch<MyData, SyntaxError>(() => JSON.parse(...));
|
|
100
|
+
*
|
|
101
|
+
* // asynchronous
|
|
102
|
+
* async function doSomethingCanFail() {
|
|
103
|
+
* if (Math.random() < 0.5) {
|
|
104
|
+
* return 42;
|
|
105
|
+
* }
|
|
106
|
+
* throw "oops";
|
|
107
|
+
* }
|
|
108
|
+
* const result = await tryAsync<number, string>(() => doStuff);
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* ## Returning void
|
|
112
|
+
* Use `Void<E>` as the return type if the function returns `void` on success
|
|
113
|
+
* ```typescript
|
|
114
|
+
* const x = doSomethingThatVoidsOnSuccess();
|
|
115
|
+
* if (x.err) {
|
|
116
|
+
* return x;
|
|
117
|
+
* }
|
|
118
|
+
* // type of x is Record<string, never>, i.e. empty object
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* ## Why is there no `match`/`map`/`mapErr`, etc?
|
|
122
|
+
*
|
|
123
|
+
* If you are thinking this is a great idea:
|
|
124
|
+
* ```typescript
|
|
125
|
+
* const result = foo(bar);
|
|
126
|
+
* match(result,
|
|
127
|
+
* (okValue) => {
|
|
128
|
+
* // handle ok case
|
|
129
|
+
* },
|
|
130
|
+
* (errValue) => {
|
|
131
|
+
* // handle err case
|
|
132
|
+
* },
|
|
133
|
+
* );
|
|
134
|
+
* ```
|
|
135
|
+
* The vanilla `if` doesn't allocate the closures, and has less code, and you can
|
|
136
|
+
* control the flow properly inside the blocks with `return`/`break`/`continue`
|
|
137
|
+
* ```typescript
|
|
138
|
+
* const result = foo(bar);
|
|
139
|
+
* if (result.err) {
|
|
140
|
+
* // handle err case
|
|
141
|
+
* } else {
|
|
142
|
+
* // handle ok case
|
|
143
|
+
* }
|
|
144
|
+
* ```
|
|
145
|
+
*
|
|
146
|
+
* As for the other utility functions from Rust's Result type, they really only benefit
|
|
147
|
+
* because you can early return with `?` AND those abstractions are zero-cost in Rust.
|
|
148
|
+
* Neither is true in JavaScript.
|
|
149
|
+
*
|
|
150
|
+
* You can also easily write them yourself if you really want to.
|
|
151
|
+
*
|
|
152
|
+
* @module
|
|
153
|
+
*/
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A value that either a success (Ok) or an error (Err)
|
|
157
|
+
*
|
|
158
|
+
* Construct a success with { val: ... } and an error with { err: ... }
|
|
159
|
+
*/
|
|
160
|
+
export type Result<T, E> = Ok<T> | Err<E>;
|
|
161
|
+
|
|
162
|
+
// If these look weird, it's because TypeScript is weird
|
|
163
|
+
// This is to get type narrowing to work most of the time
|
|
164
|
+
|
|
165
|
+
/** A success value */
|
|
166
|
+
export type Ok<T> = { val: T; err?: never };
|
|
167
|
+
/** An error value */
|
|
168
|
+
export type Err<E> = { err: E; val?: never };
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A value that is either `void` or an error
|
|
172
|
+
*
|
|
173
|
+
* Construct success with `{}` and an error with `{ err: ... }`
|
|
174
|
+
*/
|
|
175
|
+
export type Void<E> = { val?: never; err?: never } | { err: E };
|
|
176
|
+
/** A value that is a success `void` */
|
|
177
|
+
export type VoidOk = Record<string, never>;
|
|
178
|
+
|
|
179
|
+
/** Wrap a function with try-catch and return a Result. */
|
|
180
|
+
export function tryCatch<T, E = unknown>(fn: () => T): Result<T, E> {
|
|
181
|
+
try {
|
|
182
|
+
return { val: fn() };
|
|
183
|
+
} catch (e) {
|
|
184
|
+
return { err: e as E };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Wrap an async function with try-catch and return a Promise<Result>. */
|
|
189
|
+
export async function tryAsync<T, E = unknown>(
|
|
190
|
+
fn: () => Promise<T>,
|
|
191
|
+
): Promise<Result<T, E>> {
|
|
192
|
+
try {
|
|
193
|
+
return { val: await fn() };
|
|
194
|
+
} catch (e) {
|
|
195
|
+
return { err: e as E };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Try best effort converting an error to a string */
|
|
200
|
+
export function errstr(e: unknown): string {
|
|
201
|
+
if (typeof e === "string") {
|
|
202
|
+
return e;
|
|
203
|
+
}
|
|
204
|
+
if (e) {
|
|
205
|
+
if (typeof e === "object" && "message" in e) {
|
|
206
|
+
if (typeof e.message === "string") {
|
|
207
|
+
return e.message;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (typeof e === "object" && "toString" in e) {
|
|
211
|
+
return e.toString();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return `${e}`;
|
|
215
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debounce executes an action after a certain delay. Any
|
|
3
|
+
* call to the action during this delay resets the timer.
|
|
4
|
+
*
|
|
5
|
+
* ## Warning
|
|
6
|
+
* The implementation is naive and does not handle the case
|
|
7
|
+
* where the action keeps getting called before the delay,
|
|
8
|
+
* in which case the action will never be executed. This
|
|
9
|
+
* will be improved in the future...
|
|
10
|
+
*/
|
|
11
|
+
export class Debounce<TResult> {
|
|
12
|
+
private timer: number | undefined;
|
|
13
|
+
constructor(
|
|
14
|
+
private fn: () => Promise<TResult>,
|
|
15
|
+
private delay: number,
|
|
16
|
+
) {
|
|
17
|
+
this.timer = undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public execute(): Promise<TResult> {
|
|
21
|
+
if (this.timer !== undefined) {
|
|
22
|
+
clearTimeout(this.timer);
|
|
23
|
+
}
|
|
24
|
+
return new Promise<TResult>((resolve, reject) => {
|
|
25
|
+
this.timer = setTimeout(async () => {
|
|
26
|
+
this.timer = undefined;
|
|
27
|
+
try {
|
|
28
|
+
resolve(await this.fn());
|
|
29
|
+
} catch (error) {
|
|
30
|
+
reject(error);
|
|
31
|
+
}
|
|
32
|
+
}, this.delay);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Result } from "../result/index.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Latest is a synchronization utility to allow
|
|
5
|
+
* only one async operation to be executed at a time,
|
|
6
|
+
* and any call will only return the result of the latest
|
|
7
|
+
* operation.
|
|
8
|
+
*
|
|
9
|
+
* ## Example
|
|
10
|
+
* In the example below, both call will return the result
|
|
11
|
+
* of the second call (2)
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { Latest } from "@pistonite/pure/sync";
|
|
14
|
+
*
|
|
15
|
+
* let counter = 0;
|
|
16
|
+
*
|
|
17
|
+
* const operation = async () => {
|
|
18
|
+
* counter++;
|
|
19
|
+
* await new Promise((resolve) => setTimeout(() => {
|
|
20
|
+
* resolve(counter);
|
|
21
|
+
* }, 1000));
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* const call = new Latest(operation);
|
|
25
|
+
*
|
|
26
|
+
* const result1 = call.execute();
|
|
27
|
+
* const result2 = call.execute();
|
|
28
|
+
* console.log(await result1); // 2
|
|
29
|
+
* console.log(await result2); // 2
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export class Latest<TResult> {
|
|
33
|
+
private isRunning = false;
|
|
34
|
+
private pending: {
|
|
35
|
+
resolve: (result: TResult) => void;
|
|
36
|
+
reject: (error: unknown) => void;
|
|
37
|
+
}[] = [];
|
|
38
|
+
|
|
39
|
+
constructor(private fn: () => Promise<TResult>) {}
|
|
40
|
+
|
|
41
|
+
public async execute(): Promise<TResult> {
|
|
42
|
+
if (this.isRunning) {
|
|
43
|
+
return new Promise<TResult>((resolve, reject) => {
|
|
44
|
+
this.pending.push({ resolve, reject });
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
this.isRunning = true;
|
|
48
|
+
const alreadyPending = [];
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
|
+
let result: Result<TResult, unknown> = { err: "not executed" };
|
|
51
|
+
// eslint-disable-next-line no-constant-condition
|
|
52
|
+
while (true) {
|
|
53
|
+
try {
|
|
54
|
+
const fn = this.fn;
|
|
55
|
+
result = { val: await fn() };
|
|
56
|
+
} catch (error) {
|
|
57
|
+
result = { err: error };
|
|
58
|
+
}
|
|
59
|
+
if (this.pending.length) {
|
|
60
|
+
alreadyPending.push(...this.pending);
|
|
61
|
+
this.pending = [];
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
this.isRunning = false;
|
|
67
|
+
if ("err" in result) {
|
|
68
|
+
alreadyPending.forEach(({ reject }) => reject(result.err));
|
|
69
|
+
throw result.err;
|
|
70
|
+
} else {
|
|
71
|
+
alreadyPending.forEach(({ resolve }) => resolve(result.val));
|
|
72
|
+
return result.val;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import Deque from "denque";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ensure you have exclusive access in concurrent code
|
|
5
|
+
*
|
|
6
|
+
* Only guaranteed if no one else has reference to the inner object
|
|
7
|
+
*
|
|
8
|
+
* It can take a second type parameter to specify interface with write methods
|
|
9
|
+
*/
|
|
10
|
+
export class RwLock<TRead, TWrite extends TRead = TRead> {
|
|
11
|
+
/**
|
|
12
|
+
* This is public so inner object can be accessed directly
|
|
13
|
+
* ONLY SAFE in sync context
|
|
14
|
+
*/
|
|
15
|
+
public inner: TWrite;
|
|
16
|
+
|
|
17
|
+
private readers: number = 0;
|
|
18
|
+
private isWriting: boolean = false;
|
|
19
|
+
private readWaiters: Deque<() => void> = new Deque();
|
|
20
|
+
private writeWaiters: Deque<() => void> = new Deque();
|
|
21
|
+
|
|
22
|
+
constructor(t: TWrite) {
|
|
23
|
+
this.inner = t;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Acquire a read (shared) lock and call fn with the value. Release the lock when fn returns or throws. */
|
|
27
|
+
public async scopedRead<R>(fn: (t: TRead) => Promise<R>): Promise<R> {
|
|
28
|
+
if (this.isWriting) {
|
|
29
|
+
await new Promise<void>((resolve) => {
|
|
30
|
+
// need to check again to make sure it's not already done
|
|
31
|
+
if (this.isWriting) {
|
|
32
|
+
this.readWaiters.push(resolve);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
resolve();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// acquired
|
|
39
|
+
this.readers++;
|
|
40
|
+
try {
|
|
41
|
+
return await fn(this.inner);
|
|
42
|
+
} finally {
|
|
43
|
+
this.readers--;
|
|
44
|
+
if (this.writeWaiters.length > 0) {
|
|
45
|
+
if (this.readers === 0) {
|
|
46
|
+
// notify one writer
|
|
47
|
+
this.writeWaiters.shift()!();
|
|
48
|
+
}
|
|
49
|
+
// don't notify anyone if there are still readers
|
|
50
|
+
} else {
|
|
51
|
+
// notify all readers
|
|
52
|
+
while (this.readWaiters.length > 0) {
|
|
53
|
+
this.readWaiters.shift()!();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Acquire a write (exclusive) lock and call fn with the value. Release the lock when fn returns or throws.
|
|
61
|
+
*
|
|
62
|
+
* fn takes a setter function as second parameter, which you can use to update the value like `x = set(newX)`
|
|
63
|
+
*/
|
|
64
|
+
public async scopedWrite<R>(
|
|
65
|
+
fn: (t: TWrite, setter: (t: TWrite) => TWrite) => Promise<R>,
|
|
66
|
+
): Promise<R> {
|
|
67
|
+
if (this.isWriting || this.readers > 0) {
|
|
68
|
+
await new Promise<void>((resolve) => {
|
|
69
|
+
// need to check again to make sure it's not already done
|
|
70
|
+
if (this.isWriting || this.readers > 0) {
|
|
71
|
+
this.writeWaiters.push(resolve);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
resolve();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// acquired
|
|
78
|
+
this.isWriting = true;
|
|
79
|
+
try {
|
|
80
|
+
return await fn(this.inner, (t: TWrite) => {
|
|
81
|
+
this.inner = t;
|
|
82
|
+
return t;
|
|
83
|
+
});
|
|
84
|
+
} finally {
|
|
85
|
+
this.isWriting = false;
|
|
86
|
+
if (this.readWaiters.length > 0) {
|
|
87
|
+
// notify one reader
|
|
88
|
+
this.readWaiters.shift()!();
|
|
89
|
+
} else if (this.writeWaiters.length > 0) {
|
|
90
|
+
// notify one writer
|
|
91
|
+
this.writeWaiters.shift()!();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { Void, Err, VoidOk } from "../result/index.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An async event that can be cancelled when a new one starts
|
|
5
|
+
*
|
|
6
|
+
* ## Example
|
|
7
|
+
*
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { Serial } from "@pistonite/pure/sync";
|
|
10
|
+
*
|
|
11
|
+
* // helper function to simulate async work
|
|
12
|
+
* const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
*
|
|
14
|
+
* // Create the event
|
|
15
|
+
* const event = new Serial();
|
|
16
|
+
*
|
|
17
|
+
* // The cancellation system uses the Result type
|
|
18
|
+
* // and returns an error when it is cancelled
|
|
19
|
+
* const promise1 = event.run(async (shouldCancel) => {
|
|
20
|
+
* for (let i = 0; i < 10; i++) {
|
|
21
|
+
* await wait(1000);
|
|
22
|
+
* const cancelResult = shouldCancel();
|
|
23
|
+
* if (cancelResult.err) {
|
|
24
|
+
* return cancelResult;
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* return { val: 42 };
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* await wait(3000);
|
|
31
|
+
*
|
|
32
|
+
* // calling event.run a second time will cause `shouldCancel` to return false
|
|
33
|
+
* // the next time it's called by the first event
|
|
34
|
+
* const promise2 = event.run(async (shouldCancel) => {
|
|
35
|
+
* for (let i = 0; i < 10; i++) {
|
|
36
|
+
* await wait(1000);
|
|
37
|
+
* const cancelResult = shouldCancel();
|
|
38
|
+
* if (cancelResult.err) {
|
|
39
|
+
* return cancelResult;
|
|
40
|
+
* }
|
|
41
|
+
* }
|
|
42
|
+
* return { val: 43 };
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* console.log(await promise1); // { err: "cancel" }
|
|
46
|
+
* console.log(await promise2); // { val: 43 }
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* ## Getting the current serial number
|
|
50
|
+
* The serial number has type `bigint` and is incremented every time `run` is called.
|
|
51
|
+
*
|
|
52
|
+
* The callback function receives the current serial number as the second argument, if you need it
|
|
53
|
+
* ```typescript
|
|
54
|
+
* import { Serial } from "@pistonite/pure/sync";
|
|
55
|
+
*
|
|
56
|
+
* const event = new Serial();
|
|
57
|
+
* const promise = event.run(async (shouldCancel, serial) => {
|
|
58
|
+
* console.log(serial);
|
|
59
|
+
* return {};
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* ## Checking for cancel
|
|
64
|
+
* It's the event handler's responsibility to check if the event is cancelled by
|
|
65
|
+
* calling the `shouldCancel` function. This function returns an `Err` if it should be cancelled.
|
|
66
|
+
*
|
|
67
|
+
* ```typescript
|
|
68
|
+
* import { Serial } from "@pistonite/pure/sync";
|
|
69
|
+
*
|
|
70
|
+
* const event = new Serial();
|
|
71
|
+
* await event.run(async (shouldCancel, serial) => {
|
|
72
|
+
* // do some operations
|
|
73
|
+
* ...
|
|
74
|
+
*
|
|
75
|
+
* const cancelResult = shouldCancel();
|
|
76
|
+
* if (cancelResult.err) {
|
|
77
|
+
* return cancelResult;
|
|
78
|
+
* }
|
|
79
|
+
*
|
|
80
|
+
* // not cancelled, continue
|
|
81
|
+
* ...
|
|
82
|
+
* });
|
|
83
|
+
* ```
|
|
84
|
+
* It's possible the operation is cheap enough that an outdated event should probably be let finish.
|
|
85
|
+
* It's ok in that case to not call `shouldCancel`. The `Serial` class checks it one
|
|
86
|
+
* last time before returning the result after the callback finishes.
|
|
87
|
+
*
|
|
88
|
+
* ## Handling cancelled event
|
|
89
|
+
* To check if an event is completed or cancelled, simply `await`
|
|
90
|
+
* on the promise returned by `event.run` and check the `err`
|
|
91
|
+
* ```typescript
|
|
92
|
+
* import { Serial } from "@pistonite/pure/sync";
|
|
93
|
+
*
|
|
94
|
+
* const event = new Serial();
|
|
95
|
+
* const result = await event.run(async (shouldCancel) => {
|
|
96
|
+
* // your code here ...
|
|
97
|
+
* );
|
|
98
|
+
* if (result.err === "cancel") {
|
|
99
|
+
* console.log("event was cancelled");
|
|
100
|
+
* } else {
|
|
101
|
+
* console.log("event completed");
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* You can also pass in a callback to the constructor, which will be called
|
|
106
|
+
* when the event is cancelled. This event is guaranteed to fire at most once per run
|
|
107
|
+
* ```typescript
|
|
108
|
+
* import { Serial } from "@pistonite/pure/sync";
|
|
109
|
+
*
|
|
110
|
+
* const event = new Serial((current, latest) => {
|
|
111
|
+
* console.log(`Event with serial ${current} is cancelled because the latest serial is ${latest}`);
|
|
112
|
+
* });
|
|
113
|
+
* ```
|
|
114
|
+
*
|
|
115
|
+
*
|
|
116
|
+
*/
|
|
117
|
+
export class Serial {
|
|
118
|
+
private serial: bigint;
|
|
119
|
+
private onCancel: SerialEventCancelCallback;
|
|
120
|
+
|
|
121
|
+
constructor(onCancel?: SerialEventCancelCallback) {
|
|
122
|
+
this.serial = 0n;
|
|
123
|
+
if (onCancel) {
|
|
124
|
+
this.onCancel = onCancel;
|
|
125
|
+
} else {
|
|
126
|
+
this.onCancel = () => {};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public async run<T = VoidOk>(
|
|
131
|
+
callback: SerialEventCallback<T>,
|
|
132
|
+
): Promise<T | Err<SerialEventCancelToken>> {
|
|
133
|
+
let cancelled = false;
|
|
134
|
+
const currentSerial = ++this.serial;
|
|
135
|
+
const shouldCancel = () => {
|
|
136
|
+
if (currentSerial !== this.serial) {
|
|
137
|
+
if (!cancelled) {
|
|
138
|
+
cancelled = true;
|
|
139
|
+
this.onCancel(currentSerial, this.serial);
|
|
140
|
+
}
|
|
141
|
+
return { err: "cancel" as const };
|
|
142
|
+
}
|
|
143
|
+
return {};
|
|
144
|
+
};
|
|
145
|
+
const result = await callback(shouldCancel, currentSerial);
|
|
146
|
+
const cancelResult = shouldCancel();
|
|
147
|
+
if (cancelResult.err) {
|
|
148
|
+
return cancelResult;
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The callback type passed to SerialEvent constructor to be called
|
|
156
|
+
* when the event is cancelled
|
|
157
|
+
*/
|
|
158
|
+
export type SerialEventCancelCallback = (
|
|
159
|
+
current: bigint,
|
|
160
|
+
latest: bigint,
|
|
161
|
+
) => void;
|
|
162
|
+
|
|
163
|
+
/** The callback type passed to SerialEvent.run */
|
|
164
|
+
export type SerialEventCallback<T = VoidOk> = (
|
|
165
|
+
shouldCancel: () => Void<SerialEventCancelToken>,
|
|
166
|
+
current: bigint,
|
|
167
|
+
) => Promise<T | Err<SerialEventCancelToken>>;
|
|
168
|
+
|
|
169
|
+
/** The error type received by caller when an event is cancelled */
|
|
170
|
+
export type SerialEventCancelToken = "cancel";
|