@maroonedsoftware/comms 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/README.md +104 -0
- package/dist/comms.error.d.ts +16 -0
- package/dist/comms.error.d.ts.map +1 -0
- package/dist/comms.event.d.ts +49 -0
- package/dist/comms.event.d.ts.map +1 -0
- package/dist/comms.message.d.ts +28 -0
- package/dist/comms.message.d.ts.map +1 -0
- package/dist/comms.reply.d.ts +37 -0
- package/dist/comms.reply.d.ts.map +1 -0
- package/dist/comms.router.d.ts +50 -0
- package/dist/comms.router.d.ts.map +1 -0
- package/dist/comms.template.d.ts +47 -0
- package/dist/comms.template.d.ts.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +155 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# @maroonedsoftware/comms
|
|
2
|
+
|
|
3
|
+
The channel-agnostic messaging core for ServerKit. Register a `command` / `action` / `message`
|
|
4
|
+
handler **once** and run it on every wired channel, replying through a uniform `Reply`.
|
|
5
|
+
|
|
6
|
+
This package is **channel-free** — it has no dependency on any chat platform. Each channel package
|
|
7
|
+
ships its own adapter as a `./comms` subpath (e.g. `@maroonedsoftware/slack/comms`) that binds to this
|
|
8
|
+
core via an optional peer dependency. So you install `comms` plus whichever channels you wire.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add @maroonedsoftware/comms @maroonedsoftware/slack # + any other channels
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Exports
|
|
17
|
+
|
|
18
|
+
| Symbol | Purpose |
|
|
19
|
+
|--------------------------------|--------------------------------------------------------------------------------------------------|
|
|
20
|
+
| `ChannelRouter` | Registers `command`/`action`/`message`/`fallback` handlers and `dispatch`es normalized events. Holds `.templates`. |
|
|
21
|
+
| `IncomingEvent` | Normalized inbound event (`channel`, `kind`, `user`, `conversation`, `text?`, `command?`, `action?`, `raw`). |
|
|
22
|
+
| `OutgoingMessage` / `OutgoingButton` | Portable outbound message (`text`, optional `subject`, optional `buttons`). |
|
|
23
|
+
| `Reply` | A `Notifier` bound to one recipient — what handlers use: `send` / `sendTemplate` / `sendNative`. |
|
|
24
|
+
| `Notifier` | Send-to-recipient interface each channel adapter implements (the seam for future push/email). |
|
|
25
|
+
| `bindReply(notifier, to)` | Builds a `Reply` from a `Notifier` + recipient. |
|
|
26
|
+
| `TemplateRegistry` | Named rich templates: `register(name, channel, fn)`, `registerDefault(name, fn)`, `render(...)`. |
|
|
27
|
+
| `CommsError` | `ServerkitError` subclass (e.g. `sendTemplate` for an unknown template). |
|
|
28
|
+
|
|
29
|
+
## Defining handlers (channel-agnostic)
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { ChannelRouter } from '@maroonedsoftware/comms';
|
|
33
|
+
|
|
34
|
+
export const router = new ChannelRouter();
|
|
35
|
+
|
|
36
|
+
router.command('deploy', async (event, reply) => {
|
|
37
|
+
await reply.send({
|
|
38
|
+
text: `Deploying ${event.command!.args || 'production'}…`,
|
|
39
|
+
buttons: [{ id: 'deploy:confirm', label: 'Confirm' }, { id: 'deploy:cancel', label: 'Cancel' }],
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
router.action('deploy:confirm', async (event, reply) => {
|
|
44
|
+
await reply.send({ text: `:rocket: Confirmed by ${event.user.username ?? event.user.id}` });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
router.message(async (event, reply) => {
|
|
48
|
+
if (/help/i.test(event.text ?? '')) await reply.send({ text: 'Try `/deploy <env>`.' });
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Routing precedence per event: `command` → by normalized name (`/Deploy` and `deploy` both match
|
|
53
|
+
`deploy`), `action` → by id, `message` → the single message handler, else the optional `fallback`.
|
|
54
|
+
|
|
55
|
+
## Wiring a channel
|
|
56
|
+
|
|
57
|
+
Each channel package exposes a `./comms` adapter. Wire it from your own HTTP route, reusing that
|
|
58
|
+
package's signature verification:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { SlackClient, SlackConfig, verifySlackSignature } from '@maroonedsoftware/slack';
|
|
62
|
+
import { dispatchSlackCommand } from '@maroonedsoftware/slack/comms';
|
|
63
|
+
import { router } from './router.js';
|
|
64
|
+
|
|
65
|
+
http.post('/slack/commands', async (ctx) => {
|
|
66
|
+
const raw = await rawBody(ctx.req, { encoding: 'utf8' });
|
|
67
|
+
verifySlackSignature({ signingSecret: ctx.container.get(SlackConfig).signingSecret, rawBody: raw,
|
|
68
|
+
timestamp: ctx.get('x-slack-request-timestamp'), signature: ctx.get('x-slack-signature') });
|
|
69
|
+
await dispatchSlackCommand(router, ctx.container.get(SlackClient), Object.fromEntries(new URLSearchParams(raw)) as any);
|
|
70
|
+
ctx.status = 200; ctx.body = '';
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The **same `router`** is reused by `@maroonedsoftware/discord/comms`, `/whatsapp/comms`, and
|
|
75
|
+
`/telegram/comms`. See each channel package's README for its adapter's exports and any caveats (e.g.
|
|
76
|
+
Discord has no inbound `message`; WhatsApp/Telegram commands come from `/`-prefixed text).
|
|
77
|
+
|
|
78
|
+
## Rich outbound via the template registry
|
|
79
|
+
|
|
80
|
+
For anything beyond text + buttons, register a named template — rich per channel, portable fallback
|
|
81
|
+
elsewhere — and call it channel-agnostically:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
router.templates.register('order.card', 'slack', (d: { id: string }) => ({ blocks: [/* Block Kit */] }));
|
|
85
|
+
router.templates.registerDefault('order.card', (d: { id: string }) => ({ text: `Order ${d.id} ✅` }));
|
|
86
|
+
|
|
87
|
+
router.action('order:confirm', async (event, reply) => {
|
|
88
|
+
await reply.sendTemplate('order.card', { id: event.action!.value }); // native on Slack, fallback elsewhere
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Resolution prefers a channel-native renderer, then the portable default; an unregistered name throws
|
|
93
|
+
`CommsError`. The registry stores plain functions — back a renderer with Handlebars or any engine; no
|
|
94
|
+
template engine is bundled. For one-off native payloads, `reply.sendNative(payload)` is the raw escape
|
|
95
|
+
hatch.
|
|
96
|
+
|
|
97
|
+
## Outbound-only sends
|
|
98
|
+
|
|
99
|
+
Each adapter also exposes a `Notifier` (`create<Channel>Notifier(client, router.templates)`) for
|
|
100
|
+
proactive, non-reply sends: `notifier.send(recipientId, { text: '…' })`.
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ServerkitError } from '@maroonedsoftware/errors';
|
|
2
|
+
/**
|
|
3
|
+
* Domain error raised by the comms layer (e.g. `sendTemplate` for an
|
|
4
|
+
* unregistered template name).
|
|
5
|
+
*
|
|
6
|
+
* Extends {@link ServerkitError} so `errorMiddleware` renders a 500 with
|
|
7
|
+
* `{ message, details }` if one escapes a route handler.
|
|
8
|
+
*/
|
|
9
|
+
export declare class CommsError extends ServerkitError {
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Type guard for {@link CommsError}. Narrows `unknown` to `CommsError`. Returns
|
|
13
|
+
* `true` for any subclass.
|
|
14
|
+
*/
|
|
15
|
+
export declare const IsCommsError: (error: unknown) => error is CommsError;
|
|
16
|
+
//# sourceMappingURL=comms.error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comms.error.d.ts","sourceRoot":"","sources":["../src/comms.error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1D;;;;;;GAMG;AACH,qBAAa,UAAW,SAAQ,cAAc;CAAG;AAEjD;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,UAAyC,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identifier for a wired channel. The built-in chat adapters use the four
|
|
3
|
+
* literals; the open `string` keeps the type extensible for future channels
|
|
4
|
+
* (sms, push, email, …).
|
|
5
|
+
*/
|
|
6
|
+
export type ChannelId = 'slack' | 'discord' | 'whatsapp' | 'telegram' | (string & {});
|
|
7
|
+
/**
|
|
8
|
+
* The normalized kinds of inbound event the {@link ChannelRouter} routes on.
|
|
9
|
+
*
|
|
10
|
+
* - `command` — a slash/text command (`/deploy staging`).
|
|
11
|
+
* - `action` — a pressed button / interactive component, keyed by a developer id.
|
|
12
|
+
* - `message` — a free-text message with no command prefix.
|
|
13
|
+
*/
|
|
14
|
+
export type IncomingEventKind = 'command' | 'action' | 'message';
|
|
15
|
+
/**
|
|
16
|
+
* A channel-agnostic inbound event. Each channel's `./comms` adapter normalizes
|
|
17
|
+
* its native payload into this shape; handlers registered on the
|
|
18
|
+
* {@link ChannelRouter} receive it regardless of the source channel.
|
|
19
|
+
*/
|
|
20
|
+
export interface IncomingEvent {
|
|
21
|
+
/** Which channel produced the event. */
|
|
22
|
+
channel: ChannelId;
|
|
23
|
+
/** The routed kind — see {@link IncomingEventKind}. */
|
|
24
|
+
kind: IncomingEventKind;
|
|
25
|
+
/** The user who triggered the event. */
|
|
26
|
+
user: {
|
|
27
|
+
id: string;
|
|
28
|
+
username?: string;
|
|
29
|
+
};
|
|
30
|
+
/** The conversation/chat the event belongs to — also the address a {@link Reply} sends back to. */
|
|
31
|
+
conversation: {
|
|
32
|
+
id: string;
|
|
33
|
+
};
|
|
34
|
+
/** Message text or full command text, when present. */
|
|
35
|
+
text?: string;
|
|
36
|
+
/** Present when `kind === 'command'`. `name` is normalized (no leading slash, lowercased). */
|
|
37
|
+
command?: {
|
|
38
|
+
name: string;
|
|
39
|
+
args: string;
|
|
40
|
+
};
|
|
41
|
+
/** Present when `kind === 'action'`. `id` is the developer-defined button/component id. */
|
|
42
|
+
action?: {
|
|
43
|
+
id: string;
|
|
44
|
+
value?: string;
|
|
45
|
+
};
|
|
46
|
+
/** The channel-native payload (and context) the adapter normalized from, untouched. */
|
|
47
|
+
raw: unknown;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=comms.event.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comms.event.d.ts","sourceRoot":"","sources":["../src/comms.event.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAEtF;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEjE;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,wCAAwC;IACxC,OAAO,EAAE,SAAS,CAAC;IACnB,uDAAuD;IACvD,IAAI,EAAE,iBAAiB,CAAC;IACxB,wCAAwC;IACxC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,mGAAmG;IACnG,YAAY,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7B,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,2FAA2F;IAC3F,MAAM,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,uFAAuF;IACvF,GAAG,EAAE,OAAO,CAAC;CACd"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A portable reply button. Channels that support interactive buttons render it
|
|
3
|
+
* natively (Slack actions, Discord components, WhatsApp interactive, Telegram
|
|
4
|
+
* inline keyboard); channels without buttons degrade it (e.g. WhatsApp lists
|
|
5
|
+
* for >3, SMS numbered text).
|
|
6
|
+
*/
|
|
7
|
+
export interface OutgoingButton {
|
|
8
|
+
/** Developer-defined id echoed back as the {@link IncomingEvent.action} id when pressed. */
|
|
9
|
+
id: string;
|
|
10
|
+
/** Human-visible label. */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Optional payload value carried back on the action. */
|
|
13
|
+
value?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The portable outbound message — the lowest common denominator every channel
|
|
17
|
+
* renderer can express. Anything richer is reached through a registered template
|
|
18
|
+
* (`reply.sendTemplate`) or the raw escape hatch (`reply.sendNative`).
|
|
19
|
+
*/
|
|
20
|
+
export interface OutgoingMessage {
|
|
21
|
+
/** Body text. */
|
|
22
|
+
text: string;
|
|
23
|
+
/** Optional subject/title — used by email (subject) and push (title); chat/SMS renderers ignore it. */
|
|
24
|
+
subject?: string;
|
|
25
|
+
/** Optional reply buttons. */
|
|
26
|
+
buttons?: OutgoingButton[];
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=comms.message.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comms.message.d.ts","sourceRoot":"","sources":["../src/comms.message.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,4FAA4F;IAC5F,EAAE,EAAE,MAAM,CAAC;IACX,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,uGAAuG;IACvG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;CAC5B"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ChannelId } from './comms.event.js';
|
|
2
|
+
import type { OutgoingMessage } from './comms.message.js';
|
|
3
|
+
/**
|
|
4
|
+
* Send-to-recipient outbound interface, implemented by each channel's `./comms`
|
|
5
|
+
* adapter (`create<Channel>Notifier`). It is the forward-compatible seam that
|
|
6
|
+
* notify-only channels (push, email) will also implement.
|
|
7
|
+
*/
|
|
8
|
+
export interface Notifier {
|
|
9
|
+
/** Which channel this notifier sends on. */
|
|
10
|
+
readonly channel: ChannelId;
|
|
11
|
+
/** Send a portable message to a recipient (chat id / phone / token / address). */
|
|
12
|
+
send(to: string, message: OutgoingMessage): Promise<void>;
|
|
13
|
+
/** Render and send a registered template by name (rich per channel, portable fallback). */
|
|
14
|
+
sendTemplate(to: string, name: string, data?: unknown): Promise<void>;
|
|
15
|
+
/** Send a channel-native payload verbatim — the raw escape hatch. */
|
|
16
|
+
sendNative(to: string, payload: unknown): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A {@link Notifier} pre-bound to one recipient — what handlers receive to reply
|
|
20
|
+
* to the inbound event they're handling. Built by adapters via {@link bindReply}.
|
|
21
|
+
*/
|
|
22
|
+
export interface Reply {
|
|
23
|
+
/** Which channel the originating event came from. */
|
|
24
|
+
readonly channel: ChannelId;
|
|
25
|
+
/** Reply with a portable message. */
|
|
26
|
+
send(message: OutgoingMessage): Promise<void>;
|
|
27
|
+
/** Reply by rendering a registered template by name. */
|
|
28
|
+
sendTemplate(name: string, data?: unknown): Promise<void>;
|
|
29
|
+
/** Reply with a channel-native payload verbatim. */
|
|
30
|
+
sendNative(payload: unknown): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Binds a {@link Notifier} to a fixed recipient, yielding the {@link Reply} that
|
|
34
|
+
* handlers use. Each call delegates to the notifier with the bound `to`.
|
|
35
|
+
*/
|
|
36
|
+
export declare const bindReply: (notifier: Notifier, to: string) => Reply;
|
|
37
|
+
//# sourceMappingURL=comms.reply.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comms.reply.d.ts","sourceRoot":"","sources":["../src/comms.reply.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE1D;;;;GAIG;AACH,MAAM,WAAW,QAAQ;IACvB,4CAA4C;IAC5C,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC;IAC5B,kFAAkF;IAClF,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,2FAA2F;IAC3F,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,qEAAqE;IACrE,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AAED;;;GAGG;AACH,MAAM,WAAW,KAAK;IACpB,qDAAqD;IACrD,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC;IAC5B,qCAAqC;IACrC,IAAI,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,wDAAwD;IACxD,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,oDAAoD;IACpD,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7C;AAED;;;GAGG;AACH,eAAO,MAAM,SAAS,GAAI,UAAU,QAAQ,EAAE,IAAI,MAAM,KAAG,KAKzD,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Logger } from '@maroonedsoftware/logger';
|
|
2
|
+
import type { IncomingEvent } from './comms.event.js';
|
|
3
|
+
import type { Reply } from './comms.reply.js';
|
|
4
|
+
import { TemplateRegistry } from './comms.template.js';
|
|
5
|
+
/** A channel-agnostic handler invoked with the normalized event and a bound {@link Reply}. */
|
|
6
|
+
export type CommsHandler = (event: IncomingEvent, reply: Reply) => Promise<void> | void;
|
|
7
|
+
/** Normalizes a command name to its registry key: no leading slash, lowercased. */
|
|
8
|
+
export declare const normalizeCommandName: (name: string) => string;
|
|
9
|
+
/**
|
|
10
|
+
* The channel-agnostic router. Register `command` / `action` / `message`
|
|
11
|
+
* handlers (and an optional `fallback`) once; each channel's `./comms` adapter
|
|
12
|
+
* normalizes its inbound payload into an {@link IncomingEvent} and calls
|
|
13
|
+
* {@link dispatch}, which routes to the matching handler.
|
|
14
|
+
*
|
|
15
|
+
* Holds a {@link TemplateRegistry} (`router.templates`) the adapters read when a
|
|
16
|
+
* handler calls `reply.sendTemplate(...)`.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const router = new ChannelRouter();
|
|
21
|
+
* router.command('deploy', async (event, reply) => reply.send({ text: `Deploying ${event.command!.args}` }));
|
|
22
|
+
* router.action('deploy:confirm', async (event, reply) => reply.send({ text: 'Confirmed' }));
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare class ChannelRouter {
|
|
26
|
+
private readonly logger?;
|
|
27
|
+
/** Outbound template registry shared with the channel adapters. */
|
|
28
|
+
readonly templates: TemplateRegistry;
|
|
29
|
+
private readonly commands;
|
|
30
|
+
private readonly actions;
|
|
31
|
+
private messageHandler?;
|
|
32
|
+
private fallbackHandler?;
|
|
33
|
+
constructor(logger?: Logger | undefined);
|
|
34
|
+
/** Register a handler for a command (with or without a leading slash; name is normalized). */
|
|
35
|
+
command(name: string, handler: CommsHandler): this;
|
|
36
|
+
/** Register a handler for a button/component action id. */
|
|
37
|
+
action(id: string, handler: CommsHandler): this;
|
|
38
|
+
/** Register the single catch-all message handler (free-text, non-command messages). */
|
|
39
|
+
message(handler: CommsHandler): this;
|
|
40
|
+
/** Register a fallback handler invoked when nothing else matches. */
|
|
41
|
+
fallback(handler: CommsHandler): this;
|
|
42
|
+
/**
|
|
43
|
+
* Route a normalized event to its handler: `command` → by name, `action` → by
|
|
44
|
+
* id, `message` → the message handler. Falls back to the registered fallback
|
|
45
|
+
* (or logs at debug and returns) when nothing matches.
|
|
46
|
+
*/
|
|
47
|
+
dispatch(event: IncomingEvent, reply: Reply): Promise<void>;
|
|
48
|
+
private resolve;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=comms.router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comms.router.d.ts","sourceRoot":"","sources":["../src/comms.router.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD,8FAA8F;AAC9F,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAExF,mFAAmF;AACnF,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,MAA+C,CAAC;AAEpG;;;;;;;;;;;;;;;GAeG;AACH,qBACa,aAAa;IASZ,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IARpC,mEAAmE;IACnE,QAAQ,CAAC,SAAS,mBAA0B;IAE5C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAC5D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,cAAc,CAAC,CAAe;IACtC,OAAO,CAAC,eAAe,CAAC,CAAe;gBAEV,MAAM,CAAC,EAAE,MAAM,YAAA;IAE5C,8FAA8F;IAC9F,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI;IAKlD,2DAA2D;IAC3D,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI;IAK/C,uFAAuF;IACvF,OAAO,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IAKpC,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IAKrC;;;;OAIG;IACG,QAAQ,CAAC,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IASjE,OAAO,CAAC,OAAO;CAYhB"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ChannelId } from './comms.event.js';
|
|
2
|
+
import type { OutgoingMessage } from './comms.message.js';
|
|
3
|
+
/** Renders template data into a channel-native payload (rich, channel-specific). */
|
|
4
|
+
export type NativeRenderer<D = unknown> = (data: D) => unknown;
|
|
5
|
+
/** Renders template data into a portable {@link OutgoingMessage} (cross-channel fallback). */
|
|
6
|
+
export type PortableRenderer<D = unknown> = (data: D) => OutgoingMessage;
|
|
7
|
+
/**
|
|
8
|
+
* Result of {@link TemplateRegistry.render}: either a channel-native payload (a
|
|
9
|
+
* per-channel renderer matched) or a portable message (the default fallback),
|
|
10
|
+
* or `undefined` when no template is registered under the name.
|
|
11
|
+
*/
|
|
12
|
+
export type TemplateRenderResult = {
|
|
13
|
+
kind: 'native';
|
|
14
|
+
payload: unknown;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'portable';
|
|
17
|
+
message: OutgoingMessage;
|
|
18
|
+
} | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* A registry of named outbound templates. Register a **rich, per-channel**
|
|
21
|
+
* renderer with {@link register} and/or a **portable default** with
|
|
22
|
+
* {@link registerDefault}; adapters resolve a template for the current channel
|
|
23
|
+
* via {@link render}, with channel-native taking precedence over the default.
|
|
24
|
+
*
|
|
25
|
+
* The registry stores plain functions — a consumer is free to back a renderer
|
|
26
|
+
* with Handlebars or any engine. No template engine is bundled.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* registry.register('order.card', 'slack', d => ({ blocks: [...] }));
|
|
31
|
+
* registry.registerDefault('order.card', d => ({ text: `Order ${d.id} ✅` }));
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare class TemplateRegistry {
|
|
35
|
+
private readonly native;
|
|
36
|
+
private readonly defaults;
|
|
37
|
+
/** Register a channel-native renderer for `name` on `channel`. */
|
|
38
|
+
register<D>(name: string, channel: ChannelId, render: NativeRenderer<D>): this;
|
|
39
|
+
/** Register the portable default renderer for `name`, used on channels without a native renderer. */
|
|
40
|
+
registerDefault<D>(name: string, render: PortableRenderer<D>): this;
|
|
41
|
+
/**
|
|
42
|
+
* Resolve and run the best renderer for `name` on `channel`: a channel-native
|
|
43
|
+
* renderer if registered, else the portable default, else `undefined`.
|
|
44
|
+
*/
|
|
45
|
+
render(name: string, channel: ChannelId, data: unknown): TemplateRenderResult;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=comms.template.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comms.template.d.ts","sourceRoot":"","sources":["../src/comms.template.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE1D,oFAAoF;AACpF,MAAM,MAAM,cAAc,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC;AAC/D,8FAA8F;AAC9F,MAAM,MAAM,gBAAgB,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,eAAe,CAAC;AAEzE;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,GAAG,SAAS,CAAC;AAErI;;;;;;;;;;;;;;GAcG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqD;IAC5E,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuC;IAEhE,kEAAkE;IAClE,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,IAAI;IAU9E,qGAAqG;IACrG,eAAe,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,IAAI;IAKnE;;;OAGG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,GAAG,oBAAoB;CAO9E"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/comms.reply.ts
|
|
5
|
+
var bindReply = /* @__PURE__ */ __name((notifier, to) => ({
|
|
6
|
+
channel: notifier.channel,
|
|
7
|
+
send: /* @__PURE__ */ __name((message) => notifier.send(to, message), "send"),
|
|
8
|
+
sendTemplate: /* @__PURE__ */ __name((name, data) => notifier.sendTemplate(to, name, data), "sendTemplate"),
|
|
9
|
+
sendNative: /* @__PURE__ */ __name((payload) => notifier.sendNative(to, payload), "sendNative")
|
|
10
|
+
}), "bindReply");
|
|
11
|
+
|
|
12
|
+
// src/comms.template.ts
|
|
13
|
+
var TemplateRegistry = class {
|
|
14
|
+
static {
|
|
15
|
+
__name(this, "TemplateRegistry");
|
|
16
|
+
}
|
|
17
|
+
native = /* @__PURE__ */ new Map();
|
|
18
|
+
defaults = /* @__PURE__ */ new Map();
|
|
19
|
+
/** Register a channel-native renderer for `name` on `channel`. */
|
|
20
|
+
register(name, channel, render) {
|
|
21
|
+
let byChannel = this.native.get(name);
|
|
22
|
+
if (!byChannel) {
|
|
23
|
+
byChannel = /* @__PURE__ */ new Map();
|
|
24
|
+
this.native.set(name, byChannel);
|
|
25
|
+
}
|
|
26
|
+
byChannel.set(channel, render);
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
/** Register the portable default renderer for `name`, used on channels without a native renderer. */
|
|
30
|
+
registerDefault(name, render) {
|
|
31
|
+
this.defaults.set(name, render);
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve and run the best renderer for `name` on `channel`: a channel-native
|
|
36
|
+
* renderer if registered, else the portable default, else `undefined`.
|
|
37
|
+
*/
|
|
38
|
+
render(name, channel, data) {
|
|
39
|
+
const nativeRenderer = this.native.get(name)?.get(channel);
|
|
40
|
+
if (nativeRenderer) return {
|
|
41
|
+
kind: "native",
|
|
42
|
+
payload: nativeRenderer(data)
|
|
43
|
+
};
|
|
44
|
+
const portable = this.defaults.get(name);
|
|
45
|
+
if (portable) return {
|
|
46
|
+
kind: "portable",
|
|
47
|
+
message: portable(data)
|
|
48
|
+
};
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/comms.router.ts
|
|
54
|
+
import { Injectable } from "injectkit";
|
|
55
|
+
import { Logger } from "@maroonedsoftware/logger";
|
|
56
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
57
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
58
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
59
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
60
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
61
|
+
}
|
|
62
|
+
__name(_ts_decorate, "_ts_decorate");
|
|
63
|
+
function _ts_metadata(k, v) {
|
|
64
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
65
|
+
}
|
|
66
|
+
__name(_ts_metadata, "_ts_metadata");
|
|
67
|
+
var normalizeCommandName = /* @__PURE__ */ __name((name) => name.replace(/^\//, "").toLowerCase(), "normalizeCommandName");
|
|
68
|
+
var ChannelRouter = class {
|
|
69
|
+
static {
|
|
70
|
+
__name(this, "ChannelRouter");
|
|
71
|
+
}
|
|
72
|
+
logger;
|
|
73
|
+
/** Outbound template registry shared with the channel adapters. */
|
|
74
|
+
templates = new TemplateRegistry();
|
|
75
|
+
commands = /* @__PURE__ */ new Map();
|
|
76
|
+
actions = /* @__PURE__ */ new Map();
|
|
77
|
+
messageHandler;
|
|
78
|
+
fallbackHandler;
|
|
79
|
+
constructor(logger) {
|
|
80
|
+
this.logger = logger;
|
|
81
|
+
}
|
|
82
|
+
/** Register a handler for a command (with or without a leading slash; name is normalized). */
|
|
83
|
+
command(name, handler) {
|
|
84
|
+
this.commands.set(normalizeCommandName(name), handler);
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
/** Register a handler for a button/component action id. */
|
|
88
|
+
action(id, handler) {
|
|
89
|
+
this.actions.set(id, handler);
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
/** Register the single catch-all message handler (free-text, non-command messages). */
|
|
93
|
+
message(handler) {
|
|
94
|
+
this.messageHandler = handler;
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
/** Register a fallback handler invoked when nothing else matches. */
|
|
98
|
+
fallback(handler) {
|
|
99
|
+
this.fallbackHandler = handler;
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Route a normalized event to its handler: `command` → by name, `action` → by
|
|
104
|
+
* id, `message` → the message handler. Falls back to the registered fallback
|
|
105
|
+
* (or logs at debug and returns) when nothing matches.
|
|
106
|
+
*/
|
|
107
|
+
async dispatch(event, reply) {
|
|
108
|
+
const handler = this.resolve(event);
|
|
109
|
+
if (!handler) {
|
|
110
|
+
this.logger?.debug("No comms handler for event", {
|
|
111
|
+
channel: event.channel,
|
|
112
|
+
kind: event.kind
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
await handler(event, reply);
|
|
117
|
+
}
|
|
118
|
+
resolve(event) {
|
|
119
|
+
if (event.kind === "command" && event.command) {
|
|
120
|
+
return this.commands.get(normalizeCommandName(event.command.name)) ?? this.fallbackHandler;
|
|
121
|
+
}
|
|
122
|
+
if (event.kind === "action" && event.action) {
|
|
123
|
+
return this.actions.get(event.action.id) ?? this.fallbackHandler;
|
|
124
|
+
}
|
|
125
|
+
if (event.kind === "message") {
|
|
126
|
+
return this.messageHandler ?? this.fallbackHandler;
|
|
127
|
+
}
|
|
128
|
+
return this.fallbackHandler;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
ChannelRouter = _ts_decorate([
|
|
132
|
+
Injectable(),
|
|
133
|
+
_ts_metadata("design:type", Function),
|
|
134
|
+
_ts_metadata("design:paramtypes", [
|
|
135
|
+
typeof Logger === "undefined" ? Object : Logger
|
|
136
|
+
])
|
|
137
|
+
], ChannelRouter);
|
|
138
|
+
|
|
139
|
+
// src/comms.error.ts
|
|
140
|
+
import { ServerkitError } from "@maroonedsoftware/errors";
|
|
141
|
+
var CommsError = class extends ServerkitError {
|
|
142
|
+
static {
|
|
143
|
+
__name(this, "CommsError");
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
var IsCommsError = /* @__PURE__ */ __name((error) => error instanceof CommsError, "IsCommsError");
|
|
147
|
+
export {
|
|
148
|
+
ChannelRouter,
|
|
149
|
+
CommsError,
|
|
150
|
+
IsCommsError,
|
|
151
|
+
TemplateRegistry,
|
|
152
|
+
bindReply,
|
|
153
|
+
normalizeCommandName
|
|
154
|
+
};
|
|
155
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/comms.reply.ts","../src/comms.template.ts","../src/comms.router.ts","../src/comms.error.ts"],"sourcesContent":["import type { ChannelId } from './comms.event.js';\nimport type { OutgoingMessage } from './comms.message.js';\n\n/**\n * Send-to-recipient outbound interface, implemented by each channel's `./comms`\n * adapter (`create<Channel>Notifier`). It is the forward-compatible seam that\n * notify-only channels (push, email) will also implement.\n */\nexport interface Notifier {\n /** Which channel this notifier sends on. */\n readonly channel: ChannelId;\n /** Send a portable message to a recipient (chat id / phone / token / address). */\n send(to: string, message: OutgoingMessage): Promise<void>;\n /** Render and send a registered template by name (rich per channel, portable fallback). */\n sendTemplate(to: string, name: string, data?: unknown): Promise<void>;\n /** Send a channel-native payload verbatim — the raw escape hatch. */\n sendNative(to: string, payload: unknown): Promise<void>;\n}\n\n/**\n * A {@link Notifier} pre-bound to one recipient — what handlers receive to reply\n * to the inbound event they're handling. Built by adapters via {@link bindReply}.\n */\nexport interface Reply {\n /** Which channel the originating event came from. */\n readonly channel: ChannelId;\n /** Reply with a portable message. */\n send(message: OutgoingMessage): Promise<void>;\n /** Reply by rendering a registered template by name. */\n sendTemplate(name: string, data?: unknown): Promise<void>;\n /** Reply with a channel-native payload verbatim. */\n sendNative(payload: unknown): Promise<void>;\n}\n\n/**\n * Binds a {@link Notifier} to a fixed recipient, yielding the {@link Reply} that\n * handlers use. Each call delegates to the notifier with the bound `to`.\n */\nexport const bindReply = (notifier: Notifier, to: string): Reply => ({\n channel: notifier.channel,\n send: message => notifier.send(to, message),\n sendTemplate: (name, data) => notifier.sendTemplate(to, name, data),\n sendNative: payload => notifier.sendNative(to, payload),\n});\n","import type { ChannelId } from './comms.event.js';\nimport type { OutgoingMessage } from './comms.message.js';\n\n/** Renders template data into a channel-native payload (rich, channel-specific). */\nexport type NativeRenderer<D = unknown> = (data: D) => unknown;\n/** Renders template data into a portable {@link OutgoingMessage} (cross-channel fallback). */\nexport type PortableRenderer<D = unknown> = (data: D) => OutgoingMessage;\n\n/**\n * Result of {@link TemplateRegistry.render}: either a channel-native payload (a\n * per-channel renderer matched) or a portable message (the default fallback),\n * or `undefined` when no template is registered under the name.\n */\nexport type TemplateRenderResult = { kind: 'native'; payload: unknown } | { kind: 'portable'; message: OutgoingMessage } | undefined;\n\n/**\n * A registry of named outbound templates. Register a **rich, per-channel**\n * renderer with {@link register} and/or a **portable default** with\n * {@link registerDefault}; adapters resolve a template for the current channel\n * via {@link render}, with channel-native taking precedence over the default.\n *\n * The registry stores plain functions — a consumer is free to back a renderer\n * with Handlebars or any engine. No template engine is bundled.\n *\n * @example\n * ```ts\n * registry.register('order.card', 'slack', d => ({ blocks: [...] }));\n * registry.registerDefault('order.card', d => ({ text: `Order ${d.id} ✅` }));\n * ```\n */\nexport class TemplateRegistry {\n private readonly native = new Map<string, Map<ChannelId, NativeRenderer>>();\n private readonly defaults = new Map<string, PortableRenderer>();\n\n /** Register a channel-native renderer for `name` on `channel`. */\n register<D>(name: string, channel: ChannelId, render: NativeRenderer<D>): this {\n let byChannel = this.native.get(name);\n if (!byChannel) {\n byChannel = new Map();\n this.native.set(name, byChannel);\n }\n byChannel.set(channel, render as NativeRenderer);\n return this;\n }\n\n /** Register the portable default renderer for `name`, used on channels without a native renderer. */\n registerDefault<D>(name: string, render: PortableRenderer<D>): this {\n this.defaults.set(name, render as PortableRenderer);\n return this;\n }\n\n /**\n * Resolve and run the best renderer for `name` on `channel`: a channel-native\n * renderer if registered, else the portable default, else `undefined`.\n */\n render(name: string, channel: ChannelId, data: unknown): TemplateRenderResult {\n const nativeRenderer = this.native.get(name)?.get(channel);\n if (nativeRenderer) return { kind: 'native', payload: nativeRenderer(data) };\n const portable = this.defaults.get(name);\n if (portable) return { kind: 'portable', message: portable(data) };\n return undefined;\n }\n}\n","import { Injectable } from 'injectkit';\nimport { Logger } from '@maroonedsoftware/logger';\nimport type { IncomingEvent } from './comms.event.js';\nimport type { Reply } from './comms.reply.js';\nimport { TemplateRegistry } from './comms.template.js';\n\n/** A channel-agnostic handler invoked with the normalized event and a bound {@link Reply}. */\nexport type CommsHandler = (event: IncomingEvent, reply: Reply) => Promise<void> | void;\n\n/** Normalizes a command name to its registry key: no leading slash, lowercased. */\nexport const normalizeCommandName = (name: string): string => name.replace(/^\\//, '').toLowerCase();\n\n/**\n * The channel-agnostic router. Register `command` / `action` / `message`\n * handlers (and an optional `fallback`) once; each channel's `./comms` adapter\n * normalizes its inbound payload into an {@link IncomingEvent} and calls\n * {@link dispatch}, which routes to the matching handler.\n *\n * Holds a {@link TemplateRegistry} (`router.templates`) the adapters read when a\n * handler calls `reply.sendTemplate(...)`.\n *\n * @example\n * ```ts\n * const router = new ChannelRouter();\n * router.command('deploy', async (event, reply) => reply.send({ text: `Deploying ${event.command!.args}` }));\n * router.action('deploy:confirm', async (event, reply) => reply.send({ text: 'Confirmed' }));\n * ```\n */\n@Injectable()\nexport class ChannelRouter {\n /** Outbound template registry shared with the channel adapters. */\n readonly templates = new TemplateRegistry();\n\n private readonly commands = new Map<string, CommsHandler>();\n private readonly actions = new Map<string, CommsHandler>();\n private messageHandler?: CommsHandler;\n private fallbackHandler?: CommsHandler;\n\n constructor(private readonly logger?: Logger) {}\n\n /** Register a handler for a command (with or without a leading slash; name is normalized). */\n command(name: string, handler: CommsHandler): this {\n this.commands.set(normalizeCommandName(name), handler);\n return this;\n }\n\n /** Register a handler for a button/component action id. */\n action(id: string, handler: CommsHandler): this {\n this.actions.set(id, handler);\n return this;\n }\n\n /** Register the single catch-all message handler (free-text, non-command messages). */\n message(handler: CommsHandler): this {\n this.messageHandler = handler;\n return this;\n }\n\n /** Register a fallback handler invoked when nothing else matches. */\n fallback(handler: CommsHandler): this {\n this.fallbackHandler = handler;\n return this;\n }\n\n /**\n * Route a normalized event to its handler: `command` → by name, `action` → by\n * id, `message` → the message handler. Falls back to the registered fallback\n * (or logs at debug and returns) when nothing matches.\n */\n async dispatch(event: IncomingEvent, reply: Reply): Promise<void> {\n const handler = this.resolve(event);\n if (!handler) {\n this.logger?.debug('No comms handler for event', { channel: event.channel, kind: event.kind });\n return;\n }\n await handler(event, reply);\n }\n\n private resolve(event: IncomingEvent): CommsHandler | undefined {\n if (event.kind === 'command' && event.command) {\n return this.commands.get(normalizeCommandName(event.command.name)) ?? this.fallbackHandler;\n }\n if (event.kind === 'action' && event.action) {\n return this.actions.get(event.action.id) ?? this.fallbackHandler;\n }\n if (event.kind === 'message') {\n return this.messageHandler ?? this.fallbackHandler;\n }\n return this.fallbackHandler;\n }\n}\n","import { ServerkitError } from '@maroonedsoftware/errors';\n\n/**\n * Domain error raised by the comms layer (e.g. `sendTemplate` for an\n * unregistered template name).\n *\n * Extends {@link ServerkitError} so `errorMiddleware` renders a 500 with\n * `{ message, details }` if one escapes a route handler.\n */\nexport class CommsError extends ServerkitError {}\n\n/**\n * Type guard for {@link CommsError}. Narrows `unknown` to `CommsError`. Returns\n * `true` for any subclass.\n */\nexport const IsCommsError = (error: unknown): error is CommsError => error instanceof CommsError;\n"],"mappings":"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","names":["bindReply","notifier","to","channel","send","message","sendTemplate","name","data","sendNative","payload","TemplateRegistry","native","Map","defaults","register","name","channel","render","byChannel","get","set","registerDefault","data","nativeRenderer","kind","payload","portable","message","undefined","Injectable","Logger","normalizeCommandName","name","replace","toLowerCase","ChannelRouter","templates","TemplateRegistry","commands","Map","actions","messageHandler","fallbackHandler","logger","command","handler","set","action","id","message","fallback","dispatch","event","reply","resolve","debug","channel","kind","get","ServerkitError","CommsError","ServerkitError","IsCommsError","error"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maroonedsoftware/comms",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Channel-agnostic messaging core (router, reply, template registry) for ServerKit.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Marooned Software",
|
|
7
|
+
"url": "https://github.com/MaroonedSoftware/serverkit"
|
|
8
|
+
},
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/MaroonedSoftware/serverkit/issues"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/MaroonedSoftware/serverkit/packages/comms#readme",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"backend",
|
|
15
|
+
"comms",
|
|
16
|
+
"messaging",
|
|
17
|
+
"serverkit",
|
|
18
|
+
"typescript"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/MaroonedSoftware/serverkit.git"
|
|
23
|
+
},
|
|
24
|
+
"private": false,
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"module": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"files": [
|
|
31
|
+
"dist/**"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup src/index.ts --format esm --sourcemap && tsc --emitDeclarationOnly --declaration",
|
|
35
|
+
"build:ci": "eslint --max-warnings=0 && pnpm run build",
|
|
36
|
+
"lint": "eslint --fix",
|
|
37
|
+
"format": "prettier --write .",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:ci": "vitest run --coverage"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@maroonedsoftware/errors": "workspace:*",
|
|
43
|
+
"@maroonedsoftware/logger": "workspace:*",
|
|
44
|
+
"injectkit": "^1.5.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@repo/config-eslint": "workspace:*",
|
|
48
|
+
"@repo/config-typescript": "workspace:*"
|
|
49
|
+
}
|
|
50
|
+
}
|