@tsed/hooks 8.0.0 → 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 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.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.0",
38
- "@tsed/vitest": "8.0.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";