ts-util-core 2.0.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.
Files changed (63) hide show
  1. package/README.md +411 -0
  2. package/dist/core/ajax.d.ts +30 -0
  3. package/dist/core/ajax.d.ts.map +1 -0
  4. package/dist/core/ajax.js +110 -0
  5. package/dist/core/ajax.js.map +1 -0
  6. package/dist/core/event-emitter.d.ts +29 -0
  7. package/dist/core/event-emitter.d.ts.map +1 -0
  8. package/dist/core/event-emitter.js +67 -0
  9. package/dist/core/event-emitter.js.map +1 -0
  10. package/dist/core/message.d.ts +28 -0
  11. package/dist/core/message.d.ts.map +1 -0
  12. package/dist/core/message.js +172 -0
  13. package/dist/core/message.js.map +1 -0
  14. package/dist/core/view.d.ts +49 -0
  15. package/dist/core/view.d.ts.map +1 -0
  16. package/dist/core/view.js +87 -0
  17. package/dist/core/view.js.map +1 -0
  18. package/dist/formatting/formatters.d.ts +9 -0
  19. package/dist/formatting/formatters.d.ts.map +1 -0
  20. package/dist/formatting/formatters.js +109 -0
  21. package/dist/formatting/formatters.js.map +1 -0
  22. package/dist/formatting/registry.d.ts +31 -0
  23. package/dist/formatting/registry.d.ts.map +1 -0
  24. package/dist/formatting/registry.js +49 -0
  25. package/dist/formatting/registry.js.map +1 -0
  26. package/dist/index.d.ts +31 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +104 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/types.d.ts +66 -0
  31. package/dist/types.d.ts.map +1 -0
  32. package/dist/types.js +5 -0
  33. package/dist/types.js.map +1 -0
  34. package/dist/utils/dom.d.ts +38 -0
  35. package/dist/utils/dom.d.ts.map +1 -0
  36. package/dist/utils/dom.js +95 -0
  37. package/dist/utils/dom.js.map +1 -0
  38. package/dist/utils/sprintf.d.ts +16 -0
  39. package/dist/utils/sprintf.d.ts.map +1 -0
  40. package/dist/utils/sprintf.js +116 -0
  41. package/dist/utils/sprintf.js.map +1 -0
  42. package/dist/validation/constraints.d.ts +23 -0
  43. package/dist/validation/constraints.d.ts.map +1 -0
  44. package/dist/validation/constraints.js +131 -0
  45. package/dist/validation/constraints.js.map +1 -0
  46. package/dist/validation/validator.d.ts +45 -0
  47. package/dist/validation/validator.d.ts.map +1 -0
  48. package/dist/validation/validator.js +210 -0
  49. package/dist/validation/validator.js.map +1 -0
  50. package/package.json +26 -0
  51. package/readme.txt +4 -0
  52. package/src/core/ajax.ts +127 -0
  53. package/src/core/event-emitter.ts +84 -0
  54. package/src/core/message.ts +212 -0
  55. package/src/core/view.ts +101 -0
  56. package/src/formatting/formatters.ts +118 -0
  57. package/src/formatting/registry.ts +53 -0
  58. package/src/index.ts +142 -0
  59. package/src/types.ts +85 -0
  60. package/src/utils/dom.ts +105 -0
  61. package/src/utils/sprintf.ts +141 -0
  62. package/src/validation/constraints.ts +168 -0
  63. package/src/validation/validator.ts +276 -0
@@ -0,0 +1,45 @@
1
+ import type { EventEmitter } from '../core/event-emitter.js';
2
+ import type { AppEventMap } from '../types.js';
3
+ import { type ConstraintHandler } from './constraints.js';
4
+ type InvalidCallback = (labelNames: string[], elements: HTMLElement[]) => void;
5
+ type TextareaTooLongCallback = (labelNames: string[], maxlengths: number[], elements: HTMLElement[]) => void;
6
+ export declare class Validator {
7
+ private emitter;
8
+ private constraints;
9
+ private requiredInvalidCb;
10
+ private textareaTooLongCb;
11
+ constructor(emitter: EventEmitter<AppEventMap>);
12
+ /**
13
+ * Register a custom constraint handler.
14
+ *
15
+ * Registry Pattern — extend validation without modifying library code.
16
+ */
17
+ addConstraint(handler: ConstraintHandler): void;
18
+ /**
19
+ * Override the default behavior when required-field validation fails.
20
+ *
21
+ * Strategy Pattern — consumers inject their own notification UI.
22
+ */
23
+ setRequiredInvalidCallback(cb: InvalidCallback): void;
24
+ setTextareaTooLongCallback(cb: TextareaTooLongCallback): void;
25
+ /**
26
+ * Attach constraint handlers to all matching elements within `context`.
27
+ *
28
+ * This is the **beforeLoad hook** — it is registered with `VIEW.addBeforeLoad`
29
+ * so that dynamically loaded content is also initialized.
30
+ */
31
+ initConstraints(context: HTMLElement): void;
32
+ private bindIgnoreValidation;
33
+ /**
34
+ * Validate all required fields and textarea lengths within a container.
35
+ *
36
+ * @returns `true` if all validations pass.
37
+ */
38
+ validate(container: HTMLElement): boolean;
39
+ private checkTextareaLengths;
40
+ private checkRequired;
41
+ private handleRequiredInvalid;
42
+ private handleTextareaTooLong;
43
+ }
44
+ export {};
45
+ //# sourceMappingURL=validator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/validation/validator.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAA8C,MAAM,aAAa,CAAC;AAE3F,OAAO,EACL,KAAK,iBAAiB,EAEvB,MAAM,kBAAkB,CAAC;AAE1B,KAAK,eAAe,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC;AAC/E,KAAK,uBAAuB,GAAG,CAC7B,UAAU,EAAE,MAAM,EAAE,EACpB,UAAU,EAAE,MAAM,EAAE,EACpB,QAAQ,EAAE,WAAW,EAAE,KACpB,IAAI,CAAC;AAEV,qBAAa,SAAS;IACpB,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,WAAW,CAA6C;IAChE,OAAO,CAAC,iBAAiB,CAAgC;IACzD,OAAO,CAAC,iBAAiB,CAAwC;gBAErD,OAAO,EAAE,YAAY,CAAC,WAAW,CAAC;IAW9C;;;;OAIG;IACH,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI;IAM/C;;;;OAIG;IACH,0BAA0B,CAAC,EAAE,EAAE,eAAe,GAAG,IAAI;IAIrD,0BAA0B,CAAC,EAAE,EAAE,uBAAuB,GAAG,IAAI;IAM7D;;;;;OAKG;IACH,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI;IA+B3C,OAAO,CAAC,oBAAoB;IAyB5B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,WAAW,GAAG,OAAO;IAkBzC,OAAO,CAAC,oBAAoB;IAuB5B,OAAO,CAAC,aAAa;IA2DrB,OAAO,CAAC,qBAAqB;IAiB7B,OAAO,CAAC,qBAAqB;CAuB9B"}
@@ -0,0 +1,210 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Validator — form validation engine
3
+ //
4
+ // Design patterns used:
5
+ // - Observer Pattern : integrates with VIEW.addBeforeLoad to auto-init
6
+ // - Strategy Pattern : invalid-field handling is injectable via the emitter
7
+ // - Decorator Pattern : constraints are attached to elements by attribute
8
+ // - Registry Pattern : ConstraintHandler lookup by name
9
+ // ---------------------------------------------------------------------------
10
+ import { scrollToElement } from '../utils/dom.js';
11
+ import { builtInConstraints, } from './constraints.js';
12
+ export class Validator {
13
+ emitter;
14
+ constraints = new Map();
15
+ requiredInvalidCb = null;
16
+ textareaTooLongCb = null;
17
+ constructor(emitter) {
18
+ this.emitter = emitter;
19
+ // Register built-in constraints
20
+ for (const c of builtInConstraints) {
21
+ this.constraints.set(c.name, c);
22
+ }
23
+ }
24
+ // -- Constraint Registration ------------------------------------------------
25
+ /**
26
+ * Register a custom constraint handler.
27
+ *
28
+ * Registry Pattern — extend validation without modifying library code.
29
+ */
30
+ addConstraint(handler) {
31
+ this.constraints.set(handler.name, handler);
32
+ }
33
+ // -- Callback Setters (Strategy Pattern) ------------------------------------
34
+ /**
35
+ * Override the default behavior when required-field validation fails.
36
+ *
37
+ * Strategy Pattern — consumers inject their own notification UI.
38
+ */
39
+ setRequiredInvalidCallback(cb) {
40
+ this.requiredInvalidCb = cb;
41
+ }
42
+ setTextareaTooLongCallback(cb) {
43
+ this.textareaTooLongCb = cb;
44
+ }
45
+ // -- Initialization (called by VIEW.addBeforeLoad) --------------------------
46
+ /**
47
+ * Attach constraint handlers to all matching elements within `context`.
48
+ *
49
+ * This is the **beforeLoad hook** — it is registered with `VIEW.addBeforeLoad`
50
+ * so that dynamically loaded content is also initialized.
51
+ */
52
+ initConstraints(context) {
53
+ const elements = context.querySelectorAll('[constraint]');
54
+ for (const el of elements) {
55
+ const tokens = (el.getAttribute('constraint') || '').split(/\s+/);
56
+ for (const token of tokens) {
57
+ if (!token)
58
+ continue;
59
+ const handler = this.constraints.get(token);
60
+ handler?.attach(el);
61
+ }
62
+ }
63
+ // Auto-select text on focus (text inputs and textareas)
64
+ const focusTargets = context.querySelectorAll('input[type="text"], textarea');
65
+ for (const el of focusTargets) {
66
+ el.addEventListener('focus', () => {
67
+ setTimeout(() => el.select(), 1);
68
+ });
69
+ }
70
+ // Ignore-validation submit buttons
71
+ this.bindIgnoreValidation(context);
72
+ }
73
+ // -- Form Submit Integration ------------------------------------------------
74
+ bindIgnoreValidation(context) {
75
+ const buttons = context.querySelectorAll("input[type='submit'][ignoreValidation='true']");
76
+ for (const btn of buttons) {
77
+ btn.addEventListener('click', () => {
78
+ btn.closest('form')?.classList.add('ignoreValidation');
79
+ });
80
+ }
81
+ const forms = context.querySelectorAll('form');
82
+ for (const form of forms) {
83
+ form.addEventListener('submit', (e) => {
84
+ if (!form.classList.contains('ignoreValidation')) {
85
+ if (!this.validate(form)) {
86
+ e.preventDefault();
87
+ }
88
+ }
89
+ form.classList.remove('ignoreValidation');
90
+ });
91
+ }
92
+ }
93
+ // -- Core Validation Logic --------------------------------------------------
94
+ /**
95
+ * Validate all required fields and textarea lengths within a container.
96
+ *
97
+ * @returns `true` if all validations pass.
98
+ */
99
+ validate(container) {
100
+ // Check textarea max-length first
101
+ const tooLong = this.checkTextareaLengths(container);
102
+ if (!tooLong.valid) {
103
+ this.handleTextareaTooLong(tooLong);
104
+ return false;
105
+ }
106
+ // Check required fields
107
+ const result = this.checkRequired(container);
108
+ if (!result.valid) {
109
+ this.handleRequiredInvalid(result);
110
+ return false;
111
+ }
112
+ return true;
113
+ }
114
+ checkTextareaLengths(container) {
115
+ const textareas = container.querySelectorAll('textarea[maxlength]');
116
+ const invalidElements = [];
117
+ const labelNames = [];
118
+ const maxlengths = [];
119
+ for (const ta of textareas) {
120
+ const max = parseInt(ta.getAttribute('maxlength') || '0', 10);
121
+ if (ta.value.length > max) {
122
+ invalidElements.push(ta);
123
+ labelNames.push(ta.getAttribute('labelName') || ta.name || '');
124
+ maxlengths.push(max);
125
+ }
126
+ }
127
+ return {
128
+ valid: invalidElements.length === 0,
129
+ invalidElements,
130
+ labelNames,
131
+ maxlengths,
132
+ };
133
+ }
134
+ checkRequired(container) {
135
+ const elements = container.querySelectorAll("[constraint~='required']");
136
+ // Group by name (for radio/checkbox groups)
137
+ const groups = new Map();
138
+ let idCounter = 0;
139
+ for (const el of elements) {
140
+ const input = el;
141
+ let name = input.name || `__unnamed_${idCounter++}`;
142
+ if (!groups.has(name)) {
143
+ groups.set(name, []);
144
+ }
145
+ groups.get(name).push(el);
146
+ }
147
+ const invalidElements = [];
148
+ const labelNames = [];
149
+ for (const [, groupElements] of groups) {
150
+ let notEmpty = false;
151
+ for (const el of groupElements) {
152
+ const input = el;
153
+ if (input.type === 'radio' || input.type === 'checkbox') {
154
+ if (input.checked)
155
+ notEmpty = true;
156
+ }
157
+ else if ((el.getAttribute('constraint') || '').includes('number')) {
158
+ if (input.value !== '0' && input.value !== '')
159
+ notEmpty = true;
160
+ }
161
+ else if (input.value && input.value !== '') {
162
+ notEmpty = true;
163
+ }
164
+ }
165
+ if (!notEmpty) {
166
+ const first = groupElements[0];
167
+ invalidElements.push(first);
168
+ labelNames.push(first.getAttribute('labelName') ||
169
+ first.name ||
170
+ '');
171
+ }
172
+ }
173
+ return {
174
+ valid: invalidElements.length === 0,
175
+ invalidElements,
176
+ labelNames,
177
+ };
178
+ }
179
+ // -- Default Failure Handlers -----------------------------------------------
180
+ handleRequiredInvalid(result) {
181
+ this.emitter.emit('validation:invalid', {
182
+ labelNames: result.labelNames,
183
+ elements: result.invalidElements,
184
+ });
185
+ if (this.requiredInvalidCb) {
186
+ this.requiredInvalidCb(result.labelNames, result.invalidElements);
187
+ return;
188
+ }
189
+ // Default behavior: alert + scroll
190
+ const text = result.labelNames.map((n) => `"${n}"`).join(', ');
191
+ scrollToElement(result.invalidElements[0]);
192
+ window.alert(`${text} is required!`);
193
+ }
194
+ handleTextareaTooLong(result) {
195
+ this.emitter.emit('validation:textareaTooLong', {
196
+ labelNames: result.labelNames,
197
+ maxlengths: result.maxlengths,
198
+ elements: result.invalidElements,
199
+ });
200
+ if (this.textareaTooLongCb) {
201
+ this.textareaTooLongCb(result.labelNames, result.maxlengths, result.invalidElements);
202
+ return;
203
+ }
204
+ // Default behavior: alert + scroll
205
+ const lines = result.labelNames.map((name, i) => `"${name}" length is too long, max length is ${result.maxlengths[i]}!`);
206
+ scrollToElement(result.invalidElements[0]);
207
+ window.alert(lines.join('\n'));
208
+ }
209
+ }
210
+ //# sourceMappingURL=validator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/validation/validator.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "ts-util-core",
3
+ "version": "2.0.0",
4
+ "description": "TS-Util Core Library — form validation, AJAX, view loading, messaging, and formatting utilities",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "dev": "tsc --watch"
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "^5.7.0"
24
+ },
25
+ "license": "MIT"
26
+ }
package/readme.txt ADDED
@@ -0,0 +1,4 @@
1
+ It depends on
2
+ 1. jQuery
3
+ 2. Spinner (http://fgnass.github.io/spin.js/)
4
+ 3. Messi (https://github.com/marcosesperon/Messi/)
@@ -0,0 +1,127 @@
1
+ // ---------------------------------------------------------------------------
2
+ // AJAX module — fetch-based HTTP client with lifecycle hooks
3
+ //
4
+ // Design patterns used:
5
+ // - Facade Pattern : one `request()` call orchestrates validate →
6
+ // block → serialize → fetch → unblock
7
+ // - Template Method Pattern: `requestJSON` / `post` / `postJSON` delegate
8
+ // to the base `request` with tweaked options
9
+ // - Strategy Pattern : loading overlay behavior is injected via
10
+ // the EventEmitter (ajax:before / ajax:after)
11
+ // ---------------------------------------------------------------------------
12
+
13
+ import type { EventEmitter } from './event-emitter.js';
14
+ import type { AppEventMap, AjaxRequestParams, AjaxJsonRequestParams } from '../types.js';
15
+ import { formToJSON } from '../utils/dom.js';
16
+
17
+ export class Ajax {
18
+ private emitter: EventEmitter<AppEventMap>;
19
+ private validateForm: ((form: HTMLElement) => boolean) | null = null;
20
+
21
+ constructor(emitter: EventEmitter<AppEventMap>) {
22
+ this.emitter = emitter;
23
+ }
24
+
25
+ /** Register the form validation function (injected by the Validation module). */
26
+ setValidator(fn: (form: HTMLElement) => boolean): void {
27
+ this.validateForm = fn;
28
+ }
29
+
30
+ /**
31
+ * Send an HTTP request (POST by default).
32
+ *
33
+ * Lifecycle:
34
+ * 1. Validate form (if `params.form` provided)
35
+ * 2. Emit `ajax:before`
36
+ * 3. Serialize form data + merge `params.data`
37
+ * 4. `fetch()`
38
+ * 5. Emit `ajax:after` (on success) or `ajax:error` (on failure)
39
+ * 6. Call `params.success` / `params.error` / `params.complete`
40
+ *
41
+ * @returns The raw `Response`, or `undefined` if validation failed.
42
+ */
43
+ async request(params: AjaxRequestParams): Promise<Response | undefined> {
44
+ // 1. Validate
45
+ if (params.form && this.validateForm) {
46
+ if (!this.validateForm(params.form)) return undefined;
47
+ }
48
+
49
+ // 2. Before-hook
50
+ if (!params.noblock) {
51
+ this.emitter.emit('ajax:before', { url: params.url });
52
+ }
53
+
54
+ // 3. Build body
55
+ const body: Record<string, unknown> = {};
56
+
57
+ if (params.form) {
58
+ const formData = formToJSON(params.form, {
59
+ ignoreDisabled: params.ignoreDisabled,
60
+ });
61
+ Object.assign(body, formData);
62
+ }
63
+
64
+ if (params.data) {
65
+ Object.assign(body, params.data);
66
+ }
67
+
68
+ // 4. Fetch
69
+ const headers: Record<string, string> = {
70
+ 'Content-Type': 'application/json',
71
+ 'Ajax-Call': 'true',
72
+ ...params.headers,
73
+ };
74
+
75
+ try {
76
+ const response = await fetch(params.url, {
77
+ method: 'POST',
78
+ headers,
79
+ body: JSON.stringify(body),
80
+ });
81
+
82
+ if (!response.ok) {
83
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
84
+ }
85
+
86
+ // 5a. After-hook (success)
87
+ if (!params.noblock) {
88
+ this.emitter.emit('ajax:after', { url: params.url });
89
+ }
90
+
91
+ params.success?.(response);
92
+ return response;
93
+ } catch (err) {
94
+ const error = err instanceof Error ? err : new Error(String(err));
95
+
96
+ // 5b. After-hook (error)
97
+ if (!params.noblock) {
98
+ this.emitter.emit('ajax:after', { url: params.url });
99
+ }
100
+ this.emitter.emit('ajax:error', { url: params.url, error });
101
+
102
+ params.error?.(error);
103
+ return undefined;
104
+ } finally {
105
+ params.complete?.();
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Send a request and parse the response as JSON.
111
+ *
112
+ * Template Method — delegates to `request()`, adds JSON parsing.
113
+ */
114
+ async requestJSON<T = unknown>(
115
+ params: AjaxJsonRequestParams<T>,
116
+ ): Promise<T | undefined> {
117
+ const response = await this.request({
118
+ ...params,
119
+ success: undefined, // we handle success after JSON parse
120
+ });
121
+ if (!response) return undefined;
122
+
123
+ const data = (await response.json()) as T;
124
+ params.success?.(data);
125
+ return data;
126
+ }
127
+ }
@@ -0,0 +1,84 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Typed EventEmitter — the unified hook / callback system
3
+ //
4
+ // Design patterns used:
5
+ // - Observer Pattern : multiple listeners subscribe to named events
6
+ // - Strategy Pattern : consumers replace default behavior via listeners
7
+ // - Generics : the event map is fully typed at compile time
8
+ // ---------------------------------------------------------------------------
9
+
10
+ /**
11
+ * A fully typed event emitter.
12
+ *
13
+ * `TEvents` is a map of `{ eventName: payloadType }`.
14
+ * All `on`, `off`, and `emit` calls are type-checked against this map.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const emitter = new EventEmitter<{ 'ajax:before': { url: string } }>();
19
+ * emitter.on('ajax:before', ({ url }) => console.log(url));
20
+ * emitter.emit('ajax:before', { url: '/api' });
21
+ * ```
22
+ */
23
+ export class EventEmitter<TEvents extends { [K in keyof TEvents]: unknown }> {
24
+ private listeners = new Map<
25
+ keyof TEvents,
26
+ Set<(payload: never) => void>
27
+ >();
28
+
29
+ /** Subscribe to an event. Returns an unsubscribe function. */
30
+ on<K extends keyof TEvents>(
31
+ event: K,
32
+ listener: (payload: TEvents[K]) => void,
33
+ ): () => void {
34
+ if (!this.listeners.has(event)) {
35
+ this.listeners.set(event, new Set());
36
+ }
37
+ const set = this.listeners.get(event)!;
38
+ set.add(listener as (payload: never) => void);
39
+
40
+ // Return unsubscribe function for convenience
41
+ return () => {
42
+ set.delete(listener as (payload: never) => void);
43
+ };
44
+ }
45
+
46
+ /** Unsubscribe a specific listener. */
47
+ off<K extends keyof TEvents>(
48
+ event: K,
49
+ listener: (payload: TEvents[K]) => void,
50
+ ): void {
51
+ this.listeners.get(event)?.delete(listener as (payload: never) => void);
52
+ }
53
+
54
+ /** Subscribe to an event — listener is automatically removed after one call. */
55
+ once<K extends keyof TEvents>(
56
+ event: K,
57
+ listener: (payload: TEvents[K]) => void,
58
+ ): () => void {
59
+ const wrapper = ((payload: TEvents[K]) => {
60
+ this.off(event, wrapper);
61
+ listener(payload);
62
+ }) as (payload: TEvents[K]) => void;
63
+
64
+ return this.on(event, wrapper);
65
+ }
66
+
67
+ /** Emit an event, calling all registered listeners with the payload. */
68
+ emit<K extends keyof TEvents>(event: K, payload: TEvents[K]): void {
69
+ const set = this.listeners.get(event);
70
+ if (!set) return;
71
+ for (const listener of set) {
72
+ (listener as (payload: TEvents[K]) => void)(payload);
73
+ }
74
+ }
75
+
76
+ /** Remove all listeners for a specific event, or all events if none specified. */
77
+ clear(event?: keyof TEvents): void {
78
+ if (event) {
79
+ this.listeners.delete(event);
80
+ } else {
81
+ this.listeners.clear();
82
+ }
83
+ }
84
+ }