effect-inngest 0.1.0 → 0.1.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/README.md +83 -29
- package/dist/Function.d.ts +34 -0
- package/dist/Function.js +41 -1
- package/dist/Group.d.ts +1 -2
- package/dist/Group.js +1 -1
- package/dist/HttpApi.d.ts +3 -3
- package/dist/internal/protocol.js +3 -7
- package/dist/internal/step.js +25 -16
- package/package.json +9 -7
- package/src/Function.ts +100 -3
- package/src/Group.ts +2 -6
- package/src/internal/protocol.ts +3 -7
- package/src/internal/step.ts +41 -22
package/README.md
CHANGED
|
@@ -124,11 +124,12 @@ class UserWelcomeSent extends Schema.TaggedClass<UserWelcomeSent>()("user/welcom
|
|
|
124
124
|
// 2. Define your functions
|
|
125
125
|
const ProcessSignup = InngestFunction.make("process-signup", {
|
|
126
126
|
trigger: { event: UserSignup },
|
|
127
|
-
success: { welcomed: Schema.Boolean },
|
|
127
|
+
success: Schema.Struct({ welcomed: Schema.Boolean }),
|
|
128
128
|
});
|
|
129
129
|
|
|
130
130
|
const DailyDigest = InngestFunction.make("daily-digest", {
|
|
131
131
|
trigger: { cron: "0 9 * * *" },
|
|
132
|
+
success: Schema.Void,
|
|
132
133
|
});
|
|
133
134
|
|
|
134
135
|
// 3. Create function group and implement handlers
|
|
@@ -161,15 +162,13 @@ const Client = InngestClient.layer({
|
|
|
161
162
|
mode: "dev",
|
|
162
163
|
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
163
164
|
|
|
164
|
-
const Server =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}),
|
|
172
|
-
).pipe(Layer.provide(Handlers), Layer.provide(Client), Layer.provide(FetchHttpClient.layer));
|
|
165
|
+
const Server = HttpServer.serve(InngestGroup.toHttpApp(App), HttpMiddleware.logger).pipe(
|
|
166
|
+
HttpServer.withLogAddress,
|
|
167
|
+
Layer.provide(BunHttpServer.layer({ port: 3000 })),
|
|
168
|
+
Layer.provide(Handlers),
|
|
169
|
+
Layer.provide(Client),
|
|
170
|
+
Layer.provide(FetchHttpClient.layer),
|
|
171
|
+
);
|
|
173
172
|
|
|
174
173
|
// Run it
|
|
175
174
|
BunRuntime.runMain(Layer.launch(Server));
|
|
@@ -187,6 +186,78 @@ bunx inngest-cli@latest dev -u http://localhost:3000
|
|
|
187
186
|
|
|
188
187
|
Open http://localhost:8288 to trigger events and watch your functions run.
|
|
189
188
|
|
|
189
|
+
### Using HttpApiBuilder
|
|
190
|
+
|
|
191
|
+
For more complex applications, you can compose Inngest into an existing `HttpApi`:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import * as HttpApi from "@effect/platform/HttpApi";
|
|
195
|
+
import * as HttpApiBuilder from "@effect/platform/HttpApiBuilder";
|
|
196
|
+
import { FetchHttpClient } from "@effect/platform";
|
|
197
|
+
import { BunHttpServer, BunRuntime } from "@effect/platform-bun";
|
|
198
|
+
import { Duration, Effect, Layer, Schema } from "effect";
|
|
199
|
+
import { InngestApiGroup, layerGroup } from "effect-inngest/HttpApi";
|
|
200
|
+
import { InngestClient, InngestFunction, InngestGroup } from "effect-inngest";
|
|
201
|
+
|
|
202
|
+
// 1. Define your events
|
|
203
|
+
class UserSignup extends Schema.TaggedClass<UserSignup>()("user/signup", {
|
|
204
|
+
userId: Schema.String,
|
|
205
|
+
email: Schema.String,
|
|
206
|
+
}) {}
|
|
207
|
+
|
|
208
|
+
class UserWelcomeSent extends Schema.TaggedClass<UserWelcomeSent>()("user/welcome-sent", {
|
|
209
|
+
userId: Schema.String,
|
|
210
|
+
}) {}
|
|
211
|
+
|
|
212
|
+
// 2. Define functions
|
|
213
|
+
const ProcessSignup = InngestFunction.make("process-signup", {
|
|
214
|
+
trigger: { event: UserSignup },
|
|
215
|
+
success: Schema.Struct({ welcomed: Schema.Boolean }),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const DailyDigest = InngestFunction.make("daily-digest", {
|
|
219
|
+
trigger: { cron: "0 9 * * *" },
|
|
220
|
+
success: Schema.Void,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// 3. Create group and handlers
|
|
224
|
+
const App = InngestGroup.make(ProcessSignup, DailyDigest);
|
|
225
|
+
|
|
226
|
+
const Handlers = App.toLayer({
|
|
227
|
+
"process-signup": ({ event, step }) =>
|
|
228
|
+
Effect.gen(function* () {
|
|
229
|
+
yield* Effect.log(`Processing signup for ${event.email}`);
|
|
230
|
+
const user = yield* step.run("create-user", Effect.succeed({ id: event.userId, email: event.email }));
|
|
231
|
+
yield* step.sleep("welcome-delay", Duration.seconds(5));
|
|
232
|
+
yield* step.sendEvent("notify", new UserWelcomeSent({ userId: user.id }));
|
|
233
|
+
return { welcomed: true };
|
|
234
|
+
}),
|
|
235
|
+
"daily-digest": ({ step }) => step.run("send-digest", Effect.log("Sending daily digest...")),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// 4. Create client
|
|
239
|
+
const Client = InngestClient.layer({
|
|
240
|
+
id: "my-app",
|
|
241
|
+
mode: "dev",
|
|
242
|
+
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
243
|
+
|
|
244
|
+
// 5. Create API with Inngest group at /inngest prefix
|
|
245
|
+
const MyApi = HttpApi.make("my-api").add(InngestApiGroup.prefix("/inngest"));
|
|
246
|
+
|
|
247
|
+
// 6. Build API layer
|
|
248
|
+
const ApiLive = HttpApiBuilder.api(MyApi).pipe(
|
|
249
|
+
Layer.provide(layerGroup(MyApi, App)),
|
|
250
|
+
Layer.provide(Handlers),
|
|
251
|
+
Layer.provide(Client),
|
|
252
|
+
Layer.provide(FetchHttpClient.layer),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// 7. Serve
|
|
256
|
+
const Server = HttpApiBuilder.serve().pipe(Layer.provide(ApiLive), Layer.provide(BunHttpServer.layer({ port: 3000 })));
|
|
257
|
+
|
|
258
|
+
BunRuntime.runMain(Layer.launch(Server));
|
|
259
|
+
```
|
|
260
|
+
|
|
190
261
|
---
|
|
191
262
|
|
|
192
263
|
## Getting Started
|
|
@@ -217,12 +288,13 @@ import { InngestFunction } from "effect-inngest";
|
|
|
217
288
|
// Event-triggered function
|
|
218
289
|
const ProcessSignup = InngestFunction.make("process-signup", {
|
|
219
290
|
trigger: [{ event: UserSignup }], // pass multiple triggers as Array.
|
|
220
|
-
success: { welcomeEmailSent: Schema.Boolean },
|
|
291
|
+
success: Schema.Struct({ welcomeEmailSent: Schema.Boolean }),
|
|
221
292
|
});
|
|
222
293
|
|
|
223
294
|
// Cron-triggered function
|
|
224
295
|
const DailyReport = InngestFunction.make("daily-report", {
|
|
225
296
|
trigger: { cron: "0 9 * * *" },
|
|
297
|
+
success: Schema.Void,
|
|
226
298
|
});
|
|
227
299
|
```
|
|
228
300
|
|
|
@@ -420,19 +492,6 @@ yield * new RetryAfterError({ message: "Rate limited", retryAfter: 60000 });
|
|
|
420
492
|
|
|
421
493
|
See the [`examples/`](./examples) directory:
|
|
422
494
|
|
|
423
|
-
| Example | Description |
|
|
424
|
-
| ------------------------------------------- | ---------------------------------------------- |
|
|
425
|
-
| [simple.ts](./examples/simple.ts) | Minimal hello world with parallel steps |
|
|
426
|
-
| [KitchenSink.ts](./examples/KitchenSink.ts) | Complete e-commerce workflow with all features |
|
|
427
|
-
|
|
428
|
-
```bash
|
|
429
|
-
# Run an example
|
|
430
|
-
bun examples/simple.ts
|
|
431
|
-
|
|
432
|
-
# Start Inngest dev server
|
|
433
|
-
bunx inngest-cli@latest dev -u http://localhost:9999
|
|
434
|
-
```
|
|
435
|
-
|
|
436
495
|
---
|
|
437
496
|
|
|
438
497
|
## Current Limitations
|
|
@@ -447,11 +506,6 @@ This library is under active development. The following Inngest features are **n
|
|
|
447
506
|
|
|
448
507
|
---
|
|
449
508
|
|
|
450
|
-
## Requirements
|
|
451
|
-
|
|
452
|
-
- **TypeScript** 5.4+ with `strict: true`
|
|
453
|
-
- **Effect** 3.x
|
|
454
|
-
|
|
455
509
|
## License
|
|
456
510
|
|
|
457
511
|
MIT
|
package/dist/Function.d.ts
CHANGED
|
@@ -313,6 +313,40 @@ interface FunctionRegistration {
|
|
|
313
313
|
start?: string;
|
|
314
314
|
finish?: string;
|
|
315
315
|
};
|
|
316
|
+
readonly rateLimit?: {
|
|
317
|
+
readonly key?: string;
|
|
318
|
+
readonly limit: number;
|
|
319
|
+
readonly period: string;
|
|
320
|
+
};
|
|
321
|
+
readonly throttle?: {
|
|
322
|
+
readonly key?: string;
|
|
323
|
+
readonly limit: number;
|
|
324
|
+
readonly period: string;
|
|
325
|
+
readonly burst?: number;
|
|
326
|
+
};
|
|
327
|
+
readonly debounce?: {
|
|
328
|
+
readonly key?: string;
|
|
329
|
+
readonly period: string;
|
|
330
|
+
readonly timeout?: string;
|
|
331
|
+
};
|
|
332
|
+
readonly concurrency?: ReadonlyArray<{
|
|
333
|
+
readonly key?: string;
|
|
334
|
+
readonly limit: number;
|
|
335
|
+
readonly scope?: string;
|
|
336
|
+
}>;
|
|
337
|
+
readonly priority?: {
|
|
338
|
+
readonly run?: string;
|
|
339
|
+
};
|
|
340
|
+
readonly singleton?: {
|
|
341
|
+
readonly key?: string;
|
|
342
|
+
readonly mode: string;
|
|
343
|
+
};
|
|
344
|
+
readonly batchEvents?: {
|
|
345
|
+
readonly maxSize: number;
|
|
346
|
+
readonly timeout: string;
|
|
347
|
+
readonly key?: string;
|
|
348
|
+
};
|
|
349
|
+
readonly idempotency?: string;
|
|
316
350
|
}
|
|
317
351
|
/**
|
|
318
352
|
* An Inngest function definition.
|
package/dist/Function.js
CHANGED
|
@@ -35,6 +35,38 @@ const Proto = {
|
|
|
35
35
|
start: opts.timeouts.start ? timeStr(opts.timeouts.start) : void 0,
|
|
36
36
|
finish: opts.timeouts.finish ? timeStr(opts.timeouts.finish) : void 0
|
|
37
37
|
} : void 0;
|
|
38
|
+
const rateLimit = opts.rateLimit ? {
|
|
39
|
+
key: opts.rateLimit.key,
|
|
40
|
+
limit: opts.rateLimit.limit,
|
|
41
|
+
period: timeStr(opts.rateLimit.period)
|
|
42
|
+
} : void 0;
|
|
43
|
+
const throttle = opts.throttle ? {
|
|
44
|
+
key: opts.throttle.key,
|
|
45
|
+
limit: opts.throttle.limit,
|
|
46
|
+
period: timeStr(opts.throttle.period),
|
|
47
|
+
burst: opts.throttle.burst
|
|
48
|
+
} : void 0;
|
|
49
|
+
const debounce = opts.debounce ? {
|
|
50
|
+
key: opts.debounce.key,
|
|
51
|
+
period: timeStr(opts.debounce.period),
|
|
52
|
+
timeout: opts.debounce.timeout ? timeStr(opts.debounce.timeout) : void 0
|
|
53
|
+
} : void 0;
|
|
54
|
+
const concurrency = opts.concurrency != null ? typeof opts.concurrency === "number" ? [{ limit: opts.concurrency }] : Array.ensure(opts.concurrency).map((c) => ({
|
|
55
|
+
key: c.key,
|
|
56
|
+
limit: c.limit,
|
|
57
|
+
scope: c.scope
|
|
58
|
+
})) : void 0;
|
|
59
|
+
const priority = opts.priority ? { run: opts.priority.run } : void 0;
|
|
60
|
+
const singleton = opts.singleton ? {
|
|
61
|
+
key: opts.singleton.key,
|
|
62
|
+
mode: opts.singleton.mode
|
|
63
|
+
} : void 0;
|
|
64
|
+
const batchEvents = opts.batchEvents ? {
|
|
65
|
+
maxSize: opts.batchEvents.maxSize,
|
|
66
|
+
timeout: timeStr(opts.batchEvents.timeout),
|
|
67
|
+
key: opts.batchEvents.key
|
|
68
|
+
} : void 0;
|
|
69
|
+
const idempotency = opts.idempotency;
|
|
38
70
|
const fnId = `${config.appId}-${this._tag}`;
|
|
39
71
|
const stepUrl = new URL(config.url);
|
|
40
72
|
stepUrl.searchParams.set("fnId", fnId);
|
|
@@ -53,7 +85,15 @@ const Proto = {
|
|
|
53
85
|
retries: { attempts: opts.retries ?? 3 }
|
|
54
86
|
} },
|
|
55
87
|
cancel: cancel && cancel.length > 0 ? cancel : void 0,
|
|
56
|
-
timeouts
|
|
88
|
+
timeouts,
|
|
89
|
+
rateLimit,
|
|
90
|
+
throttle,
|
|
91
|
+
debounce,
|
|
92
|
+
concurrency,
|
|
93
|
+
priority,
|
|
94
|
+
singleton,
|
|
95
|
+
batchEvents,
|
|
96
|
+
idempotency
|
|
57
97
|
};
|
|
58
98
|
}
|
|
59
99
|
};
|
package/dist/Group.d.ts
CHANGED
|
@@ -143,9 +143,8 @@ declare const toHttpApp: (group: InngestGroup.Any) => HttpApp.Default<never, Inn
|
|
|
143
143
|
*/
|
|
144
144
|
declare const toWebHandler: <R, E>(group: InngestGroup.Any, options: {
|
|
145
145
|
readonly layer: Layer.Layer<InngestClient | HttpClient.HttpClient | R, E, never>;
|
|
146
|
-
readonly memoMap?: Layer.MemoMap;
|
|
147
146
|
}) => {
|
|
148
|
-
readonly handler: (request: Request) => Promise<Response>;
|
|
147
|
+
readonly handler: (request: Request, context?: Context.Context<never>) => Promise<Response>;
|
|
149
148
|
readonly dispose: () => Promise<void>;
|
|
150
149
|
};
|
|
151
150
|
//#endregion
|
package/dist/Group.js
CHANGED
|
@@ -158,7 +158,7 @@ const toHttpApp = (group) => Effect.gen(function* () {
|
|
|
158
158
|
* process.on("SIGTERM", dispose)
|
|
159
159
|
* ```
|
|
160
160
|
*/
|
|
161
|
-
const toWebHandler = (group, options) => HttpApp.toWebHandlerLayer(toHttpApp(group), Layer.mergeAll(options.layer, Layer.scope)
|
|
161
|
+
const toWebHandler = (group, options) => HttpApp.toWebHandlerLayer(toHttpApp(group), Layer.mergeAll(options.layer, Layer.scope));
|
|
162
162
|
|
|
163
163
|
//#endregion
|
|
164
164
|
export { Group_exports, TypeId, make, toHttpApp, toWebHandler };
|
package/dist/HttpApi.d.ts
CHANGED
|
@@ -24,20 +24,20 @@ declare class FunctionNotFoundError extends FunctionNotFoundError_base {}
|
|
|
24
24
|
* @category api
|
|
25
25
|
*/
|
|
26
26
|
declare const InngestApiGroup: HttpApiGroup.HttpApiGroup<"inngest", HttpApiEndpoint.HttpApiEndpoint<"introspect", "GET", never, never, never, never, ({
|
|
27
|
-
readonly mode: "cloud" | "dev";
|
|
28
27
|
readonly function_count: number;
|
|
29
28
|
readonly has_event_key: boolean;
|
|
30
29
|
readonly has_signing_key: boolean;
|
|
31
30
|
readonly has_signing_key_fallback: boolean;
|
|
31
|
+
readonly mode: "cloud" | "dev";
|
|
32
32
|
readonly schema_version: "2024-05-24";
|
|
33
33
|
readonly extra?: {
|
|
34
34
|
readonly [x: string]: unknown;
|
|
35
35
|
} | undefined;
|
|
36
36
|
} & {
|
|
37
|
-
readonly env: string | null;
|
|
38
37
|
readonly authentication_succeeded: true;
|
|
39
38
|
readonly api_origin: string;
|
|
40
39
|
readonly app_id: string;
|
|
40
|
+
readonly env: string | null;
|
|
41
41
|
readonly event_api_origin: string;
|
|
42
42
|
readonly event_key_hash: string | null;
|
|
43
43
|
readonly framework: string;
|
|
@@ -48,11 +48,11 @@ declare const InngestApiGroup: HttpApiGroup.HttpApiGroup<"inngest", HttpApiEndpo
|
|
|
48
48
|
readonly signing_key_fallback_hash: string | null;
|
|
49
49
|
readonly signing_key_hash: string | null;
|
|
50
50
|
}) | ({
|
|
51
|
-
readonly mode: "cloud" | "dev";
|
|
52
51
|
readonly function_count: number;
|
|
53
52
|
readonly has_event_key: boolean;
|
|
54
53
|
readonly has_signing_key: boolean;
|
|
55
54
|
readonly has_signing_key_fallback: boolean;
|
|
55
|
+
readonly mode: "cloud" | "dev";
|
|
56
56
|
readonly schema_version: "2024-05-24";
|
|
57
57
|
readonly extra?: {
|
|
58
58
|
readonly [x: string]: unknown;
|
|
@@ -6,18 +6,14 @@ import * as Schema$1 from "effect/Schema";
|
|
|
6
6
|
* Wire protocol schemas and opcode factories for Inngest communication.
|
|
7
7
|
* @internal
|
|
8
8
|
*/
|
|
9
|
-
const
|
|
10
|
-
if (Predicate.isRecord(value))
|
|
11
|
-
const stripped = Struct.omit(value, "_tag");
|
|
12
|
-
return Object.fromEntries(Object.entries(stripped).map(([k, v]) => [k, stripTags(v)]));
|
|
13
|
-
}
|
|
14
|
-
if (Array.isArray(value)) return value.map(stripTags);
|
|
9
|
+
const stripTopLevelTag = (value) => {
|
|
10
|
+
if (Predicate.isRecord(value)) return Struct.omit(value, "_tag");
|
|
15
11
|
return value;
|
|
16
12
|
};
|
|
17
13
|
const WireUnknown = Schema$1.transform(Schema$1.Unknown, Schema$1.Unknown, {
|
|
18
14
|
strict: true,
|
|
19
15
|
decode: (value) => value,
|
|
20
|
-
encode: (value) =>
|
|
16
|
+
encode: (value) => stripTopLevelTag(value)
|
|
21
17
|
});
|
|
22
18
|
const Opcode = {
|
|
23
19
|
None: "None",
|
package/dist/internal/step.js
CHANGED
|
@@ -8,7 +8,7 @@ import * as Duration from "effect/Duration";
|
|
|
8
8
|
import * as Context from "effect/Context";
|
|
9
9
|
import * as Effect from "effect/Effect";
|
|
10
10
|
import * as Option from "effect/Option";
|
|
11
|
-
import "effect/Schema";
|
|
11
|
+
import * as Schema from "effect/Schema";
|
|
12
12
|
import * as Predicate from "effect/Predicate";
|
|
13
13
|
import * as Arr from "effect/Array";
|
|
14
14
|
import * as HashMap from "effect/HashMap";
|
|
@@ -38,6 +38,11 @@ const errorOtelAttributes = (err) => {
|
|
|
38
38
|
} else attrs[OtelAttributes.ExceptionMessage] = String(err);
|
|
39
39
|
return attrs;
|
|
40
40
|
};
|
|
41
|
+
const encodeTaggedEvent = (event) => {
|
|
42
|
+
const ctor = event.constructor;
|
|
43
|
+
if (Schema.isSchema(ctor)) return Schema.encode(ctor)(event).pipe(Effect.orElseSucceed(() => event));
|
|
44
|
+
return Effect.succeed(event);
|
|
45
|
+
};
|
|
41
46
|
var Step = class extends Context.Tag("effect-inngest/Step")() {};
|
|
42
47
|
const normalizeOpts = (opts) => typeof opts === "string" ? {
|
|
43
48
|
id: opts,
|
|
@@ -89,16 +94,20 @@ const createStepTools = (request, appName, stepIdCounts) => {
|
|
|
89
94
|
timeout: timeStr(options.timeout),
|
|
90
95
|
if: options.if
|
|
91
96
|
}))), Match.exhaustive));
|
|
92
|
-
const invoke = (opts, options) => Effect.flatMap(getInfo(opts), (info) => pipe(getMemo(info.hash), Match.value, Match.tag("MemoData", ({ data }) => Effect.succeed(data)), Match.tag("MemoError", ({ error }) => stepError(info.id, Predicate.hasProperty(error, "message") ? String(error.message) : "Invoke failed", { cause: error })), Match.tag("MemoTimeout", () => stepError(info.id, "Invoke timed out", { noRetry: true })), Match.tag("MemoInput", () => Effect.succeed(void 0)), Match.tag("MemoNone", () =>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
const invoke = (opts, options) => Effect.flatMap(getInfo(opts), (info) => pipe(getMemo(info.hash), Match.value, Match.tag("MemoData", ({ data }) => Effect.succeed(data)), Match.tag("MemoError", ({ error }) => stepError(info.id, Predicate.hasProperty(error, "message") ? String(error.message) : "Invoke failed", { cause: error })), Match.tag("MemoTimeout", () => stepError(info.id, "Invoke timed out", { noRetry: true })), Match.tag("MemoInput", () => Effect.succeed(void 0)), Match.tag("MemoNone", () => {
|
|
98
|
+
const rawData = Predicate.hasProperty(options, "data") ? options.data : void 0;
|
|
99
|
+
const encodeData = rawData ? encodeTaggedEvent(rawData) : Effect.succeed(void 0);
|
|
100
|
+
return Effect.flatMap(encodeData, (encodedData) => Effect.fail(invokeInterrupt({
|
|
101
|
+
info,
|
|
102
|
+
functionId: `${appName}-${options.function._tag}`,
|
|
103
|
+
payload: {
|
|
104
|
+
data: encodedData,
|
|
105
|
+
user: options.user ?? {},
|
|
106
|
+
v: options.v ?? "1"
|
|
107
|
+
},
|
|
108
|
+
timeout: options.timeout ? timeStr(options.timeout) : "365d"
|
|
109
|
+
})));
|
|
110
|
+
}), Match.exhaustive));
|
|
102
111
|
const run = (opts, effect) => Effect.flatMap(getInfo(opts), (info) => pipe(getMemo(info.hash), Match.value, Match.tag("MemoData", ({ data }) => Effect.succeed(data)), Match.tag("MemoError", ({ error }) => stepError(info.id, Predicate.hasProperty(error, "message") ? String(error.message) : "Step failed", { noRetry: true })), Match.tag("MemoTimeout", () => stepError(info.id, "Step timed out", { noRetry: true })), Match.tag("MemoInput", () => stepError(info.id, "Unexpected step result type: input")), Match.tag("MemoNone", () => {
|
|
103
112
|
if (isBlocked(info.hash) || !canExecute(info.hash)) return Effect.fail(plannedInterrupt({ info }));
|
|
104
113
|
return effect.pipe(Effect.withSpan(`inngest.step/run/${info.id}`, { attributes: {
|
|
@@ -132,17 +141,17 @@ const createStepTools = (request, appName, stepIdCounts) => {
|
|
|
132
141
|
events: []
|
|
133
142
|
}))), Match.tag("MemoInput", () => Effect.succeed({ ids: [] })), Match.tag("MemoNone", () => {
|
|
134
143
|
if (isBlocked(info.hash) || !canExecute(info.hash)) return Effect.fail(plannedInterrupt({ info }));
|
|
135
|
-
const
|
|
144
|
+
const events = Arr.ensure(payload);
|
|
145
|
+
return Effect.flatMap(Effect.forEach(events, (e) => Effect.map(encodeTaggedEvent(e), (encoded) => ({
|
|
136
146
|
name: e._tag,
|
|
137
|
-
data:
|
|
138
|
-
}))
|
|
139
|
-
return Effect.flatMap(InngestClient, (client) => client.sendEvent(eventPayloads).pipe(Effect.withSpan(`inngest.step/sendEvent/${info.id}`, { attributes: {
|
|
147
|
+
data: encoded
|
|
148
|
+
}))), (eventPayloads) => Effect.flatMap(InngestClient, (client) => client.sendEvent(eventPayloads).pipe(Effect.withSpan(`inngest.step/sendEvent/${info.id}`, { attributes: {
|
|
140
149
|
[OtelAttributes.StepId]: info.id,
|
|
141
150
|
[OtelAttributes.StepType]: "sendEvent"
|
|
142
151
|
} }), Effect.flatMap((result) => Effect.fail(runInterrupt({
|
|
143
152
|
info,
|
|
144
153
|
data: result
|
|
145
|
-
})))));
|
|
154
|
+
}))))));
|
|
146
155
|
}), Match.exhaustive));
|
|
147
156
|
return {
|
|
148
157
|
run,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-inngest",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Native Effect client library for Inngest - build durable, type-safe workflows",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"background-jobs",
|
|
@@ -23,9 +23,6 @@
|
|
|
23
23
|
"type": "git",
|
|
24
24
|
"url": "https://github.com/erikshestopal/effect-inngest.git"
|
|
25
25
|
},
|
|
26
|
-
"workspaces": [
|
|
27
|
-
"examples"
|
|
28
|
-
],
|
|
29
26
|
"files": [
|
|
30
27
|
"dist/**/*.d.ts",
|
|
31
28
|
"dist/**/*.d.ts.map",
|
|
@@ -106,16 +103,21 @@
|
|
|
106
103
|
"devDependencies": {
|
|
107
104
|
"@changesets/changelog-github": "^0.5.2",
|
|
108
105
|
"@changesets/cli": "^2.29.8",
|
|
106
|
+
"@effect/cluster": "^0.56.1",
|
|
109
107
|
"@effect/language-service": "0.72.0",
|
|
108
|
+
"@effect/platform": "^0.94.2",
|
|
109
|
+
"@effect/platform-bun": "^0.87.0",
|
|
110
110
|
"@types/bun": "1.3.6",
|
|
111
111
|
"@typescript/native-preview": "^7.0.0-dev.20260124.1",
|
|
112
|
+
"effect": "^3.15.0",
|
|
112
113
|
"oxfmt": "0.26.0",
|
|
113
114
|
"oxlint": "1.41.0",
|
|
114
115
|
"oxlint-tsgolint": "0.11.1",
|
|
115
|
-
"tsdown": "0.20.1"
|
|
116
|
+
"tsdown": "0.20.1",
|
|
117
|
+
"typescript": "^5.9.3"
|
|
116
118
|
},
|
|
117
119
|
"peerDependencies": {
|
|
118
|
-
"@effect/platform": ">=0.
|
|
119
|
-
"effect": ">=3.
|
|
120
|
+
"@effect/platform": ">=0.87.0",
|
|
121
|
+
"effect": ">=3.15.0"
|
|
120
122
|
}
|
|
121
123
|
}
|
package/src/Function.ts
CHANGED
|
@@ -331,7 +331,11 @@ interface RegistrationConfig {
|
|
|
331
331
|
interface FunctionRegistration {
|
|
332
332
|
readonly id: string;
|
|
333
333
|
readonly name: string;
|
|
334
|
-
readonly triggers: ReadonlyArray<{
|
|
334
|
+
readonly triggers: ReadonlyArray<{
|
|
335
|
+
event?: string;
|
|
336
|
+
cron?: string;
|
|
337
|
+
expression?: string;
|
|
338
|
+
}>;
|
|
335
339
|
readonly steps: {
|
|
336
340
|
readonly step: {
|
|
337
341
|
readonly id: string;
|
|
@@ -340,8 +344,41 @@ interface FunctionRegistration {
|
|
|
340
344
|
readonly retries?: { readonly attempts: number };
|
|
341
345
|
};
|
|
342
346
|
};
|
|
343
|
-
readonly cancel?: ReadonlyArray<{
|
|
347
|
+
readonly cancel?: ReadonlyArray<{
|
|
348
|
+
event: string;
|
|
349
|
+
if?: string;
|
|
350
|
+
timeout?: string;
|
|
351
|
+
}>;
|
|
344
352
|
readonly timeouts?: { start?: string; finish?: string };
|
|
353
|
+
readonly rateLimit?: {
|
|
354
|
+
readonly key?: string;
|
|
355
|
+
readonly limit: number;
|
|
356
|
+
readonly period: string;
|
|
357
|
+
};
|
|
358
|
+
readonly throttle?: {
|
|
359
|
+
readonly key?: string;
|
|
360
|
+
readonly limit: number;
|
|
361
|
+
readonly period: string;
|
|
362
|
+
readonly burst?: number;
|
|
363
|
+
};
|
|
364
|
+
readonly debounce?: {
|
|
365
|
+
readonly key?: string;
|
|
366
|
+
readonly period: string;
|
|
367
|
+
readonly timeout?: string;
|
|
368
|
+
};
|
|
369
|
+
readonly concurrency?: ReadonlyArray<{
|
|
370
|
+
readonly key?: string;
|
|
371
|
+
readonly limit: number;
|
|
372
|
+
readonly scope?: string;
|
|
373
|
+
}>;
|
|
374
|
+
readonly priority?: { readonly run?: string };
|
|
375
|
+
readonly singleton?: { readonly key?: string; readonly mode: string };
|
|
376
|
+
readonly batchEvents?: {
|
|
377
|
+
readonly maxSize: number;
|
|
378
|
+
readonly timeout: string;
|
|
379
|
+
readonly key?: string;
|
|
380
|
+
};
|
|
381
|
+
readonly idempotency?: string;
|
|
345
382
|
}
|
|
346
383
|
|
|
347
384
|
/**
|
|
@@ -392,7 +429,11 @@ const Proto = {
|
|
|
392
429
|
[TypeId]: TypeId,
|
|
393
430
|
|
|
394
431
|
toRegistration(this: InngestFunction.Any, config: RegistrationConfig): FunctionRegistration {
|
|
395
|
-
const triggers: Array<{
|
|
432
|
+
const triggers: Array<{
|
|
433
|
+
event?: string;
|
|
434
|
+
cron?: string;
|
|
435
|
+
expression?: string;
|
|
436
|
+
}> = [];
|
|
396
437
|
|
|
397
438
|
for (const t of this.triggers) {
|
|
398
439
|
if (isEventTrigger(t)) {
|
|
@@ -416,6 +457,54 @@ const Proto = {
|
|
|
416
457
|
}
|
|
417
458
|
: undefined;
|
|
418
459
|
|
|
460
|
+
const rateLimit = opts.rateLimit
|
|
461
|
+
? {
|
|
462
|
+
key: opts.rateLimit.key,
|
|
463
|
+
limit: opts.rateLimit.limit,
|
|
464
|
+
period: timeStr(opts.rateLimit.period),
|
|
465
|
+
}
|
|
466
|
+
: undefined;
|
|
467
|
+
|
|
468
|
+
const throttle = opts.throttle
|
|
469
|
+
? {
|
|
470
|
+
key: opts.throttle.key,
|
|
471
|
+
limit: opts.throttle.limit,
|
|
472
|
+
period: timeStr(opts.throttle.period),
|
|
473
|
+
burst: opts.throttle.burst,
|
|
474
|
+
}
|
|
475
|
+
: undefined;
|
|
476
|
+
|
|
477
|
+
const debounce = opts.debounce
|
|
478
|
+
? {
|
|
479
|
+
key: opts.debounce.key,
|
|
480
|
+
period: timeStr(opts.debounce.period),
|
|
481
|
+
timeout: opts.debounce.timeout ? timeStr(opts.debounce.timeout) : undefined,
|
|
482
|
+
}
|
|
483
|
+
: undefined;
|
|
484
|
+
|
|
485
|
+
const concurrency =
|
|
486
|
+
opts.concurrency != null
|
|
487
|
+
? typeof opts.concurrency === "number"
|
|
488
|
+
? [{ limit: opts.concurrency }]
|
|
489
|
+
: Arr.ensure(opts.concurrency).map((c) => ({
|
|
490
|
+
key: c.key,
|
|
491
|
+
limit: c.limit,
|
|
492
|
+
scope: c.scope,
|
|
493
|
+
}))
|
|
494
|
+
: undefined;
|
|
495
|
+
|
|
496
|
+
const priority = opts.priority ? { run: opts.priority.run } : undefined;
|
|
497
|
+
const singleton = opts.singleton ? { key: opts.singleton.key, mode: opts.singleton.mode } : undefined;
|
|
498
|
+
const batchEvents = opts.batchEvents
|
|
499
|
+
? {
|
|
500
|
+
maxSize: opts.batchEvents.maxSize,
|
|
501
|
+
timeout: timeStr(opts.batchEvents.timeout),
|
|
502
|
+
key: opts.batchEvents.key,
|
|
503
|
+
}
|
|
504
|
+
: undefined;
|
|
505
|
+
|
|
506
|
+
const idempotency = opts.idempotency;
|
|
507
|
+
|
|
419
508
|
const fnId = `${config.appId}-${this._tag}`;
|
|
420
509
|
const stepUrl = new URL(config.url);
|
|
421
510
|
stepUrl.searchParams.set("fnId", fnId);
|
|
@@ -435,6 +524,14 @@ const Proto = {
|
|
|
435
524
|
},
|
|
436
525
|
cancel: cancel && cancel.length > 0 ? cancel : undefined,
|
|
437
526
|
timeouts,
|
|
527
|
+
rateLimit,
|
|
528
|
+
throttle,
|
|
529
|
+
debounce,
|
|
530
|
+
concurrency,
|
|
531
|
+
priority,
|
|
532
|
+
singleton,
|
|
533
|
+
batchEvents,
|
|
534
|
+
idempotency,
|
|
438
535
|
};
|
|
439
536
|
},
|
|
440
537
|
};
|
package/src/Group.ts
CHANGED
|
@@ -303,12 +303,8 @@ export const toWebHandler = <R, E>(
|
|
|
303
303
|
group: InngestGroup.Any,
|
|
304
304
|
options: {
|
|
305
305
|
readonly layer: Layer.Layer<InngestClient | HttpClient.HttpClient | R, E, never>;
|
|
306
|
-
readonly memoMap?: Layer.MemoMap;
|
|
307
306
|
},
|
|
308
307
|
): {
|
|
309
|
-
readonly handler: (request: Request) => Promise<Response>;
|
|
308
|
+
readonly handler: (request: Request, context?: Context.Context<never>) => Promise<Response>;
|
|
310
309
|
readonly dispose: () => Promise<void>;
|
|
311
|
-
} =>
|
|
312
|
-
HttpApp.toWebHandlerLayer(toHttpApp(group), Layer.mergeAll(options.layer, Layer.scope), {
|
|
313
|
-
memoMap: options.memoMap,
|
|
314
|
-
});
|
|
310
|
+
} => HttpApp.toWebHandlerLayer(toHttpApp(group), Layer.mergeAll(options.layer, Layer.scope));
|
package/src/internal/protocol.ts
CHANGED
|
@@ -5,13 +5,9 @@
|
|
|
5
5
|
import { Predicate, Struct } from "effect";
|
|
6
6
|
import * as Schema from "effect/Schema";
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const stripTopLevelTag = (value: unknown): unknown => {
|
|
9
9
|
if (Predicate.isRecord(value)) {
|
|
10
|
-
|
|
11
|
-
return Object.fromEntries(Object.entries(stripped).map(([k, v]) => [k, stripTags(v)]));
|
|
12
|
-
}
|
|
13
|
-
if (Array.isArray(value)) {
|
|
14
|
-
return value.map(stripTags);
|
|
10
|
+
return Struct.omit(value as Record<string, unknown>, "_tag");
|
|
15
11
|
}
|
|
16
12
|
return value;
|
|
17
13
|
};
|
|
@@ -19,7 +15,7 @@ const stripTags = (value: unknown): unknown => {
|
|
|
19
15
|
const WireUnknown = Schema.transform(Schema.Unknown, Schema.Unknown, {
|
|
20
16
|
strict: true,
|
|
21
17
|
decode: (value) => value,
|
|
22
|
-
encode: (value) =>
|
|
18
|
+
encode: (value) => stripTopLevelTag(value),
|
|
23
19
|
});
|
|
24
20
|
|
|
25
21
|
export const Opcode = {
|
package/src/internal/step.ts
CHANGED
|
@@ -86,6 +86,16 @@ type InvokeOptions<F extends InngestFunction.Any> = [InngestFunction.EventType<F
|
|
|
86
86
|
type EventSchema = Schema.Schema.Any & { readonly _tag: string };
|
|
87
87
|
type TaggedEvent = { readonly _tag: string };
|
|
88
88
|
|
|
89
|
+
const encodeTaggedEvent = (event: TaggedEvent): Effect.Effect<unknown, never> => {
|
|
90
|
+
const ctor = event.constructor;
|
|
91
|
+
if (Schema.isSchema(ctor)) {
|
|
92
|
+
return Schema.encode(ctor as unknown as Schema.Schema.AnyNoContext)(event).pipe(
|
|
93
|
+
Effect.orElseSucceed(() => event),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return Effect.succeed(event);
|
|
97
|
+
};
|
|
98
|
+
|
|
89
99
|
interface StepTools {
|
|
90
100
|
readonly run: <A, Err, R>(
|
|
91
101
|
id: StepOptionsOrId,
|
|
@@ -226,20 +236,24 @@ export const createStepTools = (
|
|
|
226
236
|
),
|
|
227
237
|
Match.tag("MemoTimeout", () => stepError(info.id, "Invoke timed out", { noRetry: true })),
|
|
228
238
|
Match.tag("MemoInput", () => Effect.succeed(undefined as InngestFunction.Success<F>)),
|
|
229
|
-
Match.tag("MemoNone", () =>
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
239
|
+
Match.tag("MemoNone", () => {
|
|
240
|
+
const rawData = Predicate.hasProperty(options, "data") ? (options.data as TaggedEvent) : undefined;
|
|
241
|
+
const encodeData = rawData ? encodeTaggedEvent(rawData) : Effect.succeed(undefined);
|
|
242
|
+
return Effect.flatMap(encodeData, (encodedData) =>
|
|
243
|
+
Effect.fail(
|
|
244
|
+
invokeInterrupt({
|
|
245
|
+
info,
|
|
246
|
+
functionId: `${appName}-${options.function._tag}`,
|
|
247
|
+
payload: {
|
|
248
|
+
data: encodedData,
|
|
249
|
+
user: options.user ?? {},
|
|
250
|
+
v: options.v ?? "1",
|
|
251
|
+
},
|
|
252
|
+
timeout: options.timeout ? timeStr(options.timeout) : "365d",
|
|
253
|
+
}),
|
|
254
|
+
),
|
|
255
|
+
);
|
|
256
|
+
}),
|
|
243
257
|
Match.exhaustive,
|
|
244
258
|
),
|
|
245
259
|
);
|
|
@@ -311,15 +325,20 @@ export const createStepTools = (
|
|
|
311
325
|
return Effect.fail(plannedInterrupt({ info }));
|
|
312
326
|
}
|
|
313
327
|
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
Effect.withSpan(`inngest.step/sendEvent/${info.id}`, {
|
|
319
|
-
attributes: { [OtelAttributes.StepId]: info.id, [OtelAttributes.StepType]: "sendEvent" },
|
|
320
|
-
}),
|
|
321
|
-
Effect.flatMap((result) => Effect.fail(runInterrupt({ info, data: result }))),
|
|
328
|
+
const events = Arr.ensure(payload);
|
|
329
|
+
return Effect.flatMap(
|
|
330
|
+
Effect.forEach(events, (e) =>
|
|
331
|
+
Effect.map(encodeTaggedEvent(e), (encoded) => ({ name: e._tag, data: encoded })),
|
|
322
332
|
),
|
|
333
|
+
(eventPayloads) =>
|
|
334
|
+
Effect.flatMap(InngestClient, (client) =>
|
|
335
|
+
client.sendEvent(eventPayloads).pipe(
|
|
336
|
+
Effect.withSpan(`inngest.step/sendEvent/${info.id}`, {
|
|
337
|
+
attributes: { [OtelAttributes.StepId]: info.id, [OtelAttributes.StepType]: "sendEvent" },
|
|
338
|
+
}),
|
|
339
|
+
Effect.flatMap((result) => Effect.fail(runInterrupt({ info, data: result }))),
|
|
340
|
+
),
|
|
341
|
+
),
|
|
323
342
|
);
|
|
324
343
|
}),
|
|
325
344
|
Match.exhaustive,
|