Skip to content

Commit 8e7624e

Browse files
authored
feat: major refactoring and effect improvements and fixes (#21)
- Refactor core modules across the codebase (flight, wings, ack, vcr) - Add record batch with metadata support (flight) - Improve testing and standards (wings-testing) - Update connectors and templates - ack skill - Miscellaneous file changes
2 parents f900e0b + 2a206fe commit 8e7624e

167 files changed

Lines changed: 5697 additions & 4622 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/airfoil-kit/README.md

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ Airfoil producer connector end-to-end.
88
- Confirms no existing implementation is being copied.
99
- Copies `templates/producer-template/` into `connectors/producer-<name>/`.
1010
- Helps you research the target API and derive schemas from recorded traffic.
11-
- Wires Effect v4 `Config`, API clients, `WebhookRoute`, and streams.
11+
- Wires current Effect v4 `Config`, API client layers, `Webhook.route(...)`,
12+
connector layers, and streams.
1213
- Guides deterministic replay testing (VCR for REST/GraphQL, fixtures/mocks for gRPC).
1314
- Enforces a Definition of Done before declaring the task complete.
1415

@@ -29,6 +30,42 @@ Canonical process docs:
2930

3031
Example-oriented docs are optional aids, not normative contracts.
3132

33+
## Public package surfaces you should know
34+
35+
Current root surfaces used most often by connector work:
36+
37+
- `@useairfoil/connector-kit`
38+
- core exports flattened at root
39+
- `Ingestion`
40+
- `Publisher`
41+
- `Streams`
42+
- `Webhook`
43+
- flat root errors
44+
- `@useairfoil/effect-vcr`
45+
- `CassetteStore`
46+
- `FileSystemCassetteStore`
47+
- `VcrHttpClient`
48+
- flat root VCR types
49+
- focused subpath exports for cassette store, file-system cassette store,
50+
types, and VCR HTTP client
51+
- `@useairfoil/wings`
52+
- `Cluster`
53+
- `ClusterClient`
54+
- `WingsClient`
55+
- `Arrow`
56+
- `Partition`
57+
- `Schema`
58+
- `Topic`
59+
- flat root errors
60+
- `@useairfoil/flight`
61+
- `ArrowFlightClient`
62+
- `ArrowFlightSqlClient`
63+
- `FlightClientError`
64+
- root encoder/proto exports and typed client options
65+
66+
When writing examples or guidance, prefer the actual current package surface
67+
over historical helper names or internal file-level imports.
68+
3269
## Files
3370

3471
```
@@ -40,9 +77,9 @@ references/
4077
├── api-mode-graphql.md # GraphQL implementation contract
4178
├── api-mode-grpc.md # gRPC implementation contract
4279
├── connector-kit-api.md # exhaustive @useairfoil/connector-kit docs
43-
├── effect-vcr-api.md # exhaustive @useairfoil/effect-vcr docs
80+
├── effect-vcr-api.md # current @useairfoil/effect-vcr docs and wiring
4481
├── effect-v4-essentials.md # Effect v4 idioms relevant to connectors
45-
├── patterns.md # shared patterns (cursor, cutoff, streams)
82+
├── patterns.md # shared naming, layer, cursor, cutoff, and stream patterns
4683
├── webhooks.md # WebhookRoute + signature verification
4784
├── vcr-workflow.md # record/replay + ACK_DISABLE_VCR
4885
├── api-research.md # how to learn a real API's shape

.agents/skills/airfoil-kit/SKILL.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ inside this monorepo. Work in small, verified steps. Use the template as your
1010
starting point, never guess API shapes, and keep changes aligned with the
1111
existing patterns in `connectors/producer-polar/`.
1212

13+
The current repo shape matters more than historical examples. When in doubt,
14+
follow the current source packages and the refreshed reference docs in this
15+
skill.
16+
1317
---
1418

1519
## Hard rules (do not violate)
@@ -25,7 +29,9 @@ existing patterns in `connectors/producer-polar/`.
2529
3. **Use Effect v4 only** (`effect@4.x`, `@effect/vitest@4.x`, `@effect/platform-*@4.x`).
2630
No legacy `@effect/platform`, `@effect/schema`, or Effect v2/v3 patterns.
2731
Read [`references/effect-v4-essentials.md`](./references/effect-v4-essentials.md)
28-
whenever you reach for a new Effect module.
32+
whenever you reach for a new Effect module. For Effect guidance, consult
33+
`effect-smol` only. Do not use the older official Effect docs as source of
34+
truth for this repo right now.
2935
4. **No `process.env` reads in connector code or tests.** Use
3036
`Config`/`ConfigProvider` everywhere. Sandbox/runtime layers attach
3137
`ConfigProvider.fromEnv()`; tests attach `ConfigProvider.fromUnknown({ ... })`
@@ -68,6 +74,16 @@ existing patterns in `connectors/producer-polar/`.
6874
item in [`references/definition-of-done.md`](./references/definition-of-done.md)
6975
passes (lint, typecheck, build, test:ci, and mode-appropriate deterministic
7076
replay: VCR for REST/GraphQL, fixtures or mock servers for gRPC).
77+
14. **Use current names.** Prefer `make`, `layer(config)`,
78+
`layerConfig(Config.Wrap<...>)`, namespace entrypoint exports,
79+
`Ingestion.runConnector(...)`, `Ingestion.layerMemory`,
80+
`Publisher.Publisher`, and `Webhook.route(...)`.
81+
15. **Use correct layer semantics.** `Layer.mergeAll(...)` is for independent
82+
layers. If a layer needs another to build, satisfy that dependency with
83+
`Layer.provide(...)` before merging.
84+
16. **Do not hide dependency graph mistakes behind casts.** If a runtime or
85+
test entrypoint seems to need `as Effect.Effect<...>`, inspect the layer
86+
graph first.
7187

7288
---
7389

@@ -108,14 +124,15 @@ existing patterns in `connectors/producer-polar/`.
108124
[`references/patterns.md`](./references/patterns.md),
109125
[`references/webhooks.md`](./references/webhooks.md)
110126
10. **Update the sandbox runner** — rename config names and port, keep the
111-
telemetry + console publisher boilerplate.
127+
telemetry + console publisher shape, and preserve the dependency graph.
112128
11. **Write tests**
113129
- REST/GraphQL: `api.vcr.test.ts` replays the backfill path.
114130
- gRPC: deterministic fixture/mock-server tests cover equivalent paths.
115131
- `webhook.test.ts` exercises webhook endpoint behavior in-memory.
116132
Switch to replay mode (or fixture-only deterministic mode) before
117133
committing.
118-
12. **Run the CI gate locally**`pnpm run lint && pnpm run typecheck && pnpm run build && pnpm run test:ci`.
134+
12. **Run local verification in order**`pnpm install`, then the relevant
135+
`build`, `typecheck`, `test:ci`, format, and lint checks.
119136
Every one must pass. → [`references/definition-of-done.md`](./references/definition-of-done.md)
120137

121138
A detailed, numbered version of this flow lives at

.agents/skills/airfoil-kit/assets/rename-checklist.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@ rg -l "template" connectors/producer-<service> --glob '!**/__cassettes__' --glob
3030
| `@useairfoil/producer-template` | `@useairfoil/producer-<service>` |
3131
| `TEMPLATE_` (env prefix) | `<SERVICE>_` |
3232
| `TemplateApiClient` | `<Service>ApiClient` |
33-
| `TemplateApiClientConfig` | `<Service>ApiClientConfig` |
33+
| API raw-config layer | `layer` |
3434
| `TemplateApiClientService` | `<Service>ApiClientService` |
3535
| `TemplateListPage` | `<Service>ListPage` |
3636
| `TemplateConfig` (type) | `<Service>Config` |
3737
| `TemplateConfigConfig` (Config value) | `<Service>ConfigConfig` |
3838
| `TemplateConnector` (service tag) | `<Service>Connector` |
39-
| `TemplateConnectorConfig` (layer factory) | `<Service>ConnectorConfig` |
39+
| Config-decoded layers | `layerConfig(config)` |
4040
| `TemplateConnectorRuntime` | `<Service>ConnectorRuntime` |
41-
| `makeTemplateConnector` | `make<Service>Connector` |
41+
| Connector constructor | `make` |
4242
| `Template` (any other identifier prefix) | `<Service>` |
4343
| `template` (lowercase in strings / URNs) | `<service>` |
4444
| `@useairfoil/producer-template/TemplateApiClient` | `@useairfoil/producer-<service>/<Service>ApiClient` |
@@ -92,7 +92,8 @@ recreate it.
9292
Rewrite `connectors/producer-<service>/README.md`:
9393

9494
- Drop every JSONPlaceholder reference.
95-
- Document the real API entities, auth, base URLs, env vars.
95+
- Document the current public exports, real API entities, auth, runtime
96+
wiring, base URLs, and env vars.
9697
- List known limitations specific to the target (rate limits, missing
9798
historical data, sandbox quirks).
9899

.agents/skills/airfoil-kit/references/connector-kit-api.md

Lines changed: 65 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,10 @@ import {
1111
defineConnector,
1212
defineEntity,
1313
defineEvent,
14-
makePullStream,
15-
makeWebhookQueue,
14+
Ingestion,
1615
Publisher,
17-
runConnector,
18-
StateStore,
19-
StateStoreInMemory,
20-
WingsPublisherLayer,
21-
ConnectorRuntimeContext,
22-
ConnectorRuntimeContextLayer,
23-
buildWebhookRouter,
16+
Streams,
17+
Webhook,
2418
} from "@useairfoil/connector-kit";
2519

2620
import type {
@@ -37,11 +31,8 @@ import type {
3731
IngestionState,
3832
LiveSource,
3933
LiveStream,
40-
RunConnectorOptions,
4134
StreamState,
4235
Transform,
43-
WebhookRoute,
44-
WebhookStream,
4536
} from "@useairfoil/connector-kit";
4637
```
4738

@@ -232,12 +223,12 @@ runConnector(
232223
): Effect.Effect<void, ConnectorError, StateStore | Publisher>;
233224

234225
// With webhook: also requires HttpServer
235-
runConnector<TPayload>(
226+
runConnector(
236227
connector,
237228
options: {
238229
initialCutoff?: Cursor;
239230
webhook: {
240-
routes: ReadonlyArray<WebhookRoute<TPayload>>;
231+
routes: ReadonlyArray<Webhook.WebhookRoute>;
241232
healthPath?: HttpRouter.PathInput; // default "/health"
242233
disableHttpLogger?: boolean; // default true
243234
};
@@ -247,15 +238,35 @@ runConnector<TPayload>(
247238

248239
Internally:
249240

250-
- Provides `ConnectorRuntimeContextLayer(connector)` so downstream spans can
251-
tag metrics with `connector.name`.
241+
- Provides an internal connector runtime context so downstream spans can tag
242+
metrics with `connector.name`.
252243
- Wraps the whole run in an `Effect.withSpan("connector.run", ...)`.
253244
- Emits `connector_batches_total`, `connector_rows_total`, and
254245
`connector_batch_size` via `effect/Metric`.
255246
- For webhooks, composes `buildWebhookRouter(routes)` with a `/health`
256247
route and serves it via `HttpRouter.serve(app, { disableLogger })`.
257248

258-
### `RunConnectorOptions<TWebhookPayload>`
249+
Current runtime composition pattern around `runConnector(...)`:
250+
251+
```ts
252+
const ConnectorLayer = layerConfig.pipe(Layer.provide(EnvLayer));
253+
254+
const program = Effect.gen(function* () {
255+
const { connector, routes } = yield* MyConnector;
256+
const serverLayer = NodeHttpServer.layer(createServer, { port: 8080 });
257+
258+
return yield* Ingestion.runConnector(connector, {
259+
initialCutoff: new Date(),
260+
webhook: {
261+
routes,
262+
healthPath: "/health",
263+
disableHttpLogger: true,
264+
},
265+
}).pipe(Effect.provide(serverLayer));
266+
});
267+
```
268+
269+
### `Ingestion.RunConnectorOptions`
259270

260271
Exposed type for callers who build options programmatically.
261272

@@ -282,7 +293,7 @@ class StateStore extends Context.Service<
282293

283294
Keyed by entity/event name. One row per stream.
284295

285-
### `StateStoreInMemory`
296+
### `Ingestion.layerMemory`
286297

287298
In-process `Map<string, IngestionState>` backed `StateStore` layer. Use for
288299
the sandbox runner and tests. Production deployments provide a durable
@@ -310,10 +321,10 @@ class Publisher extends Context.Service<
310321
`PublishAck = { readonly success: boolean }`. The engine fails the stream
311322
if `publish` fails.
312323

313-
### `WingsPublisherLayer(config)`
324+
### `Publisher.layerWings(config)`
314325

315326
```ts
316-
WingsPublisherLayer({
327+
Publisher.layerWings({
317328
connector,
318329
topics: { customers: customerTopic, orders: orderTopic },
319330
partitionValues: { customers: "account_id" },
@@ -323,24 +334,27 @@ WingsPublisherLayer({
323334
Production-grade publisher that fans each entity into a Wings topic. For
324335
the sandbox / tests, use a hand-written console publisher instead.
325336

337+
Current tag access pattern in this repo is `Publisher.Publisher` from the root
338+
module namespace.
339+
326340
---
327341

328342
## Streams
329343

330-
### `makeWebhookQueue<T>(options?)`
344+
### `Streams.makeWebhookQueue<T>(options?)`
331345

332346
```ts
333-
makeWebhookQueue<T>({ capacity?: number }): Effect.Effect<WebhookStream<T>>;
347+
Streams.makeWebhookQueue<T>({ capacity?: number }): Effect.Effect<WebhookStream<T>>;
334348
```
335349

336350
Creates a bounded `Queue` (default capacity 1024) and its `Stream.fromQueue`
337351
view. Always keep the queue bounded — unbounded queues can let a noisy
338352
webhook drown the publisher.
339353

340-
### `makePullStream<T, R>(options)`
354+
### `Streams.makePullStream<T, R>(options)`
341355

342356
```ts
343-
makePullStream({
357+
Streams.makePullStream({
344358
initialCursor?: Cursor,
345359
fetchPage: (cursor: Cursor | undefined) => Effect.Effect<PullPage<T>, ConnectorError, R>,
346360
}): Stream.Stream<Batch<T>, ConnectorError, R>;
@@ -360,14 +374,14 @@ list endpoint.
360374

361375
## Webhooks
362376

363-
### `WebhookRoute<TPayload>`
377+
### `Webhook.WebhookRoute<S>`
364378

365379
```ts
366-
type WebhookRoute<TPayload> = {
380+
type WebhookRoute<S extends Schema.Schema<any>> = {
367381
readonly path: HttpRouter.PathInput;
368-
readonly schema: Schema.Schema<TPayload>;
382+
readonly schema: S;
369383
readonly handle: (
370-
payload: TPayload,
384+
payload: Schema.Schema.Type<S>,
371385
request: HttpServerRequest.HttpServerRequest,
372386
rawBody?: Uint8Array,
373387
) => Effect.Effect<void, ConnectorError>;
@@ -378,7 +392,7 @@ The framework decodes the request body, validates against `schema`, and
378392
invokes `handle(payload, request, rawBody)`. Use `rawBody` for HMAC
379393
verification; use `payload` for dispatch.
380394

381-
### `buildWebhookRouter(routes)`
395+
### `Webhook.buildWebhookRouter(routes)`
382396

383397
Low-level helper that turns an array of routes into an `HttpRouter` Layer.
384398
`runConnector(...)` uses this internally; you rarely call it directly.
@@ -387,19 +401,6 @@ Low-level helper that turns an array of routes into an `HttpRouter` Layer.
387401

388402
## Runtime context
389403

390-
### `ConnectorRuntimeContext`
391-
392-
Service tag exposing `{ connector: ConnectorDefinition }`. The engine sets
393-
this via `ConnectorRuntimeContextLayer(connector)`. Metrics attributes use
394-
it to tag batches with `connector.name`.
395-
396-
### `ConnectorRuntimeContextLayer(connector)`
397-
398-
Returns a `Layer.succeed(ConnectorRuntimeContext)({ connector })`. Call this
399-
in custom test harnesses if you bypass `runConnector`.
400-
401-
---
402-
403404
## Observability (provided by the engine)
404405

405406
### Spans
@@ -429,27 +430,36 @@ sandbox uses `Observability.Otlp.layerJson({ baseUrl, resource })` from
429430
## Typical composition recipe
430431

431432
```ts
432-
const runtimeLayer = Layer.mergeAll(
433-
StateStoreInMemory,
434-
ConsolePublisherLayer, // or WingsPublisherLayer(...)
435-
MyConnectorConfig(), // Layer<MyConnector, ConnectorError, HttpClient>
433+
const EnvLayer = Layer.mergeAll(
434+
FetchHttpClient.layer,
435+
Layer.succeed(ConfigProvider.ConfigProvider, ConfigProvider.fromEnv()),
436+
)
437+
438+
const ConnectorLayer = layerConfig.pipe(Layer.provide(EnvLayer))
439+
440+
const TelemetryLayer = Layer.unwrap(...).pipe(Layer.provide(EnvLayer))
441+
442+
const RuntimeLayer = Layer.mergeAll(
443+
Ingestion.layerMemory,
444+
ConsolePublisherLayer, // or Publisher.layerWings(...)
445+
ConnectorLayer,
436446
Logger.layer([Logger.consolePretty()]),
437-
TelemetryLayer, // optional
438-
Layer.mergeAll(
439-
FetchHttpClient.layer,
440-
Layer.succeed(ConfigProvider.ConfigProvider, ConfigProvider.fromEnv()),
441-
),
447+
TelemetryLayer,
442448
);
443449

444450
const program = Effect.gen(function* () {
445451
const { connector, routes } = yield* MyConnector;
446-
return yield* runConnector(connector, {
452+
return yield* Ingestion.runConnector(connector, {
447453
initialCutoff: new Date(),
448-
webhook: { routes },
454+
webhook: {
455+
routes,
456+
healthPath: "/health",
457+
disableHttpLogger: true,
458+
},
449459
}).pipe(Effect.provide(NodeHttpServer.layer(createServer, { port: 8080 })));
450-
});
460+
}).pipe(Effect.annotateLogs({ component: "producer-foo" }));
451461

452-
Effect.runPromise(Effect.scoped(program).pipe(Effect.provide(runtimeLayer)));
462+
Effect.runPromise(Effect.scoped(program).pipe(Effect.provide(RuntimeLayer)));
453463
```
454464

455465
See `connectors/producer-polar/src/sandbox.ts` for the live reference.

0 commit comments

Comments
 (0)