@beignet/provider-inngest 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +290 -0
- package/dist/index.d.ts +124 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +225 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
- package/src/index.ts +377 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# @beignet/provider-inngest
|
|
2
|
+
|
|
3
|
+
Inngest-backed `JobDispatcherPort` provider for Beignet applications.
|
|
4
|
+
|
|
5
|
+
The provider installs the app-facing `ctx.ports.jobs` dispatcher for durable
|
|
6
|
+
background jobs using [Inngest](https://www.inngest.com/) and exposes
|
|
7
|
+
`ctx.ports.inngest` only as an escape hatch for raw Inngest access.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun add @beignet/provider-inngest @beignet/core inngest
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { createNextServer } from "@beignet/next";
|
|
19
|
+
import { definePorts } from "@beignet/core/ports";
|
|
20
|
+
import { inngestProvider } from "@beignet/provider-inngest";
|
|
21
|
+
import { routes } from "@/server/routes";
|
|
22
|
+
|
|
23
|
+
// Set environment variables:
|
|
24
|
+
// INNGEST_APP_NAME=my-app (optional, defaults to "beignet-app")
|
|
25
|
+
// INNGEST_EVENT_KEY=your-event-key (optional, required for Inngest Cloud)
|
|
26
|
+
|
|
27
|
+
const appPorts = definePorts({});
|
|
28
|
+
|
|
29
|
+
export const server = await createNextServer({
|
|
30
|
+
ports: appPorts,
|
|
31
|
+
providers: [inngestProvider],
|
|
32
|
+
createContext: ({ ports }) => ({
|
|
33
|
+
ports,
|
|
34
|
+
}),
|
|
35
|
+
routes,
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
Once the provider is registered, your ports include:
|
|
42
|
+
|
|
43
|
+
- `jobs`: the canonical `JobDispatcherPort` for app code.
|
|
44
|
+
- `inngest`: the raw Inngest escape hatch for advanced usage.
|
|
45
|
+
|
|
46
|
+
Define jobs with `@beignet/core/jobs`, then dispatch them through
|
|
47
|
+
`ctx.ports.jobs`:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { createJobHandlers } from "@beignet/core/jobs";
|
|
51
|
+
import { z } from "zod";
|
|
52
|
+
|
|
53
|
+
const jobs = createJobHandlers<AppCtx>();
|
|
54
|
+
|
|
55
|
+
export const SendInviteEmailJob = jobs.defineJob("mail.invite.send", {
|
|
56
|
+
payload: z.object({
|
|
57
|
+
inviteId: z.string(),
|
|
58
|
+
inviteeEmail: z.string().email(),
|
|
59
|
+
}),
|
|
60
|
+
retry: {
|
|
61
|
+
attempts: 3,
|
|
62
|
+
},
|
|
63
|
+
async handle({ payload, ctx }) {
|
|
64
|
+
await ctx.ports.mailer.send({
|
|
65
|
+
to: payload.inviteeEmail,
|
|
66
|
+
subject: "You were invited",
|
|
67
|
+
text: `Invite id: ${payload.inviteId}`,
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
async function inviteUser(ctx: AppCtx, input: InviteUserInput) {
|
|
73
|
+
const invite = await ctx.ports.db.invites.create({
|
|
74
|
+
inviterId: ctx.actor.id,
|
|
75
|
+
inviteeEmail: input.email,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await ctx.ports.jobs.dispatch(SendInviteEmailJob, {
|
|
79
|
+
inviteId: invite.id,
|
|
80
|
+
inviteeEmail: input.email,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return invite;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Configuration
|
|
88
|
+
|
|
89
|
+
The Inngest provider reads configuration from environment variables with the `INNGEST_` prefix:
|
|
90
|
+
|
|
91
|
+
| Variable | Required | Description | Default |
|
|
92
|
+
|----------|----------|-------------|---------|
|
|
93
|
+
| `INNGEST_APP_NAME` | No | Friendly application name shown in Inngest | `"beignet-app"` |
|
|
94
|
+
| `INNGEST_EVENT_KEY` | No | Event key / signing key for Inngest Cloud | - |
|
|
95
|
+
|
|
96
|
+
**Note:** `INNGEST_EVENT_KEY` is required when using Inngest Cloud for production deployments.
|
|
97
|
+
|
|
98
|
+
## Ports
|
|
99
|
+
|
|
100
|
+
### `jobs: JobDispatcherPort`
|
|
101
|
+
|
|
102
|
+
The `jobs` port is the recommended application API. It validates and parses the
|
|
103
|
+
job payload with the job definition before sending an Inngest event using the
|
|
104
|
+
job name.
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
await ctx.ports.jobs.dispatch(SendInviteEmailJob, {
|
|
108
|
+
inviteId: invite.id,
|
|
109
|
+
inviteeEmail: input.email,
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### `inngest: InngestPort`
|
|
114
|
+
|
|
115
|
+
The `inngest` port is an escape hatch for direct Inngest usage.
|
|
116
|
+
|
|
117
|
+
#### `send<TData>(args: { name: string; data: TData }): Promise<void>`
|
|
118
|
+
|
|
119
|
+
Send a raw event to Inngest. Prefer `ctx.ports.jobs.dispatch(...)` for
|
|
120
|
+
first-class Beignet jobs.
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
await ctx.ports.inngest.send({
|
|
124
|
+
name: "user.invited",
|
|
125
|
+
data: {
|
|
126
|
+
inviterId: ctx.actor.id,
|
|
127
|
+
inviteeEmail: input.email,
|
|
128
|
+
inviteId: createdInvite.id,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### `client: Inngest`
|
|
134
|
+
|
|
135
|
+
Access the underlying Inngest client for advanced operations.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// Define Inngest functions using the client directly
|
|
139
|
+
const myFunction = ctx.ports.inngest.client.createFunction(
|
|
140
|
+
{ id: "my-function" },
|
|
141
|
+
{ event: "user.invited" },
|
|
142
|
+
async ({ event, step }) => {
|
|
143
|
+
// Your function logic
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Devtools
|
|
149
|
+
|
|
150
|
+
When `@beignet/devtools` is registered before this provider, calls to
|
|
151
|
+
`ctx.ports.jobs.dispatch(...)` and `ctx.ports.inngest.send(...)` are recorded
|
|
152
|
+
automatically under the `jobs` watcher. Successful enqueues are recorded as
|
|
153
|
+
`scheduled`; failed enqueues are recorded as `failed` with schedule-phase error
|
|
154
|
+
details.
|
|
155
|
+
|
|
156
|
+
Pass an instrumentation target to `createInngestJobFunction(...)` to record
|
|
157
|
+
worker execution as `started`, `completed`, and `failed` events:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const sendInviteEmail = createInngestJobFunction({
|
|
161
|
+
client: inngest,
|
|
162
|
+
job: SendInviteEmailJob,
|
|
163
|
+
ctx: () => createBackgroundContext(),
|
|
164
|
+
instrumentation: appPorts.devtools,
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## TypeScript support
|
|
169
|
+
|
|
170
|
+
To get proper type inference, include both provider-contributed ports in your
|
|
171
|
+
app ports type:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import { definePorts } from "@beignet/core/ports";
|
|
175
|
+
import type { JobDispatcherPort } from "@beignet/core/ports";
|
|
176
|
+
import type { InngestPort } from "@beignet/provider-inngest";
|
|
177
|
+
|
|
178
|
+
// Your base ports, if any
|
|
179
|
+
const basePorts = definePorts({});
|
|
180
|
+
|
|
181
|
+
type AppPorts = typeof basePorts & {
|
|
182
|
+
jobs: JobDispatcherPort;
|
|
183
|
+
inngest: InngestPort;
|
|
184
|
+
};
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Wiring domain events → Inngest jobs
|
|
188
|
+
|
|
189
|
+
This provider does NOT automatically subscribe to domain events. That is
|
|
190
|
+
intentional: events are facts that happened, while jobs are explicit work to do.
|
|
191
|
+
|
|
192
|
+
To wire a domain event to a durable job, register a listener in your application
|
|
193
|
+
and dispatch the job from the listener:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// features/users/listeners.ts
|
|
197
|
+
import { createEventHandlers } from "@beignet/core/events";
|
|
198
|
+
import { UserInvited } from "@/features/users/domain/events";
|
|
199
|
+
import { SendInviteEmailJob } from "@/features/users/jobs";
|
|
200
|
+
import type { AppCtx } from "@/app-context";
|
|
201
|
+
|
|
202
|
+
const events = createEventHandlers<AppCtx>();
|
|
203
|
+
|
|
204
|
+
export const sendInviteEmail = events.defineListener(UserInvited, {
|
|
205
|
+
name: "mail.send-invite-email",
|
|
206
|
+
async handle({ payload, ctx }) {
|
|
207
|
+
await ctx.ports.jobs.dispatch(SendInviteEmailJob, {
|
|
208
|
+
inviteId: payload.inviteId,
|
|
209
|
+
inviteeEmail: payload.inviteeEmail,
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Register that listener against your event bus during infrastructure startup.
|
|
216
|
+
In tests, use `createInlineJobDispatcher(...)` from `@beignet/core/jobs`; in
|
|
217
|
+
production, install `inngestProvider`.
|
|
218
|
+
|
|
219
|
+
## Inngest functions
|
|
220
|
+
|
|
221
|
+
Use `createInngestJobFunction(...)` to turn a first-class Beignet job into
|
|
222
|
+
an Inngest function. The helper subscribes to `job.name`, validates incoming
|
|
223
|
+
event data with `parseJobPayload`, and then calls `job.handle(...)`.
|
|
224
|
+
|
|
225
|
+
If the job defines `retry.attempts`, the helper maps it to Inngest's
|
|
226
|
+
function-level `retries` option. Inngest supports integer values from `0` to
|
|
227
|
+
`20`; values outside that range throw during function creation.
|
|
228
|
+
|
|
229
|
+
Define functions separately from your Beignet server, usually in a
|
|
230
|
+
serverless function or route handler:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
// app/api/inngest/route.ts (Next.js App Router example)
|
|
234
|
+
import { createInngestJobFunction } from "@beignet/provider-inngest";
|
|
235
|
+
import { serve } from "inngest/next";
|
|
236
|
+
import { inngest } from "@/infra/inngest";
|
|
237
|
+
import { SendInviteEmailJob } from "@/features/users/jobs";
|
|
238
|
+
import { createBackgroundContext } from "@/infra/background-context";
|
|
239
|
+
|
|
240
|
+
const sendInviteEmail = createInngestJobFunction({
|
|
241
|
+
client: inngest,
|
|
242
|
+
job: SendInviteEmailJob,
|
|
243
|
+
ctx: () => createBackgroundContext(),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
export const { GET, POST, PUT } = serve({
|
|
247
|
+
client: inngest,
|
|
248
|
+
functions: [sendInviteEmail],
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`createBackgroundContext()` is application-owned. Put it near your
|
|
253
|
+
infrastructure wiring and return the ports, logger, auth assumptions, and
|
|
254
|
+
devtools context your background workers need.
|
|
255
|
+
|
|
256
|
+
## Lifecycle
|
|
257
|
+
|
|
258
|
+
The Inngest provider:
|
|
259
|
+
|
|
260
|
+
1. **During `setup`**:
|
|
261
|
+
- Creates Inngest client
|
|
262
|
+
- Returns the `jobs` port
|
|
263
|
+
- Returns the `inngest` escape hatch port
|
|
264
|
+
2. **No cleanup needed**: Inngest client doesn't require explicit shutdown
|
|
265
|
+
|
|
266
|
+
## Error handling
|
|
267
|
+
|
|
268
|
+
The provider will throw errors in these cases:
|
|
269
|
+
|
|
270
|
+
- Missing required configuration (though all fields have defaults or are optional)
|
|
271
|
+
- Invalid configuration values
|
|
272
|
+
|
|
273
|
+
Make sure to handle these during application startup.
|
|
274
|
+
|
|
275
|
+
## Local development
|
|
276
|
+
|
|
277
|
+
For local development, you can run the Inngest Dev Server:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
npx inngest-cli@latest dev
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
This provides a local UI at `http://localhost:8288` where you can:
|
|
284
|
+
- View events
|
|
285
|
+
- Trigger functions
|
|
286
|
+
- Debug execution
|
|
287
|
+
|
|
288
|
+
## License
|
|
289
|
+
|
|
290
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beignet/provider-inngest
|
|
3
|
+
*
|
|
4
|
+
* Inngest provider that extends ports with background job and event capabilities using Inngest.
|
|
5
|
+
*
|
|
6
|
+
* Configuration:
|
|
7
|
+
* - INNGEST_APP_NAME: Friendly application name shown in Inngest (optional, default: "beignet-app")
|
|
8
|
+
* - INNGEST_EVENT_KEY: Optional event key / signing key for Inngest cloud
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { createNextServer } from "@beignet/next";
|
|
13
|
+
* import { inngestProvider } from "@beignet/provider-inngest";
|
|
14
|
+
*
|
|
15
|
+
* const server = await createNextServer({
|
|
16
|
+
* ports: basePorts,
|
|
17
|
+
* providers: [inngestProvider],
|
|
18
|
+
* // ...
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // In your use cases:
|
|
22
|
+
* await ctx.ports.jobs.dispatch(SendInviteEmailJob, {
|
|
23
|
+
* inviteId: createdInvite.id,
|
|
24
|
+
* inviteeEmail: input.email,
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
import type { JobDef, MaybePromise, StandardSchema } from "@beignet/core/jobs";
|
|
29
|
+
import type { JobDispatcherPort } from "@beignet/core/ports";
|
|
30
|
+
import { type ProviderInstrumentationTarget } from "@beignet/core/providers";
|
|
31
|
+
import { Inngest, type InngestFunction } from "inngest";
|
|
32
|
+
import { z } from "zod";
|
|
33
|
+
/**
|
|
34
|
+
* Configuration schema for the Inngest provider.
|
|
35
|
+
* Validates environment variables with INNGEST_ prefix.
|
|
36
|
+
*/
|
|
37
|
+
declare const InngestConfigSchema: z.ZodObject<{
|
|
38
|
+
APP_NAME: z.ZodDefault<z.ZodString>;
|
|
39
|
+
EVENT_KEY: z.ZodOptional<z.ZodString>;
|
|
40
|
+
}, z.core.$strip>;
|
|
41
|
+
/**
|
|
42
|
+
* Inferred configuration type for Inngest provider.
|
|
43
|
+
*/
|
|
44
|
+
export type InngestConfig = z.infer<typeof InngestConfigSchema>;
|
|
45
|
+
/**
|
|
46
|
+
* Port interface for Inngest integration.
|
|
47
|
+
* Provides a stable abstraction over Inngest's client API.
|
|
48
|
+
*/
|
|
49
|
+
export interface InngestPort {
|
|
50
|
+
/**
|
|
51
|
+
* The raw Inngest client instance.
|
|
52
|
+
* Useful for advanced operations like defining functions.
|
|
53
|
+
*/
|
|
54
|
+
client: Inngest;
|
|
55
|
+
/**
|
|
56
|
+
* Send an Inngest event.
|
|
57
|
+
* Equivalent to inngest.send({ name, data }).
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* await ctx.ports.inngest.send({
|
|
62
|
+
* name: "user.invited",
|
|
63
|
+
* data: { inviteId, email }
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
send<TData>(args: {
|
|
68
|
+
name: string;
|
|
69
|
+
data: TData;
|
|
70
|
+
}): Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
export interface InngestJobDispatcherOptions {
|
|
73
|
+
instrumentation?: ProviderInstrumentationTarget;
|
|
74
|
+
}
|
|
75
|
+
export type InngestFunctionRetryAttempts = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20;
|
|
76
|
+
export type InngestJobFunctionContext<Ctx> = Ctx | ((args: InngestJobFunctionContextArgs) => MaybePromise<Ctx>);
|
|
77
|
+
export interface InngestJobFunctionContextArgs {
|
|
78
|
+
event: {
|
|
79
|
+
name: string;
|
|
80
|
+
data: unknown;
|
|
81
|
+
};
|
|
82
|
+
step: unknown;
|
|
83
|
+
}
|
|
84
|
+
export type InngestJobFunctionInstrumentation = ProviderInstrumentationTarget;
|
|
85
|
+
export interface CreateInngestJobFunctionOptions<J extends JobDef<string, StandardSchema, Ctx>, Ctx> {
|
|
86
|
+
client: Inngest;
|
|
87
|
+
job: J;
|
|
88
|
+
id?: string;
|
|
89
|
+
name?: string;
|
|
90
|
+
description?: string;
|
|
91
|
+
ctx?: InngestJobFunctionContext<Ctx>;
|
|
92
|
+
instrumentation?: InngestJobFunctionInstrumentation;
|
|
93
|
+
}
|
|
94
|
+
export declare function createInngestJobDispatcher(client: Inngest, options?: InngestJobDispatcherOptions): JobDispatcherPort;
|
|
95
|
+
export declare function createInngestJobFunction<Ctx, J extends JobDef<string, StandardSchema, Ctx>>(options: CreateInngestJobFunctionOptions<J, Ctx>): InngestFunction.Any;
|
|
96
|
+
/**
|
|
97
|
+
* Inngest provider that extends ports with background job and event capabilities.
|
|
98
|
+
*
|
|
99
|
+
* This provider creates an Inngest client and exposes it through the ports system.
|
|
100
|
+
* It contributes a canonical `jobs` port for app code and keeps the raw
|
|
101
|
+
* Inngest client available as an escape hatch through `ports.inngest.client`.
|
|
102
|
+
*
|
|
103
|
+
* Configuration via environment variables:
|
|
104
|
+
* - INNGEST_APP_NAME: Application name (optional, default: "beignet-app")
|
|
105
|
+
* - INNGEST_EVENT_KEY: Event key for Inngest cloud (optional)
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* const server = await createNextServer({
|
|
110
|
+
* ports: basePorts,
|
|
111
|
+
* providers: [inngestProvider],
|
|
112
|
+
* // ...
|
|
113
|
+
* });
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export declare const inngestProvider: import("@beignet/core/providers").ServiceProvider<unknown, z.ZodObject<{
|
|
117
|
+
APP_NAME: z.ZodDefault<z.ZodString>;
|
|
118
|
+
EVENT_KEY: z.ZodOptional<z.ZodString>;
|
|
119
|
+
}, z.core.$strip>, {
|
|
120
|
+
inngest: InngestPort;
|
|
121
|
+
jobs: JobDispatcherPort;
|
|
122
|
+
}>;
|
|
123
|
+
export {};
|
|
124
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAEV,MAAM,EACN,YAAY,EACZ,cAAc,EACf,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAGL,KAAK,6BAA6B,EACnC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;GAGG;AACH,QAAA,MAAM,mBAAmB;;;iBAYvB,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,KAAK,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjE;AAED,MAAM,WAAW,2BAA2B;IAC1C,eAAe,CAAC,EAAE,6BAA6B,CAAC;CACjD;AAED,MAAM,MAAM,4BAA4B,GACpC,CAAC,GACD,CAAC,GACD,CAAC,GACD,CAAC,GACD,CAAC,GACD,CAAC,GACD,CAAC,GACD,CAAC,GACD,CAAC,GACD,CAAC,GACD,EAAE,GACF,EAAE,GACF,EAAE,GACF,EAAE,GACF,EAAE,GACF,EAAE,GACF,EAAE,GACF,EAAE,GACF,EAAE,GACF,EAAE,GACF,EAAE,CAAC;AAEP,MAAM,MAAM,yBAAyB,CAAC,GAAG,IACrC,GAAG,GACH,CAAC,CAAC,IAAI,EAAE,6BAA6B,KAAK,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC;AAEjE,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,OAAO,CAAC;KACf,CAAC;IACF,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,MAAM,iCAAiC,GAAG,6BAA6B,CAAC;AAE9E,MAAM,WAAW,+BAA+B,CAC9C,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,EAC7C,GAAG;IAEH,MAAM,EAAE,OAAO,CAAC;IAChB,GAAG,EAAE,CAAC,CAAC;IACP,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,yBAAyB,CAAC,GAAG,CAAC,CAAC;IACrC,eAAe,CAAC,EAAE,iCAAiC,CAAC;CACrD;AAgDD,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,OAAO,EACf,OAAO,GAAE,2BAAgC,GACxC,iBAAiB,CAoCnB;AAED,wBAAgB,wBAAwB,CACtC,GAAG,EACH,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,EAC7C,OAAO,EAAE,+BAA+B,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,eAAe,CAAC,GAAG,CAyDvE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe;;;;;;EA4D1B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beignet/provider-inngest
|
|
3
|
+
*
|
|
4
|
+
* Inngest provider that extends ports with background job and event capabilities using Inngest.
|
|
5
|
+
*
|
|
6
|
+
* Configuration:
|
|
7
|
+
* - INNGEST_APP_NAME: Friendly application name shown in Inngest (optional, default: "beignet-app")
|
|
8
|
+
* - INNGEST_EVENT_KEY: Optional event key / signing key for Inngest cloud
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { createNextServer } from "@beignet/next";
|
|
13
|
+
* import { inngestProvider } from "@beignet/provider-inngest";
|
|
14
|
+
*
|
|
15
|
+
* const server = await createNextServer({
|
|
16
|
+
* ports: basePorts,
|
|
17
|
+
* providers: [inngestProvider],
|
|
18
|
+
* // ...
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // In your use cases:
|
|
22
|
+
* await ctx.ports.jobs.dispatch(SendInviteEmailJob, {
|
|
23
|
+
* inviteId: createdInvite.id,
|
|
24
|
+
* inviteeEmail: input.email,
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
import { parseJobPayload } from "@beignet/core/jobs";
|
|
29
|
+
import { createProvider, createProviderInstrumentation, } from "@beignet/core/providers";
|
|
30
|
+
import { Inngest } from "inngest";
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
/**
|
|
33
|
+
* Configuration schema for the Inngest provider.
|
|
34
|
+
* Validates environment variables with INNGEST_ prefix.
|
|
35
|
+
*/
|
|
36
|
+
const InngestConfigSchema = z.object({
|
|
37
|
+
/**
|
|
38
|
+
* Application ID used as the client's `id` property in Inngest v3.
|
|
39
|
+
* This value uniquely identifies your application in Inngest and may be shown in the dashboard.
|
|
40
|
+
*/
|
|
41
|
+
APP_NAME: z.string().default("beignet-app"),
|
|
42
|
+
/**
|
|
43
|
+
* Optional event key / signing key for Inngest cloud.
|
|
44
|
+
* Required when using Inngest Cloud for production deployments.
|
|
45
|
+
*/
|
|
46
|
+
EVENT_KEY: z.string().optional(),
|
|
47
|
+
});
|
|
48
|
+
async function resolveJobContext(ctx, args) {
|
|
49
|
+
if (typeof ctx === "function") {
|
|
50
|
+
return ctx(args);
|
|
51
|
+
}
|
|
52
|
+
return ctx;
|
|
53
|
+
}
|
|
54
|
+
function errorDetails(phase, error) {
|
|
55
|
+
if (error instanceof Error) {
|
|
56
|
+
return {
|
|
57
|
+
phase,
|
|
58
|
+
error: {
|
|
59
|
+
name: error.name,
|
|
60
|
+
message: error.message,
|
|
61
|
+
stack: error.stack,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
phase,
|
|
67
|
+
error: String(error),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function resolveRetryAttempts(job) {
|
|
71
|
+
const attempts = job.retry?.attempts;
|
|
72
|
+
if (attempts === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (!Number.isInteger(attempts) || attempts < 0 || attempts > 20) {
|
|
75
|
+
throw new Error(`[provider-inngest] Job "${job.name}" retry.attempts must be an integer between 0 and 20.`);
|
|
76
|
+
}
|
|
77
|
+
return attempts;
|
|
78
|
+
}
|
|
79
|
+
export function createInngestJobDispatcher(client, options = {}) {
|
|
80
|
+
const instrumentation = createProviderInstrumentation(options.instrumentation, {
|
|
81
|
+
providerName: "inngest",
|
|
82
|
+
watcher: "jobs",
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
async dispatch(job, payload) {
|
|
86
|
+
const parsed = await parseJobPayload(job, payload);
|
|
87
|
+
try {
|
|
88
|
+
await client.send({ name: job.name, data: parsed });
|
|
89
|
+
instrumentation.record({
|
|
90
|
+
type: "job",
|
|
91
|
+
jobName: job.name,
|
|
92
|
+
status: "scheduled",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
instrumentation.record({
|
|
97
|
+
type: "job",
|
|
98
|
+
jobName: job.name,
|
|
99
|
+
status: "failed",
|
|
100
|
+
details: errorDetails("schedule", error),
|
|
101
|
+
});
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export function createInngestJobFunction(options) {
|
|
108
|
+
const { client, job } = options;
|
|
109
|
+
const retries = resolveRetryAttempts(job);
|
|
110
|
+
const instrumentation = createProviderInstrumentation(options.instrumentation, {
|
|
111
|
+
providerName: "inngest",
|
|
112
|
+
watcher: "jobs",
|
|
113
|
+
});
|
|
114
|
+
return client.createFunction({
|
|
115
|
+
id: options.id ?? job.name,
|
|
116
|
+
name: options.name ?? job.name,
|
|
117
|
+
description: options.description ?? job.description,
|
|
118
|
+
...(retries === undefined ? {} : { retries }),
|
|
119
|
+
}, { event: job.name }, async ({ event, step }) => {
|
|
120
|
+
instrumentation.record({
|
|
121
|
+
type: "job",
|
|
122
|
+
jobName: job.name,
|
|
123
|
+
status: "started",
|
|
124
|
+
});
|
|
125
|
+
try {
|
|
126
|
+
const payload = await parseJobPayload(job, event.data);
|
|
127
|
+
await job.handle({
|
|
128
|
+
job,
|
|
129
|
+
payload,
|
|
130
|
+
ctx: await resolveJobContext(options.ctx, {
|
|
131
|
+
event: {
|
|
132
|
+
name: event.name,
|
|
133
|
+
data: event.data,
|
|
134
|
+
},
|
|
135
|
+
step,
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
instrumentation.record({
|
|
139
|
+
type: "job",
|
|
140
|
+
jobName: job.name,
|
|
141
|
+
status: "completed",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
instrumentation.record({
|
|
146
|
+
type: "job",
|
|
147
|
+
jobName: job.name,
|
|
148
|
+
status: "failed",
|
|
149
|
+
details: errorDetails("execute", error),
|
|
150
|
+
});
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Inngest provider that extends ports with background job and event capabilities.
|
|
157
|
+
*
|
|
158
|
+
* This provider creates an Inngest client and exposes it through the ports system.
|
|
159
|
+
* It contributes a canonical `jobs` port for app code and keeps the raw
|
|
160
|
+
* Inngest client available as an escape hatch through `ports.inngest.client`.
|
|
161
|
+
*
|
|
162
|
+
* Configuration via environment variables:
|
|
163
|
+
* - INNGEST_APP_NAME: Application name (optional, default: "beignet-app")
|
|
164
|
+
* - INNGEST_EVENT_KEY: Event key for Inngest cloud (optional)
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```ts
|
|
168
|
+
* const server = await createNextServer({
|
|
169
|
+
* ports: basePorts,
|
|
170
|
+
* providers: [inngestProvider],
|
|
171
|
+
* // ...
|
|
172
|
+
* });
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
export const inngestProvider = createProvider({
|
|
176
|
+
name: "inngest",
|
|
177
|
+
config: {
|
|
178
|
+
schema: InngestConfigSchema,
|
|
179
|
+
envPrefix: "INNGEST_",
|
|
180
|
+
},
|
|
181
|
+
async setup({ ports, config }) {
|
|
182
|
+
if (!config) {
|
|
183
|
+
throw new Error("[inngestProvider] Missing config. Please set INNGEST_APP_NAME and optional INNGEST_EVENT_KEY.");
|
|
184
|
+
}
|
|
185
|
+
// Create Inngest client
|
|
186
|
+
const clientOptions = {
|
|
187
|
+
id: config.APP_NAME,
|
|
188
|
+
};
|
|
189
|
+
// Add event key if provided (required for Inngest Cloud)
|
|
190
|
+
if (config.EVENT_KEY) {
|
|
191
|
+
clientOptions.eventKey = config.EVENT_KEY;
|
|
192
|
+
}
|
|
193
|
+
const client = new Inngest(clientOptions);
|
|
194
|
+
const instrumentation = createProviderInstrumentation(ports, {
|
|
195
|
+
providerName: "inngest",
|
|
196
|
+
watcher: "jobs",
|
|
197
|
+
});
|
|
198
|
+
// Build port
|
|
199
|
+
const inngestPort = {
|
|
200
|
+
client,
|
|
201
|
+
async send({ name, data }) {
|
|
202
|
+
try {
|
|
203
|
+
await client.send({ name, data });
|
|
204
|
+
instrumentation.record({
|
|
205
|
+
type: "job",
|
|
206
|
+
jobName: name,
|
|
207
|
+
status: "scheduled",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
instrumentation.record({
|
|
212
|
+
type: "job",
|
|
213
|
+
jobName: name,
|
|
214
|
+
status: "failed",
|
|
215
|
+
details: errorDetails("schedule", error),
|
|
216
|
+
});
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
const jobs = createInngestJobDispatcher(client, { instrumentation });
|
|
222
|
+
return { ports: { inngest: inngestPort, jobs } };
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@beignet/provider-inngest",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Inngest provider for Beignet - adds inngest port for background jobs and events",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src",
|
|
17
|
+
"!src/**/*.test.ts",
|
|
18
|
+
"!src/**/*.test.tsx",
|
|
19
|
+
"!src/**/*.test-d.ts",
|
|
20
|
+
"README.md",
|
|
21
|
+
"CHANGELOG.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"dev": "tsc --watch",
|
|
26
|
+
"clean": "rm -rf dist coverage .turbo",
|
|
27
|
+
"test": "bun test",
|
|
28
|
+
"test:coverage": "bun test --coverage",
|
|
29
|
+
"lint": "biome check ."
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"beignet",
|
|
33
|
+
"inngest",
|
|
34
|
+
"provider",
|
|
35
|
+
"jobs",
|
|
36
|
+
"events",
|
|
37
|
+
"background-jobs",
|
|
38
|
+
"ports"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/taylorbryant/beignet.git",
|
|
44
|
+
"directory": "packages/provider-inngest"
|
|
45
|
+
},
|
|
46
|
+
"author": "Taylor Bryant",
|
|
47
|
+
"homepage": "https://github.com/taylorbryant/beignet#readme",
|
|
48
|
+
"bugs": "https://github.com/taylorbryant/beignet/issues",
|
|
49
|
+
"sideEffects": false,
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18.0.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"inngest": "^3.0.0"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"zod": "^4.0.0",
|
|
61
|
+
"@beignet/core": "*"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@beignet/devtools": "*",
|
|
65
|
+
"@types/bun": "^1.3.13",
|
|
66
|
+
"@types/node": "^20.10.0",
|
|
67
|
+
"inngest": "^3.0.0",
|
|
68
|
+
"typescript": "^5.3.0"
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beignet/provider-inngest
|
|
3
|
+
*
|
|
4
|
+
* Inngest provider that extends ports with background job and event capabilities using Inngest.
|
|
5
|
+
*
|
|
6
|
+
* Configuration:
|
|
7
|
+
* - INNGEST_APP_NAME: Friendly application name shown in Inngest (optional, default: "beignet-app")
|
|
8
|
+
* - INNGEST_EVENT_KEY: Optional event key / signing key for Inngest cloud
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { createNextServer } from "@beignet/next";
|
|
13
|
+
* import { inngestProvider } from "@beignet/provider-inngest";
|
|
14
|
+
*
|
|
15
|
+
* const server = await createNextServer({
|
|
16
|
+
* ports: basePorts,
|
|
17
|
+
* providers: [inngestProvider],
|
|
18
|
+
* // ...
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // In your use cases:
|
|
22
|
+
* await ctx.ports.jobs.dispatch(SendInviteEmailJob, {
|
|
23
|
+
* inviteId: createdInvite.id,
|
|
24
|
+
* inviteeEmail: input.email,
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type {
|
|
30
|
+
InferJobPayload,
|
|
31
|
+
JobDef,
|
|
32
|
+
MaybePromise,
|
|
33
|
+
StandardSchema,
|
|
34
|
+
} from "@beignet/core/jobs";
|
|
35
|
+
import { parseJobPayload } from "@beignet/core/jobs";
|
|
36
|
+
import type { JobDispatcherPort } from "@beignet/core/ports";
|
|
37
|
+
import {
|
|
38
|
+
createProvider,
|
|
39
|
+
createProviderInstrumentation,
|
|
40
|
+
type ProviderInstrumentationTarget,
|
|
41
|
+
} from "@beignet/core/providers";
|
|
42
|
+
import { Inngest, type InngestFunction } from "inngest";
|
|
43
|
+
import { z } from "zod";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Configuration schema for the Inngest provider.
|
|
47
|
+
* Validates environment variables with INNGEST_ prefix.
|
|
48
|
+
*/
|
|
49
|
+
const InngestConfigSchema = z.object({
|
|
50
|
+
/**
|
|
51
|
+
* Application ID used as the client's `id` property in Inngest v3.
|
|
52
|
+
* This value uniquely identifies your application in Inngest and may be shown in the dashboard.
|
|
53
|
+
*/
|
|
54
|
+
APP_NAME: z.string().default("beignet-app"),
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Optional event key / signing key for Inngest cloud.
|
|
58
|
+
* Required when using Inngest Cloud for production deployments.
|
|
59
|
+
*/
|
|
60
|
+
EVENT_KEY: z.string().optional(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Inferred configuration type for Inngest provider.
|
|
65
|
+
*/
|
|
66
|
+
export type InngestConfig = z.infer<typeof InngestConfigSchema>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Port interface for Inngest integration.
|
|
70
|
+
* Provides a stable abstraction over Inngest's client API.
|
|
71
|
+
*/
|
|
72
|
+
export interface InngestPort {
|
|
73
|
+
/**
|
|
74
|
+
* The raw Inngest client instance.
|
|
75
|
+
* Useful for advanced operations like defining functions.
|
|
76
|
+
*/
|
|
77
|
+
client: Inngest;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Send an Inngest event.
|
|
81
|
+
* Equivalent to inngest.send({ name, data }).
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* await ctx.ports.inngest.send({
|
|
86
|
+
* name: "user.invited",
|
|
87
|
+
* data: { inviteId, email }
|
|
88
|
+
* });
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
send<TData>(args: { name: string; data: TData }): Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface InngestJobDispatcherOptions {
|
|
95
|
+
instrumentation?: ProviderInstrumentationTarget;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type InngestFunctionRetryAttempts =
|
|
99
|
+
| 0
|
|
100
|
+
| 1
|
|
101
|
+
| 2
|
|
102
|
+
| 3
|
|
103
|
+
| 4
|
|
104
|
+
| 5
|
|
105
|
+
| 6
|
|
106
|
+
| 7
|
|
107
|
+
| 8
|
|
108
|
+
| 9
|
|
109
|
+
| 10
|
|
110
|
+
| 11
|
|
111
|
+
| 12
|
|
112
|
+
| 13
|
|
113
|
+
| 14
|
|
114
|
+
| 15
|
|
115
|
+
| 16
|
|
116
|
+
| 17
|
|
117
|
+
| 18
|
|
118
|
+
| 19
|
|
119
|
+
| 20;
|
|
120
|
+
|
|
121
|
+
export type InngestJobFunctionContext<Ctx> =
|
|
122
|
+
| Ctx
|
|
123
|
+
| ((args: InngestJobFunctionContextArgs) => MaybePromise<Ctx>);
|
|
124
|
+
|
|
125
|
+
export interface InngestJobFunctionContextArgs {
|
|
126
|
+
event: {
|
|
127
|
+
name: string;
|
|
128
|
+
data: unknown;
|
|
129
|
+
};
|
|
130
|
+
step: unknown;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type InngestJobFunctionInstrumentation = ProviderInstrumentationTarget;
|
|
134
|
+
|
|
135
|
+
export interface CreateInngestJobFunctionOptions<
|
|
136
|
+
J extends JobDef<string, StandardSchema, Ctx>,
|
|
137
|
+
Ctx,
|
|
138
|
+
> {
|
|
139
|
+
client: Inngest;
|
|
140
|
+
job: J;
|
|
141
|
+
id?: string;
|
|
142
|
+
name?: string;
|
|
143
|
+
description?: string;
|
|
144
|
+
ctx?: InngestJobFunctionContext<Ctx>;
|
|
145
|
+
instrumentation?: InngestJobFunctionInstrumentation;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function resolveJobContext<Ctx>(
|
|
149
|
+
ctx: InngestJobFunctionContext<Ctx> | undefined,
|
|
150
|
+
args: InngestJobFunctionContextArgs,
|
|
151
|
+
): Promise<Ctx> {
|
|
152
|
+
if (typeof ctx === "function") {
|
|
153
|
+
return (ctx as (args: InngestJobFunctionContextArgs) => MaybePromise<Ctx>)(
|
|
154
|
+
args,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return ctx as Ctx;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function errorDetails(phase: "schedule" | "execute", error: unknown) {
|
|
162
|
+
if (error instanceof Error) {
|
|
163
|
+
return {
|
|
164
|
+
phase,
|
|
165
|
+
error: {
|
|
166
|
+
name: error.name,
|
|
167
|
+
message: error.message,
|
|
168
|
+
stack: error.stack,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
phase,
|
|
175
|
+
error: String(error),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function resolveRetryAttempts(
|
|
180
|
+
job: JobDef,
|
|
181
|
+
): InngestFunctionRetryAttempts | undefined {
|
|
182
|
+
const attempts = job.retry?.attempts;
|
|
183
|
+
if (attempts === undefined) return undefined;
|
|
184
|
+
|
|
185
|
+
if (!Number.isInteger(attempts) || attempts < 0 || attempts > 20) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`[provider-inngest] Job "${job.name}" retry.attempts must be an integer between 0 and 20.`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return attempts as InngestFunctionRetryAttempts;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function createInngestJobDispatcher(
|
|
195
|
+
client: Inngest,
|
|
196
|
+
options: InngestJobDispatcherOptions = {},
|
|
197
|
+
): JobDispatcherPort {
|
|
198
|
+
const instrumentation = createProviderInstrumentation(
|
|
199
|
+
options.instrumentation,
|
|
200
|
+
{
|
|
201
|
+
providerName: "inngest",
|
|
202
|
+
watcher: "jobs",
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
async dispatch<J extends JobDef>(
|
|
208
|
+
job: J,
|
|
209
|
+
payload: InferJobPayload<J>,
|
|
210
|
+
): Promise<void> {
|
|
211
|
+
const parsed = await parseJobPayload(job, payload);
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
await client.send({ name: job.name, data: parsed });
|
|
215
|
+
|
|
216
|
+
instrumentation.record({
|
|
217
|
+
type: "job",
|
|
218
|
+
jobName: job.name,
|
|
219
|
+
status: "scheduled",
|
|
220
|
+
});
|
|
221
|
+
} catch (error) {
|
|
222
|
+
instrumentation.record({
|
|
223
|
+
type: "job",
|
|
224
|
+
jobName: job.name,
|
|
225
|
+
status: "failed",
|
|
226
|
+
details: errorDetails("schedule", error),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function createInngestJobFunction<
|
|
236
|
+
Ctx,
|
|
237
|
+
J extends JobDef<string, StandardSchema, Ctx>,
|
|
238
|
+
>(options: CreateInngestJobFunctionOptions<J, Ctx>): InngestFunction.Any {
|
|
239
|
+
const { client, job } = options;
|
|
240
|
+
const retries = resolveRetryAttempts(job);
|
|
241
|
+
const instrumentation = createProviderInstrumentation(
|
|
242
|
+
options.instrumentation,
|
|
243
|
+
{
|
|
244
|
+
providerName: "inngest",
|
|
245
|
+
watcher: "jobs",
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
return client.createFunction(
|
|
250
|
+
{
|
|
251
|
+
id: options.id ?? job.name,
|
|
252
|
+
name: options.name ?? job.name,
|
|
253
|
+
description: options.description ?? job.description,
|
|
254
|
+
...(retries === undefined ? {} : { retries }),
|
|
255
|
+
},
|
|
256
|
+
{ event: job.name },
|
|
257
|
+
async ({ event, step }) => {
|
|
258
|
+
instrumentation.record({
|
|
259
|
+
type: "job",
|
|
260
|
+
jobName: job.name,
|
|
261
|
+
status: "started",
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const payload = await parseJobPayload(job, event.data);
|
|
266
|
+
await job.handle({
|
|
267
|
+
job,
|
|
268
|
+
payload,
|
|
269
|
+
ctx: await resolveJobContext(options.ctx, {
|
|
270
|
+
event: {
|
|
271
|
+
name: event.name,
|
|
272
|
+
data: event.data,
|
|
273
|
+
},
|
|
274
|
+
step,
|
|
275
|
+
}),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
instrumentation.record({
|
|
279
|
+
type: "job",
|
|
280
|
+
jobName: job.name,
|
|
281
|
+
status: "completed",
|
|
282
|
+
});
|
|
283
|
+
} catch (error) {
|
|
284
|
+
instrumentation.record({
|
|
285
|
+
type: "job",
|
|
286
|
+
jobName: job.name,
|
|
287
|
+
status: "failed",
|
|
288
|
+
details: errorDetails("execute", error),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
) as InngestFunction.Any;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Inngest provider that extends ports with background job and event capabilities.
|
|
299
|
+
*
|
|
300
|
+
* This provider creates an Inngest client and exposes it through the ports system.
|
|
301
|
+
* It contributes a canonical `jobs` port for app code and keeps the raw
|
|
302
|
+
* Inngest client available as an escape hatch through `ports.inngest.client`.
|
|
303
|
+
*
|
|
304
|
+
* Configuration via environment variables:
|
|
305
|
+
* - INNGEST_APP_NAME: Application name (optional, default: "beignet-app")
|
|
306
|
+
* - INNGEST_EVENT_KEY: Event key for Inngest cloud (optional)
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* ```ts
|
|
310
|
+
* const server = await createNextServer({
|
|
311
|
+
* ports: basePorts,
|
|
312
|
+
* providers: [inngestProvider],
|
|
313
|
+
* // ...
|
|
314
|
+
* });
|
|
315
|
+
* ```
|
|
316
|
+
*/
|
|
317
|
+
export const inngestProvider = createProvider({
|
|
318
|
+
name: "inngest",
|
|
319
|
+
|
|
320
|
+
config: {
|
|
321
|
+
schema: InngestConfigSchema,
|
|
322
|
+
envPrefix: "INNGEST_",
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
async setup({ ports, config }) {
|
|
326
|
+
if (!config) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
"[inngestProvider] Missing config. Please set INNGEST_APP_NAME and optional INNGEST_EVENT_KEY.",
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Create Inngest client
|
|
333
|
+
const clientOptions: { id: string; eventKey?: string } = {
|
|
334
|
+
id: config.APP_NAME,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Add event key if provided (required for Inngest Cloud)
|
|
338
|
+
if (config.EVENT_KEY) {
|
|
339
|
+
clientOptions.eventKey = config.EVENT_KEY;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const client = new Inngest(clientOptions);
|
|
343
|
+
const instrumentation = createProviderInstrumentation(ports, {
|
|
344
|
+
providerName: "inngest",
|
|
345
|
+
watcher: "jobs",
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Build port
|
|
349
|
+
const inngestPort: InngestPort = {
|
|
350
|
+
client,
|
|
351
|
+
async send({ name, data }) {
|
|
352
|
+
try {
|
|
353
|
+
await client.send({ name, data });
|
|
354
|
+
|
|
355
|
+
instrumentation.record({
|
|
356
|
+
type: "job",
|
|
357
|
+
jobName: name,
|
|
358
|
+
status: "scheduled",
|
|
359
|
+
});
|
|
360
|
+
} catch (error) {
|
|
361
|
+
instrumentation.record({
|
|
362
|
+
type: "job",
|
|
363
|
+
jobName: name,
|
|
364
|
+
status: "failed",
|
|
365
|
+
details: errorDetails("schedule", error),
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const jobs = createInngestJobDispatcher(client, { instrumentation });
|
|
374
|
+
|
|
375
|
+
return { ports: { inngest: inngestPort, jobs } };
|
|
376
|
+
},
|
|
377
|
+
});
|