@useairfoil/connector-kit 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +279 -90
  2. package/dist/chunk-Bp6m_JJh.js +13 -0
  3. package/dist/connector-app/index.d.ts +9 -0
  4. package/dist/connector-app/index.js +8 -0
  5. package/dist/connector-app-CvqbfN6R.js +32 -0
  6. package/dist/connector-app-CvqbfN6R.js.map +1 -0
  7. package/dist/errors/index.d.ts +2 -0
  8. package/dist/errors/index.js +3 -0
  9. package/dist/errors-DHWylqRZ.js +8 -0
  10. package/dist/errors-DHWylqRZ.js.map +1 -0
  11. package/dist/index-8KnVpoZp.d.ts +32 -0
  12. package/dist/index-BlhJCpEk.d.ts +124 -0
  13. package/dist/index-CQJsFJs3.d.ts +23 -0
  14. package/dist/index-DK9YcR6W.d.ts +25 -0
  15. package/dist/index-DaNW63Mv.d.ts +21 -0
  16. package/dist/index-DyuT2jzP.d.ts +14 -0
  17. package/dist/index.d.ts +9 -166
  18. package/dist/index.js +44 -229
  19. package/dist/index.js.map +1 -1
  20. package/dist/ingestion/index.d.ts +6 -0
  21. package/dist/ingestion/index.js +7 -0
  22. package/dist/ingestion-CMCroK7Y.js +171 -0
  23. package/dist/ingestion-CMCroK7Y.js.map +1 -0
  24. package/dist/publisher/index.d.ts +5 -0
  25. package/dist/publisher/index.js +5 -0
  26. package/dist/publisher-DusgYvaV.js +131 -0
  27. package/dist/publisher-DusgYvaV.js.map +1 -0
  28. package/dist/server-DAeFWT4o.js +113 -0
  29. package/dist/server-DAeFWT4o.js.map +1 -0
  30. package/dist/service-B7g_lHMI.d.ts +32 -0
  31. package/dist/state-store/index.d.ts +4 -0
  32. package/dist/state-store/index.js +3 -0
  33. package/dist/state-store-DG_4QpWG.js +31 -0
  34. package/dist/state-store-DG_4QpWG.js.map +1 -0
  35. package/dist/telemetry-p0dE5Mdu.js +171 -0
  36. package/dist/telemetry-p0dE5Mdu.js.map +1 -0
  37. package/dist/types-ZNrA-Dn0.d.ts +160 -0
  38. package/dist/webhook/index.d.ts +5 -0
  39. package/dist/webhook/index.js +6 -0
  40. package/dist/webhook-BSA-tXtU.js +13 -0
  41. package/dist/webhook-BSA-tXtU.js.map +1 -0
  42. package/package.json +41 -14
  43. package/src/connector-app/index.ts +1 -0
  44. package/src/connector-app/start.ts +38 -0
  45. package/src/core/builder.ts +73 -15
  46. package/src/core/index.ts +22 -0
  47. package/src/core/types.ts +186 -78
  48. package/src/errors/index.ts +1 -0
  49. package/src/index.ts +8 -28
  50. package/src/ingestion/engine.ts +302 -206
  51. package/src/ingestion/index.ts +1 -0
  52. package/src/publisher/console.ts +18 -0
  53. package/src/publisher/index.ts +7 -0
  54. package/src/publisher/service.ts +33 -14
  55. package/src/publisher/wings.ts +162 -82
  56. package/src/runtime/context.ts +15 -0
  57. package/src/state-store/index.ts +2 -0
  58. package/src/state-store/memory.ts +25 -0
  59. package/src/state-store/service.ts +18 -0
  60. package/src/telemetry.ts +246 -0
  61. package/src/webhook/index.ts +8 -0
  62. package/src/webhook/server.ts +177 -54
  63. package/src/webhook/types.ts +6 -16
  64. package/src/ingestion/state-store.ts +0 -36
  65. package/src/streams/pull-stream.ts +0 -55
  66. package/src/streams/webhook-queue.ts +0 -15
  67. /package/src/{core/errors.ts → errors/connector-error.ts} +0 -0
@@ -0,0 +1,23 @@
1
+ import { _ as WebhookAckMode, b as WebhookRouteContext, c as ResourceBatch, l as ResourceDefinition, x as WebhookRouteInput, y as WebhookRoute } from "./types-ZNrA-Dn0.js";
2
+ import { i as Publisher } from "./service-B7g_lHMI.js";
3
+ import { Context, Queue } from "effect";
4
+ import { HttpRouter } from "effect/unstable/http";
5
+ import * as effect_Layer0 from "effect/Layer";
6
+
7
+ //#region src/webhook/server.d.ts
8
+ type QueuedWebhookBatch = {
9
+ readonly resource: string;
10
+ readonly batch: ResourceBatch;
11
+ };
12
+ declare const WebhookQueue_base: Context.ServiceClass<WebhookQueue, "@useairfoil/connector-kit/WebhookQueue", {
13
+ readonly queue: Queue.Queue<QueuedWebhookBatch>;
14
+ }>;
15
+ declare class WebhookQueue extends WebhookQueue_base {}
16
+ declare const router: (routes: ReadonlyArray<WebhookRoute>) => effect_Layer0.Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Requires", Publisher> | HttpRouter.Request<"Requires", WebhookQueue>>;
17
+ declare namespace index_d_exports {
18
+ export { WebhookAckMode, WebhookRoute, WebhookRouteContext, WebhookRouteInput, route, router };
19
+ }
20
+ declare const route: <const Resources extends ReadonlyArray<ResourceDefinition>, Payload>(definition: WebhookRouteInput<Resources, Payload>) => WebhookRoute<Resources, Payload>;
21
+ //#endregion
22
+ export { route as n, router as r, index_d_exports as t };
23
+ //# sourceMappingURL=index-CQJsFJs3.d.ts.map
@@ -0,0 +1,25 @@
1
+ import { t as ConnectorError } from "./index-DyuT2jzP.js";
2
+ import { c as ResourceBatch, n as ConnectorDefinition } from "./types-ZNrA-Dn0.js";
3
+ import { a as PublisherService, i as Publisher, n as PublishOptions, r as PublishSource, t as PublishAck } from "./service-B7g_lHMI.js";
4
+ import { Layer } from "effect";
5
+ import * as Wings from "@useairfoil/wings";
6
+
7
+ //#region src/publisher/wings.d.ts
8
+ type WingsTableMapping = {
9
+ readonly name: string;
10
+ readonly partitionValue?: Wings.PartitionValue.PartitionValue;
11
+ };
12
+ type WingsPublisherConfig = {
13
+ readonly connector: ConnectorDefinition;
14
+ readonly tables: Record<string, string | WingsTableMapping>;
15
+ };
16
+ declare const layerWings: (config: WingsPublisherConfig) => Layer.Layer<Publisher, ConnectorError, Wings.WingsClient.WingsClient>;
17
+ //#endregion
18
+ //#region src/publisher/console.d.ts
19
+ declare const layerConsole: Layer.Layer<Publisher, never, never>;
20
+ declare namespace index_d_exports {
21
+ export { PublishAck, PublishOptions, PublishSource, Publisher, PublisherService, ResourceBatch, WingsPublisherConfig, layerConsole, layerWings };
22
+ }
23
+ //#endregion
24
+ export { layerWings as i, layerConsole as n, WingsPublisherConfig as r, index_d_exports as t };
25
+ //# sourceMappingURL=index-DK9YcR6W.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { t as ConnectorError } from "./index-DyuT2jzP.js";
2
+ import { g as ResourceState } from "./types-ZNrA-Dn0.js";
3
+ import { Context, Effect, Layer } from "effect";
4
+
5
+ //#region src/state-store/service.d.ts
6
+ interface StateStoreService {
7
+ readonly getResourceState: (resource: string) => Effect.Effect<ResourceState | undefined, ConnectorError>;
8
+ readonly setResourceState: (resource: string, state: ResourceState) => Effect.Effect<void, ConnectorError>;
9
+ }
10
+ declare const StateStore_base: Context.ServiceClass<StateStore, "@useairfoil/connector-kit/StateStore", StateStoreService>;
11
+ declare class StateStore extends StateStore_base {}
12
+ //#endregion
13
+ //#region src/state-store/memory.d.ts
14
+ /** In-memory resource state store backed by a `Ref<Map>`. Suitable for development and tests. */
15
+ declare const layerMemory: Layer.Layer<StateStore>;
16
+ declare namespace index_d_exports {
17
+ export { StateStore, StateStoreService, layerMemory };
18
+ }
19
+ //#endregion
20
+ export { StateStoreService as i, layerMemory as n, StateStore as r, index_d_exports as t };
21
+ //# sourceMappingURL=index-DaNW63Mv.d.ts.map
@@ -0,0 +1,14 @@
1
+ import * as effect_Types0 from "effect/Types";
2
+ import * as effect_Cause0 from "effect/Cause";
3
+
4
+ //#region src/errors/connector-error.d.ts
5
+ declare const ConnectorError_base: new <A extends Record<string, any> = {}>(args: effect_Types0.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => effect_Cause0.YieldableError & {
6
+ readonly _tag: "ConnectorError";
7
+ } & Readonly<A>;
8
+ declare class ConnectorError extends ConnectorError_base<{
9
+ readonly message: string;
10
+ readonly cause?: unknown;
11
+ }> {}
12
+ //#endregion
13
+ export { ConnectorError as t };
14
+ //# sourceMappingURL=index-DyuT2jzP.d.ts.map
package/dist/index.d.ts CHANGED
@@ -1,166 +1,9 @@
1
- import { Effect, Layer, Queue, Schema, Stream } from "effect";
2
- import * as ServiceMap from "effect/ServiceMap";
3
- import * as Wings from "@useairfoil/wings";
4
- import { PartitionValue } from "@useairfoil/wings";
5
- import { HttpRouter, HttpServerRequest } from "effect/unstable/http";
6
- import * as effect_Types0 from "effect/Types";
7
- import * as effect_Cause0 from "effect/Cause";
8
- import * as Effect$1 from "effect/Effect";
9
- import * as effect_Layer0 from "effect/Layer";
10
- import * as Schema$1 from "effect/Schema";
11
-
12
- //#region src/core/errors.d.ts
13
- declare const ConnectorError_base: new <A extends Record<string, any> = {}>(args: effect_Types0.MatchRecord<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }, void, { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => effect_Cause0.YieldableError & {
14
- readonly _tag: "ConnectorError";
15
- } & Readonly<A>;
16
- declare class ConnectorError extends ConnectorError_base<{
17
- readonly message: string;
18
- readonly cause?: unknown;
19
- }> {}
20
- //#endregion
21
- //#region src/core/types.d.ts
22
- type Cursor = string | number | bigint | Date;
23
- type Batch<T> = {
24
- readonly cursor: Cursor;
25
- readonly rows: ReadonlyArray<T>;
26
- };
27
- type StreamState<C = Cursor> = {
28
- readonly cutoff: C;
29
- readonly current?: C;
30
- };
31
- type IngestionState<C = Cursor> = {
32
- readonly backfill: StreamState<C>;
33
- readonly live: StreamState<C>;
34
- };
35
- type Transform<T> = (row: T) => Effect.Effect<T, ConnectorError>;
36
- type LiveStream<T> = Stream.Stream<Batch<T>, ConnectorError>;
37
- type BackfillStream<T> = Stream.Stream<Batch<T>, ConnectorError>;
38
- type WebhookStream<T> = {
39
- /**
40
- * The queue is used to store the batches that are received from the webhook.
41
- */
42
- readonly queue: Queue.Queue<Batch<T>>;
43
- /**
44
- * The stream is used to process the batches that are received from the webhook.
45
- */
46
- readonly stream: Stream.Stream<Batch<T>, ConnectorError>;
47
- };
48
- type LiveSource<T> = LiveStream<T> | WebhookStream<T>;
49
- /** Schema type used by connector definitions. */
50
- type EntitySchema = Schema.Schema<unknown>;
51
- /** Decoded row type produced by a schema. */
52
- type EntityType<S extends EntitySchema> = Schema.Schema.Type<S>;
53
- /** Primary key type derived from the decoded schema shape. */
54
- type EntityKey<S extends EntitySchema> = EntityType<S> extends Record<string, unknown> ? keyof EntityType<S> & string : never;
55
- /** Row type constrained to object-like shapes for ingestion. */
56
- type EntityRow<S extends EntitySchema> = EntityType<S> & Record<string, unknown>;
57
- type EntityDefinition<S extends EntitySchema> = {
58
- readonly name: string;
59
- readonly schema: S;
60
- /**
61
- * The primary key is used to identify the rows in the database.
62
- */
63
- readonly primaryKey: EntityKey<S>;
64
- /**
65
- * The live source is used to stream the live data from the database.
66
- */
67
- readonly live: LiveSource<EntityRow<S>>;
68
- /**
69
- * The backfill stream is used to stream the backfill data from the database.
70
- */
71
- readonly backfill: BackfillStream<EntityRow<S>>;
72
- /**
73
- * The transform is used to transform the rows before they are published.
74
- */
75
- readonly transform?: Transform<EntityRow<S>>;
76
- };
77
- type EventDefinition<S extends EntitySchema> = {
78
- readonly name: string;
79
- readonly schema: S;
80
- /**
81
- * The live source is used to stream the live data from the database.
82
- */
83
- readonly live: LiveSource<EntityRow<S>>;
84
- /**
85
- * The backfill stream is used to stream the backfill data from the database.
86
- */
87
- readonly backfill?: BackfillStream<EntityRow<S>>;
88
- /**
89
- * The transform is used to transform the rows before they are published.
90
- */
91
- readonly transform?: Transform<EntityRow<S>>;
92
- };
93
- type ConnectorDefinition<Entities extends ReadonlyArray<EntityDefinition<EntitySchema>> = ReadonlyArray<EntityDefinition<EntitySchema>>, Events extends ReadonlyArray<EventDefinition<EntitySchema>> = ReadonlyArray<EventDefinition<EntitySchema>>> = {
94
- readonly name: string;
95
- readonly entities: Entities;
96
- readonly events: Events;
97
- };
98
- //#endregion
99
- //#region src/core/builder.d.ts
100
- declare const defineConnector: <const Entities extends ReadonlyArray<EntityDefinition<EntitySchema>>, const Events extends ReadonlyArray<EventDefinition<EntitySchema>>>(definition: ConnectorDefinition<Entities, Events>) => ConnectorDefinition<Entities, Events>;
101
- declare const defineEntity: <S extends EntitySchema>(definition: EntityDefinition<S>) => EntityDefinition<S>;
102
- declare const defineEvent: <S extends EntitySchema>(definition: EventDefinition<S>) => EventDefinition<S>;
103
- //#endregion
104
- //#region src/publisher/service.d.ts
105
- type PublishAck = {
106
- readonly success: boolean;
107
- };
108
- declare const Publisher_base: ServiceMap.ServiceClass<Publisher, "Publisher", {
109
- readonly publish: (options: {
110
- readonly name: string;
111
- readonly batch: Batch<Record<string, unknown>>;
112
- }) => Effect$1.Effect<PublishAck, ConnectorError>;
113
- }>;
114
- declare class Publisher extends Publisher_base {}
115
- //#endregion
116
- //#region src/ingestion/state-store.d.ts
117
- declare const StateStore_base: ServiceMap.ServiceClass<StateStore, "StateStore", {
118
- readonly getState: (key: string) => Effect.Effect<IngestionState<Cursor> | undefined, ConnectorError>;
119
- readonly setState: (key: string, state: IngestionState<Cursor>) => Effect.Effect<void, ConnectorError>;
120
- }>;
121
- declare class StateStore extends StateStore_base {}
122
- declare const StateStoreInMemory: Layer.Layer<StateStore, never, never>;
123
- //#endregion
124
- //#region src/ingestion/engine.d.ts
125
- declare const runConnector: (connector: ConnectorDefinition, initialCutoff: Cursor) => Effect.Effect<void, ConnectorError, StateStore | Publisher>;
126
- //#endregion
127
- //#region src/publisher/wings.d.ts
128
- type WingsPublisherConfig = {
129
- readonly connector: ConnectorDefinition;
130
- /** Map of entity/event name to Wings topic. */
131
- readonly topics: Record<string, Wings.Cluster.Topic.Topic>;
132
- /** per-stream partition value (key is entity/event name). */
133
- readonly partitionValues?: Record<string, PartitionValue>;
134
- };
135
- declare const WingsPublisherLayer: (config: WingsPublisherConfig) => Layer.Layer<Publisher, ConnectorError, Wings.WingsClient.WingsClient>;
136
- //#endregion
137
- //#region src/streams/pull-stream.d.ts
138
- type PullPage<T> = {
139
- readonly cursor: Cursor;
140
- readonly rows: ReadonlyArray<T>;
141
- readonly hasMore: boolean;
142
- };
143
- type PullFetcher<T, R = never> = (cursor: Cursor | undefined) => Effect.Effect<PullPage<T>, ConnectorError, R>;
144
- type PullStreamOptions<T, R = never> = {
145
- readonly initialCursor?: Cursor;
146
- readonly fetchPage: PullFetcher<T, R>;
147
- };
148
- declare const makePullStream: <T, R = never>(options: PullStreamOptions<T, R>) => Stream.Stream<Batch<T>, ConnectorError, R>;
149
- //#endregion
150
- //#region src/streams/webhook-queue.d.ts
151
- declare const makeWebhookQueue: <T>(options?: {
152
- readonly capacity?: number;
153
- }) => Effect.Effect<WebhookStream<T>, never>;
154
- //#endregion
155
- //#region src/webhook/types.d.ts
156
- type WebhookRoute<TPayload> = {
157
- readonly path: HttpRouter.PathInput;
158
- readonly schema: Schema$1.Schema<TPayload>;
159
- readonly handle: (payload: TPayload, request: HttpServerRequest.HttpServerRequest, rawBody?: Uint8Array) => Effect.Effect<void, ConnectorError>;
160
- };
161
- //#endregion
162
- //#region src/webhook/server.d.ts
163
- declare const buildWebhookRouter: <TPayload>(routes: ReadonlyArray<WebhookRoute<TPayload>>) => effect_Layer0.Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Requires", unknown>>;
164
- //#endregion
165
- export { type BackfillStream, type Batch, type ConnectorDefinition, ConnectorError, type Cursor, type EntityDefinition, type EntityKey, type EntityRow, type EntitySchema, type EntityType, type EventDefinition, type IngestionState, type LiveSource, type LiveStream, Publisher, StateStore, StateStoreInMemory, type StreamState, type Transform, type WebhookRoute, type WebhookStream, WingsPublisherLayer, buildWebhookRouter, defineConnector, defineEntity, defineEvent, makePullStream, makeWebhookQueue, runConnector };
166
- //# sourceMappingURL=index.d.ts.map
1
+ import { t as ConnectorError } from "./index-DyuT2jzP.js";
2
+ import { _ as WebhookAckMode, a as FetchChangesResult, b as WebhookRouteContext, c as ResourceBatch, d as ResourceMutation, f as ResourcePayload, g as ResourceState, h as ResourceSchema, l as ResourceDefinition, m as ResourceRows, n as ConnectorDefinition, o as FetchPageResult, p as ResourceRow, r as Cursor$1, s as PageFetch, t as ChangesFetch, u as ResourceField, v as WebhookHandler, y as WebhookRoute } from "./types-ZNrA-Dn0.js";
3
+ import { a as telemetry_d_exports, c as Fetch, l as Resource, o as Connector, s as Cursor, t as index_d_exports } from "./index-BlhJCpEk.js";
4
+ import "./service-B7g_lHMI.js";
5
+ import { t as index_d_exports$3 } from "./index-DaNW63Mv.js";
6
+ import { t as index_d_exports$1 } from "./index-8KnVpoZp.js";
7
+ import { t as index_d_exports$2 } from "./index-DK9YcR6W.js";
8
+ import { t as index_d_exports$4 } from "./index-CQJsFJs3.js";
9
+ export { ChangesFetch, Connector, index_d_exports as ConnectorApp, ConnectorDefinition, ConnectorError, Cursor, Cursor$1 as CursorTypes, Fetch, FetchChangesResult, FetchPageResult, index_d_exports$1 as Ingestion, PageFetch, index_d_exports$2 as Publisher, Resource, ResourceBatch, ResourceDefinition, ResourceField, ResourceMutation, ResourcePayload, ResourceRow, ResourceRows, ResourceSchema, ResourceState, index_d_exports$3 as StateStore, telemetry_d_exports as Telemetry, index_d_exports$4 as Webhook, WebhookAckMode, WebhookHandler, WebhookRoute, WebhookRouteContext };
package/dist/index.js CHANGED
@@ -1,237 +1,52 @@
1
- import { Cause, Data, Effect, Layer, Queue, Ref, Schema, Stream } from "effect";
2
- import * as ServiceMap from "effect/ServiceMap";
3
- import * as Wings from "@useairfoil/wings";
4
- import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
1
+ import { t as ConnectorError } from "./errors-DHWylqRZ.js";
2
+ import { s as telemetry_exports } from "./telemetry-p0dE5Mdu.js";
3
+ import { t as state_store_exports } from "./state-store-DG_4QpWG.js";
4
+ import "./server-DAeFWT4o.js";
5
+ import { t as ingestion_exports } from "./ingestion-CMCroK7Y.js";
6
+ import { t as connector_app_exports } from "./connector-app-CvqbfN6R.js";
7
+ import { t as publisher_exports } from "./publisher-DusgYvaV.js";
8
+ import { n as webhook_exports } from "./webhook-BSA-tXtU.js";
9
+ import { DateTime, Effect } from "effect";
5
10
 
6
11
  //#region src/core/builder.ts
7
- const defineConnector = (definition) => definition;
8
- const defineEntity = (definition) => definition;
9
- const defineEvent = (definition) => definition;
10
-
11
- //#endregion
12
- //#region src/core/errors.ts
13
- var ConnectorError = class extends Data.TaggedError("ConnectorError") {};
14
-
15
- //#endregion
16
- //#region src/publisher/service.ts
17
- var Publisher = class extends ServiceMap.Service()("Publisher") {};
18
-
19
- //#endregion
20
- //#region src/ingestion/state-store.ts
21
- var StateStore = class extends ServiceMap.Service()("StateStore") {};
22
- const StateStoreInMemory = Layer.effect(StateStore)(Effect.gen(function* () {
23
- const ref = yield* Ref.make(/* @__PURE__ */ new Map());
24
- return {
25
- getState: (key) => Effect.map(Ref.get(ref), (map) => map.get(key)),
26
- setState: (key, state) => Effect.map(Ref.update(ref, (map) => {
27
- const next = new Map(map);
28
- next.set(key, state);
29
- return next;
30
- }), () => void 0)
31
- };
32
- }));
33
-
34
- //#endregion
35
- //#region src/ingestion/engine.ts
36
- const runConnector = (connector, initialCutoff) => Effect.gen(function* () {
37
- const entityRuns = connector.entities.map((entity) => runEntity(entity, initialCutoff));
38
- const eventRuns = connector.events.map((event) => runEvent(event, initialCutoff));
39
- yield* Effect.all([...entityRuns, ...eventRuns], { concurrency: "unbounded" });
40
- });
41
- const createInitialState = (cutoff) => ({
42
- backfill: { cutoff },
43
- live: { cutoff }
44
- });
45
- const makeStateRef = (key, initialCutoff) => Effect.gen(function* () {
46
- const initial = (yield* (yield* StateStore).getState(key)) ?? createInitialState(initialCutoff);
47
- return yield* Ref.make(initial);
48
- });
49
- const runEntity = (entity, initialCutoff) => Effect.gen(function* () {
50
- const stateRef = yield* makeStateRef(entity.name, initialCutoff);
51
- const seenRef = yield* Ref.make(/* @__PURE__ */ new Set());
52
- const liveStream = resolveLiveStream(entity.live);
53
- const tagLive = (batch) => ({
54
- source: "live",
55
- batch
56
- });
57
- const updateSeen = (rows) => Ref.update(seenRef, (seen) => {
58
- const next = new Set(seen);
59
- for (const row of rows) {
60
- const key = String(row[entity.primaryKey]);
61
- next.add(key);
62
- }
63
- return next;
64
- });
65
- const backfillTagged = Stream.mapEffect(entity.backfill, (batch) => Ref.get(seenRef).pipe(Effect.map((seen) => {
66
- const filtered = batch.rows.filter((row) => {
67
- const key = String(row[entity.primaryKey]);
68
- return !seen.has(key);
69
- });
70
- return {
71
- source: "backfill",
72
- batch: {
73
- cursor: batch.cursor,
74
- rows: filtered
75
- }
76
- };
77
- }))).pipe(Stream.tap(({ batch }) => updateSeen(batch.rows)));
78
- if (isWebhookStream(entity.live)) {
79
- const firstBatch = yield* Queue.take(entity.live.queue);
80
- yield* updateSeen(firstBatch.rows);
81
- yield* processTaggedStream(Stream.make({
82
- source: "live",
83
- batch: firstBatch
84
- }), entity.name, entity.transform, stateRef);
85
- const liveTailTagged = Stream.map(liveStream, tagLive).pipe(Stream.tap(({ batch }) => updateSeen(batch.rows)));
86
- yield* processTaggedStream(Stream.merge(liveTailTagged, backfillTagged), entity.name, entity.transform, stateRef);
87
- return;
88
- }
89
- const liveTagged = Stream.map(liveStream, tagLive).pipe(Stream.tap(({ batch }) => updateSeen(batch.rows)));
90
- yield* processTaggedStream(Stream.merge(liveTagged, backfillTagged), entity.name, entity.transform, stateRef);
91
- });
92
- const runEvent = (event, initialCutoff) => Effect.gen(function* () {
93
- const stateRef = yield* makeStateRef(event.name, initialCutoff);
94
- const liveStream = resolveLiveStream(event.live);
95
- if (event.backfill) yield* processTaggedStream(Stream.map(event.backfill, (batch) => ({
96
- source: "backfill",
97
- batch
98
- })), event.name, event.transform, stateRef);
99
- yield* processTaggedStream(Stream.map(liveStream, (batch) => ({
100
- source: "live",
101
- batch
102
- })), event.name, event.transform, stateRef);
103
- });
104
- const updateState = (state, source, cursor) => source === "live" ? {
105
- ...state,
106
- live: {
107
- ...state.live,
108
- current: cursor
109
- }
110
- } : {
111
- ...state,
112
- backfill: {
113
- ...state.backfill,
114
- current: cursor
115
- }
12
+ const Connector = { define: (definition) => definition };
13
+ const Resource = {
14
+ entity: (definition) => definition,
15
+ webhook: (definition) => definition,
16
+ upsert: (row) => ({
17
+ op: "upsert",
18
+ row
19
+ }),
20
+ delete: (options) => ({
21
+ op: "delete",
22
+ key: options.key,
23
+ version: options.version
24
+ })
116
25
  };
117
- const resolveLiveStream = (source) => isWebhookStream(source) ? source.stream : source;
118
- const isWebhookStream = (source) => typeof source === "object" && source !== null && "queue" in source && "stream" in source;
119
- const processTaggedStream = (stream, name, transform, stateRef) => Stream.runForEach(stream, ({ source, batch }) => Effect.gen(function* () {
120
- const rows = transform ? yield* Effect.forEach(batch.rows, transform) : batch.rows;
121
- yield* (yield* Publisher).publish({
122
- name,
123
- batch: {
124
- cursor: batch.cursor,
125
- rows
26
+ const Fetch = {
27
+ page: (definition) => definition,
28
+ changes: (definition) => definition
29
+ };
30
+ const decodeFailure = (kind, value) => new ConnectorError({ message: `Invalid ${kind} cursor value: ${String(value)}` });
31
+ const Cursor = {
32
+ string: () => ({
33
+ kind: "string",
34
+ decode: (value) => typeof value === "string" ? Effect.succeed(value) : Effect.fail(decodeFailure("string", value))
35
+ }),
36
+ number: () => ({
37
+ kind: "number",
38
+ decode: (value) => typeof value === "number" && Number.isFinite(value) ? Effect.succeed(value) : Effect.fail(decodeFailure("number", value))
39
+ }),
40
+ isoDateTime: () => ({
41
+ kind: "isoDateTime",
42
+ decode: (value) => {
43
+ if (typeof value !== "string" || Number.isNaN(Date.parse(value))) return Effect.fail(decodeFailure("ISO datetime", value));
44
+ return Effect.succeed(value);
126
45
  }
127
- });
128
- const nextState = yield* Ref.updateAndGet(stateRef, (state) => updateState(state, source, batch.cursor));
129
- yield* (yield* StateStore).setState(name, nextState);
130
- }));
131
-
132
- //#endregion
133
- //#region src/publisher/wings.ts
134
- const buildRecordBatch = (rows) => {
135
- const [batch] = Wings.tableFromJSON(Array.from(rows)).batches;
136
- if (!batch) throw new ConnectorError({ message: "No rows to publish" });
137
- return batch;
46
+ }),
47
+ nowIsoDateTime: Effect.map(DateTime.now, DateTime.formatIso)
138
48
  };
139
- const WingsPublisherLayer = (config) => Layer.effect(Publisher)(Effect.gen(function* () {
140
- const entries = /* @__PURE__ */ new Map();
141
- for (const def of [...config.connector.entities, ...config.connector.events]) {
142
- const topic = config.topics[def.name];
143
- if (!topic) return yield* Effect.fail(new ConnectorError({ message: `Missing topic for ${def.name}` }));
144
- const partitionIndex = topic.partitionKey !== void 0 ? topic.schema.fields.findIndex((field) => field.id === topic.partitionKey) : void 0;
145
- const partitionField = partitionIndex !== void 0 && partitionIndex >= 0 ? topic.schema.fields[partitionIndex]?.name : void 0;
146
- const publisher = yield* Wings.WingsClient.publisher({
147
- topic,
148
- partitionValue: config.partitionValues?.[def.name]
149
- }).pipe(Effect.mapError((error) => new ConnectorError({
150
- message: error.message,
151
- cause: error
152
- })));
153
- entries.set(def.name, {
154
- publisher,
155
- partitionField,
156
- partitionValue: config.partitionValues?.[def.name]
157
- });
158
- }
159
- return { publish: ({ name, batch }) => Effect.gen(function* () {
160
- const entry = entries.get(name);
161
- if (!entry) return yield* Effect.fail(new ConnectorError({ message: `Unknown stream ${name}` }));
162
- if (batch.rows.length === 0) return { success: true };
163
- const recordBatch = buildRecordBatch(batch.rows);
164
- const result = yield* entry.publisher.push({
165
- batch: recordBatch,
166
- partitionValue: entry.partitionValue
167
- }).pipe(Effect.mapError((error) => new ConnectorError({
168
- message: error.message,
169
- cause: error
170
- })));
171
- return { success: !!(result.result && result.result.$case === "accepted") };
172
- }) };
173
- }));
174
-
175
- //#endregion
176
- //#region src/streams/pull-stream.ts
177
- const makePullStream = (options) => Stream.unfold({
178
- cursor: options.initialCursor,
179
- done: false
180
- }, (state) => Effect.gen(function* () {
181
- if (state.done) return;
182
- let nextCursor = state.cursor;
183
- while (true) {
184
- const page = yield* options.fetchPage(nextCursor);
185
- if (page.rows.length > 0) return [{
186
- cursor: page.cursor,
187
- rows: page.rows
188
- }, page.hasMore ? {
189
- cursor: page.cursor,
190
- done: false
191
- } : {
192
- cursor: void 0,
193
- done: true
194
- }];
195
- if (!page.hasMore) return;
196
- nextCursor = page.cursor;
197
- }
198
- }));
199
-
200
- //#endregion
201
- //#region src/streams/webhook-queue.ts
202
- const makeWebhookQueue = (options) => Effect.gen(function* () {
203
- const capacity = options?.capacity ?? 1024;
204
- const queue = yield* Queue.bounded(capacity);
205
- return {
206
- queue,
207
- stream: Stream.fromQueue(queue)
208
- };
209
- });
210
-
211
- //#endregion
212
- //#region src/webhook/server.ts
213
- var InvalidWebhookPayloadError = class extends Data.TaggedError("InvalidWebhookPayloadError") {};
214
- const makeHandler = (route) => Effect.gen(function* () {
215
- const request = yield* HttpServerRequest.HttpServerRequest;
216
- const rawBuffer = yield* request.arrayBuffer.pipe(Effect.mapError(() => new InvalidWebhookPayloadError({ message: "Failed to read request body" })));
217
- const rawBody = new Uint8Array(rawBuffer);
218
- const rawText = new TextDecoder().decode(rawBody);
219
- const rawJson = yield* Effect.try({
220
- try: () => JSON.parse(rawText),
221
- catch: () => new InvalidWebhookPayloadError({ message: "Invalid JSON body" })
222
- });
223
- const payload = yield* Schema.decodeUnknownEffect(route.schema)(rawJson).pipe(Effect.mapError(() => new InvalidWebhookPayloadError({ message: "Invalid webhook payload" })));
224
- yield* route.handle(payload, request, rawBody);
225
- return HttpServerResponse.jsonUnsafe({ ok: true });
226
- }).pipe(Effect.catchTag("InvalidWebhookPayloadError", () => Effect.succeed(HttpServerResponse.jsonUnsafe({
227
- ok: false,
228
- error: "Invalid webhook payload"
229
- }, { status: 400 }))), Effect.catchCause((cause) => Effect.logWarning(`Webhook handler error: ${Cause.pretty(cause)}`).pipe(Effect.andThen(Effect.succeed(HttpServerResponse.jsonUnsafe({
230
- ok: false,
231
- error: "Webhook handler failed"
232
- }, { status: 500 }))))));
233
- const buildWebhookRouter = (routes) => HttpRouter.addAll(routes.map((route) => HttpRouter.route("POST", route.path, makeHandler(route))));
234
49
 
235
50
  //#endregion
236
- export { ConnectorError, Publisher, StateStore, StateStoreInMemory, WingsPublisherLayer, buildWebhookRouter, defineConnector, defineEntity, defineEvent, makePullStream, makeWebhookQueue, runConnector };
51
+ export { Connector, connector_app_exports as ConnectorApp, ConnectorError, Cursor, Fetch, ingestion_exports as Ingestion, publisher_exports as Publisher, Resource, state_store_exports as StateStore, telemetry_exports as Telemetry, webhook_exports as Webhook };
237
52
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["nextCursor: Cursor | undefined"],"sources":["../src/core/builder.ts","../src/core/errors.ts","../src/publisher/service.ts","../src/ingestion/state-store.ts","../src/ingestion/engine.ts","../src/publisher/wings.ts","../src/streams/pull-stream.ts","../src/streams/webhook-queue.ts","../src/webhook/server.ts"],"sourcesContent":["import type {\n ConnectorDefinition,\n EntityDefinition,\n EntitySchema,\n EventDefinition,\n} from \"./types\";\n\nexport const defineConnector = <\n const Entities extends ReadonlyArray<EntityDefinition<EntitySchema>>,\n const Events extends ReadonlyArray<EventDefinition<EntitySchema>>,\n>(\n definition: ConnectorDefinition<Entities, Events>,\n) => definition;\n\nexport const defineEntity = <S extends EntitySchema>(\n definition: EntityDefinition<S>,\n) => definition;\n\nexport const defineEvent = <S extends EntitySchema>(\n definition: EventDefinition<S>,\n) => definition;\n","import { Data } from \"effect\";\n\nexport class ConnectorError extends Data.TaggedError(\"ConnectorError\")<{\n readonly message: string;\n readonly cause?: unknown;\n}> {}\n","import type * as Effect from \"effect/Effect\";\nimport * as ServiceMap from \"effect/ServiceMap\";\nimport type { ConnectorError } from \"../core/errors\";\nimport type { Batch } from \"../core/types\";\n\nexport type PublishAck = {\n readonly success: boolean;\n};\n\nexport class Publisher extends ServiceMap.Service<\n Publisher,\n {\n readonly publish: (options: {\n readonly name: string;\n readonly batch: Batch<Record<string, unknown>>;\n }) => Effect.Effect<PublishAck, ConnectorError>;\n }\n>()(\"Publisher\") {}\n","import { Effect, Layer, Ref } from \"effect\";\nimport * as ServiceMap from \"effect/ServiceMap\";\nimport type { ConnectorError } from \"../core/errors\";\nimport type { Cursor, IngestionState } from \"../core/types\";\n\nexport class StateStore extends ServiceMap.Service<\n StateStore,\n {\n readonly getState: (\n key: string,\n ) => Effect.Effect<IngestionState<Cursor> | undefined, ConnectorError>;\n readonly setState: (\n key: string,\n state: IngestionState<Cursor>,\n ) => Effect.Effect<void, ConnectorError>;\n }\n>()(\"StateStore\") {}\n\nexport const StateStoreInMemory = Layer.effect(StateStore)(\n Effect.gen(function* () {\n const ref = yield* Ref.make(new Map<string, IngestionState<Cursor>>());\n\n return {\n getState: (key) => Effect.map(Ref.get(ref), (map) => map.get(key)),\n setState: (key, state) =>\n Effect.map(\n Ref.update(ref, (map) => {\n const next = new Map(map);\n next.set(key, state);\n return next;\n }),\n () => undefined,\n ),\n };\n }),\n);\n","import { Effect, Queue, Ref, Stream } from \"effect\";\nimport type { ConnectorError } from \"../core/errors\";\nimport type {\n Batch,\n ConnectorDefinition,\n Cursor,\n EntityDefinition,\n EntityRow,\n EntitySchema,\n EventDefinition,\n IngestionState,\n LiveSource,\n Transform,\n WebhookStream,\n} from \"../core/types\";\nimport { Publisher } from \"../publisher/service\";\nimport { StateStore } from \"./state-store\";\n\ntype TaggedBatch<T> = {\n readonly source: \"live\" | \"backfill\";\n readonly batch: Batch<T>;\n};\n\nexport const runConnector = (\n connector: ConnectorDefinition,\n initialCutoff: Cursor,\n): Effect.Effect<void, ConnectorError, StateStore | Publisher> =>\n Effect.gen(function* () {\n // Start ingestion for every entity and event in parallel.\n const entityRuns = connector.entities.map((entity) =>\n runEntity(entity, initialCutoff),\n );\n const eventRuns = connector.events.map((event) =>\n runEvent(event, initialCutoff),\n );\n // main runner\n yield* Effect.all([...entityRuns, ...eventRuns], {\n concurrency: \"unbounded\",\n });\n });\n\nconst createInitialState = (cutoff: Cursor): IngestionState<Cursor> => ({\n backfill: { cutoff },\n live: { cutoff },\n});\n\nconst makeStateRef = (\n key: string,\n initialCutoff: Cursor,\n): Effect.Effect<Ref.Ref<IngestionState<Cursor>>, ConnectorError, StateStore> =>\n Effect.gen(function* () {\n // Load persisted state or initialize a new one for this stream.\n const store = yield* StateStore;\n const existing = yield* store.getState(key);\n const initial = existing ?? createInitialState(initialCutoff);\n return yield* Ref.make(initial);\n });\n\nconst runEntity = <S extends EntitySchema>(\n entity: EntityDefinition<S>,\n initialCutoff: Cursor,\n): Effect.Effect<void, ConnectorError, StateStore | Publisher> =>\n Effect.gen(function* () {\n type Row = EntityRow<S>;\n const stateRef = yield* makeStateRef(entity.name, initialCutoff);\n // Tracks which primary keys have already been emitted.\n const seenRef = yield* Ref.make(new Set<string>());\n\n const liveStream = resolveLiveStream(entity.live);\n const tagLive = (batch: Batch<Row>) => ({\n source: \"live\" as const,\n batch,\n });\n const updateSeen = (rows: ReadonlyArray<Row>) =>\n Ref.update(seenRef, (seen) => {\n const next = new Set(seen);\n for (const row of rows) {\n const key = String(row[entity.primaryKey]);\n next.add(key);\n }\n return next;\n });\n\n // Entities are upserts, so live and backfill can overlap. We keep an in-memory\n // seen set (primary keys) so backfill does not re-emit rows already observed live.\n const backfillTagged = Stream.mapEffect(entity.backfill, (batch) =>\n Ref.get(seenRef).pipe(\n Effect.map((seen) => {\n const filtered = batch.rows.filter((row) => {\n const key = String(row[entity.primaryKey]);\n return !seen.has(key);\n });\n return {\n source: \"backfill\" as const,\n batch: { cursor: batch.cursor, rows: filtered },\n };\n }),\n ),\n ).pipe(Stream.tap(({ batch }) => updateSeen(batch.rows)));\n\n // For webhook live sources, we wait for the first live batch before starting\n // backfill. That first batch establishes the cutoff timestamp and seeds the\n // seen set so backfill can de-dupe correctly.\n // Queue-backed streams are single-consumer; splitting with take/drop would\n // consume and discard elements. Take the first element directly from the\n // queue, then let Stream.fromQueue continue from element #2.\n if (isWebhookStream(entity.live)) {\n const firstBatch = yield* Queue.take(entity.live.queue);\n yield* updateSeen(firstBatch.rows);\n yield* processTaggedStream(\n Stream.make({ source: \"live\" as const, batch: firstBatch }),\n entity.name,\n entity.transform,\n stateRef,\n );\n\n // liveStream is Stream.fromQueue on the same queue, continues from element #2.\n const liveTailTagged = Stream.map(liveStream, tagLive).pipe(\n Stream.tap(({ batch }) => updateSeen(batch.rows)),\n );\n const merged = Stream.merge(liveTailTagged, backfillTagged);\n yield* processTaggedStream(\n merged,\n entity.name,\n entity.transform,\n stateRef,\n );\n return;\n }\n\n // For pull-based live sources, we can merge immediately because there is no\n // webhook cutoff gating and the live stream is not queue-backed.\n const liveTagged = Stream.map(liveStream, tagLive).pipe(\n Stream.tap(({ batch }) => updateSeen(batch.rows)),\n );\n\n const merged = Stream.merge(liveTagged, backfillTagged);\n yield* processTaggedStream(merged, entity.name, entity.transform, stateRef);\n });\n\nconst runEvent = <S extends EntitySchema>(\n event: EventDefinition<S>,\n initialCutoff: Cursor,\n): Effect.Effect<void, ConnectorError, StateStore | Publisher> =>\n Effect.gen(function* () {\n type Row = EntityRow<S>;\n const stateRef = yield* makeStateRef(event.name, initialCutoff);\n const liveStream = resolveLiveStream<Row>(event.live);\n\n // Events must backfill first to preserve ordering.\n if (event.backfill) {\n const backfillTagged = Stream.map(event.backfill, (batch) => ({\n source: \"backfill\" as const,\n batch,\n }));\n yield* processTaggedStream(\n backfillTagged,\n event.name,\n event.transform,\n stateRef,\n );\n }\n\n const liveTagged = Stream.map(liveStream, (batch) => ({\n source: \"live\" as const,\n batch,\n }));\n yield* processTaggedStream(\n liveTagged,\n event.name,\n event.transform,\n stateRef,\n );\n });\n\nconst updateState = (\n state: IngestionState<Cursor>,\n source: \"live\" | \"backfill\",\n cursor: Cursor,\n): IngestionState<Cursor> =>\n source === \"live\"\n ? { ...state, live: { ...state.live, current: cursor } }\n : { ...state, backfill: { ...state.backfill, current: cursor } };\n\nconst resolveLiveStream = <T>(\n source: LiveSource<T>,\n): Stream.Stream<Batch<T>, ConnectorError> =>\n isWebhookStream(source) ? source.stream : source;\n\nconst isWebhookStream = <T>(\n source: LiveSource<T>,\n): source is WebhookStream<T> =>\n typeof source === \"object\" &&\n source !== null &&\n \"queue\" in source &&\n \"stream\" in source;\n\nconst processTaggedStream = <T extends Record<string, unknown>>(\n stream: Stream.Stream<TaggedBatch<T>, ConnectorError>,\n name: string,\n transform: Transform<T> | undefined,\n stateRef: Ref.Ref<IngestionState<Cursor>>,\n): Effect.Effect<void, ConnectorError, StateStore | Publisher> =>\n Stream.runForEach(stream, ({ source, batch }) =>\n Effect.gen(function* () {\n // Optional per-row transformation.\n const rows = transform\n ? yield* Effect.forEach(batch.rows, transform)\n : batch.rows;\n\n // Publish before updating cursor state.\n const publisher = yield* Publisher;\n yield* publisher.publish({\n name,\n batch: {\n cursor: batch.cursor,\n rows,\n },\n });\n\n // Persist state only after publish succeeds.\n const nextState = yield* Ref.updateAndGet(stateRef, (state) =>\n updateState(state, source, batch.cursor),\n );\n\n const store = yield* StateStore;\n yield* store.setState(name, nextState);\n }),\n );\n","import type { PartitionValue } from \"@useairfoil/wings\";\nimport * as Wings from \"@useairfoil/wings\";\nimport { Effect, Layer } from \"effect\";\nimport { ConnectorError } from \"../core/errors\";\nimport type { ConnectorDefinition } from \"../core/types\";\nimport { Publisher } from \"./service\";\n\ntype Rows = Record<string, unknown>;\n\nexport type WingsPublisherConfig = {\n readonly connector: ConnectorDefinition;\n /** Map of entity/event name to Wings topic. */\n readonly topics: Record<string, Wings.Cluster.Topic.Topic>;\n /** per-stream partition value (key is entity/event name). */\n readonly partitionValues?: Record<string, PartitionValue>;\n};\n\n/** Publisher entry for a single entity/event. */\ntype PublisherEntry = {\n /** Wings publisher for the entity/event. */\n readonly publisher: Wings.WingsClient.Publisher;\n /** Partition field name (if any). */\n readonly partitionField?: string;\n /** Partition value (if any). */\n readonly partitionValue?: PartitionValue;\n};\n\nconst buildRecordBatch = (rows: ReadonlyArray<Rows>) => {\n // Convert JSON rows into an Arrow RecordBatch for Wings.\n const table = Wings.tableFromJSON(Array.from(rows));\n const [batch] = table.batches;\n\n if (!batch) {\n throw new ConnectorError({ message: \"No rows to publish\" });\n }\n\n return batch;\n};\n\nexport const WingsPublisherLayer = (\n config: WingsPublisherConfig,\n): Layer.Layer<Publisher, ConnectorError, Wings.WingsClient.WingsClient> =>\n Layer.effect(Publisher)(\n Effect.gen(function* () {\n const entries = new Map<string, PublisherEntry>();\n\n // create and store a wings publisher for each entity/event.\n for (const def of [\n ...config.connector.entities,\n ...config.connector.events,\n ]) {\n const topic = config.topics[def.name];\n if (!topic) {\n return yield* Effect.fail(\n new ConnectorError({ message: `Missing topic for ${def.name}` }),\n );\n }\n\n const partitionIndex =\n topic.partitionKey !== undefined\n ? topic.schema.fields.findIndex(\n (field) => field.id === topic.partitionKey,\n )\n : undefined;\n\n const partitionField =\n partitionIndex !== undefined && partitionIndex >= 0\n ? topic.schema.fields[partitionIndex]?.name\n : undefined;\n\n const publisher = yield* Wings.WingsClient.publisher({\n topic,\n partitionValue: config.partitionValues?.[def.name],\n }).pipe(\n Effect.mapError(\n (error) =>\n new ConnectorError({ message: error.message, cause: error }),\n ),\n );\n\n entries.set(def.name, {\n publisher,\n partitionField,\n partitionValue: config.partitionValues?.[def.name],\n });\n }\n\n return {\n publish: ({ name, batch }) =>\n Effect.gen(function* () {\n const entry = entries.get(name);\n if (!entry) {\n return yield* Effect.fail(\n new ConnectorError({ message: `Unknown stream ${name}` }),\n );\n }\n\n if (batch.rows.length === 0) {\n return { success: true };\n }\n\n const recordBatch = buildRecordBatch(batch.rows);\n const result = yield* entry.publisher\n .push({\n batch: recordBatch,\n partitionValue: entry.partitionValue,\n })\n .pipe(\n Effect.mapError(\n (error) =>\n new ConnectorError({\n message: error.message,\n cause: error,\n }),\n ),\n );\n\n return {\n success: !!(result.result && result.result.$case === \"accepted\"),\n };\n }),\n };\n }),\n );\n","import { Effect, Stream } from \"effect\";\nimport type { ConnectorError } from \"../core/errors\";\nimport type { Batch, Cursor } from \"../core/types\";\n\nexport type PullPage<T> = {\n readonly cursor: Cursor;\n readonly rows: ReadonlyArray<T>;\n readonly hasMore: boolean;\n};\n\nexport type PullFetcher<T, R = never> = (\n cursor: Cursor | undefined,\n) => Effect.Effect<PullPage<T>, ConnectorError, R>;\n\nexport type PullStreamOptions<T, R = never> = {\n readonly initialCursor?: Cursor;\n readonly fetchPage: PullFetcher<T, R>;\n};\n\nexport const makePullStream = <T, R = never>(\n options: PullStreamOptions<T, R>,\n): Stream.Stream<Batch<T>, ConnectorError, R> =>\n Stream.unfold({ cursor: options.initialCursor, done: false }, (state) =>\n Effect.gen(function* () {\n if (state.done) {\n return undefined;\n }\n\n let nextCursor: Cursor | undefined = state.cursor;\n\n while (true) {\n const page = yield* options.fetchPage(nextCursor);\n\n if (page.rows.length > 0) {\n const batch: Batch<T> = {\n cursor: page.cursor,\n rows: page.rows,\n };\n\n return [\n batch,\n page.hasMore\n ? { cursor: page.cursor, done: false }\n : { cursor: undefined, done: true },\n ] as const;\n }\n\n if (!page.hasMore) {\n return undefined;\n }\n\n nextCursor = page.cursor;\n }\n }),\n );\n","import { Effect, Queue, Stream } from \"effect\";\nimport type { ConnectorError } from \"../core/errors\";\nimport type { Batch, WebhookStream } from \"../core/types\";\n\nexport const makeWebhookQueue = <T>(options?: {\n readonly capacity?: number;\n}): Effect.Effect<WebhookStream<T>, never> =>\n Effect.gen(function* () {\n const capacity = options?.capacity ?? 1024;\n const queue = yield* Queue.bounded<Batch<T>>(capacity);\n const stream: Stream.Stream<Batch<T>, ConnectorError> = Stream.fromQueue(\n queue,\n );\n return { queue, stream };\n });\n","import { Cause, Data, Effect, Schema } from \"effect\";\nimport {\n HttpRouter,\n HttpServerRequest,\n HttpServerResponse,\n} from \"effect/unstable/http\";\nimport type { WebhookRoute } from \"./types\";\n\nclass InvalidWebhookPayloadError extends Data.TaggedError(\n \"InvalidWebhookPayloadError\",\n)<{\n readonly message: string;\n}> {}\n\nconst makeHandler = <TPayload>(route: WebhookRoute<TPayload>) =>\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const rawBuffer = yield* request.arrayBuffer.pipe(\n Effect.mapError(\n () =>\n new InvalidWebhookPayloadError({\n message: \"Failed to read request body\",\n }),\n ),\n );\n const rawBody = new Uint8Array(rawBuffer);\n const rawText = new TextDecoder().decode(rawBody);\n const rawJson = yield* Effect.try({\n try: () => JSON.parse(rawText) as unknown,\n catch: () =>\n new InvalidWebhookPayloadError({ message: \"Invalid JSON body\" }),\n });\n const payload = yield* Schema.decodeUnknownEffect(route.schema)(\n rawJson,\n ).pipe(\n Effect.mapError(\n () =>\n new InvalidWebhookPayloadError({\n message: \"Invalid webhook payload\",\n }),\n ),\n );\n yield* route.handle(payload, request, rawBody);\n // unsafeJson serializes synchronously — no Effect, no HttpBodyError\n return HttpServerResponse.jsonUnsafe({ ok: true });\n }).pipe(\n Effect.catchTag(\"InvalidWebhookPayloadError\", () =>\n Effect.succeed(\n HttpServerResponse.jsonUnsafe(\n { ok: false, error: \"Invalid webhook payload\" },\n { status: 400 },\n ),\n ),\n ),\n Effect.catchCause((cause) =>\n Effect.logWarning(`Webhook handler error: ${Cause.pretty(cause)}`).pipe(\n Effect.andThen(\n Effect.succeed(\n HttpServerResponse.jsonUnsafe(\n { ok: false, error: \"Webhook handler failed\" },\n { status: 500 },\n ),\n ),\n ),\n ),\n ),\n );\n\nexport const buildWebhookRouter = <TPayload>(\n routes: ReadonlyArray<WebhookRoute<TPayload>>,\n) =>\n HttpRouter.addAll(\n routes.map((route) =>\n HttpRouter.route(\"POST\", route.path, makeHandler(route)),\n ),\n );\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/core/builder.ts"],"sourcesContent":["import { DateTime, Effect } from \"effect\";\n\nimport type {\n ChangesFetch,\n ConnectorDefinition,\n Cursor as CursorType,\n DeleteValue,\n PageFetch,\n ResourceDefinition,\n ResourceMutation,\n ResourceSchema,\n WebhookHandler,\n} from \"./types\";\n\nimport { ConnectorError } from \"../errors\";\n\nexport const Connector = {\n define: <const Resources extends ReadonlyArray<ResourceDefinition>>(\n definition: ConnectorDefinition<Resources>,\n ) => definition,\n};\n\nexport const Resource = {\n entity: <S extends ResourceSchema, Payload = never, R = never>(\n definition: ResourceDefinition<S, Payload, R>,\n ): ResourceDefinition<S, Payload, R> => definition,\n\n webhook: <Payload, Row extends object = never>(definition: WebhookHandler<Row, Payload>) =>\n definition,\n\n upsert: <Row extends object>(row: Row): ResourceMutation<Row> => ({ op: \"upsert\", row }),\n\n delete: (options: {\n readonly key: DeleteValue;\n readonly version: DeleteValue;\n }): ResourceMutation<never> => ({\n op: \"delete\",\n key: options.key,\n version: options.version,\n }),\n};\n\nexport const Fetch = {\n page: <Row extends object = never, R = never>(definition: PageFetch<Row, R>) => definition,\n changes: <Row extends object = never, R = never>(definition: ChangesFetch<Row, R>) => definition,\n};\n\nconst decodeFailure = (kind: string, value: unknown) =>\n new ConnectorError({ message: `Invalid ${kind} cursor value: ${String(value)}` });\n\nexport const Cursor = {\n string: (): CursorType.Definition<string> => ({\n kind: \"string\",\n decode: (value) =>\n typeof value === \"string\"\n ? Effect.succeed(value)\n : Effect.fail(decodeFailure(\"string\", value)),\n }),\n\n number: (): CursorType.Definition<number> => ({\n kind: \"number\",\n decode: (value) =>\n typeof value === \"number\" && Number.isFinite(value)\n ? Effect.succeed(value)\n : Effect.fail(decodeFailure(\"number\", value)),\n }),\n\n isoDateTime: (): CursorType.Definition<string> => ({\n kind: \"isoDateTime\",\n decode: (value) => {\n if (typeof value !== \"string\" || Number.isNaN(Date.parse(value))) {\n return Effect.fail(decodeFailure(\"ISO datetime\", value));\n }\n return Effect.succeed(value);\n },\n }),\n\n nowIsoDateTime: Effect.map(DateTime.now, DateTime.formatIso),\n};\n"],"mappings":";;;;;;;;;;;AAgBA,MAAa,YAAY,EACvB,SACE,eACG,YACN;AAED,MAAa,WAAW;CACtB,SACE,eACsC;CAExC,UAA+C,eAC7C;CAEF,SAA6B,SAAqC;EAAE,IAAI;EAAU;EAAK;CAEvF,SAAS,aAGuB;EAC9B,IAAI;EACJ,KAAK,QAAQ;EACb,SAAS,QAAQ;EAClB;CACF;AAED,MAAa,QAAQ;CACnB,OAA8C,eAAkC;CAChF,UAAiD,eAAqC;CACvF;AAED,MAAM,iBAAiB,MAAc,UACnC,IAAI,eAAe,EAAE,SAAS,WAAW,KAAK,iBAAiB,OAAO,MAAM,IAAI,CAAC;AAEnF,MAAa,SAAS;CACpB,eAA8C;EAC5C,MAAM;EACN,SAAS,UACP,OAAO,UAAU,WACb,OAAO,QAAQ,MAAM,GACrB,OAAO,KAAK,cAAc,UAAU,MAAM,CAAC;EAClD;CAED,eAA8C;EAC5C,MAAM;EACN,SAAS,UACP,OAAO,UAAU,YAAY,OAAO,SAAS,MAAM,GAC/C,OAAO,QAAQ,MAAM,GACrB,OAAO,KAAK,cAAc,UAAU,MAAM,CAAC;EAClD;CAED,oBAAmD;EACjD,MAAM;EACN,SAAS,UAAU;AACjB,OAAI,OAAO,UAAU,YAAY,OAAO,MAAM,KAAK,MAAM,MAAM,CAAC,CAC9D,QAAO,OAAO,KAAK,cAAc,gBAAgB,MAAM,CAAC;AAE1D,UAAO,OAAO,QAAQ,MAAM;;EAE/B;CAED,gBAAgB,OAAO,IAAI,SAAS,KAAK,SAAS,UAAU;CAC7D"}
@@ -0,0 +1,6 @@
1
+ import "../index-DyuT2jzP.js";
2
+ import "../types-ZNrA-Dn0.js";
3
+ import "../service-B7g_lHMI.js";
4
+ import "../index-DaNW63Mv.js";
5
+ import { n as RunOptions, r as run } from "../index-8KnVpoZp.js";
6
+ export { RunOptions, run };
@@ -0,0 +1,7 @@
1
+ import "../errors-DHWylqRZ.js";
2
+ import "../telemetry-p0dE5Mdu.js";
3
+ import "../state-store-DG_4QpWG.js";
4
+ import "../server-DAeFWT4o.js";
5
+ import { n as run } from "../ingestion-CMCroK7Y.js";
6
+
7
+ export { run };