1import * as GCP from "alchemy/GCP";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import * as HttpServerResponse from "effect/http/HttpServerResponse";
5import {
6 decodeOrderEvent,
7 OrderEvents,
8 OrderEventsTable,
9} from "./resources.ts";
10
11/**
12 * Records every order event, of every type, as a row in BigQuery.
13 *
14 * No filter: this subscription receives everything published to the
15 * topic, including the events the email consumer never sees and the
16 * ones it dead-letters.
17 */
18export default class Analytics extends GCP.Function<Analytics>()(
19 "Analytics",
20 {
21 main: import.meta.url,
22 },
23 Effect.gen(function* () {
24 const table = yield* GCP.BigQuery.WriteTable(OrderEventsTable);
25
26 yield* GCP.PubSub.consumeTopicMessages(OrderEvents, (messages) =>
27 messages.pipe(
28 Stream.runForEach(({ message }) =>
29 Effect.gen(function* () {
30 const event = yield* decodeOrderEvent(message.data);
31 yield* table.insert(
32 [
33 {
34 eventId: event.eventId,
35 type: event.type,
36 orderId: event.orderId,
37 email: event.email,
38 total: event.total,
39 occurredAt: new Date(event.occurredAt),
40 receivedAt: new Date(),
41 },
42 ],
43 // BigQuery drops a repeated insertId within its dedup
44 // window, so a redelivered event does not add a second row.
45 { insertIds: [event.eventId] },
46 );
47 yield* Effect.log(`analytics: ${event.type} ${event.orderId}`);
48 }),
49 ),
50 Effect.orDie,
51 ),
52 );
53
54 return {
55 fetch: Effect.succeed(HttpServerResponse.text("ok")),
56 };
57 }).pipe(
58 Effect.provide([GCP.Run.TopicEventSource, GCP.BigQuery.WriteTableHttp]),
59 ),
60) {}