@pistonite/pure 0.0.19 → 0.21.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.
- package/package.json +2 -2
- package/src/memory/index.ts +8 -0
- package/src/memory/weak.ts +139 -0
- package/src/pref/dark.ts +1 -1
- package/src/pref/locale.ts +1 -1
- package/src/sync/batch.ts +12 -13
- package/src/sync/debounce.ts +9 -9
- package/src/sync/index.ts +10 -4
- package/src/sync/latest.test.ts +57 -11
- package/src/sync/latest.ts +96 -26
- package/src/sync/once.ts +7 -15
- package/src/sync/serial.ts +3 -4
- package/src/sync/util.ts +6 -0
- /package/src/{sync → memory}/cell.ts +0 -0
- /package/src/{sync → memory}/persist.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pistonite/pure",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pure TypeScript libraries for my projects",
|
|
6
6
|
"homepage": "https://github.com/Pistonite/pure",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"@types/file-saver": "^2.0.7",
|
|
34
34
|
"eslint": "^9.19.0",
|
|
35
35
|
"typescript": "^5.7.2",
|
|
36
|
-
"vitest": "^
|
|
36
|
+
"vitest": "^3.0.5",
|
|
37
37
|
"mono-dev": "0.0.0"
|
|
38
38
|
}
|
|
39
39
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export type ExternalWeakRef<TUnderlying, TType> = {
|
|
2
|
+
/**
|
|
3
|
+
* A marker value for the underlying object type.
|
|
4
|
+
*
|
|
5
|
+
* This is commonly a string literal or a symbol.
|
|
6
|
+
*/
|
|
7
|
+
type: TType;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The underlying object reference.
|
|
11
|
+
*/
|
|
12
|
+
ref: TUnderlying | undefined;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Free the underlying object.
|
|
16
|
+
*/
|
|
17
|
+
free: () => void;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Update the underlying object reference.
|
|
21
|
+
*
|
|
22
|
+
* If the new reference is the same as the old one, nothing will happen.
|
|
23
|
+
* If the old reference is not undefined, it will be freed.
|
|
24
|
+
*/
|
|
25
|
+
set: (value: TUnderlying | undefined) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ExternalWeakRefConstructor<TUnderlying, TType> = {
|
|
29
|
+
/**
|
|
30
|
+
* A marker value for the underlying object type.
|
|
31
|
+
*
|
|
32
|
+
* This is commonly a string literal or a symbol.
|
|
33
|
+
*/
|
|
34
|
+
marker: TType;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The function to free the underlying object.
|
|
38
|
+
*/
|
|
39
|
+
free: (obj: TUnderlying) => void;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a weak reference type for managing externally memory-managed object. This means
|
|
44
|
+
* the objects needs to be freed manually by the external code.
|
|
45
|
+
*
|
|
46
|
+
* The `marker` option is used to distinguish between different types of weak references
|
|
47
|
+
* with the same underlying representation for the reference.
|
|
48
|
+
*
|
|
49
|
+
* Note that the underlying representation should not be undefined-able!
|
|
50
|
+
*
|
|
51
|
+
* ## Example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* import { makeExternalWeakRefType } from "@pistonite/pure/memory";
|
|
54
|
+
*
|
|
55
|
+
* // assume `number` is the JS type used to represent the external object
|
|
56
|
+
* // for example, this can be a pointer to a C++ object
|
|
57
|
+
* declare function freeFoo(obj: number) => void;
|
|
58
|
+
*
|
|
59
|
+
* // some function that allocates a foo object externally and returns
|
|
60
|
+
* // a reference
|
|
61
|
+
* declare function getFoo(): number;
|
|
62
|
+
*
|
|
63
|
+
* const makeFooRef = makeExternalWeakRefType({
|
|
64
|
+
* marker: "foo",
|
|
65
|
+
* free: (obj) => {
|
|
66
|
+
* freeFoo(obj);
|
|
67
|
+
* }
|
|
68
|
+
* });
|
|
69
|
+
* type FooRef = ReturnType<typeof makeFooRef>;
|
|
70
|
+
*
|
|
71
|
+
* // create a reference to a foo object
|
|
72
|
+
* // now this reference can be passed around in JS,
|
|
73
|
+
* // as long as the ownership model is clear and the owner
|
|
74
|
+
* // remembers to free it
|
|
75
|
+
* const fooRef = makeFooRef(getFoo());
|
|
76
|
+
*
|
|
77
|
+
* // free the foo object when it is no longer needed
|
|
78
|
+
* fooRef.free();
|
|
79
|
+
*
|
|
80
|
+
* ## Updating the reference
|
|
81
|
+
* The `set` method will update the reference and free the old one if exists
|
|
82
|
+
* ```
|
|
83
|
+
* const fooRef = makeFooRef(getFoo());
|
|
84
|
+
* fooRef.set(getFoo()); // the old one will be freed, unless it is the same as the new one
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* This has a major pitfall: If the ExternalWeakRef is shared, the new object will be accessible
|
|
88
|
+
* by code that has the old reference. In other words, when the reference is updated, code that
|
|
89
|
+
* already has the old reference will not able to know that it has changed.
|
|
90
|
+
*
|
|
91
|
+
* If this is a problem, you should use this pattern instead:
|
|
92
|
+
* ```typescript
|
|
93
|
+
* // track the "current" valid reference
|
|
94
|
+
* let currentRef = makeFooRef(undefined);
|
|
95
|
+
*
|
|
96
|
+
* export const getFooRef = (): FooRef => {
|
|
97
|
+
* // because of this function, many other places can hold
|
|
98
|
+
* // a valid reference to foo
|
|
99
|
+
* return currentRef;
|
|
100
|
+
* }
|
|
101
|
+
*
|
|
102
|
+
* export const updateFooRef = (newFoo: number): void => {
|
|
103
|
+
* // when updating the reference, we create a new weak ref and free the old one
|
|
104
|
+
* if (currentRef.ref === newFoo) {
|
|
105
|
+
* return; // always need to check if old and new are the same, otherwise we will be freeing the new one
|
|
106
|
+
* }
|
|
107
|
+
* const newRef = makeFooRef(newFoo);
|
|
108
|
+
* currentRef.free();
|
|
109
|
+
* currentRef = newRef;
|
|
110
|
+
*
|
|
111
|
+
* // now other places that hold the old reference will see it's freed
|
|
112
|
+
* }
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export const makeExternalWeakRefType = <TUnderlying, TType>({
|
|
116
|
+
marker,
|
|
117
|
+
free,
|
|
118
|
+
}: ExternalWeakRefConstructor<TUnderlying, TType>) => {
|
|
119
|
+
return (
|
|
120
|
+
obj: TUnderlying | undefined,
|
|
121
|
+
): ExternalWeakRef<TUnderlying, TType> => {
|
|
122
|
+
const weakRefObj = {
|
|
123
|
+
type: marker,
|
|
124
|
+
ref: obj,
|
|
125
|
+
free: () => {
|
|
126
|
+
if (weakRefObj.ref !== undefined) {
|
|
127
|
+
free(weakRefObj.ref);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
set: (value: TUnderlying | undefined) => {
|
|
131
|
+
if (weakRefObj.ref !== undefined && weakRefObj.ref !== value) {
|
|
132
|
+
free(weakRefObj.ref);
|
|
133
|
+
}
|
|
134
|
+
weakRefObj.ref = value;
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
return weakRefObj;
|
|
138
|
+
};
|
|
139
|
+
};
|
package/src/pref/dark.ts
CHANGED
package/src/pref/locale.ts
CHANGED
package/src/sync/batch.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AnyFn,
|
|
3
|
+
makePromise,
|
|
4
|
+
type PromiseHandle,
|
|
5
|
+
type AwaitRet,
|
|
6
|
+
} from "./util.ts";
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* An async event wrapper that allows multiple calls in an interval
|
|
@@ -87,8 +92,7 @@ import { makePromise, type AwaitRet } from "./util.ts";
|
|
|
87
92
|
* ```
|
|
88
93
|
*
|
|
89
94
|
*/
|
|
90
|
-
|
|
91
|
-
export function batch<TFn extends (...args: any[]) => any>({
|
|
95
|
+
export function batch<TFn extends AnyFn>({
|
|
92
96
|
fn,
|
|
93
97
|
batch,
|
|
94
98
|
unbatch,
|
|
@@ -108,8 +112,7 @@ export function batch<TFn extends (...args: any[]) => any>({
|
|
|
108
112
|
/**
|
|
109
113
|
* Options to construct a `batch` function
|
|
110
114
|
*/
|
|
111
|
-
|
|
112
|
-
export type BatchConstructor<TFn extends (...args: any[]) => any> = {
|
|
115
|
+
export type BatchConstructor<TFn extends AnyFn> = {
|
|
113
116
|
/** Function to be wrapped */
|
|
114
117
|
fn: TFn;
|
|
115
118
|
/** Function to batch the inputs across multiple calls */
|
|
@@ -135,15 +138,11 @@ export type BatchConstructor<TFn extends (...args: any[]) => any> = {
|
|
|
135
138
|
disregardExecutionTime?: boolean;
|
|
136
139
|
};
|
|
137
140
|
|
|
138
|
-
|
|
139
|
-
class BatchImpl<TFn extends (...args: any[]) => any> {
|
|
141
|
+
class BatchImpl<TFn extends AnyFn> {
|
|
140
142
|
private idle: boolean;
|
|
141
|
-
private scheduled: {
|
|
143
|
+
private scheduled: (PromiseHandle<AwaitRet<TFn>> & {
|
|
142
144
|
input: Parameters<TFn>;
|
|
143
|
-
|
|
144
|
-
resolve: (value: AwaitRet<TFn>) => void;
|
|
145
|
-
reject: (error: unknown) => void;
|
|
146
|
-
}[];
|
|
145
|
+
})[];
|
|
147
146
|
|
|
148
147
|
constructor(
|
|
149
148
|
private fn: TFn,
|
|
@@ -166,7 +165,7 @@ class BatchImpl<TFn extends (...args: any[]) => any> {
|
|
|
166
165
|
this.idle = false;
|
|
167
166
|
return this.execute(...args);
|
|
168
167
|
}
|
|
169
|
-
const { promise,
|
|
168
|
+
const { promise, resolve, reject } = makePromise<AwaitRet<TFn>>();
|
|
170
169
|
this.scheduled.push({
|
|
171
170
|
input: args,
|
|
172
171
|
promise,
|
package/src/sync/debounce.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AnyFn,
|
|
3
|
+
type AwaitRet,
|
|
4
|
+
makePromise,
|
|
5
|
+
type PromiseHandle,
|
|
6
|
+
} from "./util.ts";
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* An async event wrapper that is guaranteed to:
|
|
@@ -78,8 +83,7 @@ import { type AwaitRet, makePromise } from "./util.ts";
|
|
|
78
83
|
* });
|
|
79
84
|
* ```
|
|
80
85
|
*/
|
|
81
|
-
|
|
82
|
-
export function debounce<TFn extends (...args: any[]) => any>({
|
|
86
|
+
export function debounce<TFn extends AnyFn>({
|
|
83
87
|
fn,
|
|
84
88
|
interval,
|
|
85
89
|
disregardExecutionTime,
|
|
@@ -114,14 +118,10 @@ export type DebounceConstructor<TFn> = {
|
|
|
114
118
|
disregardExecutionTime?: boolean;
|
|
115
119
|
};
|
|
116
120
|
|
|
117
|
-
|
|
118
|
-
class DebounceImpl<TFn extends (...args: any[]) => any> {
|
|
121
|
+
class DebounceImpl<TFn extends AnyFn> {
|
|
119
122
|
private idle: boolean;
|
|
120
|
-
private next?: {
|
|
123
|
+
private next?: PromiseHandle<AwaitRet<TFn>> & {
|
|
121
124
|
args: Parameters<TFn>;
|
|
122
|
-
promise: Promise<AwaitRet<TFn>>;
|
|
123
|
-
resolve: (result: AwaitRet<TFn>) => void;
|
|
124
|
-
reject: (error: unknown) => void;
|
|
125
125
|
};
|
|
126
126
|
constructor(
|
|
127
127
|
private fn: TFn,
|
package/src/sync/index.ts
CHANGED
|
@@ -4,13 +4,19 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
|
-
export {
|
|
8
|
-
|
|
7
|
+
export {
|
|
8
|
+
serial,
|
|
9
|
+
type SerialConstructor,
|
|
10
|
+
type SerialEventCancelCallback,
|
|
11
|
+
type SerialCancelToken,
|
|
12
|
+
} from "./serial.ts";
|
|
13
|
+
export { latest, type LatestConstructor, type UpdateArgsFn } from "./latest.ts";
|
|
9
14
|
export { debounce, type DebounceConstructor } from "./debounce.ts";
|
|
10
15
|
export { batch, type BatchConstructor } from "./batch.ts";
|
|
11
|
-
export { cell, type CellConstructor, type Cell } from "./cell.ts";
|
|
12
|
-
export { persist, type PersistConstructor, type Persist } from "./persist.ts";
|
|
13
16
|
export { once, type OnceConstructor } from "./once.ts";
|
|
14
17
|
|
|
18
|
+
// types
|
|
19
|
+
export type { AnyFn, AwaitRet } from "./util.ts";
|
|
20
|
+
|
|
15
21
|
// unstable
|
|
16
22
|
export { RwLock } from "./RwLock.ts";
|
package/src/sync/latest.test.ts
CHANGED
|
@@ -1,18 +1,64 @@
|
|
|
1
|
-
import { expect, test } from "vitest";
|
|
1
|
+
import { expect, test, describe } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { latest } from "./latest.ts";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
describe("latest", () => {
|
|
6
|
+
test("returns latest result", async () => {
|
|
7
|
+
let counter = 0;
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const execute = latest({
|
|
10
|
+
fn: () => {
|
|
11
|
+
return ++counter;
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const result1 = execute();
|
|
16
|
+
const result2 = execute();
|
|
17
|
+
expect(await result1).toStrictEqual(2);
|
|
18
|
+
expect(await result2).toStrictEqual(2);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("uses are args equal, args are equal", async () => {
|
|
22
|
+
let counter = 0;
|
|
23
|
+
|
|
24
|
+
const execute = latest({
|
|
25
|
+
fn: async (name: string) => {
|
|
26
|
+
++counter;
|
|
27
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
28
|
+
return "hello " + name;
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
areArgsEqual: ([nameA], [nameB]) => nameA === nameB,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const result1 = execute("foo");
|
|
35
|
+
const result2 = execute("foo");
|
|
36
|
+
// vi.advanceTimersByTime(50);
|
|
37
|
+
expect(await result1).toStrictEqual("hello foo");
|
|
38
|
+
expect(await result2).toStrictEqual("hello foo");
|
|
39
|
+
// should only be called once
|
|
40
|
+
expect(counter).toEqual(1);
|
|
12
41
|
});
|
|
13
42
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
43
|
+
// this test doesn't pass with fake timers for some reason
|
|
44
|
+
test("uses are args equal, args are not equal", async () => {
|
|
45
|
+
let counter = 0;
|
|
46
|
+
|
|
47
|
+
const execute = latest({
|
|
48
|
+
fn: async (name: string) => {
|
|
49
|
+
++counter;
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
51
|
+
return "hello " + name;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
areArgsEqual: ([nameA], [nameB]) => nameA === nameB,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const result1 = execute("foo");
|
|
58
|
+
const result2 = execute("bar");
|
|
59
|
+
// returns latest - both resolved at same time
|
|
60
|
+
expect(await result1).toStrictEqual("hello bar");
|
|
61
|
+
expect(await result2).toStrictEqual("hello bar");
|
|
62
|
+
expect(counter).toEqual(2);
|
|
63
|
+
});
|
|
18
64
|
});
|
package/src/sync/latest.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AnyFn,
|
|
3
|
+
type AwaitRet,
|
|
4
|
+
makePromise,
|
|
5
|
+
type PromiseHandle,
|
|
6
|
+
} from "./util.ts";
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* An async event wrapper that always resolve to the result of the latest
|
|
@@ -26,54 +31,119 @@ import { makePromise } from "./util.ts";
|
|
|
26
31
|
* console.log(await result1); // 2
|
|
27
32
|
* console.log(await result2); // 2
|
|
28
33
|
* ```
|
|
34
|
+
*
|
|
35
|
+
* ## Advanced Usage
|
|
36
|
+
* See the constructor options for more advanced usage, for example,
|
|
37
|
+
* control how arguments are updated when new calls are made.
|
|
29
38
|
*/
|
|
30
|
-
|
|
31
|
-
export function latest<TFn extends (...args: any[]) => any>({
|
|
39
|
+
export function latest<TFn extends AnyFn>({
|
|
32
40
|
fn,
|
|
41
|
+
areArgsEqual,
|
|
42
|
+
updateArgs,
|
|
33
43
|
}: LatestConstructor<TFn>) {
|
|
34
|
-
const impl = new LatestImpl(fn);
|
|
44
|
+
const impl = new LatestImpl(fn, areArgsEqual, updateArgs);
|
|
35
45
|
return (...args: Parameters<TFn>) => impl.invoke(...args);
|
|
36
46
|
}
|
|
37
47
|
|
|
38
|
-
export type LatestConstructor<TFn> = {
|
|
48
|
+
export type LatestConstructor<TFn extends AnyFn> = {
|
|
39
49
|
/** Function to be wrapped */
|
|
40
50
|
fn: TFn;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Optional function to compare if arguments of 2 calls are equal.
|
|
54
|
+
*
|
|
55
|
+
* By default, separate calls are considered different, and the result
|
|
56
|
+
* of the latest call will be returned. However, if the function is pure,
|
|
57
|
+
* and the argument of a new call is the same as the call being executed,
|
|
58
|
+
* then the result of the call being executed will be returned. In other words,
|
|
59
|
+
* the new call will not result in another execution of the function.
|
|
60
|
+
*/
|
|
61
|
+
areArgsEqual?: (a: Parameters<TFn>, b: Parameters<TFn>) => boolean;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Optional function to update the arguments.
|
|
65
|
+
*
|
|
66
|
+
* By default, when new calls are made while the previous call is being executed,
|
|
67
|
+
* The function will be executed again with the latest arguments. This function
|
|
68
|
+
* is used to change this behavior and is called when new calls are made. In other words,
|
|
69
|
+
* the default value for this function is `(_current, _middle, latest) => latest`.
|
|
70
|
+
*
|
|
71
|
+
* The arguments are:
|
|
72
|
+
* - `current`: The arguments of the call currently being executed
|
|
73
|
+
* - `latest`: The argument of this new call
|
|
74
|
+
* - `middle`: If more than one call is made while the previous call is being executed,
|
|
75
|
+
* this array contains arguments of the calls between `current` and `latest`
|
|
76
|
+
* - `next`: This is the returned value of the previous call to updateArgs, i.e. the args
|
|
77
|
+
* to be executed next.
|
|
78
|
+
*
|
|
79
|
+
*/
|
|
80
|
+
updateArgs?: UpdateArgsFn<TFn>;
|
|
41
81
|
};
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
82
|
+
export type UpdateArgsFn<TFn extends AnyFn> = (
|
|
83
|
+
current: Parameters<TFn>,
|
|
84
|
+
middle: Parameters<TFn>[],
|
|
85
|
+
latest: Parameters<TFn>,
|
|
86
|
+
next: Parameters<TFn> | undefined,
|
|
87
|
+
) => Parameters<TFn>;
|
|
88
|
+
export class LatestImpl<TFn extends AnyFn> {
|
|
89
|
+
private pending?: PromiseHandle<AwaitRet<TFn>>;
|
|
90
|
+
|
|
91
|
+
/** current arguments. undefined means no current call */
|
|
92
|
+
private currentArgs?: Parameters<TFn>;
|
|
93
|
+
/** next arguments. undefined means no newer call */
|
|
94
|
+
private nextArgs?: Parameters<TFn>;
|
|
95
|
+
|
|
96
|
+
private middleArgs: Parameters<TFn>[];
|
|
50
97
|
|
|
51
|
-
|
|
52
|
-
|
|
98
|
+
private areArgsEqual: (a: Parameters<TFn>, b: Parameters<TFn>) => boolean;
|
|
99
|
+
private updateArgs: UpdateArgsFn<TFn>;
|
|
100
|
+
|
|
101
|
+
constructor(
|
|
102
|
+
private fn: TFn,
|
|
103
|
+
areArgsEqual?: (a: Parameters<TFn>, b: Parameters<TFn>) => boolean,
|
|
104
|
+
updateArgs?: UpdateArgsFn<TFn>,
|
|
105
|
+
) {
|
|
106
|
+
this.middleArgs = [];
|
|
107
|
+
this.areArgsEqual = areArgsEqual || (() => false);
|
|
108
|
+
this.updateArgs = updateArgs || ((_current, _middle, latest) => latest);
|
|
53
109
|
}
|
|
54
110
|
|
|
55
|
-
public async invoke(
|
|
56
|
-
...args: Parameters<TFn>
|
|
57
|
-
): Promise<Awaited<ReturnType<TFn>>> {
|
|
111
|
+
public async invoke(...args: Parameters<TFn>): Promise<AwaitRet<TFn>> {
|
|
58
112
|
if (this.pending) {
|
|
59
|
-
|
|
113
|
+
// pending means currentArgs is not undefined
|
|
114
|
+
const currentArgs = this.currentArgs as Parameters<TFn>;
|
|
115
|
+
const nextArgs = this.updateArgs(
|
|
116
|
+
currentArgs,
|
|
117
|
+
this.middleArgs,
|
|
118
|
+
args,
|
|
119
|
+
this.nextArgs,
|
|
120
|
+
);
|
|
121
|
+
if (this.areArgsEqual(nextArgs, currentArgs)) {
|
|
122
|
+
// do not schedule new call
|
|
123
|
+
this.nextArgs = undefined;
|
|
124
|
+
} else {
|
|
125
|
+
this.nextArgs = nextArgs;
|
|
126
|
+
}
|
|
60
127
|
return this.pending.promise;
|
|
61
128
|
}
|
|
62
|
-
|
|
129
|
+
|
|
130
|
+
// assign to next args to make the loop cleaner
|
|
131
|
+
this.nextArgs = args;
|
|
132
|
+
|
|
133
|
+
this.pending = makePromise();
|
|
63
134
|
let error = undefined;
|
|
64
135
|
let result;
|
|
65
|
-
while (
|
|
66
|
-
this.
|
|
136
|
+
while (this.nextArgs) {
|
|
137
|
+
this.currentArgs = this.nextArgs;
|
|
138
|
+
this.nextArgs = undefined;
|
|
67
139
|
try {
|
|
68
140
|
const fn = this.fn;
|
|
69
|
-
result = await fn(...
|
|
141
|
+
result = await fn(...this.currentArgs);
|
|
70
142
|
} catch (e) {
|
|
71
143
|
error = e;
|
|
72
144
|
}
|
|
73
|
-
if (!this.hasNewer) {
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
145
|
}
|
|
146
|
+
this.currentArgs = undefined;
|
|
77
147
|
const pending = this.pending;
|
|
78
148
|
this.pending = undefined;
|
|
79
149
|
if (error) {
|
package/src/sync/once.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { makePromise } from "./util";
|
|
1
|
+
import { type AnyFn, type AwaitRet, makePromise } from "./util.ts";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* An async event wrapper that ensures an async initialization is only ran once.
|
|
@@ -66,13 +66,9 @@ import { makePromise } from "./util";
|
|
|
66
66
|
* This is not an issue if the resource doesn't leak other resources,
|
|
67
67
|
* since it will eventually be GC'd.
|
|
68
68
|
*/
|
|
69
|
-
|
|
70
|
-
export function once<TFn extends (...args: any[]) => any>({
|
|
71
|
-
fn,
|
|
72
|
-
}: OnceConstructor<TFn>) {
|
|
69
|
+
export function once<TFn extends AnyFn>({ fn }: OnceConstructor<TFn>) {
|
|
73
70
|
const impl = new OnceImpl(fn);
|
|
74
|
-
return (...args: Parameters<TFn>)
|
|
75
|
-
impl.invoke(...args);
|
|
71
|
+
return (...args: Parameters<TFn>) => impl.invoke(...args);
|
|
76
72
|
}
|
|
77
73
|
|
|
78
74
|
export type OnceConstructor<TFn> = {
|
|
@@ -80,22 +76,18 @@ export type OnceConstructor<TFn> = {
|
|
|
80
76
|
fn: TFn;
|
|
81
77
|
};
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
private promise: Promise<Awaited<ReturnType<TFn>>> | undefined;
|
|
79
|
+
export class OnceImpl<TFn extends AnyFn> {
|
|
80
|
+
private promise: Promise<AwaitRet<TFn>> | undefined;
|
|
86
81
|
|
|
87
82
|
constructor(private fn: TFn) {
|
|
88
83
|
this.fn = fn;
|
|
89
84
|
}
|
|
90
85
|
|
|
91
|
-
public async invoke(
|
|
92
|
-
...args: Parameters<TFn>
|
|
93
|
-
): Promise<Awaited<ReturnType<TFn>>> {
|
|
86
|
+
public async invoke(...args: Parameters<TFn>): Promise<AwaitRet<TFn>> {
|
|
94
87
|
if (this.promise) {
|
|
95
88
|
return this.promise;
|
|
96
89
|
}
|
|
97
|
-
const { promise, resolve, reject } =
|
|
98
|
-
makePromise<Awaited<ReturnType<TFn>>>();
|
|
90
|
+
const { promise, resolve, reject } = makePromise<AwaitRet<TFn>>();
|
|
99
91
|
this.promise = promise;
|
|
100
92
|
try {
|
|
101
93
|
const result = await this.fn(...args);
|
package/src/sync/serial.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Result } from "../result/index.ts";
|
|
2
|
+
import type { AnyFn } from "./util.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* An async event wrapper that is cancelled when a new one starts.
|
|
@@ -132,8 +133,7 @@ import type { Result } from "../result/index.ts";
|
|
|
132
133
|
* If the underlying function throws, the exception will be re-thrown to the caller.
|
|
133
134
|
*/
|
|
134
135
|
|
|
135
|
-
|
|
136
|
-
export function serial<TFn extends (...args: any[]) => any>({
|
|
136
|
+
export function serial<TFn extends AnyFn>({
|
|
137
137
|
fn,
|
|
138
138
|
onCancel,
|
|
139
139
|
}: SerialConstructor<TFn>) {
|
|
@@ -157,8 +157,7 @@ export type SerialConstructor<TFn> = {
|
|
|
157
157
|
onCancel?: SerialEventCancelCallback;
|
|
158
158
|
};
|
|
159
159
|
|
|
160
|
-
|
|
161
|
-
class SerialImpl<TFn extends (...args: any[]) => any> {
|
|
160
|
+
class SerialImpl<TFn extends AnyFn> {
|
|
162
161
|
private serial: SerialId;
|
|
163
162
|
private fn: SerialFnCreator<TFn>;
|
|
164
163
|
private onCancel: SerialEventCancelCallback;
|
package/src/sync/util.ts
CHANGED
|
@@ -21,8 +21,14 @@ export const makePromise = <T>(): {
|
|
|
21
21
|
};
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
export type PromiseHandle<T> = ReturnType<typeof makePromise<T>>;
|
|
25
|
+
|
|
24
26
|
/** Shorthand for Awaited<ReturnType<T>> */
|
|
25
27
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
28
|
export type AwaitRet<T> = T extends (...args: any[]) => infer R
|
|
27
29
|
? Awaited<R>
|
|
28
30
|
: never;
|
|
31
|
+
|
|
32
|
+
/** Type for any function */
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
+
export type AnyFn = (...args: any[]) => any;
|
|
File without changes
|
|
File without changes
|