@useairfoil/connector-kit 0.6.0 → 0.7.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.
Files changed (182) hide show
  1. package/LICENSE.txt +201 -0
  2. package/METRICS.md +38 -42
  3. package/README.md +111 -444
  4. package/dist/annotations-vzSzXtr-.d.mts +34 -0
  5. package/dist/arrow-flight-DDQ0ktx9.mjs +251 -0
  6. package/dist/arrow-flight-DDQ0ktx9.mjs.map +1 -0
  7. package/dist/auth/index.d.mts +2 -0
  8. package/dist/{auth-DvU_CZ7C.js → auth/index.mjs} +6 -8
  9. package/dist/auth/index.mjs.map +1 -0
  10. package/dist/connector-app/index.d.mts +2 -0
  11. package/dist/connector-app/index.mjs +2 -0
  12. package/dist/connector-app-C6_dZnhE.mjs +256 -0
  13. package/dist/connector-app-C6_dZnhE.mjs.map +1 -0
  14. package/dist/{constants-D113Ut1x.js → constants-v3UX-hf6.mjs} +5 -5
  15. package/dist/constants-v3UX-hf6.mjs.map +1 -0
  16. package/dist/demo/index.d.mts +2 -0
  17. package/dist/demo/index.mjs +2 -0
  18. package/dist/demo-DqTJ7TWI.mjs +204 -0
  19. package/dist/demo-DqTJ7TWI.mjs.map +1 -0
  20. package/dist/errors/index.d.mts +2 -0
  21. package/dist/errors/index.mjs +2 -0
  22. package/dist/{errors-DHWylqRZ.js → errors-pT0YVJU5.mjs} +2 -3
  23. package/dist/errors-pT0YVJU5.mjs.map +1 -0
  24. package/dist/iceberg/index.d.mts +3 -0
  25. package/dist/iceberg/index.mjs +3 -0
  26. package/dist/iceberg-Cj2PwlNm.mjs +30 -0
  27. package/dist/iceberg-Cj2PwlNm.mjs.map +1 -0
  28. package/dist/index-BTAbexBp.d.mts +55 -0
  29. package/dist/{index-DXa26Ry5.d.ts → index-BX8G7d41.d.mts} +1 -2
  30. package/dist/{index-VpHaZXZw.d.ts → index-Byr8NTdG.d.mts} +11 -13
  31. package/dist/index-CV6V6fyx.d.mts +79 -0
  32. package/dist/{index-C5Ma7V7t.d.ts → index-CZKx2Yce.d.mts} +72 -37
  33. package/dist/{index-DYpEMJf-.d.ts → index-CgvFomO6.d.mts} +6 -7
  34. package/dist/{index-bfjH7Yb0.d.ts → index-DeTwIUKl.d.mts} +9 -10
  35. package/dist/{index-DKoRwyFn.d.ts → index-DkVNO6ks.d.mts} +2 -5
  36. package/dist/{index-BKw7jA2a.d.ts → index-dHS59EZl.d.mts} +10 -8
  37. package/dist/{index-CR9HJDdr.d.ts → index-eFuoMZZ3.d.mts} +18 -20
  38. package/dist/{index-ZSuiZX5H.d.ts → index-fHIJJoRe.d.mts} +25 -30
  39. package/dist/index-uFa2wwYo.d.mts +27 -0
  40. package/dist/index.d.mts +16 -0
  41. package/dist/index.mjs +45 -0
  42. package/dist/index.mjs.map +1 -0
  43. package/dist/ingestion/index.d.mts +2 -0
  44. package/dist/ingestion/index.mjs +2 -0
  45. package/dist/ingestion-DoCUwm5N.mjs +275 -0
  46. package/dist/ingestion-DoCUwm5N.mjs.map +1 -0
  47. package/dist/ingestor/index.d.mts +3 -0
  48. package/dist/ingestor/index.mjs +4 -0
  49. package/dist/ingestor-DjCEzBzZ.mjs +26 -0
  50. package/dist/ingestor-DjCEzBzZ.mjs.map +1 -0
  51. package/dist/instrumented-Bq1WX2L5.mjs +29 -0
  52. package/dist/instrumented-Bq1WX2L5.mjs.map +1 -0
  53. package/dist/manifest/index.d.mts +2 -0
  54. package/dist/manifest/{index.js → index.mjs} +2 -4
  55. package/dist/{manifest-P7kBnch9.js → manifest-CABeIhAG.mjs} +14 -19
  56. package/dist/manifest-CABeIhAG.mjs.map +1 -0
  57. package/dist/{telemetry-j_nxwYfh.js → metrics-C901IIYZ.mjs} +172 -26
  58. package/dist/metrics-C901IIYZ.mjs.map +1 -0
  59. package/dist/{metrics-C4V1pm6v.d.ts → metrics-DoJ0WeKu.d.mts} +18 -13
  60. package/dist/metrics.d.mts +2 -0
  61. package/dist/metrics.mjs +2 -0
  62. package/dist/{chunk-Bp6m_JJh.js → rolldown-runtime-D7D4PA-g.mjs} +4 -4
  63. package/dist/runtime-config/index.d.mts +2 -0
  64. package/dist/runtime-config/index.mjs +3 -0
  65. package/dist/runtime-config-Bn8Fivt1.mjs +100 -0
  66. package/dist/runtime-config-Bn8Fivt1.mjs.map +1 -0
  67. package/dist/server-D1TTN0_D.mjs +144 -0
  68. package/dist/server-D1TTN0_D.mjs.map +1 -0
  69. package/dist/service-BRoy_EId.d.mts +27 -0
  70. package/dist/service-jWxxfEht.mjs +7 -0
  71. package/dist/service-jWxxfEht.mjs.map +1 -0
  72. package/dist/state-store/index.d.mts +2 -0
  73. package/dist/state-store/index.mjs +2 -0
  74. package/dist/{state-store-BdYulQOY.js → state-store-DhSrKVEH.mjs} +15 -22
  75. package/dist/state-store-DhSrKVEH.mjs.map +1 -0
  76. package/dist/status/{index.d.ts → index.d.mts} +1 -4
  77. package/dist/status/{index.js → index.mjs} +2 -6
  78. package/dist/{status-BTTd7AUR.js → status-4h6Oxh-O.mjs} +14 -16
  79. package/dist/status-4h6Oxh-O.mjs.map +1 -0
  80. package/dist/table-CxM09xf7.mjs +307 -0
  81. package/dist/table-CxM09xf7.mjs.map +1 -0
  82. package/dist/types-NtSv6gwl.d.mts +220 -0
  83. package/dist/webhook/index.d.mts +3 -0
  84. package/dist/webhook/index.mjs +12 -0
  85. package/dist/webhook/index.mjs.map +1 -0
  86. package/package.json +72 -58
  87. package/src/catalog/retry.ts +25 -0
  88. package/src/catalog/table.ts +237 -0
  89. package/src/connector-app/create-table.ts +47 -0
  90. package/src/connector-app/demo.ts +47 -0
  91. package/src/connector-app/index.ts +3 -0
  92. package/src/connector-app/setup.ts +69 -0
  93. package/src/connector-app/start.ts +1 -1
  94. package/src/core/builder.ts +15 -23
  95. package/src/core/index.ts +11 -1
  96. package/src/core/types.ts +114 -39
  97. package/src/demo/definition.ts +68 -0
  98. package/src/demo/generate.ts +167 -0
  99. package/src/demo/index.ts +3 -0
  100. package/src/demo/run.ts +172 -0
  101. package/src/iceberg/annotations.ts +49 -0
  102. package/src/iceberg/error.ts +9 -0
  103. package/src/iceberg/index.ts +3 -0
  104. package/src/iceberg/table.ts +461 -0
  105. package/src/index.ts +3 -1
  106. package/src/ingestion/engine.ts +240 -60
  107. package/src/ingestor/arrow-flight.ts +141 -0
  108. package/src/ingestor/arrow.ts +248 -0
  109. package/src/ingestor/console.ts +13 -0
  110. package/src/ingestor/index.ts +13 -0
  111. package/src/ingestor/instrumented.ts +61 -0
  112. package/src/ingestor/service.ts +30 -0
  113. package/src/manifest/config.ts +3 -3
  114. package/src/metrics.ts +66 -20
  115. package/src/runtime-config/config.ts +11 -8
  116. package/src/runtime-config/constants.ts +3 -2
  117. package/src/runtime-config/index.ts +2 -7
  118. package/src/runtime-config/layer.ts +1 -1
  119. package/src/runtime-config/table-bindings.ts +50 -136
  120. package/src/state-store/layer-key-value-store.ts +8 -3
  121. package/src/state-store/layer-memory.ts +1 -1
  122. package/src/state-store/layer-sql.ts +4 -4
  123. package/src/state-store/schema.ts +2 -2
  124. package/src/state-store/state.ts +14 -2
  125. package/src/status/route.ts +2 -2
  126. package/src/telemetry.ts +20 -28
  127. package/src/webhook/server.ts +39 -40
  128. package/dist/auth/index.d.ts +0 -2
  129. package/dist/auth/index.js +0 -3
  130. package/dist/auth-DvU_CZ7C.js.map +0 -1
  131. package/dist/connector-app/index.d.ts +0 -14
  132. package/dist/connector-app/index.js +0 -12
  133. package/dist/connector-app-BljHd0Pe.js +0 -64
  134. package/dist/connector-app-BljHd0Pe.js.map +0 -1
  135. package/dist/constants-D113Ut1x.js.map +0 -1
  136. package/dist/errors/index.d.ts +0 -2
  137. package/dist/errors/index.js +0 -3
  138. package/dist/errors-DHWylqRZ.js.map +0 -1
  139. package/dist/index-BA8ASHKr.d.ts +0 -31
  140. package/dist/index.d.ts +0 -14
  141. package/dist/index.js +0 -60
  142. package/dist/index.js.map +0 -1
  143. package/dist/ingestion/index.d.ts +0 -6
  144. package/dist/ingestion/index.js +0 -11
  145. package/dist/ingestion-QBGKrFr9.js +0 -200
  146. package/dist/ingestion-QBGKrFr9.js.map +0 -1
  147. package/dist/manifest/index.d.ts +0 -2
  148. package/dist/manifest-P7kBnch9.js.map +0 -1
  149. package/dist/metrics-inkCAxKq.js +0 -136
  150. package/dist/metrics-inkCAxKq.js.map +0 -1
  151. package/dist/metrics.d.ts +0 -4
  152. package/dist/metrics.js +0 -5
  153. package/dist/publisher/index.d.ts +0 -6
  154. package/dist/publisher/index.js +0 -8
  155. package/dist/publisher-BdWJOfer.js +0 -138
  156. package/dist/publisher-BdWJOfer.js.map +0 -1
  157. package/dist/runtime-config/index.d.ts +0 -4
  158. package/dist/runtime-config/index.js +0 -5
  159. package/dist/runtime-config-IbyCVpei.js +0 -60
  160. package/dist/runtime-config-IbyCVpei.js.map +0 -1
  161. package/dist/server-D-eLX3yN.js +0 -183
  162. package/dist/server-D-eLX3yN.js.map +0 -1
  163. package/dist/service-CUjGhe-7.d.ts +0 -32
  164. package/dist/service-DiP-XETo.js +0 -8
  165. package/dist/service-DiP-XETo.js.map +0 -1
  166. package/dist/state-store/index.d.ts +0 -3
  167. package/dist/state-store/index.js +0 -5
  168. package/dist/state-store-BdYulQOY.js.map +0 -1
  169. package/dist/status-BTTd7AUR.js.map +0 -1
  170. package/dist/table-bindings-Dp1DhTeS.js +0 -106
  171. package/dist/table-bindings-Dp1DhTeS.js.map +0 -1
  172. package/dist/telemetry-j_nxwYfh.js.map +0 -1
  173. package/dist/types-jVD5Ys43.d.ts +0 -174
  174. package/dist/webhook/index.d.ts +0 -7
  175. package/dist/webhook/index.js +0 -9
  176. package/dist/webhook-MhFcLW-o.js +0 -13
  177. package/dist/webhook-MhFcLW-o.js.map +0 -1
  178. package/src/publisher/console.ts +0 -18
  179. package/src/publisher/index.ts +0 -7
  180. package/src/publisher/instrumented.ts +0 -75
  181. package/src/publisher/service.ts +0 -37
  182. package/src/publisher/wings.ts +0 -204
@@ -1,67 +1,30 @@
1
- import * as Wings from "@useairfoil/wings";
2
- import { Config, Effect, Schema } from "effect";
1
+ import type { TableIdentifier } from "iceberg-js";
3
2
 
4
- import type { ConnectorDefinition, ResourceName } from "../core/types";
3
+ import { Config, Effect, Schema } from "effect";
5
4
 
6
5
  import { PlatformRuntimeKey } from "./constants";
7
6
  import { RuntimeConfigError } from "./error";
8
7
 
9
- /** Table binding shape accepted in `AIRFOIL_TABLE_BINDINGS`. */
10
- export type HostedTableBinding =
11
- | string
12
- | {
13
- readonly name: string;
14
- readonly partition?: Wings.PartitionValue.Json;
15
- };
16
-
17
- /** Validated table binding ready for the Wings publisher. */
18
- export type ResolvedTableBinding = {
19
- readonly name: string;
20
- readonly partitionValue?: Wings.PartitionValue.PartitionValue;
8
+ type BindingSource = {
9
+ readonly resources: ReadonlyArray<{ readonly name: string }>;
21
10
  };
22
11
 
23
- /** One resolved binding for each resource in a connector manifest. */
24
- export type ResolvedTableBindings<Connector extends ConnectorDefinition> = Readonly<
25
- Record<ResourceName<Connector["resources"]>, ResolvedTableBinding>
12
+ /** An Iceberg table and its storage location for each connector resource. */
13
+ export type TableBinding = TableIdentifier & { readonly location?: string | undefined };
14
+
15
+ export type ResolvedTableBindings<Source extends BindingSource> = Readonly<
16
+ Partial<Record<Source["resources"][number]["name"], TableBinding>>
26
17
  >;
27
18
 
28
- const RawBindingsSchema = Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown));
19
+ const TableIdentifierSchema = Schema.Struct({
20
+ namespace: Schema.NonEmptyArray(Schema.NonEmptyString),
21
+ name: Schema.NonEmptyString,
22
+ location: Schema.optional(Schema.NonEmptyString),
23
+ });
29
24
 
30
- // JSON.parse keeps the last duplicate key, so check before decoding.
31
- const duplicateTopLevelKey = (json: string): string | undefined => {
32
- const keys = new Set<string>();
33
- let depth = 0;
34
- let expectKey = false;
35
- for (let index = 0; index < json.length; index += 1) {
36
- const char = json[index];
37
- if (char === '"') {
38
- const start = index;
39
- for (index += 1; index < json.length; index += 1) {
40
- if (json[index] === "\\") {
41
- index += 1;
42
- } else if (json[index] === '"') {
43
- break;
44
- }
45
- }
46
- if (depth === 1 && expectKey) {
47
- const key = JSON.parse(json.slice(start, index + 1)) as string;
48
- if (keys.has(key)) return key;
49
- keys.add(key);
50
- expectKey = false;
51
- }
52
- continue;
53
- }
54
- if (char === "{") {
55
- depth += 1;
56
- if (depth === 1) expectKey = true;
57
- } else if (char === "}") {
58
- depth -= 1;
59
- } else if (char === "," && depth === 1) {
60
- expectKey = true;
61
- }
62
- }
63
- return undefined;
64
- };
25
+ const RawBindingsSchema = Schema.fromJsonString(
26
+ Schema.Record(Schema.String, TableIdentifierSchema),
27
+ );
65
28
 
66
29
  const asError = (cause: unknown): Error =>
67
30
  cause instanceof Error ? cause : new Error(String(cause));
@@ -69,64 +32,12 @@ const asError = (cause: unknown): Error =>
69
32
  const fail = (code: "TABLE_BINDINGS_INVALID" | "TABLE_BINDINGS_MISMATCH", message: string) =>
70
33
  new RuntimeConfigError({ code, message, cause: new Error(message) });
71
34
 
72
- const isRecord = (value: unknown): value is Record<string, unknown> =>
73
- typeof value === "object" && value !== null && !Array.isArray(value);
74
-
75
- const hasExactKeys = (value: Record<string, unknown>, allowed: ReadonlyArray<string>) => {
76
- const keys = Object.keys(value);
77
- return (
78
- keys.length === allowed.filter((key) => key in value).length &&
79
- keys.every((key) => allowed.includes(key))
80
- );
81
- };
82
-
83
- const nonEmptyName = (value: unknown): string => {
84
- if (typeof value !== "string" || value.length === 0 || value.trim() !== value) {
85
- throw fail("TABLE_BINDINGS_INVALID", "Table binding names must be non-empty strings");
86
- }
87
- return value;
88
- };
89
-
90
- const resolveBinding = (input: unknown): Effect.Effect<ResolvedTableBinding, RuntimeConfigError> =>
91
- Effect.gen(function* () {
92
- const binding = yield* Effect.try({
93
- try: () => {
94
- if (typeof input === "string") return { name: nonEmptyName(input) } as const;
95
- if (!isRecord(input) || !hasExactKeys(input, ["name", "partition"])) {
96
- throw fail(
97
- "TABLE_BINDINGS_INVALID",
98
- "Table bindings must be a name string or an object containing name and optional partition",
99
- );
100
- }
101
- return { name: nonEmptyName(input.name), partition: input.partition } as const;
102
- },
103
- catch: (cause) =>
104
- cause instanceof RuntimeConfigError
105
- ? cause
106
- : fail("TABLE_BINDINGS_INVALID", "Hosted table binding is invalid"),
107
- });
108
- if (!("partition" in binding) || binding.partition === undefined) {
109
- return { name: binding.name };
110
- }
111
- const partitionValue = yield* Wings.PartitionValue.decodeJson(binding.partition).pipe(
112
- Effect.mapError(
113
- (cause) =>
114
- new RuntimeConfigError({
115
- code: "TABLE_BINDINGS_INVALID",
116
- message: "Hosted table binding has an invalid partition value",
117
- cause,
118
- }),
119
- ),
120
- );
121
- return { name: binding.name, partitionValue };
122
- });
123
-
124
- /** Loads and validates the complete hosted Wings table map for a connector. */
125
- export const loadTableBindings = <const Connector extends ConnectorDefinition>(
126
- connector: Connector,
127
- ): Effect.Effect<ResolvedTableBindings<Connector>, RuntimeConfigError> =>
35
+ /** Reads hosted table bindings and checks they name resources of the connector. */
36
+ export const loadTableBindings = <const Source extends BindingSource>(
37
+ connector: Source,
38
+ ): Effect.Effect<ResolvedTableBindings<Source>, RuntimeConfigError> =>
128
39
  Effect.gen(function* () {
129
- const raw = yield* Config.nonEmptyString(PlatformRuntimeKey.tableBindings).pipe(
40
+ const raw = yield* Config.NonEmptyString(PlatformRuntimeKey.tableBindings).pipe(
130
41
  Effect.mapError(
131
42
  (cause) =>
132
43
  new RuntimeConfigError({
@@ -136,45 +47,48 @@ export const loadTableBindings = <const Connector extends ConnectorDefinition>(
136
47
  }),
137
48
  ),
138
49
  );
139
- const duplicate = yield* Effect.try({
140
- try: () => duplicateTopLevelKey(raw),
141
- catch: (cause) =>
142
- new RuntimeConfigError({
143
- code: "TABLE_BINDINGS_INVALID",
144
- message: "Hosted table bindings are not valid JSON",
145
- cause: asError(cause),
146
- }),
147
- });
148
- if (duplicate !== undefined) {
149
- return yield* Effect.fail(
150
- fail("TABLE_BINDINGS_INVALID", "Hosted table bindings contain a duplicate resource"),
151
- );
152
- }
153
- const decoded = yield* Schema.decodeUnknownEffect(RawBindingsSchema)(raw).pipe(
50
+
51
+ const decoded = yield* Schema.decodeUnknownEffect(RawBindingsSchema)(raw, {
52
+ onExcessProperty: "error",
53
+ }).pipe(
154
54
  Effect.mapError(
155
55
  (cause) =>
156
56
  new RuntimeConfigError({
157
57
  code: "TABLE_BINDINGS_INVALID",
158
- message: "Hosted table bindings are not valid JSON",
58
+ message: "Hosted table bindings are invalid",
159
59
  cause: asError(cause),
160
60
  }),
161
61
  ),
162
62
  );
163
- // Starting with a partial map would leave some manifest resources unwired.
164
- const expected = new Set(connector.resources.map((resource) => resource.name));
63
+
165
64
  const actual = Object.keys(decoded);
166
- const missing = [...expected].filter((name) => !(name in decoded));
65
+ if (actual.length === 0) {
66
+ return yield* Effect.fail(
67
+ fail("TABLE_BINDINGS_INVALID", "Hosted table bindings name no resources"),
68
+ );
69
+ }
70
+
71
+ // Bindings come from the platform, resources from the connector. An unknown name
72
+ // means one of them is out of date, and we could write to the wrong table.
73
+ const expected = new Set(connector.resources.map((resource) => resource.name));
167
74
  const unknown = actual.filter((name) => !expected.has(name));
168
- if (missing.length > 0 || unknown.length > 0) {
75
+ if (unknown.length > 0) {
169
76
  return yield* Effect.fail(
170
77
  fail(
171
78
  "TABLE_BINDINGS_MISMATCH",
172
- `Hosted table bindings do not match connector resources (missing: ${missing.join(", ") || "none"}; unknown resources: ${unknown.length})`,
79
+ `Hosted table bindings name resources the connector doesn't have (${unknown.length})`,
173
80
  ),
174
81
  );
175
82
  }
176
- const entries = yield* Effect.forEach(actual, (name) =>
177
- resolveBinding(decoded[name]).pipe(Effect.map((binding) => [name, binding] as const)),
178
- );
179
- return Object.fromEntries(entries) as ResolvedTableBindings<Connector>;
83
+
84
+ return Object.fromEntries(
85
+ Object.entries(decoded).map(([resource, identifier]) => [
86
+ resource,
87
+ {
88
+ namespace: Array.from(identifier.namespace),
89
+ name: identifier.name,
90
+ ...(identifier.location === undefined ? {} : { location: identifier.location }),
91
+ },
92
+ ]),
93
+ ) as ResolvedTableBindings<Source>;
180
94
  });
@@ -1,5 +1,5 @@
1
1
  import { DateTime, Effect, Layer, Option, Schema } from "effect";
2
- import { KeyValueStore } from "effect/unstable/persistence";
2
+ import { KeyValueStore } from "effect/persistence";
3
3
 
4
4
  import { ConnectorError } from "../errors";
5
5
  import { resourceComponentKey, resourceErrorKey } from "./keys";
@@ -33,8 +33,13 @@ const resourceErrorContext = (
33
33
  switch (operation) {
34
34
  case "fetch":
35
35
  return { source, operation, code: "fetch_failed", message: `${sourceName} fetch failed` };
36
- case "publish":
37
- return { source, operation, code: "publish_failed", message: `${sourceName} publish failed` };
36
+ case "ingest":
37
+ return {
38
+ source,
39
+ operation,
40
+ code: "ingest_failed",
41
+ message: `${sourceName} ingestion failed`,
42
+ };
38
43
  case "checkpoint":
39
44
  return {
40
45
  source,
@@ -1,5 +1,5 @@
1
1
  import { Layer } from "effect";
2
- import { KeyValueStore } from "effect/unstable/persistence";
2
+ import { KeyValueStore } from "effect/persistence";
3
3
 
4
4
  import type { StateStore } from "./service";
5
5
 
@@ -1,6 +1,6 @@
1
1
  import { Config, Effect, Layer } from "effect";
2
- import { KeyValueStore } from "effect/unstable/persistence";
3
- import { SqlClient } from "effect/unstable/sql";
2
+ import { KeyValueStore } from "effect/persistence";
3
+ import { SqlClient } from "effect/sql";
4
4
 
5
5
  import type { StateStore } from "./service";
6
6
 
@@ -32,8 +32,8 @@ const scopedKeyValueStore = (connectorInstanceId: string) =>
32
32
  export const layerSql = (): Layer.Layer<StateStore, Config.ConfigError, SqlClient.SqlClient> =>
33
33
  Layer.unwrap(
34
34
  Config.all({
35
- connectorInstanceId: Config.nonEmptyString(PlatformRuntimeKey.connectorInstanceId),
36
- table: Config.nonEmptyString(PlatformRuntimeKey.stateTable).pipe(
35
+ connectorInstanceId: Config.NonEmptyString(PlatformRuntimeKey.connectorInstanceId),
36
+ table: Config.NonEmptyString(PlatformRuntimeKey.stateTable).pipe(
37
37
  Config.withDefault(PlatformRuntimeDefault.stateTable),
38
38
  ),
39
39
  }).pipe(
@@ -18,11 +18,11 @@ export const ChangesStateSchema = Schema.Struct({
18
18
 
19
19
  export const StateSourceSchema = Schema.Literals(["backfill", "changes"]);
20
20
 
21
- export const StateOperationSchema = Schema.Literals(["fetch", "publish", "checkpoint"]);
21
+ export const StateOperationSchema = Schema.Literals(["fetch", "ingest", "checkpoint"]);
22
22
 
23
23
  export const StateErrorCodeSchema = Schema.Literals([
24
24
  "fetch_failed",
25
- "publish_failed",
25
+ "ingest_failed",
26
26
  "checkpoint_failed",
27
27
  ]);
28
28
 
@@ -1,16 +1,28 @@
1
- import type { Cursor, ResourceDefinition, ResourceState, SyncState } from "../core/types";
1
+ import type {
2
+ ConnectorDefinition,
3
+ Cursor,
4
+ ResourceDefinition,
5
+ ResourceState,
6
+ SyncState,
7
+ } from "../core/types";
2
8
 
3
9
  /** Normalizes a cursor value to its persisted/wire representation (never a Date). */
4
10
  export const normalizeCursor = (value: Cursor.Value): string | number =>
5
11
  value instanceof Date ? value.toISOString() : value;
6
12
 
13
+ /** Whether a resource gets changes, from its own `changes` or the connector's feed. */
14
+ export const hasChanges = (connector: ConnectorDefinition, resource: ResourceDefinition) =>
15
+ resource.changes !== undefined ||
16
+ (connector.changes?.resources.some((member) => member.name === resource.name) ?? false);
17
+
7
18
  /** Derives connector-facing sync state from durable state and resource capabilities. */
8
19
  export const deriveSyncState = (
20
+ connector: ConnectorDefinition,
9
21
  resource: ResourceDefinition,
10
22
  state: ResourceState | undefined,
11
23
  ): SyncState => {
12
24
  if (state?.lastError) return "error";
13
- if (!resource.backfill && !resource.changes) return "live";
25
+ if (!resource.backfill && !hasChanges(connector, resource)) return "live";
14
26
  if (!state) return "pending";
15
27
  if (resource.backfill && (!state.backfill || !state.backfill.completed)) return "backfilling";
16
28
  return "live";
@@ -1,5 +1,5 @@
1
1
  import { Cause, Effect } from "effect";
2
- import { HttpRouter, HttpServerResponse } from "effect/unstable/http";
2
+ import { HttpRouter, HttpServerResponse } from "effect/http";
3
3
 
4
4
  import type { ConnectorDefinition } from "../core/types";
5
5
  import type { ResourceStatus, StatusUnavailableResponse } from "./types";
@@ -24,7 +24,7 @@ export const statusRoute = (
24
24
  Effect.map(
25
25
  (state): ResourceStatus => ({
26
26
  name: resource.name,
27
- state: deriveSyncState(resource, state),
27
+ state: deriveSyncState(connector, resource, state),
28
28
  ...(state?.backfill
29
29
  ? {
30
30
  backfill: {
package/src/telemetry.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { Cause, Config, Effect, Layer, Metric, Option, type Duration } from "effect";
2
- import { Headers } from "effect/unstable/http";
3
- import * as Observability from "effect/unstable/observability";
1
+ import { Cause, Config, Effect, Layer, Metric, Option, Schema, type Duration } from "effect";
2
+ import { Headers } from "effect/http";
3
+ import * as Observability from "effect/observability";
4
4
 
5
5
  import { PlatformRuntimeKey } from "./runtime-config/constants";
6
6
 
@@ -9,7 +9,7 @@ export const SpanName = {
9
9
  batchProcess: "connector.batch.process",
10
10
  webhookDecode: "connector.webhook.decode",
11
11
  webhookHandle: "connector.webhook.handle",
12
- publish: "connector.publish",
12
+ ingest: "connector.ingest",
13
13
  stateSet: "connector.state.set",
14
14
  apiFetch: "connector.api.fetch",
15
15
  } as const;
@@ -20,13 +20,13 @@ export const Attr = {
20
20
  connectorResourcesCount: "airfoil.connector.resources.count",
21
21
  resourceName: "airfoil.resource.name",
22
22
  resourceSource: "airfoil.resource.source",
23
- batchMutations: "airfoil.batch.mutations",
23
+ batchRows: "airfoil.batch.rows",
24
24
  batchOutcome: "airfoil.batch.outcome",
25
25
  webhookPath: "airfoil.webhook.path",
26
26
  webhookOutcome: "airfoil.webhook.outcome",
27
27
  syncState: "airfoil.connector.sync.state",
28
28
  apiRetryReason: "airfoil.connector.api.retry.reason",
29
- publisherSuccess: "airfoil.publisher.success",
29
+ ingestionSuccess: "airfoil.ingestion.success",
30
30
  stateKey: "airfoil.state.key",
31
31
  apiPath: "airfoil.api.path",
32
32
  errorPhase: "airfoil.error.phase",
@@ -58,9 +58,16 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
58
58
  const maxErrorMessageLength = 500;
59
59
 
60
60
  const OtlpEnvConfig = Config.all({
61
- enabled: Config.boolean(PlatformRuntimeKey.otelEnabled).pipe(Config.withDefault(false)),
62
- baseUrl: Config.option(Config.string(PlatformRuntimeKey.otelExporterOtlpEndpoint)),
63
- rawHeaders: Config.option(Config.string(PlatformRuntimeKey.otelExporterOtlpHeaders)),
61
+ enabled: Config.Boolean(PlatformRuntimeKey.otelEnabled).pipe(Config.withDefault(false)),
62
+ baseUrl: Config.option(Config.String(PlatformRuntimeKey.otelExporterOtlpEndpoint)),
63
+ headers: Config.option(
64
+ Config.Record(
65
+ Schema.String,
66
+ Schema.StringFromUriComponent,
67
+ PlatformRuntimeKey.otelExporterOtlpHeaders,
68
+ { trim: true },
69
+ ),
70
+ ),
64
71
  });
65
72
 
66
73
  const defaultRedactedHeaders: ReadonlyArray<string | RegExp> = [
@@ -74,19 +81,6 @@ const defaultRedactedHeaders: ReadonlyArray<string | RegExp> = [
74
81
  /token/i,
75
82
  ];
76
83
 
77
- const parseOtelHeaders = (value: string): Record<string, string> =>
78
- Object.fromEntries(
79
- value
80
- .split(",")
81
- .map((entry) => entry.trim())
82
- .filter(Boolean)
83
- .flatMap((entry) => {
84
- const separator = entry.indexOf("=");
85
- if (separator < 1) return [];
86
- return [[entry.slice(0, separator).trim(), entry.slice(separator + 1).trim()]];
87
- }),
88
- );
89
-
90
84
  const appendPath = (baseUrl: string, path: string) =>
91
85
  `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
92
86
 
@@ -223,8 +217,8 @@ export const layer = (config: OtlpTracingConfig = {}, options: OtlpTracingOption
223
217
  *
224
218
  * @example
225
219
  * Telemetry.layerConfig({
226
- * enabled: Config.boolean("MY_OTEL_ENABLED"),
227
- * endpoint: Config.string("MY_COLLECTOR_URL"),
220
+ * enabled: Config.Boolean("MY_OTEL_ENABLED"),
221
+ * endpoint: Config.String("MY_COLLECTOR_URL"),
228
222
  * })
229
223
  */
230
224
  export const layerConfig = (
@@ -254,9 +248,7 @@ export const layerOtlpTracing = (options: OtlpTracingOptions = {}) =>
254
248
  {
255
249
  enabled: raw.enabled,
256
250
  endpoint: Option.getOrUndefined(raw.baseUrl),
257
- headers: Option.isSome(raw.rawHeaders)
258
- ? parseOtelHeaders(raw.rawHeaders.value)
259
- : undefined,
251
+ headers: Option.getOrUndefined(raw.headers),
260
252
  },
261
253
  options,
262
254
  );
@@ -276,7 +268,7 @@ export const layerOtlpMetrics = (options: OtlpMetricsOptions = {}) =>
276
268
  );
277
269
  return Observability.OtlpMetrics.layer({
278
270
  url: appendPath(endpoint, "/v1/metrics"),
279
- headers: Option.isSome(raw.rawHeaders) ? parseOtelHeaders(raw.rawHeaders.value) : undefined,
271
+ headers: Option.getOrUndefined(raw.headers),
280
272
  exportInterval: options.exportInterval,
281
273
  temporality: "cumulative",
282
274
  }).pipe(Layer.provide(Observability.OtlpSerialization.layerJson));
@@ -1,18 +1,16 @@
1
1
  import { Cause, Context, Effect, Queue, Schema } from "effect";
2
- import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
2
+ import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
3
3
 
4
4
  import type {
5
5
  ResourceBatch,
6
6
  ResourceDefinition,
7
- ResourceMutation,
8
- WebhookHandler,
9
7
  WebhookRoute,
10
8
  WebhookRouteContext,
11
9
  } from "../core/types";
12
10
 
13
11
  import { ConnectorError } from "../errors";
12
+ import { ingestBatch } from "../ingestor/instrumented";
14
13
  import * as Metrics from "../metrics";
15
- import { publishBatch } from "../publisher/instrumented";
16
14
  import { Attr, SpanName, annotateError } from "../telemetry";
17
15
 
18
16
  export type QueuedWebhookBatch = {
@@ -27,39 +25,23 @@ export class WebhookQueue extends Context.Service<
27
25
  }
28
26
  >()("@useairfoil/connector-kit/WebhookQueue") {}
29
27
 
30
- const resourceBatch = <Row extends object>(
31
- mutations: ReadonlyArray<ResourceMutation<Row>>,
32
- ): ResourceBatch<Row> => ({
33
- mutations,
28
+ const resourceBatch = <Row extends object>(rows: ReadonlyArray<Row>): ResourceBatch<Row> => ({
29
+ rows,
34
30
  });
35
31
 
36
- const decodeAndHandleWebhook = <Row extends object, Payload>(
37
- resourceName: string,
38
- webhook: WebhookHandler<Row, Payload>,
39
- payload: unknown,
40
- ) =>
41
- Schema.decodeUnknownEffect(webhook.schema)(payload).pipe(
42
- Effect.mapError(
43
- (cause) =>
44
- new ConnectorError({
45
- message: `Invalid webhook payload for resource ${resourceName}`,
46
- cause,
47
- }),
48
- ),
49
- Effect.flatMap((decoded) => webhook.handler({ payload: decoded })),
50
- );
51
-
52
32
  const makeRouteContext = (
53
33
  request: HttpServerRequest.HttpServerRequest,
54
34
  rawBody: Uint8Array,
55
35
  payload: unknown,
56
36
  batches: Array<QueuedWebhookBatch>,
37
+ resourceNames: ReadonlyArray<string>,
57
38
  ): WebhookRouteContext<ReadonlyArray<ResourceDefinition>, unknown> => ({
58
39
  request,
59
40
  rawBody,
60
41
  payload,
61
42
  to: (resource, payload) =>
62
43
  Effect.gen(function* () {
44
+ if (!resourceNames.includes(resource.name)) return;
63
45
  if (!resource.webhook) {
64
46
  return yield* Effect.fail(
65
47
  new ConnectorError({
@@ -68,27 +50,28 @@ const makeRouteContext = (
68
50
  );
69
51
  }
70
52
 
71
- const mutations = yield* decodeAndHandleWebhook(resource.name, resource.webhook, payload);
53
+ // The route already decoded the payload, so don't decode it again.
54
+ const rows = yield* resource.webhook.handler({ payload });
72
55
  batches.push({
73
56
  resource: resource.name,
74
- batch: resourceBatch(mutations),
57
+ batch: resourceBatch(rows),
75
58
  });
76
59
  }),
77
60
  });
78
61
 
79
62
  const compactBatches = (batches: ReadonlyArray<QueuedWebhookBatch>) => {
80
- const grouped = new Map<string, ResourceMutation[]>();
63
+ const grouped = new Map<string, object[]>();
81
64
  for (const item of batches) {
82
65
  const current = grouped.get(item.resource) ?? [];
83
- grouped.set(item.resource, [...current, ...item.batch.mutations]);
66
+ grouped.set(item.resource, [...current, ...item.batch.rows]);
84
67
  }
85
- return Array.from(grouped.entries()).map(([resource, mutations]) => ({
68
+ return Array.from(grouped.entries()).map(([resource, rows]) => ({
86
69
  resource,
87
- batch: resourceBatch(mutations),
70
+ batch: resourceBatch(rows),
88
71
  }));
89
72
  };
90
73
 
91
- const publishBatches = (
74
+ const ingestBatches = (
92
75
  batches: ReadonlyArray<QueuedWebhookBatch>,
93
76
  options: { readonly connectorName: string },
94
77
  ) =>
@@ -96,7 +79,7 @@ const publishBatches = (
96
79
  yield* Effect.forEach(
97
80
  batches,
98
81
  (batch) =>
99
- publishBatch({
82
+ ingestBatch({
100
83
  connector: options.connectorName,
101
84
  resource: batch.resource,
102
85
  source: "webhook",
@@ -118,7 +101,13 @@ const parseJson = (rawBody: Uint8Array) =>
118
101
  catch: (cause) => new ConnectorError({ message: "Invalid webhook JSON", cause }),
119
102
  });
120
103
 
121
- const makeHandler = (route: WebhookRoute, options: { readonly connectorName: string }) =>
104
+ type RouterOptions = {
105
+ readonly connectorName: string;
106
+ /** Events for other resources are skipped. */
107
+ readonly resourceNames: ReadonlyArray<string>;
108
+ };
109
+
110
+ const makeHandler = (route: WebhookRoute, options: RouterOptions) =>
122
111
  Effect.gen(function* () {
123
112
  const request = yield* HttpServerRequest.HttpServerRequest;
124
113
  const collected: Array<QueuedWebhookBatch> = [];
@@ -129,6 +118,7 @@ const makeHandler = (route: WebhookRoute, options: { readonly connectorName: str
129
118
  onSuccess: (value) => ({ _tag: "Success" as const, value }),
130
119
  }),
131
120
  );
121
+
132
122
  if (rawBodyResult._tag === "Error") {
133
123
  yield* Metrics.recordWebhookRequest({
134
124
  connector: options.connectorName,
@@ -144,6 +134,7 @@ const makeHandler = (route: WebhookRoute, options: { readonly connectorName: str
144
134
  onSuccess: (value) => ({ _tag: "Success" as const, value }),
145
135
  }),
146
136
  );
137
+
147
138
  if (jsonResult._tag === "Error") {
148
139
  yield* Metrics.recordWebhookRequest({
149
140
  connector: options.connectorName,
@@ -159,6 +150,7 @@ const makeHandler = (route: WebhookRoute, options: { readonly connectorName: str
159
150
  onSuccess: (value) => ({ _tag: "Success" as const, value }),
160
151
  }),
161
152
  );
153
+
162
154
  if (payloadResult._tag === "Error") {
163
155
  yield* Metrics.recordWebhookRequest({
164
156
  connector: options.connectorName,
@@ -170,7 +162,15 @@ const makeHandler = (route: WebhookRoute, options: { readonly connectorName: str
170
162
 
171
163
  const response = yield* Effect.withSpan(
172
164
  route
173
- .handler(makeRouteContext(request, rawBodyResult.value, payloadResult.value, collected))
165
+ .handler(
166
+ makeRouteContext(
167
+ request,
168
+ rawBodyResult.value,
169
+ payloadResult.value,
170
+ collected,
171
+ options.resourceNames,
172
+ ),
173
+ )
174
174
  .pipe(Effect.tapError((error) => annotateError("webhook_handle", error))),
175
175
  SpanName.webhookHandle,
176
176
  { attributes: { [Attr.webhookPath]: route.path } },
@@ -188,12 +188,13 @@ const makeHandler = (route: WebhookRoute, options: { readonly connectorName: str
188
188
  path: String(route.path),
189
189
  outcome: response.status >= 400 ? "rejected" : "ok",
190
190
  });
191
+
191
192
  return response;
192
193
  }
193
194
 
194
195
  yield* Effect.withSpan(
195
- publishBatches(batches, { connectorName: options.connectorName }).pipe(
196
- Effect.tapError((error) => annotateError("webhook_publish", error)),
196
+ ingestBatches(batches, { connectorName: options.connectorName }).pipe(
197
+ Effect.tapError((error) => annotateError("webhook_ingest", error)),
197
198
  ),
198
199
  SpanName.webhookHandle,
199
200
  { attributes: { [Attr.webhookPath]: route.path } },
@@ -204,6 +205,7 @@ const makeHandler = (route: WebhookRoute, options: { readonly connectorName: str
204
205
  path: String(route.path),
205
206
  outcome: response.status >= 400 ? "rejected" : "ok",
206
207
  });
208
+
207
209
  return response;
208
210
  }).pipe(
209
211
  Effect.catchCause((cause) =>
@@ -225,10 +227,7 @@ const makeHandler = (route: WebhookRoute, options: { readonly connectorName: str
225
227
  ),
226
228
  );
227
229
 
228
- export const router = (
229
- routes: ReadonlyArray<WebhookRoute>,
230
- options: { readonly connectorName: string },
231
- ) =>
230
+ export const router = (routes: ReadonlyArray<WebhookRoute>, options: RouterOptions) =>
232
231
  HttpRouter.addAll(
233
232
  routes.map((route) => HttpRouter.route("POST", route.path, makeHandler(route, options))),
234
233
  );
@@ -1,2 +0,0 @@
1
- import { i as makeTokenCache, n as ExpiringToken, r as TokenCache } from "../index-DXa26Ry5.js";
2
- export { ExpiringToken, TokenCache, makeTokenCache };
@@ -1,3 +0,0 @@
1
- import { n as makeTokenCache } from "../auth-DvU_CZ7C.js";
2
-
3
- export { makeTokenCache };
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth-DvU_CZ7C.js","names":[],"sources":["../src/auth/token-cache.ts","../src/auth/index.ts"],"sourcesContent":["import { Cache, Duration, Effect, Exit, Number, Redacted } from \"effect\";\n\nexport interface ExpiringToken {\n /** The credential kept in memory by the cache. */\n readonly value: Redacted.Redacted<string>;\n /** The lifetime reported by the provider. */\n readonly expiresIn: Duration.Duration;\n}\n\nexport interface TokenCache<E> {\n /** Returns the cached token or performs one shared acquisition. */\n readonly get: Effect.Effect<Redacted.Redacted<string>, E>;\n /** Removes the cached token so the next `get` reacquires it. */\n readonly invalidate: Effect.Effect<void>;\n}\n\n/**\n * Creates a lazy, in-memory token cache around an acquisition effect.\n *\n * `refreshFactor` controls how much of the provider lifetime is cached. It is\n * clamped between `0` and `1`.\n *\n * @example\n * ```ts\n * const cache = yield* makeTokenCache({\n * acquire: exchangeCredentials,\n * refreshFactor: 0.5,\n * })\n *\n * const token = yield* cache.get\n * ```\n */\nexport const makeTokenCache = <E, R>(options: {\n readonly acquire: Effect.Effect<ExpiringToken, E, R>;\n readonly refreshFactor?: number;\n}): Effect.Effect<TokenCache<E>, never, R> =>\n Effect.gen(function* () {\n const refreshFactor = Number.clamp(options.refreshFactor ?? 0.5, {\n minimum: 0,\n maximum: 1,\n });\n const cache = yield* Cache.makeWith((_: void) => options.acquire, {\n capacity: 1,\n timeToLive: (exit) =>\n Exit.isSuccess(exit)\n ? Duration.millis(Duration.toMillis(exit.value.expiresIn) * refreshFactor)\n : Duration.zero,\n });\n\n return {\n get: Cache.get(cache, undefined).pipe(Effect.map((token) => token.value)),\n invalidate: Cache.invalidate(cache, undefined),\n };\n });\n","export * from \"./token-cache\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAgCA,MAAa,kBAAwB,YAInC,OAAO,IAAI,aAAa;CACtB,MAAM,gBAAgB,OAAO,MAAM,QAAQ,iBAAiB,IAAK;EAC/D,SAAS;EACT,SAAS;EACV,CAAC;CACF,MAAM,QAAQ,OAAO,MAAM,UAAU,MAAY,QAAQ,SAAS;EAChE,UAAU;EACV,aAAa,SACX,KAAK,UAAU,KAAK,GAChB,SAAS,OAAO,SAAS,SAAS,KAAK,MAAM,UAAU,GAAG,cAAc,GACxE,SAAS;EAChB,CAAC;AAEF,QAAO;EACL,KAAK,MAAM,IAAI,OAAO,OAAU,CAAC,KAAK,OAAO,KAAK,UAAU,MAAM,MAAM,CAAC;EACzE,YAAY,MAAM,WAAW,OAAO,OAAU;EAC/C;EACD"}