hermes-handler 0.0.1
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 +232 -0
- package/dist/HermesHandler.d.ts +92 -0
- package/dist/HermesHandler.js +350 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +9 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# HermesHandler
|
|
2
|
+
|
|
3
|
+
HermesHandler is a lightweight, framework-agnostic message router for browser extensions and event-driven systems. It provides structured request dispatching, a strict `{ ok, result, error }` response envelope, timeout handling, cooperative cancellation, and safe normalization.
|
|
4
|
+
|
|
5
|
+
Designed for reliability and clarity, HermesHandler is especially well-suited for LLM-driven agents, modular browser architectures, automation layers, and distributed runtime systems.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
* 🔁 Deterministic message routing via `type`
|
|
12
|
+
* 📦 Strict response envelope: `{ ok, result?, error? }`
|
|
13
|
+
* ⏱ Built-in timeout handling
|
|
14
|
+
* 🛑 Cooperative cancellation via `AbortSignal`
|
|
15
|
+
* 🧊 Immutable (shallow-frozen) responses
|
|
16
|
+
* 🧠 LLM-friendly deterministic contract
|
|
17
|
+
* 🧩 Framework-agnostic (no runtime dependencies)
|
|
18
|
+
* 📘 Type-safe via generated `.d.ts`
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install hermes-handler
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
import { HermesHandler } from "hermes-handler";
|
|
34
|
+
|
|
35
|
+
const handlers = {
|
|
36
|
+
ping: () => ({ ok: true, result: "pong" }),
|
|
37
|
+
|
|
38
|
+
greet: (msg) => {
|
|
39
|
+
return { ok: true, result: `Hello ${msg.payload.name}` };
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const hermes = new HermesHandler(handlers);
|
|
44
|
+
|
|
45
|
+
const res = await hermes.dispatch({ type: "ping" });
|
|
46
|
+
|
|
47
|
+
if (res.ok) {
|
|
48
|
+
console.log(res.result); // "pong"
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Browser Extension Usage
|
|
55
|
+
|
|
56
|
+
Attach HermesHandler to a runtime listener:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
browser.runtime.onMessage.addListener(
|
|
60
|
+
hermes.getListener()
|
|
61
|
+
);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
HermesHandler supports both:
|
|
65
|
+
|
|
66
|
+
* Promise-returning listeners (MV3 / Firefox / polyfill)
|
|
67
|
+
* Callback-style `sendResponse + return true`
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Response Contract
|
|
72
|
+
|
|
73
|
+
All responses follow a strict envelope.
|
|
74
|
+
|
|
75
|
+
### Success
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
{ ok: true, result: any }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Error
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
{ ok: false, error: string, details?: any }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Primitive return values are automatically normalized:
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
return "hello";
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Becomes:
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
{ ok: true, result: "hello" }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Malformed responses are safely coerced into valid error envelopes.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Timeouts
|
|
104
|
+
|
|
105
|
+
Handlers can be time-limited:
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
const hermes = new HermesHandler(handlers, {
|
|
109
|
+
timeoutMs: 7000
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
If exceeded, HermesHandler returns:
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
{ ok: false, error: "Handler <type> timed out (7000 ms)" }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Cooperative Cancellation
|
|
122
|
+
|
|
123
|
+
Each handler receives an `AbortSignal`:
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
async function longTask(msg, ctx) {
|
|
127
|
+
if (ctx.signal?.aborted) {
|
|
128
|
+
return { ok: false, error: "Cancelled" };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
ctx.signal?.addEventListener("abort", () => {
|
|
132
|
+
console.log("Cancelled externally");
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
HermesHandler aborts the signal once a request lifecycle completes.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## API
|
|
142
|
+
|
|
143
|
+
### `new HermesHandler(initialHandlers?, options?)`
|
|
144
|
+
|
|
145
|
+
**initialHandlers**
|
|
146
|
+
`Record<string, HermesHandlerFn>`
|
|
147
|
+
|
|
148
|
+
**options**
|
|
149
|
+
|
|
150
|
+
* `timeoutMs?: number`
|
|
151
|
+
* `onUnknown?: (msg, ctx) => HermesResponse`
|
|
152
|
+
* `onError?: (err, msg, ctx) => HermesResponse`
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
### `.register(type, fn)`
|
|
157
|
+
|
|
158
|
+
Register or overwrite a handler.
|
|
159
|
+
|
|
160
|
+
### `.registerMany(map)`
|
|
161
|
+
|
|
162
|
+
Register multiple handlers at once.
|
|
163
|
+
|
|
164
|
+
### `.unregister(type)`
|
|
165
|
+
|
|
166
|
+
Remove a handler.
|
|
167
|
+
|
|
168
|
+
### `.has(type)`
|
|
169
|
+
|
|
170
|
+
Check if a handler exists.
|
|
171
|
+
|
|
172
|
+
### `.getListener()`
|
|
173
|
+
|
|
174
|
+
Returns a runtime-compatible message listener.
|
|
175
|
+
|
|
176
|
+
### `.dispatch(msg, sender?)`
|
|
177
|
+
|
|
178
|
+
Dispatch a message manually (useful for testing or non-extension environments).
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Logging
|
|
183
|
+
|
|
184
|
+
HermesHandler emits warnings and errors through a configurable logger.
|
|
185
|
+
|
|
186
|
+
By default, it uses the global `console`. You can disable logging entirely or provide a custom logger implementation.
|
|
187
|
+
|
|
188
|
+
### Disable Logging
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
const hermes = new HermesHandler(handlers, {
|
|
192
|
+
logger: null
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Custom Logger
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
const hermes = new HermesHandler(handlers, {
|
|
200
|
+
logger: {
|
|
201
|
+
warn: (...args) => myLogger.warn(...args),
|
|
202
|
+
error: (...args) => myLogger.error(...args)
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**HermesLogger shape**
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
interface HermesLogger {
|
|
211
|
+
debug?(message?: any, ...optionalParams: any[]): void;
|
|
212
|
+
info?(message?: any, ...optionalParams: any[]): void;
|
|
213
|
+
warn?(message?: any, ...optionalParams: any[]): void;
|
|
214
|
+
error?(message?: any, ...optionalParams: any[]): void;
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
If `logger` is `null`, HermesHandler will not emit any console output.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Design Goals
|
|
223
|
+
|
|
224
|
+
HermesHandler enforces a predictable and deterministic runtime contract. By standardizing request/response handling and isolating message dispatch logic, it simplifies reasoning about complex systems—particularly those involving automation, background scripts, or LLM-driven tool execution.
|
|
225
|
+
|
|
226
|
+
The core remains intentionally minimal, dependency-free, and portable.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## License
|
|
231
|
+
|
|
232
|
+
MIT
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export class HermesHandler {
|
|
2
|
+
/**
|
|
3
|
+
* @param {Record<string, HermesHandlerFn>} initialHandlers
|
|
4
|
+
* @param {Object} [options]
|
|
5
|
+
* @param {number} [options.timeoutMs=5000] max time a handler can take before auto-fail
|
|
6
|
+
* @param {(msg: any, ctx: any) => any} [options.onUnknown] override unknown-type response
|
|
7
|
+
* @param {(err: any, msg: any, ctx: any) => any} [options.onError] override error response
|
|
8
|
+
* @param {HermesLogger|null} [options.logger=console] set to null to silence logs
|
|
9
|
+
*/
|
|
10
|
+
constructor(initialHandlers?: Record<string, HermesHandlerFn>, options?: {
|
|
11
|
+
timeoutMs?: number | undefined;
|
|
12
|
+
onUnknown?: ((msg: any, ctx: any) => any) | undefined;
|
|
13
|
+
onError?: ((err: any, msg: any, ctx: any) => any) | undefined;
|
|
14
|
+
logger?: HermesLogger | null | undefined;
|
|
15
|
+
});
|
|
16
|
+
/** @type {Map<string, HermesHandlerFn>} */
|
|
17
|
+
_handlers: Map<string, HermesHandlerFn>;
|
|
18
|
+
_timeoutMs: number;
|
|
19
|
+
_onUnknown: (msg: any, ctx: any) => any;
|
|
20
|
+
_onError: (err: any, msg: any, ctx: any) => any;
|
|
21
|
+
/** @type {HermesLogger|null} */
|
|
22
|
+
_logger: HermesLogger | null;
|
|
23
|
+
/**
|
|
24
|
+
* Register (or overwrite) a handler for a given msg.type
|
|
25
|
+
* @param {string} type
|
|
26
|
+
* @param {HermesHandlerFn} fn
|
|
27
|
+
* @returns {void}
|
|
28
|
+
*/
|
|
29
|
+
register(type: string, fn: HermesHandlerFn): void;
|
|
30
|
+
/**
|
|
31
|
+
* Register multiple handlers at once
|
|
32
|
+
* @param {Record<string, HermesHandlerFn>} map
|
|
33
|
+
* @returns {void}
|
|
34
|
+
*/
|
|
35
|
+
registerMany(map: Record<string, HermesHandlerFn>): void;
|
|
36
|
+
/** Remove a handler for a given msg.type */
|
|
37
|
+
/** @param {string} type */
|
|
38
|
+
unregister(type: string): void;
|
|
39
|
+
/** Check if a handler exists for a given msg.type */
|
|
40
|
+
/** @param {string} type */
|
|
41
|
+
has(type: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The listener you add using browser.runtime.onMessage.addListener
|
|
44
|
+
*
|
|
45
|
+
* Supports BOTH reply styles:
|
|
46
|
+
* • Promise-returning listener (Firefox / MV3 / polyfill)
|
|
47
|
+
* • sendResponse + return true (callback-style)
|
|
48
|
+
* @returns {(msg: any, sender: any, sendResponse?: (payload: any) => void) => any}
|
|
49
|
+
*/
|
|
50
|
+
getListener(): (msg: any, sender: any, sendResponse?: (payload: any) => void) => any;
|
|
51
|
+
/**
|
|
52
|
+
* @param {any} msg
|
|
53
|
+
* @param {any} sender
|
|
54
|
+
* @returns {Promise<HermesResponse<any>>}
|
|
55
|
+
*/
|
|
56
|
+
_dispatch(msg: any, sender: any): Promise<HermesResponse<any>>;
|
|
57
|
+
/**
|
|
58
|
+
* Dispatch a message through the router (useful for testing / non-runtime environments).
|
|
59
|
+
* @param {HermesMessage} msg
|
|
60
|
+
* @param {any} [sender]
|
|
61
|
+
* @returns {Promise<HermesResponse<any>>}
|
|
62
|
+
*/
|
|
63
|
+
dispatch(msg: HermesMessage, sender?: any): Promise<HermesResponse<any>>;
|
|
64
|
+
}
|
|
65
|
+
export type HermesOk<T> = {
|
|
66
|
+
ok: true;
|
|
67
|
+
result?: T | undefined;
|
|
68
|
+
};
|
|
69
|
+
export type HermesErr = {
|
|
70
|
+
ok: false;
|
|
71
|
+
error: string;
|
|
72
|
+
details?: any;
|
|
73
|
+
};
|
|
74
|
+
export type HermesResponse<T> = HermesOk<T> | HermesErr;
|
|
75
|
+
export type HermesMessage = {
|
|
76
|
+
type: string;
|
|
77
|
+
payload?: any;
|
|
78
|
+
requestId?: string | undefined;
|
|
79
|
+
};
|
|
80
|
+
export type HermesContext = {
|
|
81
|
+
sender: any;
|
|
82
|
+
tabId: number | undefined;
|
|
83
|
+
signal: AbortSignal | undefined;
|
|
84
|
+
send: (payload: any) => void;
|
|
85
|
+
};
|
|
86
|
+
export type HermesHandlerFn = (msg: HermesMessage, ctx: HermesContext) => HermesResponse<any> | Promise<HermesResponse<any>> | any;
|
|
87
|
+
export type HermesLogger = {
|
|
88
|
+
debug?: ((message?: any, ...optionalParams: any[]) => void) | undefined;
|
|
89
|
+
info?: ((message?: any, ...optionalParams: any[]) => void) | undefined;
|
|
90
|
+
warn?: ((message?: any, ...optionalParams: any[]) => void) | undefined;
|
|
91
|
+
error?: ((message?: any, ...optionalParams: any[]) => void) | undefined;
|
|
92
|
+
};
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// src/HermesHandler.js
|
|
2
|
+
|
|
3
|
+
// ------------------------------------------------------------
|
|
4
|
+
// HermesHandler — universal message router / gatekeeper
|
|
5
|
+
// ------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @template T
|
|
11
|
+
* @typedef {Object} HermesOk
|
|
12
|
+
* @property {true} ok
|
|
13
|
+
* @property {T} [result]
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} HermesErr
|
|
19
|
+
* @property {false} ok
|
|
20
|
+
* @property {string} error
|
|
21
|
+
* @property {any} [details]
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @template T
|
|
27
|
+
* @typedef {HermesOk<T> | HermesErr} HermesResponse
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Object} HermesMessage
|
|
33
|
+
* @property {string} type
|
|
34
|
+
* @property {any} [payload]
|
|
35
|
+
* @property {string} [requestId] // optional correlation id
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {Object} HermesContext
|
|
41
|
+
* @property {any} sender
|
|
42
|
+
* @property {number|undefined} tabId
|
|
43
|
+
* @property {AbortSignal|undefined} signal
|
|
44
|
+
* @property {(payload: any) => void} send
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @callback HermesHandlerFn
|
|
50
|
+
* @param {HermesMessage} msg
|
|
51
|
+
* @param {HermesContext} ctx
|
|
52
|
+
* @returns {HermesResponse<any>|Promise<HermesResponse<any>>|any}
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {Object} HermesLogger
|
|
58
|
+
* @property {(message?: any, ...optionalParams: any[]) => void} [debug]
|
|
59
|
+
* @property {(message?: any, ...optionalParams: any[]) => void} [info]
|
|
60
|
+
* @property {(message?: any, ...optionalParams: any[]) => void} [warn]
|
|
61
|
+
* @property {(message?: any, ...optionalParams: any[]) => void} [error]
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
/* ------------------------------------------------------------
|
|
67
|
+
* Internal helpers
|
|
68
|
+
* ---------------------------------------------------------- */
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {any} x
|
|
72
|
+
* @returns {x is Function}
|
|
73
|
+
*/
|
|
74
|
+
function isFn(x) {
|
|
75
|
+
return typeof x === "function";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** @param {any} err */
|
|
79
|
+
function toErrorString(err) {
|
|
80
|
+
if (err instanceof Error) return err.message || String(err);
|
|
81
|
+
return String(err);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @param {any} payload */
|
|
85
|
+
function normalizePayload(payload) {
|
|
86
|
+
if (payload && typeof payload === "object" && "ok" in payload) {
|
|
87
|
+
// Enforce strict boolean ok
|
|
88
|
+
if (typeof payload.ok !== "boolean") {
|
|
89
|
+
return { ok: false, error: "Invalid response: 'ok' must be boolean" };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// If ok:false, ensure error exists
|
|
93
|
+
if (payload.ok === false && typeof payload.error !== "string") {
|
|
94
|
+
return { ok: false, error: "Invalid response: missing 'error' string" };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return payload;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { ok: true, result: payload };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** @param {any} payload */
|
|
104
|
+
function freezeNormalized(payload) {
|
|
105
|
+
const normalized = normalizePayload(payload);
|
|
106
|
+
return normalized && typeof normalized === "object"
|
|
107
|
+
? Object.freeze(normalized)
|
|
108
|
+
: normalized;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @template T
|
|
114
|
+
* @param {() => T | Promise<T>} fn
|
|
115
|
+
* @param {number} ms
|
|
116
|
+
* @param {() => any} onTimeout
|
|
117
|
+
* @returns {Promise<T>}
|
|
118
|
+
*/
|
|
119
|
+
function withTimeout(fn, ms, onTimeout) {
|
|
120
|
+
if (!Number.isFinite(ms) || ms <= 0) {
|
|
121
|
+
return Promise.resolve().then(fn);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const makeTimeoutError = () => {
|
|
125
|
+
try {
|
|
126
|
+
return onTimeout();
|
|
127
|
+
} catch (e) {
|
|
128
|
+
return e;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
|
|
134
|
+
/** @type {ReturnType<typeof setTimeout>} */
|
|
135
|
+
let timerId;
|
|
136
|
+
|
|
137
|
+
const cleanup = () => clearTimeout(timerId);
|
|
138
|
+
|
|
139
|
+
/** @param {T} value */
|
|
140
|
+
const resolveWithCleanup = (value) => {
|
|
141
|
+
cleanup();
|
|
142
|
+
resolve(value);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** @param {any} err */
|
|
146
|
+
const rejectWithCleanup = (err) => {
|
|
147
|
+
cleanup();
|
|
148
|
+
reject(err);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
timerId = setTimeout(() => rejectWithCleanup(makeTimeoutError()), ms);
|
|
152
|
+
|
|
153
|
+
Promise.resolve()
|
|
154
|
+
.then(fn)
|
|
155
|
+
.then(resolveWithCleanup)
|
|
156
|
+
.catch(rejectWithCleanup);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
export class HermesHandler {
|
|
162
|
+
/**
|
|
163
|
+
* @param {Record<string, HermesHandlerFn>} initialHandlers
|
|
164
|
+
* @param {Object} [options]
|
|
165
|
+
* @param {number} [options.timeoutMs=5000] max time a handler can take before auto-fail
|
|
166
|
+
* @param {(msg: any, ctx: any) => any} [options.onUnknown] override unknown-type response
|
|
167
|
+
* @param {(err: any, msg: any, ctx: any) => any} [options.onError] override error response
|
|
168
|
+
* @param {HermesLogger|null} [options.logger=console] set to null to silence logs
|
|
169
|
+
*/
|
|
170
|
+
constructor(initialHandlers = {}, options = {}) {
|
|
171
|
+
const {
|
|
172
|
+
timeoutMs = 5000,
|
|
173
|
+
onUnknown = (msg) => ({ ok: false, error: `Unknown msg.type: ${msg?.type}` }),
|
|
174
|
+
onError = (err) => ({ ok: false, error: toErrorString(err) }),
|
|
175
|
+
logger = console
|
|
176
|
+
} = options;
|
|
177
|
+
|
|
178
|
+
/** @type {Map<string, HermesHandlerFn>} */
|
|
179
|
+
this._handlers = new Map(Object.entries(initialHandlers));
|
|
180
|
+
this._timeoutMs = timeoutMs;
|
|
181
|
+
this._onUnknown = onUnknown;
|
|
182
|
+
this._onError = onError;
|
|
183
|
+
|
|
184
|
+
/** @type {HermesLogger|null} */
|
|
185
|
+
this._logger = logger;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Register (or overwrite) a handler for a given msg.type
|
|
190
|
+
* @param {string} type
|
|
191
|
+
* @param {HermesHandlerFn} fn
|
|
192
|
+
* @returns {void}
|
|
193
|
+
*/
|
|
194
|
+
register(type, fn) {
|
|
195
|
+
if (!isFn(fn)) {
|
|
196
|
+
throw new Error(`Handler for ${type} must be a function`);
|
|
197
|
+
}
|
|
198
|
+
this._handlers.set(type, fn);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Register multiple handlers at once
|
|
203
|
+
* @param {Record<string, HermesHandlerFn>} map
|
|
204
|
+
* @returns {void}
|
|
205
|
+
*/
|
|
206
|
+
registerMany(map) {
|
|
207
|
+
for (const [type, fn] of Object.entries(map ?? {})) {
|
|
208
|
+
this.register(type, fn);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Remove a handler for a given msg.type */
|
|
213
|
+
/** @param {string} type */
|
|
214
|
+
unregister(type) {
|
|
215
|
+
this._handlers.delete(type);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Check if a handler exists for a given msg.type */
|
|
219
|
+
/** @param {string} type */
|
|
220
|
+
has(type) {
|
|
221
|
+
return this._handlers.has(type);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The listener you add using browser.runtime.onMessage.addListener
|
|
227
|
+
*
|
|
228
|
+
* Supports BOTH reply styles:
|
|
229
|
+
* • Promise-returning listener (Firefox / MV3 / polyfill)
|
|
230
|
+
* • sendResponse + return true (callback-style)
|
|
231
|
+
* @returns {(msg: any, sender: any, sendResponse?: (payload: any) => void) => any}
|
|
232
|
+
*/
|
|
233
|
+
getListener() {
|
|
234
|
+
return (msg, sender, sendResponse) => {
|
|
235
|
+
const p = this._dispatch(msg, sender);
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
// Callback-style (works everywhere)
|
|
239
|
+
if (isFn(sendResponse)) {
|
|
240
|
+
p.then(sendResponse).catch((err) =>
|
|
241
|
+
sendResponse(freezeNormalized(this._onError(err, msg, { sender })))
|
|
242
|
+
);
|
|
243
|
+
return true; // keep the port open for async response
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
// Promise-returning style
|
|
248
|
+
return p;
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ---- Core dispatch ------------------------------------------------------
|
|
253
|
+
/**
|
|
254
|
+
* @param {any} msg
|
|
255
|
+
* @param {any} sender
|
|
256
|
+
* @returns {Promise<HermesResponse<any>>}
|
|
257
|
+
*/
|
|
258
|
+
async _dispatch(msg, sender) {
|
|
259
|
+
|
|
260
|
+
if (!msg || typeof msg !== "object") {
|
|
261
|
+
return freezeNormalized({ ok: false, error: "Invalid message: msg expected to be an object" });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const type = msg.type;
|
|
265
|
+
|
|
266
|
+
if (typeof type !== "string" || !type) {
|
|
267
|
+
return freezeNormalized({ ok: false, error: "Invalid message: msg missing string 'type'" });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
let responded = false;
|
|
273
|
+
|
|
274
|
+
/** @type {HermesResponse<any>} */
|
|
275
|
+
let payloadToReturn = freezeNormalized({ ok: false, error: "No response" });
|
|
276
|
+
|
|
277
|
+
// Cooperative cancellation: handlers MAY honor ctx.signal
|
|
278
|
+
const controller = typeof AbortController !== "undefined"
|
|
279
|
+
? new AbortController()
|
|
280
|
+
: { signal: undefined, abort: () => { } };
|
|
281
|
+
|
|
282
|
+
const ctx = {
|
|
283
|
+
sender,
|
|
284
|
+
tabId: sender?.tab?.id,
|
|
285
|
+
signal: controller.signal,
|
|
286
|
+
send: (/** @type {any} */payload) => {
|
|
287
|
+
if (responded) {
|
|
288
|
+
this._logger?.warn?.("[Hermes] Multiple send attempts", { type });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
responded = true;
|
|
293
|
+
|
|
294
|
+
// Freeze to prevent accidental mutation after responding
|
|
295
|
+
// (shallow freeze is enough and avoids surprising perf hits).
|
|
296
|
+
payloadToReturn = freezeNormalized(payload);
|
|
297
|
+
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const fn = this._handlers.get(type);
|
|
303
|
+
|
|
304
|
+
if (!fn) {
|
|
305
|
+
ctx.send(this._onUnknown(msg, ctx));
|
|
306
|
+
return payloadToReturn;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const maybeReturn = await withTimeout(
|
|
311
|
+
() => fn(msg, ctx),
|
|
312
|
+
this._timeoutMs,
|
|
313
|
+
() => new Error(`Handler ${type} timed out (${this._timeoutMs} ms)`)
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Handler may either return a payload OR call ctx.send(payload)
|
|
317
|
+
if (!responded && maybeReturn !== undefined) {
|
|
318
|
+
ctx.send(maybeReturn);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!responded) {
|
|
322
|
+
ctx.send({ ok: false, error: `Handler ${type} returned no response` });
|
|
323
|
+
}
|
|
324
|
+
} catch (err) {
|
|
325
|
+
this._logger?.error?.(`[Hermes] Handler error for ${type}:`, err);
|
|
326
|
+
if (!responded) {
|
|
327
|
+
ctx.send(this._onError(err, msg, ctx));
|
|
328
|
+
}
|
|
329
|
+
} finally {
|
|
330
|
+
// NOTE: Abort does not stop JS execution, but lets handlers cooperate.
|
|
331
|
+
controller.abort();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return payloadToReturn;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Dispatch a message through the router (useful for testing / non-runtime environments).
|
|
340
|
+
* @param {HermesMessage} msg
|
|
341
|
+
* @param {any} [sender]
|
|
342
|
+
* @returns {Promise<HermesResponse<any>>}
|
|
343
|
+
*/
|
|
344
|
+
dispatch(msg, sender) {
|
|
345
|
+
return this._dispatch(msg, sender);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hermes-handler",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "HermesHandler is a lightweight, framework-agnostic message router for browser extensions and event-driven systems, with strict {ok,result,error} envelopes, timeouts, cooperative cancellation, and safe normalization—ideal for reliable LLM agent backbones.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "WATT3D",
|
|
7
|
+
"homepage": "https://github.com/iWhatty/HermesHandler-JS#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/iWhatty/HermesHandler-JS.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/iWhatty/HermesHandler-JS/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"module": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"clean": "rimraf dist",
|
|
36
|
+
"build": "npm run clean && npm run build:js && npm run build:types",
|
|
37
|
+
"build:js": "node ./scripts/copy-src-to-dist.mjs",
|
|
38
|
+
"build:types": "tsc -p tsconfig.build.json",
|
|
39
|
+
"lint": "eslint .",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"prepublishOnly": "npm run build && npm test"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"eslint": "^9.0.0",
|
|
45
|
+
"rimraf": "^6.0.0",
|
|
46
|
+
"typescript": "^5.9.3",
|
|
47
|
+
"vitest": "^2.1.9"
|
|
48
|
+
},
|
|
49
|
+
"keywords": []
|
|
50
|
+
}
|