1import type * as Firehose from "@distilled.cloud/aws/firehose";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Binding from "../../Binding.ts";
5import * as Output from "../../Output.ts";
6import { makeBatchedSink } from "../internal/BatchedSink.ts";
7import { isBindingHost } from "../Lambda/Function.ts";
8import type { DeliveryStream } from "./DeliveryStream.ts";
9import {
10 DeliveryStreamSink,
11 type DeliveryStreamSinkRecord,
12} from "./DeliveryStreamSink.ts";
13import { PutRecordBatch } from "./PutRecordBatch.ts";
14
15/**
16 * HTTP implementation of {@link DeliveryStreamSink}. At deploy time it grants
17 * `firehose:PutRecordBatch` on the bound delivery stream; at runtime it
18 * batches stream elements into `PutRecordBatch` calls (500 records / 4 MiB)
19 * with bounded retry of transient per-record failures. Provide this layer on
20 * the Function using the sink.
21 */
22export const DeliveryStreamSinkHttp = Layer.effect(
23 DeliveryStreamSink,
24 Effect.gen(function* () {
25 const putRecordBatch = yield* PutRecordBatch;
26
27 return Effect.fn(function* (deliveryStream: DeliveryStream) {
28 if (!globalThis.__ALCHEMY_RUNTIME__) {
29 const host = yield* Binding.Host;
30 if (isBindingHost(host)) {
31 yield* host.bind`Allow(${host}, AWS.Firehose.DeliveryStreamSink(${deliveryStream}))`(
32 {
33 policyStatements: [
34 {
35 Effect: "Allow",
36 Action: ["firehose:PutRecordBatch"],
37 Resource: [
38 Output.interpolate`${deliveryStream.deliveryStreamArn}`,
39 ],
40 },
41 ],
42 },
43 );
44 }
45 }
46 const send = yield* putRecordBatch(deliveryStream);
47 return makeBatchedSink<
48 DeliveryStreamSinkRecord,
49 Firehose.PutRecordBatchOutput,
50 Firehose.PutRecordBatchError
51 >({
52 maxRecords: 500,
53 maxBytes: 4 * 1024 * 1024,
54 sizeOf: (record) => record.Data.length,
55 send: (batch) => send({ Records: [...batch] }),
56 // PutRecordBatch failures are positional: RequestResponses[i] mirrors
57 // the request order and carries an ErrorCode (ServiceUnavailable /
58 // internal failure) — all transient, so re-submit them on the
59 // bounded schedule.
60 unprocessed: (out, batch) => {
61 if (!out.FailedPutCount) {
62 return [];
63 }
64 return batch.filter(
65 (_, index) => out.RequestResponses[index]?.ErrorCode !== undefined,
66 );
67 },
68 });
69 });
70 }),
71);