@flare-engine/events 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/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/index.d.mts +39 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +155 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +127 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Grzegorz Otto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @flare-engine/events
|
|
2
|
+
|
|
3
|
+
Type-safe event primitives — `EventEmitter<EventMap>` for keyed events and `Signal<T>` for single-payload broadcast. Both support `once()`, deferred-remove during `emit()`, and zero-alloc `Unsubscribe` returns.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun add @flare-engine/events
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { EventEmitter, Signal } from '@flare-engine/events';
|
|
15
|
+
|
|
16
|
+
type GameEvents = { 'enemy-killed': { id: number; score: number } };
|
|
17
|
+
const bus = new EventEmitter<GameEvents>();
|
|
18
|
+
const off = bus.on('enemy-killed', (e) => console.log(e.score));
|
|
19
|
+
|
|
20
|
+
const onHit = new Signal<number>();
|
|
21
|
+
onHit.add((dmg) => console.log(dmg));
|
|
22
|
+
onHit.emit(10);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
See [API spec](../../docs/design/api-specs.md#flare-engineevents--1kb-gzip) and [skill reference](../../.github/skills/flare-events/SKILL.md).
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
|
|
29
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Unsubscribe function returned by on/once. */
|
|
2
|
+
type Unsubscribe = () => void;
|
|
3
|
+
/** Map of event names to handler signatures. */
|
|
4
|
+
type EventMap = Record<string, (...args: never[]) => void>;
|
|
5
|
+
/** Type-safe event emitter. */
|
|
6
|
+
declare class EventEmitter<E extends EventMap> {
|
|
7
|
+
private listeners;
|
|
8
|
+
private iterating;
|
|
9
|
+
private pendingRemove;
|
|
10
|
+
/** Subscribe to an event. Returns unsubscribe function. */
|
|
11
|
+
on<K extends keyof E>(event: K, handler: E[K]): Unsubscribe;
|
|
12
|
+
/** Subscribe to an event once. Returns unsubscribe function. */
|
|
13
|
+
once<K extends keyof E>(event: K, handler: E[K]): Unsubscribe;
|
|
14
|
+
/** Unsubscribe a specific handler. */
|
|
15
|
+
off<K extends keyof E>(event: K, handler: E[K]): void;
|
|
16
|
+
/** Emit an event to all subscribers. Zero-alloc on the hot path. */
|
|
17
|
+
emit<K extends keyof E>(event: K, ...args: Parameters<E[K]>): void;
|
|
18
|
+
/** Remove all listeners for a specific event, or all listeners. */
|
|
19
|
+
removeAll(event?: keyof E): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Lightweight single-event emitter. Less overhead than EventEmitter. */
|
|
23
|
+
declare class Signal<T = void> {
|
|
24
|
+
private handlers;
|
|
25
|
+
private iterating;
|
|
26
|
+
private pendingRemove;
|
|
27
|
+
/** Subscribe to the signal. Returns unsubscribe function. */
|
|
28
|
+
add(handler: (value: T) => void): Unsubscribe;
|
|
29
|
+
/** Subscribe once. Returns unsubscribe function. */
|
|
30
|
+
once(handler: (value: T) => void): Unsubscribe;
|
|
31
|
+
/** Unsubscribe a specific handler. */
|
|
32
|
+
remove(handler: (value: T) => void): void;
|
|
33
|
+
/** Emit a value to all subscribers. Zero-alloc on the hot path. */
|
|
34
|
+
emit(...args: T extends void ? [] : [value: T]): void;
|
|
35
|
+
/** Remove all handlers. */
|
|
36
|
+
clear(): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { EventEmitter, type EventMap, Signal, type Unsubscribe };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Unsubscribe function returned by on/once. */
|
|
2
|
+
type Unsubscribe = () => void;
|
|
3
|
+
/** Map of event names to handler signatures. */
|
|
4
|
+
type EventMap = Record<string, (...args: never[]) => void>;
|
|
5
|
+
/** Type-safe event emitter. */
|
|
6
|
+
declare class EventEmitter<E extends EventMap> {
|
|
7
|
+
private listeners;
|
|
8
|
+
private iterating;
|
|
9
|
+
private pendingRemove;
|
|
10
|
+
/** Subscribe to an event. Returns unsubscribe function. */
|
|
11
|
+
on<K extends keyof E>(event: K, handler: E[K]): Unsubscribe;
|
|
12
|
+
/** Subscribe to an event once. Returns unsubscribe function. */
|
|
13
|
+
once<K extends keyof E>(event: K, handler: E[K]): Unsubscribe;
|
|
14
|
+
/** Unsubscribe a specific handler. */
|
|
15
|
+
off<K extends keyof E>(event: K, handler: E[K]): void;
|
|
16
|
+
/** Emit an event to all subscribers. Zero-alloc on the hot path. */
|
|
17
|
+
emit<K extends keyof E>(event: K, ...args: Parameters<E[K]>): void;
|
|
18
|
+
/** Remove all listeners for a specific event, or all listeners. */
|
|
19
|
+
removeAll(event?: keyof E): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Lightweight single-event emitter. Less overhead than EventEmitter. */
|
|
23
|
+
declare class Signal<T = void> {
|
|
24
|
+
private handlers;
|
|
25
|
+
private iterating;
|
|
26
|
+
private pendingRemove;
|
|
27
|
+
/** Subscribe to the signal. Returns unsubscribe function. */
|
|
28
|
+
add(handler: (value: T) => void): Unsubscribe;
|
|
29
|
+
/** Subscribe once. Returns unsubscribe function. */
|
|
30
|
+
once(handler: (value: T) => void): Unsubscribe;
|
|
31
|
+
/** Unsubscribe a specific handler. */
|
|
32
|
+
remove(handler: (value: T) => void): void;
|
|
33
|
+
/** Emit a value to all subscribers. Zero-alloc on the hot path. */
|
|
34
|
+
emit(...args: T extends void ? [] : [value: T]): void;
|
|
35
|
+
/** Remove all handlers. */
|
|
36
|
+
clear(): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { EventEmitter, type EventMap, Signal, type Unsubscribe };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
EventEmitter: () => EventEmitter,
|
|
24
|
+
Signal: () => Signal
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/emitter.ts
|
|
29
|
+
var EventEmitter = class {
|
|
30
|
+
listeners = /* @__PURE__ */ new Map();
|
|
31
|
+
iterating = false;
|
|
32
|
+
pendingRemove = [];
|
|
33
|
+
/** Subscribe to an event. Returns unsubscribe function. */
|
|
34
|
+
on(event, handler) {
|
|
35
|
+
let set = this.listeners.get(event);
|
|
36
|
+
if (!set) {
|
|
37
|
+
set = /* @__PURE__ */ new Set();
|
|
38
|
+
this.listeners.set(event, set);
|
|
39
|
+
}
|
|
40
|
+
set.add(handler);
|
|
41
|
+
return () => {
|
|
42
|
+
this.off(event, handler);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Subscribe to an event once. Returns unsubscribe function. */
|
|
46
|
+
once(event, handler) {
|
|
47
|
+
const wrapper = ((...args) => {
|
|
48
|
+
unsub();
|
|
49
|
+
handler(...args);
|
|
50
|
+
});
|
|
51
|
+
const unsub = this.on(event, wrapper);
|
|
52
|
+
return unsub;
|
|
53
|
+
}
|
|
54
|
+
/** Unsubscribe a specific handler. */
|
|
55
|
+
off(event, handler) {
|
|
56
|
+
if (this.iterating) {
|
|
57
|
+
this.pendingRemove.push({ event, handler });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const set = this.listeners.get(event);
|
|
61
|
+
if (set) {
|
|
62
|
+
set.delete(handler);
|
|
63
|
+
if (set.size === 0) this.listeners.delete(event);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Emit an event to all subscribers. Zero-alloc on the hot path. */
|
|
67
|
+
emit(event, ...args) {
|
|
68
|
+
const set = this.listeners.get(event);
|
|
69
|
+
if (!set) return;
|
|
70
|
+
this.iterating = true;
|
|
71
|
+
for (const handler of set) {
|
|
72
|
+
handler(...args);
|
|
73
|
+
}
|
|
74
|
+
this.iterating = false;
|
|
75
|
+
if (this.pendingRemove.length > 0) {
|
|
76
|
+
for (let i = 0; i < this.pendingRemove.length; i++) {
|
|
77
|
+
const { event: ev, handler: h } = this.pendingRemove[i];
|
|
78
|
+
const s = this.listeners.get(ev);
|
|
79
|
+
if (s) {
|
|
80
|
+
s.delete(h);
|
|
81
|
+
if (s.size === 0) this.listeners.delete(ev);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.pendingRemove.length = 0;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Remove all listeners for a specific event, or all listeners. */
|
|
88
|
+
removeAll(event) {
|
|
89
|
+
if (event !== void 0) {
|
|
90
|
+
this.listeners.delete(event);
|
|
91
|
+
} else {
|
|
92
|
+
this.listeners.clear();
|
|
93
|
+
}
|
|
94
|
+
this.pendingRemove.length = 0;
|
|
95
|
+
this.iterating = false;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// src/signal.ts
|
|
100
|
+
var Signal = class {
|
|
101
|
+
handlers = /* @__PURE__ */ new Set();
|
|
102
|
+
iterating = false;
|
|
103
|
+
pendingRemove = [];
|
|
104
|
+
/** Subscribe to the signal. Returns unsubscribe function. */
|
|
105
|
+
add(handler) {
|
|
106
|
+
this.handlers.add(handler);
|
|
107
|
+
return () => {
|
|
108
|
+
this.remove(handler);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Subscribe once. Returns unsubscribe function. */
|
|
112
|
+
once(handler) {
|
|
113
|
+
const wrapper = (value) => {
|
|
114
|
+
unsub();
|
|
115
|
+
handler(value);
|
|
116
|
+
};
|
|
117
|
+
const unsub = this.add(wrapper);
|
|
118
|
+
return unsub;
|
|
119
|
+
}
|
|
120
|
+
/** Unsubscribe a specific handler. */
|
|
121
|
+
remove(handler) {
|
|
122
|
+
if (this.iterating) {
|
|
123
|
+
this.pendingRemove.push(handler);
|
|
124
|
+
} else {
|
|
125
|
+
this.handlers.delete(handler);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Emit a value to all subscribers. Zero-alloc on the hot path. */
|
|
129
|
+
emit(...args) {
|
|
130
|
+
const value = args[0];
|
|
131
|
+
this.iterating = true;
|
|
132
|
+
for (const handler of this.handlers) {
|
|
133
|
+
handler(value);
|
|
134
|
+
}
|
|
135
|
+
this.iterating = false;
|
|
136
|
+
if (this.pendingRemove.length > 0) {
|
|
137
|
+
for (let i = 0; i < this.pendingRemove.length; i++) {
|
|
138
|
+
this.handlers.delete(this.pendingRemove[i]);
|
|
139
|
+
}
|
|
140
|
+
this.pendingRemove.length = 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Remove all handlers. */
|
|
144
|
+
clear() {
|
|
145
|
+
this.handlers.clear();
|
|
146
|
+
this.pendingRemove.length = 0;
|
|
147
|
+
this.iterating = false;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
151
|
+
0 && (module.exports = {
|
|
152
|
+
EventEmitter,
|
|
153
|
+
Signal
|
|
154
|
+
});
|
|
155
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/emitter.ts","../src/signal.ts"],"sourcesContent":["export { EventEmitter } from './emitter.js';\nexport type { Unsubscribe, EventMap } from './emitter.js';\nexport { Signal } from './signal.js';\n","/** Unsubscribe function returned by on/once. */\nexport type Unsubscribe = () => void;\n\n/** Map of event names to handler signatures. */\nexport type EventMap = Record<string, (...args: never[]) => void>;\n\n/** Type-safe event emitter. */\nexport class EventEmitter<E extends EventMap> {\n private listeners = new Map<keyof E, Set<E[keyof E]>>();\n private iterating = false;\n private pendingRemove: Array<{ event: keyof E; handler: E[keyof E] }> = [];\n\n /** Subscribe to an event. Returns unsubscribe function. */\n on<K extends keyof E>(event: K, handler: E[K]): Unsubscribe {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(handler);\n return () => {\n this.off(event, handler);\n };\n }\n\n /** Subscribe to an event once. Returns unsubscribe function. */\n once<K extends keyof E>(event: K, handler: E[K]): Unsubscribe {\n const wrapper = ((...args: never[]) => {\n unsub();\n // biome-ignore lint/complexity/noBannedTypes: generic handler call\n (handler as Function)(...args);\n }) as E[K];\n const unsub = this.on(event, wrapper);\n return unsub;\n }\n\n /** Unsubscribe a specific handler. */\n off<K extends keyof E>(event: K, handler: E[K]): void {\n if (this.iterating) {\n this.pendingRemove.push({ event, handler });\n return;\n }\n const set = this.listeners.get(event);\n if (set) {\n set.delete(handler);\n if (set.size === 0) this.listeners.delete(event);\n }\n }\n\n /** Emit an event to all subscribers. Zero-alloc on the hot path. */\n emit<K extends keyof E>(event: K, ...args: Parameters<E[K]>): void {\n const set = this.listeners.get(event);\n if (!set) return;\n this.iterating = true;\n for (const handler of set) {\n // biome-ignore lint/complexity/noBannedTypes: generic handler call\n (handler as Function)(...args);\n }\n this.iterating = false;\n // Flush pending removes (rare path — only when handler unsubscribed during emit)\n if (this.pendingRemove.length > 0) {\n for (let i = 0; i < this.pendingRemove.length; i++) {\n const { event: ev, handler: h } = this.pendingRemove[i]!;\n const s = this.listeners.get(ev);\n if (s) {\n s.delete(h);\n if (s.size === 0) this.listeners.delete(ev);\n }\n }\n this.pendingRemove.length = 0;\n }\n }\n\n /** Remove all listeners for a specific event, or all listeners. */\n removeAll(event?: keyof E): void {\n if (event !== undefined) {\n this.listeners.delete(event);\n } else {\n this.listeners.clear();\n }\n this.pendingRemove.length = 0;\n this.iterating = false;\n }\n}\n","import type { Unsubscribe } from './emitter.js';\n\n/** Lightweight single-event emitter. Less overhead than EventEmitter. */\nexport class Signal<T = void> {\n private handlers = new Set<(value: T) => void>();\n private iterating = false;\n private pendingRemove: Array<(value: T) => void> = [];\n\n /** Subscribe to the signal. Returns unsubscribe function. */\n add(handler: (value: T) => void): Unsubscribe {\n this.handlers.add(handler);\n return () => {\n this.remove(handler);\n };\n }\n\n /** Subscribe once. Returns unsubscribe function. */\n once(handler: (value: T) => void): Unsubscribe {\n const wrapper = (value: T) => {\n unsub();\n handler(value);\n };\n const unsub = this.add(wrapper);\n return unsub;\n }\n\n /** Unsubscribe a specific handler. */\n remove(handler: (value: T) => void): void {\n if (this.iterating) {\n this.pendingRemove.push(handler);\n } else {\n this.handlers.delete(handler);\n }\n }\n\n /** Emit a value to all subscribers. Zero-alloc on the hot path. */\n emit(...args: T extends void ? [] : [value: T]): void {\n const value = args[0] as T;\n this.iterating = true;\n for (const handler of this.handlers) {\n handler(value);\n }\n this.iterating = false;\n // Flush pending removes (rare path — only when handler removed itself during emit)\n if (this.pendingRemove.length > 0) {\n for (let i = 0; i < this.pendingRemove.length; i++) {\n this.handlers.delete(this.pendingRemove[i]!);\n }\n this.pendingRemove.length = 0;\n }\n }\n\n /** Remove all handlers. */\n clear(): void {\n this.handlers.clear();\n this.pendingRemove.length = 0;\n this.iterating = false;\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// src/emitter.ts
|
|
2
|
+
var EventEmitter = class {
|
|
3
|
+
listeners = /* @__PURE__ */ new Map();
|
|
4
|
+
iterating = false;
|
|
5
|
+
pendingRemove = [];
|
|
6
|
+
/** Subscribe to an event. Returns unsubscribe function. */
|
|
7
|
+
on(event, handler) {
|
|
8
|
+
let set = this.listeners.get(event);
|
|
9
|
+
if (!set) {
|
|
10
|
+
set = /* @__PURE__ */ new Set();
|
|
11
|
+
this.listeners.set(event, set);
|
|
12
|
+
}
|
|
13
|
+
set.add(handler);
|
|
14
|
+
return () => {
|
|
15
|
+
this.off(event, handler);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Subscribe to an event once. Returns unsubscribe function. */
|
|
19
|
+
once(event, handler) {
|
|
20
|
+
const wrapper = ((...args) => {
|
|
21
|
+
unsub();
|
|
22
|
+
handler(...args);
|
|
23
|
+
});
|
|
24
|
+
const unsub = this.on(event, wrapper);
|
|
25
|
+
return unsub;
|
|
26
|
+
}
|
|
27
|
+
/** Unsubscribe a specific handler. */
|
|
28
|
+
off(event, handler) {
|
|
29
|
+
if (this.iterating) {
|
|
30
|
+
this.pendingRemove.push({ event, handler });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const set = this.listeners.get(event);
|
|
34
|
+
if (set) {
|
|
35
|
+
set.delete(handler);
|
|
36
|
+
if (set.size === 0) this.listeners.delete(event);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Emit an event to all subscribers. Zero-alloc on the hot path. */
|
|
40
|
+
emit(event, ...args) {
|
|
41
|
+
const set = this.listeners.get(event);
|
|
42
|
+
if (!set) return;
|
|
43
|
+
this.iterating = true;
|
|
44
|
+
for (const handler of set) {
|
|
45
|
+
handler(...args);
|
|
46
|
+
}
|
|
47
|
+
this.iterating = false;
|
|
48
|
+
if (this.pendingRemove.length > 0) {
|
|
49
|
+
for (let i = 0; i < this.pendingRemove.length; i++) {
|
|
50
|
+
const { event: ev, handler: h } = this.pendingRemove[i];
|
|
51
|
+
const s = this.listeners.get(ev);
|
|
52
|
+
if (s) {
|
|
53
|
+
s.delete(h);
|
|
54
|
+
if (s.size === 0) this.listeners.delete(ev);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
this.pendingRemove.length = 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Remove all listeners for a specific event, or all listeners. */
|
|
61
|
+
removeAll(event) {
|
|
62
|
+
if (event !== void 0) {
|
|
63
|
+
this.listeners.delete(event);
|
|
64
|
+
} else {
|
|
65
|
+
this.listeners.clear();
|
|
66
|
+
}
|
|
67
|
+
this.pendingRemove.length = 0;
|
|
68
|
+
this.iterating = false;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// src/signal.ts
|
|
73
|
+
var Signal = class {
|
|
74
|
+
handlers = /* @__PURE__ */ new Set();
|
|
75
|
+
iterating = false;
|
|
76
|
+
pendingRemove = [];
|
|
77
|
+
/** Subscribe to the signal. Returns unsubscribe function. */
|
|
78
|
+
add(handler) {
|
|
79
|
+
this.handlers.add(handler);
|
|
80
|
+
return () => {
|
|
81
|
+
this.remove(handler);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Subscribe once. Returns unsubscribe function. */
|
|
85
|
+
once(handler) {
|
|
86
|
+
const wrapper = (value) => {
|
|
87
|
+
unsub();
|
|
88
|
+
handler(value);
|
|
89
|
+
};
|
|
90
|
+
const unsub = this.add(wrapper);
|
|
91
|
+
return unsub;
|
|
92
|
+
}
|
|
93
|
+
/** Unsubscribe a specific handler. */
|
|
94
|
+
remove(handler) {
|
|
95
|
+
if (this.iterating) {
|
|
96
|
+
this.pendingRemove.push(handler);
|
|
97
|
+
} else {
|
|
98
|
+
this.handlers.delete(handler);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Emit a value to all subscribers. Zero-alloc on the hot path. */
|
|
102
|
+
emit(...args) {
|
|
103
|
+
const value = args[0];
|
|
104
|
+
this.iterating = true;
|
|
105
|
+
for (const handler of this.handlers) {
|
|
106
|
+
handler(value);
|
|
107
|
+
}
|
|
108
|
+
this.iterating = false;
|
|
109
|
+
if (this.pendingRemove.length > 0) {
|
|
110
|
+
for (let i = 0; i < this.pendingRemove.length; i++) {
|
|
111
|
+
this.handlers.delete(this.pendingRemove[i]);
|
|
112
|
+
}
|
|
113
|
+
this.pendingRemove.length = 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Remove all handlers. */
|
|
117
|
+
clear() {
|
|
118
|
+
this.handlers.clear();
|
|
119
|
+
this.pendingRemove.length = 0;
|
|
120
|
+
this.iterating = false;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
export {
|
|
124
|
+
EventEmitter,
|
|
125
|
+
Signal
|
|
126
|
+
};
|
|
127
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/emitter.ts","../src/signal.ts"],"sourcesContent":["/** Unsubscribe function returned by on/once. */\nexport type Unsubscribe = () => void;\n\n/** Map of event names to handler signatures. */\nexport type EventMap = Record<string, (...args: never[]) => void>;\n\n/** Type-safe event emitter. */\nexport class EventEmitter<E extends EventMap> {\n private listeners = new Map<keyof E, Set<E[keyof E]>>();\n private iterating = false;\n private pendingRemove: Array<{ event: keyof E; handler: E[keyof E] }> = [];\n\n /** Subscribe to an event. Returns unsubscribe function. */\n on<K extends keyof E>(event: K, handler: E[K]): Unsubscribe {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(handler);\n return () => {\n this.off(event, handler);\n };\n }\n\n /** Subscribe to an event once. Returns unsubscribe function. */\n once<K extends keyof E>(event: K, handler: E[K]): Unsubscribe {\n const wrapper = ((...args: never[]) => {\n unsub();\n // biome-ignore lint/complexity/noBannedTypes: generic handler call\n (handler as Function)(...args);\n }) as E[K];\n const unsub = this.on(event, wrapper);\n return unsub;\n }\n\n /** Unsubscribe a specific handler. */\n off<K extends keyof E>(event: K, handler: E[K]): void {\n if (this.iterating) {\n this.pendingRemove.push({ event, handler });\n return;\n }\n const set = this.listeners.get(event);\n if (set) {\n set.delete(handler);\n if (set.size === 0) this.listeners.delete(event);\n }\n }\n\n /** Emit an event to all subscribers. Zero-alloc on the hot path. */\n emit<K extends keyof E>(event: K, ...args: Parameters<E[K]>): void {\n const set = this.listeners.get(event);\n if (!set) return;\n this.iterating = true;\n for (const handler of set) {\n // biome-ignore lint/complexity/noBannedTypes: generic handler call\n (handler as Function)(...args);\n }\n this.iterating = false;\n // Flush pending removes (rare path — only when handler unsubscribed during emit)\n if (this.pendingRemove.length > 0) {\n for (let i = 0; i < this.pendingRemove.length; i++) {\n const { event: ev, handler: h } = this.pendingRemove[i]!;\n const s = this.listeners.get(ev);\n if (s) {\n s.delete(h);\n if (s.size === 0) this.listeners.delete(ev);\n }\n }\n this.pendingRemove.length = 0;\n }\n }\n\n /** Remove all listeners for a specific event, or all listeners. */\n removeAll(event?: keyof E): void {\n if (event !== undefined) {\n this.listeners.delete(event);\n } else {\n this.listeners.clear();\n }\n this.pendingRemove.length = 0;\n this.iterating = false;\n }\n}\n","import type { Unsubscribe } from './emitter.js';\n\n/** Lightweight single-event emitter. Less overhead than EventEmitter. */\nexport class Signal<T = void> {\n private handlers = new Set<(value: T) => void>();\n private iterating = false;\n private pendingRemove: Array<(value: T) => void> = [];\n\n /** Subscribe to the signal. Returns unsubscribe function. */\n add(handler: (value: T) => void): Unsubscribe {\n this.handlers.add(handler);\n return () => {\n this.remove(handler);\n };\n }\n\n /** Subscribe once. Returns unsubscribe function. */\n once(handler: (value: T) => void): Unsubscribe {\n const wrapper = (value: T) => {\n unsub();\n handler(value);\n };\n const unsub = this.add(wrapper);\n return unsub;\n }\n\n /** Unsubscribe a specific handler. */\n remove(handler: (value: T) => void): void {\n if (this.iterating) {\n this.pendingRemove.push(handler);\n } else {\n this.handlers.delete(handler);\n }\n }\n\n /** Emit a value to all subscribers. Zero-alloc on the hot path. */\n emit(...args: T extends void ? [] : [value: T]): void {\n const value = args[0] as T;\n this.iterating = true;\n for (const handler of this.handlers) {\n handler(value);\n }\n this.iterating = false;\n // Flush pending removes (rare path — only when handler removed itself during emit)\n if (this.pendingRemove.length > 0) {\n for (let i = 0; i < this.pendingRemove.length; i++) {\n this.handlers.delete(this.pendingRemove[i]!);\n }\n this.pendingRemove.length = 0;\n }\n }\n\n /** Remove all handlers. */\n clear(): void {\n this.handlers.clear();\n this.pendingRemove.length = 0;\n this.iterating = false;\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flare-engine/events",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Type-safe EventEmitter and Signal",
|
|
5
|
+
"keywords": ["flare-engine", "events", "signal", "emitter"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Grzegorz Otto",
|
|
8
|
+
"homepage": "https://github.com/grzott/flare-engine#readme",
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/grzott/flare-engine/issues"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/grzott/flare-engine.git",
|
|
15
|
+
"directory": "packages/events"
|
|
16
|
+
},
|
|
17
|
+
"main": "dist/index.js",
|
|
18
|
+
"module": "dist/index.mjs",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.mjs",
|
|
24
|
+
"require": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"test": "bun test",
|
|
31
|
+
"lint": "biome check src/",
|
|
32
|
+
"typecheck": "tsc --noEmit"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "restricted",
|
|
36
|
+
"registry": "https://registry.npmjs.org/"
|
|
37
|
+
},
|
|
38
|
+
"sideEffects": false
|
|
39
|
+
}
|