@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
|
@@ -1,67 +1,28 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { TableIdentifier } from "iceberg-js";
|
|
2
|
+
|
|
2
3
|
import { Config, Effect, Schema } from "effect";
|
|
3
4
|
|
|
4
|
-
import type {
|
|
5
|
+
import type { TableSource } from "../core/types";
|
|
5
6
|
|
|
6
7
|
import { PlatformRuntimeKey } from "./constants";
|
|
7
8
|
import { RuntimeConfigError } from "./error";
|
|
8
9
|
|
|
9
|
-
/**
|
|
10
|
-
export type
|
|
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;
|
|
21
|
-
};
|
|
10
|
+
/** An Iceberg table and its storage location for each connector resource. */
|
|
11
|
+
export type TableBinding = TableIdentifier & { readonly location?: string | undefined };
|
|
22
12
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
Record<ResourceName<Connector["resources"]>, ResolvedTableBinding>
|
|
13
|
+
export type ResolvedTableBindings<Source extends TableSource> = Readonly<
|
|
14
|
+
Partial<Record<Source["resources"][number]["name"], TableBinding>>
|
|
26
15
|
>;
|
|
27
16
|
|
|
28
|
-
const
|
|
17
|
+
const TableIdentifierSchema = Schema.Struct({
|
|
18
|
+
namespace: Schema.NonEmptyArray(Schema.NonEmptyString),
|
|
19
|
+
name: Schema.NonEmptyString,
|
|
20
|
+
location: Schema.optional(Schema.NonEmptyString),
|
|
21
|
+
});
|
|
29
22
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
};
|
|
23
|
+
const RawBindingsSchema = Schema.fromJsonString(
|
|
24
|
+
Schema.Record(Schema.String, TableIdentifierSchema),
|
|
25
|
+
);
|
|
65
26
|
|
|
66
27
|
const asError = (cause: unknown): Error =>
|
|
67
28
|
cause instanceof Error ? cause : new Error(String(cause));
|
|
@@ -69,64 +30,12 @@ const asError = (cause: unknown): Error =>
|
|
|
69
30
|
const fail = (code: "TABLE_BINDINGS_INVALID" | "TABLE_BINDINGS_MISMATCH", message: string) =>
|
|
70
31
|
new RuntimeConfigError({ code, message, cause: new Error(message) });
|
|
71
32
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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> =>
|
|
33
|
+
/** Reads hosted table bindings and checks they name resources of the connector. */
|
|
34
|
+
export const loadTableBindings = <const Source extends TableSource>(
|
|
35
|
+
connector: Source,
|
|
36
|
+
): Effect.Effect<ResolvedTableBindings<Source>, RuntimeConfigError> =>
|
|
91
37
|
Effect.gen(function* () {
|
|
92
|
-
const
|
|
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> =>
|
|
128
|
-
Effect.gen(function* () {
|
|
129
|
-
const raw = yield* Config.nonEmptyString(PlatformRuntimeKey.tableBindings).pipe(
|
|
38
|
+
const raw = yield* Config.NonEmptyString(PlatformRuntimeKey.tableBindings).pipe(
|
|
130
39
|
Effect.mapError(
|
|
131
40
|
(cause) =>
|
|
132
41
|
new RuntimeConfigError({
|
|
@@ -136,45 +45,48 @@ export const loadTableBindings = <const Connector extends ConnectorDefinition>(
|
|
|
136
45
|
}),
|
|
137
46
|
),
|
|
138
47
|
);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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(
|
|
48
|
+
|
|
49
|
+
const decoded = yield* Schema.decodeUnknownEffect(RawBindingsSchema)(raw, {
|
|
50
|
+
onExcessProperty: "error",
|
|
51
|
+
}).pipe(
|
|
154
52
|
Effect.mapError(
|
|
155
53
|
(cause) =>
|
|
156
54
|
new RuntimeConfigError({
|
|
157
55
|
code: "TABLE_BINDINGS_INVALID",
|
|
158
|
-
message: "Hosted table bindings are
|
|
56
|
+
message: "Hosted table bindings are invalid",
|
|
159
57
|
cause: asError(cause),
|
|
160
58
|
}),
|
|
161
59
|
),
|
|
162
60
|
);
|
|
163
|
-
|
|
164
|
-
const expected = new Set(connector.resources.map((resource) => resource.name));
|
|
61
|
+
|
|
165
62
|
const actual = Object.keys(decoded);
|
|
166
|
-
|
|
63
|
+
if (actual.length === 0) {
|
|
64
|
+
return yield* Effect.fail(
|
|
65
|
+
fail("TABLE_BINDINGS_INVALID", "Hosted table bindings name no resources"),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Bindings come from the platform, resources from the connector. An unknown name
|
|
70
|
+
// means one of them is out of date, and we could write to the wrong table.
|
|
71
|
+
const expected = new Set(connector.resources.map((resource) => resource.name));
|
|
167
72
|
const unknown = actual.filter((name) => !expected.has(name));
|
|
168
|
-
if (
|
|
73
|
+
if (unknown.length > 0) {
|
|
169
74
|
return yield* Effect.fail(
|
|
170
75
|
fail(
|
|
171
76
|
"TABLE_BINDINGS_MISMATCH",
|
|
172
|
-
`Hosted table bindings
|
|
77
|
+
`Hosted table bindings name resources the connector doesn't have (${unknown.length})`,
|
|
173
78
|
),
|
|
174
79
|
);
|
|
175
80
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
81
|
+
|
|
82
|
+
return Object.fromEntries(
|
|
83
|
+
Object.entries(decoded).map(([resource, identifier]) => [
|
|
84
|
+
resource,
|
|
85
|
+
{
|
|
86
|
+
namespace: Array.from(identifier.namespace),
|
|
87
|
+
name: identifier.name,
|
|
88
|
+
...(identifier.location === undefined ? {} : { location: identifier.location }),
|
|
89
|
+
},
|
|
90
|
+
]),
|
|
91
|
+
) as ResolvedTableBindings<Source>;
|
|
180
92
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DateTime, Effect, Layer, Option, Schema } from "effect";
|
|
2
|
-
import { KeyValueStore } from "effect/
|
|
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 "
|
|
37
|
-
return {
|
|
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,6 +1,6 @@
|
|
|
1
1
|
import { Config, Effect, Layer } from "effect";
|
|
2
|
-
import { KeyValueStore } from "effect/
|
|
3
|
-
import { SqlClient } from "effect/
|
|
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.
|
|
36
|
-
table: Config.
|
|
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", "
|
|
21
|
+
export const StateOperationSchema = Schema.Literals(["fetch", "ingest", "checkpoint"]);
|
|
22
22
|
|
|
23
23
|
export const StateErrorCodeSchema = Schema.Literals([
|
|
24
24
|
"fetch_failed",
|
|
25
|
-
"
|
|
25
|
+
"ingest_failed",
|
|
26
26
|
"checkpoint_failed",
|
|
27
27
|
]);
|
|
28
28
|
|
package/src/state-store/state.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
|
-
import type {
|
|
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
|
|
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";
|
package/src/status/route.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Cause, Effect } from "effect";
|
|
2
|
-
import { HttpRouter, HttpServerResponse } from "effect/
|
|
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
1
|
import { Cause, Config, Effect, Layer, Metric, Option, type Duration } from "effect";
|
|
2
|
-
import { Headers } from "effect/
|
|
3
|
-
import * as Observability 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,9 @@ 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.
|
|
62
|
-
baseUrl: Config.option(Config.
|
|
63
|
-
rawHeaders: Config.option(Config.
|
|
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)),
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
const defaultRedactedHeaders: ReadonlyArray<string | RegExp> = [
|
|
@@ -223,8 +223,8 @@ export const layer = (config: OtlpTracingConfig = {}, options: OtlpTracingOption
|
|
|
223
223
|
*
|
|
224
224
|
* @example
|
|
225
225
|
* Telemetry.layerConfig({
|
|
226
|
-
* enabled: Config.
|
|
227
|
-
* endpoint: Config.
|
|
226
|
+
* enabled: Config.Boolean("MY_OTEL_ENABLED"),
|
|
227
|
+
* endpoint: Config.String("MY_COLLECTOR_URL"),
|
|
228
228
|
* })
|
|
229
229
|
*/
|
|
230
230
|
export const layerConfig = (
|
package/src/webhook/server.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import { Cause, Context, Effect, Queue, Schema } from "effect";
|
|
2
|
-
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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.
|
|
66
|
+
grouped.set(item.resource, [...current, ...item.batch.rows]);
|
|
84
67
|
}
|
|
85
|
-
return Array.from(grouped.entries()).map(([resource,
|
|
68
|
+
return Array.from(grouped.entries()).map(([resource, rows]) => ({
|
|
86
69
|
resource,
|
|
87
|
-
batch: resourceBatch(
|
|
70
|
+
batch: resourceBatch(rows),
|
|
88
71
|
}));
|
|
89
72
|
};
|
|
90
73
|
|
|
91
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
196
|
-
Effect.tapError((error) => annotateError("
|
|
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
|
);
|
package/dist/auth/index.d.ts
DELETED
package/dist/auth/index.js
DELETED
|
@@ -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"}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import "../index-DXa26Ry5.js";
|
|
2
|
-
import "../index-DKoRwyFn.js";
|
|
3
|
-
import "../types-jVD5Ys43.js";
|
|
4
|
-
import { d as CheckResult, f as ResourceCheckResult, i as start, n as App, p as check, r as StartOptions, u as CheckOptions } from "../index-C5Ma7V7t.js";
|
|
5
|
-
import "../service-CUjGhe-7.js";
|
|
6
|
-
import "../index-DYpEMJf-.js";
|
|
7
|
-
import "../index-bfjH7Yb0.js";
|
|
8
|
-
import "../index-CR9HJDdr.js";
|
|
9
|
-
import "../metrics-C4V1pm6v.js";
|
|
10
|
-
import "../index-ZSuiZX5H.js";
|
|
11
|
-
import "../index-BA8ASHKr.js";
|
|
12
|
-
import "../index-VpHaZXZw.js";
|
|
13
|
-
import "../index-BKw7jA2a.js";
|
|
14
|
-
export { App, CheckOptions, CheckResult, ResourceCheckResult, StartOptions, check, start };
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import "../errors-DHWylqRZ.js";
|
|
2
|
-
import { n as start, r as check } from "../connector-app-BljHd0Pe.js";
|
|
3
|
-
import "../constants-D113Ut1x.js";
|
|
4
|
-
import "../telemetry-j_nxwYfh.js";
|
|
5
|
-
import "../metrics-inkCAxKq.js";
|
|
6
|
-
import "../service-DiP-XETo.js";
|
|
7
|
-
import "../server-D-eLX3yN.js";
|
|
8
|
-
import "../state-store-BdYulQOY.js";
|
|
9
|
-
import "../status-BTTd7AUR.js";
|
|
10
|
-
import "../ingestion-QBGKrFr9.js";
|
|
11
|
-
|
|
12
|
-
export { check, start };
|