@mengine/utils 0.1.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/dist/index.d.ts +140 -0
- package/dist/index.js +251 -0
- package/package.json +37 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
//#region src/disposeable.d.ts
|
|
2
|
+
declare abstract class Disposable {
|
|
3
|
+
abstract dispose(): void;
|
|
4
|
+
[Symbol.dispose](): void;
|
|
5
|
+
}
|
|
6
|
+
declare class FnDisposable extends Disposable {
|
|
7
|
+
private teardown;
|
|
8
|
+
constructor(teardown: () => void);
|
|
9
|
+
dispose(): void;
|
|
10
|
+
}
|
|
11
|
+
declare class DisposableSet extends Disposable {
|
|
12
|
+
private disposables;
|
|
13
|
+
dispose(): void;
|
|
14
|
+
add<T extends () => void>(teardown: T): void;
|
|
15
|
+
add<T extends Disposable>(teardown: T): void;
|
|
16
|
+
delete<T extends () => void>(teardown: T): void;
|
|
17
|
+
delete<T extends Disposable>(teardown: T): void;
|
|
18
|
+
clear(): void;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/event.d.ts
|
|
22
|
+
interface EventSchema {
|
|
23
|
+
[key: string]: [any, any?];
|
|
24
|
+
}
|
|
25
|
+
type KeyToKey<T extends EventSchema> = { [K in keyof T]: string extends K ? never : K };
|
|
26
|
+
declare type ValuesOf<T> = T extends { [K in keyof T]: infer _U } ? _U : never;
|
|
27
|
+
type EventNames<T extends EventSchema> = ValuesOf<KeyToKey<T>>;
|
|
28
|
+
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
29
|
+
type RequiredInput<In> = IsAny<In> extends true ? [In] : [In] extends [never] ? [] : [In] extends [void] ? [] : [In];
|
|
30
|
+
type EventInput<Events extends EventSchema, Type extends EventNames<Events>> = Type extends keyof Events ? Events[Type] extends [infer In] ? RequiredInput<In> : Events[Type] extends [infer In, infer _Out] ? RequiredInput<In> : never : never;
|
|
31
|
+
declare class EventBus<Events extends EventSchema> {
|
|
32
|
+
private listeners;
|
|
33
|
+
on<EventName extends EventNames<Events>>(eventName: EventName, callback: (...input: EventInput<Events, EventName>) => void): () => void;
|
|
34
|
+
off<EventName extends EventNames<Events>>(eventName: EventName, callback: (...input: EventInput<Events, EventName>) => void): void;
|
|
35
|
+
emit<EventName extends EventNames<Events>>(eventName: EventName, ...input: EventInput<Events, EventName>): void;
|
|
36
|
+
clear<EventName extends EventNames<Events>>(eventName: EventName): void;
|
|
37
|
+
clearAll(): void;
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/loop.d.ts
|
|
41
|
+
/**
|
|
42
|
+
* 运行一段异步任务,并在其完成后检查取消点:即使任务本身没有响应取消,
|
|
43
|
+
* 只要 `signal` 在任务返回时已 abort,就抛出 `signal.reason` 而不是返回结果——
|
|
44
|
+
* 避免把 abort 之后的结果泄漏给调用方。
|
|
45
|
+
*
|
|
46
|
+
* 取代原先的 `@checkpoint()` 方法装饰器:显式传入 signal,不再靠“参数位”约定,
|
|
47
|
+
* 也就不需要在方法上保留只为定位 signal 的未用参数。
|
|
48
|
+
*/
|
|
49
|
+
declare function runWithCheckpoint<T>(signal: AbortSignal, task: () => Promise<T>): Promise<T>;
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/promise.d.ts
|
|
52
|
+
declare function timeout<T>(inner: Promise<T>, timeout: number): Promise<T>;
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/task.d.ts
|
|
55
|
+
declare const MANUALLY_STOP = "Manually stopped";
|
|
56
|
+
/**
|
|
57
|
+
* Runtime scope owned by a single Task instance.
|
|
58
|
+
*
|
|
59
|
+
* The signal is task-owned: callers should proxy outside signals with
|
|
60
|
+
* `task.abortOn(signal)` instead of wiring them into executors by hand.
|
|
61
|
+
* Disposers run once when the task settles by resolve, reject, cancel,
|
|
62
|
+
* timeout, or sidecar abort.
|
|
63
|
+
*/
|
|
64
|
+
interface TaskScope {
|
|
65
|
+
readonly signal: AbortSignal;
|
|
66
|
+
readonly disposer: {
|
|
67
|
+
add(dispose: () => void): void;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
type TaskExecutor<T> = (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void, scope: TaskScope) => void;
|
|
71
|
+
/**
|
|
72
|
+
* Promise with task-owned cancellation and cleanup.
|
|
73
|
+
*
|
|
74
|
+
* `Task` keeps cancellation control inside its own AbortController. Outside
|
|
75
|
+
* abort signals are sidecars that can only proxy into this controller. Task
|
|
76
|
+
* combinators build a cancellation tree, so aborting a parent cancels child
|
|
77
|
+
* tasks without leaking parent signal wiring into child executors.
|
|
78
|
+
*/
|
|
79
|
+
declare class Task<T> extends Promise<T> {
|
|
80
|
+
private abortController;
|
|
81
|
+
/**
|
|
82
|
+
* Keep native Promise chaining from constructing Task without an executor.
|
|
83
|
+
* */
|
|
84
|
+
static get [Symbol.species](): PromiseConstructor;
|
|
85
|
+
/**
|
|
86
|
+
* Create a cancellable promise.
|
|
87
|
+
*
|
|
88
|
+
* Use `scope.signal` when an underlying API accepts AbortSignal. Use
|
|
89
|
+
* `scope.disposer.add()` for listeners, timers, subscriptions, and other
|
|
90
|
+
* cleanup that must run when the task settles.
|
|
91
|
+
*/
|
|
92
|
+
constructor(executor: TaskExecutor<T>);
|
|
93
|
+
/**
|
|
94
|
+
* Cancel this task with a caller-provided reason, or MANUALLY_STOP.
|
|
95
|
+
* */
|
|
96
|
+
cancel(reason?: any): void;
|
|
97
|
+
/**
|
|
98
|
+
* Cancel this task when the timeout elapses.
|
|
99
|
+
*
|
|
100
|
+
* The timer is cleared on any settlement, and timeout cancellation follows
|
|
101
|
+
* the same internal abort path as `cancel()` and `abortOn()`.
|
|
102
|
+
*/
|
|
103
|
+
timeout(timeoutMs: number): Task<T>;
|
|
104
|
+
/**
|
|
105
|
+
* Proxy an outside AbortSignal into this task's internal controller.
|
|
106
|
+
*
|
|
107
|
+
* This does not abort the outside signal. The listener is removed when this
|
|
108
|
+
* task settles, and an already-aborted signal cancels the task immediately.
|
|
109
|
+
*/
|
|
110
|
+
abortOn(signal: AbortSignal): Task<T>;
|
|
111
|
+
/**
|
|
112
|
+
* Run an executor that returns or throws instead of calling resolve/reject.
|
|
113
|
+
*
|
|
114
|
+
* The provided scope is the same task-owned scope used by the constructor,
|
|
115
|
+
* so simple tasks can still register disposers without manual promise wiring.
|
|
116
|
+
*/
|
|
117
|
+
static spawn<T>(executor: (scope: TaskScope) => T | PromiseLike<T>): Task<T>;
|
|
118
|
+
/**
|
|
119
|
+
* Resolve after `timeoutMs` unless cancelled first.
|
|
120
|
+
*
|
|
121
|
+
* The timer is registered as a disposer, so cancellation clears it through
|
|
122
|
+
* normal task cleanup.
|
|
123
|
+
*/
|
|
124
|
+
static delay(timeoutMs: number): Task<void>;
|
|
125
|
+
/**
|
|
126
|
+
* Promise.all with Task cancellation semantics.
|
|
127
|
+
*
|
|
128
|
+
* Parent cancellation cancels child Task instances. If one child rejects,
|
|
129
|
+
* pending child Task instances are cancelled with the same reason. Plain
|
|
130
|
+
* promises are awaited but cannot be cancelled.
|
|
131
|
+
*/
|
|
132
|
+
static all<T extends readonly unknown[] | []>(values: T): Task<{ -readonly [P in keyof T]: Awaited<T[P]> }>;
|
|
133
|
+
static all<T>(values: Iterable<T | PromiseLike<T>>): Task<Awaited<T>[]>;
|
|
134
|
+
/**
|
|
135
|
+
* Raise an error on the task's internal controller.
|
|
136
|
+
*/
|
|
137
|
+
private raise;
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
export { Disposable, DisposableSet, EventBus, EventInput, EventNames, EventSchema, FnDisposable, MANUALLY_STOP, Task, TaskScope, runWithCheckpoint, timeout };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
//#region src/disposeable.ts
|
|
2
|
+
var Disposable = class {
|
|
3
|
+
[Symbol.dispose]() {
|
|
4
|
+
this.dispose();
|
|
5
|
+
}
|
|
6
|
+
};
|
|
7
|
+
var FnDisposable = class extends Disposable {
|
|
8
|
+
teardown;
|
|
9
|
+
constructor(teardown) {
|
|
10
|
+
super();
|
|
11
|
+
this.teardown = teardown;
|
|
12
|
+
}
|
|
13
|
+
dispose() {
|
|
14
|
+
this.teardown();
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var DisposableSet = class extends Disposable {
|
|
18
|
+
disposables = /* @__PURE__ */ new Set();
|
|
19
|
+
dispose() {
|
|
20
|
+
for (const disposable of this.disposables) disposable.dispose();
|
|
21
|
+
this.disposables.clear();
|
|
22
|
+
}
|
|
23
|
+
add(teardown) {
|
|
24
|
+
if (teardown instanceof Disposable) this.disposables.add(teardown);
|
|
25
|
+
else this.disposables.add(new FnDisposable(teardown));
|
|
26
|
+
}
|
|
27
|
+
delete(teardown) {
|
|
28
|
+
if (teardown instanceof Disposable) this.disposables.delete(teardown);
|
|
29
|
+
}
|
|
30
|
+
clear() {
|
|
31
|
+
this.disposables.clear();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/event.ts
|
|
36
|
+
var EventBus = class {
|
|
37
|
+
listeners = /* @__PURE__ */ new Map();
|
|
38
|
+
on(eventName, callback) {
|
|
39
|
+
let listeners = this.listeners.get(eventName);
|
|
40
|
+
if (!listeners) {
|
|
41
|
+
listeners = /* @__PURE__ */ new Set();
|
|
42
|
+
this.listeners.set(eventName, listeners);
|
|
43
|
+
}
|
|
44
|
+
listeners.add(callback);
|
|
45
|
+
return () => {
|
|
46
|
+
listeners?.delete(callback);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
off(eventName, callback) {
|
|
50
|
+
this.listeners.get(eventName)?.delete(callback);
|
|
51
|
+
}
|
|
52
|
+
emit(eventName, ...input) {
|
|
53
|
+
this.listeners.get(eventName)?.forEach((callback) => callback(...input));
|
|
54
|
+
}
|
|
55
|
+
clear(eventName) {
|
|
56
|
+
this.listeners.delete(eventName);
|
|
57
|
+
}
|
|
58
|
+
clearAll() {
|
|
59
|
+
this.listeners.clear();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/loop.ts
|
|
64
|
+
/**
|
|
65
|
+
* 运行一段异步任务,并在其完成后检查取消点:即使任务本身没有响应取消,
|
|
66
|
+
* 只要 `signal` 在任务返回时已 abort,就抛出 `signal.reason` 而不是返回结果——
|
|
67
|
+
* 避免把 abort 之后的结果泄漏给调用方。
|
|
68
|
+
*
|
|
69
|
+
* 取代原先的 `@checkpoint()` 方法装饰器:显式传入 signal,不再靠“参数位”约定,
|
|
70
|
+
* 也就不需要在方法上保留只为定位 signal 的未用参数。
|
|
71
|
+
*/
|
|
72
|
+
async function runWithCheckpoint(signal, task) {
|
|
73
|
+
const result = await task();
|
|
74
|
+
if (signal.aborted) throw signal.reason;
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/promise.ts
|
|
79
|
+
async function timeout(inner, timeout) {
|
|
80
|
+
const timer = Promise.withResolvers();
|
|
81
|
+
const timerHandler = setTimeout(() => {
|
|
82
|
+
timer.reject(/* @__PURE__ */ new Error(`Timeout after ${timeout}ms`));
|
|
83
|
+
}, timeout);
|
|
84
|
+
return Promise.race([inner, timer.promise]).finally(() => {
|
|
85
|
+
clearTimeout(timerHandler);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/task.ts
|
|
90
|
+
const MANUALLY_STOP = "Manually stopped";
|
|
91
|
+
/**
|
|
92
|
+
* Promise with task-owned cancellation and cleanup.
|
|
93
|
+
*
|
|
94
|
+
* `Task` keeps cancellation control inside its own AbortController. Outside
|
|
95
|
+
* abort signals are sidecars that can only proxy into this controller. Task
|
|
96
|
+
* combinators build a cancellation tree, so aborting a parent cancels child
|
|
97
|
+
* tasks without leaking parent signal wiring into child executors.
|
|
98
|
+
*/
|
|
99
|
+
var Task = class Task extends Promise {
|
|
100
|
+
abortController;
|
|
101
|
+
/**
|
|
102
|
+
* Keep native Promise chaining from constructing Task without an executor.
|
|
103
|
+
* */
|
|
104
|
+
static get [Symbol.species]() {
|
|
105
|
+
return Promise;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Create a cancellable promise.
|
|
109
|
+
*
|
|
110
|
+
* Use `scope.signal` when an underlying API accepts AbortSignal. Use
|
|
111
|
+
* `scope.disposer.add()` for listeners, timers, subscriptions, and other
|
|
112
|
+
* cleanup that must run when the task settles.
|
|
113
|
+
*/
|
|
114
|
+
constructor(executor) {
|
|
115
|
+
const abortController = new AbortController();
|
|
116
|
+
const disposes = /* @__PURE__ */ new Set();
|
|
117
|
+
let cleanedUp = false;
|
|
118
|
+
const runDispose = (dispose) => {
|
|
119
|
+
try {
|
|
120
|
+
dispose();
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error("Task disposer failed", error);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const cleanup = () => {
|
|
126
|
+
if (cleanedUp) return;
|
|
127
|
+
cleanedUp = true;
|
|
128
|
+
for (const dispose of disposes) runDispose(dispose);
|
|
129
|
+
disposes.clear();
|
|
130
|
+
};
|
|
131
|
+
const scope = {
|
|
132
|
+
signal: abortController.signal,
|
|
133
|
+
disposer: { add(dispose) {
|
|
134
|
+
if (cleanedUp) {
|
|
135
|
+
runDispose(dispose);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
disposes.add(dispose);
|
|
139
|
+
} }
|
|
140
|
+
};
|
|
141
|
+
super((resolve, reject) => {
|
|
142
|
+
abortController.signal.addEventListener("abort", () => {
|
|
143
|
+
reject(abortController.signal.reason);
|
|
144
|
+
});
|
|
145
|
+
executor(resolve, reject, scope);
|
|
146
|
+
});
|
|
147
|
+
this.abortController = abortController;
|
|
148
|
+
this.then(cleanup, cleanup);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Cancel this task with a caller-provided reason, or MANUALLY_STOP.
|
|
152
|
+
* */
|
|
153
|
+
cancel(reason) {
|
|
154
|
+
this.raise(reason ?? "Manually stopped");
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Cancel this task when the timeout elapses.
|
|
158
|
+
*
|
|
159
|
+
* The timer is cleared on any settlement, and timeout cancellation follows
|
|
160
|
+
* the same internal abort path as `cancel()` and `abortOn()`.
|
|
161
|
+
*/
|
|
162
|
+
timeout(timeoutMs) {
|
|
163
|
+
const timer = setTimeout(() => {
|
|
164
|
+
this.raise(/* @__PURE__ */ new Error(`Timeout after ${timeoutMs} ms`));
|
|
165
|
+
}, timeoutMs);
|
|
166
|
+
const clear = () => {
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
};
|
|
169
|
+
this.then(clear, clear);
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Proxy an outside AbortSignal into this task's internal controller.
|
|
174
|
+
*
|
|
175
|
+
* This does not abort the outside signal. The listener is removed when this
|
|
176
|
+
* task settles, and an already-aborted signal cancels the task immediately.
|
|
177
|
+
*/
|
|
178
|
+
abortOn(signal) {
|
|
179
|
+
if (signal.aborted) {
|
|
180
|
+
this.raise(signal.reason);
|
|
181
|
+
return this;
|
|
182
|
+
}
|
|
183
|
+
const abort = () => {
|
|
184
|
+
this.raise(signal.reason);
|
|
185
|
+
};
|
|
186
|
+
const cleanup = () => {
|
|
187
|
+
signal.removeEventListener("abort", abort);
|
|
188
|
+
};
|
|
189
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
190
|
+
this.then(cleanup, cleanup);
|
|
191
|
+
return this;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Run an executor that returns or throws instead of calling resolve/reject.
|
|
195
|
+
*
|
|
196
|
+
* The provided scope is the same task-owned scope used by the constructor,
|
|
197
|
+
* so simple tasks can still register disposers without manual promise wiring.
|
|
198
|
+
*/
|
|
199
|
+
static spawn(executor) {
|
|
200
|
+
return new Task((resolve, reject, scope) => {
|
|
201
|
+
try {
|
|
202
|
+
const result = executor(scope);
|
|
203
|
+
Promise.resolve(result).then(resolve, reject);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
reject(error);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Resolve after `timeoutMs` unless cancelled first.
|
|
211
|
+
*
|
|
212
|
+
* The timer is registered as a disposer, so cancellation clears it through
|
|
213
|
+
* normal task cleanup.
|
|
214
|
+
*/
|
|
215
|
+
static delay(timeoutMs) {
|
|
216
|
+
return new Task((resolve, _reject, scope) => {
|
|
217
|
+
const timer = setTimeout(resolve, timeoutMs);
|
|
218
|
+
scope.disposer.add(() => {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
static all(values) {
|
|
224
|
+
const items = Array.from(values);
|
|
225
|
+
return new Task((resolve, reject, scope) => {
|
|
226
|
+
const cancelItems = (reason) => {
|
|
227
|
+
for (const item of items) if (item instanceof Task) item.cancel(reason);
|
|
228
|
+
};
|
|
229
|
+
const abortChildren = () => {
|
|
230
|
+
cancelItems(scope.signal.reason);
|
|
231
|
+
};
|
|
232
|
+
scope.signal.addEventListener("abort", abortChildren, { once: true });
|
|
233
|
+
scope.disposer.add(() => {
|
|
234
|
+
scope.signal.removeEventListener("abort", abortChildren);
|
|
235
|
+
});
|
|
236
|
+
Promise.all(items).then(resolve, (reason) => {
|
|
237
|
+
cancelItems(reason);
|
|
238
|
+
reject(reason);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Raise an error on the task's internal controller.
|
|
244
|
+
*/
|
|
245
|
+
raise(reason) {
|
|
246
|
+
if (this.abortController.signal.aborted) return;
|
|
247
|
+
this.abortController.abort(reason);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
//#endregion
|
|
251
|
+
export { Disposable, DisposableSet, EventBus, FnDisposable, MANUALLY_STOP, Task, runWithCheckpoint, timeout };
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mengine/utils",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "UNLICENSED",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/one2x-ai/medeo-engine.git",
|
|
8
|
+
"directory": "packages/utils"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./src/index.ts",
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./dist/index.js",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"access": "public",
|
|
24
|
+
"registry": "https://registry.npmjs.org/"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "vp pack",
|
|
28
|
+
"dev": "vp pack --watch",
|
|
29
|
+
"test": "vp test",
|
|
30
|
+
"prepublishOnly": "vp pack"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@typescript/native-preview": "catalog:",
|
|
34
|
+
"typescript": "catalog:",
|
|
35
|
+
"vite-plus": "catalog:"
|
|
36
|
+
}
|
|
37
|
+
}
|