@useairfoil/connector-kit 0.5.0 → 0.5.2
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 +78 -0
- package/README.md +190 -21
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.js +3 -0
- package/dist/auth-DvU_CZ7C.js +42 -0
- package/dist/auth-DvU_CZ7C.js.map +1 -0
- package/dist/connector-app/index.d.ts +14 -9
- package/dist/connector-app/index.js +9 -6
- package/dist/connector-app-d7Z-SZLQ.js +64 -0
- package/dist/connector-app-d7Z-SZLQ.js.map +1 -0
- package/dist/constants-BRhIX_rz.js +27 -0
- package/dist/constants-BRhIX_rz.js.map +1 -0
- package/dist/errors/index.d.ts +1 -1
- package/dist/{index-DK9YcR6W.d.ts → index-Byr0Lf0C.d.ts} +4 -4
- package/dist/index-CBT-JX-T.d.ts +173 -0
- package/dist/index-CTHVudvQ.d.ts +41 -0
- package/dist/index-Coh_SkBs.d.ts +95 -0
- package/dist/index-Cq2IzY7T.d.ts +80 -0
- package/dist/{index-DyuT2jzP.d.ts → index-D54ySAKn.d.ts} +1 -1
- package/dist/{index-CQJsFJs3.d.ts → index-DKWIh6M1.d.ts} +7 -5
- package/dist/{index-BlhJCpEk.d.ts → index-DtJX_tic.d.ts} +39 -10
- package/dist/{index-8KnVpoZp.d.ts → index-J42AZaXU.d.ts} +11 -5
- package/dist/index-o1-Z3n7V.d.ts +55 -0
- package/dist/index.d.ts +14 -9
- package/dist/index.js +14 -8
- package/dist/index.js.map +1 -1
- package/dist/ingestion/index.d.ts +5 -5
- package/dist/ingestion/index.js +7 -4
- package/dist/ingestion-BQo4cbGw.js +200 -0
- package/dist/ingestion-BQo4cbGw.js.map +1 -0
- package/dist/manifest/index.d.ts +2 -0
- package/dist/manifest/index.js +4 -0
- package/dist/manifest-ByE6YPG0.js +221 -0
- package/dist/manifest-ByE6YPG0.js.map +1 -0
- package/dist/metrics-CVAk1ALR.d.ts +53 -0
- package/dist/metrics-CWPy2Aqc.js +96 -0
- package/dist/metrics-CWPy2Aqc.js.map +1 -0
- package/dist/metrics.d.ts +4 -0
- package/dist/metrics.js +3 -0
- package/dist/publisher/index.d.ts +4 -4
- package/dist/publisher/index.js +3 -2
- package/dist/{publisher-DusgYvaV.js → publisher-BYT3RLVO.js} +2 -2
- package/dist/{publisher-DusgYvaV.js.map → publisher-BYT3RLVO.js.map} +1 -1
- package/dist/runtime-config/index.d.ts +2 -0
- package/dist/runtime-config/index.js +4 -0
- package/dist/runtime-config-BpB-d_bF.js +51 -0
- package/dist/runtime-config-BpB-d_bF.js.map +1 -0
- package/dist/server-CXTQ7odl.js +183 -0
- package/dist/server-CXTQ7odl.js.map +1 -0
- package/dist/{telemetry-p0dE5Mdu.js → service-DuFiQwid.js} +30 -10
- package/dist/service-DuFiQwid.js.map +1 -0
- package/dist/{service-B7g_lHMI.d.ts → service-IE5qaf9x.d.ts} +3 -3
- package/dist/state-store/index.d.ts +3 -4
- package/dist/state-store/index.js +4 -2
- package/dist/state-store-yl6LiTQ0.js +198 -0
- package/dist/state-store-yl6LiTQ0.js.map +1 -0
- package/dist/status/index.d.ts +5 -0
- package/dist/status/index.js +6 -0
- package/dist/status-B-oZBc33.js +84 -0
- package/dist/status-B-oZBc33.js.map +1 -0
- package/dist/{types-ZNrA-Dn0.d.ts → types-B35J5YPu.d.ts} +20 -6
- package/dist/webhook/index.d.ts +5 -4
- package/dist/webhook/index.js +5 -3
- package/dist/{webhook-BSA-tXtU.js → webhook-DI-Afxkg.js} +2 -2
- package/dist/{webhook-BSA-tXtU.js.map → webhook-DI-Afxkg.js.map} +1 -1
- package/package.json +36 -7
- package/src/auth/index.ts +1 -0
- package/src/auth/token-cache.ts +54 -0
- package/src/connector-app/check.ts +76 -0
- package/src/connector-app/index.ts +1 -0
- package/src/connector-app/start.ts +4 -0
- package/src/core/builder.ts +8 -3
- package/src/core/types.ts +20 -2
- package/src/index.ts +5 -0
- package/src/ingestion/engine.ts +246 -133
- package/src/manifest/config.ts +232 -0
- package/src/manifest/index.ts +14 -0
- package/src/manifest/manifest.ts +31 -0
- package/src/manifest/number-schema.ts +17 -0
- package/src/manifest/schema.ts +160 -0
- package/src/metrics.ts +128 -0
- package/src/publisher/instrumented.ts +68 -0
- package/src/runtime-config/constants.ts +27 -0
- package/src/runtime-config/error.ts +13 -0
- package/src/runtime-config/index.ts +3 -0
- package/src/runtime-config/layer.ts +56 -0
- package/src/state-store/index.ts +2 -1
- package/src/state-store/keys.ts +22 -0
- package/src/state-store/layer-key-value-store.ts +132 -0
- package/src/state-store/layer-memory.ts +11 -0
- package/src/state-store/layer-sql.ts +50 -0
- package/src/state-store/schema.ts +57 -0
- package/src/state-store/service.ts +39 -6
- package/src/state-store/state.ts +17 -0
- package/src/status/index.ts +2 -0
- package/src/status/route.ts +77 -0
- package/src/status/types.ts +33 -0
- package/src/telemetry.ts +46 -4
- package/src/webhook/server.ts +54 -19
- package/dist/connector-app-CvqbfN6R.js +0 -32
- package/dist/connector-app-CvqbfN6R.js.map +0 -1
- package/dist/index-DaNW63Mv.d.ts +0 -21
- package/dist/ingestion-CMCroK7Y.js +0 -171
- package/dist/ingestion-CMCroK7Y.js.map +0 -1
- package/dist/server-DAeFWT4o.js +0 -113
- package/dist/server-DAeFWT4o.js.map +0 -1
- package/dist/state-store-DG_4QpWG.js +0 -31
- package/dist/state-store-DG_4QpWG.js.map +0 -1
- package/dist/telemetry-p0dE5Mdu.js.map +0 -1
- package/src/state-store/memory.ts +0 -25
package/METRICS.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Connector Metrics And Status
|
|
2
|
+
|
|
3
|
+
Connector Kit emits standard metrics through Effect metrics. Production runtimes should use `Telemetry.layerOtlp()` with `OTEL_ENABLED=true`, `OTEL_EXPORTER_OTLP_ENDPOINT`, and optional `OTEL_EXPORTER_OTLP_HEADERS`. HTTP runtimes expose shallow process health at `GET /health`, Prometheus text at `GET /metrics`, and durable sync state at `GET /status`. Sandboxes can also merge `Telemetry.layerMetricsConsoleDump()` for periodic log output.
|
|
4
|
+
|
|
5
|
+
## Metrics
|
|
6
|
+
|
|
7
|
+
| Name | Type | Unit | Attributes |
|
|
8
|
+
| -------------------------------------------------- | --------------- | --------------- | -------------------------------------------- |
|
|
9
|
+
| `airfoil_connector_entities_upserted_total` | counter | mutations | `connector`, `resource`, `source` |
|
|
10
|
+
| `airfoil_connector_entities_deleted_total` | counter | mutations | `connector`, `resource`, `source` |
|
|
11
|
+
| `airfoil_connector_batches_total` | counter | batches | `connector`, `resource`, `source`, `outcome` |
|
|
12
|
+
| `airfoil_connector_batch_size` | histogram | mutations/batch | `connector`, `resource`, `source` |
|
|
13
|
+
| `airfoil_connector_publish_duration_ms` | histogram timer | ms | `connector`, `resource`, `source` |
|
|
14
|
+
| `airfoil_connector_webhook_requests_total` | counter | requests | `connector`, `path`, `outcome` |
|
|
15
|
+
| `airfoil_connector_webhook_queue_depth` | gauge | batches | `connector` |
|
|
16
|
+
| `airfoil_connector_sync_state` | gauge | none | `connector`, `resource`, `state` |
|
|
17
|
+
| `airfoil_connector_last_success_timestamp_seconds` | gauge | Unix seconds | `connector`, `resource`, `source` |
|
|
18
|
+
| `airfoil_connector_api_retries_total` | counter | retries | `connector`, `reason` |
|
|
19
|
+
|
|
20
|
+
Batch `source` is `backfill`, `changes`, or `webhook`. Batch `outcome` is `accepted`, `rejected`, or `error`. Webhook `outcome` is `ok`, `read_error`, `invalid_json`, `invalid_payload`, `rejected`, or `handler_error`. Sync `state` is `pending`, `backfilling`, `live`, or `error`.
|
|
21
|
+
|
|
22
|
+
API retry `reason` is `transport`, `timeout`, `rate_limit`, or `server_error`. The counter increments only when another request will be attempted. Provider-side limiter waits without a failed request are not retries.
|
|
23
|
+
|
|
24
|
+
Counters are activity since process start, not entity population. Use `increase()` or `rate()` and let Prometheus/OTLP consumers handle process restarts. `sum(upserts) - sum(deletes)` is connector activity, not the current table size; current totals belong in Wings/data-lake tables.
|
|
25
|
+
|
|
26
|
+
The last-success gauge is restored from durable state after restart and is `0` until the source commits its first checkpoint. Calculate source freshness in Prometheus:
|
|
27
|
+
|
|
28
|
+
```promql
|
|
29
|
+
time() - airfoil_connector_last_success_timestamp_seconds
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Sync Status
|
|
33
|
+
|
|
34
|
+
HTTP connector runtimes expose `GET /status` by default. The response shape is decoded by `Status.StatusResponseSchema`:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"connector": "producer-example",
|
|
39
|
+
"resources": [
|
|
40
|
+
{
|
|
41
|
+
"name": "products",
|
|
42
|
+
"state": "live",
|
|
43
|
+
"backfill": {
|
|
44
|
+
"completed": true,
|
|
45
|
+
"cutoff": "2026-01-01T00:00:00Z",
|
|
46
|
+
"lastSuccessAt": "2026-01-01T00:05:00Z"
|
|
47
|
+
},
|
|
48
|
+
"changes": {
|
|
49
|
+
"cursor": "2026-01-01T00:10:00Z",
|
|
50
|
+
"lastSuccessAt": "2026-01-01T00:10:01Z"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`ResourceStatusSchema`, `ResourceErrorSchema`, and `StatusUnavailableResponseSchema` are also exported from `@useairfoil/connector-kit/status` with their derived TypeScript types.
|
|
58
|
+
|
|
59
|
+
A resource with no persisted state is `pending`; an incomplete backfill is `backfilling`; completed backfill or changes-only resources are `live`. `lastSuccessAt` advances in the same durable write as its checkpoint and only after Wings accepts the batch.
|
|
60
|
+
|
|
61
|
+
Backfill or changes failures set `error` with a structured `lastError`. The error includes `source`, `operation`, a stable `code` and SDK-owned `message`, plus `at`; it never contains the raw Effect cause. Backfill and changes errors are stored independently, and `/status` returns the newer outstanding error when both exist.
|
|
62
|
+
|
|
63
|
+
When an expected source failure reaches the ingestion engine after any dependency retries, that source is parked until the process restarts. Other resources, webhooks, and HTTP routes keep running. Defects and interruption still terminate the runtime. `GET /health` only proves that the HTTP process is serving requests; provider, Wings, or PostgreSQL failures are reported through status and metrics instead.
|
|
64
|
+
|
|
65
|
+
## Wings Contract
|
|
66
|
+
|
|
67
|
+
Import pure connector manifests from first-party producer packages, for example `@useairfoil/producer-polar/manifest`, through a registry. Use `Manifest.decodeConfig(manifest, input)` for canonical server-side validation and `Schema.toStandardSchemaV1(Manifest.configSchema(manifest))` for form resolvers. The schema accepts JSON-shaped config and browser form-shaped values, including stringified numbers/booleans and empty optional fields. Treat `field.required` and `field.default` independently: the canonical decoder treats an empty optional field as omitted and resolves its default, while an optional field without a default remains absent. Store user-facing connector config by manifest field `name`, then use `Manifest.toRuntimeDocument(...)` to map it to `field.runtimeKey` names. Hosted pods mount that complete JSON document read-only and point `AIRFOIL_CONFIG_PATH` at it; they do not receive one environment variable per user field. Secret rendering/redaction comes from `field.secret`, and existing secret values must not be returned to the browser. Platform-owned runtime config such as Wings host, namespace, tables, PostgreSQL bindings, webhook ports, and telemetry remains infrastructure-owned environment/Secret configuration outside the manifest document.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import * as Manifest from "@useairfoil/connector-kit/manifest";
|
|
71
|
+
import { manifest as polarManifest } from "@useairfoil/producer-polar/manifest";
|
|
72
|
+
import { Schema } from "effect";
|
|
73
|
+
|
|
74
|
+
const schema = Schema.toStandardSchemaV1(Manifest.configSchema(polarManifest));
|
|
75
|
+
const result = await schema["~standard"].validate(formValues);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Poll `GET /status` and decode with `Status.StatusResponseSchema`. Chart ingestion with `increase(airfoil_connector_entities_upserted_total[...])`, alert on `airfoil_connector_sync_state{state="error"} == 1`, and alert on non-`ok` webhook outcomes.
|
package/README.md
CHANGED
|
@@ -12,17 +12,19 @@ Toolkit for building Airfoil producer connectors with Effect.
|
|
|
12
12
|
- `Ingestion.run(...)` runs resource ingestion and checkpoints after accepted publishes.
|
|
13
13
|
- `Publisher.layerConsole` logs accepted batches locally.
|
|
14
14
|
- `Publisher.layerWings(...)` publishes resource mutations into Wings tables.
|
|
15
|
+
- `ConnectorApp.check(...)` validates selected resources before provisioning.
|
|
15
16
|
- `ConnectorApp.start(...)` starts a complete Node HTTP connector process.
|
|
17
|
+
- `Auth.makeTokenCache(...)` caches short-lived provider access tokens in memory.
|
|
16
18
|
|
|
17
19
|
The package is intentionally Layer-oriented: connector code defines resources and routes, while entrypoints provide runtime dependencies such as publishers, state stores, telemetry, and API clients.
|
|
18
20
|
|
|
19
21
|
## Install
|
|
20
22
|
|
|
21
23
|
```bash
|
|
22
|
-
pnpm add @useairfoil/connector-kit effect
|
|
24
|
+
pnpm add @useairfoil/connector-kit effect@^4.0.0-rc.108
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
This repo currently uses Effect v4
|
|
27
|
+
This repo currently uses the Effect v4 release candidate. In workspace packages, prefer the workspace catalog (`"effect": "catalog:"`) so all packages stay on the supported RC version.
|
|
26
28
|
|
|
27
29
|
## Package Shape
|
|
28
30
|
|
|
@@ -30,15 +32,20 @@ The package root exposes the common connector authoring API:
|
|
|
30
32
|
|
|
31
33
|
```ts
|
|
32
34
|
import {
|
|
35
|
+
Auth,
|
|
33
36
|
Connector,
|
|
34
37
|
ConnectorApp,
|
|
35
38
|
ConnectorError,
|
|
36
39
|
Cursor,
|
|
37
40
|
Fetch,
|
|
38
41
|
Ingestion,
|
|
42
|
+
Manifest,
|
|
43
|
+
Metrics,
|
|
39
44
|
Publisher,
|
|
40
45
|
Resource,
|
|
46
|
+
RuntimeConfig,
|
|
41
47
|
StateStore,
|
|
48
|
+
Status,
|
|
42
49
|
Telemetry,
|
|
43
50
|
Webhook,
|
|
44
51
|
} from "@useairfoil/connector-kit";
|
|
@@ -46,15 +53,139 @@ import {
|
|
|
46
53
|
|
|
47
54
|
Subpath exports are available for runtime domains:
|
|
48
55
|
|
|
56
|
+
- `@useairfoil/connector-kit/auth`
|
|
49
57
|
- `@useairfoil/connector-kit/ingestion`
|
|
50
58
|
- `@useairfoil/connector-kit/connector-app`
|
|
59
|
+
- `@useairfoil/connector-kit/manifest`
|
|
60
|
+
- `@useairfoil/connector-kit/metrics`
|
|
51
61
|
- `@useairfoil/connector-kit/publisher`
|
|
62
|
+
- `@useairfoil/connector-kit/runtime-config`
|
|
52
63
|
- `@useairfoil/connector-kit/state-store`
|
|
64
|
+
- `@useairfoil/connector-kit/status`
|
|
53
65
|
- `@useairfoil/connector-kit/webhook`
|
|
54
66
|
- `@useairfoil/connector-kit/errors`
|
|
55
67
|
|
|
56
68
|
Core definition helpers and `Telemetry` are intentionally root-only.
|
|
57
69
|
|
|
70
|
+
## Provider Credentials
|
|
71
|
+
|
|
72
|
+
`Auth.makeTokenCache(...)` wraps a provider token exchange with Effect's cache. Acquisition stays lazy, concurrent callers share one exchange, and failures are not cached. The default refresh point is halfway through the provider lifetime.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { Auth } from "@useairfoil/connector-kit";
|
|
76
|
+
import { Duration, Effect, Redacted } from "effect";
|
|
77
|
+
|
|
78
|
+
const tokenCache =
|
|
79
|
+
yield *
|
|
80
|
+
Auth.makeTokenCache({
|
|
81
|
+
acquire: exchangeCredentials.pipe(
|
|
82
|
+
Effect.map(({ accessToken, expiresInSeconds }) => ({
|
|
83
|
+
value: Redacted.make(accessToken),
|
|
84
|
+
expiresIn: Duration.seconds(expiresInSeconds),
|
|
85
|
+
})),
|
|
86
|
+
),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const accessToken = yield * tokenCache.get;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The cache is process-local and does not use `StateStore`. It does not add retries or background refresh. The first request after the refresh point performs the next exchange.
|
|
93
|
+
|
|
94
|
+
## Manifests
|
|
95
|
+
|
|
96
|
+
Connector manifests are pure metadata for UI, Wings deployment, and validation. Put user-facing connector config in `src/manifest.ts` and keep platform-owned runtime config, such as ports, Wings host, table names, and OTEL flags, as normal Effect `Config` in entrypoints.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import * as Manifest from "@useairfoil/connector-kit/manifest";
|
|
100
|
+
|
|
101
|
+
export const ExampleConfigDef = Manifest.defineConfig({
|
|
102
|
+
apiBaseUrl: Manifest.string({
|
|
103
|
+
runtimeKey: "EXAMPLE_API_BASE_URL",
|
|
104
|
+
required: false,
|
|
105
|
+
default: "https://api.example.test",
|
|
106
|
+
}),
|
|
107
|
+
apiToken: Manifest.secret({ runtimeKey: "EXAMPLE_API_TOKEN" }),
|
|
108
|
+
maxRetries: Manifest.number({
|
|
109
|
+
runtimeKey: "EXAMPLE_MAX_RETRIES",
|
|
110
|
+
default: 5,
|
|
111
|
+
integer: true,
|
|
112
|
+
minimum: 0,
|
|
113
|
+
}),
|
|
114
|
+
webhookSecret: Manifest.optional(Manifest.secret({ runtimeKey: "EXAMPLE_WEBHOOK_SECRET" })),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
export type ExampleConfig = Manifest.ConfigValuesOf<typeof ExampleConfigDef>;
|
|
118
|
+
|
|
119
|
+
export const manifest = Manifest.define({
|
|
120
|
+
name: "producer-example",
|
|
121
|
+
title: "Example",
|
|
122
|
+
config: ExampleConfigDef.spec,
|
|
123
|
+
resources: [{ name: "posts", capabilities: ["backfill", "webhook"] }],
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`ExampleConfigDef.config` is the runtime Effect config. `ExampleConfigDef.spec` is serializable manifest metadata. `required` defaults to `true`. Number fields may set `integer` and `minimum`; Effect Schema applies the same constraints to runtime and form values. Use `required: false` with a default when the form may omit a field but the runtime must always receive a concrete value. Use `Manifest.optional(...)` when the runtime result should be an `Option`. Secret fields cannot declare defaults, so optional secrets use `Manifest.optional(Manifest.secret(...))`.
|
|
128
|
+
|
|
129
|
+
Use `Manifest.configSchema(manifest)` for form validation and `Schema.toStandardSchemaV1(...)` for standard-schema form resolvers. The schema accepts normal JSON values and browser form-shaped values: number fields may arrive as strings, boolean fields may arrive as booleans or `"true"`/`"false"`, optional fields may be omitted or submitted as `""`, and required strings reject `""`.
|
|
130
|
+
|
|
131
|
+
Use `Manifest.decodeConfig(manifest, input)` as the canonical server-side decoder. It rejects undeclared fields, treats `""` on a declared optional field as not supplied, and then applies manifest defaults. Therefore clearing or omitting a defaulted optional control means “use the manifest default”; it does not disable that setting. Model disabling as an explicit select/boolean/nullable contract instead of overloading an empty string. An optional field without a default remains absent.
|
|
132
|
+
|
|
133
|
+
Frontend/backend validation example:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import * as Manifest from "@useairfoil/connector-kit/manifest";
|
|
137
|
+
import { manifest as polarManifest } from "@useairfoil/producer-polar/manifest";
|
|
138
|
+
import { Schema } from "effect";
|
|
139
|
+
|
|
140
|
+
const configSchema = Manifest.configSchema(polarManifest);
|
|
141
|
+
const formSchema = Schema.toStandardSchemaV1(configSchema);
|
|
142
|
+
|
|
143
|
+
const result = await formSchema["~standard"].validate({
|
|
144
|
+
accessToken: "polar_oat_xxx",
|
|
145
|
+
apiBaseUrl: "https://api.polar.sh/v1/",
|
|
146
|
+
organizationId: "",
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if ("issues" in result) {
|
|
150
|
+
// Show validation issues in your form.
|
|
151
|
+
} else {
|
|
152
|
+
// Store by manifest field name: accessToken, apiBaseUrl, organizationId.
|
|
153
|
+
// Secrets should be persisted server-side and not echoed back to the browser.
|
|
154
|
+
const values = result.value;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Render fields from `manifest.config`: use `field.name` as the form/storage key, `field.description` for help text, `field.secret` for password inputs/redaction, `field.values` for selects, `field.default` for the documented fallback, `field.required` for required markers, and number `integer` and `minimum` values for input constraints. The UI may show a default as an initial value or clearly labelled placeholder, but clearing it still resolves to that default during canonical decoding. Decode submitted values with `Manifest.decodeConfig(...)`, then map the canonical logical object to its flat runtime document with `Manifest.toRuntimeDocument(...)`. Each `field.runtimeKey` is an Effect Config key, not an instruction to create one Pod environment variable.
|
|
159
|
+
|
|
160
|
+
Hosted connectors receive the runtime document as a read-only JSON file. `AIRFOIL_CONFIG_PATH` selects the file, and `RuntimeConfig.layerHosted()` adds the file beneath Effect's existing environment provider. The file is required and must contain valid JSON; each connector's Effect Config validates the fields it reads. Local sandboxes use Effect's default environment provider. Platform-owned config such as Wings host, namespace, table names, PostgreSQL settings, ports, and OTEL settings stays outside the manifest and outside the connector JSON.
|
|
161
|
+
|
|
162
|
+
### Pre-provision Checks
|
|
163
|
+
|
|
164
|
+
The dashboard backend imports each connector package and runs `ConnectorApp.check(...)` after manifest decoding but before persisting secrets or creating database and Kubernetes resources. Validate submitted resource names against `manifest.resources` at the request boundary before calling the typed check API. Replace the backend process provider with the submitted runtime document so unrelated backend environment variables cannot satisfy connector configuration:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { ConnectorApp, Manifest } from "@useairfoil/connector-kit";
|
|
168
|
+
import { PolarConnector, manifest } from "@useairfoil/producer-polar";
|
|
169
|
+
import { ConfigProvider, Effect } from "effect";
|
|
170
|
+
import { FetchHttpClient } from "effect/unstable/http";
|
|
171
|
+
|
|
172
|
+
const values = yield * Manifest.decodeConfig(manifest, submittedConfig);
|
|
173
|
+
const runtimeDocument = Manifest.toRuntimeDocument(manifest, values);
|
|
174
|
+
const provider = ConfigProvider.fromUnknown(runtimeDocument, { preserveEmptyStrings: true });
|
|
175
|
+
|
|
176
|
+
const result =
|
|
177
|
+
yield *
|
|
178
|
+
ConnectorApp.check(
|
|
179
|
+
PolarConnector.PolarConnector,
|
|
180
|
+
PolarConnector.layerConfig(PolarConnector.PolarConfigDef.config),
|
|
181
|
+
{ resources: ["customers", "orders"] },
|
|
182
|
+
).pipe(Effect.provide(ConfigProvider.layer(provider)), Effect.provide(FetchHttpClient.layer));
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Every resource declares a required, read-only `check` effect. Layer/configuration failures are returned for every selected resource; resource-specific failures are returned only under that resource name. Expected failures are values (`{ _tag: "error", message }`), while defects and interruption remain Effect failures. Provision only when all selected results are `{ _tag: "ok" }`. Checks must never publish, checkpoint, or mutate the upstream provider.
|
|
186
|
+
|
|
187
|
+
`RuntimeConfig.PlatformRuntimeKey` is the canonical shared-key contract used by hosted config, StateStore, telemetry, Wings, and PostgreSQL bootstrap code. `Manifest.defineConfig` rejects only those exact shared values; it does not impose connector-specific naming, uniqueness, or metadata policy. Connector packages are first-party code, so their table, port, field, and resource naming remains the connector author's responsibility.
|
|
188
|
+
|
|
58
189
|
## Resource Model
|
|
59
190
|
|
|
60
191
|
Resources emit mutations, not raw rows. Connector Kit currently supports `Resource.entity(...)` for Wings table-backed resources.
|
|
@@ -65,6 +196,7 @@ const Posts = Resource.entity({
|
|
|
65
196
|
schema: PostSchema,
|
|
66
197
|
key: "id",
|
|
67
198
|
version: "updatedAt",
|
|
199
|
+
check: api.fetchPosts({ page: 1, limit: 1 }).pipe(Effect.asVoid),
|
|
68
200
|
backfill: Fetch.page({
|
|
69
201
|
pageCursor: Cursor.number(),
|
|
70
202
|
cutoff: Cursor.isoDateTime(),
|
|
@@ -118,6 +250,7 @@ const Posts = Resource.entity({
|
|
|
118
250
|
schema: PostSchema,
|
|
119
251
|
key: "id",
|
|
120
252
|
version: "updatedAt",
|
|
253
|
+
check: Effect.void,
|
|
121
254
|
webhook: Resource.webhook({
|
|
122
255
|
schema: PostEventSchema,
|
|
123
256
|
handler: ({ payload }) => Effect.succeed([Resource.upsert(payload.data)]),
|
|
@@ -173,9 +306,36 @@ Current runtime behavior:
|
|
|
173
306
|
- checkpoints only after an accepted publish ACK
|
|
174
307
|
- does not checkpoint rejected publishes
|
|
175
308
|
- allows empty accepted batches to advance state
|
|
309
|
+
- stores `lastSuccessAt` with each durable checkpoint
|
|
176
310
|
- persists resource state through `StateStore.StateStore`
|
|
311
|
+
- parks a source when an expected failure reaches the ingestion engine, after any dependency retries
|
|
312
|
+
- keeps other sources, webhooks, and HTTP routes running when one source is parked
|
|
313
|
+
|
|
314
|
+
Parked sources resume after process restart or configuration redeployment. Defects and interruption are not isolated and still stop the runtime.
|
|
315
|
+
|
|
316
|
+
`Ingestion.run` depends only on the typed Airfoil `StateStore`; it does not expose the underlying Effect `KeyValueStore`. Provide `StateStore.layerMemory` for development/tests. Production entrypoints provide `StateStore.layerSql()`, which privately composes Effect's `KeyValueStore.layerSql` and scopes every key with the non-empty `AIRFOIL_CONNECTOR_INSTANCE_ID`. The platform ID is opaque and is not required to be a UUID.
|
|
317
|
+
|
|
318
|
+
State keys use plain colon-delimited segments and are intentionally inspectable; they are not escaped or encoded. The trusted backend must issue connector instance IDs without `:`, and connector authors must keep resource names free of `:`. Connector Kit currently relies on that naming contract rather than adding runtime validation.
|
|
177
319
|
|
|
178
|
-
|
|
320
|
+
The shared SQL table defaults to `_airfoil_connectors_state`. Set `AIRFOIL_STATE_TABLE` only to override that default. `POSTGRES_CONNECTION_STRING` is consumed by the PostgreSQL client layer, not by `StateStore` itself:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
import { PgClient } from "@effect/sql-pg";
|
|
324
|
+
import { RuntimeConfig, StateStore } from "@useairfoil/connector-kit";
|
|
325
|
+
import { Config, Layer, Schema } from "effect";
|
|
326
|
+
|
|
327
|
+
const PostgresLive = PgClient.layerConfig({
|
|
328
|
+
url: Config.schema(
|
|
329
|
+
Schema.Redacted(Schema.NonEmptyString),
|
|
330
|
+
RuntimeConfig.PlatformRuntimeKey.postgresConnectionString,
|
|
331
|
+
),
|
|
332
|
+
maxConnections: Config.succeed(2),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const StateStoreLive = StateStore.layerSql().pipe(Layer.provide(PostgresLive));
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
The platform injects the instance ID and Secret-backed PostgreSQL connection string used by `PgClient`, plus an optional table-name override. Connector business code receives `StateStore` but does not receive the connector instance ID, raw SQL client, or global clear/size operations.
|
|
179
339
|
|
|
180
340
|
## Publisher
|
|
181
341
|
|
|
@@ -224,7 +384,7 @@ import {
|
|
|
224
384
|
StateStore,
|
|
225
385
|
Telemetry,
|
|
226
386
|
} from "@useairfoil/connector-kit";
|
|
227
|
-
import {
|
|
387
|
+
import { Effect, Layer, Schema } from "effect";
|
|
228
388
|
import { FetchHttpClient } from "effect/unstable/http";
|
|
229
389
|
|
|
230
390
|
const PostSchema = Schema.Struct({
|
|
@@ -240,6 +400,7 @@ const Posts = Resource.entity({
|
|
|
240
400
|
schema: PostSchema,
|
|
241
401
|
key: "id",
|
|
242
402
|
version: "updatedAt",
|
|
403
|
+
check: Effect.void,
|
|
243
404
|
backfill: Fetch.page({
|
|
244
405
|
pageCursor: Cursor.number(),
|
|
245
406
|
cutoff: Cursor.isoDateTime(),
|
|
@@ -259,29 +420,36 @@ const connector = Connector.define({
|
|
|
259
420
|
resources: [Posts],
|
|
260
421
|
});
|
|
261
422
|
|
|
262
|
-
const
|
|
263
|
-
FetchHttpClient.layer,
|
|
264
|
-
Layer.succeed(ConfigProvider.ConfigProvider, ConfigProvider.fromEnv()),
|
|
265
|
-
);
|
|
423
|
+
const BootstrapLayer = FetchHttpClient.layer;
|
|
266
424
|
|
|
267
|
-
const TelemetryLayer = Telemetry.
|
|
268
|
-
const
|
|
425
|
+
const TelemetryLayer = Telemetry.layerOtlp().pipe(Layer.provide(BootstrapLayer));
|
|
426
|
+
const RuntimeLayer = Layer.mergeAll(
|
|
427
|
+
BootstrapLayer,
|
|
428
|
+
StateStore.layerMemory,
|
|
429
|
+
Publisher.layerConsole,
|
|
430
|
+
TelemetryLayer,
|
|
431
|
+
);
|
|
269
432
|
|
|
270
433
|
Effect.scoped(ConnectorApp.start(connector, { port: 8080 })).pipe(
|
|
271
|
-
Effect.provide(
|
|
434
|
+
Effect.provide(RuntimeLayer),
|
|
272
435
|
NodeRuntime.runMain,
|
|
273
436
|
);
|
|
274
437
|
```
|
|
275
438
|
|
|
276
439
|
## Telemetry
|
|
277
440
|
|
|
278
|
-
`Telemetry` contains connector-kit span names, span attributes, error annotation helpers, and OTLP
|
|
441
|
+
`Telemetry` contains connector-kit span names, span attributes, error annotation helpers, OTLP tracing layers, and OTLP metrics layers. HTTP connector runtimes expose shallow process health at `GET /health`, Prometheus metrics at `GET /metrics`, and durable sync state at `GET /status` by default. Provider, Wings, and PostgreSQL failures do not make `/health` fail.
|
|
442
|
+
|
|
443
|
+
`/status` includes `lastSuccessAt` for each checkpointed backfill or changes source. Use `time() - airfoil_connector_last_success_timestamp_seconds` to monitor source freshness without interpreting provider cursors.
|
|
279
444
|
|
|
280
445
|
Common entry points:
|
|
281
446
|
|
|
282
447
|
- `Telemetry.layer(config, options?)`
|
|
283
448
|
- `Telemetry.layerConfig(config, options?)`
|
|
284
|
-
- `Telemetry.layerOtlpTracing(options?)`
|
|
449
|
+
- `Telemetry.layerOtlpTracing(options?)` for OTLP traces
|
|
450
|
+
- `Telemetry.layerOtlpMetrics(options?)` for OTLP metrics
|
|
451
|
+
- `Telemetry.layerOtlp(options?)` for both traces and metrics
|
|
452
|
+
- `Telemetry.layerMetricsConsoleDump(interval?)`
|
|
285
453
|
|
|
286
454
|
Default redacted headers:
|
|
287
455
|
|
|
@@ -296,14 +464,14 @@ Default redacted headers:
|
|
|
296
464
|
|
|
297
465
|
Telemetry environment variables:
|
|
298
466
|
|
|
299
|
-
| Variable | Read by | Description
|
|
300
|
-
| ----------------------------- | ------------- |
|
|
301
|
-
| `OTEL_ENABLED` | Connector Kit | Enables
|
|
302
|
-
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Connector Kit | OTLP base URL, for example `http://localhost:4318`. Connector Kit appends `/v1/traces
|
|
303
|
-
| `OTEL_EXPORTER_OTLP_HEADERS` | Connector Kit | Optional comma-separated headers, for example `Authorization=Bearer <token>,X-Axiom-Dataset=<dataset>`.
|
|
304
|
-
| `OTEL_SERVICE_NAME` | Effect | Service name resource attribute, for example `producer-shopify`.
|
|
305
|
-
| `OTEL_SERVICE_VERSION` | Effect | Optional service version resource attribute.
|
|
306
|
-
| `OTEL_RESOURCE_ATTRIBUTES` | Effect | Optional comma-separated resource attributes, for example `deployment.environment=production,team=data`.
|
|
467
|
+
| Variable | Read by | Description |
|
|
468
|
+
| ----------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
469
|
+
| `OTEL_ENABLED` | Connector Kit | Enables OTLP export when `true`. Defaults to `false`. |
|
|
470
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Connector Kit | OTLP base URL, for example `http://localhost:4318`. Connector Kit appends `/v1/traces` and `/v1/metrics` as needed. Required when `OTEL_ENABLED=true`. |
|
|
471
|
+
| `OTEL_EXPORTER_OTLP_HEADERS` | Connector Kit | Optional comma-separated headers, for example `Authorization=Bearer <token>,X-Axiom-Dataset=<dataset>`. |
|
|
472
|
+
| `OTEL_SERVICE_NAME` | Effect | Service name resource attribute, for example `producer-shopify`. |
|
|
473
|
+
| `OTEL_SERVICE_VERSION` | Effect | Optional service version resource attribute. |
|
|
474
|
+
| `OTEL_RESOURCE_ATTRIBUTES` | Effect | Optional comma-separated resource attributes, for example `deployment.environment=production,team=data`. |
|
|
307
475
|
|
|
308
476
|
## Testing
|
|
309
477
|
|
|
@@ -311,6 +479,7 @@ For tests, the most common setup is:
|
|
|
311
479
|
|
|
312
480
|
- `StateStore.layerMemory` for state
|
|
313
481
|
- a small in-memory `Publisher.Publisher` test layer
|
|
482
|
+
- `ConnectorApp.check(...)` with an in-memory `ConfigProvider` for pre-provision validation
|
|
314
483
|
- `Ingestion.run(...)` or `ConnectorApp.start(...)` inside `Effect.scoped`
|
|
315
484
|
|
|
316
485
|
Keep HTTP recording concerns outside connector logic by providing a VCR-backed `HttpClient` Layer.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { t as __export } from "./chunk-Bp6m_JJh.js";
|
|
2
|
+
import { Cache, Duration, Effect, Exit, Number, Redacted } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/auth/token-cache.ts
|
|
5
|
+
/**
|
|
6
|
+
* Creates a lazy, in-memory token cache around an acquisition effect.
|
|
7
|
+
*
|
|
8
|
+
* `refreshFactor` controls how much of the provider lifetime is cached. It is
|
|
9
|
+
* clamped between `0` and `1`.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const cache = yield* makeTokenCache({
|
|
14
|
+
* acquire: exchangeCredentials,
|
|
15
|
+
* refreshFactor: 0.5,
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* const token = yield* cache.get
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
const makeTokenCache = (options) => Effect.gen(function* () {
|
|
22
|
+
const refreshFactor = Number.clamp(options.refreshFactor ?? .5, {
|
|
23
|
+
minimum: 0,
|
|
24
|
+
maximum: 1
|
|
25
|
+
});
|
|
26
|
+
const cache = yield* Cache.makeWith((_) => options.acquire, {
|
|
27
|
+
capacity: 1,
|
|
28
|
+
timeToLive: (exit) => Exit.isSuccess(exit) ? Duration.millis(Duration.toMillis(exit.value.expiresIn) * refreshFactor) : Duration.zero
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
get: Cache.get(cache, void 0).pipe(Effect.map((token) => token.value)),
|
|
32
|
+
invalidate: Cache.invalidate(cache, void 0)
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/auth/index.ts
|
|
38
|
+
var auth_exports = /* @__PURE__ */ __export({ makeTokenCache: () => makeTokenCache });
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
export { makeTokenCache as n, auth_exports as t };
|
|
42
|
+
//# sourceMappingURL=auth-DvU_CZ7C.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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,9 +1,14 @@
|
|
|
1
|
-
import "../index-
|
|
2
|
-
import "../
|
|
3
|
-
import
|
|
4
|
-
import "../
|
|
5
|
-
import "../
|
|
6
|
-
import "../index-
|
|
7
|
-
import "../index-
|
|
8
|
-
import "../index-
|
|
9
|
-
|
|
1
|
+
import "../index-CTHVudvQ.js";
|
|
2
|
+
import "../index-D54ySAKn.js";
|
|
3
|
+
import "../types-B35J5YPu.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-DtJX_tic.js";
|
|
5
|
+
import "../service-IE5qaf9x.js";
|
|
6
|
+
import "../index-Coh_SkBs.js";
|
|
7
|
+
import "../index-J42AZaXU.js";
|
|
8
|
+
import "../index-CBT-JX-T.js";
|
|
9
|
+
import "../metrics-CVAk1ALR.js";
|
|
10
|
+
import "../index-Byr0Lf0C.js";
|
|
11
|
+
import "../index-o1-Z3n7V.js";
|
|
12
|
+
import "../index-Cq2IzY7T.js";
|
|
13
|
+
import "../index-DKWIh6M1.js";
|
|
14
|
+
export { App, CheckOptions, CheckResult, ResourceCheckResult, StartOptions, check, start };
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import "../errors-DHWylqRZ.js";
|
|
2
|
-
import "../
|
|
3
|
-
import "../
|
|
4
|
-
import "../
|
|
5
|
-
import "../
|
|
6
|
-
import
|
|
2
|
+
import { n as start, r as check } from "../connector-app-d7Z-SZLQ.js";
|
|
3
|
+
import "../metrics-CWPy2Aqc.js";
|
|
4
|
+
import "../constants-BRhIX_rz.js";
|
|
5
|
+
import "../service-DuFiQwid.js";
|
|
6
|
+
import "../server-CXTQ7odl.js";
|
|
7
|
+
import "../state-store-yl6LiTQ0.js";
|
|
8
|
+
import "../status-B-oZBc33.js";
|
|
9
|
+
import "../ingestion-BQo4cbGw.js";
|
|
7
10
|
|
|
8
|
-
export { start };
|
|
11
|
+
export { check, start };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { t as __export } from "./chunk-Bp6m_JJh.js";
|
|
2
|
+
import { n as run } from "./ingestion-BQo4cbGw.js";
|
|
3
|
+
import { Context, DateTime, Effect, Layer, Tuple } from "effect";
|
|
4
|
+
import { NodeHttpServer } from "@effect/platform-node";
|
|
5
|
+
import { HttpRouter } from "effect/unstable/http";
|
|
6
|
+
import { createServer } from "node:http";
|
|
7
|
+
|
|
8
|
+
//#region src/connector-app/check.ts
|
|
9
|
+
const ok = { _tag: "ok" };
|
|
10
|
+
const error = (message) => ({
|
|
11
|
+
_tag: "error",
|
|
12
|
+
message
|
|
13
|
+
});
|
|
14
|
+
const fromEntries = (entries) => Object.fromEntries(entries);
|
|
15
|
+
const resultsFor = (names, result) => fromEntries(names.map((name) => Tuple.make(name, result)));
|
|
16
|
+
/**
|
|
17
|
+
* Builds the connector service from its layer and validates only the selected
|
|
18
|
+
* resources. The layer remains open until every check finishes.
|
|
19
|
+
*/
|
|
20
|
+
function check(service, layer, options) {
|
|
21
|
+
if (options.resources.length === 0) return Effect.succeed(fromEntries([]));
|
|
22
|
+
return service.pipe(Effect.flatMap((connector) => Effect.forEach(options.resources, (name) => {
|
|
23
|
+
const resource = connector.resources.find((candidate) => candidate.name === name);
|
|
24
|
+
return (resource ? resource.check.pipe(Effect.match({
|
|
25
|
+
onFailure: (failure) => error(failure.message),
|
|
26
|
+
onSuccess: () => ok
|
|
27
|
+
})) : Effect.succeed(error(`Unknown connector resource: ${name}`))).pipe(Effect.map((result) => Tuple.make(name, result)));
|
|
28
|
+
}).pipe(Effect.map(fromEntries))), Effect.provide(layer), Effect.match({
|
|
29
|
+
onFailure: (failure) => resultsFor(options.resources, error(failure.message)),
|
|
30
|
+
onSuccess: (results) => results
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/connector-app/start.ts
|
|
36
|
+
const start = (app, options) => Effect.gen(function* () {
|
|
37
|
+
const initialCutoff = options.initialCutoff ?? (yield* DateTime.now.pipe(Effect.map(DateTime.formatIso)));
|
|
38
|
+
const routes = app.webhooks?.map((route) => route.path) ?? [];
|
|
39
|
+
yield* Effect.logInfo("webhook server ready").pipe(Effect.annotateLogs({
|
|
40
|
+
port: options.port,
|
|
41
|
+
routes
|
|
42
|
+
}));
|
|
43
|
+
return yield* run(app, {
|
|
44
|
+
initialCutoff,
|
|
45
|
+
webhook: {
|
|
46
|
+
routes: app.webhooks ?? [],
|
|
47
|
+
healthPath: options.healthPath ?? "/health",
|
|
48
|
+
metricsPath: options.metricsPath,
|
|
49
|
+
statusPath: options.statusPath,
|
|
50
|
+
disableHttpLogger: options.disableHttpLogger ?? true
|
|
51
|
+
}
|
|
52
|
+
}).pipe(Effect.provide(NodeHttpServer.layer(createServer, { port: options.port })));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/connector-app/index.ts
|
|
57
|
+
var connector_app_exports = /* @__PURE__ */ __export({
|
|
58
|
+
check: () => check,
|
|
59
|
+
start: () => start
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
export { start as n, check as r, connector_app_exports as t };
|
|
64
|
+
//# sourceMappingURL=connector-app-d7Z-SZLQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connector-app-d7Z-SZLQ.js","names":["ok: ResourceCheckResult","runIngestion"],"sources":["../src/connector-app/check.ts","../src/connector-app/start.ts","../src/connector-app/index.ts"],"sourcesContent":["import { Context, Effect, Layer, Tuple } from \"effect\";\n\nimport type { ConnectorDefinition, ResourceDefinition, ResourceName } from \"../core/types\";\n\nexport type ResourceCheckResult =\n | { readonly _tag: \"ok\" }\n | { readonly _tag: \"error\"; readonly message: string };\n\nexport type CheckOptions<Name extends string> = {\n readonly resources: ReadonlyArray<Name>;\n};\n\ntype ResourceCheckResults<Name extends PropertyKey> = {\n readonly [Key in Name]: ResourceCheckResult;\n};\n\nexport type CheckResult<\n Resources extends ReadonlyArray<ResourceDefinition>,\n Name extends ResourceName<Resources> = ResourceName<Resources>,\n> = ResourceCheckResults<Name>;\n\ntype CheckFailure = { readonly message: string };\n\nconst ok: ResourceCheckResult = { _tag: \"ok\" };\n\nconst error = (message: string): ResourceCheckResult => ({ _tag: \"error\", message });\n\nconst fromEntries = <Name extends string>(\n entries: ReadonlyArray<readonly [Name, ResourceCheckResult]>,\n): ResourceCheckResults<Name> => Object.fromEntries(entries) as ResourceCheckResults<Name>;\n\nconst resultsFor = <Name extends string>(names: ReadonlyArray<Name>, result: ResourceCheckResult) =>\n fromEntries(names.map((name) => Tuple.make(name, result)));\n\n/**\n * Builds the connector service from its layer and validates only the selected\n * resources. The layer remains open until every check finishes.\n */\nexport function check<\n Identifier,\n const Resources extends ReadonlyArray<ResourceDefinition>,\n E extends CheckFailure,\n R,\n const Name extends ResourceName<Resources>,\n>(\n service: Context.Key<Identifier, ConnectorDefinition<Resources>>,\n layer: Layer.Layer<Identifier, E, R>,\n options: CheckOptions<Name>,\n): Effect.Effect<CheckResult<Resources, Name>, never, R> {\n if (options.resources.length === 0) {\n return Effect.succeed(fromEntries<Name>([]));\n }\n\n return service.pipe(\n Effect.flatMap((connector) =>\n Effect.forEach(options.resources, (name) => {\n const resource = connector.resources.find((candidate) => candidate.name === name);\n const result = resource\n ? resource.check.pipe(\n Effect.match({\n onFailure: (failure) => error(failure.message),\n onSuccess: () => ok,\n }),\n )\n : Effect.succeed(error(`Unknown connector resource: ${name}`));\n\n return result.pipe(Effect.map((result) => Tuple.make(name, result)));\n }).pipe(Effect.map(fromEntries)),\n ),\n Effect.provide(layer),\n Effect.match({\n onFailure: (failure) => resultsFor(options.resources, error(failure.message)),\n onSuccess: (results) => results,\n }),\n );\n}\n","import { NodeHttpServer } from \"@effect/platform-node\";\nimport { DateTime, Effect } from \"effect\";\nimport { HttpRouter } from \"effect/unstable/http\";\nimport { createServer } from \"node:http\";\n\nimport type { ConnectorDefinition } from \"../core\";\nimport type { Cursor } from \"../core/types\";\n\nimport { run as runIngestion } from \"../ingestion\";\n\nexport type App = ConnectorDefinition;\n\nexport type StartOptions = {\n readonly port: number;\n readonly initialCutoff?: Cursor.Value;\n readonly healthPath?: HttpRouter.PathInput;\n readonly metricsPath?: HttpRouter.PathInput | false;\n readonly statusPath?: HttpRouter.PathInput | false;\n readonly disableHttpLogger?: boolean;\n};\n\nexport const start = (app: App, options: StartOptions) =>\n Effect.gen(function* () {\n const initialCutoff =\n options.initialCutoff ?? (yield* DateTime.now.pipe(Effect.map(DateTime.formatIso)));\n const routes = app.webhooks?.map((route) => route.path) ?? [];\n\n yield* Effect.logInfo(\"webhook server ready\").pipe(\n Effect.annotateLogs({ port: options.port, routes }),\n );\n\n return yield* runIngestion(app, {\n initialCutoff,\n webhook: {\n routes: app.webhooks ?? [],\n healthPath: options.healthPath ?? \"/health\",\n metricsPath: options.metricsPath,\n statusPath: options.statusPath,\n disableHttpLogger: options.disableHttpLogger ?? true,\n },\n }).pipe(Effect.provide(NodeHttpServer.layer(createServer, { port: options.port })));\n });\n","export { check, type CheckOptions, type CheckResult, type ResourceCheckResult } from \"./check\";\nexport { start, type App, type StartOptions } from \"./start\";\n"],"mappings":";;;;;;;;AAuBA,MAAMA,KAA0B,EAAE,MAAM,MAAM;AAE9C,MAAM,SAAS,aAA0C;CAAE,MAAM;CAAS;CAAS;AAEnF,MAAM,eACJ,YAC+B,OAAO,YAAY,QAAQ;AAE5D,MAAM,cAAmC,OAA4B,WACnE,YAAY,MAAM,KAAK,SAAS,MAAM,KAAK,MAAM,OAAO,CAAC,CAAC;;;;;AAM5D,SAAgB,MAOd,SACA,OACA,SACuD;AACvD,KAAI,QAAQ,UAAU,WAAW,EAC/B,QAAO,OAAO,QAAQ,YAAkB,EAAE,CAAC,CAAC;AAG9C,QAAO,QAAQ,KACb,OAAO,SAAS,cACd,OAAO,QAAQ,QAAQ,YAAY,SAAS;EAC1C,MAAM,WAAW,UAAU,UAAU,MAAM,cAAc,UAAU,SAAS,KAAK;AAUjF,UATe,WACX,SAAS,MAAM,KACb,OAAO,MAAM;GACX,YAAY,YAAY,MAAM,QAAQ,QAAQ;GAC9C,iBAAiB;GAClB,CAAC,CACH,GACD,OAAO,QAAQ,MAAM,+BAA+B,OAAO,CAAC,EAElD,KAAK,OAAO,KAAK,WAAW,MAAM,KAAK,MAAM,OAAO,CAAC,CAAC;GACpE,CAAC,KAAK,OAAO,IAAI,YAAY,CAAC,CACjC,EACD,OAAO,QAAQ,MAAM,EACrB,OAAO,MAAM;EACX,YAAY,YAAY,WAAW,QAAQ,WAAW,MAAM,QAAQ,QAAQ,CAAC;EAC7E,YAAY,YAAY;EACzB,CAAC,CACH;;;;;ACrDH,MAAa,SAAS,KAAU,YAC9B,OAAO,IAAI,aAAa;CACtB,MAAM,gBACJ,QAAQ,kBAAkB,OAAO,SAAS,IAAI,KAAK,OAAO,IAAI,SAAS,UAAU,CAAC;CACpF,MAAM,SAAS,IAAI,UAAU,KAAK,UAAU,MAAM,KAAK,IAAI,EAAE;AAE7D,QAAO,OAAO,QAAQ,uBAAuB,CAAC,KAC5C,OAAO,aAAa;EAAE,MAAM,QAAQ;EAAM;EAAQ,CAAC,CACpD;AAED,QAAO,OAAOC,IAAa,KAAK;EAC9B;EACA,SAAS;GACP,QAAQ,IAAI,YAAY,EAAE;GAC1B,YAAY,QAAQ,cAAc;GAClC,aAAa,QAAQ;GACrB,YAAY,QAAQ;GACpB,mBAAmB,QAAQ,qBAAqB;GACjD;EACF,CAAC,CAAC,KAAK,OAAO,QAAQ,eAAe,MAAM,cAAc,EAAE,MAAM,QAAQ,MAAM,CAAC,CAAC,CAAC;EACnF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { HashSet } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/runtime-config/constants.ts
|
|
4
|
+
/** Shared runtime keys owned by Airfoil rather than an individual connector. */
|
|
5
|
+
const PlatformRuntimeKey = {
|
|
6
|
+
configPath: "AIRFOIL_CONFIG_PATH",
|
|
7
|
+
connectorInstanceId: "AIRFOIL_CONNECTOR_INSTANCE_ID",
|
|
8
|
+
stateTable: "AIRFOIL_STATE_TABLE",
|
|
9
|
+
postgresConnectionString: "POSTGRES_CONNECTION_STRING",
|
|
10
|
+
wingsHost: "WINGS_HOST",
|
|
11
|
+
wingsNamespace: "WINGS_NAMESPACE",
|
|
12
|
+
otelEnabled: "OTEL_ENABLED",
|
|
13
|
+
otelExporterOtlpEndpoint: "OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
14
|
+
otelExporterOtlpHeaders: "OTEL_EXPORTER_OTLP_HEADERS",
|
|
15
|
+
otelServiceName: "OTEL_SERVICE_NAME",
|
|
16
|
+
otelServiceVersion: "OTEL_SERVICE_VERSION",
|
|
17
|
+
otelResourceAttributes: "OTEL_RESOURCE_ATTRIBUTES"
|
|
18
|
+
};
|
|
19
|
+
/** Defaults for optional platform-owned runtime configuration. */
|
|
20
|
+
const PlatformRuntimeDefault = { stateTable: "_airfoil_connectors_state" };
|
|
21
|
+
const platformRuntimeKeys = HashSet.fromIterable(Object.values(PlatformRuntimeKey));
|
|
22
|
+
/** @internal */
|
|
23
|
+
const isPlatformRuntimeKey = (key) => HashSet.has(platformRuntimeKeys, key);
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
export { PlatformRuntimeKey as n, isPlatformRuntimeKey as r, PlatformRuntimeDefault as t };
|
|
27
|
+
//# sourceMappingURL=constants-BRhIX_rz.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants-BRhIX_rz.js","names":[],"sources":["../src/runtime-config/constants.ts"],"sourcesContent":["import { HashSet } from \"effect\";\n\n/** Shared runtime keys owned by Airfoil rather than an individual connector. */\nexport const PlatformRuntimeKey = {\n configPath: \"AIRFOIL_CONFIG_PATH\",\n connectorInstanceId: \"AIRFOIL_CONNECTOR_INSTANCE_ID\",\n stateTable: \"AIRFOIL_STATE_TABLE\",\n postgresConnectionString: \"POSTGRES_CONNECTION_STRING\",\n wingsHost: \"WINGS_HOST\",\n wingsNamespace: \"WINGS_NAMESPACE\",\n otelEnabled: \"OTEL_ENABLED\",\n otelExporterOtlpEndpoint: \"OTEL_EXPORTER_OTLP_ENDPOINT\",\n otelExporterOtlpHeaders: \"OTEL_EXPORTER_OTLP_HEADERS\",\n otelServiceName: \"OTEL_SERVICE_NAME\",\n otelServiceVersion: \"OTEL_SERVICE_VERSION\",\n otelResourceAttributes: \"OTEL_RESOURCE_ATTRIBUTES\",\n} as const;\n\n/** Defaults for optional platform-owned runtime configuration. */\nexport const PlatformRuntimeDefault = {\n stateTable: \"_airfoil_connectors_state\",\n} as const;\n\nconst platformRuntimeKeys = HashSet.fromIterable(Object.values(PlatformRuntimeKey));\n\n/** @internal */\nexport const isPlatformRuntimeKey = (key: string): boolean => HashSet.has(platformRuntimeKeys, key);\n"],"mappings":";;;;AAGA,MAAa,qBAAqB;CAChC,YAAY;CACZ,qBAAqB;CACrB,YAAY;CACZ,0BAA0B;CAC1B,WAAW;CACX,gBAAgB;CAChB,aAAa;CACb,0BAA0B;CAC1B,yBAAyB;CACzB,iBAAiB;CACjB,oBAAoB;CACpB,wBAAwB;CACzB;;AAGD,MAAa,yBAAyB,EACpC,YAAY,6BACb;AAED,MAAM,sBAAsB,QAAQ,aAAa,OAAO,OAAO,mBAAmB,CAAC;;AAGnF,MAAa,wBAAwB,QAAyB,QAAQ,IAAI,qBAAqB,IAAI"}
|
package/dist/errors/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as ConnectorError } from "../index-
|
|
1
|
+
import { t as ConnectorError } from "../index-D54ySAKn.js";
|
|
2
2
|
export { ConnectorError };
|