@useairfoil/connector-kit 0.4.0 → 0.5.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.
Files changed (67) hide show
  1. package/README.md +283 -90
  2. package/dist/chunk-Bp6m_JJh.js +13 -0
  3. package/dist/connector-app/index.d.ts +9 -0
  4. package/dist/connector-app/index.js +8 -0
  5. package/dist/connector-app-CvqbfN6R.js +32 -0
  6. package/dist/connector-app-CvqbfN6R.js.map +1 -0
  7. package/dist/errors/index.d.ts +2 -0
  8. package/dist/errors/index.js +3 -0
  9. package/dist/errors-DHWylqRZ.js +8 -0
  10. package/dist/errors-DHWylqRZ.js.map +1 -0
  11. package/dist/index-8KnVpoZp.d.ts +32 -0
  12. package/dist/index-BlhJCpEk.d.ts +124 -0
  13. package/dist/index-CQJsFJs3.d.ts +23 -0
  14. package/dist/index-DK9YcR6W.d.ts +25 -0
  15. package/dist/index-DaNW63Mv.d.ts +21 -0
  16. package/dist/index-DyuT2jzP.d.ts +14 -0
  17. package/dist/index.d.ts +9 -165
  18. package/dist/index.js +44 -228
  19. package/dist/index.js.map +1 -1
  20. package/dist/ingestion/index.d.ts +6 -0
  21. package/dist/ingestion/index.js +7 -0
  22. package/dist/ingestion-CMCroK7Y.js +171 -0
  23. package/dist/ingestion-CMCroK7Y.js.map +1 -0
  24. package/dist/publisher/index.d.ts +5 -0
  25. package/dist/publisher/index.js +5 -0
  26. package/dist/publisher-DusgYvaV.js +131 -0
  27. package/dist/publisher-DusgYvaV.js.map +1 -0
  28. package/dist/server-DAeFWT4o.js +113 -0
  29. package/dist/server-DAeFWT4o.js.map +1 -0
  30. package/dist/service-B7g_lHMI.d.ts +32 -0
  31. package/dist/state-store/index.d.ts +4 -0
  32. package/dist/state-store/index.js +3 -0
  33. package/dist/state-store-DG_4QpWG.js +31 -0
  34. package/dist/state-store-DG_4QpWG.js.map +1 -0
  35. package/dist/telemetry-p0dE5Mdu.js +171 -0
  36. package/dist/telemetry-p0dE5Mdu.js.map +1 -0
  37. package/dist/types-ZNrA-Dn0.d.ts +160 -0
  38. package/dist/webhook/index.d.ts +5 -0
  39. package/dist/webhook/index.js +6 -0
  40. package/dist/webhook-BSA-tXtU.js +13 -0
  41. package/dist/webhook-BSA-tXtU.js.map +1 -0
  42. package/package.json +36 -7
  43. package/src/connector-app/index.ts +1 -0
  44. package/src/connector-app/start.ts +38 -0
  45. package/src/core/builder.ts +76 -9
  46. package/src/core/index.ts +22 -0
  47. package/src/core/types.ts +185 -75
  48. package/src/errors/index.ts +1 -0
  49. package/src/index.ts +8 -28
  50. package/src/ingestion/engine.ts +301 -179
  51. package/src/ingestion/index.ts +1 -0
  52. package/src/publisher/console.ts +18 -0
  53. package/src/publisher/index.ts +7 -0
  54. package/src/publisher/service.ts +30 -13
  55. package/src/publisher/wings.ts +159 -72
  56. package/src/runtime/context.ts +15 -0
  57. package/src/state-store/index.ts +2 -0
  58. package/src/state-store/memory.ts +25 -0
  59. package/src/state-store/service.ts +18 -0
  60. package/src/telemetry.ts +246 -0
  61. package/src/webhook/index.ts +8 -0
  62. package/src/webhook/server.ts +172 -37
  63. package/src/webhook/types.ts +6 -17
  64. package/src/ingestion/state-store.ts +0 -36
  65. package/src/streams/pull-stream.ts +0 -54
  66. package/src/streams/webhook-queue.ts +0 -14
  67. /package/src/{core/errors.ts → errors/connector-error.ts} +0 -0
@@ -0,0 +1,171 @@
1
+ import { t as __export } from "./chunk-Bp6m_JJh.js";
2
+ import { t as ConnectorError } from "./errors-DHWylqRZ.js";
3
+ import { a as addCurrentSpanEvent, c as Publisher, i as SpanName, n as EventAttr, o as annotateError, r as EventName, t as Attr } from "./telemetry-p0dE5Mdu.js";
4
+ import { r as StateStore } from "./state-store-DG_4QpWG.js";
5
+ import { n as router, t as WebhookQueue } from "./server-DAeFWT4o.js";
6
+ import { DateTime, Effect, Layer, Metric, Queue } from "effect";
7
+ import { HttpRouter, HttpServerResponse } from "effect/unstable/http";
8
+
9
+ //#region src/ingestion/engine.ts
10
+ const connectorBatchesTotal = Metric.counter("connector_batches_total", { description: "Total resource batches attempted by connector sources" });
11
+ const connectorMutationsTotal = Metric.counter("connector_mutations_total", { description: "Total resource mutations attempted by connector sources" });
12
+ const connectorBatchSize = Metric.histogram("connector_batch_size", {
13
+ description: "Distribution of resource mutation batch sizes",
14
+ boundaries: [
15
+ 1,
16
+ 5,
17
+ 10,
18
+ 25,
19
+ 50,
20
+ 100,
21
+ 250,
22
+ 500,
23
+ 1e3
24
+ ]
25
+ });
26
+ const normalizeCursor = (value) => value instanceof Date ? value.toISOString() : value;
27
+ const normalizeResourceState = (state) => ({
28
+ backfill: state.backfill ? {
29
+ cutoff: normalizeCursor(state.backfill.cutoff),
30
+ pageCursor: state.backfill.pageCursor === void 0 ? void 0 : normalizeCursor(state.backfill.pageCursor),
31
+ completed: state.backfill.completed
32
+ } : void 0,
33
+ changes: state.changes ? { cursor: normalizeCursor(state.changes.cursor) } : void 0
34
+ });
35
+ function run(connector, options) {
36
+ const runtimeLayer = options?.webhook ? Layer.mergeAll(makeWebhookQueueLayer(), makeWebhookServerLayer(options.webhook)) : Layer.empty;
37
+ return Effect.gen(function* () {
38
+ const initialCutoff = options?.initialCutoff ?? (yield* DateTime.now.pipe(Effect.map(DateTime.formatIso)));
39
+ yield* Effect.logInfo("Connector started").pipe(Effect.annotateLogs({
40
+ [Attr.connectorName]: connector.name,
41
+ resources: connector.resources.length
42
+ }));
43
+ const sourceRun = runIngestion(connector, initialCutoff);
44
+ const webhookRun = options?.webhook ? runWebhookRuntime(options.webhook.routes) : Effect.void;
45
+ return yield* Effect.all([sourceRun, webhookRun], { concurrency: "unbounded" }).pipe(Effect.asVoid);
46
+ }).pipe(Effect.provide(runtimeLayer));
47
+ }
48
+ const makeWebhookQueueLayer = () => Layer.effect(WebhookQueue)(Effect.gen(function* () {
49
+ const queue = yield* Queue.bounded(1024);
50
+ return WebhookQueue.of({ queue });
51
+ }));
52
+ const runWebhookRuntime = (routes) => {
53
+ const hasAfterEnqueue = routes.some((route) => route.ackMode === "after-enqueue");
54
+ return Effect.all([hasAfterEnqueue ? runWebhookQueueConsumer : Effect.void, Effect.never], { concurrency: "unbounded" }).pipe(Effect.asVoid);
55
+ };
56
+ const runWebhookQueueConsumer = Effect.gen(function* () {
57
+ const queue = yield* WebhookQueue;
58
+ while (true) {
59
+ const batch = yield* Queue.take(queue.queue);
60
+ yield* publishBatch({
61
+ resource: batch.resource,
62
+ source: "webhook",
63
+ batch: batch.batch
64
+ });
65
+ }
66
+ });
67
+ const makeWebhookServerLayer = (options) => {
68
+ const healthPath = options.healthPath ?? "/health";
69
+ const app = Layer.mergeAll(router(options.routes), HttpRouter.add("GET", healthPath, Effect.succeed(HttpServerResponse.text("ok"))));
70
+ return HttpRouter.serve(app, { disableLogger: options.disableHttpLogger ?? true });
71
+ };
72
+ const runIngestion = (connector, initialCutoff) => Effect.forEach(connector.resources, (resource) => runResourceSources(connector, resource, initialCutoff), { concurrency: "unbounded" }).pipe(Effect.asVoid);
73
+ const initializeResourceState = (existing, initialCutoff) => ({
74
+ changes: existing?.changes ?? { cursor: initialCutoff },
75
+ backfill: existing?.backfill ?? {
76
+ cutoff: initialCutoff,
77
+ completed: false
78
+ }
79
+ });
80
+ const getInitializedState = Effect.fnUntraced(function* (resource, initialCutoff) {
81
+ return initializeResourceState(yield* (yield* StateStore).getResourceState(resource), initialCutoff);
82
+ });
83
+ const runResourceSources = (connector, resource, initialCutoff) => {
84
+ const runs = [resource.backfill ? runBackfill(connector, resource, initialCutoff) : Effect.void, resource.changes ? runChanges(connector, resource, initialCutoff) : Effect.void];
85
+ return Effect.all(runs, { concurrency: "unbounded" }).pipe(Effect.asVoid);
86
+ };
87
+ const runBackfill = Effect.fnUntraced(function* (connector, resource, initialCutoff) {
88
+ if (!resource.backfill) return;
89
+ const store = yield* StateStore;
90
+ let state = yield* getInitializedState(resource.name, initialCutoff);
91
+ while (state.backfill?.completed !== true) {
92
+ const backfill = state.backfill ?? {
93
+ cutoff: initialCutoff,
94
+ completed: false
95
+ };
96
+ const page = yield* resource.backfill.fetch({
97
+ pageCursor: backfill.pageCursor,
98
+ cutoff: backfill.cutoff
99
+ });
100
+ yield* publishBatch({
101
+ connector,
102
+ resource: resource.name,
103
+ source: "backfill",
104
+ batch: {
105
+ cursor: page.nextPageCursor ?? backfill.cutoff,
106
+ mutations: page.mutations
107
+ }
108
+ });
109
+ state = {
110
+ ...state,
111
+ backfill: {
112
+ cutoff: backfill.cutoff,
113
+ pageCursor: page.nextPageCursor,
114
+ completed: !page.hasMore
115
+ }
116
+ };
117
+ yield* Effect.withSpan(store.setResourceState(resource.name, normalizeResourceState(state)).pipe(Effect.tapError((error) => annotateError("state_set", error))), SpanName.stateSet, { attributes: { [Attr.stateKey]: resource.name } });
118
+ }
119
+ });
120
+ const runChanges = Effect.fnUntraced(function* (connector, resource, initialCutoff) {
121
+ if (!resource.changes) return;
122
+ const store = yield* StateStore;
123
+ while (true) {
124
+ const state = yield* getInitializedState(resource.name, initialCutoff);
125
+ const cursor = state.changes?.cursor ?? initialCutoff;
126
+ const page = yield* resource.changes.fetch({ cursor });
127
+ yield* publishBatch({
128
+ connector,
129
+ resource: resource.name,
130
+ source: "changes",
131
+ batch: {
132
+ cursor: page.cursor,
133
+ mutations: page.mutations
134
+ }
135
+ });
136
+ yield* Effect.withSpan(store.setResourceState(resource.name, normalizeResourceState({
137
+ ...state,
138
+ changes: { cursor: page.cursor }
139
+ })).pipe(Effect.tapError((error) => annotateError("state_set", error))), SpanName.stateSet, { attributes: { [Attr.stateKey]: resource.name } });
140
+ yield* Effect.sleep(resource.changes.interval ?? "1 minute");
141
+ }
142
+ });
143
+ const publishBatch = Effect.fnUntraced(function* (options) {
144
+ const metric = {
145
+ connector: options.connector?.name ?? "unknown",
146
+ resource: options.resource,
147
+ source: options.source
148
+ };
149
+ yield* Metric.update(Metric.withAttributes(connectorBatchesTotal, metric), 1);
150
+ yield* Metric.update(Metric.withAttributes(connectorMutationsTotal, metric), options.batch.mutations.length);
151
+ yield* Metric.update(Metric.withAttributes(connectorBatchSize, metric), options.batch.mutations.length);
152
+ const ack = yield* (yield* Publisher).publish({
153
+ resource: options.resource,
154
+ source: options.source,
155
+ batch: options.batch
156
+ });
157
+ yield* Effect.annotateCurrentSpan({ [Attr.publisherSuccess]: ack.status === "accepted" });
158
+ if (ack.status === "rejected") {
159
+ yield* Effect.annotateCurrentSpan({ [Attr.errorPhase]: "publish" });
160
+ return yield* Effect.fail(new ConnectorError({ message: `Publisher rejected batch for ${options.resource}: ${ack.reason}` }));
161
+ }
162
+ if (options.batch.cursor !== void 0) yield* addCurrentSpanEvent(EventName.batchCheckpoint, { [EventAttr.batchCursor]: options.batch.cursor });
163
+ });
164
+
165
+ //#endregion
166
+ //#region src/ingestion/index.ts
167
+ var ingestion_exports = /* @__PURE__ */ __export({ run: () => run });
168
+
169
+ //#endregion
170
+ export { run as n, ingestion_exports as t };
171
+ //# sourceMappingURL=ingestion-CMCroK7Y.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingestion-CMCroK7Y.js","names":["healthPath: HttpRouter.PathInput"],"sources":["../src/ingestion/engine.ts","../src/ingestion/index.ts"],"sourcesContent":["import { DateTime, Effect, Layer, Metric, Queue } from \"effect\";\nimport { HttpRouter, type HttpServer, HttpServerResponse } from \"effect/unstable/http\";\n\nimport type {\n ConnectorDefinition,\n Cursor,\n ResourceBatch,\n ResourceDefinition,\n ResourceState,\n WebhookRoute,\n} from \"../core/types\";\n\nimport { ConnectorError } from \"../errors\";\nimport { Publisher } from \"../publisher/service\";\nimport { StateStore } from \"../state-store\";\nimport {\n Attr,\n EventAttr,\n EventName,\n SpanName,\n addCurrentSpanEvent,\n annotateError,\n} from \"../telemetry\";\nimport { router, WebhookQueue, type QueuedWebhookBatch } from \"../webhook/server\";\n\nconst connectorBatchesTotal = Metric.counter(\"connector_batches_total\", {\n description: \"Total resource batches attempted by connector sources\",\n});\n\nconst connectorMutationsTotal = Metric.counter(\"connector_mutations_total\", {\n description: \"Total resource mutations attempted by connector sources\",\n});\n\nconst connectorBatchSize = Metric.histogram(\"connector_batch_size\", {\n description: \"Distribution of resource mutation batch sizes\",\n boundaries: [1, 5, 10, 25, 50, 100, 250, 500, 1000],\n});\n\ntype RunBaseOptions = {\n readonly initialCutoff?: Cursor.Value;\n};\n\nconst normalizeCursor = (value: Cursor.Value): Cursor.Value =>\n value instanceof Date ? value.toISOString() : value;\n\nconst normalizeResourceState = (state: ResourceState): ResourceState => ({\n backfill: state.backfill\n ? {\n cutoff: normalizeCursor(state.backfill.cutoff),\n pageCursor:\n state.backfill.pageCursor === undefined\n ? undefined\n : normalizeCursor(state.backfill.pageCursor),\n completed: state.backfill.completed,\n }\n : undefined,\n changes: state.changes\n ? {\n cursor: normalizeCursor(state.changes.cursor),\n }\n : undefined,\n});\n\nexport type RunOptions = RunBaseOptions & {\n readonly webhook?: {\n readonly routes: ReadonlyArray<WebhookRoute>;\n readonly healthPath?: HttpRouter.PathInput;\n readonly disableHttpLogger?: boolean;\n };\n};\n\ntype RunNoWebhookOptions = RunBaseOptions & {\n readonly webhook?: undefined;\n};\n\ntype RunWebhookOptions = RunOptions & {\n readonly webhook: NonNullable<RunOptions[\"webhook\"]>;\n};\n\nexport function run<const Resources extends ReadonlyArray<ResourceDefinition>>(\n connector: ConnectorDefinition<Resources>,\n options?: RunNoWebhookOptions,\n): Effect.Effect<void, ConnectorError, StateStore | Publisher>;\nexport function run<const Resources extends ReadonlyArray<ResourceDefinition>>(\n connector: ConnectorDefinition<Resources>,\n options: RunWebhookOptions,\n): Effect.Effect<void, ConnectorError, StateStore | Publisher | HttpServer.HttpServer>;\nexport function run(connector: ConnectorDefinition, options?: RunOptions) {\n const runtimeLayer = options?.webhook\n ? Layer.mergeAll(makeWebhookQueueLayer(), makeWebhookServerLayer(options.webhook))\n : Layer.empty;\n\n return Effect.gen(function* () {\n const initialCutoff =\n options?.initialCutoff ?? (yield* DateTime.now.pipe(Effect.map(DateTime.formatIso)));\n yield* Effect.logInfo(\"Connector started\").pipe(\n Effect.annotateLogs({\n [Attr.connectorName]: connector.name,\n resources: connector.resources.length,\n }),\n );\n\n const sourceRun = runIngestion(connector, initialCutoff);\n const webhookRun = options?.webhook ? runWebhookRuntime(options.webhook.routes) : Effect.void;\n\n return yield* Effect.all([sourceRun, webhookRun], { concurrency: \"unbounded\" }).pipe(\n Effect.asVoid,\n );\n }).pipe(Effect.provide(runtimeLayer));\n}\n\nconst makeWebhookQueueLayer = (): Layer.Layer<WebhookQueue> =>\n Layer.effect(WebhookQueue)(\n Effect.gen(function* () {\n const queue = yield* Queue.bounded<QueuedWebhookBatch>(1024);\n return WebhookQueue.of({ queue });\n }),\n );\n\nconst runWebhookRuntime = (routes: ReadonlyArray<WebhookRoute>) => {\n const hasAfterEnqueue = routes.some((route) => route.ackMode === \"after-enqueue\");\n return Effect.all([hasAfterEnqueue ? runWebhookQueueConsumer : Effect.void, Effect.never], {\n concurrency: \"unbounded\",\n }).pipe(Effect.asVoid);\n};\n\nconst runWebhookQueueConsumer = Effect.gen(function* () {\n const queue = yield* WebhookQueue;\n while (true) {\n const batch = yield* Queue.take(queue.queue);\n yield* publishBatch({ resource: batch.resource, source: \"webhook\", batch: batch.batch });\n }\n});\n\nconst makeWebhookServerLayer = (options: {\n readonly routes: ReadonlyArray<WebhookRoute>;\n readonly healthPath?: HttpRouter.PathInput;\n readonly disableHttpLogger?: boolean;\n}): Layer.Layer<never, never, HttpServer.HttpServer> => {\n const healthPath: HttpRouter.PathInput = options.healthPath ?? \"/health\";\n const app = Layer.mergeAll(\n router(options.routes),\n HttpRouter.add(\"GET\", healthPath, Effect.succeed(HttpServerResponse.text(\"ok\"))),\n );\n\n return HttpRouter.serve(app, {\n disableLogger: options.disableHttpLogger ?? true,\n });\n};\n\nconst runIngestion = (\n connector: ConnectorDefinition,\n initialCutoff: Cursor.Value,\n): Effect.Effect<void, ConnectorError, StateStore | Publisher> =>\n Effect.forEach(\n connector.resources,\n (resource) => runResourceSources(connector, resource, initialCutoff),\n {\n concurrency: \"unbounded\",\n },\n ).pipe(Effect.asVoid);\n\nconst initializeResourceState = (\n existing: ResourceState | undefined,\n initialCutoff: Cursor.Value,\n): ResourceState => ({\n changes: existing?.changes ?? { cursor: initialCutoff },\n backfill: existing?.backfill ?? {\n cutoff: initialCutoff,\n completed: false,\n },\n});\n\nconst getInitializedState = Effect.fnUntraced(function* (\n resource: string,\n initialCutoff: Cursor.Value,\n) {\n const store = yield* StateStore;\n const existing = yield* store.getResourceState(resource);\n return initializeResourceState(existing, initialCutoff);\n});\n\nconst runResourceSources = (\n connector: ConnectorDefinition,\n resource: ResourceDefinition,\n initialCutoff: Cursor.Value,\n) => {\n const runs = [\n resource.backfill ? runBackfill(connector, resource, initialCutoff) : Effect.void,\n resource.changes ? runChanges(connector, resource, initialCutoff) : Effect.void,\n ];\n\n return Effect.all(runs, { concurrency: \"unbounded\" }).pipe(Effect.asVoid);\n};\n\nconst runBackfill = Effect.fnUntraced(function* (\n connector: ConnectorDefinition,\n resource: ResourceDefinition,\n initialCutoff: Cursor.Value,\n) {\n if (!resource.backfill) return;\n const store = yield* StateStore;\n let state = yield* getInitializedState(resource.name, initialCutoff);\n\n while (state.backfill?.completed !== true) {\n const backfill = state.backfill ?? { cutoff: initialCutoff, completed: false };\n const page = yield* resource.backfill.fetch({\n pageCursor: backfill.pageCursor,\n cutoff: backfill.cutoff,\n });\n\n yield* publishBatch({\n connector,\n resource: resource.name,\n source: \"backfill\",\n batch: {\n cursor: page.nextPageCursor ?? backfill.cutoff,\n mutations: page.mutations,\n },\n });\n\n state = {\n ...state,\n backfill: {\n cutoff: backfill.cutoff,\n pageCursor: page.nextPageCursor,\n completed: !page.hasMore,\n },\n };\n\n yield* Effect.withSpan(\n store\n .setResourceState(resource.name, normalizeResourceState(state))\n .pipe(Effect.tapError((error) => annotateError(\"state_set\", error))),\n SpanName.stateSet,\n { attributes: { [Attr.stateKey]: resource.name } },\n );\n }\n});\n\nconst runChanges = Effect.fnUntraced(function* (\n connector: ConnectorDefinition,\n resource: ResourceDefinition,\n initialCutoff: Cursor.Value,\n) {\n if (!resource.changes) return;\n const store = yield* StateStore;\n\n while (true) {\n const state = yield* getInitializedState(resource.name, initialCutoff);\n const cursor = state.changes?.cursor ?? initialCutoff;\n const page = yield* resource.changes.fetch({ cursor });\n\n yield* publishBatch({\n connector,\n resource: resource.name,\n source: \"changes\",\n batch: {\n cursor: page.cursor,\n mutations: page.mutations,\n },\n });\n\n yield* Effect.withSpan(\n store\n .setResourceState(\n resource.name,\n normalizeResourceState({\n ...state,\n changes: { cursor: page.cursor },\n }),\n )\n .pipe(Effect.tapError((error) => annotateError(\"state_set\", error))),\n SpanName.stateSet,\n { attributes: { [Attr.stateKey]: resource.name } },\n );\n\n yield* Effect.sleep(resource.changes.interval ?? \"1 minute\");\n }\n});\n\nconst publishBatch = Effect.fnUntraced(function* (options: {\n readonly connector?: ConnectorDefinition;\n readonly resource: string;\n readonly source: \"backfill\" | \"changes\" | \"webhook\";\n readonly batch: ResourceBatch;\n}) {\n const metric = {\n connector: options.connector?.name ?? \"unknown\",\n resource: options.resource,\n source: options.source,\n };\n yield* Metric.update(Metric.withAttributes(connectorBatchesTotal, metric), 1);\n yield* Metric.update(\n Metric.withAttributes(connectorMutationsTotal, metric),\n options.batch.mutations.length,\n );\n yield* Metric.update(\n Metric.withAttributes(connectorBatchSize, metric),\n options.batch.mutations.length,\n );\n\n const publisher = yield* Publisher;\n const ack = yield* publisher.publish({\n resource: options.resource,\n source: options.source,\n batch: options.batch,\n });\n\n yield* Effect.annotateCurrentSpan({ [Attr.publisherSuccess]: ack.status === \"accepted\" });\n if (ack.status === \"rejected\") {\n yield* Effect.annotateCurrentSpan({ [Attr.errorPhase]: \"publish\" });\n return yield* Effect.fail(\n new ConnectorError({\n message: `Publisher rejected batch for ${options.resource}: ${ack.reason}`,\n }),\n );\n }\n\n if (options.batch.cursor !== undefined) {\n yield* addCurrentSpanEvent(EventName.batchCheckpoint, {\n [EventAttr.batchCursor]: options.batch.cursor,\n });\n }\n});\n","export { run, type RunOptions } from \"./engine\";\n"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import "../index-DyuT2jzP.js";
2
+ import { c as ResourceBatch } from "../types-ZNrA-Dn0.js";
3
+ import { a as PublisherService, i as Publisher, n as PublishOptions, r as PublishSource, t as PublishAck } from "../service-B7g_lHMI.js";
4
+ import { i as layerWings, n as layerConsole, r as WingsPublisherConfig } from "../index-DK9YcR6W.js";
5
+ export { PublishAck, PublishOptions, PublishSource, Publisher, PublisherService, ResourceBatch, WingsPublisherConfig, layerConsole, layerWings };
@@ -0,0 +1,5 @@
1
+ import "../errors-DHWylqRZ.js";
2
+ import { c as Publisher } from "../telemetry-p0dE5Mdu.js";
3
+ import { n as layerWings, r as layerConsole } from "../publisher-DusgYvaV.js";
4
+
5
+ export { Publisher, layerConsole, layerWings };
@@ -0,0 +1,131 @@
1
+ import { t as __export } from "./chunk-Bp6m_JJh.js";
2
+ import { t as ConnectorError } from "./errors-DHWylqRZ.js";
3
+ import { c as Publisher, i as SpanName } from "./telemetry-p0dE5Mdu.js";
4
+ import { Effect, Layer } from "effect";
5
+ import * as Wings from "@useairfoil/wings";
6
+
7
+ //#region src/publisher/console.ts
8
+ const layerConsole = Layer.succeed(Publisher)({ publish: ({ resource, source, batch }) => Effect.gen(function* () {
9
+ yield* Effect.logInfo(`[publisher] -> Source: ${source} | Resource: ${resource}`).pipe(Effect.annotateLogs({
10
+ mutations: batch.mutations.length,
11
+ cursor: batch.cursor,
12
+ source
13
+ }));
14
+ return {
15
+ status: "accepted",
16
+ resource
17
+ };
18
+ }) });
19
+
20
+ //#endregion
21
+ //#region src/publisher/wings.ts
22
+ const normalizeMapping = (mapping) => typeof mapping === "string" ? { name: mapping } : mapping;
23
+ const buildRecordBatch = (rows) => {
24
+ const [batch] = Wings.Arrow.tableFromJSON(Array.from(rows)).batches;
25
+ return batch ? Effect.succeed(batch) : Effect.fail(new ConnectorError({ message: "No rows to publish" }));
26
+ };
27
+ const validateTableMapping = (options) => Effect.try({
28
+ try: () => {
29
+ const keyField = Wings.TableUtils.getKeyField(options.table);
30
+ const versionField = Wings.TableUtils.getVersionField(options.table);
31
+ const partitionField = Wings.TableUtils.getPartitionField(options.table);
32
+ if (keyField.name !== options.resource.key) throw new Error(`Resource ${options.resource.name} key '${options.resource.key}' does not match Wings table key '${keyField.name}'`);
33
+ if (versionField.name !== options.resource.version) throw new Error(`Resource ${options.resource.name} version '${options.resource.version}' does not match Wings table version '${versionField.name}'`);
34
+ if (options.resource.partition?.required === true && !options.partitionValue) throw new Error(`Resource ${options.resource.name} requires a partition value`);
35
+ if (partitionField && !options.partitionValue) throw new Error(`Wings table ${options.table.name} is partitioned but no partition value was provided`);
36
+ if (!partitionField && options.partitionValue) throw new Error(`Wings table ${options.table.name} is not partitioned but a partition value was provided`);
37
+ return {
38
+ keyField,
39
+ versionField
40
+ };
41
+ },
42
+ catch: (cause) => new ConnectorError({
43
+ message: `Invalid Wings table mapping for ${options.resource.name}`,
44
+ cause
45
+ })
46
+ });
47
+ const accepted = (entry) => ({
48
+ status: "accepted",
49
+ resource: entry.resource.name,
50
+ partition: entry.partitionValue
51
+ });
52
+ const rejected = (entry, reason, rejectedRows) => ({
53
+ status: "rejected",
54
+ resource: entry.resource.name,
55
+ reason,
56
+ rejectedRows,
57
+ partition: entry.partitionValue
58
+ });
59
+ const layerWings = (config) => Layer.effect(Publisher)(Effect.gen(function* () {
60
+ const entries = /* @__PURE__ */ new Map();
61
+ const clusterClient = yield* Wings.WingsClient.clusterClient;
62
+ for (const resource of config.connector.resources) {
63
+ const mappingInput = config.tables[resource.name];
64
+ if (!mappingInput) return yield* Effect.fail(new ConnectorError({ message: `Missing table for ${resource.name}` }));
65
+ const mapping = normalizeMapping(mappingInput);
66
+ const table = yield* clusterClient.getTable({ name: mapping.name }).pipe(Effect.mapError((error) => new ConnectorError({
67
+ message: `Failed to resolve Wings table for ${resource.name}`,
68
+ cause: error
69
+ })));
70
+ const { keyField, versionField } = yield* validateTableMapping({
71
+ resource,
72
+ table,
73
+ partitionValue: mapping.partitionValue
74
+ });
75
+ const publisher = yield* Wings.WingsClient.publisher({
76
+ table,
77
+ partitionValue: mapping.partitionValue
78
+ }).pipe(Effect.mapError((error) => new ConnectorError({
79
+ message: error.message,
80
+ cause: error
81
+ })));
82
+ entries.set(resource.name, {
83
+ resource,
84
+ table,
85
+ publisher,
86
+ partitionValue: mapping.partitionValue,
87
+ keyField,
88
+ versionField
89
+ });
90
+ }
91
+ const pushRows = (entry, operation, rows) => Effect.gen(function* () {
92
+ if (rows.length === 0) return accepted(entry);
93
+ const batch = yield* buildRecordBatch(rows);
94
+ return yield* entry.publisher.push({
95
+ operation,
96
+ batch,
97
+ partitionValue: entry.partitionValue
98
+ }).pipe(Effect.matchEffect({
99
+ onFailure: (error) => Effect.succeed(rejected(entry, error.message, rows.length)),
100
+ onSuccess: () => Effect.succeed(accepted(entry))
101
+ }));
102
+ });
103
+ const service = { publish: Effect.fn(SpanName.publish, { kind: "producer" })(function* ({ resource, source: _source, batch }) {
104
+ const entry = entries.get(resource);
105
+ if (!entry) return yield* Effect.fail(new ConnectorError({ message: `Unknown resource ${resource}` }));
106
+ if (batch.mutations.length === 0) return accepted(entry);
107
+ const upserts = [];
108
+ const deletes = [];
109
+ for (const mutation of batch.mutations) if (mutation.op === "upsert") upserts.push(mutation.row);
110
+ else deletes.push({
111
+ [entry.keyField.name]: mutation.key,
112
+ [entry.versionField.name]: mutation.version
113
+ });
114
+ const upsertAck = yield* pushRows(entry, "upsert", upserts);
115
+ if (upsertAck.status === "rejected") return upsertAck;
116
+ return yield* pushRows(entry, "delete", deletes);
117
+ }) };
118
+ return Publisher.of(service);
119
+ }));
120
+
121
+ //#endregion
122
+ //#region src/publisher/index.ts
123
+ var publisher_exports = /* @__PURE__ */ __export({
124
+ Publisher: () => Publisher,
125
+ layerConsole: () => layerConsole,
126
+ layerWings: () => layerWings
127
+ });
128
+
129
+ //#endregion
130
+ export { layerWings as n, layerConsole as r, publisher_exports as t };
131
+ //# sourceMappingURL=publisher-DusgYvaV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publisher-DusgYvaV.js","names":["service: PublisherService","upserts: Rows[]","deletes: Rows[]"],"sources":["../src/publisher/console.ts","../src/publisher/wings.ts","../src/publisher/index.ts"],"sourcesContent":["import { Effect, Layer } from \"effect\";\n\nimport { Publisher } from \"./service\";\n\nexport const layerConsole = Layer.succeed(Publisher)({\n publish: ({ resource, source, batch }) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`[publisher] -> Source: ${source} | Resource: ${resource}`).pipe(\n Effect.annotateLogs({\n mutations: batch.mutations.length,\n cursor: batch.cursor,\n source,\n }),\n );\n\n return { status: \"accepted\" as const, resource };\n }),\n});\n","import * as Wings from \"@useairfoil/wings\";\nimport { Effect, Layer } from \"effect\";\n\nimport type { ConnectorDefinition, ResourceDefinition } from \"../core/types\";\n\nimport { ConnectorError } from \"../errors\";\nimport { SpanName } from \"../telemetry\";\nimport { Publisher, type PublishAck, type PublisherService } from \"./service\";\n\ntype Rows = Record<string, unknown>;\n\nexport type WingsTableMapping = {\n readonly name: string;\n readonly partitionValue?: Wings.PartitionValue.PartitionValue;\n};\n\nexport type WingsPublisherConfig = {\n readonly connector: ConnectorDefinition;\n readonly tables: Record<string, string | WingsTableMapping>;\n};\n\ntype PublisherEntry = {\n readonly resource: ResourceDefinition;\n readonly table: Wings.Cluster.Table.Table;\n readonly publisher: Wings.WingsClient.Publisher;\n readonly partitionValue?: Wings.PartitionValue.PartitionValue;\n readonly keyField: Wings.Cluster.ArrowType.Field;\n readonly versionField: Wings.Cluster.ArrowType.Field;\n};\n\nconst normalizeMapping = (mapping: string | WingsTableMapping): WingsTableMapping =>\n typeof mapping === \"string\" ? { name: mapping } : mapping;\n\nconst buildRecordBatch = (rows: ReadonlyArray<Rows>) => {\n const table = Wings.Arrow.tableFromJSON(Array.from(rows));\n const [batch] = table.batches;\n return batch\n ? Effect.succeed(batch)\n : Effect.fail(new ConnectorError({ message: \"No rows to publish\" }));\n};\n\nconst validateTableMapping = (options: {\n readonly resource: ResourceDefinition;\n readonly table: Wings.Cluster.Table.Table;\n readonly partitionValue?: Wings.PartitionValue.PartitionValue;\n}) =>\n Effect.try({\n try: () => {\n const keyField = Wings.TableUtils.getKeyField(options.table);\n const versionField = Wings.TableUtils.getVersionField(options.table);\n const partitionField = Wings.TableUtils.getPartitionField(options.table);\n\n if (keyField.name !== options.resource.key) {\n throw new Error(\n `Resource ${options.resource.name} key '${options.resource.key}' does not match Wings table key '${keyField.name}'`,\n );\n }\n if (versionField.name !== options.resource.version) {\n throw new Error(\n `Resource ${options.resource.name} version '${options.resource.version}' does not match Wings table version '${versionField.name}'`,\n );\n }\n if (options.resource.partition?.required === true && !options.partitionValue) {\n throw new Error(`Resource ${options.resource.name} requires a partition value`);\n }\n if (partitionField && !options.partitionValue) {\n throw new Error(\n `Wings table ${options.table.name} is partitioned but no partition value was provided`,\n );\n }\n if (!partitionField && options.partitionValue) {\n throw new Error(\n `Wings table ${options.table.name} is not partitioned but a partition value was provided`,\n );\n }\n\n return { keyField, versionField };\n },\n catch: (cause) =>\n new ConnectorError({\n message: `Invalid Wings table mapping for ${options.resource.name}`,\n cause,\n }),\n });\n\nconst accepted = (entry: PublisherEntry): PublishAck => ({\n status: \"accepted\",\n resource: entry.resource.name,\n partition: entry.partitionValue,\n});\n\nconst rejected = (entry: PublisherEntry, reason: string, rejectedRows: number): PublishAck => ({\n status: \"rejected\",\n resource: entry.resource.name,\n reason,\n rejectedRows,\n partition: entry.partitionValue,\n});\n\nexport const layerWings = (\n config: WingsPublisherConfig,\n): Layer.Layer<Publisher, ConnectorError, Wings.WingsClient.WingsClient> =>\n Layer.effect(Publisher)(\n Effect.gen(function* () {\n const entries = new Map<string, PublisherEntry>();\n const clusterClient = yield* Wings.WingsClient.clusterClient;\n\n for (const resource of config.connector.resources) {\n const mappingInput = config.tables[resource.name];\n if (!mappingInput) {\n return yield* Effect.fail(\n new ConnectorError({ message: `Missing table for ${resource.name}` }),\n );\n }\n\n const mapping = normalizeMapping(mappingInput);\n const table = yield* clusterClient.getTable({ name: mapping.name }).pipe(\n Effect.mapError(\n (error) =>\n new ConnectorError({\n message: `Failed to resolve Wings table for ${resource.name}`,\n cause: error,\n }),\n ),\n );\n const { keyField, versionField } = yield* validateTableMapping({\n resource,\n table,\n partitionValue: mapping.partitionValue,\n });\n\n const publisher = yield* Wings.WingsClient.publisher({\n table,\n partitionValue: mapping.partitionValue,\n }).pipe(\n Effect.mapError((error) => new ConnectorError({ message: error.message, cause: error })),\n );\n\n entries.set(resource.name, {\n resource,\n table,\n publisher,\n partitionValue: mapping.partitionValue,\n keyField,\n versionField,\n });\n }\n\n const pushRows = (\n entry: PublisherEntry,\n operation: \"upsert\" | \"delete\",\n rows: ReadonlyArray<Rows>,\n ) =>\n Effect.gen(function* () {\n if (rows.length === 0) return accepted(entry);\n const batch = yield* buildRecordBatch(rows);\n return yield* entry.publisher\n .push({ operation, batch, partitionValue: entry.partitionValue })\n .pipe(\n Effect.matchEffect({\n onFailure: (error) => Effect.succeed(rejected(entry, error.message, rows.length)),\n onSuccess: () => Effect.succeed(accepted(entry)),\n }),\n );\n });\n\n const service: PublisherService = {\n publish: Effect.fn(SpanName.publish, { kind: \"producer\" })(function* ({\n resource,\n source: _source,\n batch,\n }) {\n const entry = entries.get(resource);\n if (!entry) {\n return yield* Effect.fail(\n new ConnectorError({ message: `Unknown resource ${resource}` }),\n );\n }\n\n if (batch.mutations.length === 0) return accepted(entry);\n\n const upserts: Rows[] = [];\n const deletes: Rows[] = [];\n for (const mutation of batch.mutations) {\n if (mutation.op === \"upsert\") {\n upserts.push(mutation.row as Rows);\n } else {\n deletes.push({\n [entry.keyField.name]: mutation.key,\n [entry.versionField.name]: mutation.version,\n });\n }\n }\n\n const upsertAck = yield* pushRows(entry, \"upsert\", upserts);\n if (upsertAck.status === \"rejected\") return upsertAck;\n\n return yield* pushRows(entry, \"delete\", deletes);\n }),\n };\n\n return Publisher.of(service);\n }),\n );\n","export type { ResourceBatch } from \"../core/types\";\nexport type { PublishAck, PublishOptions, PublisherService, PublishSource } from \"./service\";\nexport type { WingsPublisherConfig } from \"./wings\";\n\nexport { layerConsole } from \"./console\";\nexport { Publisher } from \"./service\";\nexport { layerWings } from \"./wings\";\n"],"mappings":"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"}
@@ -0,0 +1,113 @@
1
+ import { t as ConnectorError } from "./errors-DHWylqRZ.js";
2
+ import { c as Publisher, i as SpanName, o as annotateError, t as Attr } from "./telemetry-p0dE5Mdu.js";
3
+ import { Cause, Context, Effect, Queue, Schema } from "effect";
4
+ import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
5
+
6
+ //#region src/webhook/server.ts
7
+ var WebhookQueue = class extends Context.Service()("@useairfoil/connector-kit/WebhookQueue") {};
8
+ const resourceBatch = (mutations) => ({ mutations });
9
+ const decodeAndHandleWebhook = (resourceName, webhook, payload) => Schema.decodeUnknownEffect(webhook.schema)(payload).pipe(Effect.mapError((cause) => new ConnectorError({
10
+ message: `Invalid webhook payload for resource ${resourceName}`,
11
+ cause
12
+ })), Effect.flatMap((decoded) => webhook.handler({ payload: decoded })));
13
+ const makeRouteContext = (request, rawBody, payload, batches) => ({
14
+ request,
15
+ rawBody,
16
+ payload,
17
+ to: (resource, payload$1) => Effect.gen(function* () {
18
+ if (!resource.webhook) return yield* Effect.fail(new ConnectorError({ message: `Resource ${resource.name} does not define a webhook handler` }));
19
+ const mutations = yield* decodeAndHandleWebhook(resource.name, resource.webhook, payload$1);
20
+ batches.push({
21
+ resource: resource.name,
22
+ batch: resourceBatch(mutations)
23
+ });
24
+ })
25
+ });
26
+ const compactBatches = (batches) => {
27
+ const grouped = /* @__PURE__ */ new Map();
28
+ for (const item of batches) {
29
+ const current = grouped.get(item.resource) ?? [];
30
+ grouped.set(item.resource, [...current, ...item.batch.mutations]);
31
+ }
32
+ return Array.from(grouped.entries()).map(([resource, mutations]) => ({
33
+ resource,
34
+ batch: resourceBatch(mutations)
35
+ }));
36
+ };
37
+ const publishBatches = (batches) => Effect.gen(function* () {
38
+ const publisher = yield* Publisher;
39
+ yield* Effect.forEach(batches, (batch) => Effect.gen(function* () {
40
+ const ack = yield* publisher.publish({
41
+ resource: batch.resource,
42
+ source: "webhook",
43
+ batch: batch.batch
44
+ });
45
+ if (ack.status === "rejected") return yield* Effect.fail(new ConnectorError({ message: ack.reason }));
46
+ }), { concurrency: "unbounded" });
47
+ });
48
+ const badRequest = (message) => HttpServerResponse.jsonUnsafe({
49
+ ok: false,
50
+ error: message
51
+ }, { status: 400 });
52
+ const readRawBody = (request) => request.arrayBuffer.pipe(Effect.map((buffer) => new Uint8Array(buffer)));
53
+ const parseJson = (rawBody) => Effect.try({
54
+ try: () => JSON.parse(new TextDecoder().decode(rawBody)),
55
+ catch: (cause) => new ConnectorError({
56
+ message: "Invalid webhook JSON",
57
+ cause
58
+ })
59
+ });
60
+ const makeHandler = (route) => Effect.gen(function* () {
61
+ const request = yield* HttpServerRequest.HttpServerRequest;
62
+ const collected = [];
63
+ const rawBodyResult = yield* readRawBody(request).pipe(Effect.match({
64
+ onFailure: () => ({
65
+ _tag: "Error",
66
+ message: "Failed to read webhook body"
67
+ }),
68
+ onSuccess: (value) => ({
69
+ _tag: "Success",
70
+ value
71
+ })
72
+ }));
73
+ if (rawBodyResult._tag === "Error") return badRequest(rawBodyResult.message);
74
+ const jsonResult = yield* parseJson(rawBodyResult.value).pipe(Effect.match({
75
+ onFailure: (error) => ({
76
+ _tag: "Error",
77
+ message: error.message
78
+ }),
79
+ onSuccess: (value) => ({
80
+ _tag: "Success",
81
+ value
82
+ })
83
+ }));
84
+ if (jsonResult._tag === "Error") return badRequest(jsonResult.message);
85
+ const payloadResult = yield* Schema.decodeUnknownEffect(route.schema)(jsonResult.value).pipe(Effect.match({
86
+ onFailure: () => ({
87
+ _tag: "Error",
88
+ message: "Invalid webhook payload"
89
+ }),
90
+ onSuccess: (value) => ({
91
+ _tag: "Success",
92
+ value
93
+ })
94
+ }));
95
+ if (payloadResult._tag === "Error") return badRequest(payloadResult.message);
96
+ const response = yield* Effect.withSpan(route.handler(makeRouteContext(request, rawBodyResult.value, payloadResult.value, collected)).pipe(Effect.tapError((error) => annotateError("webhook_handle", error))), SpanName.webhookHandle, { attributes: { [Attr.webhookPath]: route.path } });
97
+ const batches = compactBatches(collected);
98
+ if (route.ackMode === "after-enqueue") {
99
+ const queue = yield* WebhookQueue;
100
+ yield* Queue.offerAll(queue.queue, batches);
101
+ return response;
102
+ }
103
+ yield* Effect.withSpan(publishBatches(batches).pipe(Effect.tapError((error) => annotateError("webhook_publish", error))), SpanName.webhookHandle, { attributes: { [Attr.webhookPath]: route.path } });
104
+ return response;
105
+ }).pipe(Effect.catchCause((cause) => Effect.logWarning(`Webhook handler error: ${Cause.pretty(cause)}`).pipe(Effect.andThen(Effect.succeed(HttpServerResponse.jsonUnsafe({
106
+ ok: false,
107
+ error: "Webhook handler failed"
108
+ }, { status: 500 }))))));
109
+ const router = (routes) => HttpRouter.addAll(routes.map((route) => HttpRouter.route("POST", route.path, makeHandler(route))));
110
+
111
+ //#endregion
112
+ export { router as n, WebhookQueue as t };
113
+ //# sourceMappingURL=server-DAeFWT4o.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-DAeFWT4o.js","names":["payload","collected: Array<QueuedWebhookBatch>"],"sources":["../src/webhook/server.ts"],"sourcesContent":["import { Cause, Context, Effect, Queue, Schema } from \"effect\";\nimport { HttpRouter, HttpServerRequest, HttpServerResponse } from \"effect/unstable/http\";\n\nimport type {\n ResourceBatch,\n ResourceDefinition,\n ResourceMutation,\n WebhookHandler,\n WebhookRoute,\n} from \"../core/types\";\n\nimport { ConnectorError } from \"../errors\";\nimport { Publisher } from \"../publisher/service\";\nimport { Attr, SpanName, annotateError } from \"../telemetry\";\n\nexport type QueuedWebhookBatch = {\n readonly resource: string;\n readonly batch: ResourceBatch;\n};\n\nexport class WebhookQueue extends Context.Service<\n WebhookQueue,\n {\n readonly queue: Queue.Queue<QueuedWebhookBatch>;\n }\n>()(\"@useairfoil/connector-kit/WebhookQueue\") {}\n\nconst resourceBatch = <Row extends object>(\n mutations: ReadonlyArray<ResourceMutation<Row>>,\n): ResourceBatch<Row> => ({\n mutations,\n});\n\nconst decodeAndHandleWebhook = <Row extends object, Payload>(\n resourceName: string,\n webhook: WebhookHandler<Row, Payload>,\n payload: unknown,\n) =>\n Schema.decodeUnknownEffect(webhook.schema)(payload).pipe(\n Effect.mapError(\n (cause) =>\n new ConnectorError({\n message: `Invalid webhook payload for resource ${resourceName}`,\n cause,\n }),\n ),\n Effect.flatMap((decoded) => webhook.handler({ payload: decoded })),\n );\n\nconst makeRouteContext = (\n request: HttpServerRequest.HttpServerRequest,\n rawBody: Uint8Array,\n payload: unknown,\n batches: Array<QueuedWebhookBatch>,\n): import(\"../core/types\").WebhookRouteContext<ReadonlyArray<ResourceDefinition>, unknown> => ({\n request,\n rawBody,\n payload,\n to: (resource, payload) =>\n Effect.gen(function* () {\n if (!resource.webhook) {\n return yield* Effect.fail(\n new ConnectorError({\n message: `Resource ${resource.name} does not define a webhook handler`,\n }),\n );\n }\n\n const mutations = yield* decodeAndHandleWebhook(resource.name, resource.webhook, payload);\n batches.push({\n resource: resource.name,\n batch: resourceBatch(mutations),\n });\n }),\n});\n\nconst compactBatches = (batches: ReadonlyArray<QueuedWebhookBatch>) => {\n const grouped = new Map<string, ResourceMutation[]>();\n for (const item of batches) {\n const current = grouped.get(item.resource) ?? [];\n grouped.set(item.resource, [...current, ...item.batch.mutations]);\n }\n return Array.from(grouped.entries()).map(([resource, mutations]) => ({\n resource,\n batch: resourceBatch(mutations),\n }));\n};\n\nconst publishBatches = (batches: ReadonlyArray<QueuedWebhookBatch>) =>\n Effect.gen(function* () {\n const publisher = yield* Publisher;\n yield* Effect.forEach(\n batches,\n (batch) =>\n Effect.gen(function* () {\n const ack = yield* publisher.publish({\n resource: batch.resource,\n source: \"webhook\",\n batch: batch.batch,\n });\n if (ack.status === \"rejected\") {\n return yield* Effect.fail(new ConnectorError({ message: ack.reason }));\n }\n }),\n { concurrency: \"unbounded\" },\n );\n });\n\nconst badRequest = (message: string) =>\n HttpServerResponse.jsonUnsafe({ ok: false, error: message }, { status: 400 });\n\nconst readRawBody = (request: HttpServerRequest.HttpServerRequest) =>\n request.arrayBuffer.pipe(Effect.map((buffer) => new Uint8Array(buffer)));\n\nconst parseJson = (rawBody: Uint8Array) =>\n Effect.try({\n try: () => JSON.parse(new TextDecoder().decode(rawBody)) as unknown,\n catch: (cause) => new ConnectorError({ message: \"Invalid webhook JSON\", cause }),\n });\n\nconst makeHandler = (route: WebhookRoute) =>\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const collected: Array<QueuedWebhookBatch> = [];\n\n const rawBodyResult = yield* readRawBody(request).pipe(\n Effect.match({\n onFailure: () => ({ _tag: \"Error\" as const, message: \"Failed to read webhook body\" }),\n onSuccess: (value) => ({ _tag: \"Success\" as const, value }),\n }),\n );\n if (rawBodyResult._tag === \"Error\") {\n return badRequest(rawBodyResult.message);\n }\n\n const jsonResult = yield* parseJson(rawBodyResult.value).pipe(\n Effect.match({\n onFailure: (error) => ({ _tag: \"Error\" as const, message: error.message }),\n onSuccess: (value) => ({ _tag: \"Success\" as const, value }),\n }),\n );\n if (jsonResult._tag === \"Error\") {\n return badRequest(jsonResult.message);\n }\n\n const payloadResult = yield* Schema.decodeUnknownEffect(route.schema)(jsonResult.value).pipe(\n Effect.match({\n onFailure: () => ({ _tag: \"Error\" as const, message: \"Invalid webhook payload\" }),\n onSuccess: (value) => ({ _tag: \"Success\" as const, value }),\n }),\n );\n if (payloadResult._tag === \"Error\") {\n return badRequest(payloadResult.message);\n }\n\n const response = yield* Effect.withSpan(\n route\n .handler(makeRouteContext(request, rawBodyResult.value, payloadResult.value, collected))\n .pipe(Effect.tapError((error) => annotateError(\"webhook_handle\", error))),\n SpanName.webhookHandle,\n { attributes: { [Attr.webhookPath]: route.path } },\n );\n\n const batches = compactBatches(collected);\n\n if (route.ackMode === \"after-enqueue\") {\n const queue = yield* WebhookQueue;\n yield* Queue.offerAll(queue.queue, batches);\n return response;\n }\n\n yield* Effect.withSpan(\n publishBatches(batches).pipe(\n Effect.tapError((error) => annotateError(\"webhook_publish\", error)),\n ),\n SpanName.webhookHandle,\n { attributes: { [Attr.webhookPath]: route.path } },\n );\n\n return response;\n }).pipe(\n Effect.catchCause((cause) =>\n Effect.logWarning(`Webhook handler error: ${Cause.pretty(cause)}`).pipe(\n Effect.andThen(\n Effect.succeed(\n HttpServerResponse.jsonUnsafe(\n { ok: false, error: \"Webhook handler failed\" },\n { status: 500 },\n ),\n ),\n ),\n ),\n ),\n );\n\nexport const router = (routes: ReadonlyArray<WebhookRoute>) =>\n HttpRouter.addAll(\n routes.map((route) => HttpRouter.route(\"POST\", route.path, makeHandler(route))),\n );\n"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import { t as ConnectorError } from "./index-DyuT2jzP.js";
2
+ import { c as ResourceBatch } from "./types-ZNrA-Dn0.js";
3
+ import { Context } from "effect";
4
+ import * as Wings from "@useairfoil/wings";
5
+ import * as Effect$1 from "effect/Effect";
6
+
7
+ //#region src/publisher/service.d.ts
8
+ type PublishSource = "backfill" | "changes" | "webhook";
9
+ type PublishAck = {
10
+ readonly status: "accepted";
11
+ readonly resource: string;
12
+ readonly partition?: Wings.PartitionValue.PartitionValue;
13
+ } | {
14
+ readonly status: "rejected";
15
+ readonly resource: string;
16
+ readonly reason: string;
17
+ readonly rejectedRows?: number;
18
+ readonly partition?: Wings.PartitionValue.PartitionValue;
19
+ };
20
+ type PublishOptions = {
21
+ readonly resource: string;
22
+ readonly source: PublishSource;
23
+ readonly batch: ResourceBatch;
24
+ };
25
+ interface PublisherService {
26
+ readonly publish: (options: PublishOptions) => Effect$1.Effect<PublishAck, ConnectorError>;
27
+ }
28
+ declare const Publisher_base: Context.ServiceClass<Publisher, "@useairfoil/connector-kit/Publisher", PublisherService>;
29
+ declare class Publisher extends Publisher_base {}
30
+ //#endregion
31
+ export { PublisherService as a, Publisher as i, PublishOptions as n, PublishSource as r, PublishAck as t };
32
+ //# sourceMappingURL=service-B7g_lHMI.d.ts.map
@@ -0,0 +1,4 @@
1
+ import "../index-DyuT2jzP.js";
2
+ import "../types-ZNrA-Dn0.js";
3
+ import { i as StateStoreService, n as layerMemory, r as StateStore } from "../index-DaNW63Mv.js";
4
+ export { StateStore, StateStoreService, layerMemory };
@@ -0,0 +1,3 @@
1
+ import { n as layerMemory, r as StateStore } from "../state-store-DG_4QpWG.js";
2
+
3
+ export { StateStore, layerMemory };
@@ -0,0 +1,31 @@
1
+ import { t as __export } from "./chunk-Bp6m_JJh.js";
2
+ import { Context, Effect, Layer, Ref } from "effect";
3
+
4
+ //#region src/state-store/service.ts
5
+ var StateStore = class extends Context.Service()("@useairfoil/connector-kit/StateStore") {};
6
+
7
+ //#endregion
8
+ //#region src/state-store/memory.ts
9
+ /** In-memory resource state store backed by a `Ref<Map>`. Suitable for development and tests. */
10
+ const layerMemory = Layer.effect(StateStore)(Effect.gen(function* () {
11
+ const ref = yield* Ref.make(/* @__PURE__ */ new Map());
12
+ return StateStore.of({
13
+ getResourceState: (resource) => Effect.map(Ref.get(ref), (map) => map.get(resource)),
14
+ setResourceState: (resource, state) => Effect.map(Ref.update(ref, (map) => {
15
+ const next = new Map(map);
16
+ next.set(resource, state);
17
+ return next;
18
+ }), () => void 0)
19
+ });
20
+ }));
21
+
22
+ //#endregion
23
+ //#region src/state-store/index.ts
24
+ var state_store_exports = /* @__PURE__ */ __export({
25
+ StateStore: () => StateStore,
26
+ layerMemory: () => layerMemory
27
+ });
28
+
29
+ //#endregion
30
+ export { layerMemory as n, StateStore as r, state_store_exports as t };
31
+ //# sourceMappingURL=state-store-DG_4QpWG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-store-DG_4QpWG.js","names":["layerMemory: Layer.Layer<StateStore>"],"sources":["../src/state-store/service.ts","../src/state-store/memory.ts","../src/state-store/index.ts"],"sourcesContent":["import { Context, type Effect } from \"effect\";\n\nimport type { ResourceState } from \"../core/types\";\nimport type { ConnectorError } from \"../errors\";\n\nexport interface StateStoreService {\n readonly getResourceState: (\n resource: string,\n ) => Effect.Effect<ResourceState | undefined, ConnectorError>;\n readonly setResourceState: (\n resource: string,\n state: ResourceState,\n ) => Effect.Effect<void, ConnectorError>;\n}\n\nexport class StateStore extends Context.Service<StateStore, StateStoreService>()(\n \"@useairfoil/connector-kit/StateStore\",\n) {}\n","import { Effect, Layer, Ref } from \"effect\";\n\nimport type { ResourceState } from \"../core/types\";\n\nimport { StateStore } from \"./service\";\n\n/** In-memory resource state store backed by a `Ref<Map>`. Suitable for development and tests. */\nexport const layerMemory: Layer.Layer<StateStore> = Layer.effect(StateStore)(\n Effect.gen(function* () {\n const ref = yield* Ref.make(new Map<string, ResourceState>());\n\n return StateStore.of({\n getResourceState: (resource) => Effect.map(Ref.get(ref), (map) => map.get(resource)),\n setResourceState: (resource, state) =>\n Effect.map(\n Ref.update(ref, (map) => {\n const next = new Map(map);\n next.set(resource, state);\n return next;\n }),\n () => undefined,\n ),\n });\n }),\n);\n","export { layerMemory } from \"./memory\";\nexport { StateStore, type StateStoreService } from \"./service\";\n"],"mappings":";;;;AAeA,IAAa,aAAb,cAAgC,QAAQ,SAAwC,CAC9E,uCACD,CAAC;;;;;ACVF,MAAaA,cAAuC,MAAM,OAAO,WAAW,CAC1E,OAAO,IAAI,aAAa;CACtB,MAAM,MAAM,OAAO,IAAI,qBAAK,IAAI,KAA4B,CAAC;AAE7D,QAAO,WAAW,GAAG;EACnB,mBAAmB,aAAa,OAAO,IAAI,IAAI,IAAI,IAAI,GAAG,QAAQ,IAAI,IAAI,SAAS,CAAC;EACpF,mBAAmB,UAAU,UAC3B,OAAO,IACL,IAAI,OAAO,MAAM,QAAQ;GACvB,MAAM,OAAO,IAAI,IAAI,IAAI;AACzB,QAAK,IAAI,UAAU,MAAM;AACzB,UAAO;IACP,QACI,OACP;EACJ,CAAC;EACF,CACH"}