@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 ADDED
@@ -0,0 +1,5 @@
1
+ # @beignet/provider-inngest
2
+
3
+ ## 0.0.1
4
+
5
+ - Initial Beignet release under the `@beignet` npm scope.
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
@@ -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
+ });