@kwirthmagnify/kwirth-sender-tee 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +79 -0
  2. package/front.js +1 -1
  3. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # Tee Sender
2
+
3
+ Sender intermedio que recibe un mensaje y lo reenvía en paralelo a múltiples senders de destino configurados, sin hacer ningún envío final él mismo. Funciona como el comando Unix `tee`: lo que entra, sale por todos los canales a la vez.
4
+
5
+ ## Cuándo usarlo
6
+
7
+ - Enviar la misma alerta simultáneamente a consola y a fichero.
8
+ - Construir pipelines de notificación: un único punto de entrada que abanica hacia varios destinos.
9
+ - Encadenar tees para topologías más complejas (un tee apuntando a otro tee).
10
+
11
+ ## Configuración
12
+
13
+ Cada config tiene un campo `targets`: lista de pares `(senderId, configName)` que identifican el sender y la config de destino.
14
+
15
+ | Campo | Tipo | Requerido | Descripción |
16
+ |-----------|-----------------------------------|-----------|------------------------------------------------|
17
+ | `name` | `string` | Sí | Nombre de esta config |
18
+ | `targets` | `Array<{senderId, configName}>` | Sí | Lista de senders de destino |
19
+
20
+ ### Estructura de un target
21
+
22
+ ```json
23
+ {
24
+ "senderId": "console", // id del sender destino (ej: "console", "file", "tee")
25
+ "configName": "mi-console" // nombre de la config ya registrada en ese sender
26
+ }
27
+ ```
28
+
29
+ ## Ejemplos
30
+
31
+ ### Fan-out a consola y fichero
32
+
33
+ Prerequisito: tener configurados un `ConsoleSender` con config `"alerts-console"` y un `FileSender` con config `"alerts-file"`.
34
+
35
+ ```json
36
+ {
37
+ "name": "alerts-tee",
38
+ "targets": [
39
+ { "senderId": "console", "configName": "alerts-console" },
40
+ { "senderId": "file", "configName": "alerts-file" }
41
+ ]
42
+ }
43
+ ```
44
+
45
+ Invocar:
46
+ ```ts
47
+ senderAccess.send('tee', 'alerts-tee', {
48
+ level: 'error',
49
+ subject: 'Pod crash',
50
+ body: 'nginx-7d4f crashed in namespace prod'
51
+ })
52
+ ```
53
+
54
+ El mensaje llega al mismo tiempo a stdout y al fichero de log.
55
+
56
+ ### Cadena de tees
57
+
58
+ ```
59
+ tee-main
60
+ ├─ tee-ops → console + file
61
+ └─ tee-biz → kafka + slack
62
+ ```
63
+
64
+ Config `tee-main`:
65
+ ```json
66
+ {
67
+ "name": "tee-main",
68
+ "targets": [
69
+ { "senderId": "tee", "configName": "tee-ops" },
70
+ { "senderId": "tee", "configName": "tee-biz" }
71
+ ]
72
+ }
73
+ ```
74
+
75
+ ## Comportamiento
76
+
77
+ - Los envíos a todos los targets se hacen en **paralelo** (`Promise.all`).
78
+ - Si un target falla, el error se propaga (el `Promise.all` rechaza). Los targets completados antes del fallo **no se revierten**.
79
+ - El `ISenderAccess` se inyecta en `startSender()` — el tee no funciona antes de que el sistema lo inicialice.
package/front.js CHANGED
@@ -199,7 +199,7 @@
199
199
  ), /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "text.secondary", fontWeight: "bold" }, "Targets"), /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: { flex: 1, border: 1, borderColor: "divider", borderRadius: 1, overflowY: "auto" } }, targets.length === 0 ? /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "text.disabled", sx: { p: 1, display: "block" } }, "No targets yet.") : targets.map((t, i) => /* @__PURE__ */ import_react.default.createElement(import_material.Box, { key: i, sx: { display: "flex", alignItems: "center", gap: 1, px: 1, py: 0.5, borderBottom: 1, borderColor: "divider" } }, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2", sx: { flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, t.senderId), /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2", color: "text.secondary", sx: { flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, t.configName), /* @__PURE__ */ import_react.default.createElement(import_material.IconButton, { size: "small", color: "error", onClick: () => setTargets((prev) => prev.filter((_, idx) => idx !== i)) }, /* @__PURE__ */ import_react.default.createElement(import_icons_material.Delete, { sx: { fontSize: 14 } }))))), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 1, alignItems: "center" }, /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { flex: 1 } }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Sender"), /* @__PURE__ */ import_react.default.createElement(import_material.Select, { label: "Sender", value: newSenderId, onChange: (e) => {
200
200
  setNewSenderId(e.target.value);
201
201
  setNewConfigName("");
202
- } }, senders.map((s) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: s.id, value: s.id }, s.displayName || s.id)))), /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { flex: 1 }, disabled: !newSenderId }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Config"), /* @__PURE__ */ import_react.default.createElement(import_material.Select, { label: "Config", value: newConfigName, onChange: (e) => setNewConfigName(e.target.value) }, newSenderConfigs.map((c) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: c, value: c }, c)))), /* @__PURE__ */ import_react.default.createElement(import_material.Tooltip, { title: "Add target" }, /* @__PURE__ */ import_react.default.createElement("span", null, /* @__PURE__ */ import_react.default.createElement(import_material.IconButton, { size: "small", color: "primary", disabled: !newSenderId || !newConfigName, onClick: addTarget }, /* @__PURE__ */ import_react.default.createElement(import_icons_material.Add, null))))), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", justifyContent: "flex-end", alignItems: "center", spacing: 1 }, error && /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "error", sx: { flex: 1 } }, error), /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", variant: "contained", disabled: saving || !editName.trim(), onClick: save }, saving ? /* @__PURE__ */ import_react.default.createElement(import_material.CircularProgress, { size: 14 }) : selectedName ? "Update" : "Add")))), /* @__PURE__ */ import_react.default.createElement(import_material.DialogActions, null, /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", onClick: onClose }, "Close")));
202
+ } }, senders.map((s) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: s.id, value: s.id }, s.displayName || s.id)))), /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { flex: 1 }, disabled: !newSenderId }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Config"), /* @__PURE__ */ import_react.default.createElement(import_material.Select, { label: "Config", value: newConfigName, onChange: (e) => setNewConfigName(e.target.value) }, newSenderConfigs.map((c) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: c, value: c }, c)))), /* @__PURE__ */ import_react.default.createElement(import_material.Tooltip, { title: "Add target" }, /* @__PURE__ */ import_react.default.createElement("span", null, /* @__PURE__ */ import_react.default.createElement(import_material.IconButton, { size: "small", color: "primary", disabled: !newSenderId || !newConfigName, onClick: addTarget }, /* @__PURE__ */ import_react.default.createElement(import_icons_material.Add, null))))), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", justifyContent: "flex-end", alignItems: "center", spacing: 1 }, error && /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "error", sx: { flex: 1 } }, error), /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", variant: "contained", disabled: saving || !editName.trim(), onClick: save }, saving ? /* @__PURE__ */ import_react.default.createElement(import_material.CircularProgress, { size: 14 }) : selectedName ? "Update" : "Add")))), /* @__PURE__ */ import_react.default.createElement(import_material.DialogActions, null, /* @__PURE__ */ import_react.default.createElement(import_material.Button, { onClick: onClose }, "Close")));
203
203
  };
204
204
  var TeeSenderDialog_default = TeeSenderDialog;
205
205
 
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "id": "tee",
5
5
  "name": "@kwirthmagnify/kwirth-sender-tee",
6
6
  "displayName": "Tee Sender",
7
- "version": "0.1.12",
7
+ "version": "0.1.14",
8
8
  "description": "Tee sender plugin for Kwirth — fans out an incoming message to multiple downstream senders simultaneously, acting as an intermediate pipeline node",
9
9
  "requiresRestart": false,
10
10
  "requiresExtension": []