@tsed/hooks 8.0.1 → 8.0.2
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 +4 -3
- package/src/Hooks.spec.ts +219 -0
- package/src/Hooks.ts +241 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@tsed/hooks",
|
|
3
3
|
"description": "Hooks module for Ts.ED Framework",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "8.0.
|
|
5
|
+
"version": "8.0.2",
|
|
6
6
|
"source": "./src/index.ts",
|
|
7
7
|
"main": "./lib/esm/index.js",
|
|
8
8
|
"module": "./lib/esm/index.js",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"browser": "./lib/browser/core.umd.min.js",
|
|
11
11
|
"exports": {
|
|
12
12
|
".": {
|
|
13
|
+
"@tsed/source": "./src/index.ts",
|
|
13
14
|
"types": "./lib/types/index.d.ts",
|
|
14
15
|
"browser": "./lib/browser/hooks.umd.min.js",
|
|
15
16
|
"import": "./lib/esm/index.js",
|
|
@@ -34,8 +35,8 @@
|
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@tsed/monorepo-utils": "2.3.9",
|
|
37
|
-
"@tsed/typescript": "8.0.
|
|
38
|
-
"@tsed/vitest": "8.0.
|
|
38
|
+
"@tsed/typescript": "8.0.2",
|
|
39
|
+
"@tsed/vitest": "8.0.2",
|
|
39
40
|
"eslint": "9.12.0",
|
|
40
41
|
"typescript": "5.4.5",
|
|
41
42
|
"vite": "^5.4.8",
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {Hooks} from "./Hooks.js";
|
|
2
|
+
|
|
3
|
+
describe("Hooks", () => {
|
|
4
|
+
describe("off()", () => {
|
|
5
|
+
it("should remove a listener (using event, cb)", () => {
|
|
6
|
+
const hooks = new Hooks();
|
|
7
|
+
const fn = vi.fn();
|
|
8
|
+
const fn2 = vi.fn();
|
|
9
|
+
const fn3 = vi.fn();
|
|
10
|
+
|
|
11
|
+
hooks.on("event", fn);
|
|
12
|
+
hooks.on("event2", fn2);
|
|
13
|
+
hooks.on("event2", fn3);
|
|
14
|
+
|
|
15
|
+
expect(hooks.has("event")).toBe(true);
|
|
16
|
+
expect(hooks.has("event2")).toBe(true);
|
|
17
|
+
|
|
18
|
+
hooks.off("event", fn);
|
|
19
|
+
hooks.off("event2", fn2);
|
|
20
|
+
|
|
21
|
+
expect(hooks.has("event")).toBe(false);
|
|
22
|
+
expect(hooks.has("event2")).toBe(true);
|
|
23
|
+
|
|
24
|
+
hooks.off("event3", fn2);
|
|
25
|
+
|
|
26
|
+
expect(hooks.has("event2")).toBe(true);
|
|
27
|
+
|
|
28
|
+
hooks.emit("event", ["arg1"]);
|
|
29
|
+
|
|
30
|
+
expect(fn).not.toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
it("should remove a listener (using ref)", () => {
|
|
33
|
+
const hooks = new Hooks();
|
|
34
|
+
const fn = vi.fn();
|
|
35
|
+
const fn2 = vi.fn();
|
|
36
|
+
const ref = Symbol("ref");
|
|
37
|
+
|
|
38
|
+
hooks.on("event", ref, fn);
|
|
39
|
+
hooks.on("event2", ref, fn2);
|
|
40
|
+
|
|
41
|
+
expect(hooks.has("event")).toBe(true);
|
|
42
|
+
expect(hooks.has("event2")).toBe(true);
|
|
43
|
+
|
|
44
|
+
hooks.off(ref);
|
|
45
|
+
|
|
46
|
+
expect(hooks.has("event")).toBe(false);
|
|
47
|
+
expect(hooks.has("event2")).toBe(false);
|
|
48
|
+
|
|
49
|
+
hooks.emit("event", ["arg1"]);
|
|
50
|
+
hooks.emit("event2", ["arg1"]);
|
|
51
|
+
|
|
52
|
+
expect(fn).not.toHaveBeenCalled();
|
|
53
|
+
expect(fn2).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe("once()", () => {
|
|
57
|
+
it("should call once a listener (using event, cb)", () => {
|
|
58
|
+
const hooks = new Hooks();
|
|
59
|
+
const fn = vi.fn();
|
|
60
|
+
|
|
61
|
+
hooks.once("event", fn);
|
|
62
|
+
|
|
63
|
+
expect(hooks.has("event")).toBe(true);
|
|
64
|
+
|
|
65
|
+
hooks.emit("event", ["arg1"]);
|
|
66
|
+
|
|
67
|
+
expect(hooks.has("event")).toBe(false);
|
|
68
|
+
expect(fn).toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
it("should call once a listener (using ref)", () => {
|
|
71
|
+
const hooks = new Hooks();
|
|
72
|
+
const fn = vi.fn();
|
|
73
|
+
const fn2 = vi.fn();
|
|
74
|
+
const ref = Symbol("ref");
|
|
75
|
+
|
|
76
|
+
hooks.once("event", ref, fn);
|
|
77
|
+
hooks.once("event2", ref, fn2);
|
|
78
|
+
|
|
79
|
+
expect(hooks.has("event")).toBe(true);
|
|
80
|
+
expect(hooks.has("event2")).toBe(true);
|
|
81
|
+
|
|
82
|
+
hooks.emit("event", ["arg1"]);
|
|
83
|
+
hooks.emit("event2", ["arg1"]);
|
|
84
|
+
|
|
85
|
+
expect(hooks.has("event")).toBe(false);
|
|
86
|
+
expect(hooks.has("event2")).toBe(false);
|
|
87
|
+
expect(fn).toHaveBeenCalled();
|
|
88
|
+
expect(fn2).toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe("emit()", () => {
|
|
92
|
+
it("should listen a hook and calls listener", () => {
|
|
93
|
+
const hooks = new Hooks();
|
|
94
|
+
const fn = vi.fn();
|
|
95
|
+
|
|
96
|
+
hooks.on("event", fn);
|
|
97
|
+
|
|
98
|
+
hooks.emit("event", ["arg1"]);
|
|
99
|
+
|
|
100
|
+
expect(fn).toHaveBeenCalledWith("arg1");
|
|
101
|
+
|
|
102
|
+
hooks.off("event", fn);
|
|
103
|
+
});
|
|
104
|
+
it("should calls listeners that match listeners with matching ref and without ref", () => {
|
|
105
|
+
const hooks = new Hooks();
|
|
106
|
+
const fn: any = vi.fn();
|
|
107
|
+
fn.id = "f1";
|
|
108
|
+
|
|
109
|
+
const fn2: any = vi.fn();
|
|
110
|
+
fn.id = "f2";
|
|
111
|
+
|
|
112
|
+
const fn3: any = vi.fn();
|
|
113
|
+
fn3.id = "f3";
|
|
114
|
+
const ref = Symbol("ref");
|
|
115
|
+
|
|
116
|
+
hooks.on("event", ref, fn);
|
|
117
|
+
hooks.on("event2", ref, fn2);
|
|
118
|
+
hooks.on("event2", fn3);
|
|
119
|
+
|
|
120
|
+
hooks.emit("event", ref, ["arg1"]);
|
|
121
|
+
|
|
122
|
+
expect(fn).toHaveBeenCalledWith("arg1");
|
|
123
|
+
expect(fn2).not.toHaveBeenCalledWith("arg1");
|
|
124
|
+
expect(fn3).not.toHaveBeenCalledWith("arg1");
|
|
125
|
+
|
|
126
|
+
hooks.emit("event2", ref, ["arg1"]);
|
|
127
|
+
|
|
128
|
+
expect(fn2).toHaveBeenCalledWith("arg1");
|
|
129
|
+
expect(fn3).toHaveBeenCalledWith("arg1");
|
|
130
|
+
|
|
131
|
+
hooks.off(ref);
|
|
132
|
+
});
|
|
133
|
+
it("should not call listeners if the ref mismatch", () => {
|
|
134
|
+
const hooks = new Hooks();
|
|
135
|
+
const fn = vi.fn();
|
|
136
|
+
const fn2 = vi.fn();
|
|
137
|
+
const fn3 = vi.fn();
|
|
138
|
+
const ref = Symbol("ref");
|
|
139
|
+
const ref2 = Symbol("ref");
|
|
140
|
+
|
|
141
|
+
hooks.on("event", ref, fn);
|
|
142
|
+
hooks.on("event2", ref, fn2);
|
|
143
|
+
hooks.on("event2", ref, fn3);
|
|
144
|
+
|
|
145
|
+
hooks.emit("event", ref2, ["arg1"]);
|
|
146
|
+
|
|
147
|
+
expect(fn).not.toHaveBeenCalledWith("arg1");
|
|
148
|
+
expect(fn2).not.toHaveBeenCalledWith("arg1");
|
|
149
|
+
expect(fn3).not.toHaveBeenCalledWith("arg1");
|
|
150
|
+
|
|
151
|
+
hooks.off(ref);
|
|
152
|
+
});
|
|
153
|
+
it("should call", () => {
|
|
154
|
+
const hooks = new Hooks();
|
|
155
|
+
const fn = vi.fn();
|
|
156
|
+
const fn2 = vi.fn();
|
|
157
|
+
const fn3 = vi.fn();
|
|
158
|
+
const ref = Symbol("ref");
|
|
159
|
+
const ref2 = Symbol("ref");
|
|
160
|
+
|
|
161
|
+
hooks.on("event", ref, fn);
|
|
162
|
+
hooks.on("event2", ref, fn2);
|
|
163
|
+
hooks.on("event2", ref, fn3);
|
|
164
|
+
|
|
165
|
+
hooks.emit("event", ref2, ["arg1"]);
|
|
166
|
+
|
|
167
|
+
expect(fn).not.toHaveBeenCalledWith("arg1");
|
|
168
|
+
expect(fn2).not.toHaveBeenCalledWith("arg1");
|
|
169
|
+
expect(fn3).not.toHaveBeenCalledWith("arg1");
|
|
170
|
+
|
|
171
|
+
hooks.off(ref);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
describe("asyncEmit()", () => {
|
|
175
|
+
it("should async listen a hook and calls listener", async () => {
|
|
176
|
+
const hooks = new Hooks();
|
|
177
|
+
const fn = vi.fn();
|
|
178
|
+
|
|
179
|
+
hooks.on("event", fn);
|
|
180
|
+
|
|
181
|
+
await hooks.asyncEmit("event", ["arg1"]);
|
|
182
|
+
|
|
183
|
+
expect(fn).toHaveBeenCalledWith("arg1");
|
|
184
|
+
|
|
185
|
+
hooks.off("event", fn);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
describe("alter()", () => {
|
|
189
|
+
it("should listen a hook and calls listener", () => {
|
|
190
|
+
const hooks = new Hooks();
|
|
191
|
+
const fn = vi.fn().mockReturnValue("valueAltered");
|
|
192
|
+
|
|
193
|
+
hooks.on("event", fn);
|
|
194
|
+
|
|
195
|
+
const value = hooks.alter("event", "value");
|
|
196
|
+
|
|
197
|
+
expect(fn).toHaveBeenCalledWith("value");
|
|
198
|
+
expect(value).toBe("valueAltered");
|
|
199
|
+
|
|
200
|
+
hooks.off("event", fn);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
describe("alterAsync()", () => {
|
|
204
|
+
it("should async listen a hook and calls listener", async () => {
|
|
205
|
+
const hooks = new Hooks();
|
|
206
|
+
const fn = vi.fn().mockReturnValue("valueAltered");
|
|
207
|
+
|
|
208
|
+
hooks.on("event", fn);
|
|
209
|
+
|
|
210
|
+
await hooks.asyncAlter("event", "value", ["arg1"]);
|
|
211
|
+
|
|
212
|
+
expect(fn).toHaveBeenCalledWith("value", "arg1");
|
|
213
|
+
|
|
214
|
+
hooks.off("event", fn);
|
|
215
|
+
|
|
216
|
+
hooks.destroy();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
package/src/Hooks.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
export type HookRef = string | symbol | any | Function;
|
|
2
|
+
export type HookListener = Function;
|
|
3
|
+
|
|
4
|
+
type HookItem = {cb: HookListener; ref?: HookRef};
|
|
5
|
+
|
|
6
|
+
function match(ref: HookRef | unknown[] | undefined, item: HookItem) {
|
|
7
|
+
return !ref || !item.ref || (ref && item.ref === ref);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class Hooks {
|
|
11
|
+
#listeners: Map<string, HookItem[]> = new Map();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if an event has listeners
|
|
15
|
+
* @param event
|
|
16
|
+
*/
|
|
17
|
+
has(event: string) {
|
|
18
|
+
return !!this.#listeners.get(event)?.length;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Listen a hook event
|
|
23
|
+
* @param event The event name
|
|
24
|
+
* @param ref The reference of the listener
|
|
25
|
+
* @param cb The callback
|
|
26
|
+
*/
|
|
27
|
+
on(event: string, ref: HookRef, cb: HookListener): this;
|
|
28
|
+
/**
|
|
29
|
+
* Listen a hook event
|
|
30
|
+
* @param event The event name
|
|
31
|
+
* @param cb The callback
|
|
32
|
+
*/
|
|
33
|
+
on(event: string, cb: HookListener): this;
|
|
34
|
+
on(event: string, cbORef: HookRef | HookListener, cb?: HookListener) {
|
|
35
|
+
let ref: HookRef | HookListener | undefined = cbORef;
|
|
36
|
+
|
|
37
|
+
if (!cb) {
|
|
38
|
+
cb = ref as HookListener;
|
|
39
|
+
ref = undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const items = this.#listeners.get(event) || [];
|
|
43
|
+
|
|
44
|
+
items.push({
|
|
45
|
+
cb,
|
|
46
|
+
ref
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
this.#listeners.set(event, items);
|
|
50
|
+
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Listen a hook event once
|
|
56
|
+
*
|
|
57
|
+
* @param event The event name
|
|
58
|
+
* @param ref The reference of the listener
|
|
59
|
+
* @param cb The callback
|
|
60
|
+
*/
|
|
61
|
+
once(event: string, ref: HookRef, cb: HookListener): this;
|
|
62
|
+
/**
|
|
63
|
+
* Listen a hook event once
|
|
64
|
+
* @param event
|
|
65
|
+
* @param cb
|
|
66
|
+
*/
|
|
67
|
+
once(event: string, cb: HookListener): this;
|
|
68
|
+
once(event: string, ref: HookRef | HookListener, cb?: HookListener) {
|
|
69
|
+
if (!cb) {
|
|
70
|
+
cb = ref as HookListener;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const onceCb = (...args: unknown[]) => {
|
|
74
|
+
cb(...args);
|
|
75
|
+
this.off(event, onceCb);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
this.on(event, ref, onceCb);
|
|
79
|
+
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Remove a listener attached to an event
|
|
85
|
+
* @param ref
|
|
86
|
+
*/
|
|
87
|
+
off(ref: HookRef): this;
|
|
88
|
+
/**
|
|
89
|
+
* Remove a listener attached to an event
|
|
90
|
+
* @param event
|
|
91
|
+
* @param cb
|
|
92
|
+
*/
|
|
93
|
+
off(event: string, cb: HookListener): this;
|
|
94
|
+
off(event: string | HookRef, cb?: HookListener) {
|
|
95
|
+
const set = (event: string, items: HookItem[]) => {
|
|
96
|
+
if (items.length) {
|
|
97
|
+
this.#listeners.set(event, items);
|
|
98
|
+
} else {
|
|
99
|
+
this.#listeners.delete(event);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (typeof event === "string" && cb) {
|
|
104
|
+
let items = this.#listeners.get(event);
|
|
105
|
+
|
|
106
|
+
if (items) {
|
|
107
|
+
set(
|
|
108
|
+
event,
|
|
109
|
+
items.filter((item) => item.cb !== cb)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
const ref = event as HookRef;
|
|
114
|
+
|
|
115
|
+
this.#listeners.forEach((items, event) => {
|
|
116
|
+
set(
|
|
117
|
+
event,
|
|
118
|
+
items.filter((item) => item.ref !== ref)
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Trigger an event without arguments.
|
|
128
|
+
* @param event The event name
|
|
129
|
+
*/
|
|
130
|
+
emit(event: string): void;
|
|
131
|
+
/**
|
|
132
|
+
* Trigger an event and call listener.
|
|
133
|
+
* @param event
|
|
134
|
+
* @param args
|
|
135
|
+
* @param callThis
|
|
136
|
+
*/
|
|
137
|
+
emit(event: string, args: unknown[]): void;
|
|
138
|
+
/**
|
|
139
|
+
* Trigger an event with arguments and call only listener attached to the given ref.
|
|
140
|
+
* @param event The event name
|
|
141
|
+
* @param ref The reference of the listener
|
|
142
|
+
* @param args The arguments
|
|
143
|
+
*/
|
|
144
|
+
emit(event: string, ref: HookRef, args?: unknown[]): void;
|
|
145
|
+
emit(event: string, ref?: HookRef | unknown[], args?: unknown[]): void {
|
|
146
|
+
if (Array.isArray(ref)) {
|
|
147
|
+
args = ref;
|
|
148
|
+
ref = undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
args ||= [];
|
|
152
|
+
|
|
153
|
+
const items = this.#listeners.get(event);
|
|
154
|
+
|
|
155
|
+
if (items?.length) {
|
|
156
|
+
for (const item of items) {
|
|
157
|
+
if (match(ref, item)) {
|
|
158
|
+
item.cb.apply(null, args);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Trigger an event and call async listener.
|
|
166
|
+
* @param event The event name
|
|
167
|
+
*/
|
|
168
|
+
async asyncEmit(event: string): Promise<void>;
|
|
169
|
+
async asyncEmit(event: string, args: unknown[]): Promise<void>;
|
|
170
|
+
async asyncEmit(event: string, ref: HookRef, args?: unknown[]): Promise<void>;
|
|
171
|
+
async asyncEmit(event: string, ref?: HookRef | unknown[], args?: unknown[]): Promise<void> {
|
|
172
|
+
if (Array.isArray(ref)) {
|
|
173
|
+
args = ref;
|
|
174
|
+
ref = undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const items = this.#listeners.get(event);
|
|
178
|
+
|
|
179
|
+
if (items?.length) {
|
|
180
|
+
const promises = items.filter((item) => match(ref, item)).map((item) => item.cb.apply(null, args));
|
|
181
|
+
|
|
182
|
+
await Promise.all(promises);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Trigger an event, listener alter given value and return it.
|
|
188
|
+
* @param event
|
|
189
|
+
* @param value
|
|
190
|
+
* @param args
|
|
191
|
+
* @param callThis
|
|
192
|
+
*/
|
|
193
|
+
alter<Arg = unknown, AlteredArg = Arg>(event: string, value: Arg, args: unknown[] = [], callThis: unknown = null): AlteredArg {
|
|
194
|
+
const items = this.#listeners.get(event);
|
|
195
|
+
|
|
196
|
+
if (items?.length) {
|
|
197
|
+
for (const {cb} of items) {
|
|
198
|
+
value = cb.call(callThis, value, ...args);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return value as unknown as AlteredArg;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Trigger an event, async listener alter given value and return it.
|
|
207
|
+
* @param event
|
|
208
|
+
* @param value
|
|
209
|
+
* @param args
|
|
210
|
+
* @param callThis
|
|
211
|
+
*/
|
|
212
|
+
async asyncAlter<Arg = unknown, AlteredArg = Arg>(
|
|
213
|
+
event: string,
|
|
214
|
+
value: Arg,
|
|
215
|
+
args: unknown[] = [],
|
|
216
|
+
callThis: unknown = null
|
|
217
|
+
): Promise<AlteredArg> {
|
|
218
|
+
const items = this.#listeners.get(event);
|
|
219
|
+
|
|
220
|
+
if (items?.length) {
|
|
221
|
+
for (const item of items) {
|
|
222
|
+
value = await item.cb.call(callThis, value, ...args);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return value as unknown as AlteredArg;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
destroy() {
|
|
230
|
+
this.#listeners.clear();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export const hooks = new Hooks();
|
|
235
|
+
export const $on: typeof hooks.on = hooks.on.bind(hooks);
|
|
236
|
+
export const $once: typeof hooks.once = hooks.once.bind(hooks);
|
|
237
|
+
export const $off: typeof hooks.off = hooks.off.bind(hooks);
|
|
238
|
+
export const $emit: typeof hooks.emit = hooks.emit.bind(hooks);
|
|
239
|
+
export const $asyncEmit: typeof hooks.asyncEmit = hooks.asyncEmit.bind(hooks);
|
|
240
|
+
export const $alter: typeof hooks.alter = hooks.alter.bind(hooks);
|
|
241
|
+
export const $asyncAlter: typeof hooks.asyncAlter = hooks.asyncAlter.bind(hooks);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Hooks.js";
|