@useairfoil/connector-kit 0.4.0 → 0.5.1

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 (117) hide show
  1. package/METRICS.md +78 -0
  2. package/README.md +450 -88
  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/chunk-Bp6m_JJh.js +13 -0
  8. package/dist/connector-app/index.d.ts +14 -0
  9. package/dist/connector-app/index.js +11 -0
  10. package/dist/connector-app-d7Z-SZLQ.js +64 -0
  11. package/dist/connector-app-d7Z-SZLQ.js.map +1 -0
  12. package/dist/constants-BRhIX_rz.js +27 -0
  13. package/dist/constants-BRhIX_rz.js.map +1 -0
  14. package/dist/errors/index.d.ts +2 -0
  15. package/dist/errors/index.js +3 -0
  16. package/dist/errors-DHWylqRZ.js +8 -0
  17. package/dist/errors-DHWylqRZ.js.map +1 -0
  18. package/dist/index-Byr0Lf0C.d.ts +25 -0
  19. package/dist/index-CBT-JX-T.d.ts +173 -0
  20. package/dist/index-CTHVudvQ.d.ts +41 -0
  21. package/dist/index-CTLYQ74P.d.ts +80 -0
  22. package/dist/index-Coh_SkBs.d.ts +95 -0
  23. package/dist/index-D54ySAKn.d.ts +14 -0
  24. package/dist/index-DtJX_tic.d.ts +153 -0
  25. package/dist/index-J42AZaXU.d.ts +38 -0
  26. package/dist/index-fIIBVAai.d.ts +25 -0
  27. package/dist/index-o1-Z3n7V.d.ts +55 -0
  28. package/dist/index.d.ts +14 -165
  29. package/dist/index.js +50 -228
  30. package/dist/index.js.map +1 -1
  31. package/dist/ingestion/index.d.ts +6 -0
  32. package/dist/ingestion/index.js +10 -0
  33. package/dist/ingestion-BQo4cbGw.js +200 -0
  34. package/dist/ingestion-BQo4cbGw.js.map +1 -0
  35. package/dist/manifest/index.d.ts +2 -0
  36. package/dist/manifest/index.js +4 -0
  37. package/dist/manifest-ByE6YPG0.js +221 -0
  38. package/dist/manifest-ByE6YPG0.js.map +1 -0
  39. package/dist/metrics-CVAk1ALR.d.ts +53 -0
  40. package/dist/metrics-CWPy2Aqc.js +96 -0
  41. package/dist/metrics-CWPy2Aqc.js.map +1 -0
  42. package/dist/metrics.d.ts +4 -0
  43. package/dist/metrics.js +3 -0
  44. package/dist/publisher/index.d.ts +5 -0
  45. package/dist/publisher/index.js +6 -0
  46. package/dist/publisher-BYT3RLVO.js +131 -0
  47. package/dist/publisher-BYT3RLVO.js.map +1 -0
  48. package/dist/runtime-config/index.d.ts +2 -0
  49. package/dist/runtime-config/index.js +4 -0
  50. package/dist/runtime-config-BpB-d_bF.js +51 -0
  51. package/dist/runtime-config-BpB-d_bF.js.map +1 -0
  52. package/dist/server-CXTQ7odl.js +183 -0
  53. package/dist/server-CXTQ7odl.js.map +1 -0
  54. package/dist/service-DuFiQwid.js +191 -0
  55. package/dist/service-DuFiQwid.js.map +1 -0
  56. package/dist/service-IE5qaf9x.d.ts +32 -0
  57. package/dist/state-store/index.d.ts +3 -0
  58. package/dist/state-store/index.js +5 -0
  59. package/dist/state-store-yl6LiTQ0.js +198 -0
  60. package/dist/state-store-yl6LiTQ0.js.map +1 -0
  61. package/dist/status/index.d.ts +5 -0
  62. package/dist/status/index.js +6 -0
  63. package/dist/status-B-oZBc33.js +84 -0
  64. package/dist/status-B-oZBc33.js.map +1 -0
  65. package/dist/types-B35J5YPu.d.ts +174 -0
  66. package/dist/webhook/index.d.ts +6 -0
  67. package/dist/webhook/index.js +8 -0
  68. package/dist/webhook-DI-Afxkg.js +13 -0
  69. package/dist/webhook-DI-Afxkg.js.map +1 -0
  70. package/package.json +66 -8
  71. package/src/auth/index.ts +1 -0
  72. package/src/auth/token-cache.ts +54 -0
  73. package/src/connector-app/check.ts +76 -0
  74. package/src/connector-app/index.ts +2 -0
  75. package/src/connector-app/start.ts +42 -0
  76. package/src/core/builder.ts +81 -9
  77. package/src/core/index.ts +22 -0
  78. package/src/core/types.ts +204 -76
  79. package/src/errors/index.ts +1 -0
  80. package/src/index.ts +13 -28
  81. package/src/ingestion/engine.ts +409 -174
  82. package/src/ingestion/index.ts +1 -0
  83. package/src/manifest/config.ts +232 -0
  84. package/src/manifest/index.ts +14 -0
  85. package/src/manifest/manifest.ts +31 -0
  86. package/src/manifest/number-schema.ts +17 -0
  87. package/src/manifest/schema.ts +160 -0
  88. package/src/metrics.ts +128 -0
  89. package/src/publisher/console.ts +18 -0
  90. package/src/publisher/index.ts +7 -0
  91. package/src/publisher/instrumented.ts +68 -0
  92. package/src/publisher/service.ts +30 -13
  93. package/src/publisher/wings.ts +159 -72
  94. package/src/runtime/context.ts +15 -0
  95. package/src/runtime-config/constants.ts +27 -0
  96. package/src/runtime-config/error.ts +13 -0
  97. package/src/runtime-config/index.ts +3 -0
  98. package/src/runtime-config/layer.ts +56 -0
  99. package/src/state-store/index.ts +3 -0
  100. package/src/state-store/keys.ts +22 -0
  101. package/src/state-store/layer-key-value-store.ts +132 -0
  102. package/src/state-store/layer-memory.ts +11 -0
  103. package/src/state-store/layer-sql.ts +50 -0
  104. package/src/state-store/schema.ts +57 -0
  105. package/src/state-store/service.ts +51 -0
  106. package/src/state-store/state.ts +17 -0
  107. package/src/status/index.ts +2 -0
  108. package/src/status/route.ts +77 -0
  109. package/src/status/types.ts +33 -0
  110. package/src/telemetry.ts +288 -0
  111. package/src/webhook/index.ts +8 -0
  112. package/src/webhook/server.ts +209 -39
  113. package/src/webhook/types.ts +6 -17
  114. package/src/ingestion/state-store.ts +0 -36
  115. package/src/streams/pull-stream.ts +0 -54
  116. package/src/streams/webhook-queue.ts +0 -14
  117. /package/src/{core/errors.ts → errors/connector-error.ts} +0 -0
@@ -1,51 +1,218 @@
1
- import { Cause, Data, Effect, Schema } from "effect";
1
+ import { Cause, Context, Effect, Queue, Schema } from "effect";
2
2
  import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
3
3
 
4
- import type { WebhookRoute } from "./types";
4
+ import type {
5
+ ResourceBatch,
6
+ ResourceDefinition,
7
+ ResourceMutation,
8
+ WebhookHandler,
9
+ WebhookRoute,
10
+ WebhookRouteContext,
11
+ } from "../core/types";
5
12
 
6
- class InvalidWebhookPayloadError extends Data.TaggedError("InvalidWebhookPayloadError")<{
7
- readonly message: string;
8
- }> {}
13
+ import { ConnectorError } from "../errors";
14
+ import * as Metrics from "../metrics";
15
+ import { publishBatch } from "../publisher/instrumented";
16
+ import { Attr, SpanName, annotateError } from "../telemetry";
9
17
 
10
- const makeHandler = <TPayload>(route: WebhookRoute<TPayload>) =>
18
+ export type QueuedWebhookBatch = {
19
+ readonly resource: string;
20
+ readonly batch: ResourceBatch;
21
+ };
22
+
23
+ export class WebhookQueue extends Context.Service<
24
+ WebhookQueue,
25
+ {
26
+ readonly queue: Queue.Queue<QueuedWebhookBatch>;
27
+ }
28
+ >()("@useairfoil/connector-kit/WebhookQueue") {}
29
+
30
+ const resourceBatch = <Row extends object>(
31
+ mutations: ReadonlyArray<ResourceMutation<Row>>,
32
+ ): ResourceBatch<Row> => ({
33
+ mutations,
34
+ });
35
+
36
+ const decodeAndHandleWebhook = <Row extends object, Payload>(
37
+ resourceName: string,
38
+ webhook: WebhookHandler<Row, Payload>,
39
+ payload: unknown,
40
+ ) =>
41
+ Schema.decodeUnknownEffect(webhook.schema)(payload).pipe(
42
+ Effect.mapError(
43
+ (cause) =>
44
+ new ConnectorError({
45
+ message: `Invalid webhook payload for resource ${resourceName}`,
46
+ cause,
47
+ }),
48
+ ),
49
+ Effect.flatMap((decoded) => webhook.handler({ payload: decoded })),
50
+ );
51
+
52
+ const makeRouteContext = (
53
+ request: HttpServerRequest.HttpServerRequest,
54
+ rawBody: Uint8Array,
55
+ payload: unknown,
56
+ batches: Array<QueuedWebhookBatch>,
57
+ ): WebhookRouteContext<ReadonlyArray<ResourceDefinition>, unknown> => ({
58
+ request,
59
+ rawBody,
60
+ payload,
61
+ to: (resource, payload) =>
62
+ Effect.gen(function* () {
63
+ if (!resource.webhook) {
64
+ return yield* Effect.fail(
65
+ new ConnectorError({
66
+ message: `Resource ${resource.name} does not define a webhook handler`,
67
+ }),
68
+ );
69
+ }
70
+
71
+ const mutations = yield* decodeAndHandleWebhook(resource.name, resource.webhook, payload);
72
+ batches.push({
73
+ resource: resource.name,
74
+ batch: resourceBatch(mutations),
75
+ });
76
+ }),
77
+ });
78
+
79
+ const compactBatches = (batches: ReadonlyArray<QueuedWebhookBatch>) => {
80
+ const grouped = new Map<string, ResourceMutation[]>();
81
+ for (const item of batches) {
82
+ const current = grouped.get(item.resource) ?? [];
83
+ grouped.set(item.resource, [...current, ...item.batch.mutations]);
84
+ }
85
+ return Array.from(grouped.entries()).map(([resource, mutations]) => ({
86
+ resource,
87
+ batch: resourceBatch(mutations),
88
+ }));
89
+ };
90
+
91
+ const publishBatches = (
92
+ batches: ReadonlyArray<QueuedWebhookBatch>,
93
+ options: { readonly connectorName: string },
94
+ ) =>
95
+ Effect.gen(function* () {
96
+ yield* Effect.forEach(
97
+ batches,
98
+ (batch) =>
99
+ publishBatch({
100
+ connector: options.connectorName,
101
+ resource: batch.resource,
102
+ source: "webhook",
103
+ batch: batch.batch,
104
+ }),
105
+ { concurrency: "unbounded" },
106
+ );
107
+ });
108
+
109
+ const badRequest = (message: string) =>
110
+ HttpServerResponse.jsonUnsafe({ ok: false, error: message }, { status: 400 });
111
+
112
+ const readRawBody = (request: HttpServerRequest.HttpServerRequest) =>
113
+ request.arrayBuffer.pipe(Effect.map((buffer) => new Uint8Array(buffer)));
114
+
115
+ const parseJson = (rawBody: Uint8Array) =>
116
+ Effect.try({
117
+ try: () => JSON.parse(new TextDecoder().decode(rawBody)),
118
+ catch: (cause) => new ConnectorError({ message: "Invalid webhook JSON", cause }),
119
+ });
120
+
121
+ const makeHandler = (route: WebhookRoute, options: { readonly connectorName: string }) =>
11
122
  Effect.gen(function* () {
12
123
  const request = yield* HttpServerRequest.HttpServerRequest;
13
- const rawBuffer = yield* request.arrayBuffer.pipe(
14
- Effect.mapError(
15
- () =>
16
- new InvalidWebhookPayloadError({
17
- message: "Failed to read request body",
18
- }),
19
- ),
124
+ const collected: Array<QueuedWebhookBatch> = [];
125
+
126
+ const rawBodyResult = yield* readRawBody(request).pipe(
127
+ Effect.match({
128
+ onFailure: () => ({ _tag: "Error" as const, message: "Failed to read webhook body" }),
129
+ onSuccess: (value) => ({ _tag: "Success" as const, value }),
130
+ }),
20
131
  );
21
- const rawBody = new Uint8Array(rawBuffer);
22
- const rawText = new TextDecoder().decode(rawBody);
23
- const rawJson = yield* Effect.try({
24
- try: () => JSON.parse(rawText) as unknown,
25
- catch: () => new InvalidWebhookPayloadError({ message: "Invalid JSON body" }),
26
- });
27
- const payload = yield* Schema.decodeUnknownEffect(route.schema)(rawJson).pipe(
28
- Effect.mapError(
29
- () =>
30
- new InvalidWebhookPayloadError({
31
- message: "Invalid webhook payload",
32
- }),
132
+ if (rawBodyResult._tag === "Error") {
133
+ yield* Metrics.recordWebhookRequest({
134
+ connector: options.connectorName,
135
+ path: String(route.path),
136
+ outcome: "read_error",
137
+ });
138
+ return badRequest(rawBodyResult.message);
139
+ }
140
+
141
+ const jsonResult = yield* parseJson(rawBodyResult.value).pipe(
142
+ Effect.match({
143
+ onFailure: (error) => ({ _tag: "Error" as const, message: error.message }),
144
+ onSuccess: (value) => ({ _tag: "Success" as const, value }),
145
+ }),
146
+ );
147
+ if (jsonResult._tag === "Error") {
148
+ yield* Metrics.recordWebhookRequest({
149
+ connector: options.connectorName,
150
+ path: String(route.path),
151
+ outcome: "invalid_json",
152
+ });
153
+ return badRequest(jsonResult.message);
154
+ }
155
+
156
+ const payloadResult = yield* Schema.decodeUnknownEffect(route.schema)(jsonResult.value).pipe(
157
+ Effect.match({
158
+ onFailure: () => ({ _tag: "Error" as const, message: "Invalid webhook payload" }),
159
+ onSuccess: (value) => ({ _tag: "Success" as const, value }),
160
+ }),
161
+ );
162
+ if (payloadResult._tag === "Error") {
163
+ yield* Metrics.recordWebhookRequest({
164
+ connector: options.connectorName,
165
+ path: String(route.path),
166
+ outcome: "invalid_payload",
167
+ });
168
+ return badRequest(payloadResult.message);
169
+ }
170
+
171
+ const response = yield* Effect.withSpan(
172
+ route
173
+ .handler(makeRouteContext(request, rawBodyResult.value, payloadResult.value, collected))
174
+ .pipe(Effect.tapError((error) => annotateError("webhook_handle", error))),
175
+ SpanName.webhookHandle,
176
+ { attributes: { [Attr.webhookPath]: route.path } },
177
+ );
178
+
179
+ const batches = compactBatches(collected);
180
+
181
+ if (route.ackMode === "after-enqueue") {
182
+ const queue = yield* WebhookQueue;
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
+ });
191
+ return response;
192
+ }
193
+
194
+ yield* Effect.withSpan(
195
+ publishBatches(batches, { connectorName: options.connectorName }).pipe(
196
+ Effect.tapError((error) => annotateError("webhook_publish", error)),
33
197
  ),
198
+ SpanName.webhookHandle,
199
+ { attributes: { [Attr.webhookPath]: route.path } },
34
200
  );
35
- yield* route.handle(payload, request, rawBody);
36
- // unsafeJson serializes synchronously — no Effect, no HttpBodyError
37
- return HttpServerResponse.jsonUnsafe({ ok: true });
201
+
202
+ yield* Metrics.recordWebhookRequest({
203
+ connector: options.connectorName,
204
+ path: String(route.path),
205
+ outcome: response.status >= 400 ? "rejected" : "ok",
206
+ });
207
+ return response;
38
208
  }).pipe(
39
- Effect.catchTag("InvalidWebhookPayloadError", () =>
40
- Effect.succeed(
41
- HttpServerResponse.jsonUnsafe(
42
- { ok: false, error: "Invalid webhook payload" },
43
- { status: 400 },
44
- ),
45
- ),
46
- ),
47
209
  Effect.catchCause((cause) =>
48
- 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)}`)),
49
216
  Effect.andThen(
50
217
  Effect.succeed(
51
218
  HttpServerResponse.jsonUnsafe(
@@ -58,7 +225,10 @@ const makeHandler = <TPayload>(route: WebhookRoute<TPayload>) =>
58
225
  ),
59
226
  );
60
227
 
61
- export const buildWebhookRouter = <TPayload>(routes: ReadonlyArray<WebhookRoute<TPayload>>) =>
228
+ export const router = (
229
+ routes: ReadonlyArray<WebhookRoute>,
230
+ options: { readonly connectorName: string },
231
+ ) =>
62
232
  HttpRouter.addAll(
63
- routes.map((route) => HttpRouter.route("POST", route.path, makeHandler(route))),
233
+ routes.map((route) => HttpRouter.route("POST", route.path, makeHandler(route, options))),
64
234
  );
@@ -1,17 +1,6 @@
1
- /** biome-ignore-all lint/suspicious/noExplicitAny: Effect Schema encodes invariantly; `any` on the encoded type is intentional. */
2
-
3
- import type { Effect } from "effect";
4
- import type * as Schema from "effect/Schema";
5
- import type { HttpRouter, HttpServerRequest } from "effect/unstable/http";
6
-
7
- import type { ConnectorError } from "../core/errors";
8
-
9
- export type WebhookRoute<TPayload> = {
10
- readonly path: HttpRouter.PathInput;
11
- readonly schema: Schema.Schema<TPayload>;
12
- readonly handle: (
13
- payload: TPayload,
14
- request: HttpServerRequest.HttpServerRequest,
15
- rawBody?: Uint8Array,
16
- ) => Effect.Effect<void, ConnectorError>;
17
- };
1
+ export type {
2
+ WebhookAckMode,
3
+ WebhookRoute,
4
+ WebhookRouteContext,
5
+ WebhookRouteInput,
6
+ } from "../core/types";
@@ -1,36 +0,0 @@
1
- import { Context, Effect, Layer, Ref } from "effect";
2
-
3
- import type { ConnectorError } from "../core/errors";
4
- import type { Cursor, IngestionState } from "../core/types";
5
-
6
- export class StateStore extends Context.Service<
7
- StateStore,
8
- {
9
- readonly getState: (
10
- key: string,
11
- ) => Effect.Effect<IngestionState<Cursor> | undefined, ConnectorError>;
12
- readonly setState: (
13
- key: string,
14
- state: IngestionState<Cursor>,
15
- ) => Effect.Effect<void, ConnectorError>;
16
- }
17
- >()("StateStore") {}
18
-
19
- export const StateStoreInMemory = Layer.effect(StateStore)(
20
- Effect.gen(function* () {
21
- const ref = yield* Ref.make(new Map<string, IngestionState<Cursor>>());
22
-
23
- return {
24
- getState: (key) => Effect.map(Ref.get(ref), (map) => map.get(key)),
25
- setState: (key, state) =>
26
- Effect.map(
27
- Ref.update(ref, (map) => {
28
- const next = new Map(map);
29
- next.set(key, state);
30
- return next;
31
- }),
32
- () => undefined,
33
- ),
34
- };
35
- }),
36
- );
@@ -1,54 +0,0 @@
1
- import { Effect, Stream } from "effect";
2
-
3
- import type { ConnectorError } from "../core/errors";
4
- import type { Batch, Cursor } from "../core/types";
5
-
6
- export type PullPage<T> = {
7
- readonly cursor: Cursor;
8
- readonly rows: ReadonlyArray<T>;
9
- readonly hasMore: boolean;
10
- };
11
-
12
- export type PullFetcher<T, R = never> = (
13
- cursor: Cursor | undefined,
14
- ) => Effect.Effect<PullPage<T>, ConnectorError, R>;
15
-
16
- export type PullStreamOptions<T, R = never> = {
17
- readonly initialCursor?: Cursor;
18
- readonly fetchPage: PullFetcher<T, R>;
19
- };
20
-
21
- export const makePullStream = <T, R = never>(
22
- options: PullStreamOptions<T, R>,
23
- ): Stream.Stream<Batch<T>, ConnectorError, R> =>
24
- Stream.unfold({ cursor: options.initialCursor, done: false }, (state) =>
25
- Effect.gen(function* () {
26
- if (state.done) {
27
- return undefined;
28
- }
29
-
30
- let nextCursor: Cursor | undefined = state.cursor;
31
-
32
- while (true) {
33
- const page = yield* options.fetchPage(nextCursor);
34
-
35
- if (page.rows.length > 0) {
36
- const batch: Batch<T> = {
37
- cursor: page.cursor,
38
- rows: page.rows,
39
- };
40
-
41
- return [
42
- batch,
43
- page.hasMore ? { cursor: page.cursor, done: false } : { cursor: undefined, done: true },
44
- ] as const;
45
- }
46
-
47
- if (!page.hasMore) {
48
- return undefined;
49
- }
50
-
51
- nextCursor = page.cursor;
52
- }
53
- }),
54
- );
@@ -1,14 +0,0 @@
1
- import { Effect, Queue, Stream } from "effect";
2
-
3
- import type { ConnectorError } from "../core/errors";
4
- import type { Batch, WebhookStream } from "../core/types";
5
-
6
- export const makeWebhookQueue = <T>(options?: {
7
- readonly capacity?: number;
8
- }): Effect.Effect<WebhookStream<T>, never> =>
9
- Effect.gen(function* () {
10
- const capacity = options?.capacity ?? 1024;
11
- const queue = yield* Queue.bounded<Batch<T>>(capacity);
12
- const stream: Stream.Stream<Batch<T>, ConnectorError> = Stream.fromQueue(queue);
13
- return { queue, stream };
14
- });