@praxisjs/concurrent 1.1.1 → 1.2.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/CHANGELOG.md +30 -0
- package/dist/__tests__/decorators.test.js +85 -108
- package/dist/__tests__/decorators.test.js.map +1 -1
- package/dist/decorators.d.ts +27 -3
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +34 -34
- package/dist/decorators.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/decorators.test.ts +112 -130
- package/src/decorators.ts +81 -37
- package/src/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @praxisjs/concurrent
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 21f2053: Redesign `@Task`, `@Queue`, and `@Pool` as field decorators
|
|
8
|
+
|
|
9
|
+
The decorators now go on a separate field instead of the async method itself. The method name is always the first argument, followed by options. Reactive state is accessed as sub-properties on the field with full TypeScript intellisense via `TaskOf`, `QueueOf`, and `PoolOf` type helpers.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// Before
|
|
13
|
+
@Task()
|
|
14
|
+
async loadUser(id: number) { ... }
|
|
15
|
+
// this.loadUser_loading() — no intellisense
|
|
16
|
+
|
|
17
|
+
// After
|
|
18
|
+
async loadUser(id: number) { ... }
|
|
19
|
+
|
|
20
|
+
@Task('loadUser')
|
|
21
|
+
taskLoadUser!: TaskOf<MyClass, 'loadUser'>
|
|
22
|
+
// this.taskLoadUser.loading() ✓
|
|
23
|
+
// this.taskLoadUser.error() ✓
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`@Pool` argument order changed: method name is now first, concurrency second (previously `@Pool(3, 'method')`, now `@Pool('method', 3)`).
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- Updated dependencies [2b8c768]
|
|
31
|
+
- @praxisjs/decorators@0.7.0
|
|
32
|
+
|
|
3
33
|
## 1.1.1
|
|
4
34
|
|
|
5
35
|
### Patch Changes
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { Task, Queue, Pool } from "../decorators";
|
|
3
|
-
function
|
|
3
|
+
function fieldCtx(name) {
|
|
4
4
|
const initializers = [];
|
|
5
5
|
return {
|
|
6
6
|
ctx: {
|
|
7
7
|
name,
|
|
8
|
-
kind: "
|
|
8
|
+
kind: "field",
|
|
9
9
|
addInitializer(fn) { initializers.push(fn); },
|
|
10
10
|
},
|
|
11
11
|
run(instance) { initializers.forEach((fn) => { fn.call(instance); }); },
|
|
@@ -13,149 +13,126 @@ function methodCtx(name) {
|
|
|
13
13
|
}
|
|
14
14
|
// ── Task ──────────────────────────────────────────────────────────────────────
|
|
15
15
|
describe("Task decorator", () => {
|
|
16
|
-
it("
|
|
17
|
-
const { ctx, run } =
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
it("wraps the named method and exposes signals as sub-properties", async () => {
|
|
17
|
+
const { ctx, run } = fieldCtx("taskLoad");
|
|
18
|
+
Task("load")(undefined, ctx);
|
|
19
|
+
const instance = {
|
|
20
|
+
load: async (x) => x * 2,
|
|
21
|
+
};
|
|
21
22
|
run(instance);
|
|
22
|
-
|
|
23
|
-
expect(
|
|
24
|
-
expect(
|
|
25
|
-
expect(
|
|
26
|
-
|
|
23
|
+
const taskLoad = instance.taskLoad;
|
|
24
|
+
expect(typeof taskLoad).toBe("function");
|
|
25
|
+
expect(taskLoad.loading).toBeDefined();
|
|
26
|
+
expect(taskLoad.error).toBeDefined();
|
|
27
|
+
expect(taskLoad.lastResult).toBeDefined();
|
|
28
|
+
const result = await taskLoad(5);
|
|
27
29
|
expect(result).toBe(10);
|
|
28
|
-
expect(
|
|
30
|
+
expect(taskLoad.lastResult()).toBe(10);
|
|
29
31
|
});
|
|
30
|
-
it("sets loading
|
|
31
|
-
const { ctx, run } =
|
|
32
|
+
it("sets loading while running", async () => {
|
|
33
|
+
const { ctx, run } = fieldCtx("taskFetch");
|
|
34
|
+
Task("fetch")(undefined, ctx);
|
|
32
35
|
let resolve;
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
const instance = {
|
|
37
|
+
fetch: async () => new Promise((r) => { resolve = r; }),
|
|
38
|
+
};
|
|
36
39
|
run(instance);
|
|
37
|
-
const
|
|
38
|
-
|
|
40
|
+
const taskFetch = instance.taskFetch;
|
|
41
|
+
const p = taskFetch();
|
|
42
|
+
expect(taskFetch.loading()).toBe(true);
|
|
39
43
|
resolve("done");
|
|
40
44
|
await p;
|
|
41
|
-
expect(
|
|
45
|
+
expect(taskFetch.loading()).toBe(false);
|
|
42
46
|
});
|
|
43
47
|
});
|
|
44
48
|
// ── Queue ─────────────────────────────────────────────────────────────────────
|
|
45
49
|
describe("Queue decorator", () => {
|
|
46
|
-
it("
|
|
47
|
-
const { ctx, run } =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
it("wraps the named method and exposes signals", async () => {
|
|
51
|
+
const { ctx, run } = fieldCtx("taskSave");
|
|
52
|
+
Queue("save")(undefined, ctx);
|
|
53
|
+
const instance = {
|
|
54
|
+
save: async (x) => String(x),
|
|
55
|
+
};
|
|
51
56
|
run(instance);
|
|
52
|
-
|
|
53
|
-
expect(
|
|
54
|
-
expect(
|
|
55
|
-
expect(
|
|
56
|
-
|
|
57
|
+
const taskSave = instance.taskSave;
|
|
58
|
+
expect(typeof taskSave).toBe("function");
|
|
59
|
+
expect(taskSave.loading).toBeDefined();
|
|
60
|
+
expect(taskSave.pending).toBeDefined();
|
|
61
|
+
expect(taskSave.error).toBeDefined();
|
|
62
|
+
expect(taskSave.clear).toBeDefined();
|
|
63
|
+
const result = await taskSave("hello");
|
|
57
64
|
expect(result).toBe("hello");
|
|
58
65
|
});
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
it("clear() is accessible on the instance via method_clear property", async () => {
|
|
63
|
-
const { ctx, run } = methodCtx("save");
|
|
66
|
+
it("clear() rejects queued calls with QueueClearedError", async () => {
|
|
67
|
+
const { ctx, run } = fieldCtx("taskSave");
|
|
68
|
+
Queue("save")(undefined, ctx);
|
|
64
69
|
let resolveFirst;
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
const instance = {
|
|
71
|
+
save: async (_, idx) => idx === 0
|
|
72
|
+
? new Promise((r) => { resolveFirst = r; })
|
|
73
|
+
: Promise.resolve(),
|
|
74
|
+
};
|
|
70
75
|
run(instance);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// Call clear via the exposed property
|
|
76
|
-
instance.save_clear();
|
|
76
|
+
const taskSave = instance.taskSave;
|
|
77
|
+
taskSave(null, 0);
|
|
78
|
+
const p1 = taskSave(null, 1);
|
|
79
|
+
taskSave.clear();
|
|
77
80
|
await expect(p1).rejects.toThrow("Queue cleared");
|
|
78
81
|
resolveFirst();
|
|
79
82
|
});
|
|
80
83
|
});
|
|
81
84
|
// ── Pool ──────────────────────────────────────────────────────────────────────
|
|
82
85
|
describe("Pool decorator", () => {
|
|
83
|
-
it("
|
|
84
|
-
const { ctx, run } =
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
it("wraps the named method and exposes signals", async () => {
|
|
87
|
+
const { ctx, run } = fieldCtx("taskProcess");
|
|
88
|
+
Pool("process", 2)(undefined, ctx);
|
|
89
|
+
const instance = {
|
|
90
|
+
process: async (x) => x + 1,
|
|
91
|
+
};
|
|
88
92
|
run(instance);
|
|
89
|
-
|
|
90
|
-
expect(
|
|
91
|
-
expect(
|
|
92
|
-
expect(
|
|
93
|
-
expect(
|
|
94
|
-
|
|
93
|
+
const taskProcess = instance.taskProcess;
|
|
94
|
+
expect(typeof taskProcess).toBe("function");
|
|
95
|
+
expect(taskProcess.loading).toBeDefined();
|
|
96
|
+
expect(taskProcess.active).toBeDefined();
|
|
97
|
+
expect(taskProcess.pending).toBeDefined();
|
|
98
|
+
expect(taskProcess.error).toBeDefined();
|
|
99
|
+
const result = await taskProcess(9);
|
|
95
100
|
expect(result).toBe(10);
|
|
96
101
|
});
|
|
97
102
|
it("respects concurrency limit", async () => {
|
|
98
|
-
const { ctx, run } =
|
|
103
|
+
const { ctx, run } = fieldCtx("taskWork");
|
|
104
|
+
Pool("work", 2)(undefined, ctx);
|
|
99
105
|
const resolvers = [];
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
const instance = {
|
|
107
|
+
work: async () => new Promise((r) => resolvers.push(r)),
|
|
108
|
+
};
|
|
103
109
|
run(instance);
|
|
104
|
-
const
|
|
105
|
-
const t1 =
|
|
106
|
-
const t2 =
|
|
107
|
-
|
|
108
|
-
expect(
|
|
109
|
-
expect(
|
|
110
|
+
const taskWork = instance.taskWork;
|
|
111
|
+
const t1 = taskWork();
|
|
112
|
+
const t2 = taskWork();
|
|
113
|
+
taskWork();
|
|
114
|
+
expect(taskWork.active()).toBe(2);
|
|
115
|
+
expect(taskWork.pending()).toBe(1);
|
|
110
116
|
resolvers.forEach((r) => { r(); });
|
|
111
117
|
await Promise.all([t1, t2]);
|
|
112
118
|
});
|
|
113
|
-
it("
|
|
114
|
-
const { ctx, run } =
|
|
119
|
+
it("defaults concurrency to 1", async () => {
|
|
120
|
+
const { ctx, run } = fieldCtx("taskWork");
|
|
121
|
+
Pool("work")(undefined, ctx);
|
|
115
122
|
const resolvers = [];
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
123
|
+
const instance = {
|
|
124
|
+
work: async () => new Promise((r) => resolvers.push(r)),
|
|
125
|
+
};
|
|
119
126
|
run(instance);
|
|
120
|
-
const
|
|
121
|
-
const t1 =
|
|
122
|
-
const t2 =
|
|
123
|
-
|
|
124
|
-
expect(
|
|
125
|
-
expect(instance.work_pending()).toBe(1);
|
|
127
|
+
const taskWork = instance.taskWork;
|
|
128
|
+
const t1 = taskWork();
|
|
129
|
+
const t2 = taskWork();
|
|
130
|
+
expect(taskWork.active()).toBe(1);
|
|
131
|
+
expect(taskWork.pending()).toBe(1);
|
|
126
132
|
resolvers[0]();
|
|
127
133
|
await t1;
|
|
128
134
|
resolvers[1]();
|
|
129
135
|
await t2;
|
|
130
136
|
});
|
|
131
|
-
it("decorated method called concurrently up to pool limit — active() signal is accurate", async () => {
|
|
132
|
-
const { ctx, run } = methodCtx("process");
|
|
133
|
-
const resolvers = [];
|
|
134
|
-
const original = async () => new Promise((r) => resolvers.push(r));
|
|
135
|
-
Pool(3)(original, ctx);
|
|
136
|
-
const instance = {};
|
|
137
|
-
run(instance);
|
|
138
|
-
const process = instance.process;
|
|
139
|
-
const active = instance.process_active;
|
|
140
|
-
const t1 = process();
|
|
141
|
-
expect(active()).toBe(1);
|
|
142
|
-
const t2 = process();
|
|
143
|
-
expect(active()).toBe(2);
|
|
144
|
-
const t3 = process();
|
|
145
|
-
expect(active()).toBe(3);
|
|
146
|
-
// 4th task exceeds limit, goes to pending
|
|
147
|
-
const t4 = process();
|
|
148
|
-
expect(active()).toBe(3);
|
|
149
|
-
expect(instance.process_pending()).toBe(1);
|
|
150
|
-
// Resolve t1 first — this will allow t4 to start and push its resolver
|
|
151
|
-
resolvers[0]();
|
|
152
|
-
await t1;
|
|
153
|
-
// Now t4 has started and pushed a resolver
|
|
154
|
-
resolvers[1]();
|
|
155
|
-
resolvers[2]();
|
|
156
|
-
resolvers[3]();
|
|
157
|
-
await Promise.all([t2, t3, t4]);
|
|
158
|
-
expect(active()).toBe(0);
|
|
159
|
-
});
|
|
160
137
|
});
|
|
161
138
|
//# sourceMappingURL=decorators.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decorators.test.js","sourceRoot":"","sources":["../../src/__tests__/decorators.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAElD,SAAS,
|
|
1
|
+
{"version":3,"file":"decorators.test.js","sourceRoot":"","sources":["../../src/__tests__/decorators.test.ts"],"names":[],"mappings":"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"}
|
package/dist/decorators.d.ts
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export
|
|
1
|
+
import type { Computed } from "@praxisjs/shared";
|
|
2
|
+
type AnyAsyncFn = (this: any, ...args: any[]) => Promise<any>;
|
|
3
|
+
export type TaskDecorated<T extends AnyAsyncFn> = ((...args: Parameters<T>) => ReturnType<T>) & {
|
|
4
|
+
loading: Computed<boolean>;
|
|
5
|
+
error: Computed<Error | null>;
|
|
6
|
+
lastResult: Computed<Awaited<ReturnType<T>> | null>;
|
|
7
|
+
cancelAll(): void;
|
|
8
|
+
};
|
|
9
|
+
export type QueueDecorated<T extends AnyAsyncFn> = ((...args: Parameters<T>) => ReturnType<T>) & {
|
|
10
|
+
loading: Computed<boolean>;
|
|
11
|
+
pending: Computed<number>;
|
|
12
|
+
error: Computed<Error | null>;
|
|
13
|
+
clear(): void;
|
|
14
|
+
};
|
|
15
|
+
export type PoolDecorated<T extends AnyAsyncFn> = ((...args: Parameters<T>) => ReturnType<T>) & {
|
|
16
|
+
loading: Computed<boolean>;
|
|
17
|
+
active: Computed<number>;
|
|
18
|
+
pending: Computed<number>;
|
|
19
|
+
error: Computed<Error | null>;
|
|
20
|
+
};
|
|
21
|
+
export type TaskOf<C, K extends keyof C> = C[K] extends AnyAsyncFn ? TaskDecorated<C[K]> : never;
|
|
22
|
+
export type QueueOf<C, K extends keyof C> = C[K] extends AnyAsyncFn ? QueueDecorated<C[K]> : never;
|
|
23
|
+
export type PoolOf<C, K extends keyof C> = C[K] extends AnyAsyncFn ? PoolDecorated<C[K]> : never;
|
|
24
|
+
export declare function Task(methodName: string): (_value: undefined, context: ClassFieldDecoratorContext<object, unknown>) => void;
|
|
25
|
+
export declare function Queue(methodName: string): (_value: undefined, context: ClassFieldDecoratorContext<object, unknown>) => void;
|
|
26
|
+
export declare function Pool(methodName: string, concurrency?: number): (_value: undefined, context: ClassFieldDecoratorContext<object, unknown>) => void;
|
|
27
|
+
export {};
|
|
4
28
|
//# sourceMappingURL=decorators.d.ts.map
|
package/dist/decorators.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAOjD,KAAK,UAAU,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAI9D,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,UAAU,IAC5C,CAAC,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG;IAC5C,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3B,KAAK,EAAE,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAC9B,UAAU,EAAE,QAAQ,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACpD,SAAS,IAAI,IAAI,CAAC;CACnB,CAAC;AAEJ,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,UAAU,IAC7C,CAAC,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG;IAC5C,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3B,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC1B,KAAK,EAAE,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAC9B,KAAK,IAAI,IAAI,CAAC;CACf,CAAC;AAEJ,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,UAAU,IAC5C,CAAC,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG;IAC5C,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3B,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACzB,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC1B,KAAK,EAAE,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;CAC/B,CAAC;AAIJ,MAAM,MAAM,MAAM,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,IACrC,CAAC,CAAC,CAAC,CAAC,SAAS,UAAU,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;AAExD,MAAM,MAAM,OAAO,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,IACtC,CAAC,CAAC,CAAC,CAAC,SAAS,UAAU,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;AAEzD,MAAM,MAAM,MAAM,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,IACrC,CAAC,CAAC,CAAC,CAAC,SAAS,UAAU,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;AAIxD,wBAAgB,IAAI,CAAC,UAAU,EAAE,MAAM,qFAYtC;AAID,wBAAgB,KAAK,CAAC,UAAU,EAAE,MAAM,qFAYvC;AAID,wBAAgB,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,SAAI,qFAYvD"}
|
package/dist/decorators.js
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createFieldDecorator } from "@praxisjs/decorators";
|
|
2
2
|
import { pool } from "./pool";
|
|
3
3
|
import { queue } from "./queue";
|
|
4
4
|
import { task } from "./task";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
// ── @Task ──────────────────────────────────────────────────────────────────────
|
|
6
|
+
export function Task(methodName) {
|
|
7
|
+
return createFieldDecorator({
|
|
8
|
+
bind(instance, _name) {
|
|
9
|
+
const inst = instance;
|
|
10
|
+
return {
|
|
11
|
+
descriptor: {
|
|
12
|
+
value: task(inst[methodName].bind(instance)),
|
|
13
|
+
writable: true,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
14
16
|
},
|
|
15
|
-
// Concurrent decorators work on any class, not just StatefulComponent
|
|
16
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
// ── @Queue ─────────────────────────────────────────────────────────────────────
|
|
20
|
+
export function Queue(methodName) {
|
|
21
|
+
return createFieldDecorator({
|
|
22
|
+
bind(instance, _name) {
|
|
23
|
+
const inst = instance;
|
|
24
|
+
return {
|
|
25
|
+
descriptor: {
|
|
26
|
+
value: queue(inst[methodName].bind(instance)),
|
|
27
|
+
writable: true,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
29
30
|
},
|
|
30
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
// ── @Pool ──────────────────────────────────────────────────────────────────────
|
|
34
|
+
export function Pool(methodName, concurrency = 1) {
|
|
35
|
+
return createFieldDecorator({
|
|
36
|
+
bind(instance, _name) {
|
|
37
|
+
const inst = instance;
|
|
38
|
+
return {
|
|
39
|
+
descriptor: {
|
|
40
|
+
value: pool(concurrency, inst[methodName].bind(instance)),
|
|
41
|
+
writable: true,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
43
44
|
},
|
|
44
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
//# sourceMappingURL=decorators.js.map
|
package/dist/decorators.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decorators.js","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"decorators.js","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAG5D,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AA0C9B,kFAAkF;AAElF,MAAM,UAAU,IAAI,CAAC,UAAkB;IACrC,OAAO,oBAAoB,CAAC;QAC1B,IAAI,CAAC,QAAgB,EAAE,KAAa;YAClC,MAAM,IAAI,GAAG,QAAsC,CAAC;YACpD,OAAO;gBACL,UAAU,EAAE;oBACV,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAY;oBACvD,QAAQ,EAAE,IAAI;iBACf;aACF,CAAC;QACJ,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,kFAAkF;AAElF,MAAM,UAAU,KAAK,CAAC,UAAkB;IACtC,OAAO,oBAAoB,CAAC;QAC1B,IAAI,CAAC,QAAgB,EAAE,KAAa;YAClC,MAAM,IAAI,GAAG,QAAsC,CAAC;YACpD,OAAO;gBACL,UAAU,EAAE;oBACV,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAY;oBACxD,QAAQ,EAAE,IAAI;iBACf;aACF,CAAC;QACJ,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,kFAAkF;AAElF,MAAM,UAAU,IAAI,CAAC,UAAkB,EAAE,WAAW,GAAG,CAAC;IACtD,OAAO,oBAAoB,CAAC;QAC1B,IAAI,CAAC,QAAgB,EAAE,KAAa;YAClC,MAAM,IAAI,GAAG,QAAsC,CAAC;YACpD,OAAO;gBACL,UAAU,EAAE;oBACV,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAY;oBACpE,QAAQ,EAAE,IAAI;iBACf;aACF,CAAC;QACJ,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACjD,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praxisjs/concurrent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@praxisjs/core": "1.1.0",
|
|
18
|
-
"@praxisjs/decorators": "0.
|
|
18
|
+
"@praxisjs/decorators": "0.7.0",
|
|
19
19
|
"@praxisjs/shared": "0.2.0"
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
@@ -2,14 +2,14 @@ import { describe, it, expect } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import { Task, Queue, Pool } from "../decorators";
|
|
4
4
|
|
|
5
|
-
function
|
|
5
|
+
function fieldCtx(name: string) {
|
|
6
6
|
const initializers: Array<(this: unknown) => void> = [];
|
|
7
7
|
return {
|
|
8
8
|
ctx: {
|
|
9
9
|
name,
|
|
10
|
-
kind: "
|
|
10
|
+
kind: "field" as const,
|
|
11
11
|
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
12
|
-
} as
|
|
12
|
+
} as ClassFieldDecoratorContext,
|
|
13
13
|
run(instance: unknown) { initializers.forEach((fn) => { fn.call(instance); }); },
|
|
14
14
|
};
|
|
15
15
|
}
|
|
@@ -17,87 +17,98 @@ function methodCtx(name: string) {
|
|
|
17
17
|
// ── Task ──────────────────────────────────────────────────────────────────────
|
|
18
18
|
|
|
19
19
|
describe("Task decorator", () => {
|
|
20
|
-
it("
|
|
21
|
-
const { ctx, run } =
|
|
22
|
-
|
|
23
|
-
Task()(original, ctx);
|
|
20
|
+
it("wraps the named method and exposes signals as sub-properties", async () => {
|
|
21
|
+
const { ctx, run } = fieldCtx("taskLoad");
|
|
22
|
+
Task("load")(undefined, ctx);
|
|
24
23
|
|
|
25
|
-
const instance: Record<string, unknown> = {
|
|
24
|
+
const instance: Record<string, unknown> = {
|
|
25
|
+
load: async (x: unknown) => (x as number) * 2,
|
|
26
|
+
};
|
|
26
27
|
run(instance);
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
const taskLoad = instance.taskLoad as {
|
|
30
|
+
(...args: unknown[]): Promise<number>;
|
|
31
|
+
loading: () => boolean;
|
|
32
|
+
error: () => Error | null;
|
|
33
|
+
lastResult: () => number | null;
|
|
34
|
+
};
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
expect(typeof taskLoad).toBe("function");
|
|
37
|
+
expect(taskLoad.loading).toBeDefined();
|
|
38
|
+
expect(taskLoad.error).toBeDefined();
|
|
39
|
+
expect(taskLoad.lastResult).toBeDefined();
|
|
40
|
+
|
|
41
|
+
const result = await taskLoad(5);
|
|
34
42
|
expect(result).toBe(10);
|
|
35
|
-
expect(
|
|
36
|
-
(instance.load_lastResult as () => number)(),
|
|
37
|
-
).toBe(10);
|
|
43
|
+
expect(taskLoad.lastResult()).toBe(10);
|
|
38
44
|
});
|
|
39
45
|
|
|
40
|
-
it("sets loading
|
|
41
|
-
const { ctx, run } =
|
|
42
|
-
|
|
43
|
-
const original = async () => new Promise<string>((r) => { resolve = r; });
|
|
44
|
-
Task()(original, ctx);
|
|
46
|
+
it("sets loading while running", async () => {
|
|
47
|
+
const { ctx, run } = fieldCtx("taskFetch");
|
|
48
|
+
Task("fetch")(undefined, ctx);
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
let resolve!: (v: string) => void;
|
|
51
|
+
const instance: Record<string, unknown> = {
|
|
52
|
+
fetch: async () => new Promise<string>((r) => { resolve = r; }),
|
|
53
|
+
};
|
|
47
54
|
run(instance);
|
|
48
55
|
|
|
49
|
-
const
|
|
50
|
-
|
|
56
|
+
const taskFetch = instance.taskFetch as { (): Promise<string>; loading: () => boolean };
|
|
57
|
+
const p = taskFetch();
|
|
58
|
+
expect(taskFetch.loading()).toBe(true);
|
|
51
59
|
resolve("done");
|
|
52
60
|
await p;
|
|
53
|
-
expect(
|
|
61
|
+
expect(taskFetch.loading()).toBe(false);
|
|
54
62
|
});
|
|
55
63
|
});
|
|
56
64
|
|
|
57
65
|
// ── Queue ─────────────────────────────────────────────────────────────────────
|
|
58
66
|
|
|
59
67
|
describe("Queue decorator", () => {
|
|
60
|
-
it("
|
|
61
|
-
const { ctx, run } =
|
|
62
|
-
|
|
63
|
-
Queue()(original, ctx);
|
|
68
|
+
it("wraps the named method and exposes signals", async () => {
|
|
69
|
+
const { ctx, run } = fieldCtx("taskSave");
|
|
70
|
+
Queue("save")(undefined, ctx);
|
|
64
71
|
|
|
65
|
-
const instance: Record<string, unknown> = {
|
|
72
|
+
const instance: Record<string, unknown> = {
|
|
73
|
+
save: async (x: unknown) => String(x),
|
|
74
|
+
};
|
|
66
75
|
run(instance);
|
|
67
76
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
const taskSave = instance.taskSave as {
|
|
78
|
+
(...args: unknown[]): Promise<string>;
|
|
79
|
+
loading: () => boolean;
|
|
80
|
+
pending: () => number;
|
|
81
|
+
error: () => Error | null;
|
|
82
|
+
clear: () => void;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
expect(typeof taskSave).toBe("function");
|
|
86
|
+
expect(taskSave.loading).toBeDefined();
|
|
87
|
+
expect(taskSave.pending).toBeDefined();
|
|
88
|
+
expect(taskSave.error).toBeDefined();
|
|
89
|
+
expect(taskSave.clear).toBeDefined();
|
|
90
|
+
|
|
91
|
+
const result = await taskSave("hello");
|
|
74
92
|
expect(result).toBe("hello");
|
|
75
93
|
});
|
|
76
|
-
});
|
|
77
94
|
|
|
78
|
-
|
|
95
|
+
it("clear() rejects queued calls with QueueClearedError", async () => {
|
|
96
|
+
const { ctx, run } = fieldCtx("taskSave");
|
|
97
|
+
Queue("save")(undefined, ctx);
|
|
79
98
|
|
|
80
|
-
describe("Queue decorator (additional)", () => {
|
|
81
|
-
it("clear() is accessible on the instance via method_clear property", async () => {
|
|
82
|
-
const { ctx, run } = methodCtx("save");
|
|
83
99
|
let resolveFirst!: () => void;
|
|
84
|
-
const
|
|
85
|
-
idx
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const instance: Record<string, unknown> = {};
|
|
100
|
+
const instance: Record<string, unknown> = {
|
|
101
|
+
save: async (_: unknown, idx: unknown) =>
|
|
102
|
+
idx === 0
|
|
103
|
+
? new Promise<void>((r) => { resolveFirst = r; })
|
|
104
|
+
: Promise.resolve(),
|
|
105
|
+
};
|
|
91
106
|
run(instance);
|
|
92
107
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
const p1 = save(null, 1);
|
|
98
|
-
|
|
99
|
-
// Call clear via the exposed property
|
|
100
|
-
(instance.save_clear as () => void)();
|
|
108
|
+
const taskSave = instance.taskSave as { (...args: unknown[]): Promise<unknown>; clear: () => void };
|
|
109
|
+
taskSave(null, 0);
|
|
110
|
+
const p1 = taskSave(null, 1);
|
|
111
|
+
taskSave.clear();
|
|
101
112
|
|
|
102
113
|
await expect(p1).rejects.toThrow("Queue cleared");
|
|
103
114
|
resolveFirst();
|
|
@@ -107,104 +118,75 @@ describe("Queue decorator (additional)", () => {
|
|
|
107
118
|
// ── Pool ──────────────────────────────────────────────────────────────────────
|
|
108
119
|
|
|
109
120
|
describe("Pool decorator", () => {
|
|
110
|
-
it("
|
|
111
|
-
const { ctx, run } =
|
|
112
|
-
|
|
113
|
-
Pool(2)(original, ctx);
|
|
121
|
+
it("wraps the named method and exposes signals", async () => {
|
|
122
|
+
const { ctx, run } = fieldCtx("taskProcess");
|
|
123
|
+
Pool("process", 2)(undefined, ctx);
|
|
114
124
|
|
|
115
|
-
const instance: Record<string, unknown> = {
|
|
125
|
+
const instance: Record<string, unknown> = {
|
|
126
|
+
process: async (x: unknown) => (x as number) + 1,
|
|
127
|
+
};
|
|
116
128
|
run(instance);
|
|
117
129
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
130
|
+
const taskProcess = instance.taskProcess as {
|
|
131
|
+
(...args: unknown[]): Promise<number>;
|
|
132
|
+
loading: () => boolean;
|
|
133
|
+
active: () => number;
|
|
134
|
+
pending: () => number;
|
|
135
|
+
error: () => Error | null;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
expect(typeof taskProcess).toBe("function");
|
|
139
|
+
expect(taskProcess.loading).toBeDefined();
|
|
140
|
+
expect(taskProcess.active).toBeDefined();
|
|
141
|
+
expect(taskProcess.pending).toBeDefined();
|
|
142
|
+
expect(taskProcess.error).toBeDefined();
|
|
143
|
+
|
|
144
|
+
const result = await taskProcess(9);
|
|
125
145
|
expect(result).toBe(10);
|
|
126
146
|
});
|
|
127
147
|
|
|
128
148
|
it("respects concurrency limit", async () => {
|
|
129
|
-
const { ctx, run } =
|
|
130
|
-
|
|
131
|
-
const original = async () => new Promise<void>((r) => resolvers.push(r));
|
|
132
|
-
Pool(2)(original, ctx);
|
|
149
|
+
const { ctx, run } = fieldCtx("taskWork");
|
|
150
|
+
Pool("work", 2)(undefined, ctx);
|
|
133
151
|
|
|
134
|
-
const
|
|
152
|
+
const resolvers: Array<() => void> = [];
|
|
153
|
+
const instance: Record<string, unknown> = {
|
|
154
|
+
work: async () => new Promise<void>((r) => resolvers.push(r)),
|
|
155
|
+
};
|
|
135
156
|
run(instance);
|
|
136
157
|
|
|
137
|
-
const
|
|
138
|
-
const t1 =
|
|
139
|
-
const t2 =
|
|
140
|
-
|
|
158
|
+
const taskWork = instance.taskWork as { (): Promise<void>; active: () => number; pending: () => number };
|
|
159
|
+
const t1 = taskWork();
|
|
160
|
+
const t2 = taskWork();
|
|
161
|
+
taskWork();
|
|
141
162
|
|
|
142
|
-
expect(
|
|
143
|
-
expect(
|
|
163
|
+
expect(taskWork.active()).toBe(2);
|
|
164
|
+
expect(taskWork.pending()).toBe(1);
|
|
144
165
|
|
|
145
166
|
resolvers.forEach((r) => { r(); });
|
|
146
167
|
await Promise.all([t1, t2]);
|
|
147
168
|
});
|
|
148
169
|
|
|
149
|
-
it("
|
|
150
|
-
const { ctx, run } =
|
|
151
|
-
|
|
152
|
-
const original = async () => new Promise<void>((r) => resolvers.push(r));
|
|
153
|
-
Pool(-1)(original, ctx);
|
|
154
|
-
|
|
155
|
-
const instance: Record<string, unknown> = {};
|
|
156
|
-
run(instance);
|
|
157
|
-
|
|
158
|
-
const work = instance.work as () => Promise<void>;
|
|
159
|
-
const t1 = work();
|
|
160
|
-
const t2 = work();
|
|
170
|
+
it("defaults concurrency to 1", async () => {
|
|
171
|
+
const { ctx, run } = fieldCtx("taskWork");
|
|
172
|
+
Pool("work")(undefined, ctx);
|
|
161
173
|
|
|
162
|
-
// With concurrency clamped to 1, only one task should be active at a time
|
|
163
|
-
expect((instance.work_active as () => number)()).toBe(1);
|
|
164
|
-
expect((instance.work_pending as () => number)()).toBe(1);
|
|
165
|
-
|
|
166
|
-
resolvers[0]();
|
|
167
|
-
await t1;
|
|
168
|
-
resolvers[1]();
|
|
169
|
-
await t2;
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("decorated method called concurrently up to pool limit — active() signal is accurate", async () => {
|
|
173
|
-
const { ctx, run } = methodCtx("process");
|
|
174
174
|
const resolvers: Array<() => void> = [];
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const instance: Record<string, unknown> = {};
|
|
175
|
+
const instance: Record<string, unknown> = {
|
|
176
|
+
work: async () => new Promise<void>((r) => resolvers.push(r)),
|
|
177
|
+
};
|
|
179
178
|
run(instance);
|
|
180
179
|
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
const t1 = process();
|
|
185
|
-
expect(active()).toBe(1);
|
|
186
|
-
|
|
187
|
-
const t2 = process();
|
|
188
|
-
expect(active()).toBe(2);
|
|
189
|
-
|
|
190
|
-
const t3 = process();
|
|
191
|
-
expect(active()).toBe(3);
|
|
180
|
+
const taskWork = instance.taskWork as { (): Promise<void>; active: () => number; pending: () => number };
|
|
181
|
+
const t1 = taskWork();
|
|
182
|
+
const t2 = taskWork();
|
|
192
183
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
expect(active()).toBe(3);
|
|
196
|
-
expect((instance.process_pending as () => number)()).toBe(1);
|
|
184
|
+
expect(taskWork.active()).toBe(1);
|
|
185
|
+
expect(taskWork.pending()).toBe(1);
|
|
197
186
|
|
|
198
|
-
// Resolve t1 first — this will allow t4 to start and push its resolver
|
|
199
187
|
resolvers[0]();
|
|
200
188
|
await t1;
|
|
201
|
-
|
|
202
|
-
// Now t4 has started and pushed a resolver
|
|
203
189
|
resolvers[1]();
|
|
204
|
-
|
|
205
|
-
resolvers[3]();
|
|
206
|
-
await Promise.all([t2, t3, t4]);
|
|
207
|
-
|
|
208
|
-
expect(active()).toBe(0);
|
|
190
|
+
await t2;
|
|
209
191
|
});
|
|
210
192
|
});
|
package/src/decorators.ts
CHANGED
|
@@ -1,50 +1,94 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createFieldDecorator } from "@praxisjs/decorators";
|
|
2
|
+
import type { Computed } from "@praxisjs/shared";
|
|
2
3
|
|
|
3
4
|
import { pool } from "./pool";
|
|
4
5
|
import { queue } from "./queue";
|
|
5
6
|
import { task } from "./task";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
type AnyAsyncFn = (this: any, ...args: any[]) => Promise<any>;
|
|
10
|
+
|
|
11
|
+
// ── Decorated types ────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export type TaskDecorated<T extends AnyAsyncFn> =
|
|
14
|
+
((...args: Parameters<T>) => ReturnType<T>) & {
|
|
15
|
+
loading: Computed<boolean>;
|
|
16
|
+
error: Computed<Error | null>;
|
|
17
|
+
lastResult: Computed<Awaited<ReturnType<T>> | null>;
|
|
18
|
+
cancelAll(): void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type QueueDecorated<T extends AnyAsyncFn> =
|
|
22
|
+
((...args: Parameters<T>) => ReturnType<T>) & {
|
|
23
|
+
loading: Computed<boolean>;
|
|
24
|
+
pending: Computed<number>;
|
|
25
|
+
error: Computed<Error | null>;
|
|
26
|
+
clear(): void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type PoolDecorated<T extends AnyAsyncFn> =
|
|
30
|
+
((...args: Parameters<T>) => ReturnType<T>) & {
|
|
31
|
+
loading: Computed<boolean>;
|
|
32
|
+
active: Computed<number>;
|
|
33
|
+
pending: Computed<number>;
|
|
34
|
+
error: Computed<Error | null>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ── Type helpers ───────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export type TaskOf<C, K extends keyof C> =
|
|
40
|
+
C[K] extends AnyAsyncFn ? TaskDecorated<C[K]> : never;
|
|
41
|
+
|
|
42
|
+
export type QueueOf<C, K extends keyof C> =
|
|
43
|
+
C[K] extends AnyAsyncFn ? QueueDecorated<C[K]> : never;
|
|
44
|
+
|
|
45
|
+
export type PoolOf<C, K extends keyof C> =
|
|
46
|
+
C[K] extends AnyAsyncFn ? PoolDecorated<C[K]> : never;
|
|
47
|
+
|
|
48
|
+
// ── @Task ──────────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export function Task(methodName: string) {
|
|
51
|
+
return createFieldDecorator({
|
|
52
|
+
bind(instance: object, _name: string) {
|
|
53
|
+
const inst = instance as Record<string, AnyAsyncFn>;
|
|
54
|
+
return {
|
|
55
|
+
descriptor: {
|
|
56
|
+
value: task(inst[methodName].bind(instance)) as unknown,
|
|
57
|
+
writable: true,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
16
60
|
},
|
|
17
|
-
|
|
18
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
-
}) as unknown as (value: (...args: any[]) => Promise<any>, context: ClassMethodDecoratorContext<any>) => void;
|
|
61
|
+
});
|
|
20
62
|
}
|
|
21
63
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
64
|
+
// ── @Queue ─────────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export function Queue(methodName: string) {
|
|
67
|
+
return createFieldDecorator({
|
|
68
|
+
bind(instance: object, _name: string) {
|
|
69
|
+
const inst = instance as Record<string, AnyAsyncFn>;
|
|
70
|
+
return {
|
|
71
|
+
descriptor: {
|
|
72
|
+
value: queue(inst[methodName].bind(instance)) as unknown,
|
|
73
|
+
writable: true,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
32
76
|
},
|
|
33
|
-
|
|
34
|
-
}) as unknown as (value: (...args: any[]) => Promise<any>, context: ClassMethodDecoratorContext<any>) => void;
|
|
77
|
+
});
|
|
35
78
|
}
|
|
36
79
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
80
|
+
// ── @Pool ──────────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export function Pool(methodName: string, concurrency = 1) {
|
|
83
|
+
return createFieldDecorator({
|
|
84
|
+
bind(instance: object, _name: string) {
|
|
85
|
+
const inst = instance as Record<string, AnyAsyncFn>;
|
|
86
|
+
return {
|
|
87
|
+
descriptor: {
|
|
88
|
+
value: pool(concurrency, inst[methodName].bind(instance)) as unknown,
|
|
89
|
+
writable: true,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
47
92
|
},
|
|
48
|
-
|
|
49
|
-
}) as unknown as (value: (...args: any[]) => Promise<any>, context: ClassMethodDecoratorContext<any>) => void;
|
|
93
|
+
});
|
|
50
94
|
}
|
package/src/index.ts
CHANGED