1import type * as Kinesis from "@distilled.cloud/aws/kinesis";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Binding from "../../Binding.ts";
5import { makeBatchedSink } from "../internal/BatchedSink.ts";
6import { isBindingHost } from "../Lambda/Function.ts";
7import { PutRecords } from "./PutRecords.ts";
8import { StreamSink, type StreamSinkRecord } from "./StreamSink.ts";
9import type { Stream } from "./Stream.ts";
10
11const encoder = new TextEncoder();
12
13export const StreamSinkHttp = Layer.effect(
14 StreamSink,
15 Effect.gen(function* () {
16 const putRecords = yield* PutRecords;
17
18 return Effect.fn(function* (stream: Stream) {
19 if (!globalThis.__ALCHEMY_RUNTIME__) {
20 const host = yield* Binding.Host;
21 if (isBindingHost(host)) {
22 yield* host.bind`Allow(${host}, AWS.Kinesis.StreamSink(${stream}))`({
23 policyStatements: [
24 {
25 Effect: "Allow",
26 Action: ["kinesis:PutRecords"],
27 Resource: [stream.streamArn],
28 },
29 ],
30 });
31 }
32 }
33 const publish = yield* putRecords(stream);
34 return makeBatchedSink<
35 StreamSinkRecord,
36 Kinesis.PutRecordsOutput,
37 Kinesis.PutRecordsError
38 >({
39 maxRecords: 500,
40 maxBytes: 5 * 1024 * 1024,
41 sizeOf: (record) =>
42 record.Data.length + encoder.encode(record.PartitionKey).length,
43 send: (batch) => publish({ Records: [...batch] }),
44 // PutRecords failures are positional: Records[i] mirrors the request
45 // order and carries an ErrorCode (throughput exceeded / internal
46 // failure) — all transient, so re-submit them on the bounded schedule.
47 unprocessed: (out, batch) => {
48 if (!out.FailedRecordCount) {
49 return [];
50 }
51 return batch.filter(
52 (_, index) => out.Records[index]?.ErrorCode !== undefined,
53 );
54 },
55 });
56 });
57 }),
58);