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