@useairfoil/connector-kit 0.5.0 → 0.5.2

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 (110) hide show
  1. package/METRICS.md +78 -0
  2. package/README.md +190 -21
  3. package/dist/auth/index.d.ts +2 -0
  4. package/dist/auth/index.js +3 -0
  5. package/dist/auth-DvU_CZ7C.js +42 -0
  6. package/dist/auth-DvU_CZ7C.js.map +1 -0
  7. package/dist/connector-app/index.d.ts +14 -9
  8. package/dist/connector-app/index.js +9 -6
  9. package/dist/connector-app-d7Z-SZLQ.js +64 -0
  10. package/dist/connector-app-d7Z-SZLQ.js.map +1 -0
  11. package/dist/constants-BRhIX_rz.js +27 -0
  12. package/dist/constants-BRhIX_rz.js.map +1 -0
  13. package/dist/errors/index.d.ts +1 -1
  14. package/dist/{index-DK9YcR6W.d.ts → index-Byr0Lf0C.d.ts} +4 -4
  15. package/dist/index-CBT-JX-T.d.ts +173 -0
  16. package/dist/index-CTHVudvQ.d.ts +41 -0
  17. package/dist/index-Coh_SkBs.d.ts +95 -0
  18. package/dist/index-Cq2IzY7T.d.ts +80 -0
  19. package/dist/{index-DyuT2jzP.d.ts → index-D54ySAKn.d.ts} +1 -1
  20. package/dist/{index-CQJsFJs3.d.ts → index-DKWIh6M1.d.ts} +7 -5
  21. package/dist/{index-BlhJCpEk.d.ts → index-DtJX_tic.d.ts} +39 -10
  22. package/dist/{index-8KnVpoZp.d.ts → index-J42AZaXU.d.ts} +11 -5
  23. package/dist/index-o1-Z3n7V.d.ts +55 -0
  24. package/dist/index.d.ts +14 -9
  25. package/dist/index.js +14 -8
  26. package/dist/index.js.map +1 -1
  27. package/dist/ingestion/index.d.ts +5 -5
  28. package/dist/ingestion/index.js +7 -4
  29. package/dist/ingestion-BQo4cbGw.js +200 -0
  30. package/dist/ingestion-BQo4cbGw.js.map +1 -0
  31. package/dist/manifest/index.d.ts +2 -0
  32. package/dist/manifest/index.js +4 -0
  33. package/dist/manifest-ByE6YPG0.js +221 -0
  34. package/dist/manifest-ByE6YPG0.js.map +1 -0
  35. package/dist/metrics-CVAk1ALR.d.ts +53 -0
  36. package/dist/metrics-CWPy2Aqc.js +96 -0
  37. package/dist/metrics-CWPy2Aqc.js.map +1 -0
  38. package/dist/metrics.d.ts +4 -0
  39. package/dist/metrics.js +3 -0
  40. package/dist/publisher/index.d.ts +4 -4
  41. package/dist/publisher/index.js +3 -2
  42. package/dist/{publisher-DusgYvaV.js → publisher-BYT3RLVO.js} +2 -2
  43. package/dist/{publisher-DusgYvaV.js.map → publisher-BYT3RLVO.js.map} +1 -1
  44. package/dist/runtime-config/index.d.ts +2 -0
  45. package/dist/runtime-config/index.js +4 -0
  46. package/dist/runtime-config-BpB-d_bF.js +51 -0
  47. package/dist/runtime-config-BpB-d_bF.js.map +1 -0
  48. package/dist/server-CXTQ7odl.js +183 -0
  49. package/dist/server-CXTQ7odl.js.map +1 -0
  50. package/dist/{telemetry-p0dE5Mdu.js → service-DuFiQwid.js} +30 -10
  51. package/dist/service-DuFiQwid.js.map +1 -0
  52. package/dist/{service-B7g_lHMI.d.ts → service-IE5qaf9x.d.ts} +3 -3
  53. package/dist/state-store/index.d.ts +3 -4
  54. package/dist/state-store/index.js +4 -2
  55. package/dist/state-store-yl6LiTQ0.js +198 -0
  56. package/dist/state-store-yl6LiTQ0.js.map +1 -0
  57. package/dist/status/index.d.ts +5 -0
  58. package/dist/status/index.js +6 -0
  59. package/dist/status-B-oZBc33.js +84 -0
  60. package/dist/status-B-oZBc33.js.map +1 -0
  61. package/dist/{types-ZNrA-Dn0.d.ts → types-B35J5YPu.d.ts} +20 -6
  62. package/dist/webhook/index.d.ts +5 -4
  63. package/dist/webhook/index.js +5 -3
  64. package/dist/{webhook-BSA-tXtU.js → webhook-DI-Afxkg.js} +2 -2
  65. package/dist/{webhook-BSA-tXtU.js.map → webhook-DI-Afxkg.js.map} +1 -1
  66. package/package.json +36 -7
  67. package/src/auth/index.ts +1 -0
  68. package/src/auth/token-cache.ts +54 -0
  69. package/src/connector-app/check.ts +76 -0
  70. package/src/connector-app/index.ts +1 -0
  71. package/src/connector-app/start.ts +4 -0
  72. package/src/core/builder.ts +8 -3
  73. package/src/core/types.ts +20 -2
  74. package/src/index.ts +5 -0
  75. package/src/ingestion/engine.ts +246 -133
  76. package/src/manifest/config.ts +232 -0
  77. package/src/manifest/index.ts +14 -0
  78. package/src/manifest/manifest.ts +31 -0
  79. package/src/manifest/number-schema.ts +17 -0
  80. package/src/manifest/schema.ts +160 -0
  81. package/src/metrics.ts +128 -0
  82. package/src/publisher/instrumented.ts +68 -0
  83. package/src/runtime-config/constants.ts +27 -0
  84. package/src/runtime-config/error.ts +13 -0
  85. package/src/runtime-config/index.ts +3 -0
  86. package/src/runtime-config/layer.ts +56 -0
  87. package/src/state-store/index.ts +2 -1
  88. package/src/state-store/keys.ts +22 -0
  89. package/src/state-store/layer-key-value-store.ts +132 -0
  90. package/src/state-store/layer-memory.ts +11 -0
  91. package/src/state-store/layer-sql.ts +50 -0
  92. package/src/state-store/schema.ts +57 -0
  93. package/src/state-store/service.ts +39 -6
  94. package/src/state-store/state.ts +17 -0
  95. package/src/status/index.ts +2 -0
  96. package/src/status/route.ts +77 -0
  97. package/src/status/types.ts +33 -0
  98. package/src/telemetry.ts +46 -4
  99. package/src/webhook/server.ts +54 -19
  100. package/dist/connector-app-CvqbfN6R.js +0 -32
  101. package/dist/connector-app-CvqbfN6R.js.map +0 -1
  102. package/dist/index-DaNW63Mv.d.ts +0 -21
  103. package/dist/ingestion-CMCroK7Y.js +0 -171
  104. package/dist/ingestion-CMCroK7Y.js.map +0 -1
  105. package/dist/server-DAeFWT4o.js +0 -113
  106. package/dist/server-DAeFWT4o.js.map +0 -1
  107. package/dist/state-store-DG_4QpWG.js +0 -31
  108. package/dist/state-store-DG_4QpWG.js.map +0 -1
  109. package/dist/telemetry-p0dE5Mdu.js.map +0 -1
  110. package/src/state-store/memory.ts +0 -25
@@ -7,10 +7,12 @@ import type {
7
7
  ResourceMutation,
8
8
  WebhookHandler,
9
9
  WebhookRoute,
10
+ WebhookRouteContext,
10
11
  } from "../core/types";
11
12
 
12
13
  import { ConnectorError } from "../errors";
13
- import { Publisher } from "../publisher/service";
14
+ import * as Metrics from "../metrics";
15
+ import { publishBatch } from "../publisher/instrumented";
14
16
  import { Attr, SpanName, annotateError } from "../telemetry";
15
17
 
16
18
  export type QueuedWebhookBatch = {
@@ -52,7 +54,7 @@ const makeRouteContext = (
52
54
  rawBody: Uint8Array,
53
55
  payload: unknown,
54
56
  batches: Array<QueuedWebhookBatch>,
55
- ): import("../core/types").WebhookRouteContext<ReadonlyArray<ResourceDefinition>, unknown> => ({
57
+ ): WebhookRouteContext<ReadonlyArray<ResourceDefinition>, unknown> => ({
56
58
  request,
57
59
  rawBody,
58
60
  payload,
@@ -86,21 +88,19 @@ const compactBatches = (batches: ReadonlyArray<QueuedWebhookBatch>) => {
86
88
  }));
87
89
  };
88
90
 
89
- const publishBatches = (batches: ReadonlyArray<QueuedWebhookBatch>) =>
91
+ const publishBatches = (
92
+ batches: ReadonlyArray<QueuedWebhookBatch>,
93
+ options: { readonly connectorName: string },
94
+ ) =>
90
95
  Effect.gen(function* () {
91
- const publisher = yield* Publisher;
92
96
  yield* Effect.forEach(
93
97
  batches,
94
98
  (batch) =>
95
- Effect.gen(function* () {
96
- const ack = yield* publisher.publish({
97
- resource: batch.resource,
98
- source: "webhook",
99
- batch: batch.batch,
100
- });
101
- if (ack.status === "rejected") {
102
- return yield* Effect.fail(new ConnectorError({ message: ack.reason }));
103
- }
99
+ publishBatch({
100
+ connector: options.connectorName,
101
+ resource: batch.resource,
102
+ source: "webhook",
103
+ batch: batch.batch,
104
104
  }),
105
105
  { concurrency: "unbounded" },
106
106
  );
@@ -114,11 +114,11 @@ const readRawBody = (request: HttpServerRequest.HttpServerRequest) =>
114
114
 
115
115
  const parseJson = (rawBody: Uint8Array) =>
116
116
  Effect.try({
117
- try: () => JSON.parse(new TextDecoder().decode(rawBody)) as unknown,
117
+ try: () => JSON.parse(new TextDecoder().decode(rawBody)),
118
118
  catch: (cause) => new ConnectorError({ message: "Invalid webhook JSON", cause }),
119
119
  });
120
120
 
121
- const makeHandler = (route: WebhookRoute) =>
121
+ const makeHandler = (route: WebhookRoute, options: { readonly connectorName: string }) =>
122
122
  Effect.gen(function* () {
123
123
  const request = yield* HttpServerRequest.HttpServerRequest;
124
124
  const collected: Array<QueuedWebhookBatch> = [];
@@ -130,6 +130,11 @@ const makeHandler = (route: WebhookRoute) =>
130
130
  }),
131
131
  );
132
132
  if (rawBodyResult._tag === "Error") {
133
+ yield* Metrics.recordWebhookRequest({
134
+ connector: options.connectorName,
135
+ path: String(route.path),
136
+ outcome: "read_error",
137
+ });
133
138
  return badRequest(rawBodyResult.message);
134
139
  }
135
140
 
@@ -140,6 +145,11 @@ const makeHandler = (route: WebhookRoute) =>
140
145
  }),
141
146
  );
142
147
  if (jsonResult._tag === "Error") {
148
+ yield* Metrics.recordWebhookRequest({
149
+ connector: options.connectorName,
150
+ path: String(route.path),
151
+ outcome: "invalid_json",
152
+ });
143
153
  return badRequest(jsonResult.message);
144
154
  }
145
155
 
@@ -150,6 +160,11 @@ const makeHandler = (route: WebhookRoute) =>
150
160
  }),
151
161
  );
152
162
  if (payloadResult._tag === "Error") {
163
+ yield* Metrics.recordWebhookRequest({
164
+ connector: options.connectorName,
165
+ path: String(route.path),
166
+ outcome: "invalid_payload",
167
+ });
153
168
  return badRequest(payloadResult.message);
154
169
  }
155
170
 
@@ -166,21 +181,38 @@ const makeHandler = (route: WebhookRoute) =>
166
181
  if (route.ackMode === "after-enqueue") {
167
182
  const queue = yield* WebhookQueue;
168
183
  yield* Queue.offerAll(queue.queue, batches);
184
+ const depth = yield* Queue.size(queue.queue);
185
+ yield* Metrics.setWebhookQueueDepth(options.connectorName, depth);
186
+ yield* Metrics.recordWebhookRequest({
187
+ connector: options.connectorName,
188
+ path: String(route.path),
189
+ outcome: response.status >= 400 ? "rejected" : "ok",
190
+ });
169
191
  return response;
170
192
  }
171
193
 
172
194
  yield* Effect.withSpan(
173
- publishBatches(batches).pipe(
195
+ publishBatches(batches, { connectorName: options.connectorName }).pipe(
174
196
  Effect.tapError((error) => annotateError("webhook_publish", error)),
175
197
  ),
176
198
  SpanName.webhookHandle,
177
199
  { attributes: { [Attr.webhookPath]: route.path } },
178
200
  );
179
201
 
202
+ yield* Metrics.recordWebhookRequest({
203
+ connector: options.connectorName,
204
+ path: String(route.path),
205
+ outcome: response.status >= 400 ? "rejected" : "ok",
206
+ });
180
207
  return response;
181
208
  }).pipe(
182
209
  Effect.catchCause((cause) =>
183
- Effect.logWarning(`Webhook handler error: ${Cause.pretty(cause)}`).pipe(
210
+ Metrics.recordWebhookRequest({
211
+ connector: options.connectorName,
212
+ path: String(route.path),
213
+ outcome: "handler_error",
214
+ }).pipe(
215
+ Effect.andThen(Effect.logWarning(`Webhook handler error: ${Cause.pretty(cause)}`)),
184
216
  Effect.andThen(
185
217
  Effect.succeed(
186
218
  HttpServerResponse.jsonUnsafe(
@@ -193,7 +225,10 @@ const makeHandler = (route: WebhookRoute) =>
193
225
  ),
194
226
  );
195
227
 
196
- export const router = (routes: ReadonlyArray<WebhookRoute>) =>
228
+ export const router = (
229
+ routes: ReadonlyArray<WebhookRoute>,
230
+ options: { readonly connectorName: string },
231
+ ) =>
197
232
  HttpRouter.addAll(
198
- routes.map((route) => HttpRouter.route("POST", route.path, makeHandler(route))),
233
+ routes.map((route) => HttpRouter.route("POST", route.path, makeHandler(route, options))),
199
234
  );
@@ -1,32 +0,0 @@
1
- import { t as __export } from "./chunk-Bp6m_JJh.js";
2
- import { n as run } from "./ingestion-CMCroK7Y.js";
3
- import { DateTime, Effect } from "effect";
4
- import { NodeHttpServer } from "@effect/platform-node";
5
- import { HttpRouter } from "effect/unstable/http";
6
- import { createServer } from "node:http";
7
-
8
- //#region src/connector-app/start.ts
9
- const start = (app, options) => Effect.gen(function* () {
10
- const initialCutoff = options.initialCutoff ?? (yield* DateTime.now.pipe(Effect.map(DateTime.formatIso)));
11
- const routes = app.webhooks?.map((route) => route.path) ?? [];
12
- yield* Effect.logInfo("webhook server ready").pipe(Effect.annotateLogs({
13
- port: options.port,
14
- routes
15
- }));
16
- return yield* run(app, {
17
- initialCutoff,
18
- webhook: {
19
- routes: app.webhooks ?? [],
20
- healthPath: options.healthPath ?? "/health",
21
- disableHttpLogger: options.disableHttpLogger ?? true
22
- }
23
- }).pipe(Effect.provide(NodeHttpServer.layer(createServer, { port: options.port })));
24
- });
25
-
26
- //#endregion
27
- //#region src/connector-app/index.ts
28
- var connector_app_exports = /* @__PURE__ */ __export({ start: () => start });
29
-
30
- //#endregion
31
- export { start as n, connector_app_exports as t };
32
- //# sourceMappingURL=connector-app-CvqbfN6R.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"connector-app-CvqbfN6R.js","names":["runIngestion"],"sources":["../src/connector-app/start.ts","../src/connector-app/index.ts"],"sourcesContent":["import { NodeHttpServer } from \"@effect/platform-node\";\nimport { DateTime, Effect } from \"effect\";\nimport { HttpRouter } from \"effect/unstable/http\";\nimport { createServer } from \"node:http\";\n\nimport type { ConnectorDefinition } from \"../core\";\nimport type { Cursor } from \"../core/types\";\n\nimport { run as runIngestion } from \"../ingestion\";\n\nexport type App = ConnectorDefinition;\n\nexport type StartOptions = {\n readonly port: number;\n readonly initialCutoff?: Cursor.Value;\n readonly healthPath?: HttpRouter.PathInput;\n readonly disableHttpLogger?: boolean;\n};\n\nexport const start = (app: App, options: StartOptions) =>\n Effect.gen(function* () {\n const initialCutoff =\n options.initialCutoff ?? (yield* DateTime.now.pipe(Effect.map(DateTime.formatIso)));\n const routes = app.webhooks?.map((route) => route.path) ?? [];\n\n yield* Effect.logInfo(\"webhook server ready\").pipe(\n Effect.annotateLogs({ port: options.port, routes }),\n );\n\n return yield* runIngestion(app, {\n initialCutoff,\n webhook: {\n routes: app.webhooks ?? [],\n healthPath: options.healthPath ?? \"/health\",\n disableHttpLogger: options.disableHttpLogger ?? true,\n },\n }).pipe(Effect.provide(NodeHttpServer.layer(createServer, { port: options.port })));\n });\n","export { start, type App, type StartOptions } from \"./start\";\n"],"mappings":";;;;;;;;AAmBA,MAAa,SAAS,KAAU,YAC9B,OAAO,IAAI,aAAa;CACtB,MAAM,gBACJ,QAAQ,kBAAkB,OAAO,SAAS,IAAI,KAAK,OAAO,IAAI,SAAS,UAAU,CAAC;CACpF,MAAM,SAAS,IAAI,UAAU,KAAK,UAAU,MAAM,KAAK,IAAI,EAAE;AAE7D,QAAO,OAAO,QAAQ,uBAAuB,CAAC,KAC5C,OAAO,aAAa;EAAE,MAAM,QAAQ;EAAM;EAAQ,CAAC,CACpD;AAED,QAAO,OAAOA,IAAa,KAAK;EAC9B;EACA,SAAS;GACP,QAAQ,IAAI,YAAY,EAAE;GAC1B,YAAY,QAAQ,cAAc;GAClC,mBAAmB,QAAQ,qBAAqB;GACjD;EACF,CAAC,CAAC,KAAK,OAAO,QAAQ,eAAe,MAAM,cAAc,EAAE,MAAM,QAAQ,MAAM,CAAC,CAAC,CAAC;EACnF"}
@@ -1,21 +0,0 @@
1
- import { t as ConnectorError } from "./index-DyuT2jzP.js";
2
- import { g as ResourceState } from "./types-ZNrA-Dn0.js";
3
- import { Context, Effect, Layer } from "effect";
4
-
5
- //#region src/state-store/service.d.ts
6
- interface StateStoreService {
7
- readonly getResourceState: (resource: string) => Effect.Effect<ResourceState | undefined, ConnectorError>;
8
- readonly setResourceState: (resource: string, state: ResourceState) => Effect.Effect<void, ConnectorError>;
9
- }
10
- declare const StateStore_base: Context.ServiceClass<StateStore, "@useairfoil/connector-kit/StateStore", StateStoreService>;
11
- declare class StateStore extends StateStore_base {}
12
- //#endregion
13
- //#region src/state-store/memory.d.ts
14
- /** In-memory resource state store backed by a `Ref<Map>`. Suitable for development and tests. */
15
- declare const layerMemory: Layer.Layer<StateStore>;
16
- declare namespace index_d_exports {
17
- export { StateStore, StateStoreService, layerMemory };
18
- }
19
- //#endregion
20
- export { StateStoreService as i, layerMemory as n, StateStore as r, index_d_exports as t };
21
- //# sourceMappingURL=index-DaNW63Mv.d.ts.map
@@ -1,171 +0,0 @@
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
@@ -1 +0,0 @@
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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"}
@@ -1,113 +0,0 @@
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
@@ -1 +0,0 @@
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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"}
@@ -1,31 +0,0 @@
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
@@ -1 +0,0 @@
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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"telemetry-p0dE5Mdu.js","names":["defaultRedactedHeaders: ReadonlyArray<string | RegExp>"],"sources":["../src/publisher/service.ts","../src/telemetry.ts"],"sourcesContent":["import type * as Wings from \"@useairfoil/wings\";\nimport type * as Effect from \"effect/Effect\";\n\nimport { Context } from \"effect\";\n\nimport type { ResourceBatch } from \"../core/types\";\nimport type { ConnectorError } from \"../errors\";\n\nexport type PublishSource = \"backfill\" | \"changes\" | \"webhook\";\n\nexport type PublishAck =\n | {\n readonly status: \"accepted\";\n readonly resource: string;\n readonly partition?: Wings.PartitionValue.PartitionValue;\n }\n | {\n readonly status: \"rejected\";\n readonly resource: string;\n readonly reason: string;\n readonly rejectedRows?: number;\n readonly partition?: Wings.PartitionValue.PartitionValue;\n };\n\nexport type PublishOptions = {\n readonly resource: string;\n readonly source: PublishSource;\n readonly batch: ResourceBatch;\n};\n\nexport interface PublisherService {\n readonly publish: (options: PublishOptions) => Effect.Effect<PublishAck, ConnectorError>;\n}\n\nexport class Publisher extends Context.Service<Publisher, PublisherService>()(\n \"@useairfoil/connector-kit/Publisher\",\n) {}\n","import { Cause, Config, Effect, Layer, Option } from \"effect\";\nimport { Headers } from \"effect/unstable/http\";\nimport * as Observability from \"effect/unstable/observability\";\n\n/** Canonical span names emitted by connector-kit runtime instrumentation. */\nexport const SpanName = {\n batchProcess: \"connector.batch.process\",\n webhookDecode: \"connector.webhook.decode\",\n webhookHandle: \"connector.webhook.handle\",\n publish: \"connector.publish\",\n stateSet: \"connector.state.set\",\n apiFetch: \"connector.api.fetch\",\n} as const;\n\n/** Canonical span attribute names. Keep these stable for collector queries. */\nexport const Attr = {\n connectorName: \"airfoil.connector.name\",\n connectorResourcesCount: \"airfoil.connector.resources.count\",\n resourceName: \"airfoil.resource.name\",\n resourceSource: \"airfoil.resource.source\",\n batchMutations: \"airfoil.batch.mutations\",\n webhookPath: \"airfoil.webhook.path\",\n publisherSuccess: \"airfoil.publisher.success\",\n stateKey: \"airfoil.state.key\",\n apiPath: \"airfoil.api.path\",\n errorPhase: \"airfoil.error.phase\",\n errorType: \"airfoil.error.type\",\n errorMessage: \"airfoil.error.message\",\n errorDetails: \"airfoil.error.details\",\n} as const;\n\n/** Span event names for high-cardinality reproduction details. */\nexport const EventName = {\n batchCheckpoint: \"airfoil.batch.checkpoint\",\n} as const;\n\n/** Event-only attributes. These are intentionally not span attributes. */\nexport const EventAttr = {\n batchCursor: \"airfoil.batch.cursor\",\n} as const;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null;\n\nconst maxErrorMessageLength = 500;\n\nconst OtlpEnvConfig = Config.all({\n enabled: Config.boolean(\"OTEL_ENABLED\").pipe(Config.withDefault(false)),\n baseUrl: Config.option(Config.string(\"OTEL_EXPORTER_OTLP_ENDPOINT\")),\n rawHeaders: Config.option(Config.string(\"OTEL_EXPORTER_OTLP_HEADERS\")),\n});\n\nconst defaultRedactedHeaders: ReadonlyArray<string | RegExp> = [\n \"authorization\",\n \"cookie\",\n \"set-cookie\",\n \"x-api-key\",\n /api[-_]?key/i,\n /secret/i,\n /signature/i,\n /token/i,\n];\n\nconst parseOtelHeaders = (value: string): Record<string, string> =>\n Object.fromEntries(\n value\n .split(\",\")\n .map((entry) => entry.trim())\n .filter(Boolean)\n .flatMap((entry) => {\n const separator = entry.indexOf(\"=\");\n if (separator < 1) return [];\n return [[entry.slice(0, separator).trim(), entry.slice(separator + 1).trim()]];\n }),\n );\n\nconst appendPath = (baseUrl: string, path: string) =>\n `${baseUrl.replace(/\\/$/, \"\")}/${path.replace(/^\\//, \"\")}`;\n\n/**\n * Best-effort stable error classifier for span metadata.\n *\n * Effect/Data tagged errors expose `_tag`; that is the most useful value for\n * connector bugs (`ConnectorError`, `InvalidWebhookPayloadError`, etc.). For\n * non-tagged errors we fall back to the JS `Error.name`, then constructor name,\n * then the primitive type.\n */\nconst errorType = (error: unknown) => {\n if (isRecord(error) && typeof error._tag === \"string\") {\n return String(error._tag);\n }\n if (error instanceof Error) {\n return error.name;\n }\n if (isRecord(error) && typeof error.constructor?.name === \"string\") {\n return error.constructor.name;\n }\n return typeof error;\n};\n\n/**\n * Human-readable error summary for span metadata.\n *\n * Tagged errors in this package carry a `message` field, so this preserves the\n * important diagnostic text. Unknown object failures are intentionally reduced\n * to their type; full structured error payloads belong in logs, not span attrs.\n */\nconst errorMessage = (error: unknown) => {\n const message = (() => {\n if (isRecord(error) && typeof error.message === \"string\") {\n return error.message;\n }\n if (typeof error === \"string\") {\n return error;\n }\n return errorType(error);\n })();\n\n if (message.length > maxErrorMessageLength) {\n return `${message.slice(0, maxErrorMessageLength)}...`;\n }\n\n return message;\n};\n\nconst errorDetails = (error: unknown): string | undefined => {\n if (!isRecord(error) || error.cause === undefined) return undefined;\n return Cause.pretty(Cause.die(error.cause));\n};\n\n/** Annotates the active span with phase-aware error metadata before re-failing. */\nexport const annotateError = (phase: string, error: unknown) => {\n const details = errorDetails(error);\n return Effect.annotateCurrentSpan({\n [Attr.errorPhase]: phase,\n [Attr.errorType]: errorType(error),\n [Attr.errorMessage]: errorMessage(error),\n ...(details !== undefined ? { [Attr.errorDetails]: details } : {}),\n });\n};\n\n/** Adds an event to the active span, or no-ops when no span is active. */\nexport const addCurrentSpanEvent = (name: string, attributes?: Record<string, unknown>) =>\n Effect.currentSpan.pipe(\n Effect.flatMap((span) =>\n Effect.clockWith((clock) =>\n Effect.sync(() => span.event(name, clock.currentTimeNanosUnsafe(), attributes)),\n ),\n ),\n Effect.ignore,\n );\n\n/** Transport configuration for OTLP trace export. All fields are direct values. */\nexport type OtlpTracingConfig = {\n readonly enabled?: boolean;\n readonly endpoint?: string;\n readonly headers?: Record<string, string>;\n};\n\n/** Runtime options — not config-wrappable since RegExp cannot live in Config. */\nexport type OtlpTracingOptions = {\n /** Additional connector-specific header names or regexes to redact from HTTP logs/traces. */\n readonly redactedHeaders?: ReadonlyArray<string | RegExp>;\n};\n\nconst buildOtlpTracingLayer = (config: OtlpTracingConfig, options: OtlpTracingOptions) =>\n Effect.gen(function* () {\n const allRedacted = [...defaultRedactedHeaders, ...(options.redactedHeaders ?? [])];\n const RedactionLayer = Layer.succeed(Headers.CurrentRedactedNames)(allRedacted);\n\n if (!config.enabled) {\n return RedactionLayer;\n }\n\n if (!config.endpoint) {\n return yield* Effect.fail(new Error(\"enabled=true requires endpoint\"));\n }\n\n yield* Effect.logInfo(\"telemetry enabled\").pipe(\n Effect.annotateLogs({\n endpoint: config.endpoint,\n headers: config.headers ? Object.keys(config.headers) : [],\n }),\n );\n\n return Layer.mergeAll(\n RedactionLayer,\n Observability.OtlpTracer.layer({\n url: appendPath(config.endpoint, \"/v1/traces\"),\n headers: config.headers,\n }).pipe(Layer.provide(Observability.OtlpSerialization.layerJson)),\n );\n });\n\n/**\n * Trace-only OTLP export with sensitive HTTP header redaction.\n * Takes direct config values — no `ConfigProvider` required.\n */\nexport const layer = (config: OtlpTracingConfig = {}, options: OtlpTracingOptions = {}) =>\n Layer.unwrap(buildOtlpTracingLayer(config, options));\n\n/**\n * Trace-only OTLP export with sensitive HTTP header redaction.\n * Takes Effect Config-wrapped values — reads from `ConfigProvider`.\n *\n * @example\n * Telemetry.layerConfig({\n * enabled: Config.boolean(\"MY_OTEL_ENABLED\"),\n * endpoint: Config.string(\"MY_COLLECTOR_URL\"),\n * })\n */\nexport const layerConfig = (\n config: Config.Wrap<OtlpTracingConfig>,\n options: OtlpTracingOptions = {},\n) =>\n Layer.unwrap(\n Effect.gen(function* () {\n const resolved = yield* Config.unwrap(config);\n return yield* buildOtlpTracingLayer(resolved, options);\n }),\n );\n\n/**\n * Trace-only OTLP export with sensitive HTTP header redaction.\n * Zero-config shortcut that reads the standard `OTEL_*` env var names.\n *\n * Reads `OTEL_ENABLED`, `OTEL_EXPORTER_OTLP_ENDPOINT`, and\n * `OTEL_EXPORTER_OTLP_HEADERS`. Effect's OTLP resource handling reads\n * `OTEL_SERVICE_NAME`, `OTEL_SERVICE_VERSION`, and `OTEL_RESOURCE_ATTRIBUTES`.\n */\nexport const layerOtlpTracing = (options: OtlpTracingOptions = {}) =>\n Layer.unwrap(\n Effect.gen(function* () {\n const raw = yield* OtlpEnvConfig;\n return yield* buildOtlpTracingLayer(\n {\n enabled: raw.enabled,\n endpoint: Option.getOrUndefined(raw.baseUrl),\n headers: Option.isSome(raw.rawHeaders)\n ? parseOtelHeaders(raw.rawHeaders.value)\n : undefined,\n },\n options,\n );\n }),\n );\n"],"mappings":"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"}