@violice/rmu 0.2.5 → 0.2.7

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/README.md CHANGED
@@ -5,14 +5,14 @@ RMU is a small, zero-dependency utility to control modals in React apps
5
5
  <!-- ## Installation
6
6
 
7
7
  ```bash
8
- npm install --save rmu
9
- yarn add rmu
8
+ npm install --save @violice/rmu
9
+ yarn add @violice/rmu
10
10
  ``` -->
11
11
 
12
12
  ## Usage
13
13
 
14
14
  ```js
15
- import { openModal, closeModal, RMUOutlet, RMUProvider } from 'rmu';
15
+ import { openModal, closeModal, RMUOutlet, RMUProvider } from '@violice/rmu';
16
16
 
17
17
  const Example = () => {
18
18
  return (
@@ -45,7 +45,7 @@ const Example = () => {
45
45
 
46
46
  ## RoadMap
47
47
  - [x] Custom events, new API (0.2.0)
48
- - [ ] Connected modals (Context modals)
48
+ - [ ] Connected modals
49
49
  - [ ] Auto-close
50
50
  - [ ] Use for toasts example
51
51
 
@@ -0,0 +1,49 @@
1
+ import React, { ReactNode } from "react";
2
+
3
+ //#region src/rmu-provider.d.ts
4
+ declare const RMUProvider: ({
5
+ children
6
+ }: {
7
+ children: React.ReactNode;
8
+ }) => React.JSX.Element;
9
+ //#endregion
10
+ //#region src/rmu-outlet.d.ts
11
+ declare const RMUOutlet: ({
12
+ outletId
13
+ }: {
14
+ outletId?: string | undefined;
15
+ }) => React.JSX.Element;
16
+ //#endregion
17
+ //#region src/types.d.ts
18
+ type RMUContextState = {
19
+ outlets: Record<string, Record<string, ReactNode>>;
20
+ openModal: ({
21
+ modalId,
22
+ modalComponent,
23
+ outletId
24
+ }: {
25
+ modalId: string;
26
+ modalComponent: ReactNode;
27
+ outletId: string;
28
+ }) => void;
29
+ closeModal: ({
30
+ modalId,
31
+ outletId
32
+ }: {
33
+ modalId: string;
34
+ outletId: string;
35
+ }) => void;
36
+ addOutlet: (outletId: string) => void;
37
+ removeOutlet: (outletId: string) => void;
38
+ };
39
+ //#endregion
40
+ //#region src/events.d.ts
41
+ declare const openModal: (modalComponent: ReactNode, config?: {
42
+ outletId?: string;
43
+ }) => {
44
+ modalId: string;
45
+ outletId: string;
46
+ };
47
+ declare const closeModal: RMUContextState['closeModal'];
48
+ //#endregion
49
+ export { RMUOutlet, RMUProvider, closeModal, openModal };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,49 @@
1
- export { RMUProvider } from './rmu-provider';
2
- export { RMUOutlet } from './rmu-outlet';
3
- export { openModal, closeModal } from './events';
1
+ import React, { ReactNode } from "react";
2
+
3
+ //#region src/rmu-provider.d.ts
4
+ declare const RMUProvider: ({
5
+ children
6
+ }: {
7
+ children: React.ReactNode;
8
+ }) => React.JSX.Element;
9
+ //#endregion
10
+ //#region src/rmu-outlet.d.ts
11
+ declare const RMUOutlet: ({
12
+ outletId
13
+ }: {
14
+ outletId?: string | undefined;
15
+ }) => React.JSX.Element;
16
+ //#endregion
17
+ //#region src/types.d.ts
18
+ type RMUContextState = {
19
+ outlets: Record<string, Record<string, ReactNode>>;
20
+ openModal: ({
21
+ modalId,
22
+ modalComponent,
23
+ outletId
24
+ }: {
25
+ modalId: string;
26
+ modalComponent: ReactNode;
27
+ outletId: string;
28
+ }) => void;
29
+ closeModal: ({
30
+ modalId,
31
+ outletId
32
+ }: {
33
+ modalId: string;
34
+ outletId: string;
35
+ }) => void;
36
+ addOutlet: (outletId: string) => void;
37
+ removeOutlet: (outletId: string) => void;
38
+ };
39
+ //#endregion
40
+ //#region src/events.d.ts
41
+ declare const openModal: (modalComponent: ReactNode, config?: {
42
+ outletId?: string;
43
+ }) => {
44
+ modalId: string;
45
+ outletId: string;
46
+ };
47
+ declare const closeModal: RMUContextState['closeModal'];
48
+ //#endregion
49
+ export { RMUOutlet, RMUProvider, closeModal, openModal };
package/dist/index.js CHANGED
@@ -1,8 +1,231 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
1
22
 
2
- 'use strict'
23
+ //#endregion
24
+ let react = require("react");
25
+ react = __toESM(react);
3
26
 
4
- if (process.env.NODE_ENV === 'production') {
5
- module.exports = require('./rmu.cjs.production.min.js')
6
- } else {
7
- module.exports = require('./rmu.cjs.development.js')
8
- }
27
+ //#region src/rmu-context.ts
28
+ const RMUContext = (0, react.createContext)(null);
29
+
30
+ //#endregion
31
+ //#region src/emitter.ts
32
+ const listenersByType = {};
33
+ const emitter = {
34
+ subscribe(type, handler) {
35
+ if (!listenersByType[type]) listenersByType[type] = /* @__PURE__ */ new Set();
36
+ listenersByType[type].add(handler);
37
+ },
38
+ unsubscribe(type, handler) {
39
+ const listeners = listenersByType[type];
40
+ if (listeners) {
41
+ listeners.delete(handler);
42
+ if (listeners.size === 0) delete listenersByType[type];
43
+ }
44
+ },
45
+ emit(type, payload) {
46
+ const listeners = listenersByType[type];
47
+ if (!listeners) return;
48
+ [...listeners].forEach((handler) => {
49
+ try {
50
+ handler(payload);
51
+ } catch {}
52
+ });
53
+ }
54
+ };
55
+
56
+ //#endregion
57
+ //#region src/events.ts
58
+ const RMU_EVENTS = {
59
+ open: "rmu:open-modal",
60
+ close: "rmu:close-modal"
61
+ };
62
+ const openModal = (modalComponent, config = {}) => {
63
+ const modalId = `rmu-modal-${(/* @__PURE__ */ new Date()).getTime().toString()}`;
64
+ const { outletId = "rmu-default-outlet" } = config;
65
+ emitter.emit(RMU_EVENTS.open, {
66
+ modalId,
67
+ modalComponent,
68
+ outletId
69
+ });
70
+ return {
71
+ modalId,
72
+ outletId
73
+ };
74
+ };
75
+ const closeModal = ({ modalId, outletId }) => {
76
+ emitter.emit(RMU_EVENTS.close, {
77
+ modalId,
78
+ outletId
79
+ });
80
+ };
81
+
82
+ //#endregion
83
+ //#region src/use-rmu-events.ts
84
+ const useRMUEvents = (ctx) => {
85
+ const events = {
86
+ open: (payload) => ctx.openModal(payload),
87
+ close: (payload) => ctx.closeModal(payload)
88
+ };
89
+ (0, react.useEffect)(() => {
90
+ Object.keys(events).forEach((event) => {
91
+ emitter.subscribe(RMU_EVENTS[event], events[event]);
92
+ });
93
+ return () => {
94
+ Object.keys(events).forEach((event) => {
95
+ emitter.unsubscribe(RMU_EVENTS[event], events[event]);
96
+ });
97
+ };
98
+ }, []);
99
+ };
100
+
101
+ //#endregion
102
+ //#region src/use-rmu-state.ts
103
+ const ACTIONS = {
104
+ openModal: "rmu:open-modal",
105
+ closeModal: "rmu:close-modal",
106
+ addOutlet: "rmu:add-modal",
107
+ removeOutlet: "rmu:remove-outlet"
108
+ };
109
+ const reducer = (state, action) => {
110
+ switch (action.type) {
111
+ case ACTIONS.openModal: {
112
+ const { modalId, modalComponent, outletId } = action.payload;
113
+ const modalOutlet = state.outlets[outletId];
114
+ if (!modalOutlet) throw new Error(`Outlet with id ${outletId} not found`);
115
+ return {
116
+ ...state,
117
+ outlets: {
118
+ ...state.outlets,
119
+ [outletId]: {
120
+ ...modalOutlet,
121
+ [modalId]: modalComponent
122
+ }
123
+ }
124
+ };
125
+ }
126
+ case ACTIONS.closeModal: {
127
+ const { modalId, outletId } = action.payload;
128
+ const modalOutlet = state.outlets[outletId];
129
+ if (!modalOutlet) throw new Error(`Outlet with id ${outletId} not found`);
130
+ const { [modalId]: _removed,...restModals } = modalOutlet;
131
+ return {
132
+ ...state,
133
+ outlets: {
134
+ ...state.outlets,
135
+ [outletId]: restModals
136
+ }
137
+ };
138
+ }
139
+ case ACTIONS.addOutlet: {
140
+ const { outletId } = action.payload;
141
+ if (!!state.outlets[outletId]) throw new Error(`Outlet with id ${outletId} already exists`);
142
+ return {
143
+ ...state,
144
+ outlets: {
145
+ ...state.outlets,
146
+ [outletId]: {}
147
+ }
148
+ };
149
+ }
150
+ case ACTIONS.removeOutlet: {
151
+ const { outletId } = action.payload;
152
+ const { [outletId]: _removed,...restOutlets } = state.outlets;
153
+ return {
154
+ ...state,
155
+ outlets: restOutlets
156
+ };
157
+ }
158
+ default: return state;
159
+ }
160
+ };
161
+ const useRMUState = () => {
162
+ const [state, dispatch] = (0, react.useReducer)(reducer, { outlets: {} });
163
+ const openModal$1 = ({ modalId, modalComponent, outletId }) => {
164
+ dispatch({
165
+ type: ACTIONS.openModal,
166
+ payload: {
167
+ modalId,
168
+ modalComponent,
169
+ outletId
170
+ }
171
+ });
172
+ };
173
+ const closeModal$1 = ({ modalId, outletId }) => {
174
+ dispatch({
175
+ type: ACTIONS.closeModal,
176
+ payload: {
177
+ modalId,
178
+ outletId
179
+ }
180
+ });
181
+ };
182
+ const addOutlet = (outletId) => {
183
+ dispatch({
184
+ type: ACTIONS.addOutlet,
185
+ payload: { outletId }
186
+ });
187
+ };
188
+ const removeOutlet = (outletId) => {
189
+ dispatch({
190
+ type: ACTIONS.removeOutlet,
191
+ payload: { outletId }
192
+ });
193
+ };
194
+ return {
195
+ ...state,
196
+ openModal: openModal$1,
197
+ closeModal: closeModal$1,
198
+ addOutlet,
199
+ removeOutlet
200
+ };
201
+ };
202
+
203
+ //#endregion
204
+ //#region src/rmu-provider.tsx
205
+ const RMUProvider = ({ children }) => {
206
+ const state = useRMUState();
207
+ useRMUEvents(state);
208
+ return /* @__PURE__ */ react.default.createElement(RMUContext.Provider, { value: state }, children);
209
+ };
210
+
211
+ //#endregion
212
+ //#region src/rmu-outlet.tsx
213
+ const RMUOutlet = ({ outletId = "rmu-default-outlet" }) => {
214
+ const ctx = (0, react.useContext)(RMUContext);
215
+ if (!ctx) throw new Error("RMUProvider not found in component tree");
216
+ const { outlets, addOutlet, removeOutlet } = ctx;
217
+ (0, react.useEffect)(() => {
218
+ addOutlet(outletId);
219
+ return () => {
220
+ removeOutlet(outletId);
221
+ };
222
+ }, []);
223
+ const modals = outlets[outletId] ?? {};
224
+ return /* @__PURE__ */ react.default.createElement(react.default.Fragment, null, Object.entries(modals).map(([modalId, modalComponent]) => /* @__PURE__ */ react.default.createElement(react.Fragment, { key: modalId }, modalComponent)));
225
+ };
226
+
227
+ //#endregion
228
+ exports.RMUOutlet = RMUOutlet;
229
+ exports.RMUProvider = RMUProvider;
230
+ exports.closeModal = closeModal;
231
+ exports.openModal = openModal;
package/dist/index.mjs ADDED
@@ -0,0 +1,204 @@
1
+ import React, { Fragment, createContext, useContext, useEffect, useReducer } from "react";
2
+
3
+ //#region src/rmu-context.ts
4
+ const RMUContext = createContext(null);
5
+
6
+ //#endregion
7
+ //#region src/emitter.ts
8
+ const listenersByType = {};
9
+ const emitter = {
10
+ subscribe(type, handler) {
11
+ if (!listenersByType[type]) listenersByType[type] = /* @__PURE__ */ new Set();
12
+ listenersByType[type].add(handler);
13
+ },
14
+ unsubscribe(type, handler) {
15
+ const listeners = listenersByType[type];
16
+ if (listeners) {
17
+ listeners.delete(handler);
18
+ if (listeners.size === 0) delete listenersByType[type];
19
+ }
20
+ },
21
+ emit(type, payload) {
22
+ const listeners = listenersByType[type];
23
+ if (!listeners) return;
24
+ [...listeners].forEach((handler) => {
25
+ try {
26
+ handler(payload);
27
+ } catch {}
28
+ });
29
+ }
30
+ };
31
+
32
+ //#endregion
33
+ //#region src/events.ts
34
+ const RMU_EVENTS = {
35
+ open: "rmu:open-modal",
36
+ close: "rmu:close-modal"
37
+ };
38
+ const openModal = (modalComponent, config = {}) => {
39
+ const modalId = `rmu-modal-${(/* @__PURE__ */ new Date()).getTime().toString()}`;
40
+ const { outletId = "rmu-default-outlet" } = config;
41
+ emitter.emit(RMU_EVENTS.open, {
42
+ modalId,
43
+ modalComponent,
44
+ outletId
45
+ });
46
+ return {
47
+ modalId,
48
+ outletId
49
+ };
50
+ };
51
+ const closeModal = ({ modalId, outletId }) => {
52
+ emitter.emit(RMU_EVENTS.close, {
53
+ modalId,
54
+ outletId
55
+ });
56
+ };
57
+
58
+ //#endregion
59
+ //#region src/use-rmu-events.ts
60
+ const useRMUEvents = (ctx) => {
61
+ const events = {
62
+ open: (payload) => ctx.openModal(payload),
63
+ close: (payload) => ctx.closeModal(payload)
64
+ };
65
+ useEffect(() => {
66
+ Object.keys(events).forEach((event) => {
67
+ emitter.subscribe(RMU_EVENTS[event], events[event]);
68
+ });
69
+ return () => {
70
+ Object.keys(events).forEach((event) => {
71
+ emitter.unsubscribe(RMU_EVENTS[event], events[event]);
72
+ });
73
+ };
74
+ }, []);
75
+ };
76
+
77
+ //#endregion
78
+ //#region src/use-rmu-state.ts
79
+ const ACTIONS = {
80
+ openModal: "rmu:open-modal",
81
+ closeModal: "rmu:close-modal",
82
+ addOutlet: "rmu:add-modal",
83
+ removeOutlet: "rmu:remove-outlet"
84
+ };
85
+ const reducer = (state, action) => {
86
+ switch (action.type) {
87
+ case ACTIONS.openModal: {
88
+ const { modalId, modalComponent, outletId } = action.payload;
89
+ const modalOutlet = state.outlets[outletId];
90
+ if (!modalOutlet) throw new Error(`Outlet with id ${outletId} not found`);
91
+ return {
92
+ ...state,
93
+ outlets: {
94
+ ...state.outlets,
95
+ [outletId]: {
96
+ ...modalOutlet,
97
+ [modalId]: modalComponent
98
+ }
99
+ }
100
+ };
101
+ }
102
+ case ACTIONS.closeModal: {
103
+ const { modalId, outletId } = action.payload;
104
+ const modalOutlet = state.outlets[outletId];
105
+ if (!modalOutlet) throw new Error(`Outlet with id ${outletId} not found`);
106
+ const { [modalId]: _removed,...restModals } = modalOutlet;
107
+ return {
108
+ ...state,
109
+ outlets: {
110
+ ...state.outlets,
111
+ [outletId]: restModals
112
+ }
113
+ };
114
+ }
115
+ case ACTIONS.addOutlet: {
116
+ const { outletId } = action.payload;
117
+ if (!!state.outlets[outletId]) throw new Error(`Outlet with id ${outletId} already exists`);
118
+ return {
119
+ ...state,
120
+ outlets: {
121
+ ...state.outlets,
122
+ [outletId]: {}
123
+ }
124
+ };
125
+ }
126
+ case ACTIONS.removeOutlet: {
127
+ const { outletId } = action.payload;
128
+ const { [outletId]: _removed,...restOutlets } = state.outlets;
129
+ return {
130
+ ...state,
131
+ outlets: restOutlets
132
+ };
133
+ }
134
+ default: return state;
135
+ }
136
+ };
137
+ const useRMUState = () => {
138
+ const [state, dispatch] = useReducer(reducer, { outlets: {} });
139
+ const openModal$1 = ({ modalId, modalComponent, outletId }) => {
140
+ dispatch({
141
+ type: ACTIONS.openModal,
142
+ payload: {
143
+ modalId,
144
+ modalComponent,
145
+ outletId
146
+ }
147
+ });
148
+ };
149
+ const closeModal$1 = ({ modalId, outletId }) => {
150
+ dispatch({
151
+ type: ACTIONS.closeModal,
152
+ payload: {
153
+ modalId,
154
+ outletId
155
+ }
156
+ });
157
+ };
158
+ const addOutlet = (outletId) => {
159
+ dispatch({
160
+ type: ACTIONS.addOutlet,
161
+ payload: { outletId }
162
+ });
163
+ };
164
+ const removeOutlet = (outletId) => {
165
+ dispatch({
166
+ type: ACTIONS.removeOutlet,
167
+ payload: { outletId }
168
+ });
169
+ };
170
+ return {
171
+ ...state,
172
+ openModal: openModal$1,
173
+ closeModal: closeModal$1,
174
+ addOutlet,
175
+ removeOutlet
176
+ };
177
+ };
178
+
179
+ //#endregion
180
+ //#region src/rmu-provider.tsx
181
+ const RMUProvider = ({ children }) => {
182
+ const state = useRMUState();
183
+ useRMUEvents(state);
184
+ return /* @__PURE__ */ React.createElement(RMUContext.Provider, { value: state }, children);
185
+ };
186
+
187
+ //#endregion
188
+ //#region src/rmu-outlet.tsx
189
+ const RMUOutlet = ({ outletId = "rmu-default-outlet" }) => {
190
+ const ctx = useContext(RMUContext);
191
+ if (!ctx) throw new Error("RMUProvider not found in component tree");
192
+ const { outlets, addOutlet, removeOutlet } = ctx;
193
+ useEffect(() => {
194
+ addOutlet(outletId);
195
+ return () => {
196
+ removeOutlet(outletId);
197
+ };
198
+ }, []);
199
+ const modals = outlets[outletId] ?? {};
200
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, Object.entries(modals).map(([modalId, modalComponent]) => /* @__PURE__ */ React.createElement(Fragment, { key: modalId }, modalComponent)));
201
+ };
202
+
203
+ //#endregion
204
+ export { RMUOutlet, RMUProvider, closeModal, openModal };
package/package.json CHANGED
@@ -1,30 +1,29 @@
1
1
  {
2
- "version": "0.2.5",
2
+ "name": "@violice/rmu",
3
+ "version": "0.2.7",
4
+ "author": "Sergey Ivashko",
3
5
  "license": "MIT",
4
- "main": "dist/index.js",
5
- "typings": "dist/index.d.ts",
6
6
  "files": [
7
7
  "dist",
8
8
  "src"
9
9
  ],
10
10
  "engines": {
11
- "node": ">=10"
11
+ "node": ">=20"
12
12
  },
13
13
  "scripts": {
14
- "start": "tsdx watch",
15
- "build": "tsdx build",
16
- "test": "tsdx test --passWithNoTests",
17
- "lint": "tsdx lint",
18
- "prepare": "tsdx build",
14
+ "start": "tsdown --watch",
15
+ "build": "tsdown",
16
+ "prepare": "tsdown",
19
17
  "size": "size-limit",
20
18
  "analyze": "size-limit --why"
21
19
  },
22
20
  "peerDependencies": {
23
- "react": ">=16"
21
+ "react": ">=16",
22
+ "react-native": ">=0.60"
24
23
  },
25
- "husky": {
26
- "hooks": {
27
- "pre-commit": "tsdx lint"
24
+ "peerDependenciesMeta": {
25
+ "react-native": {
26
+ "optional": true
28
27
  }
29
28
  },
30
29
  "prettier": {
@@ -33,29 +32,37 @@
33
32
  "singleQuote": true,
34
33
  "trailingComma": "es5"
35
34
  },
36
- "name": "@violice/rmu",
37
- "author": "Sergey Ivashko",
38
- "module": "dist/rmu.esm.js",
35
+ "main": "dist/index.js",
36
+ "module": "dist/index.mjs",
37
+ "types": "dist/index.d.ts",
38
+ "react-native": "dist/index.js",
39
+ "exports": {
40
+ ".": {
41
+ "types": "./dist/index.d.ts",
42
+ "import": "./dist/index.mjs",
43
+ "require": "./dist/index.js"
44
+ }
45
+ },
46
+ "sideEffects": false,
39
47
  "size-limit": [
40
48
  {
41
- "path": "dist/rmu.cjs.production.min.js",
42
- "limit": "10 KB"
49
+ "path": "dist/index.js",
50
+ "limit": "2 KB"
43
51
  },
44
52
  {
45
- "path": "dist/rmu.esm.js",
46
- "limit": "10 KB"
53
+ "path": "dist/index.mjs",
54
+ "limit": "2 KB"
47
55
  }
48
56
  ],
49
57
  "devDependencies": {
50
- "@size-limit/preset-small-lib": "^7.0.8",
51
- "@types/react": "^18.2.25",
52
- "@types/react-dom": "^18.2.11",
53
- "husky": "^7.0.4",
54
- "react": "^18.2.0",
55
- "react-dom": "^18.2.0",
56
- "size-limit": "^7.0.8",
57
- "tsdx": "^0.14.1",
58
- "tslib": "^2.6.2",
59
- "typescript": "^4.9.5"
58
+ "@size-limit/preset-small-lib": "^11.2.0",
59
+ "@types/react": "^18.3.25",
60
+ "@types/react-dom": "^18.3.7",
61
+ "react": "^18.3.1",
62
+ "react-dom": "^18.3.1",
63
+ "size-limit": "^11.2.0",
64
+ "tsdown": "^0.15.6",
65
+ "tslib": "^2.8.1",
66
+ "typescript": "^5.9.3"
60
67
  }
61
68
  }
package/src/emitter.ts ADDED
@@ -0,0 +1,37 @@
1
+ type EventHandler = (payload: any) => void;
2
+
3
+ type EventListenersMap = Record<string, Set<EventHandler>>;
4
+
5
+ const listenersByType: EventListenersMap = {};
6
+
7
+ export const emitter = {
8
+ subscribe(type: string, handler: EventHandler) {
9
+ if (!listenersByType[type]) {
10
+ listenersByType[type] = new Set<EventHandler>();
11
+ }
12
+ listenersByType[type].add(handler);
13
+ },
14
+
15
+ unsubscribe(type: string, handler: EventHandler) {
16
+ const listeners = listenersByType[type];
17
+ if (listeners) {
18
+ listeners.delete(handler);
19
+ if (listeners.size === 0) {
20
+ delete listenersByType[type];
21
+ }
22
+ }
23
+ },
24
+
25
+ emit(type: string, payload: any) {
26
+ const listeners = listenersByType[type];
27
+ if (!listeners) return;
28
+ // Clone to avoid issues if handlers mutate subscription during emit
29
+ [...listeners].forEach(handler => {
30
+ try {
31
+ handler(payload);
32
+ } catch {
33
+ // Swallow to ensure one handler error does not break others
34
+ }
35
+ });
36
+ },
37
+ };