@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.
Files changed (179) hide show
  1. package/METRICS.md +16 -17
  2. package/README.md +127 -442
  3. package/dist/annotations-BE74YmAO.d.mts +39 -0
  4. package/dist/auth/index.d.mts +2 -0
  5. package/dist/{auth-DvU_CZ7C.js → auth/index.mjs} +6 -8
  6. package/dist/auth/index.mjs.map +1 -0
  7. package/dist/connector-app/index.d.mts +2 -0
  8. package/dist/connector-app/index.mjs +2 -0
  9. package/dist/connector-app-C-L4QqG6.mjs +123 -0
  10. package/dist/connector-app-C-L4QqG6.mjs.map +1 -0
  11. package/dist/{constants-D113Ut1x.js → constants-D-GQzpHZ.mjs} +4 -5
  12. package/dist/constants-D-GQzpHZ.mjs.map +1 -0
  13. package/dist/demo/index.d.mts +2 -0
  14. package/dist/demo/index.mjs +2 -0
  15. package/dist/demo-B30Vlkk2.mjs +191 -0
  16. package/dist/demo-B30Vlkk2.mjs.map +1 -0
  17. package/dist/errors/index.d.mts +2 -0
  18. package/dist/errors/index.mjs +2 -0
  19. package/dist/{errors-DHWylqRZ.js → errors-pT0YVJU5.mjs} +2 -3
  20. package/dist/errors-pT0YVJU5.mjs.map +1 -0
  21. package/dist/iceberg/index.d.mts +3 -0
  22. package/dist/iceberg/index.mjs +3 -0
  23. package/dist/iceberg-D6L8I6vA.mjs +39 -0
  24. package/dist/iceberg-D6L8I6vA.mjs.map +1 -0
  25. package/dist/{index-VpHaZXZw.d.ts → index-BUHSGm9Q.d.mts} +11 -13
  26. package/dist/{index-DXa26Ry5.d.ts → index-BX8G7d41.d.mts} +1 -2
  27. package/dist/{index-bfjH7Yb0.d.ts → index-BfXpgxk9.d.mts} +9 -10
  28. package/dist/{index-BKw7jA2a.d.ts → index-CRVAwJ5-.d.mts} +10 -8
  29. package/dist/index-CUPP2A_m.d.mts +27 -0
  30. package/dist/{index-DYpEMJf-.d.ts → index-CgvFomO6.d.mts} +6 -7
  31. package/dist/index-DJhDp4Xc.d.mts +58 -0
  32. package/dist/index-Dk7urvpF.d.mts +76 -0
  33. package/dist/{index-DKoRwyFn.d.ts → index-DkVNO6ks.d.mts} +2 -5
  34. package/dist/{index-C5Ma7V7t.d.ts → index-NnCutIqM.d.mts} +62 -37
  35. package/dist/{index-CR9HJDdr.d.ts → index-eFuoMZZ3.d.mts} +18 -20
  36. package/dist/{index-ZSuiZX5H.d.ts → index-ygpml2kC.d.mts} +19 -31
  37. package/dist/index.d.mts +16 -0
  38. package/dist/index.mjs +45 -0
  39. package/dist/index.mjs.map +1 -0
  40. package/dist/ingestion/index.d.mts +2 -0
  41. package/dist/ingestion/index.mjs +2 -0
  42. package/dist/ingestion-q0WwqUSt.mjs +275 -0
  43. package/dist/ingestion-q0WwqUSt.mjs.map +1 -0
  44. package/dist/ingestor/index.d.mts +3 -0
  45. package/dist/ingestor/index.mjs +4 -0
  46. package/dist/ingestor-D4L-HPkg.mjs +26 -0
  47. package/dist/ingestor-D4L-HPkg.mjs.map +1 -0
  48. package/dist/instrumented-Bhp7fyvh.mjs +29 -0
  49. package/dist/instrumented-Bhp7fyvh.mjs.map +1 -0
  50. package/dist/manifest/index.d.mts +2 -0
  51. package/dist/manifest/{index.js → index.mjs} +2 -4
  52. package/dist/{manifest-P7kBnch9.js → manifest-BGKxmKLy.mjs} +14 -19
  53. package/dist/manifest-BGKxmKLy.mjs.map +1 -0
  54. package/dist/{telemetry-j_nxwYfh.js → metrics-BaEX__MN.mjs} +170 -19
  55. package/dist/metrics-BaEX__MN.mjs.map +1 -0
  56. package/dist/{metrics-C4V1pm6v.d.ts → metrics-CVXFL7YT.d.mts} +18 -13
  57. package/dist/metrics.d.mts +2 -0
  58. package/dist/metrics.mjs +2 -0
  59. package/dist/{chunk-Bp6m_JJh.js → rolldown-runtime-D7D4PA-g.mjs} +4 -4
  60. package/dist/runtime-config/index.d.mts +2 -0
  61. package/dist/runtime-config/index.mjs +3 -0
  62. package/dist/runtime-config-Oa55gAew.mjs +97 -0
  63. package/dist/runtime-config-Oa55gAew.mjs.map +1 -0
  64. package/dist/server-CMnxz3h0.mjs +144 -0
  65. package/dist/server-CMnxz3h0.mjs.map +1 -0
  66. package/dist/service-DLkhuxYl.d.mts +27 -0
  67. package/dist/service-jWxxfEht.mjs +7 -0
  68. package/dist/service-jWxxfEht.mjs.map +1 -0
  69. package/dist/state-store/index.d.mts +2 -0
  70. package/dist/state-store/index.mjs +2 -0
  71. package/dist/{state-store-BdYulQOY.js → state-store-uFvHY7Ax.mjs} +15 -22
  72. package/dist/state-store-uFvHY7Ax.mjs.map +1 -0
  73. package/dist/status/{index.d.ts → index.d.mts} +1 -4
  74. package/dist/status/{index.js → index.mjs} +2 -6
  75. package/dist/{status-BTTd7AUR.js → status-CKb9IhQ9.mjs} +14 -16
  76. package/dist/status-CKb9IhQ9.mjs.map +1 -0
  77. package/dist/table-V0tgUC5i.mjs +370 -0
  78. package/dist/table-V0tgUC5i.mjs.map +1 -0
  79. package/dist/types-xZtSmrGA.d.mts +220 -0
  80. package/dist/webhook/index.d.mts +3 -0
  81. package/dist/webhook/index.mjs +12 -0
  82. package/dist/webhook/index.mjs.map +1 -0
  83. package/dist/wings-I56hGtNv.mjs +325 -0
  84. package/dist/wings-I56hGtNv.mjs.map +1 -0
  85. package/package.json +64 -51
  86. package/src/catalog/table.ts +201 -0
  87. package/src/connector-app/create-table.ts +47 -0
  88. package/src/connector-app/demo.ts +42 -0
  89. package/src/connector-app/index.ts +2 -0
  90. package/src/connector-app/start.ts +1 -1
  91. package/src/core/builder.ts +15 -23
  92. package/src/core/index.ts +11 -1
  93. package/src/core/types.ts +114 -39
  94. package/src/demo/definition.ts +68 -0
  95. package/src/demo/generate.ts +167 -0
  96. package/src/demo/index.ts +3 -0
  97. package/src/demo/run.ts +142 -0
  98. package/src/iceberg/annotations.ts +62 -0
  99. package/src/iceberg/error.ts +9 -0
  100. package/src/iceberg/index.ts +3 -0
  101. package/src/iceberg/table.ts +511 -0
  102. package/src/index.ts +3 -1
  103. package/src/ingestion/engine.ts +240 -60
  104. package/src/ingestor/arrow.ts +248 -0
  105. package/src/ingestor/console.ts +13 -0
  106. package/src/ingestor/index.ts +9 -0
  107. package/src/ingestor/instrumented.ts +61 -0
  108. package/src/ingestor/service.ts +30 -0
  109. package/src/ingestor/wings.ts +143 -0
  110. package/src/manifest/config.ts +3 -3
  111. package/src/metrics.ts +66 -20
  112. package/src/runtime-config/config.ts +6 -8
  113. package/src/runtime-config/constants.ts +2 -2
  114. package/src/runtime-config/index.ts +2 -7
  115. package/src/runtime-config/layer.ts +1 -1
  116. package/src/runtime-config/table-bindings.ts +48 -136
  117. package/src/state-store/layer-key-value-store.ts +8 -3
  118. package/src/state-store/layer-memory.ts +1 -1
  119. package/src/state-store/layer-sql.ts +4 -4
  120. package/src/state-store/schema.ts +2 -2
  121. package/src/state-store/state.ts +14 -2
  122. package/src/status/route.ts +2 -2
  123. package/src/telemetry.ts +10 -10
  124. package/src/webhook/server.ts +39 -40
  125. package/dist/auth/index.d.ts +0 -2
  126. package/dist/auth/index.js +0 -3
  127. package/dist/auth-DvU_CZ7C.js.map +0 -1
  128. package/dist/connector-app/index.d.ts +0 -14
  129. package/dist/connector-app/index.js +0 -12
  130. package/dist/connector-app-BljHd0Pe.js +0 -64
  131. package/dist/connector-app-BljHd0Pe.js.map +0 -1
  132. package/dist/constants-D113Ut1x.js.map +0 -1
  133. package/dist/errors/index.d.ts +0 -2
  134. package/dist/errors/index.js +0 -3
  135. package/dist/errors-DHWylqRZ.js.map +0 -1
  136. package/dist/index-BA8ASHKr.d.ts +0 -31
  137. package/dist/index.d.ts +0 -14
  138. package/dist/index.js +0 -60
  139. package/dist/index.js.map +0 -1
  140. package/dist/ingestion/index.d.ts +0 -6
  141. package/dist/ingestion/index.js +0 -11
  142. package/dist/ingestion-QBGKrFr9.js +0 -200
  143. package/dist/ingestion-QBGKrFr9.js.map +0 -1
  144. package/dist/manifest/index.d.ts +0 -2
  145. package/dist/manifest-P7kBnch9.js.map +0 -1
  146. package/dist/metrics-inkCAxKq.js +0 -136
  147. package/dist/metrics-inkCAxKq.js.map +0 -1
  148. package/dist/metrics.d.ts +0 -4
  149. package/dist/metrics.js +0 -5
  150. package/dist/publisher/index.d.ts +0 -6
  151. package/dist/publisher/index.js +0 -8
  152. package/dist/publisher-BdWJOfer.js +0 -138
  153. package/dist/publisher-BdWJOfer.js.map +0 -1
  154. package/dist/runtime-config/index.d.ts +0 -4
  155. package/dist/runtime-config/index.js +0 -5
  156. package/dist/runtime-config-IbyCVpei.js +0 -60
  157. package/dist/runtime-config-IbyCVpei.js.map +0 -1
  158. package/dist/server-D-eLX3yN.js +0 -183
  159. package/dist/server-D-eLX3yN.js.map +0 -1
  160. package/dist/service-CUjGhe-7.d.ts +0 -32
  161. package/dist/service-DiP-XETo.js +0 -8
  162. package/dist/service-DiP-XETo.js.map +0 -1
  163. package/dist/state-store/index.d.ts +0 -3
  164. package/dist/state-store/index.js +0 -5
  165. package/dist/state-store-BdYulQOY.js.map +0 -1
  166. package/dist/status-BTTd7AUR.js.map +0 -1
  167. package/dist/table-bindings-Dp1DhTeS.js +0 -106
  168. package/dist/table-bindings-Dp1DhTeS.js.map +0 -1
  169. package/dist/telemetry-j_nxwYfh.js.map +0 -1
  170. package/dist/types-jVD5Ys43.d.ts +0 -174
  171. package/dist/webhook/index.d.ts +0 -7
  172. package/dist/webhook/index.js +0 -9
  173. package/dist/webhook-MhFcLW-o.js +0 -13
  174. package/dist/webhook-MhFcLW-o.js.map +0 -1
  175. package/src/publisher/console.ts +0 -18
  176. package/src/publisher/index.ts +0 -7
  177. package/src/publisher/instrumented.ts +0 -75
  178. package/src/publisher/service.ts +0 -37
  179. package/src/publisher/wings.ts +0 -204
@@ -0,0 +1,9 @@
1
+ export { layerConsole } from "./console";
2
+ export {
3
+ Ingestor,
4
+ type IngestOptions,
5
+ type IngestorService,
6
+ type IngestResult,
7
+ type IngestSource,
8
+ } from "./service";
9
+ export { layerWings, layerWingsConfig, type WingsIngestorConfig } from "./wings";
@@ -0,0 +1,61 @@
1
+ import { Effect, Metric } from "effect";
2
+
3
+ import type { ResourceBatch } from "../core/types";
4
+
5
+ import * as Metrics from "../metrics";
6
+ import { Attr, EventAttr, EventName, SpanName, addCurrentSpanEvent } from "../telemetry";
7
+ import { Ingestor, type IngestSource } from "./service";
8
+
9
+ export const ingestBatch = Effect.fnUntraced(function* (options: {
10
+ readonly connector?: string;
11
+ readonly resource: string;
12
+ readonly source: IngestSource;
13
+ readonly batch: ResourceBatch;
14
+ }) {
15
+ const metricAttributes = Metrics.withResourceAttributes({
16
+ connector: options.connector ?? "unknown",
17
+ resource: options.resource,
18
+ source: options.source,
19
+ });
20
+ const batchOutcome = (outcome: "success" | "error") =>
21
+ Metric.withAttributes(Metrics.batches, {
22
+ ...metricAttributes,
23
+ [Attr.batchOutcome]: outcome,
24
+ });
25
+
26
+ const ingestor = yield* Ingestor;
27
+
28
+ const ingest = ingestor.ingest(options).pipe(
29
+ Effect.tap(() => Effect.annotateCurrentSpan({ [Attr.ingestionSuccess]: true })),
30
+ Effect.tapError(() =>
31
+ Effect.annotateCurrentSpan({
32
+ [Attr.ingestionSuccess]: false,
33
+ [Attr.errorPhase]: "ingest",
34
+ }),
35
+ ),
36
+ );
37
+
38
+ // onError keeps the failure, so the engine still sees it and skips the checkpoint.
39
+ const result = yield* Effect.withSpan(ingest, SpanName.ingest, { kind: "producer" }).pipe(
40
+ Effect.trackDuration(Metric.withAttributes(Metrics.ingestDuration, metricAttributes)),
41
+ Effect.onError(() => Metric.update(batchOutcome("error"), 1)),
42
+ );
43
+
44
+ yield* Metric.update(batchOutcome("success"), 1);
45
+ yield* Metric.update(
46
+ Metric.withAttributes(Metrics.rowsIngested, metricAttributes),
47
+ options.batch.rows.length,
48
+ );
49
+ yield* Metric.update(
50
+ Metric.withAttributes(Metrics.batchSize, metricAttributes),
51
+ options.batch.rows.length,
52
+ );
53
+
54
+ if (options.batch.cursor !== undefined) {
55
+ yield* addCurrentSpanEvent(EventName.batchCheckpoint, {
56
+ [EventAttr.batchCursor]: options.batch.cursor,
57
+ });
58
+ }
59
+
60
+ return result;
61
+ });
@@ -0,0 +1,30 @@
1
+ import { Context, type Effect } from "effect";
2
+
3
+ import type { ResourceBatch } from "../core/types";
4
+ import type { ConnectorError } from "../errors";
5
+
6
+ export type IngestSource = "backfill" | "changes" | "webhook" | "demo";
7
+
8
+ /** A resource batch and the source that produced it. */
9
+ export type IngestOptions = {
10
+ readonly resource: string;
11
+ readonly source: IngestSource;
12
+ readonly batch: ResourceBatch;
13
+ };
14
+
15
+ /** Rows written, and their size as uncompressed Arrow buffers. */
16
+ export type IngestResult = {
17
+ readonly rows: number;
18
+ readonly bytes: number;
19
+ };
20
+
21
+ /** Handles batches from connector sources. */
22
+ export interface IngestorService {
23
+ readonly ingest: (options: IngestOptions) => Effect.Effect<IngestResult, ConnectorError>;
24
+ /** Resources it has tables for. Only these run. Without it, every resource runs. */
25
+ readonly resourceNames?: ReadonlyArray<string>;
26
+ }
27
+
28
+ export class Ingestor extends Context.Service<Ingestor, IngestorService>()(
29
+ "@useairfoil/connector-kit/Ingestor",
30
+ ) {}
@@ -0,0 +1,143 @@
1
+ import type { RecordBatch, TypeMap } from "apache-arrow";
2
+ import type { TableIdentifier } from "iceberg-js";
3
+
4
+ import * as Wings from "@useairfoil/wings";
5
+ import { Config, Effect, Layer } from "effect";
6
+ import { FetchHttpClient } from "effect/http";
7
+
8
+ import type { TableSource } from "../core/types";
9
+
10
+ import { ensureTable } from "../catalog/table";
11
+ import { ConnectorError } from "../errors";
12
+ import * as RuntimeConfig from "../runtime-config";
13
+ import { makeRowEncoder, type RowEncoder } from "./arrow";
14
+ import { Ingestor, type IngestorService } from "./service";
15
+
16
+ /** One catalog and an Iceberg table for each resource that runs. */
17
+ export type WingsIngestorConfig<Source extends TableSource = TableSource> = {
18
+ readonly connector: Source;
19
+ readonly catalog: string;
20
+ readonly tables: RuntimeConfig.ResolvedTableBindings<Source>;
21
+ };
22
+
23
+ type IngestorEntry = {
24
+ readonly encode: RowEncoder;
25
+ readonly push: (batch: RecordBatch<TypeMap>) => Effect.Effect<void, ConnectorError>;
26
+ };
27
+
28
+ type PreparedTable = {
29
+ readonly resource: string;
30
+ readonly identifier: TableIdentifier;
31
+ readonly encode: RowEncoder;
32
+ };
33
+
34
+ const connectorError = (message: string, cause?: unknown) => new ConnectorError({ message, cause });
35
+
36
+ /** Loads each table schema and opens one Wings stream per bound resource. */
37
+ export const layerWings = <const Source extends TableSource>(
38
+ config: WingsIngestorConfig<Source>,
39
+ ): Layer.Layer<Ingestor, ConnectorError, Wings.CatalogManager.CatalogManager> =>
40
+ Layer.effect(Ingestor)(
41
+ Effect.gen(function* () {
42
+ const manager = yield* Wings.CatalogManager.CatalogManager;
43
+ const catalog = yield* manager
44
+ .getIcebergCatalog(config.catalog)
45
+ .pipe(Effect.mapError((cause) => connectorError("Failed to load Wings catalog", cause)));
46
+
47
+ const bindings: Partial<Record<string, RuntimeConfig.TableBinding>> = config.tables;
48
+
49
+ const prepared: Array<PreparedTable> = [];
50
+ for (const resource of config.connector.resources) {
51
+ const binding = bindings[resource.name];
52
+ if (!binding) continue;
53
+
54
+ const { namespace, name, location } = binding;
55
+ const identifier = { namespace, name };
56
+ const schema = yield* ensureTable(resource.rowSchema, {
57
+ catalog,
58
+ identifier,
59
+ location,
60
+ }).pipe(
61
+ Effect.mapError((cause) =>
62
+ connectorError(`Failed to prepare table for ${resource.name}: ${cause.message}`, cause),
63
+ ),
64
+ );
65
+
66
+ const encode = yield* makeRowEncoder(schema, resource.version);
67
+ prepared.push({ resource: resource.name, identifier, encode });
68
+ }
69
+
70
+ const entries = new Map<string, IngestorEntry>();
71
+ for (const { resource, identifier, encode } of prepared) {
72
+ const ingestor = yield* manager
73
+ .ingestor({
74
+ catalog: config.catalog,
75
+ namespace: identifier.namespace,
76
+ table: identifier.name,
77
+ })
78
+ .pipe(
79
+ Effect.mapError((cause) =>
80
+ connectorError(`Failed to open ingestor for ${resource}`, cause),
81
+ ),
82
+ );
83
+
84
+ entries.set(resource, {
85
+ encode,
86
+ push: (batch) =>
87
+ ingestor
88
+ .push(batch)
89
+ .pipe(
90
+ Effect.mapError((cause) =>
91
+ connectorError(`Failed to ingest rows for ${resource}`, cause),
92
+ ),
93
+ ),
94
+ });
95
+ }
96
+
97
+ const service: IngestorService = {
98
+ resourceNames: prepared.map(({ resource }) => resource),
99
+ ingest: ({ resource, batch }) => {
100
+ const entry = entries.get(resource);
101
+
102
+ if (!entry) return Effect.fail(connectorError(`Unknown resource ${resource}`));
103
+ if (batch.rows.length === 0) return Effect.succeed({ rows: 0, bytes: 0 });
104
+
105
+ return entry
106
+ .encode(batch.rows)
107
+ .pipe(
108
+ Effect.flatMap((encoded) =>
109
+ entry
110
+ .push(encoded)
111
+ .pipe(Effect.as({ rows: batch.rows.length, bytes: encoded.data.byteLength })),
112
+ ),
113
+ );
114
+ },
115
+ };
116
+
117
+ return Ingestor.of(service);
118
+ }),
119
+ );
120
+
121
+ /** Reads the Wings URL, catalog and table bindings from hosted config. */
122
+ export const layerWingsConfig = (connector: TableSource) =>
123
+ Layer.unwrap(
124
+ Effect.gen(function* () {
125
+ const { catalog, uri } = yield* Config.all({
126
+ catalog: RuntimeConfig.catalog,
127
+ uri: RuntimeConfig.wingsUri,
128
+ }).pipe(Effect.mapError((cause) => connectorError("Invalid Wings configuration", cause)));
129
+
130
+ if (uri.protocol !== "http:" && uri.protocol !== "https:") {
131
+ return yield* Effect.fail(connectorError("Invalid Wings URL"));
132
+ }
133
+
134
+ const tables = yield* RuntimeConfig.loadTableBindings(connector).pipe(
135
+ Effect.mapError((cause) => connectorError(cause.message, cause)),
136
+ );
137
+ const manager = Wings.CatalogManager.layer({ baseUrl: uri.href }).pipe(
138
+ Layer.provide(FetchHttpClient.layer),
139
+ );
140
+
141
+ return layerWings({ connector, catalog, tables }).pipe(Layer.provide(manager));
142
+ }),
143
+ );
@@ -111,7 +111,7 @@ const withDefault = <A extends string | number | boolean>(
111
111
 
112
112
  /** Defines a required non-empty string, optionally with a default. */
113
113
  export const string = (options: FieldOptions<string>): ConfigField<string> => ({
114
- config: withDefault(Config.nonEmptyString(options.runtimeKey), options.default),
114
+ config: withDefault(Config.NonEmptyString(options.runtimeKey), options.default),
115
115
  spec: {
116
116
  runtimeKey: options.runtimeKey,
117
117
  type: "string",
@@ -145,7 +145,7 @@ export const number = (options: NumberFieldOptions): ConfigField<number> => {
145
145
 
146
146
  /** Defines a boolean, optionally with a default. */
147
147
  export const boolean = (options: FieldOptions<boolean>): ConfigField<boolean> => ({
148
- config: withDefault(Config.boolean(options.runtimeKey), options.default),
148
+ config: withDefault(Config.Boolean(options.runtimeKey), options.default),
149
149
  spec: {
150
150
  runtimeKey: options.runtimeKey,
151
151
  type: "boolean",
@@ -160,7 +160,7 @@ export const boolean = (options: FieldOptions<boolean>): ConfigField<boolean> =>
160
160
  export const select = <const Values extends readonly [string, ...Array<string>]>(
161
161
  options: FieldOptions<Values[number]> & { readonly values: Values },
162
162
  ): ConfigField<Values[number]> => ({
163
- config: withDefault(Config.literals(options.values, options.runtimeKey), options.default),
163
+ config: withDefault(Config.Literals(options.values, options.runtimeKey), options.default),
164
164
  spec: {
165
165
  runtimeKey: options.runtimeKey,
166
166
  type: "select",
package/src/metrics.ts CHANGED
@@ -1,11 +1,21 @@
1
- import { DateTime, Effect, Metric, Option } from "effect";
1
+ import {
2
+ Cause,
3
+ DateTime,
4
+ type Duration,
5
+ Effect,
6
+ Metric,
7
+ Option,
8
+ Predicate,
9
+ Schedule,
10
+ } from "effect";
11
+ import { HttpClientError, HttpClientResponse } from "effect/http";
2
12
 
3
13
  import type { SyncState } from "./core/types";
4
14
 
5
15
  import { Attr } from "./telemetry";
6
16
 
7
- export type BatchSource = "backfill" | "changes" | "webhook";
8
- export type BatchOutcome = "accepted" | "rejected" | "error";
17
+ export type BatchSource = "backfill" | "changes" | "webhook" | "demo";
18
+ export type BatchOutcome = "success" | "error";
9
19
  export type ApiRetryReason = "transport" | "timeout" | "rate_limit" | "server_error";
10
20
  export type WebhookOutcome =
11
21
  | "ok"
@@ -43,32 +53,26 @@ export type ApiRetryMetricAttrs = {
43
53
  readonly reason: ApiRetryReason;
44
54
  };
45
55
 
46
- export const entitiesUpserted = Metric.counter("airfoil.connector.entity.upserts", {
47
- description: "Total entity upsert mutations published by connector sources",
48
- incremental: true,
49
- attributes: { unit: "{mutation}" },
50
- });
51
-
52
- export const entitiesDeleted = Metric.counter("airfoil.connector.entity.deletes", {
53
- description: "Total entity delete mutations published by connector sources",
56
+ export const batches = Metric.counter("airfoil.connector.batches", {
57
+ description: "Total resource batches ingested by connector sources",
54
58
  incremental: true,
55
- attributes: { unit: "{mutation}" },
59
+ attributes: { unit: "{batch}" },
56
60
  });
57
61
 
58
- export const batches = Metric.counter("airfoil.connector.batches", {
59
- description: "Total resource batches published by connector sources",
62
+ export const rowsIngested = Metric.counter("airfoil.connector.rows.ingested", {
63
+ description: "Total resource rows successfully ingested",
60
64
  incremental: true,
61
- attributes: { unit: "{batch}" },
65
+ attributes: { unit: "{row}" },
62
66
  });
63
67
 
64
68
  export const batchSize = Metric.histogram("airfoil.connector.batch.size", {
65
- description: "Distribution of resource mutation batch sizes",
69
+ description: "Distribution of resource row batch sizes",
66
70
  boundaries: [1, 5, 10, 25, 50, 100, 250, 500, 1000],
67
- attributes: { unit: "{mutation}" },
71
+ attributes: { unit: "{row}" },
68
72
  });
69
73
 
70
- export const publishDuration = Metric.timer("airfoil.connector.publish.duration", {
71
- description: "Publisher call duration in milliseconds",
74
+ export const ingestDuration = Metric.timer("airfoil.connector.ingest.duration", {
75
+ description: "Ingestor call duration in milliseconds",
72
76
  boundaries: [1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000],
73
77
  attributes: { unit: "ms" },
74
78
  });
@@ -80,7 +84,7 @@ export const webhookRequests = Metric.counter("airfoil.connector.webhook.request
80
84
  });
81
85
 
82
86
  export const webhookQueueDepth = Metric.gauge("airfoil.connector.webhook.queue.depth", {
83
- description: "Current queued webhook batches waiting for publish",
87
+ description: "Current queued webhook batches waiting for ingestion",
84
88
  attributes: { unit: "{batch}" },
85
89
  });
86
90
 
@@ -154,6 +158,48 @@ export const recordApiRetry = (attrs: ApiRetryMetricAttrs) =>
154
158
  1,
155
159
  );
156
160
 
161
+ // Same statuses as `HttpClient.retryTransient`.
162
+ const transientStatuses = new Set([408, 429, 500, 502, 503, 504]);
163
+
164
+ // A connector only retries a `403` when the provider uses it for rate limits.
165
+ const reasonForStatus = (status: number): ApiRetryReason =>
166
+ status === 408 ? "timeout" : status === 429 || status === 403 ? "rate_limit" : "server_error";
167
+
168
+ // Effect exports the response type ID, not a guard.
169
+ const isResponse = (value: unknown): value is HttpClientResponse.HttpClientResponse =>
170
+ Predicate.hasProperty(value, HttpClientResponse.TypeId);
171
+
172
+ /** Why `HttpClient.retryTransient` retries. Undefined for a response it keeps. */
173
+ const retryReason = (value: unknown): ApiRetryReason | undefined => {
174
+ if (isResponse(value)) {
175
+ return transientStatuses.has(value.status) ? reasonForStatus(value.status) : undefined;
176
+ }
177
+ if (HttpClientError.isHttpClientError(value) && value.reason._tag === "StatusCodeError") {
178
+ return reasonForStatus(value.reason.response.status);
179
+ }
180
+ return Cause.isTimeoutError(value) ? "timeout" : "transport";
181
+ };
182
+
183
+ /**
184
+ * A jittered exponential schedule for `HttpClient.retryTransient` that records
185
+ * each retry with its reason.
186
+ */
187
+ export const retrySchedule = (options: {
188
+ readonly connector: string;
189
+ readonly baseDelay: Duration.Input;
190
+ readonly times: number;
191
+ }) =>
192
+ Schedule.exponential(options.baseDelay).pipe(
193
+ Schedule.jittered,
194
+ Schedule.upTo({ times: options.times }),
195
+ Schedule.tap(({ input }) => {
196
+ const reason = retryReason(input);
197
+ return reason === undefined
198
+ ? Effect.void
199
+ : recordApiRetry({ connector: options.connector, reason });
200
+ }),
201
+ );
202
+
157
203
  const syncStates = ["pending", "backfilling", "live", "error"] as const;
158
204
 
159
205
  export const setSyncState = (attrs: SyncStateAttrs, current: SyncState) =>
@@ -1,16 +1,14 @@
1
- import type { WingsClientOptions } from "@useairfoil/wings/wings-client";
2
-
3
1
  import { Config } from "effect";
4
2
 
5
3
  import { PlatformRuntimeDefault, PlatformRuntimeKey } from "./constants";
6
4
 
7
5
  /** Shared HTTP port used by hosted and sandbox connector entrypoints. */
8
- export const httpPort = Config.port(PlatformRuntimeKey.httpPort).pipe(
6
+ export const httpPort = Config.Port(PlatformRuntimeKey.httpPort).pipe(
9
7
  Config.withDefault(PlatformRuntimeDefault.httpPort),
10
8
  );
11
9
 
12
- /** Shared Wings client configuration supplied by the hosting platform. */
13
- export const wingsClient = {
14
- host: Config.nonEmptyString(PlatformRuntimeKey.wingsHost),
15
- namespace: Config.nonEmptyString(PlatformRuntimeKey.wingsNamespace),
16
- } satisfies Config.Wrap<WingsClientOptions>;
10
+ /** Wings URL, including the protocol. */
11
+ export const wingsUri = Config.URL(PlatformRuntimeKey.wingsUri);
12
+
13
+ /** Wings catalog ID. */
14
+ export const catalog = Config.NonEmptyString(PlatformRuntimeKey.catalog);
@@ -8,8 +8,8 @@ export const PlatformRuntimeKey = {
8
8
  connectorInstanceId: "AIRFOIL_CONNECTOR_INSTANCE_ID",
9
9
  stateTable: "AIRFOIL_STATE_TABLE",
10
10
  postgresConnectionString: "POSTGRES_CONNECTION_STRING",
11
- wingsHost: "WINGS_HOST",
12
- wingsNamespace: "WINGS_NAMESPACE",
11
+ wingsUri: "WINGS_URI",
12
+ catalog: "AIRFOIL_CATALOG",
13
13
  otelEnabled: "OTEL_ENABLED",
14
14
  otelExporterOtlpEndpoint: "OTEL_EXPORTER_OTLP_ENDPOINT",
15
15
  otelExporterOtlpHeaders: "OTEL_EXPORTER_OTLP_HEADERS",
@@ -1,10 +1,5 @@
1
1
  export { RuntimeConfigError, type RuntimeConfigErrorCode } from "./error";
2
- export { httpPort, wingsClient } from "./config";
2
+ export { catalog, httpPort, wingsUri } from "./config";
3
3
  export { PlatformRuntimeDefault, PlatformRuntimeKey } from "./constants";
4
4
  export { layerHosted } from "./layer";
5
- export {
6
- type HostedTableBinding,
7
- type ResolvedTableBinding,
8
- type ResolvedTableBindings,
9
- loadTableBindings,
10
- } from "./table-bindings";
5
+ export { type ResolvedTableBindings, type TableBinding, loadTableBindings } from "./table-bindings";
@@ -8,7 +8,7 @@ const providerFromFile: Effect.Effect<
8
8
  RuntimeConfigError,
9
9
  FileSystem.FileSystem
10
10
  > = Effect.gen(function* () {
11
- const configPath = yield* Config.nonEmptyString(PlatformRuntimeKey.configPath).pipe(
11
+ const configPath = yield* Config.NonEmptyString(PlatformRuntimeKey.configPath).pipe(
12
12
  Effect.mapError(
13
13
  (error) =>
14
14
  new RuntimeConfigError({