@useairfoil/connector-kit 0.6.0 → 0.6.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.
- package/METRICS.md +16 -17
- package/README.md +127 -442
- package/dist/annotations-BE74YmAO.d.mts +39 -0
- package/dist/auth/index.d.mts +2 -0
- package/dist/{auth-DvU_CZ7C.js → auth/index.mjs} +6 -8
- package/dist/auth/index.mjs.map +1 -0
- package/dist/connector-app/index.d.mts +2 -0
- package/dist/connector-app/index.mjs +2 -0
- package/dist/connector-app-C-L4QqG6.mjs +123 -0
- package/dist/connector-app-C-L4QqG6.mjs.map +1 -0
- package/dist/{constants-D113Ut1x.js → constants-D-GQzpHZ.mjs} +4 -5
- package/dist/constants-D-GQzpHZ.mjs.map +1 -0
- package/dist/demo/index.d.mts +2 -0
- package/dist/demo/index.mjs +2 -0
- package/dist/demo-B30Vlkk2.mjs +191 -0
- package/dist/demo-B30Vlkk2.mjs.map +1 -0
- package/dist/errors/index.d.mts +2 -0
- package/dist/errors/index.mjs +2 -0
- package/dist/{errors-DHWylqRZ.js → errors-pT0YVJU5.mjs} +2 -3
- package/dist/errors-pT0YVJU5.mjs.map +1 -0
- package/dist/iceberg/index.d.mts +3 -0
- package/dist/iceberg/index.mjs +3 -0
- package/dist/iceberg-D6L8I6vA.mjs +39 -0
- package/dist/iceberg-D6L8I6vA.mjs.map +1 -0
- package/dist/{index-VpHaZXZw.d.ts → index-BUHSGm9Q.d.mts} +11 -13
- package/dist/{index-DXa26Ry5.d.ts → index-BX8G7d41.d.mts} +1 -2
- package/dist/{index-bfjH7Yb0.d.ts → index-BfXpgxk9.d.mts} +9 -10
- package/dist/{index-BKw7jA2a.d.ts → index-CRVAwJ5-.d.mts} +10 -8
- package/dist/index-CUPP2A_m.d.mts +27 -0
- package/dist/{index-DYpEMJf-.d.ts → index-CgvFomO6.d.mts} +6 -7
- package/dist/index-DJhDp4Xc.d.mts +58 -0
- package/dist/index-Dk7urvpF.d.mts +76 -0
- package/dist/{index-DKoRwyFn.d.ts → index-DkVNO6ks.d.mts} +2 -5
- package/dist/{index-C5Ma7V7t.d.ts → index-NnCutIqM.d.mts} +62 -37
- package/dist/{index-CR9HJDdr.d.ts → index-eFuoMZZ3.d.mts} +18 -20
- package/dist/{index-ZSuiZX5H.d.ts → index-ygpml2kC.d.mts} +19 -31
- package/dist/index.d.mts +16 -0
- package/dist/index.mjs +45 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ingestion/index.d.mts +2 -0
- package/dist/ingestion/index.mjs +2 -0
- package/dist/ingestion-q0WwqUSt.mjs +275 -0
- package/dist/ingestion-q0WwqUSt.mjs.map +1 -0
- package/dist/ingestor/index.d.mts +3 -0
- package/dist/ingestor/index.mjs +4 -0
- package/dist/ingestor-D4L-HPkg.mjs +26 -0
- package/dist/ingestor-D4L-HPkg.mjs.map +1 -0
- package/dist/instrumented-Bhp7fyvh.mjs +29 -0
- package/dist/instrumented-Bhp7fyvh.mjs.map +1 -0
- package/dist/manifest/index.d.mts +2 -0
- package/dist/manifest/{index.js → index.mjs} +2 -4
- package/dist/{manifest-P7kBnch9.js → manifest-BGKxmKLy.mjs} +14 -19
- package/dist/manifest-BGKxmKLy.mjs.map +1 -0
- package/dist/{telemetry-j_nxwYfh.js → metrics-BaEX__MN.mjs} +170 -19
- package/dist/metrics-BaEX__MN.mjs.map +1 -0
- package/dist/{metrics-C4V1pm6v.d.ts → metrics-CVXFL7YT.d.mts} +18 -13
- package/dist/metrics.d.mts +2 -0
- package/dist/metrics.mjs +2 -0
- package/dist/{chunk-Bp6m_JJh.js → rolldown-runtime-D7D4PA-g.mjs} +4 -4
- package/dist/runtime-config/index.d.mts +2 -0
- package/dist/runtime-config/index.mjs +3 -0
- package/dist/runtime-config-Oa55gAew.mjs +97 -0
- package/dist/runtime-config-Oa55gAew.mjs.map +1 -0
- package/dist/server-CMnxz3h0.mjs +144 -0
- package/dist/server-CMnxz3h0.mjs.map +1 -0
- package/dist/service-DLkhuxYl.d.mts +27 -0
- package/dist/service-jWxxfEht.mjs +7 -0
- package/dist/service-jWxxfEht.mjs.map +1 -0
- package/dist/state-store/index.d.mts +2 -0
- package/dist/state-store/index.mjs +2 -0
- package/dist/{state-store-BdYulQOY.js → state-store-uFvHY7Ax.mjs} +15 -22
- package/dist/state-store-uFvHY7Ax.mjs.map +1 -0
- package/dist/status/{index.d.ts → index.d.mts} +1 -4
- package/dist/status/{index.js → index.mjs} +2 -6
- package/dist/{status-BTTd7AUR.js → status-CKb9IhQ9.mjs} +14 -16
- package/dist/status-CKb9IhQ9.mjs.map +1 -0
- package/dist/table-V0tgUC5i.mjs +370 -0
- package/dist/table-V0tgUC5i.mjs.map +1 -0
- package/dist/types-xZtSmrGA.d.mts +220 -0
- package/dist/webhook/index.d.mts +3 -0
- package/dist/webhook/index.mjs +12 -0
- package/dist/webhook/index.mjs.map +1 -0
- package/dist/wings-I56hGtNv.mjs +325 -0
- package/dist/wings-I56hGtNv.mjs.map +1 -0
- package/package.json +64 -51
- package/src/catalog/table.ts +201 -0
- package/src/connector-app/create-table.ts +47 -0
- package/src/connector-app/demo.ts +42 -0
- package/src/connector-app/index.ts +2 -0
- package/src/connector-app/start.ts +1 -1
- package/src/core/builder.ts +15 -23
- package/src/core/index.ts +11 -1
- package/src/core/types.ts +114 -39
- package/src/demo/definition.ts +68 -0
- package/src/demo/generate.ts +167 -0
- package/src/demo/index.ts +3 -0
- package/src/demo/run.ts +142 -0
- package/src/iceberg/annotations.ts +62 -0
- package/src/iceberg/error.ts +9 -0
- package/src/iceberg/index.ts +3 -0
- package/src/iceberg/table.ts +511 -0
- package/src/index.ts +3 -1
- package/src/ingestion/engine.ts +240 -60
- package/src/ingestor/arrow.ts +248 -0
- package/src/ingestor/console.ts +13 -0
- package/src/ingestor/index.ts +9 -0
- package/src/ingestor/instrumented.ts +61 -0
- package/src/ingestor/service.ts +30 -0
- package/src/ingestor/wings.ts +143 -0
- package/src/manifest/config.ts +3 -3
- package/src/metrics.ts +66 -20
- package/src/runtime-config/config.ts +6 -8
- package/src/runtime-config/constants.ts +2 -2
- package/src/runtime-config/index.ts +2 -7
- package/src/runtime-config/layer.ts +1 -1
- package/src/runtime-config/table-bindings.ts +48 -136
- package/src/state-store/layer-key-value-store.ts +8 -3
- package/src/state-store/layer-memory.ts +1 -1
- package/src/state-store/layer-sql.ts +4 -4
- package/src/state-store/schema.ts +2 -2
- package/src/state-store/state.ts +14 -2
- package/src/status/route.ts +2 -2
- package/src/telemetry.ts +10 -10
- package/src/webhook/server.ts +39 -40
- package/dist/auth/index.d.ts +0 -2
- package/dist/auth/index.js +0 -3
- package/dist/auth-DvU_CZ7C.js.map +0 -1
- package/dist/connector-app/index.d.ts +0 -14
- package/dist/connector-app/index.js +0 -12
- package/dist/connector-app-BljHd0Pe.js +0 -64
- package/dist/connector-app-BljHd0Pe.js.map +0 -1
- package/dist/constants-D113Ut1x.js.map +0 -1
- package/dist/errors/index.d.ts +0 -2
- package/dist/errors/index.js +0 -3
- package/dist/errors-DHWylqRZ.js.map +0 -1
- package/dist/index-BA8ASHKr.d.ts +0 -31
- package/dist/index.d.ts +0 -14
- package/dist/index.js +0 -60
- package/dist/index.js.map +0 -1
- package/dist/ingestion/index.d.ts +0 -6
- package/dist/ingestion/index.js +0 -11
- package/dist/ingestion-QBGKrFr9.js +0 -200
- package/dist/ingestion-QBGKrFr9.js.map +0 -1
- package/dist/manifest/index.d.ts +0 -2
- package/dist/manifest-P7kBnch9.js.map +0 -1
- package/dist/metrics-inkCAxKq.js +0 -136
- package/dist/metrics-inkCAxKq.js.map +0 -1
- package/dist/metrics.d.ts +0 -4
- package/dist/metrics.js +0 -5
- package/dist/publisher/index.d.ts +0 -6
- package/dist/publisher/index.js +0 -8
- package/dist/publisher-BdWJOfer.js +0 -138
- package/dist/publisher-BdWJOfer.js.map +0 -1
- package/dist/runtime-config/index.d.ts +0 -4
- package/dist/runtime-config/index.js +0 -5
- package/dist/runtime-config-IbyCVpei.js +0 -60
- package/dist/runtime-config-IbyCVpei.js.map +0 -1
- package/dist/server-D-eLX3yN.js +0 -183
- package/dist/server-D-eLX3yN.js.map +0 -1
- package/dist/service-CUjGhe-7.d.ts +0 -32
- package/dist/service-DiP-XETo.js +0 -8
- package/dist/service-DiP-XETo.js.map +0 -1
- package/dist/state-store/index.d.ts +0 -3
- package/dist/state-store/index.js +0 -5
- package/dist/state-store-BdYulQOY.js.map +0 -1
- package/dist/status-BTTd7AUR.js.map +0 -1
- package/dist/table-bindings-Dp1DhTeS.js +0 -106
- package/dist/table-bindings-Dp1DhTeS.js.map +0 -1
- package/dist/telemetry-j_nxwYfh.js.map +0 -1
- package/dist/types-jVD5Ys43.d.ts +0 -174
- package/dist/webhook/index.d.ts +0 -7
- package/dist/webhook/index.js +0 -9
- package/dist/webhook-MhFcLW-o.js +0 -13
- package/dist/webhook-MhFcLW-o.js.map +0 -1
- package/src/publisher/console.ts +0 -18
- package/src/publisher/index.ts +0 -7
- package/src/publisher/instrumented.ts +0 -75
- package/src/publisher/service.ts +0 -37
- package/src/publisher/wings.ts +0 -204
package/src/ingestion/engine.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { DateTime, Effect, Layer, Queue } from "effect";
|
|
2
|
-
import { HttpRouter, type HttpServer, HttpServerResponse } from "effect/
|
|
3
|
-
import * as Observability from "effect/
|
|
1
|
+
import { Array as Arr, DateTime, Effect, Layer, Order, Queue } from "effect";
|
|
2
|
+
import { HttpRouter, type HttpServer, HttpServerResponse } from "effect/http";
|
|
3
|
+
import * as Observability from "effect/observability";
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
|
+
ChangesFeed,
|
|
6
7
|
ConnectorDefinition,
|
|
7
8
|
Cursor,
|
|
8
9
|
ResourceDefinition,
|
|
@@ -12,11 +13,11 @@ import type {
|
|
|
12
13
|
import type { StateOperation, StateSource } from "../state-store/schema";
|
|
13
14
|
|
|
14
15
|
import { ConnectorError } from "../errors";
|
|
16
|
+
import { ingestBatch } from "../ingestor/instrumented";
|
|
17
|
+
import { Ingestor } from "../ingestor/service";
|
|
15
18
|
import * as Metrics from "../metrics";
|
|
16
|
-
import { publishBatch } from "../publisher/instrumented";
|
|
17
|
-
import { Publisher } from "../publisher/service";
|
|
18
19
|
import { StateStore } from "../state-store";
|
|
19
|
-
import { deriveSyncState, normalizeCursor } from "../state-store/state";
|
|
20
|
+
import { deriveSyncState, hasChanges, normalizeCursor } from "../state-store/state";
|
|
20
21
|
import { statusRoute } from "../status";
|
|
21
22
|
import { Attr, SpanName, annotateError } from "../telemetry";
|
|
22
23
|
import { router, WebhookQueue, type QueuedWebhookBatch } from "../webhook/server";
|
|
@@ -44,47 +45,70 @@ type RunWebhookOptions = RunOptions & {
|
|
|
44
45
|
};
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
|
-
* Runs all connector ingestion loops. Callers provide the
|
|
48
|
+
* Runs all connector ingestion loops. Callers provide the Ingestor and
|
|
48
49
|
* StateStore layers; webhook mode additionally requires an HttpServer layer.
|
|
49
50
|
*/
|
|
50
51
|
export function run<const Resources extends ReadonlyArray<ResourceDefinition>>(
|
|
51
52
|
connector: ConnectorDefinition<Resources>,
|
|
52
53
|
options?: RunNoWebhookOptions,
|
|
53
|
-
): Effect.Effect<void, ConnectorError, StateStore |
|
|
54
|
+
): Effect.Effect<void, ConnectorError, StateStore | Ingestor>;
|
|
54
55
|
export function run<const Resources extends ReadonlyArray<ResourceDefinition>>(
|
|
55
56
|
connector: ConnectorDefinition<Resources>,
|
|
56
57
|
options: RunWebhookOptions,
|
|
57
|
-
): Effect.Effect<void, ConnectorError, StateStore |
|
|
58
|
-
export function run(
|
|
59
|
-
const queueLayer = makeWebhookQueueLayer(connector.name);
|
|
60
|
-
const runtimeLayer = options?.webhook
|
|
61
|
-
? Layer.mergeAll(
|
|
62
|
-
queueLayer,
|
|
63
|
-
makeWebhookServerLayer(connector, options.webhook).pipe(Layer.provide(queueLayer)),
|
|
64
|
-
)
|
|
65
|
-
: Layer.empty;
|
|
66
|
-
|
|
58
|
+
): Effect.Effect<void, ConnectorError, StateStore | Ingestor | HttpServer.HttpServer>;
|
|
59
|
+
export function run(definition: ConnectorDefinition, options?: RunOptions) {
|
|
67
60
|
return Effect.gen(function* () {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
yield*
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
61
|
+
// Check the whole feed, even the resources that don't run.
|
|
62
|
+
if (definition.changes) yield* validateFeed(definition, definition.changes);
|
|
63
|
+
const ingestor = yield* Ingestor;
|
|
64
|
+
const connector = selectResources(definition, ingestor.resourceNames);
|
|
65
|
+
const queueLayer = makeWebhookQueueLayer(connector.name);
|
|
66
|
+
const runtimeLayer = options?.webhook
|
|
67
|
+
? Layer.mergeAll(
|
|
68
|
+
queueLayer,
|
|
69
|
+
makeWebhookServerLayer(connector, options.webhook).pipe(Layer.provide(queueLayer)),
|
|
70
|
+
)
|
|
71
|
+
: Layer.empty;
|
|
76
72
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
return yield* Effect.gen(function* () {
|
|
74
|
+
const initialCutoff =
|
|
75
|
+
options?.initialCutoff ?? (yield* DateTime.now.pipe(Effect.map(DateTime.formatIso)));
|
|
76
|
+
yield* Effect.logInfo("Connector started").pipe(
|
|
77
|
+
Effect.annotateLogs({
|
|
78
|
+
[Attr.connectorName]: connector.name,
|
|
79
|
+
resources: connector.resources.length,
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
const sourceRun = runIngestion(connector, initialCutoff);
|
|
84
|
+
const webhookRun = options?.webhook
|
|
85
|
+
? runWebhookRuntime(connector, options.webhook.routes)
|
|
86
|
+
: Effect.void;
|
|
87
|
+
|
|
88
|
+
return yield* Effect.all([sourceRun, webhookRun], { concurrency: "unbounded" }).pipe(
|
|
89
|
+
Effect.asVoid,
|
|
90
|
+
);
|
|
91
|
+
}).pipe(Effect.provide(runtimeLayer));
|
|
92
|
+
});
|
|
86
93
|
}
|
|
87
94
|
|
|
95
|
+
const selectResources = (
|
|
96
|
+
connector: ConnectorDefinition,
|
|
97
|
+
names: ReadonlyArray<string> | undefined,
|
|
98
|
+
): ConnectorDefinition => {
|
|
99
|
+
if (names === undefined) return connector;
|
|
100
|
+
const isSelected = (resource: ResourceDefinition) => names.includes(resource.name);
|
|
101
|
+
const { changes, ...rest } = connector;
|
|
102
|
+
const feedResources = changes?.resources.filter(isSelected) ?? [];
|
|
103
|
+
return {
|
|
104
|
+
...rest,
|
|
105
|
+
resources: connector.resources.filter(isSelected),
|
|
106
|
+
...(changes && feedResources.length > 0
|
|
107
|
+
? { changes: { ...changes, resources: feedResources } }
|
|
108
|
+
: {}),
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
88
112
|
const makeWebhookQueueLayer = (connector: string): Layer.Layer<WebhookQueue> =>
|
|
89
113
|
Layer.effect(WebhookQueue)(
|
|
90
114
|
Effect.gen(function* () {
|
|
@@ -111,7 +135,7 @@ const runWebhookQueueConsumer = (connector: ConnectorDefinition) =>
|
|
|
111
135
|
const batch = yield* Queue.take(queue.queue);
|
|
112
136
|
const depth = yield* Queue.size(queue.queue);
|
|
113
137
|
yield* Metrics.setWebhookQueueDepth(connector.name, depth);
|
|
114
|
-
yield*
|
|
138
|
+
yield* ingestBatch({
|
|
115
139
|
connector: connector.name,
|
|
116
140
|
resource: batch.resource,
|
|
117
141
|
source: "webhook",
|
|
@@ -134,7 +158,10 @@ const makeWebhookServerLayer = (
|
|
|
134
158
|
const metricsPath = options.metricsPath === undefined ? "/metrics" : options.metricsPath;
|
|
135
159
|
const statusPath = options.statusPath === undefined ? "/status" : options.statusPath;
|
|
136
160
|
const app = Layer.mergeAll(
|
|
137
|
-
router(options.routes, {
|
|
161
|
+
router(options.routes, {
|
|
162
|
+
connectorName: connector.name,
|
|
163
|
+
resourceNames: connector.resources.map((resource) => resource.name),
|
|
164
|
+
}),
|
|
138
165
|
HttpRouter.add("GET", healthPath, Effect.succeed(HttpServerResponse.text("ok"))),
|
|
139
166
|
...(metricsPath === false
|
|
140
167
|
? []
|
|
@@ -150,14 +177,57 @@ const makeWebhookServerLayer = (
|
|
|
150
177
|
const runIngestion = (
|
|
151
178
|
connector: ConnectorDefinition,
|
|
152
179
|
initialCutoff: Cursor.Value,
|
|
153
|
-
): Effect.Effect<void, ConnectorError, StateStore |
|
|
154
|
-
Effect.
|
|
155
|
-
connector.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
180
|
+
): Effect.Effect<void, ConnectorError, StateStore | Ingestor> =>
|
|
181
|
+
Effect.gen(function* () {
|
|
182
|
+
const feed = connector.changes;
|
|
183
|
+
yield* Effect.all(
|
|
184
|
+
[
|
|
185
|
+
Effect.forEach(
|
|
186
|
+
connector.resources,
|
|
187
|
+
(resource) => runResourceSources(connector, resource, initialCutoff),
|
|
188
|
+
{ concurrency: "unbounded" },
|
|
189
|
+
),
|
|
190
|
+
feed ? runChangesFeed(connector, feed, initialCutoff) : Effect.void,
|
|
191
|
+
],
|
|
192
|
+
{ concurrency: "unbounded" },
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const bySavedAt = Order.mapInput(
|
|
197
|
+
Order.String,
|
|
198
|
+
(state: NonNullable<ResourceState["changes"]>) => state.lastSuccessAt ?? "",
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const validateFeed = (connector: ConnectorDefinition, feed: ChangesFeed) =>
|
|
202
|
+
Effect.forEach(feed.resources, (member, index) => {
|
|
203
|
+
if (feed.resources.findIndex((item) => item.name === member.name) !== index) {
|
|
204
|
+
return Effect.fail(
|
|
205
|
+
new ConnectorError({ message: `Resource ${member.name} is in the changes feed twice` }),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const resource = connector.resources.find((item) => item.name === member.name);
|
|
209
|
+
if (!resource) {
|
|
210
|
+
return Effect.fail(
|
|
211
|
+
new ConnectorError({
|
|
212
|
+
message: `Changes feed resource ${member.name} is not a resource of connector ${connector.name}`,
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (resource.changes) {
|
|
217
|
+
return Effect.fail(
|
|
218
|
+
new ConnectorError({
|
|
219
|
+
message: `Resource ${member.name} has its own changes and is also in the changes feed`,
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
return Effect.void;
|
|
224
|
+
}).pipe(
|
|
225
|
+
Effect.andThen(
|
|
226
|
+
feed.resources.length === 0
|
|
227
|
+
? Effect.fail(new ConnectorError({ message: "The changes feed has no resources" }))
|
|
228
|
+
: Effect.void,
|
|
229
|
+
),
|
|
230
|
+
);
|
|
161
231
|
|
|
162
232
|
const initializeResourceState = (
|
|
163
233
|
existing: ResourceState | undefined,
|
|
@@ -207,9 +277,9 @@ const runResourceSources = (
|
|
|
207
277
|
return initializeRuntimeStatus(connector, resource, initialCutoff).pipe(
|
|
208
278
|
Effect.andThen(Effect.all(runs, { concurrency: "unbounded" })),
|
|
209
279
|
Effect.asVoid,
|
|
210
|
-
Effect.catch(() =>
|
|
280
|
+
Effect.catch((error) =>
|
|
211
281
|
Metrics.setSyncState({ connector: connector.name, resource: resource.name }, "error").pipe(
|
|
212
|
-
Effect.andThen(Effect.logError("Connector resource initialization failed")),
|
|
282
|
+
Effect.andThen(Effect.logError("Connector resource initialization failed", error)),
|
|
213
283
|
Effect.annotateLogs({
|
|
214
284
|
[Attr.connectorName]: connector.name,
|
|
215
285
|
resource: resource.name,
|
|
@@ -250,8 +320,8 @@ const isolateSourceFailure = <A, R>(
|
|
|
250
320
|
): Effect.Effect<A, never, R> =>
|
|
251
321
|
// Only typed source errors are isolated. Defects and interruption still stop the runtime.
|
|
252
322
|
effect.pipe(
|
|
253
|
-
Effect.catch(() =>
|
|
254
|
-
Effect.logError("Connector source parked").pipe(
|
|
323
|
+
Effect.catch((error) =>
|
|
324
|
+
Effect.logError("Connector source parked", error).pipe(
|
|
255
325
|
Effect.annotateLogs({
|
|
256
326
|
[Attr.connectorName]: connector.name,
|
|
257
327
|
resource: resource.name,
|
|
@@ -270,8 +340,9 @@ const refreshRuntimeStatus = Effect.fnUntraced(function* (
|
|
|
270
340
|
const state = yield* getInitializedState(resource.name, initialCutoff);
|
|
271
341
|
yield* Metrics.setSyncState(
|
|
272
342
|
{ connector: connector.name, resource: resource.name },
|
|
273
|
-
deriveSyncState(resource, state),
|
|
343
|
+
deriveSyncState(connector, resource, state),
|
|
274
344
|
);
|
|
345
|
+
|
|
275
346
|
return state;
|
|
276
347
|
});
|
|
277
348
|
|
|
@@ -283,15 +354,16 @@ const initializeRuntimeStatus = Effect.fnUntraced(function* (
|
|
|
283
354
|
const state = yield* getInitializedState(resource.name, initialCutoff);
|
|
284
355
|
yield* Metrics.setSyncState(
|
|
285
356
|
{ connector: connector.name, resource: resource.name },
|
|
286
|
-
deriveSyncState(resource, state),
|
|
357
|
+
deriveSyncState(connector, resource, state),
|
|
287
358
|
);
|
|
359
|
+
|
|
288
360
|
if (resource.backfill) {
|
|
289
361
|
yield* Metrics.setLastSuccessTimestamp(
|
|
290
362
|
{ connector: connector.name, resource: resource.name, source: "backfill" },
|
|
291
363
|
state.backfill?.lastSuccessAt,
|
|
292
364
|
);
|
|
293
365
|
}
|
|
294
|
-
if (resource
|
|
366
|
+
if (hasChanges(connector, resource)) {
|
|
295
367
|
yield* Metrics.setLastSuccessTimestamp(
|
|
296
368
|
{ connector: connector.name, resource: resource.name, source: "changes" },
|
|
297
369
|
state.changes?.lastSuccessAt,
|
|
@@ -328,15 +400,15 @@ const runBackfill = Effect.fnUntraced(function* (
|
|
|
328
400
|
})
|
|
329
401
|
.pipe(recordResourceError(connector, resource, "backfill", "fetch"));
|
|
330
402
|
|
|
331
|
-
yield*
|
|
403
|
+
yield* ingestBatch({
|
|
332
404
|
connector: connector.name,
|
|
333
405
|
resource: resource.name,
|
|
334
406
|
source: "backfill",
|
|
335
407
|
batch: {
|
|
336
408
|
cursor: page.nextPageCursor ?? backfill.cutoff,
|
|
337
|
-
|
|
409
|
+
rows: page.rows,
|
|
338
410
|
},
|
|
339
|
-
}).pipe(recordResourceError(connector, resource, "backfill", "
|
|
411
|
+
}).pipe(recordResourceError(connector, resource, "backfill", "ingest"));
|
|
340
412
|
|
|
341
413
|
const lastSuccessAt = yield* DateTime.now.pipe(Effect.map(DateTime.formatIso));
|
|
342
414
|
state = {
|
|
@@ -349,7 +421,7 @@ const runBackfill = Effect.fnUntraced(function* (
|
|
|
349
421
|
},
|
|
350
422
|
};
|
|
351
423
|
|
|
352
|
-
// Checkpoint only after
|
|
424
|
+
// Checkpoint only after ingestion succeeds so a crash cannot skip an
|
|
353
425
|
// unacknowledged page; replay therefore remains at-least-once.
|
|
354
426
|
yield* Effect.withSpan(
|
|
355
427
|
store
|
|
@@ -397,18 +469,18 @@ const runChanges = Effect.fnUntraced(function* (
|
|
|
397
469
|
.fetch({ cursor })
|
|
398
470
|
.pipe(recordResourceError(connector, resource, "changes", "fetch"));
|
|
399
471
|
|
|
400
|
-
yield*
|
|
472
|
+
yield* ingestBatch({
|
|
401
473
|
connector: connector.name,
|
|
402
474
|
resource: resource.name,
|
|
403
475
|
source: "changes",
|
|
404
476
|
batch: {
|
|
405
477
|
cursor: page.cursor,
|
|
406
|
-
|
|
478
|
+
rows: page.rows,
|
|
407
479
|
},
|
|
408
|
-
}).pipe(recordResourceError(connector, resource, "changes", "
|
|
480
|
+
}).pipe(recordResourceError(connector, resource, "changes", "ingest"));
|
|
409
481
|
|
|
410
482
|
const lastSuccessAt = yield* DateTime.now.pipe(Effect.map(DateTime.formatIso));
|
|
411
|
-
// Advance the durable cursor only after Wings
|
|
483
|
+
// Advance the durable cursor only after Wings acknowledges the page.
|
|
412
484
|
yield* Effect.withSpan(
|
|
413
485
|
store
|
|
414
486
|
.setChangesState(resource.name, {
|
|
@@ -433,6 +505,114 @@ const runChanges = Effect.fnUntraced(function* (
|
|
|
433
505
|
yield* refreshRuntimeStatus(connector, resource, initialCutoff);
|
|
434
506
|
}
|
|
435
507
|
|
|
436
|
-
|
|
508
|
+
if (!page.hasMore) {
|
|
509
|
+
yield* Effect.sleep(resource.changes.interval ?? "1 minute");
|
|
510
|
+
}
|
|
437
511
|
}
|
|
438
512
|
});
|
|
513
|
+
|
|
514
|
+
// Every member resource stores the feed cursor. All rows are ingested before any
|
|
515
|
+
// cursor is written. If a crash stops the writes partway, the next run starts
|
|
516
|
+
// from the oldest cursor.
|
|
517
|
+
const runChangesFeed = (
|
|
518
|
+
connector: ConnectorDefinition,
|
|
519
|
+
feed: ChangesFeed,
|
|
520
|
+
initialCutoff: Cursor.Value,
|
|
521
|
+
): Effect.Effect<void, never, StateStore | Ingestor> =>
|
|
522
|
+
Effect.gen(function* () {
|
|
523
|
+
const store = yield* StateStore;
|
|
524
|
+
const members = feed.resources;
|
|
525
|
+
const selected = new Set(members.map((resource) => resource.name));
|
|
526
|
+
// Any failure stops the whole feed, so it is an error for every resource in it.
|
|
527
|
+
const recordFeedError =
|
|
528
|
+
(operation: StateOperation) =>
|
|
529
|
+
<A, R>(effect: Effect.Effect<A, ConnectorError, R>) =>
|
|
530
|
+
effect.pipe(
|
|
531
|
+
Effect.tapError(() =>
|
|
532
|
+
Effect.forEach(
|
|
533
|
+
members,
|
|
534
|
+
(resource) =>
|
|
535
|
+
Metrics.setSyncState(
|
|
536
|
+
{ connector: connector.name, resource: resource.name },
|
|
537
|
+
"error",
|
|
538
|
+
).pipe(Effect.andThen(store.setResourceError(resource.name, "changes", operation))),
|
|
539
|
+
{ discard: true },
|
|
540
|
+
),
|
|
541
|
+
),
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
while (true) {
|
|
545
|
+
const states = yield* Effect.forEach(members, (resource) =>
|
|
546
|
+
store.getResourceState(resource.name),
|
|
547
|
+
);
|
|
548
|
+
// A resource that was off has an older cursor. Resume from the oldest, so it
|
|
549
|
+
// gets the changes it missed. The others read some changes again.
|
|
550
|
+
const saved = states.flatMap((state) => (state?.changes ? [state.changes] : []));
|
|
551
|
+
const cursor = Arr.isReadonlyArrayNonEmpty(saved)
|
|
552
|
+
? Arr.min(saved, bySavedAt).cursor
|
|
553
|
+
: initialCutoff;
|
|
554
|
+
const result = yield* feed.fetch({ cursor, selected }).pipe(recordFeedError("fetch"));
|
|
555
|
+
|
|
556
|
+
yield* Effect.forEach(
|
|
557
|
+
members,
|
|
558
|
+
(resource) =>
|
|
559
|
+
ingestBatch({
|
|
560
|
+
connector: connector.name,
|
|
561
|
+
resource: resource.name,
|
|
562
|
+
source: "changes",
|
|
563
|
+
batch: {
|
|
564
|
+
cursor: result.cursor,
|
|
565
|
+
rows: result.rows[resource.name] ?? [],
|
|
566
|
+
},
|
|
567
|
+
}),
|
|
568
|
+
{ discard: true },
|
|
569
|
+
).pipe(recordFeedError("ingest"));
|
|
570
|
+
|
|
571
|
+
const lastSuccessAt = yield* DateTime.now.pipe(Effect.map(DateTime.formatIso));
|
|
572
|
+
yield* Effect.forEach(
|
|
573
|
+
members,
|
|
574
|
+
(resource) =>
|
|
575
|
+
Effect.withSpan(
|
|
576
|
+
store
|
|
577
|
+
.setChangesState(resource.name, {
|
|
578
|
+
cursor: normalizeCursor(result.cursor),
|
|
579
|
+
lastSuccessAt,
|
|
580
|
+
})
|
|
581
|
+
.pipe(
|
|
582
|
+
Effect.andThen(
|
|
583
|
+
Metrics.setLastSuccessTimestamp(
|
|
584
|
+
{ connector: connector.name, resource: resource.name, source: "changes" },
|
|
585
|
+
lastSuccessAt,
|
|
586
|
+
),
|
|
587
|
+
),
|
|
588
|
+
Effect.andThen(store.clearResourceError(resource.name, "changes")),
|
|
589
|
+
Effect.tapError((error) => annotateError("state_set", error)),
|
|
590
|
+
),
|
|
591
|
+
SpanName.stateSet,
|
|
592
|
+
{ attributes: { [Attr.stateKey]: resource.name } },
|
|
593
|
+
),
|
|
594
|
+
{ discard: true },
|
|
595
|
+
).pipe(recordFeedError("checkpoint"));
|
|
596
|
+
|
|
597
|
+
yield* Effect.forEach(members, (resource, index) =>
|
|
598
|
+
states[index]?.lastError?.source === "changes"
|
|
599
|
+
? refreshRuntimeStatus(connector, resource, initialCutoff)
|
|
600
|
+
: Effect.void,
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
if (!result.hasMore) {
|
|
604
|
+
yield* Effect.sleep(feed.interval ?? "1 minute");
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}).pipe(
|
|
608
|
+
// Only typed errors park the feed. Defects and interruption still stop the runtime.
|
|
609
|
+
Effect.catch((error) =>
|
|
610
|
+
Effect.logError("Connector changes feed parked", error).pipe(
|
|
611
|
+
Effect.annotateLogs({
|
|
612
|
+
[Attr.connectorName]: connector.name,
|
|
613
|
+
resources: feed.resources.map((resource) => resource.name).join(","),
|
|
614
|
+
}),
|
|
615
|
+
Effect.andThen(Effect.never),
|
|
616
|
+
),
|
|
617
|
+
),
|
|
618
|
+
);
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Bool,
|
|
3
|
+
DateDay,
|
|
4
|
+
Decimal,
|
|
5
|
+
Field,
|
|
6
|
+
FixedSizeBinary,
|
|
7
|
+
Float32,
|
|
8
|
+
Float64,
|
|
9
|
+
Int32,
|
|
10
|
+
Int64,
|
|
11
|
+
LargeBinary,
|
|
12
|
+
List,
|
|
13
|
+
Map_,
|
|
14
|
+
RecordBatch,
|
|
15
|
+
Schema as ArrowSchema,
|
|
16
|
+
Struct,
|
|
17
|
+
TimeMicrosecond,
|
|
18
|
+
TimestampMicrosecond,
|
|
19
|
+
Utf8,
|
|
20
|
+
makeBuilder,
|
|
21
|
+
type DataType,
|
|
22
|
+
type TypeMap,
|
|
23
|
+
} from "apache-arrow";
|
|
24
|
+
import { Effect, Schema } from "effect";
|
|
25
|
+
import {
|
|
26
|
+
parseDecimalType,
|
|
27
|
+
parseFixedType,
|
|
28
|
+
type IcebergType,
|
|
29
|
+
type PrimitiveType,
|
|
30
|
+
type TableSchema,
|
|
31
|
+
} from "iceberg-js";
|
|
32
|
+
|
|
33
|
+
import { ConnectorError } from "../errors";
|
|
34
|
+
|
|
35
|
+
// Nothing converts an Iceberg schema to Arrow for us, so we do it here.
|
|
36
|
+
// Each type also gets a schema for its values, because Arrow does not reject a bad
|
|
37
|
+
// value, it converts it: "1" becomes 1, and 1.5 becomes 1.
|
|
38
|
+
|
|
39
|
+
type RowSchema = Schema.Codec<unknown, unknown>;
|
|
40
|
+
|
|
41
|
+
/** One Iceberg type: the Arrow type, and the values it accepts. */
|
|
42
|
+
type Compiled = {
|
|
43
|
+
readonly type: DataType;
|
|
44
|
+
readonly row: RowSchema;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Arrow pads or cuts binary values to fit.
|
|
48
|
+
const bytes = (length: number) =>
|
|
49
|
+
Schema.Uint8Array.check(
|
|
50
|
+
Schema.makeFilter(
|
|
51
|
+
(value: Uint8Array) => value.byteLength === length || `expected ${length} bytes`,
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Arrow wraps big integers instead of failing, so check the range.
|
|
56
|
+
const int32 = Schema.Number.check(
|
|
57
|
+
Schema.isInt(),
|
|
58
|
+
Schema.isBetween({ minimum: -2_147_483_648, maximum: 2_147_483_647 }),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const int64 = Schema.BigInt.check(
|
|
62
|
+
Schema.isBetweenBigInt({ minimum: -(2n ** 63n), maximum: 2n ** 63n - 1n }),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Arrow turns a number outside the 32-bit range into Infinity.
|
|
66
|
+
const float32 = Schema.Finite.check(
|
|
67
|
+
Schema.makeFilter(
|
|
68
|
+
(value: number) => Number.isFinite(Math.fround(value)) || "expected a 32-bit number",
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const microsecondsInDay = Schema.BigInt.check(
|
|
73
|
+
Schema.isBetweenBigInt({ minimum: 0n, maximum: 86_399_999_999n }),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const primitives: Record<string, Compiled> = {
|
|
77
|
+
boolean: { type: new Bool(), row: Schema.Boolean },
|
|
78
|
+
int: { type: new Int32(), row: int32 },
|
|
79
|
+
long: { type: new Int64(), row: int64 },
|
|
80
|
+
float: { type: new Float32(), row: float32 },
|
|
81
|
+
double: { type: new Float64(), row: Schema.Finite },
|
|
82
|
+
string: { type: new Utf8(), row: Schema.String },
|
|
83
|
+
uuid: { type: new FixedSizeBinary(16), row: bytes(16) },
|
|
84
|
+
binary: { type: new LargeBinary(), row: Schema.Uint8Array },
|
|
85
|
+
date: { type: new DateDay(), row: Schema.Date },
|
|
86
|
+
time: { type: new TimeMicrosecond(), row: microsecondsInDay },
|
|
87
|
+
timestamp: { type: new TimestampMicrosecond(), row: Schema.Date },
|
|
88
|
+
timestamptz: { type: new TimestampMicrosecond("UTC"), row: Schema.Date },
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const primitive = (type: PrimitiveType): Compiled => {
|
|
92
|
+
const known = primitives[type];
|
|
93
|
+
|
|
94
|
+
if (known) return known;
|
|
95
|
+
|
|
96
|
+
// Nothing checks the catalog metadata, and Arrow takes any precision.
|
|
97
|
+
// Better to fail here than at write time.
|
|
98
|
+
const decimal = parseDecimalType(type);
|
|
99
|
+
|
|
100
|
+
if (decimal) {
|
|
101
|
+
if (decimal.precision < 1 || decimal.precision > 38 || decimal.scale > decimal.precision) {
|
|
102
|
+
throw new Error(`Invalid Iceberg decimal type ${type}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
type: new Decimal(decimal.scale, decimal.precision),
|
|
107
|
+
row: Schema.instanceOf(Uint32Array).check(
|
|
108
|
+
Schema.makeFilter((value: Uint32Array) => value.length === 4 || "expected four words"),
|
|
109
|
+
),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const fixed = parseFixedType(type);
|
|
114
|
+
|
|
115
|
+
if (fixed) {
|
|
116
|
+
if (fixed.length < 1) throw new Error(`Invalid Iceberg fixed type ${type}`);
|
|
117
|
+
|
|
118
|
+
return { type: new FixedSizeBinary(fixed.length), row: bytes(fixed.length) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throw new Error(`Unsupported Iceberg type ${type}`);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** Absent and null both become null in Arrow. */
|
|
125
|
+
const optional = (row: RowSchema) => Schema.optional(Schema.NullOr(row));
|
|
126
|
+
|
|
127
|
+
const fieldMetadata = (id: number): Map<string, string> =>
|
|
128
|
+
new Map([["PARQUET:field_id", String(id)]]);
|
|
129
|
+
|
|
130
|
+
/** Same as `primitive`, plus struct, list and map. */
|
|
131
|
+
const compileType = (type: IcebergType): Compiled => {
|
|
132
|
+
if (typeof type === "string") return primitive(type);
|
|
133
|
+
|
|
134
|
+
if (type.type === "struct") {
|
|
135
|
+
const fields = type.fields.map((field) => ({ ...field, compiled: compileType(field.type) }));
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
type: new Struct(
|
|
139
|
+
fields.map(
|
|
140
|
+
(field) =>
|
|
141
|
+
new Field(field.name, field.compiled.type, !field.required, fieldMetadata(field.id)),
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
row: Schema.Struct(
|
|
145
|
+
Object.fromEntries(
|
|
146
|
+
fields.map((field) => [
|
|
147
|
+
field.name,
|
|
148
|
+
field.required ? field.compiled.row : optional(field.compiled.row),
|
|
149
|
+
]),
|
|
150
|
+
),
|
|
151
|
+
),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (type.type === "list") {
|
|
156
|
+
const element = compileType(type.element);
|
|
157
|
+
const required = type["element-required"];
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
type: new List(
|
|
161
|
+
new Field("element", element.type, !required, fieldMetadata(type["element-id"])),
|
|
162
|
+
),
|
|
163
|
+
row: Schema.Array(required ? element.row : Schema.NullishOr(element.row)),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const key = compileType(type.key);
|
|
168
|
+
const value = compileType(type.value);
|
|
169
|
+
const required = type["value-required"];
|
|
170
|
+
const entries = new Struct([
|
|
171
|
+
new Field("key", key.type, false, fieldMetadata(type["key-id"])),
|
|
172
|
+
new Field("value", value.type, !required, fieldMetadata(type["value-id"])),
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
type: new Map_(new Field("entries", entries, false)),
|
|
177
|
+
row: Schema.ReadonlyMap(key.row, required ? value.row : Schema.NullishOr(value.row)),
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export type RowEncoder = (
|
|
182
|
+
rows: ReadonlyArray<object>,
|
|
183
|
+
) => Effect.Effect<RecordBatch<TypeMap>, ConnectorError>;
|
|
184
|
+
|
|
185
|
+
export const makeRowEncoder = (
|
|
186
|
+
tableSchema: TableSchema,
|
|
187
|
+
version: string,
|
|
188
|
+
): Effect.Effect<RowEncoder, ConnectorError> =>
|
|
189
|
+
Effect.try({
|
|
190
|
+
try: () => {
|
|
191
|
+
const fields = tableSchema.fields.map((field) => ({
|
|
192
|
+
id: field.id,
|
|
193
|
+
name: field.name,
|
|
194
|
+
...compileType(field.type),
|
|
195
|
+
}));
|
|
196
|
+
const fieldNames = new Set(fields.map((field) => field.name));
|
|
197
|
+
const identifierIds = new Set(tableSchema["identifier-field-ids"]);
|
|
198
|
+
const keys = tableSchema.fields
|
|
199
|
+
.filter((field) => identifierIds.has(field.id))
|
|
200
|
+
.map((field) => field.name);
|
|
201
|
+
// Other columns may be missing: a delete only sends the key and version.
|
|
202
|
+
const required = new Set([...keys, version]);
|
|
203
|
+
|
|
204
|
+
if (!fieldNames.has(version)) throw new Error(`Missing version column ${version}`);
|
|
205
|
+
|
|
206
|
+
const struct = new Struct(
|
|
207
|
+
fields.map((field) => new Field(field.name, field.type, true, fieldMetadata(field.id))),
|
|
208
|
+
);
|
|
209
|
+
const schema = new ArrowSchema(struct.children);
|
|
210
|
+
|
|
211
|
+
const decodeRow = Schema.decodeUnknownSync(
|
|
212
|
+
Schema.Struct(
|
|
213
|
+
Object.fromEntries(
|
|
214
|
+
fields.map((field) => [
|
|
215
|
+
field.name,
|
|
216
|
+
required.has(field.name) ? field.row : optional(field.row),
|
|
217
|
+
]),
|
|
218
|
+
),
|
|
219
|
+
),
|
|
220
|
+
{ onExcessProperty: "error" },
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Schema says which field is wrong. This adds the row number.
|
|
224
|
+
const decodeAt = (row: object, index: number) => {
|
|
225
|
+
try {
|
|
226
|
+
return decodeRow(row) as Record<string, unknown>;
|
|
227
|
+
} catch (cause) {
|
|
228
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
229
|
+
throw new Error(`row ${index}: ${detail}`, { cause });
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return (rows: ReadonlyArray<object>) =>
|
|
234
|
+
Effect.try({
|
|
235
|
+
try: () => {
|
|
236
|
+
const builder = makeBuilder({ type: struct, nullValues: [null, undefined] });
|
|
237
|
+
|
|
238
|
+
for (const [index, row] of rows.entries()) {
|
|
239
|
+
builder.append(decodeAt(row, index) as Struct<TypeMap>["TValue"]);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return new RecordBatch(schema, builder.finish().flush());
|
|
243
|
+
},
|
|
244
|
+
catch: (cause) => new ConnectorError({ message: "Failed to encode rows", cause }),
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
catch: (cause) => new ConnectorError({ message: "Failed to convert Iceberg schema", cause }),
|
|
248
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Effect, Layer } from "effect";
|
|
2
|
+
|
|
3
|
+
import { Ingestor } from "./service";
|
|
4
|
+
|
|
5
|
+
/** Logs batches without sending them to Wings. */
|
|
6
|
+
export const layerConsole = Layer.succeed(Ingestor)({
|
|
7
|
+
ingest: ({ resource, source, batch }) =>
|
|
8
|
+
Effect.logInfo(`[ingestor] -> Source: ${source} | Resource: ${resource}`).pipe(
|
|
9
|
+
Effect.annotateLogs({ rows: batch.rows.length, cursor: batch.cursor, source }),
|
|
10
|
+
// Nothing is encoded, so there are no bytes to count.
|
|
11
|
+
Effect.as({ rows: batch.rows.length, bytes: 0 }),
|
|
12
|
+
),
|
|
13
|
+
});
|