Core decision
+One normalized contract
++ Adapters expose an incoming stream and a discrete send effect. The gateway adds + authorization, routing, and a derived outgoing-stream consumer. +
+From b286d3d652e21918f5be873631b6b61b0ee488f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Schie=C3=9Fl?= <150372753+johannesschiessl@users.noreply.github.com> Date: Sun, 19 Jul 2026 22:55:59 +0200 Subject: [PATCH 1/2] Add Effect-native chat gateway --- .plans/01-chat-gateway.html | 527 ++++++++++++++++++ apps/server/src/gateway/Adapter.ts | 20 + apps/server/src/gateway/Gateway.test.ts | 153 +++++ apps/server/src/gateway/Gateway.ts | 93 ++++ apps/server/src/gateway/GatewayConfig.test.ts | 54 ++ apps/server/src/gateway/GatewayConfig.ts | 121 ++++ apps/server/src/gateway/GatewayFiles.test.ts | 44 ++ apps/server/src/gateway/GatewayFiles.ts | 152 +++++ apps/server/src/gateway/Models.ts | 149 +++++ apps/server/src/gateway/Paths.ts | 7 + .../adapters/telegram/TelegramAdapter.test.ts | 262 +++++++++ .../adapters/telegram/TelegramAdapter.ts | 352 ++++++++++++ .../adapters/telegram/TelegramApi.test.ts | 81 +++ .../gateway/adapters/telegram/TelegramApi.ts | 288 ++++++++++ .../adapters/telegram/TelegramModels.ts | 103 ++++ .../src/gateway/adapters/telegram/index.ts | 3 + apps/server/src/gateway/index.ts | 48 ++ apps/server/src/index.ts | 1 + 18 files changed, 2458 insertions(+) create mode 100644 .plans/01-chat-gateway.html create mode 100644 apps/server/src/gateway/Adapter.ts create mode 100644 apps/server/src/gateway/Gateway.test.ts create mode 100644 apps/server/src/gateway/Gateway.ts create mode 100644 apps/server/src/gateway/GatewayConfig.test.ts create mode 100644 apps/server/src/gateway/GatewayConfig.ts create mode 100644 apps/server/src/gateway/GatewayFiles.test.ts create mode 100644 apps/server/src/gateway/GatewayFiles.ts create mode 100644 apps/server/src/gateway/Models.ts create mode 100644 apps/server/src/gateway/Paths.ts create mode 100644 apps/server/src/gateway/adapters/telegram/TelegramAdapter.test.ts create mode 100644 apps/server/src/gateway/adapters/telegram/TelegramAdapter.ts create mode 100644 apps/server/src/gateway/adapters/telegram/TelegramApi.test.ts create mode 100644 apps/server/src/gateway/adapters/telegram/TelegramApi.ts create mode 100644 apps/server/src/gateway/adapters/telegram/TelegramModels.ts create mode 100644 apps/server/src/gateway/adapters/telegram/index.ts create mode 100644 apps/server/src/gateway/index.ts diff --git a/.plans/01-chat-gateway.html b/.plans/01-chat-gateway.html new file mode 100644 index 0000000..b5700b5 --- /dev/null +++ b/.plans/01-chat-gateway.html @@ -0,0 +1,527 @@ + + +
+ + ++ A small, reusable boundary for adapter-qualified channels, finished messages, + local-file attachments, and resilient inbound streams—starting with a complete + Telegram Bot API polling adapter. +
+Core decision
++ Adapters expose an incoming stream and a discrete send effect. The gateway adds + authorization, routing, and a derived outgoing-stream consumer. +
+Access model
+
+ telegram:<chat-id> is the
+ adapter-independent authorization key. Optional topic/thread identity stays separate.
+
Files
+
+ Outbound attachments reference local paths. Inbound media is downloaded before emission
+ and points into ~/.compass/files.
+
01 / Boundary
++ Gateway.incoming + : Stream<IncomingMessage, GatewayError> +
++ Gateway.send(message) + : Effect<SentMessage, GatewayError> +
++ Gateway.sendAll(outgoing, options?) + : Stream<SentMessage, GatewayError | E, + R> +
+
+ send is the reliable primitive: one
+ finished message produces one delivery result or one typed failure.
+ sendAll is the stream-oriented
+ convenience requested by callers and uses explicit, bounded concurrency. It does not
+ silently detach fibers or swallow delivery errors.
+
02 / Architecture
+Application
+Incoming consumer
+Outgoing message stream
+Gateway
+Authorize + route
+Merge adapter streams
+Adapter
+Telegram polling
+Bot API + file transfer
+GatewayConfig
+Schema-decodes gateway.json
+GatewayFiles
+Safe durable file writes
+HttpClient
+Injected Bot API transport
++ The adapter contract contains no Telegram types. Each adapter supplies a stable name, + an incoming stream, and a send implementation. A scoped Telegram layer owns the + long-poll lifecycle, so closing the parent scope interrupts in-flight HTTP and retry + delays cleanly. +
+03 / Configuration
+{
+ "version": 1,
+ "allowedChannels": [
+ "telegram:123456789",
+ "telegram:-100987654321"
+ ],
+ "adapters": {
+ "telegram": {
+ "pollingTimeoutSeconds": 30,
+ "pollingLimit": 100,
+ "maxInboundFileBytes": 52428800,
+ "deleteWebhook": true,
+ "dropPendingUpdates": false
+ }
+ }
+}
+
+ The default path is resolved portably from
+ HOME or
+ USERPROFILE. Missing or invalid
+ configuration fails layer construction with a typed configuration error.
+
+ The bot token remains outside the JSON file and is read through Effect Config from
+ TELEGRAM_BOT_TOKEN. An optional API
+ base URL may be injected for deterministic tests.
+
+ Authorization is exact-match and deny-by-default. Topics inherit their Telegram + chat’s qualified channel authorization; the optional thread identifier only controls + reply placement. +
+04 / Telegram
+getUpdates with the current
+ offset.
+ getFile and enforce its size
+ limit.
+ ~/.compass/files without buffering
+ the full download.
+ sendMessage,
+ sendPhoto,
+ sendAudio,
+ sendVoice, or
+ sendDocument.
+ Schedule and structured logs.
+ Successful polls reset backoff. Telegram API envelopes and update payloads are
+ schema-decoded; malformed individual updates are logged and skipped without killing
+ the adapter stream. Authentication/configuration failures remain terminal.
+ 05 / Files
+apps/server/src/gateway/
+├── Gateway.ts # service, routing, authorization, stream helpers
+├── GatewayConfig.ts # gateway.json schema + default filesystem layer
+├── GatewayFiles.ts # inbound persistence + outbound path reads
+├── Models.ts # branded IDs, messages, attachments, errors
+├── Adapter.ts # platform-neutral adapter contract
+├── index.ts # public exports
+├── Gateway.test.ts
+├── GatewayConfig.test.ts
+├── GatewayFiles.test.ts
+└── adapters/
+ └── telegram/
+ ├── TelegramAdapter.ts # Effect service/layer + normalization + sending
+ ├── TelegramApi.ts # typed Bot API HttpClient boundary
+ ├── TelegramModels.ts # only the Bot API schemas needed here
+ ├── TelegramAdapter.test.ts
+ └── index.ts
+ 06 / Execution
++ Define Schema-based messages, qualified channel IDs, attachments, delivery + receipts, adapter contract, and tagged error families. +
++ Decode gateway.json, resolve Compass paths, create the files directory, sanitize + filenames, and write downloads atomically. +
++ Use Effect HttpClient and Schema for getMe, deleteWebhook, getUpdates, getFile, + downloads, JSON requests, and multipart uploads. +
++ Create a lazy incoming stream, track offset, normalize supported messages, persist + attachments, and make interruption immediate and leak-free. +
++ Merge adapter streams, enforce allowlisting in both directions, route sends, and + expose bounded outgoing-stream processing. +
++ Exercise config, authorization, ordering, retry, interruption, media + download/upload, then run repository readiness and typecheck gates. +
+07 / Validation
+| Area | +Proof | +Expected outcome | +
|---|---|---|
| Authorization | +Allowed and denied channel tests | +Exact-match, deny-by-default | +
| Polling | +Scripted getUpdates responses | ++ Offsets advance; duplicates are avoided + | +
| Resilience | +Transient failures + test clock | +Capped backoff and recovery | +
| Shutdown | +Scoped stream interruption | +Active long poll is cancelled | +
| Inbound files | +Photo, audio, voice, document fixtures | +Safe files exist before emission | +
| Outbound files | +Multipart request inspection | +Correct Bot API method and caption | +
| Repository | +
+ vp run ready vp run typecheck + |
+ Both commands pass | +
+ 08 / Deliberate limits +
+apps/server/src/gateway/
+ >packages/contracts/src/
+├── gateway.ts # schemas, domain types, errors, adapter contract
+└── gateway.test.ts
+
+apps/server/src/gateway/
├── Gateway.ts # service, routing, authorization, stream helpers
├── GatewayConfig.ts # gateway.json schema + default filesystem layer
├── GatewayFiles.ts # inbound persistence + outbound path reads
-├── Models.ts # branded IDs, messages, attachments, errors
-├── Adapter.ts # platform-neutral adapter contract
├── index.ts # public exports
├── Gateway.test.ts
├── GatewayConfig.test.ts
@@ -519,7 +521,7 @@ Not in this slice
diff --git a/apps/server/src/gateway/Adapter.ts b/apps/server/src/gateway/Adapter.ts
deleted file mode 100644
index b2e5dfd..0000000
--- a/apps/server/src/gateway/Adapter.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { Effect, Stream } from "effect";
-import type {
- AdapterName,
- ChannelId,
- GatewayAdapterError,
- IncomingMessage,
- OutgoingMessage,
- SentMessage,
-} from "./Models.ts";
-
-export interface IncomingAdapterEvent {
- readonly channel: ChannelId;
- readonly materialize: Effect.Effect;
-}
-
-export interface GatewayAdapter {
- readonly name: AdapterName;
- readonly incoming: Stream.Stream;
- readonly send: (message: OutgoingMessage) => Effect.Effect;
-}
diff --git a/apps/server/src/gateway/Gateway.test.ts b/apps/server/src/gateway/Gateway.test.ts
index 0709b3b..61d1c31 100644
--- a/apps/server/src/gateway/Gateway.test.ts
+++ b/apps/server/src/gateway/Gateway.test.ts
@@ -1,18 +1,18 @@
-import { describe, expect, it } from "@effect/vitest";
-import { Effect, Stream } from "effect";
-import type { GatewayAdapter } from "./Adapter.ts";
-import { makeGateway } from "./Gateway.ts";
-import type { GatewaySettings } from "./GatewayConfig.ts";
import {
AdapterName,
ChannelId,
+ type GatewayAdapter,
GatewayMessageId,
+ GatewayTimestampMillis,
IncomingMessage,
MessageSender,
OutgoingMessage,
SentMessage,
- TimestampMillis,
-} from "./Models.ts";
+} from "@compass/contracts";
+import { describe, expect, it } from "@effect/vitest";
+import { Effect, Stream } from "effect";
+import { makeGateway } from "./Gateway.ts";
+import type { GatewaySettings } from "./GatewayConfig.ts";
const settings = (channels: ReadonlyArray): GatewaySettings => ({
allowedChannels: new Set(channels.map((channel) => ChannelId.make(channel))),
@@ -39,7 +39,7 @@ const incomingMessage = (channel: string, text: string) =>
}),
text,
files: [],
- receivedAt: TimestampMillis.make(1),
+ receivedAt: GatewayTimestampMillis.make(1),
});
describe("Gateway", () => {
diff --git a/apps/server/src/gateway/Gateway.ts b/apps/server/src/gateway/Gateway.ts
index 286f3c0..7cf8e07 100644
--- a/apps/server/src/gateway/Gateway.ts
+++ b/apps/server/src/gateway/Gateway.ts
@@ -1,8 +1,5 @@
-import { Context, Effect, Layer, Stream } from "effect";
-import type { GatewayAdapter } from "./Adapter.ts";
-import type { GatewaySettings } from "./GatewayConfig.ts";
-import { GatewayConfig } from "./GatewayConfig.ts";
import {
+ type GatewayAdapter,
type GatewayError,
GatewayAdapterNotFoundError,
GatewayAuthorizationError,
@@ -12,7 +9,10 @@ import {
type OutgoingMessage,
type SentMessage,
parseChannelId,
-} from "./Models.ts";
+} from "@compass/contracts";
+import { Context, Effect, Layer, Stream } from "effect";
+import type { GatewaySettings } from "./GatewayConfig.ts";
+import { GatewayConfig } from "./GatewayConfig.ts";
export interface SendAllOptions {
readonly concurrency?: number | "unbounded" | undefined;
diff --git a/apps/server/src/gateway/GatewayConfig.ts b/apps/server/src/gateway/GatewayConfig.ts
index f07e69d..4ce96dc 100644
--- a/apps/server/src/gateway/GatewayConfig.ts
+++ b/apps/server/src/gateway/GatewayConfig.ts
@@ -1,5 +1,5 @@
+import { ChannelId, GatewayConfigurationError } from "@compass/contracts";
import { Context, Effect, FileSystem, Layer, Path, Schema } from "effect";
-import { ChannelId, GatewayConfigurationError } from "./Models.ts";
import { compassDirectory } from "./Paths.ts";
const IntegerBetween = (minimum: number, maximum: number) =>
diff --git a/apps/server/src/gateway/GatewayFiles.ts b/apps/server/src/gateway/GatewayFiles.ts
index e736e77..0a6b33a 100644
--- a/apps/server/src/gateway/GatewayFiles.ts
+++ b/apps/server/src/gateway/GatewayFiles.ts
@@ -1,5 +1,5 @@
+import { GatewayFile, GatewayFileError, type GatewayFileKind } from "@compass/contracts";
import { Context, Crypto, Effect, FileSystem, Layer, Path, Schema, Stream } from "effect";
-import { GatewayFile, GatewayFileError, type GatewayFileKind } from "./Models.ts";
import { compassDirectory } from "./Paths.ts";
const UNSAFE_FILENAME_CHARACTERS = /[^a-zA-Z0-9._-]+/g;
diff --git a/apps/server/src/gateway/adapters/telegram/TelegramAdapter.test.ts b/apps/server/src/gateway/adapters/telegram/TelegramAdapter.test.ts
index 31edcd9..dcacfcd 100644
--- a/apps/server/src/gateway/adapters/telegram/TelegramAdapter.test.ts
+++ b/apps/server/src/gateway/adapters/telegram/TelegramAdapter.test.ts
@@ -1,8 +1,3 @@
-import { describe, expect, it } from "@effect/vitest";
-import { Context, Effect, Latch, Layer, Stream } from "effect";
-import { makeGateway } from "../../Gateway.ts";
-import { GatewayConfig, type GatewaySettings } from "../../GatewayConfig.ts";
-import { GatewayFiles, type GatewayFilesService } from "../../GatewayFiles.ts";
import {
ChannelId,
GatewayFile,
@@ -10,7 +5,12 @@ import {
GatewayFileError,
GatewayMessageId,
OutgoingMessage,
-} from "../../Models.ts";
+} from "@compass/contracts";
+import { describe, expect, it } from "@effect/vitest";
+import { Context, Effect, Latch, Layer, Stream } from "effect";
+import { makeGateway } from "../../Gateway.ts";
+import { GatewayConfig, type GatewaySettings } from "../../GatewayConfig.ts";
+import { GatewayFiles, type GatewayFilesService } from "../../GatewayFiles.ts";
import { TelegramAdapter } from "./TelegramAdapter.ts";
import { TelegramApi, type TelegramApiService } from "./TelegramApi.ts";
diff --git a/apps/server/src/gateway/adapters/telegram/TelegramAdapter.ts b/apps/server/src/gateway/adapters/telegram/TelegramAdapter.ts
index 9790da2..2a056f7 100644
--- a/apps/server/src/gateway/adapters/telegram/TelegramAdapter.ts
+++ b/apps/server/src/gateway/adapters/telegram/TelegramAdapter.ts
@@ -1,23 +1,24 @@
-import { Context, Effect, Layer, Queue, Schedule, Schema, Stream } from "effect";
-import type { GatewayAdapter, IncomingAdapterEvent } from "../../Adapter.ts";
-import { GatewayConfig } from "../../GatewayConfig.ts";
-import { GatewayFiles } from "../../GatewayFiles.ts";
import {
AdapterName,
ChannelId,
+ type GatewayAdapter,
+ type GatewayFile,
GatewayFileError,
+ type GatewayFileKind,
GatewayMessageId,
GatewayProtocolError,
+ GatewayTimestampMillis,
+ type IncomingAdapterEvent,
IncomingMessage,
MessageSender,
+ type OutgoingMessage,
SentMessage,
ThreadId,
- TimestampMillis,
parseChannelId,
- type GatewayFile,
- type GatewayFileKind,
- type OutgoingMessage,
-} from "../../Models.ts";
+} from "@compass/contracts";
+import { Context, Effect, Layer, Queue, Schedule, Schema, Stream } from "effect";
+import { GatewayConfig } from "../../GatewayConfig.ts";
+import { GatewayFiles } from "../../GatewayFiles.ts";
import { TelegramApi, type TelegramMediaRequest } from "./TelegramApi.ts";
import {
type TelegramMessage,
@@ -249,7 +250,7 @@ export const makeTelegramAdapter = Effect.gen(function* () {
? {}
: { text: message.text ?? message.caption }),
files: saved,
- receivedAt: TimestampMillis.make(Math.max(0, Math.trunc(message.date * 1_000))),
+ receivedAt: GatewayTimestampMillis.make(Math.max(0, Math.trunc(message.date * 1_000))),
});
});
diff --git a/apps/server/src/gateway/adapters/telegram/TelegramApi.ts b/apps/server/src/gateway/adapters/telegram/TelegramApi.ts
index d801d5a..c022e2f 100644
--- a/apps/server/src/gateway/adapters/telegram/TelegramApi.ts
+++ b/apps/server/src/gateway/adapters/telegram/TelegramApi.ts
@@ -1,11 +1,11 @@
-import { Config, Context, Effect, Layer, Redacted, Schema, Stream } from "effect";
-import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
import {
AdapterName,
GatewayProtocolError,
GatewayTransportError,
type GatewayAdapterError,
-} from "../../Models.ts";
+} from "@compass/contracts";
+import { Config, Context, Effect, Layer, Redacted, Schema, Stream } from "effect";
+import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
import {
type TelegramFile,
TelegramFile as TelegramFileSchema,
diff --git a/apps/server/src/gateway/index.ts b/apps/server/src/gateway/index.ts
index c66e208..69a6bfc 100644
--- a/apps/server/src/gateway/index.ts
+++ b/apps/server/src/gateway/index.ts
@@ -7,7 +7,32 @@ import { layerDefault as gatewayFilesLayerDefault } from "./GatewayFiles.ts";
import { TelegramAdapter } from "./adapters/telegram/TelegramAdapter.ts";
import { TelegramApi } from "./adapters/telegram/TelegramApi.ts";
-export * from "./Adapter.ts";
+export {
+ AdapterName,
+ ChannelId,
+ type GatewayAdapter,
+ type GatewayAdapterError,
+ GatewayAdapterNotFoundError,
+ GatewayAuthorizationError,
+ GatewayConfigurationError,
+ type GatewayError,
+ GatewayFile,
+ GatewayFileError,
+ GatewayFileKind,
+ GatewayInvalidMessageError,
+ GatewayMessageId,
+ GatewayProtocolError,
+ GatewayTimestampMillis,
+ GatewayTransportError,
+ type IncomingAdapterEvent,
+ IncomingMessage,
+ MessageSender,
+ OutgoingMessage,
+ type ParsedChannelId,
+ SentMessage,
+ ThreadId,
+ parseChannelId,
+} from "@compass/contracts";
export * from "./Gateway.ts";
export {
GatewayConfig,
@@ -26,7 +51,6 @@ export {
layerAt as gatewayFilesLayerAt,
layerDefault as gatewayFilesLayerDefault,
} from "./GatewayFiles.ts";
-export * from "./Models.ts";
export * as Telegram from "./adapters/telegram/index.ts";
export const telegramGatewayLayer = Layer.effect(
diff --git a/packages/contracts/src/gateway.test.ts b/packages/contracts/src/gateway.test.ts
new file mode 100644
index 0000000..2b8ce2c
--- /dev/null
+++ b/packages/contracts/src/gateway.test.ts
@@ -0,0 +1,46 @@
+import { describe, expect, it } from "@effect/vitest";
+import { Effect, Option, Schema } from "effect";
+import {
+ ChannelId,
+ GatewayFile,
+ GatewayTimestampMillis,
+ IncomingMessage,
+ GatewayMessageId,
+ MessageSender,
+ parseChannelId,
+} from "./gateway.ts";
+
+describe("gateway contracts", () => {
+ it.effect("decodes adapter-qualified channels and gateway timestamps", () =>
+ Effect.gen(function* () {
+ const channel = yield* Schema.decodeUnknownEffect(ChannelId)("telegram:123");
+ const receivedAt = yield* Schema.decodeUnknownEffect(GatewayTimestampMillis)(1_000);
+
+ expect(parseChannelId(channel)).toEqual({ adapter: "telegram", nativeChannel: "123" });
+ expect(receivedAt).toBe(1_000);
+ }),
+ );
+
+ it.effect("rejects malformed gateway identifiers", () =>
+ Effect.gen(function* () {
+ expect(
+ Option.isNone(yield* Effect.option(Schema.decodeUnknownEffect(ChannelId)("123"))),
+ ).toBe(true);
+ expect(
+ Option.isNone(yield* Effect.option(Schema.decodeUnknownEffect(GatewayTimestampMillis)(-1))),
+ ).toBe(true);
+ }),
+ );
+
+ it("keeps incoming messages and files schema-backed", () => {
+ const message = new IncomingMessage({
+ id: GatewayMessageId.make("1"),
+ channel: ChannelId.make("telegram:123"),
+ sender: new MessageSender({ id: "7", displayName: "Ada", isBot: false }),
+ files: [new GatewayFile({ path: "C:/files/photo.jpg", name: "photo.jpg", kind: "image" })],
+ receivedAt: GatewayTimestampMillis.make(1_000),
+ });
+
+ expect(message.files[0]?.kind).toBe("image");
+ });
+});
diff --git a/apps/server/src/gateway/Models.ts b/packages/contracts/src/gateway.ts
similarity index 82%
rename from apps/server/src/gateway/Models.ts
rename to packages/contracts/src/gateway.ts
index 628a586..e8cc505 100644
--- a/apps/server/src/gateway/Models.ts
+++ b/packages/contracts/src/gateway.ts
@@ -1,4 +1,4 @@
-import { Schema } from "effect";
+import { type Effect, Schema, type Stream } from "effect";
const NonEmptyString = Schema.String.check(Schema.isMinLength(1));
const NonNegativeInteger = Schema.Finite.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0));
@@ -18,14 +18,16 @@ export type GatewayMessageId = typeof GatewayMessageId.Type;
export const ThreadId = NonEmptyString.pipe(Schema.brand("GatewayThreadId"));
export type ThreadId = typeof ThreadId.Type;
-export const TimestampMillis = NonNegativeInteger.pipe(Schema.brand("GatewayTimestampMillis"));
-export type TimestampMillis = typeof TimestampMillis.Type;
+export const GatewayTimestampMillis = NonNegativeInteger.pipe(
+ Schema.brand("GatewayTimestampMillis"),
+);
+export type GatewayTimestampMillis = typeof GatewayTimestampMillis.Type;
export const GatewayFileKind = Schema.Literals(["image", "audio", "video", "file"]);
export type GatewayFileKind = typeof GatewayFileKind.Type;
export class GatewayFile extends Schema.Class(
- "@compass/server/gateway/Models/GatewayFile",
+ "@compass/contracts/gateway/GatewayFile",
)({
path: NonEmptyString,
name: NonEmptyString,
@@ -35,7 +37,7 @@ export class GatewayFile extends Schema.Class(
}) {}
export class MessageSender extends Schema.Class(
- "@compass/server/gateway/Models/MessageSender",
+ "@compass/contracts/gateway/MessageSender",
)({
id: NonEmptyString,
displayName: NonEmptyString,
@@ -44,7 +46,7 @@ export class MessageSender extends Schema.Class(
}) {}
export class IncomingMessage extends Schema.Class(
- "@compass/server/gateway/Models/IncomingMessage",
+ "@compass/contracts/gateway/IncomingMessage",
)({
id: GatewayMessageId,
channel: ChannelId,
@@ -52,11 +54,11 @@ export class IncomingMessage extends Schema.Class(
sender: MessageSender,
text: Schema.optionalKey(Schema.String),
files: Schema.Array(GatewayFile),
- receivedAt: TimestampMillis,
+ receivedAt: GatewayTimestampMillis,
}) {}
export class OutgoingMessage extends Schema.Class(
- "@compass/server/gateway/Models/OutgoingMessage",
+ "@compass/contracts/gateway/OutgoingMessage",
)({
channel: ChannelId,
thread: Schema.optionalKey(ThreadId),
@@ -65,7 +67,7 @@ export class OutgoingMessage extends Schema.Class(
}) {}
export class SentMessage extends Schema.Class(
- "@compass/server/gateway/Models/SentMessage",
+ "@compass/contracts/gateway/SentMessage",
)({
channel: ChannelId,
thread: Schema.optionalKey(ThreadId),
@@ -147,3 +149,14 @@ export const parseChannelId = (channel: ChannelId): ParsedChannelId => {
nativeChannel: channel.slice(separator + 1),
};
};
+
+export interface IncomingAdapterEvent {
+ readonly channel: ChannelId;
+ readonly materialize: Effect.Effect;
+}
+
+export interface GatewayAdapter {
+ readonly name: AdapterName;
+ readonly incoming: Stream.Stream;
+ readonly send: (message: OutgoingMessage) => Effect.Effect;
+}
diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts
index d8221fc..8b4866b 100644
--- a/packages/contracts/src/index.ts
+++ b/packages/contracts/src/index.ts
@@ -1,2 +1,3 @@
export * as Schema from "effect/Schema";
+export * from "./gateway.ts";
export * from "./harness.ts";