@interactive-inc/claude-funnel 0.7.1 → 0.8.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 +155 -133
- package/dist/bin.js +1417 -0
- package/dist/gateway/daemon.js +513 -0
- package/dist/highlights-eq9cgrbb.scm +604 -0
- package/dist/highlights-ghv9g403.scm +205 -0
- package/dist/highlights-hk7bwhj4.scm +284 -0
- package/dist/highlights-r812a2qc.scm +150 -0
- package/dist/highlights-x6tmsnaa.scm +115 -0
- package/dist/injections-73j83es3.scm +27 -0
- package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/lib/bin.ts +78 -0
- package/lib/{modules → cli}/router/to-request.ts +13 -20
- package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +27 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +40 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +41 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +22 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +23 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.ts +26 -0
- package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +92 -0
- package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +22 -0
- package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +63 -0
- package/lib/cli/routes/channels.$channel.connectors.ts +26 -0
- package/lib/cli/routes/channels.$channel.rename.$newName.ts +22 -0
- package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +34 -0
- package/lib/cli/routes/channels.$channel.ts +34 -0
- package/lib/cli/routes/channels.add.$channel.ts +33 -0
- package/lib/cli/routes/channels.remove.$channel.ts +20 -0
- package/lib/cli/routes/channels.ts +39 -0
- package/lib/cli/routes/claude.ts +69 -0
- package/lib/cli/routes/gateway.listeners.ts +41 -0
- package/lib/cli/routes/gateway.logs.ts +123 -0
- package/lib/{routes/gateway/restart.ts → cli/routes/gateway.restart.ts} +20 -5
- package/lib/cli/routes/gateway.run.ts +41 -0
- package/lib/cli/routes/gateway.start.ts +50 -0
- package/lib/cli/routes/gateway.status.ts +19 -0
- package/lib/cli/routes/gateway.stop.ts +32 -0
- package/lib/cli/routes/gateway.ts +55 -0
- package/lib/cli/routes/index.ts +202 -0
- package/lib/cli/routes/profiles.$profile.as-default.ts +22 -0
- package/lib/cli/routes/profiles.$profile.rename.$newName.ts +22 -0
- package/lib/cli/routes/profiles.$profile.run.ts +36 -0
- package/lib/cli/routes/profiles.add.$profile.ts +46 -0
- package/lib/cli/routes/profiles.remove.$profile.ts +20 -0
- package/lib/cli/routes/profiles.set.$profile.ts +46 -0
- package/lib/cli/routes/profiles.ts +40 -0
- package/lib/cli/routes/status.ts +93 -0
- package/lib/cli/routes/update.ts +27 -0
- package/lib/connectors/connector-config-schema.ts +16 -0
- package/lib/connectors/connector-factory.ts +94 -0
- package/lib/connectors/connector-listener.ts +20 -0
- package/lib/{modules/connectors/funnel-discord-adapter.ts → connectors/discord-adapter.ts} +6 -11
- package/lib/{modules/connectors → connectors}/discord-connector-schema.ts +4 -1
- package/lib/connectors/discord-listener.ts +111 -0
- package/lib/{modules/connectors/funnel-gh-adapter.ts → connectors/gh-adapter.ts} +3 -6
- package/lib/{modules/connectors → connectors}/gh-connector-schema.ts +4 -1
- package/lib/{modules/connectors/funnel-gh-listener.ts → connectors/gh-listener.ts} +45 -19
- package/lib/{modules/connectors → connectors}/match-cron.ts +10 -4
- package/lib/connectors/schedule-connector-schema.ts +33 -0
- package/lib/connectors/schedule-listener.ts +207 -0
- package/lib/connectors/schedule-state-store.ts +54 -0
- package/lib/connectors/slack-adapter.ts +36 -0
- package/lib/{modules/connectors → connectors}/slack-connector-schema.ts +4 -1
- package/lib/{modules/connectors/funnel-slack-event-processor.ts → connectors/slack-event-processor.ts} +15 -9
- package/lib/{modules/connectors/funnel-slack-listener.ts → connectors/slack-listener.ts} +33 -14
- package/lib/engine/channels/channels.ts +520 -0
- package/lib/{modules/claude/funnel-claude.ts → engine/claude/claude.ts} +28 -55
- package/lib/engine/claude/gateway-controller.ts +4 -0
- package/lib/{modules/fs/funnel-file-system.ts → engine/fs/file-system.ts} +4 -0
- package/lib/{modules/fs/memory-funnel-file-system.ts → engine/fs/memory-file-system.ts} +20 -3
- package/lib/{modules/fs/node-funnel-file-system.ts → engine/fs/node-file-system.ts} +14 -2
- package/lib/{modules/http/memory-funnel-http-client.ts → engine/http/memory-http-client.ts} +1 -5
- package/lib/{modules/http/node-funnel-http-client.ts → engine/http/node-http-client.ts} +1 -5
- package/lib/{modules/id/memory-funnel-id-generator.ts → engine/id/memory-id-generator.ts} +1 -1
- package/lib/{modules/id/node-funnel-id-generator.ts → engine/id/node-id-generator.ts} +1 -1
- package/lib/{modules/logger/memory-funnel-logger.ts → engine/logger/memory-logger.ts} +1 -1
- package/lib/{modules/logger/node-funnel-logger.ts → engine/logger/node-logger.ts} +1 -1
- package/lib/{modules/logger/noop-funnel-logger.ts → engine/logger/noop-logger.ts} +1 -1
- package/lib/engine/mcp/channel-server.ts +204 -0
- package/lib/{modules/mcp/funnel-mcp.ts → engine/mcp/mcp.ts} +24 -10
- package/lib/{modules/process/memory-funnel-process-runner.ts → engine/process/memory-process-runner.ts} +1 -1
- package/lib/{modules/process/node-funnel-process-runner.ts → engine/process/node-process-runner.ts} +12 -21
- package/lib/engine/profiles/profile-channel-checker.ts +7 -0
- package/lib/{modules/profiles/funnel-profiles.ts → engine/profiles/profiles.ts} +41 -43
- package/lib/{modules/settings/mock-funnel-settings-reader.ts → engine/settings/mock-settings-reader.ts} +4 -3
- package/lib/{modules/settings/funnel-settings-reader.ts → engine/settings/settings-reader.ts} +1 -1
- package/lib/engine/settings/settings-schema.ts +46 -0
- package/lib/engine/settings/settings-store.ts +110 -0
- package/lib/{modules/time/memory-funnel-clock.ts → engine/time/memory-clock.ts} +1 -1
- package/lib/{modules/time/node-funnel-clock.ts → engine/time/node-clock.ts} +1 -1
- package/lib/funnel.ts +83 -78
- package/lib/gateway/auth-middleware.ts +44 -0
- package/lib/gateway/broadcaster.ts +319 -0
- package/lib/gateway/daemon.ts +47 -0
- package/lib/gateway/factory.ts +10 -0
- package/lib/gateway/funnel-event-store.ts +155 -0
- package/lib/gateway/gateway-server.ts +414 -0
- package/lib/gateway/gateway-token.ts +79 -0
- package/lib/{modules/gateway/funnel-gateway.ts → gateway/gateway.ts} +27 -13
- package/lib/{modules/gateway → gateway}/kill-competing-slack-gateways.ts +4 -4
- package/lib/gateway/listener-supervisor.ts +339 -0
- package/lib/gateway/listeners-client.ts +128 -0
- package/lib/gateway/resolve-daemon-script.ts +26 -0
- package/lib/gateway/routes/channels.connectors.call.ts +39 -0
- package/lib/gateway/routes/health.ts +13 -0
- package/lib/gateway/routes/index.ts +24 -0
- package/lib/gateway/routes/listeners.list.ts +6 -0
- package/lib/gateway/routes/listeners.restart.ts +15 -0
- package/lib/gateway/routes/listeners.start.ts +15 -0
- package/lib/gateway/routes/listeners.stop.ts +15 -0
- package/lib/gateway/routes/route-deps.ts +11 -0
- package/lib/gateway/routes/status.ts +15 -0
- package/lib/gateway/routes/validator.ts +17 -0
- package/lib/index.ts +50 -92
- package/lib/logger/leuco-human-file-writer.ts +65 -0
- package/lib/logger/leuco-human-logger.ts +98 -0
- package/lib/logger/leuco-human-record.ts +16 -0
- package/lib/logger/leuco-human-stdout-writer.ts +26 -0
- package/lib/logger/leuco-human-writer.ts +14 -0
- package/lib/logger/leuco-logger-memory-sink.ts +67 -0
- package/lib/logger/leuco-logger-record.ts +13 -0
- package/lib/logger/leuco-logger-sink.ts +33 -0
- package/lib/logger/leuco-logger-sqlite-sink.ts +355 -0
- package/lib/logger/leuco-logger.ts +135 -0
- package/lib/tui/app.tsx +357 -0
- package/lib/tui/components/add-row.tsx +18 -0
- package/lib/tui/components/brand.tsx +27 -0
- package/lib/tui/components/card.tsx +44 -0
- package/lib/tui/components/detail-bar.tsx +46 -0
- package/lib/tui/components/editable-field.tsx +33 -0
- package/lib/tui/components/empty-state.tsx +11 -0
- package/lib/tui/components/gateway-status.tsx +66 -0
- package/lib/tui/components/keymap.tsx +29 -0
- package/lib/tui/components/menu-item.tsx +73 -0
- package/lib/tui/components/menu.tsx +26 -0
- package/lib/tui/components/panel-header.tsx +22 -0
- package/lib/tui/components/readonly-field.tsx +18 -0
- package/lib/tui/components/section-header.tsx +25 -0
- package/lib/tui/components/selection-accent.tsx +32 -0
- package/lib/tui/components/session-item.tsx +33 -0
- package/lib/tui/components/session-list.tsx +33 -0
- package/lib/tui/components/ui/hascii/accordion-item.tsx +88 -0
- package/lib/tui/components/ui/hascii/accordion.tsx +96 -0
- package/lib/tui/components/ui/hascii/alert-dialog.tsx +43 -0
- package/lib/tui/components/ui/hascii/badge.tsx +51 -0
- package/lib/tui/components/ui/hascii/breadcrumb.tsx +58 -0
- package/lib/tui/components/ui/hascii/button.tsx +194 -0
- package/lib/tui/components/ui/hascii/card-content.tsx +14 -0
- package/lib/tui/components/ui/hascii/card-description.tsx +13 -0
- package/lib/tui/components/ui/hascii/card-footer.tsx +14 -0
- package/lib/tui/components/ui/hascii/card-header.tsx +14 -0
- package/lib/tui/components/ui/hascii/card-title.tsx +13 -0
- package/lib/tui/components/ui/hascii/card.tsx +27 -0
- package/lib/tui/components/ui/hascii/checkbox.tsx +65 -0
- package/lib/tui/components/ui/hascii/command.tsx +159 -0
- package/lib/tui/components/ui/hascii/dialog-content.tsx +14 -0
- package/lib/tui/components/ui/hascii/dialog-description.tsx +13 -0
- package/lib/tui/components/ui/hascii/dialog-footer.tsx +14 -0
- package/lib/tui/components/ui/hascii/dialog-header.tsx +14 -0
- package/lib/tui/components/ui/hascii/dialog-title.tsx +13 -0
- package/lib/tui/components/ui/hascii/dialog.tsx +27 -0
- package/lib/tui/components/ui/hascii/file-tree.tsx +142 -0
- package/lib/tui/components/ui/hascii/focus-group.tsx +62 -0
- package/lib/tui/components/ui/hascii/form-item.tsx +43 -0
- package/lib/tui/components/ui/hascii/input-otp.tsx +86 -0
- package/lib/tui/components/ui/hascii/input.tsx +130 -0
- package/lib/tui/components/ui/hascii/pagination.tsx +105 -0
- package/lib/tui/components/ui/hascii/progress.tsx +28 -0
- package/lib/tui/components/ui/hascii/select.tsx +131 -0
- package/lib/tui/components/ui/hascii/separator.tsx +35 -0
- package/lib/tui/components/ui/hascii/sidebar-content.tsx +23 -0
- package/lib/tui/components/ui/hascii/sidebar-header.tsx +14 -0
- package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +67 -0
- package/lib/tui/components/ui/hascii/sidebar.tsx +24 -0
- package/lib/tui/components/ui/hascii/skeleton.tsx +60 -0
- package/lib/tui/components/ui/hascii/slider.tsx +91 -0
- package/lib/tui/components/ui/hascii/snackbar.tsx +75 -0
- package/lib/tui/components/ui/hascii/sparkline.tsx +53 -0
- package/lib/tui/components/ui/hascii/spinner.tsx +47 -0
- package/lib/tui/components/ui/hascii/stepper.tsx +54 -0
- package/lib/tui/components/ui/hascii/switch.tsx +66 -0
- package/lib/tui/components/ui/hascii/table.tsx +95 -0
- package/lib/tui/components/ui/hascii/tabs.tsx +59 -0
- package/lib/tui/components/ui/hascii/toggle-group-item.tsx +45 -0
- package/lib/tui/components/ui/hascii/toggle-group.tsx +99 -0
- package/lib/tui/components/ui/hascii/tree.tsx +104 -0
- package/lib/tui/components/view-shell.tsx +44 -0
- package/lib/tui/filter-input.tsx +33 -0
- package/lib/tui/hooks/hascii/use-pressable.ts +54 -0
- package/lib/tui/parse-comma-list.ts +14 -0
- package/lib/tui/profile-launcher.tsx +61 -0
- package/lib/tui/scrollbar-options.ts +19 -0
- package/lib/tui/sidebar.tsx +50 -0
- package/lib/tui/theme.ts +40 -0
- package/lib/tui/tui.tsx +20 -0
- package/lib/tui/types.ts +38 -0
- package/lib/tui/unique-name.ts +18 -0
- package/lib/tui/use-event-stream.ts +133 -0
- package/lib/tui/use-snapshot.ts +99 -0
- package/lib/tui/utils/hascii/form-item-context.tsx +23 -0
- package/lib/tui/utils/hascii/input-focus-context.tsx +31 -0
- package/lib/tui/utils/hascii/theme-context.tsx +26 -0
- package/lib/tui/utils/hascii/theme.ts +176 -0
- package/lib/tui/views/channels-view.tsx +108 -0
- package/lib/tui/views/connectors-view.tsx +164 -0
- package/lib/tui/views/events-view.tsx +160 -0
- package/lib/tui/views/listeners-view.tsx +80 -0
- package/lib/tui/views/profiles-view.tsx +152 -0
- package/package.json +50 -44
- package/lib/api.ts +0 -54
- package/lib/modules/channels/channel-connector-ref-updater.ts +0 -4
- package/lib/modules/channels/funnel-channels.ts +0 -160
- package/lib/modules/connectors/connector-config-schema.ts +0 -16
- package/lib/modules/connectors/connector-existence-checker.ts +0 -3
- package/lib/modules/connectors/funnel-callable-connector-store.ts +0 -9
- package/lib/modules/connectors/funnel-connector-listener.ts +0 -5
- package/lib/modules/connectors/funnel-connector-stores.ts +0 -52
- package/lib/modules/connectors/funnel-connector-type-store.ts +0 -24
- package/lib/modules/connectors/funnel-connectors.ts +0 -151
- package/lib/modules/connectors/funnel-discord-listener.ts +0 -71
- package/lib/modules/connectors/funnel-discord-store.ts +0 -88
- package/lib/modules/connectors/funnel-gh-store.ts +0 -101
- package/lib/modules/connectors/funnel-json-connector-store.ts +0 -100
- package/lib/modules/connectors/funnel-schedule-listener.ts +0 -130
- package/lib/modules/connectors/funnel-schedule-store.ts +0 -195
- package/lib/modules/connectors/funnel-slack-adapter.ts +0 -31
- package/lib/modules/connectors/funnel-slack-store.ts +0 -90
- package/lib/modules/connectors/migrate-legacy-connectors.ts +0 -81
- package/lib/modules/connectors/schedule-connector-schema.ts +0 -18
- package/lib/modules/connectors/schedule-last-fired-store.ts +0 -48
- package/lib/modules/gateway/daemon.ts +0 -74
- package/lib/modules/gateway/funnel-broadcaster.ts +0 -37
- package/lib/modules/gateway/funnel-event-logger.ts +0 -59
- package/lib/modules/gateway/funnel-gateway-server.ts +0 -241
- package/lib/modules/mcp/channel-server.ts +0 -76
- package/lib/modules/profiles/profile-channel-checker.ts +0 -3
- package/lib/modules/profiles/profile-channel-ref-updater.ts +0 -3
- package/lib/modules/repos/funnel-repositories.ts +0 -112
- package/lib/modules/schedule/funnel-schedule.ts +0 -39
- package/lib/modules/settings/funnel-settings-store.ts +0 -56
- package/lib/modules/settings/settings-schema.ts +0 -33
- package/lib/modules/tui/app.tsx +0 -44
- package/lib/modules/tui/tui.tsx +0 -13
- package/lib/routes/channels/add.help.ts +0 -3
- package/lib/routes/channels/add.ts +0 -21
- package/lib/routes/channels/connectors-attach.help.ts +0 -3
- package/lib/routes/channels/connectors-attach.ts +0 -17
- package/lib/routes/channels/connectors-detach.help.ts +0 -3
- package/lib/routes/channels/connectors-detach.ts +0 -17
- package/lib/routes/channels/group.help.ts +0 -16
- package/lib/routes/channels/group.ts +0 -22
- package/lib/routes/channels/remove.help.ts +0 -3
- package/lib/routes/channels/remove.ts +0 -17
- package/lib/routes/channels/rename.help.ts +0 -5
- package/lib/routes/channels/rename.ts +0 -17
- package/lib/routes/channels/routes.ts +0 -19
- package/lib/routes/channels/show.help.ts +0 -1
- package/lib/routes/channels/show.ts +0 -26
- package/lib/routes/claude/claude.help.ts +0 -16
- package/lib/routes/claude/claude.ts +0 -76
- package/lib/routes/claude/routes.ts +0 -4
- package/lib/routes/connectors/add.help.ts +0 -28
- package/lib/routes/connectors/add.ts +0 -64
- package/lib/routes/connectors/group.help.ts +0 -14
- package/lib/routes/connectors/group.ts +0 -18
- package/lib/routes/connectors/remove.help.ts +0 -3
- package/lib/routes/connectors/remove.ts +0 -17
- package/lib/routes/connectors/rename.help.ts +0 -5
- package/lib/routes/connectors/rename.ts +0 -17
- package/lib/routes/connectors/routes.ts +0 -23
- package/lib/routes/connectors/schedules-add.help.ts +0 -11
- package/lib/routes/connectors/schedules-add.ts +0 -33
- package/lib/routes/connectors/schedules-group.help.ts +0 -1
- package/lib/routes/connectors/schedules-group.ts +0 -38
- package/lib/routes/connectors/schedules-remove.help.ts +0 -3
- package/lib/routes/connectors/schedules-remove.ts +0 -17
- package/lib/routes/connectors/set.help.ts +0 -8
- package/lib/routes/connectors/set.ts +0 -72
- package/lib/routes/connectors/show.help.ts +0 -1
- package/lib/routes/connectors/show.ts +0 -41
- package/lib/routes/gateway/group.help.ts +0 -15
- package/lib/routes/gateway/group.ts +0 -28
- package/lib/routes/gateway/logs.help.ts +0 -13
- package/lib/routes/gateway/logs.ts +0 -102
- package/lib/routes/gateway/restart.help.ts +0 -10
- package/lib/routes/gateway/routes.ts +0 -18
- package/lib/routes/gateway/run.help.ts +0 -12
- package/lib/routes/gateway/run.ts +0 -35
- package/lib/routes/gateway/start.help.ts +0 -15
- package/lib/routes/gateway/start.ts +0 -32
- package/lib/routes/gateway/status.help.ts +0 -9
- package/lib/routes/gateway/status.ts +0 -28
- package/lib/routes/gateway/stop.help.ts +0 -8
- package/lib/routes/gateway/stop.ts +0 -21
- package/lib/routes/profiles/add.help.ts +0 -3
- package/lib/routes/profiles/add.ts +0 -33
- package/lib/routes/profiles/group.help.ts +0 -16
- package/lib/routes/profiles/group.ts +0 -25
- package/lib/routes/profiles/launch.help.ts +0 -4
- package/lib/routes/profiles/launch.ts +0 -36
- package/lib/routes/profiles/remove.help.ts +0 -3
- package/lib/routes/profiles/remove.ts +0 -17
- package/lib/routes/profiles/rename.help.ts +0 -5
- package/lib/routes/profiles/rename.ts +0 -17
- package/lib/routes/profiles/routes.ts +0 -18
- package/lib/routes/profiles/set.help.ts +0 -5
- package/lib/routes/profiles/set.ts +0 -32
- package/lib/routes/repos/add.help.ts +0 -6
- package/lib/routes/repos/add.ts +0 -20
- package/lib/routes/repos/group.help.ts +0 -11
- package/lib/routes/repos/group.ts +0 -18
- package/lib/routes/repos/remove.help.ts +0 -3
- package/lib/routes/repos/remove.ts +0 -17
- package/lib/routes/repos/rename.help.ts +0 -5
- package/lib/routes/repos/rename.ts +0 -17
- package/lib/routes/repos/routes.ts +0 -17
- package/lib/routes/repos/set.help.ts +0 -5
- package/lib/routes/repos/set.ts +0 -21
- package/lib/routes/repos/show.help.ts +0 -1
- package/lib/routes/repos/show.ts +0 -19
- package/lib/routes/request/discord-help.ts +0 -9
- package/lib/routes/request/discord.help.ts +0 -19
- package/lib/routes/request/discord.ts +0 -65
- package/lib/routes/request/group.help.ts +0 -15
- package/lib/routes/request/group.ts +0 -9
- package/lib/routes/request/routes.ts +0 -14
- package/lib/routes/request/slack-help.ts +0 -9
- package/lib/routes/request/slack.help.ts +0 -19
- package/lib/routes/request/slack.ts +0 -61
- package/lib/routes/status/routes.ts +0 -4
- package/lib/routes/status/status.help.ts +0 -6
- package/lib/routes/status/status.ts +0 -77
- package/lib/routes/update/routes.ts +0 -4
- package/lib/routes/update/update.help.ts +0 -5
- package/lib/routes/update/update.ts +0 -21
- package/lib/routes.ts +0 -40
- /package/lib/{factory.ts → cli/factory.ts} +0 -0
- /package/lib/{modules → cli}/router/query-to-cli-args.ts +0 -0
- /package/lib/{modules → cli}/router/validator.ts +0 -0
- /package/lib/{modules/connectors/funnel-connector-adapter.ts → connectors/connector-adapter.ts} +0 -0
- /package/lib/{modules/connectors/funnel-discord-event-processor.ts → connectors/discord-event-processor.ts} +0 -0
- /package/lib/{modules/http/funnel-http-client.ts → engine/http/http-client.ts} +0 -0
- /package/lib/{modules/id/funnel-id-generator.ts → engine/id/id-generator.ts} +0 -0
- /package/lib/{modules/logger/funnel-logger.ts → engine/logger/logger.ts} +0 -0
- /package/lib/{modules/process/funnel-process-runner.ts → engine/process/process-runner.ts} +0 -0
- /package/lib/{modules/time/funnel-clock.ts → engine/time/clock.ts} +0 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import type { ServerWebSocket } from "bun"
|
|
2
|
+
import { FunnelLogger } from "@/engine/logger/logger"
|
|
3
|
+
import { NoopFunnelLogger } from "@/engine/logger/noop-logger"
|
|
4
|
+
|
|
5
|
+
const byteLengthOf = (event: { content: string; meta?: Record<string, string> }): number => {
|
|
6
|
+
let bytes = Buffer.byteLength(event.content, "utf-8")
|
|
7
|
+
|
|
8
|
+
if (event.meta) {
|
|
9
|
+
for (const [k, v] of Object.entries(event.meta)) {
|
|
10
|
+
bytes += Buffer.byteLength(k, "utf-8") + Buffer.byteLength(v, "utf-8")
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return bytes
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type ClientData = {
|
|
18
|
+
/** Stable channel id (uuid) that the WS client subscribed to. */
|
|
19
|
+
channel: string
|
|
20
|
+
/** Human-facing channel name resolved at upgrade time, kept for log readability. */
|
|
21
|
+
channelName?: string | null
|
|
22
|
+
/** Connector names belonging to that channel; used by tap-all replay filtering. */
|
|
23
|
+
connectors: string[]
|
|
24
|
+
tapAll?: boolean
|
|
25
|
+
/** Routing mode resolved from channel config at upgrade time. Defaults to fanout. */
|
|
26
|
+
delivery?: "fanout" | "exclusive"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type BroadcastEvent = {
|
|
30
|
+
content: string
|
|
31
|
+
meta?: Record<string, string>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type ReplayableEvent = BroadcastEvent & { offset: number }
|
|
35
|
+
|
|
36
|
+
export type BroadcastSubscriber = (event: ReplayableEvent) => void
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Optional persistent replay source. Wired in by the gateway-server with
|
|
40
|
+
* `FunnelEventStore` (SQLite-backed) so reconnects across daemon restarts
|
|
41
|
+
* can recover events older than the in-memory buffer via an indexed
|
|
42
|
+
* `seq > since` range scan.
|
|
43
|
+
*/
|
|
44
|
+
type ReplaySource = {
|
|
45
|
+
loadSince(since: number): ReplayableEvent[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type Deps = {
|
|
49
|
+
logger?: FunnelLogger
|
|
50
|
+
maxBufferedBytes?: number
|
|
51
|
+
now?: () => number
|
|
52
|
+
/** Number of recent events kept in the in-memory replay buffer. */
|
|
53
|
+
replayBufferSize?: number
|
|
54
|
+
/** Hard byte cap on replay buffer payloads. Older events are evicted FIFO until under this cap. */
|
|
55
|
+
replayBufferMaxBytes?: number
|
|
56
|
+
/** Persistent replay source consulted when the in-memory buffer cannot satisfy `since`. */
|
|
57
|
+
persistentReplay?: ReplaySource
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const DEFAULT_MAX_BUFFERED_BYTES = 1024 * 1024
|
|
61
|
+
const DEFAULT_REPLAY_BUFFER_SIZE = 200
|
|
62
|
+
const DEFAULT_REPLAY_BUFFER_MAX_BYTES = 4 * 1024 * 1024
|
|
63
|
+
const defaultLogger = new NoopFunnelLogger()
|
|
64
|
+
|
|
65
|
+
type BroadcasterMetrics = {
|
|
66
|
+
clients: number
|
|
67
|
+
subscribers: number
|
|
68
|
+
eventsBroadcast: number
|
|
69
|
+
droppedSlowClients: number
|
|
70
|
+
lastBroadcastAt: string | null
|
|
71
|
+
/** Latest emitted offset. Clients can `?since=<offset>` to ask for events strictly after this point. */
|
|
72
|
+
latestOffset: number
|
|
73
|
+
/** Oldest offset still held in the replay buffer. Older values cannot be replayed and trigger a full resync. */
|
|
74
|
+
oldestReplayableOffset: number | null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* In-process pub/sub for connector events.
|
|
79
|
+
*
|
|
80
|
+
* Two outbound paths:
|
|
81
|
+
* - WS clients connected via the gateway's `/ws` endpoint, scoped per channel
|
|
82
|
+
* - In-process subscribers registered via `subscribe()` (programmable API)
|
|
83
|
+
*
|
|
84
|
+
* Backpressure: if a WS client's `bufferedAmount` exceeds `maxBufferedBytes`
|
|
85
|
+
* (default 1 MiB), the client is closed with code 1009 and dropped from the
|
|
86
|
+
* registry to keep one slow consumer from blocking the daemon.
|
|
87
|
+
*
|
|
88
|
+
* Replay: every emitted event gets a strictly increasing `offset`. The latest
|
|
89
|
+
* `replayBufferSize` events are kept in memory; reconnecting WS clients can
|
|
90
|
+
* pass `?since=<offset>` and the broadcaster resends matching events before
|
|
91
|
+
* resuming the live stream. The in-memory ring covers short reconnects;
|
|
92
|
+
* older history is served from the SQLite event store wired in as
|
|
93
|
+
* `persistentReplay`.
|
|
94
|
+
*/
|
|
95
|
+
export class FunnelBroadcaster {
|
|
96
|
+
private readonly clients: Map<ServerWebSocket<unknown>, ClientData> = new Map()
|
|
97
|
+
private readonly subscribers: Set<BroadcastSubscriber> = new Set()
|
|
98
|
+
private readonly logger: FunnelLogger
|
|
99
|
+
private readonly maxBufferedBytes: number
|
|
100
|
+
private readonly now: () => number
|
|
101
|
+
private readonly replayBufferSize: number
|
|
102
|
+
private readonly replayBufferMaxBytes: number
|
|
103
|
+
private readonly replayBuffer: ReplayableEvent[] = []
|
|
104
|
+
private readonly persistentReplay: ReplaySource | null
|
|
105
|
+
private readonly exclusiveCursor = new Map<string, number>()
|
|
106
|
+
private replayBufferBytes = 0
|
|
107
|
+
private eventsBroadcast = 0
|
|
108
|
+
private droppedSlowClients = 0
|
|
109
|
+
private lastBroadcastAt: number | null = null
|
|
110
|
+
private latestOffset = 0
|
|
111
|
+
|
|
112
|
+
constructor(deps: Deps = {}) {
|
|
113
|
+
this.logger = deps.logger ?? defaultLogger
|
|
114
|
+
this.maxBufferedBytes = deps.maxBufferedBytes ?? DEFAULT_MAX_BUFFERED_BYTES
|
|
115
|
+
this.now = deps.now ?? (() => Date.now())
|
|
116
|
+
this.replayBufferSize = Math.max(0, deps.replayBufferSize ?? DEFAULT_REPLAY_BUFFER_SIZE)
|
|
117
|
+
this.replayBufferMaxBytes = Math.max(
|
|
118
|
+
0,
|
|
119
|
+
deps.replayBufferMaxBytes ?? DEFAULT_REPLAY_BUFFER_MAX_BYTES,
|
|
120
|
+
)
|
|
121
|
+
this.persistentReplay = deps.persistentReplay ?? null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
getMetrics(): BroadcasterMetrics {
|
|
125
|
+
return {
|
|
126
|
+
clients: this.clients.size,
|
|
127
|
+
subscribers: this.subscribers.size,
|
|
128
|
+
eventsBroadcast: this.eventsBroadcast,
|
|
129
|
+
droppedSlowClients: this.droppedSlowClients,
|
|
130
|
+
lastBroadcastAt: this.lastBroadcastAt ? new Date(this.lastBroadcastAt).toISOString() : null,
|
|
131
|
+
latestOffset: this.latestOffset,
|
|
132
|
+
oldestReplayableOffset: this.replayBuffer[0]?.offset ?? null,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Returns events with offset > since, filtered by the connector subscription
|
|
138
|
+
* rules of `data`. Used at WS upgrade time when the client passes `?since=<offset>`.
|
|
139
|
+
*
|
|
140
|
+
* Two-tier lookup:
|
|
141
|
+
* 1. The in-memory ring buffer (covers short reconnects, last `replayBufferSize` events).
|
|
142
|
+
* 2. If `since` predates the oldest in-memory entry and a persistent replay source
|
|
143
|
+
* is wired in (SQLite), the gap is filled from disk. This covers reconnects across
|
|
144
|
+
* daemon restarts where the in-memory buffer was lost.
|
|
145
|
+
*
|
|
146
|
+
* Result is sorted ascending by offset and de-duplicated against the in-memory buffer.
|
|
147
|
+
*/
|
|
148
|
+
replaySince(since: number, data: ClientData): ReplayableEvent[] {
|
|
149
|
+
const oldestInMemory = this.replayBuffer[0]?.offset
|
|
150
|
+
const needFallback =
|
|
151
|
+
this.persistentReplay && (oldestInMemory === undefined || since < oldestInMemory - 1)
|
|
152
|
+
const fromMemory = this.replayBuffer.filter(
|
|
153
|
+
(event) => event.offset > since && this.matchesClient(event, data),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if (!needFallback) return fromMemory
|
|
157
|
+
|
|
158
|
+
const persisted = this.persistentReplay
|
|
159
|
+
? this.persistentReplay.loadSince(since).filter((event) => this.matchesClient(event, data))
|
|
160
|
+
: []
|
|
161
|
+
const cutoff = oldestInMemory ?? Number.POSITIVE_INFINITY
|
|
162
|
+
const beforeMemory = persisted.filter((event) => event.offset < cutoff)
|
|
163
|
+
|
|
164
|
+
return [...beforeMemory, ...fromMemory]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private matchesClient(event: BroadcastEvent, data: ClientData): boolean {
|
|
168
|
+
if (data.tapAll) return true
|
|
169
|
+
|
|
170
|
+
const channelId = event.meta?.channelId
|
|
171
|
+
|
|
172
|
+
if (channelId && channelId !== data.channel) return false
|
|
173
|
+
|
|
174
|
+
const connector = event.meta?.connector
|
|
175
|
+
|
|
176
|
+
if (!connector) return true
|
|
177
|
+
|
|
178
|
+
return data.connectors.includes(connector)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Returns the list of WS clients that should receive `event`. Tap=all clients always
|
|
183
|
+
* receive (passive observation). For each per-channel group:
|
|
184
|
+
* - fanout → every matching client receives
|
|
185
|
+
* - exclusive → exactly one client receives, picked round-robin per channel
|
|
186
|
+
*/
|
|
187
|
+
private pickRecipients(event: BroadcastEvent): ServerWebSocket<unknown>[] {
|
|
188
|
+
const exclusiveByChannel = new Map<string, ServerWebSocket<unknown>[]>()
|
|
189
|
+
const recipients: ServerWebSocket<unknown>[] = []
|
|
190
|
+
|
|
191
|
+
for (const [ws, data] of this.clients) {
|
|
192
|
+
if (!this.matchesClient(event, data)) continue
|
|
193
|
+
|
|
194
|
+
if (data.tapAll) {
|
|
195
|
+
recipients.push(ws)
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (data.delivery === "exclusive") {
|
|
200
|
+
const list = exclusiveByChannel.get(data.channel) ?? []
|
|
201
|
+
|
|
202
|
+
list.push(ws)
|
|
203
|
+
exclusiveByChannel.set(data.channel, list)
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
recipients.push(ws)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const [channel, candidates] of exclusiveByChannel) {
|
|
211
|
+
if (candidates.length === 0) continue
|
|
212
|
+
|
|
213
|
+
const cursor = this.exclusiveCursor.get(channel) ?? 0
|
|
214
|
+
const picked = candidates[cursor % candidates.length]
|
|
215
|
+
|
|
216
|
+
if (picked) recipients.push(picked)
|
|
217
|
+
|
|
218
|
+
this.exclusiveCursor.set(channel, cursor + 1)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return recipients
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
addClient(ws: ServerWebSocket<unknown>, data: ClientData): void {
|
|
225
|
+
this.clients.set(ws, data)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
removeClient(ws: ServerWebSocket<unknown>): void {
|
|
229
|
+
this.clients.delete(ws)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
getClientCount(): number {
|
|
233
|
+
return this.clients.size
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
listChannels(): { channel: string; connectors: string[] }[] {
|
|
237
|
+
return [...this.clients.values()].map((d) => ({ ...d }))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
subscribe(handler: BroadcastSubscriber): () => void {
|
|
241
|
+
this.subscribers.add(handler)
|
|
242
|
+
|
|
243
|
+
return () => {
|
|
244
|
+
this.subscribers.delete(handler)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
broadcast(content: string, meta?: Record<string, string>): ReplayableEvent {
|
|
249
|
+
this.latestOffset += 1
|
|
250
|
+
const event: ReplayableEvent = { content, meta, offset: this.latestOffset }
|
|
251
|
+
const payload = JSON.stringify(event)
|
|
252
|
+
const connector = meta?.connector
|
|
253
|
+
|
|
254
|
+
this.eventsBroadcast += 1
|
|
255
|
+
this.lastBroadcastAt = this.now()
|
|
256
|
+
|
|
257
|
+
if (this.replayBufferSize > 0) {
|
|
258
|
+
const eventBytes = byteLengthOf(event)
|
|
259
|
+
|
|
260
|
+
this.replayBuffer.push(event)
|
|
261
|
+
this.replayBufferBytes += eventBytes
|
|
262
|
+
|
|
263
|
+
while (
|
|
264
|
+
(this.replayBuffer.length > this.replayBufferSize ||
|
|
265
|
+
this.replayBufferBytes > this.replayBufferMaxBytes) &&
|
|
266
|
+
this.replayBuffer.length > 0
|
|
267
|
+
) {
|
|
268
|
+
const dropped = this.replayBuffer.shift()
|
|
269
|
+
|
|
270
|
+
if (dropped) this.replayBufferBytes -= byteLengthOf(dropped)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const recipients = this.pickRecipients(event)
|
|
275
|
+
|
|
276
|
+
for (const ws of recipients) {
|
|
277
|
+
const buffered = ws.getBufferedAmount()
|
|
278
|
+
|
|
279
|
+
if (buffered > this.maxBufferedBytes) {
|
|
280
|
+
const data = this.clients.get(ws)
|
|
281
|
+
|
|
282
|
+
this.logger.warn("dropping slow WS client (backpressure)", {
|
|
283
|
+
channel: data?.channel,
|
|
284
|
+
buffered,
|
|
285
|
+
max: this.maxBufferedBytes,
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
ws.close(1009, "backpressure")
|
|
290
|
+
} catch {
|
|
291
|
+
// ignore
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
this.clients.delete(ws)
|
|
295
|
+
this.droppedSlowClients += 1
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
ws.send(payload)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for (const handler of this.subscribers) {
|
|
303
|
+
try {
|
|
304
|
+
handler(event)
|
|
305
|
+
} catch (error) {
|
|
306
|
+
this.logger.error("broadcast subscriber threw", {
|
|
307
|
+
error: error instanceof Error ? error.message : String(error),
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return event
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Forward-seed the offset counter (used at startup from the persisted event store). */
|
|
316
|
+
seedLatestOffset(offset: number): void {
|
|
317
|
+
if (offset > this.latestOffset) this.latestOffset = offset
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { FUNNEL_DIR } from "@/engine/settings/settings-store"
|
|
5
|
+
import { Funnel } from "@/funnel"
|
|
6
|
+
import { NodeFunnelLogger } from "@/engine/logger/node-logger"
|
|
7
|
+
|
|
8
|
+
const PORT = Number(process.env.FUNNEL_PORT) || 9742
|
|
9
|
+
const PID_FILE = join(FUNNEL_DIR, "gateway.pid")
|
|
10
|
+
const LOG_DIR = "/tmp/funnel/events"
|
|
11
|
+
|
|
12
|
+
const logger = new NodeFunnelLogger()
|
|
13
|
+
|
|
14
|
+
mkdirSync(FUNNEL_DIR, { recursive: true })
|
|
15
|
+
|
|
16
|
+
if (existsSync(PID_FILE)) {
|
|
17
|
+
const existing = Number(readFileSync(PID_FILE, "utf-8").trim())
|
|
18
|
+
|
|
19
|
+
if (existing > 0) {
|
|
20
|
+
const check = Bun.spawnSync(["ps", "-p", String(existing), "-o", "state="], {
|
|
21
|
+
stdout: "pipe",
|
|
22
|
+
stderr: "pipe",
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
if (check.exitCode === 0 && check.stdout.toString().trim()) {
|
|
26
|
+
logger.error("funnel gateway already running", { pid: existing })
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
writeFileSync(PID_FILE, String(process.pid))
|
|
33
|
+
|
|
34
|
+
process.on("exit", () => {
|
|
35
|
+
try {
|
|
36
|
+
unlinkSync(PID_FILE)
|
|
37
|
+
} catch {
|
|
38
|
+
// ignore
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
process.on("SIGINT", () => process.exit(130))
|
|
42
|
+
process.on("SIGTERM", () => process.exit(143))
|
|
43
|
+
|
|
44
|
+
const funnel = new Funnel({ logger })
|
|
45
|
+
const server = funnel.gatewayServer({ port: PORT, logDir: LOG_DIR })
|
|
46
|
+
|
|
47
|
+
await server.start()
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import type { ReplayableEvent } from "@/gateway/broadcaster"
|
|
3
|
+
import { LeucoLoggerSqliteSink } from "@/logger/leuco-logger-sqlite-sink"
|
|
4
|
+
|
|
5
|
+
const MAX_CONTENT_CHARS = 2000
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Replayable event payload persisted by the gateway. Domain events the
|
|
9
|
+
* broadcaster emits to WS clients land here so reconnects across daemon
|
|
10
|
+
* restarts can be served from disk. System events (gateway start, channel
|
|
11
|
+
* connected, etc.) are routed to `FunnelLogger` instead — they never go
|
|
12
|
+
* through this store, which keeps the seq space clean for replay.
|
|
13
|
+
*/
|
|
14
|
+
export const funnelEventSchema = z.object({
|
|
15
|
+
type: z.string(),
|
|
16
|
+
content: z.string(),
|
|
17
|
+
channel_id: z.string().nullable(),
|
|
18
|
+
connector_id: z.string().nullable(),
|
|
19
|
+
meta: z.record(z.string(), z.string()).nullable(),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export type FunnelEvent = z.infer<typeof funnelEventSchema>
|
|
23
|
+
|
|
24
|
+
type Props = {
|
|
25
|
+
/** SQLite database file path. Created on first write. ":memory:" for tests. */
|
|
26
|
+
path: string
|
|
27
|
+
/** Override for tests. Defaults to `Date.now`. */
|
|
28
|
+
now?: () => number
|
|
29
|
+
/** Optional row cap. Pruned on every insert. */
|
|
30
|
+
maxRows?: number
|
|
31
|
+
/** Optional age cap in ms. Pruned on every insert. */
|
|
32
|
+
maxAgeMs?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* SQLite-backed event store. One indexed table holds every broadcaster
|
|
37
|
+
* event with `channel_id` and `connector_id` as dedicated columns, so
|
|
38
|
+
* per-channel and per-connector replay is an indexed range scan.
|
|
39
|
+
*
|
|
40
|
+
* Concurrency: `seq` is `INTEGER PRIMARY KEY`, so SQLite assigns it
|
|
41
|
+
* atomically. The broadcaster owns its own offset counter at runtime
|
|
42
|
+
* (seeded from `findMaxOffset()` at startup); each broadcaster event
|
|
43
|
+
* flows in here via `record()` with that pre-assigned offset, which the
|
|
44
|
+
* sink stores via `write()` — PK uniqueness catches double-emit bugs.
|
|
45
|
+
*
|
|
46
|
+
* System events (gateway lifecycle, channel connect/disconnect, etc.) do
|
|
47
|
+
* NOT go through this store. They are diagnostic only and live in
|
|
48
|
+
* `FunnelLogger`'s file so the seq space here stays exclusive to
|
|
49
|
+
* broadcaster traffic. This is what makes the broadcaster's seq seeding
|
|
50
|
+
* (`getMaxSeq()` at startup) correct without per-event coordination.
|
|
51
|
+
*/
|
|
52
|
+
export class FunnelEventStore {
|
|
53
|
+
private readonly sink: LeucoLoggerSqliteSink<FunnelEvent, ["channel_id", "connector_id"]>
|
|
54
|
+
private readonly now: () => number
|
|
55
|
+
|
|
56
|
+
constructor(props: Props) {
|
|
57
|
+
this.now = props.now ?? (() => Date.now())
|
|
58
|
+
this.sink = new LeucoLoggerSqliteSink<FunnelEvent, ["channel_id", "connector_id"]>({
|
|
59
|
+
path: props.path,
|
|
60
|
+
indexes: ["channel_id", "connector_id"],
|
|
61
|
+
extractIndexes: (event) => ({
|
|
62
|
+
channel_id: event.channel_id,
|
|
63
|
+
connector_id: event.connector_id,
|
|
64
|
+
}),
|
|
65
|
+
now: this.now,
|
|
66
|
+
...(props.maxRows !== undefined ? { maxRows: props.maxRows } : {}),
|
|
67
|
+
...(props.maxAgeMs !== undefined ? { maxAgeMs: props.maxAgeMs } : {}),
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Persist a broadcaster-driven event with its assigned offset. Caller
|
|
73
|
+
* (the gateway-server) supplies the offset from `broadcaster.broadcast()`
|
|
74
|
+
* so this store and the broadcaster's in-memory ring stay aligned.
|
|
75
|
+
*/
|
|
76
|
+
record(props: {
|
|
77
|
+
content: string
|
|
78
|
+
channelId: string | null
|
|
79
|
+
connectorId: string | null
|
|
80
|
+
meta: Record<string, string> | null
|
|
81
|
+
offset: number
|
|
82
|
+
}): void {
|
|
83
|
+
const event: FunnelEvent = {
|
|
84
|
+
type: props.meta?.event_type ?? "unknown",
|
|
85
|
+
content: truncate(props.content),
|
|
86
|
+
channel_id: props.channelId,
|
|
87
|
+
connector_id: props.connectorId,
|
|
88
|
+
meta: props.meta,
|
|
89
|
+
}
|
|
90
|
+
this.sink.write({ seq: props.offset, ts: this.now(), event })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Returns events with offset > since. Filtering by channel/connector is
|
|
95
|
+
* the broadcaster's responsibility (it knows the client's subscription),
|
|
96
|
+
* so this returns the full slice and lets the caller filter.
|
|
97
|
+
*/
|
|
98
|
+
loadSince(since: number): ReplayableEvent[] {
|
|
99
|
+
const records = this.sink.getRecords({ sinceSeq: since })
|
|
100
|
+
const out: ReplayableEvent[] = []
|
|
101
|
+
for (const record of records) {
|
|
102
|
+
out.push({
|
|
103
|
+
content: record.event.content,
|
|
104
|
+
meta: record.event.meta ?? undefined,
|
|
105
|
+
offset: record.seq,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
return out
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Returns events for one channel (and optionally one connector). Used
|
|
113
|
+
* by the gateway logs CLI for scoped queries. Channel/connector filters
|
|
114
|
+
* are indexed columns, so this is an indexed range scan.
|
|
115
|
+
*/
|
|
116
|
+
loadForChannel(props: {
|
|
117
|
+
channelId: string
|
|
118
|
+
connectorId?: string
|
|
119
|
+
sinceSeq?: number
|
|
120
|
+
limit?: number
|
|
121
|
+
}): ReplayableEvent[] {
|
|
122
|
+
const where: { channel_id: string; connector_id?: string } = {
|
|
123
|
+
channel_id: props.channelId,
|
|
124
|
+
}
|
|
125
|
+
if (props.connectorId !== undefined) where.connector_id = props.connectorId
|
|
126
|
+
|
|
127
|
+
const records = this.sink.getRecords({
|
|
128
|
+
where,
|
|
129
|
+
...(props.sinceSeq !== undefined ? { sinceSeq: props.sinceSeq } : {}),
|
|
130
|
+
...(props.limit !== undefined ? { limit: props.limit } : {}),
|
|
131
|
+
})
|
|
132
|
+
const out: ReplayableEvent[] = []
|
|
133
|
+
for (const record of records) {
|
|
134
|
+
out.push({
|
|
135
|
+
content: record.event.content,
|
|
136
|
+
meta: record.event.meta ?? undefined,
|
|
137
|
+
offset: record.seq,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
return out
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
findMaxOffset(): number {
|
|
144
|
+
return this.sink.getMaxSeq()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
close(): void {
|
|
148
|
+
this.sink.close()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function truncate(content: string): string {
|
|
153
|
+
if (content.length <= MAX_CONTENT_CHARS) return content
|
|
154
|
+
return `${content.slice(0, MAX_CONTENT_CHARS)}...`
|
|
155
|
+
}
|