@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.
- package/METRICS.md +78 -0
- package/README.md +190 -21
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.js +3 -0
- package/dist/auth-DvU_CZ7C.js +42 -0
- package/dist/auth-DvU_CZ7C.js.map +1 -0
- package/dist/connector-app/index.d.ts +14 -9
- package/dist/connector-app/index.js +9 -6
- package/dist/connector-app-d7Z-SZLQ.js +64 -0
- package/dist/connector-app-d7Z-SZLQ.js.map +1 -0
- package/dist/constants-BRhIX_rz.js +27 -0
- package/dist/constants-BRhIX_rz.js.map +1 -0
- package/dist/errors/index.d.ts +1 -1
- package/dist/{index-DK9YcR6W.d.ts → index-Byr0Lf0C.d.ts} +4 -4
- package/dist/index-CBT-JX-T.d.ts +173 -0
- package/dist/index-CTHVudvQ.d.ts +41 -0
- package/dist/index-Coh_SkBs.d.ts +95 -0
- package/dist/index-Cq2IzY7T.d.ts +80 -0
- package/dist/{index-DyuT2jzP.d.ts → index-D54ySAKn.d.ts} +1 -1
- package/dist/{index-CQJsFJs3.d.ts → index-DKWIh6M1.d.ts} +7 -5
- package/dist/{index-BlhJCpEk.d.ts → index-DtJX_tic.d.ts} +39 -10
- package/dist/{index-8KnVpoZp.d.ts → index-J42AZaXU.d.ts} +11 -5
- package/dist/index-o1-Z3n7V.d.ts +55 -0
- package/dist/index.d.ts +14 -9
- package/dist/index.js +14 -8
- package/dist/index.js.map +1 -1
- package/dist/ingestion/index.d.ts +5 -5
- package/dist/ingestion/index.js +7 -4
- package/dist/ingestion-BQo4cbGw.js +200 -0
- package/dist/ingestion-BQo4cbGw.js.map +1 -0
- package/dist/manifest/index.d.ts +2 -0
- package/dist/manifest/index.js +4 -0
- package/dist/manifest-ByE6YPG0.js +221 -0
- package/dist/manifest-ByE6YPG0.js.map +1 -0
- package/dist/metrics-CVAk1ALR.d.ts +53 -0
- package/dist/metrics-CWPy2Aqc.js +96 -0
- package/dist/metrics-CWPy2Aqc.js.map +1 -0
- package/dist/metrics.d.ts +4 -0
- package/dist/metrics.js +3 -0
- package/dist/publisher/index.d.ts +4 -4
- package/dist/publisher/index.js +3 -2
- package/dist/{publisher-DusgYvaV.js → publisher-BYT3RLVO.js} +2 -2
- package/dist/{publisher-DusgYvaV.js.map → publisher-BYT3RLVO.js.map} +1 -1
- package/dist/runtime-config/index.d.ts +2 -0
- package/dist/runtime-config/index.js +4 -0
- package/dist/runtime-config-BpB-d_bF.js +51 -0
- package/dist/runtime-config-BpB-d_bF.js.map +1 -0
- package/dist/server-CXTQ7odl.js +183 -0
- package/dist/server-CXTQ7odl.js.map +1 -0
- package/dist/{telemetry-p0dE5Mdu.js → service-DuFiQwid.js} +30 -10
- package/dist/service-DuFiQwid.js.map +1 -0
- package/dist/{service-B7g_lHMI.d.ts → service-IE5qaf9x.d.ts} +3 -3
- package/dist/state-store/index.d.ts +3 -4
- package/dist/state-store/index.js +4 -2
- package/dist/state-store-yl6LiTQ0.js +198 -0
- package/dist/state-store-yl6LiTQ0.js.map +1 -0
- package/dist/status/index.d.ts +5 -0
- package/dist/status/index.js +6 -0
- package/dist/status-B-oZBc33.js +84 -0
- package/dist/status-B-oZBc33.js.map +1 -0
- package/dist/{types-ZNrA-Dn0.d.ts → types-B35J5YPu.d.ts} +20 -6
- package/dist/webhook/index.d.ts +5 -4
- package/dist/webhook/index.js +5 -3
- package/dist/{webhook-BSA-tXtU.js → webhook-DI-Afxkg.js} +2 -2
- package/dist/{webhook-BSA-tXtU.js.map → webhook-DI-Afxkg.js.map} +1 -1
- package/package.json +36 -7
- package/src/auth/index.ts +1 -0
- package/src/auth/token-cache.ts +54 -0
- package/src/connector-app/check.ts +76 -0
- package/src/connector-app/index.ts +1 -0
- package/src/connector-app/start.ts +4 -0
- package/src/core/builder.ts +8 -3
- package/src/core/types.ts +20 -2
- package/src/index.ts +5 -0
- package/src/ingestion/engine.ts +246 -133
- package/src/manifest/config.ts +232 -0
- package/src/manifest/index.ts +14 -0
- package/src/manifest/manifest.ts +31 -0
- package/src/manifest/number-schema.ts +17 -0
- package/src/manifest/schema.ts +160 -0
- package/src/metrics.ts +128 -0
- package/src/publisher/instrumented.ts +68 -0
- package/src/runtime-config/constants.ts +27 -0
- package/src/runtime-config/error.ts +13 -0
- package/src/runtime-config/index.ts +3 -0
- package/src/runtime-config/layer.ts +56 -0
- package/src/state-store/index.ts +2 -1
- package/src/state-store/keys.ts +22 -0
- package/src/state-store/layer-key-value-store.ts +132 -0
- package/src/state-store/layer-memory.ts +11 -0
- package/src/state-store/layer-sql.ts +50 -0
- package/src/state-store/schema.ts +57 -0
- package/src/state-store/service.ts +39 -6
- package/src/state-store/state.ts +17 -0
- package/src/status/index.ts +2 -0
- package/src/status/route.ts +77 -0
- package/src/status/types.ts +33 -0
- package/src/telemetry.ts +46 -4
- package/src/webhook/server.ts +54 -19
- package/dist/connector-app-CvqbfN6R.js +0 -32
- package/dist/connector-app-CvqbfN6R.js.map +0 -1
- package/dist/index-DaNW63Mv.d.ts +0 -21
- package/dist/ingestion-CMCroK7Y.js +0 -171
- package/dist/ingestion-CMCroK7Y.js.map +0 -1
- package/dist/server-DAeFWT4o.js +0 -113
- package/dist/server-DAeFWT4o.js.map +0 -1
- package/dist/state-store-DG_4QpWG.js +0 -31
- package/dist/state-store-DG_4QpWG.js.map +0 -1
- package/dist/telemetry-p0dE5Mdu.js.map +0 -1
- package/src/state-store/memory.ts +0 -25
package/src/webhook/server.ts
CHANGED
|
@@ -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
|
|
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
|
-
):
|
|
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 = (
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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))
|
|
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
|
-
|
|
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 = (
|
|
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"}
|
package/dist/index-DaNW63Mv.d.ts
DELETED
|
@@ -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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"}
|
package/dist/server-DAeFWT4o.js
DELETED
|
@@ -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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"}
|