@novadxhq/sveltekit-inngest 0.0.3 → 0.0.4
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 +160 -94
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,43 +1,48 @@
|
|
|
1
1
|
# @novadxhq/sveltekit-inngest
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@novadxhq/sveltekit-inngest` gives you typed realtime subscriptions in SvelteKit using Inngest + SSE. It pairs a small client manager with a server endpoint helper so your topic payloads, health state, and authorization flow all line up.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It is built for Svelte 5 and leans into state-first reads (`.current`) instead of store syntax.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- State-first client helpers (`getRealtimeState`, `getRealtimeTopicState`) for Svelte 5 `.current` reads.
|
|
9
|
-
- A server helper (`createRealtimeEndpoint`) that creates a `POST` SSE endpoint using `sveltekit-sse`.
|
|
10
|
-
- Typed topic payloads based on your `@inngest/realtime` channel definitions.
|
|
11
|
-
- Built-in connection health events (`connecting`, `connected`, `degraded`).
|
|
7
|
+
## Features
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
- **Typed topic payloads** - Topic types come from your `@inngest/realtime` channel definitions.
|
|
10
|
+
- **Svelte 5 state-first API** - Read health and topic data through `getRealtimeState()` and `getRealtimeTopicState()`.
|
|
11
|
+
- **Single server helper** - `createRealtimeEndpoint()` handles request parsing, topic checks, authorization, and SSE wiring.
|
|
12
|
+
- **Built-in health events** - Streams `connecting`, `connected`, and `degraded` lifecycle updates.
|
|
13
|
+
- **Topic-level authorization** - Return `{ allowedTopics }` from `authorize` to scope subscriptions per request.
|
|
14
|
+
- **Compatibility helpers included** - `getRealtime()` and `getRealtimeTopicJson()` are still available for store-based usage.
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
## Requirements
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
This package is intended for **Svelte 5 + SvelteKit** projects and expects these peer dependencies:
|
|
18
19
|
|
|
19
|
-
- `svelte`
|
|
20
|
+
- `svelte`
|
|
20
21
|
- `@sveltejs/kit`
|
|
21
22
|
- `sveltekit-sse`
|
|
22
23
|
- `@inngest/realtime`
|
|
23
24
|
- `inngest`
|
|
24
25
|
|
|
25
|
-
##
|
|
26
|
+
## Installation
|
|
26
27
|
|
|
27
|
-
```
|
|
28
|
+
```bash
|
|
29
|
+
pnpm add @novadxhq/sveltekit-inngest
|
|
30
|
+
# or
|
|
28
31
|
npm install @novadxhq/sveltekit-inngest
|
|
32
|
+
# or
|
|
33
|
+
bun add @novadxhq/sveltekit-inngest
|
|
29
34
|
```
|
|
30
35
|
|
|
31
|
-
##
|
|
36
|
+
## How to Use
|
|
32
37
|
|
|
33
|
-
### 1. Define your channel and
|
|
38
|
+
### 1. Define your channel and topics
|
|
34
39
|
|
|
35
40
|
```ts
|
|
36
41
|
// src/lib/realtime/orders-channel.ts
|
|
37
42
|
import { channel, topic } from "@inngest/realtime";
|
|
38
43
|
import { z } from "zod";
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
const ordersUpdatedTopic = topic("orders.updated").schema(
|
|
41
46
|
z.object({
|
|
42
47
|
orderId: z.string(),
|
|
43
48
|
status: z.string(),
|
|
@@ -47,13 +52,13 @@ export const ordersUpdatedTopic = topic("orders.updated").schema(
|
|
|
47
52
|
export const ordersChannel = channel("orders").addTopic(ordersUpdatedTopic);
|
|
48
53
|
```
|
|
49
54
|
|
|
50
|
-
### 2. Create the realtime endpoint
|
|
55
|
+
### 2. Create the realtime SSE endpoint
|
|
51
56
|
|
|
52
57
|
```ts
|
|
53
58
|
// src/routes/api/events/+server.ts
|
|
54
59
|
import { createRealtimeEndpoint } from "@novadxhq/sveltekit-inngest/server";
|
|
55
|
-
import { inngest } from "$lib/server/inngest";
|
|
56
60
|
import { ordersChannel } from "$lib/realtime/orders-channel";
|
|
61
|
+
import { inngest } from "$lib/server/inngest";
|
|
57
62
|
|
|
58
63
|
export const POST = createRealtimeEndpoint({
|
|
59
64
|
inngest,
|
|
@@ -61,12 +66,9 @@ export const POST = createRealtimeEndpoint({
|
|
|
61
66
|
healthCheck: {
|
|
62
67
|
intervalMs: 5_000,
|
|
63
68
|
},
|
|
64
|
-
authorize: ({
|
|
65
|
-
// You have full RequestEvent access here.
|
|
66
|
-
// Example: block unauthenticated users.
|
|
69
|
+
authorize: ({ locals, topics, params }) => {
|
|
67
70
|
if (!locals.user) return false;
|
|
68
71
|
|
|
69
|
-
// Example: optionally filter allowed topics.
|
|
70
72
|
if (params?.scope === "limited") {
|
|
71
73
|
return {
|
|
72
74
|
allowedTopics: topics.filter((topic) => topic === "orders.updated"),
|
|
@@ -78,7 +80,7 @@ export const POST = createRealtimeEndpoint({
|
|
|
78
80
|
});
|
|
79
81
|
```
|
|
80
82
|
|
|
81
|
-
### 3. Wrap
|
|
83
|
+
### 3. Wrap UI with `RealtimeManager`
|
|
82
84
|
|
|
83
85
|
```svelte
|
|
84
86
|
<!-- src/routes/+page.svelte -->
|
|
@@ -93,7 +95,7 @@ export const POST = createRealtimeEndpoint({
|
|
|
93
95
|
</RealtimeManager>
|
|
94
96
|
```
|
|
95
97
|
|
|
96
|
-
### 4. Read topic state in
|
|
98
|
+
### 4. Read health and topic state in child components
|
|
97
99
|
|
|
98
100
|
```svelte
|
|
99
101
|
<!-- src/routes/OrdersPanel.svelte -->
|
|
@@ -102,7 +104,7 @@ export const POST = createRealtimeEndpoint({
|
|
|
102
104
|
import { ordersChannel } from "$lib/realtime/orders-channel";
|
|
103
105
|
|
|
104
106
|
const { health } = getRealtimeState();
|
|
105
|
-
const
|
|
107
|
+
const ordersUpdated = getRealtimeTopicState<typeof ordersChannel, "orders.updated">(
|
|
106
108
|
"orders.updated"
|
|
107
109
|
);
|
|
108
110
|
</script>
|
|
@@ -115,89 +117,160 @@ export const POST = createRealtimeEndpoint({
|
|
|
115
117
|
{/if}
|
|
116
118
|
</p>
|
|
117
119
|
|
|
118
|
-
{#if
|
|
119
|
-
<pre>{JSON.stringify(
|
|
120
|
+
{#if ordersUpdated.current}
|
|
121
|
+
<pre>{JSON.stringify(ordersUpdated.current, null, 2)}</pre>
|
|
120
122
|
{/if}
|
|
121
123
|
```
|
|
122
124
|
|
|
123
|
-
|
|
125
|
+
### 5. Return only `data` when you do not need the full envelope
|
|
126
|
+
|
|
127
|
+
By default, `getRealtimeTopicState()` returns the full Inngest envelope (`topic`, `data`, `runId`, `createdAt`, and more). If you only want the payload, map it:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const payloadOnly = getRealtimeTopicState<
|
|
131
|
+
typeof ordersChannel,
|
|
132
|
+
"orders.updated",
|
|
133
|
+
{ orderId: string; status: string }
|
|
134
|
+
>("orders.updated", {
|
|
135
|
+
map: (message) => message.data,
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## API
|
|
124
140
|
|
|
125
|
-
###
|
|
141
|
+
### `<RealtimeManager />`
|
|
126
142
|
|
|
127
|
-
|
|
143
|
+
Provides realtime context to descendants and owns the client SSE connection.
|
|
128
144
|
|
|
129
|
-
|
|
145
|
+
#### `endpoint`
|
|
130
146
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
147
|
+
SSE route path. Default: `"/api/events"`.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
#### `channel`
|
|
152
|
+
|
|
153
|
+
`Realtime.Channel` or channel definition from `@inngest/realtime`.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
#### `channelArgs`
|
|
158
|
+
|
|
159
|
+
Optional argument list for channel definition factories.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
#### `topics`
|
|
164
|
+
|
|
165
|
+
Optional explicit topic subset. If omitted, all channel topics are requested.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
#### `params`
|
|
170
|
+
|
|
171
|
+
Optional scalar metadata sent in the request body and forwarded into `authorize`.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
type RealtimeRequestParams = Record<string, string | number | boolean | null>;
|
|
175
|
+
```
|
|
136
176
|
|
|
137
177
|
### `getRealtimeState()`
|
|
138
178
|
|
|
139
|
-
Returns
|
|
179
|
+
Returns manager context with Svelte 5 state wrappers:
|
|
140
180
|
|
|
141
|
-
- `health.current
|
|
142
|
-
- `channelId
|
|
181
|
+
- `health.current` - current health payload (`ok`, `status`, `ts`, optional `detail`).
|
|
182
|
+
- `channelId`
|
|
183
|
+
- `topics`
|
|
184
|
+
- `select` (low-level `sveltekit-sse` selector access)
|
|
143
185
|
|
|
144
186
|
### `getRealtimeTopicState(topic, options?)`
|
|
145
187
|
|
|
146
188
|
Returns a state wrapper (`.current`) for a topic stream.
|
|
147
189
|
|
|
148
|
-
|
|
190
|
+
#### `options.map(message)`
|
|
191
|
+
|
|
192
|
+
Transforms each parsed message before it is stored.
|
|
149
193
|
|
|
150
|
-
|
|
151
|
-
- `data`
|
|
152
|
-
- `runId`
|
|
153
|
-
- `createdAt`
|
|
154
|
-
- `kind`
|
|
155
|
-
- `envId`
|
|
156
|
-
- `fnId`
|
|
194
|
+
#### `options.or(payload)`
|
|
157
195
|
|
|
158
|
-
|
|
196
|
+
Fallback parser hook when JSON parsing fails. Receives `{ error, raw, previous }`.
|
|
159
197
|
|
|
160
|
-
|
|
198
|
+
### `getRealtimeTopicJson(topic, options?)`
|
|
199
|
+
|
|
200
|
+
Store-based variant of `getRealtimeTopicState()` that returns a Svelte `Readable`.
|
|
201
|
+
|
|
202
|
+
### `getRealtime()`
|
|
203
|
+
|
|
204
|
+
Returns the raw realtime context (`health` as a `Readable`) and throws if called outside `<RealtimeManager>`.
|
|
205
|
+
|
|
206
|
+
### `createRealtimeEndpoint(options)`
|
|
207
|
+
|
|
208
|
+
Creates a SvelteKit `POST` `RequestHandler` that validates input, authorizes topics, subscribes to Inngest realtime, and emits SSE events.
|
|
209
|
+
|
|
210
|
+
#### `options.inngest`
|
|
211
|
+
|
|
212
|
+
Your Inngest client instance.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
#### `options.channel`
|
|
217
|
+
|
|
218
|
+
Channel object or channel definition.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
#### `options.channelArgs`
|
|
223
|
+
|
|
224
|
+
Optional static args or resolver:
|
|
161
225
|
|
|
162
226
|
```ts
|
|
163
|
-
|
|
164
|
-
typeof ordersChannel,
|
|
165
|
-
"orders.updated",
|
|
166
|
-
{ orderId: string; status: string }
|
|
167
|
-
>("orders.updated", {
|
|
168
|
-
map: (message) => message.data,
|
|
169
|
-
});
|
|
227
|
+
channelArgs?: unknown[] | ((event: RequestEvent) => unknown[] | Promise<unknown[]>);
|
|
170
228
|
```
|
|
171
229
|
|
|
172
|
-
|
|
230
|
+
---
|
|
173
231
|
|
|
174
|
-
|
|
232
|
+
#### `options.healthCheck`
|
|
175
233
|
|
|
176
|
-
|
|
234
|
+
Controls health tick behavior:
|
|
177
235
|
|
|
178
|
-
|
|
236
|
+
```ts
|
|
237
|
+
healthCheck?: {
|
|
238
|
+
intervalMs?: number; // default: 5000
|
|
239
|
+
enabled?: boolean; // default: true
|
|
240
|
+
};
|
|
241
|
+
```
|
|
179
242
|
|
|
180
|
-
|
|
181
|
-
- `channel`: channel object or channel definition.
|
|
182
|
-
- `channelArgs`: optional static array or resolver `(event) => unknown[]`.
|
|
183
|
-
- `healthCheck`: heartbeat control (`intervalMs`, `enabled`).
|
|
184
|
-
- `authorize`: access control callback per request.
|
|
243
|
+
---
|
|
185
244
|
|
|
186
|
-
`authorize`
|
|
245
|
+
#### `options.authorize(context)`
|
|
187
246
|
|
|
188
|
-
|
|
189
|
-
- `locals`, `request`, `channelId`, `topics`, `params`.
|
|
247
|
+
Optional authorization hook. Useful for auth checks and topic filtering.
|
|
190
248
|
|
|
191
|
-
`
|
|
249
|
+
`context` includes:
|
|
192
250
|
|
|
193
|
-
- `
|
|
194
|
-
- `
|
|
195
|
-
- `
|
|
251
|
+
- `event`
|
|
252
|
+
- `locals`
|
|
253
|
+
- `request`
|
|
254
|
+
- `channelId`
|
|
255
|
+
- `topics`
|
|
256
|
+
- `params`
|
|
257
|
+
|
|
258
|
+
Allowed return values:
|
|
259
|
+
|
|
260
|
+
- `true` - allow requested topics.
|
|
261
|
+
- `false` - deny request (`403` JSON).
|
|
262
|
+
- `{ allowedTopics }` - allow only the intersection of requested and allowed topics.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
#### `options.heartbeatMs` (deprecated)
|
|
267
|
+
|
|
268
|
+
Deprecated alias for heartbeat interval. Prefer `healthCheck.intervalMs`.
|
|
196
269
|
|
|
197
270
|
## Behavior and Contracts
|
|
198
271
|
|
|
199
|
-
- Endpoint method
|
|
200
|
-
- Request
|
|
272
|
+
- Endpoint method is `POST`.
|
|
273
|
+
- Request payload:
|
|
201
274
|
|
|
202
275
|
```json
|
|
203
276
|
{
|
|
@@ -209,29 +282,22 @@ Key options:
|
|
|
209
282
|
}
|
|
210
283
|
```
|
|
211
284
|
|
|
212
|
-
- SSE events emitted
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
-
|
|
216
|
-
- Health emits `connecting`, then `connected`, and `degraded` on failures.
|
|
217
|
-
- Heartbeat cadence is configurable via `healthCheck.intervalMs`.
|
|
285
|
+
- SSE events emitted: `message` (realtime payload JSON), `health` (health payload JSON).
|
|
286
|
+
- Unknown topics return `400` JSON.
|
|
287
|
+
- Denied requests return `403` JSON and do not open an SSE stream.
|
|
288
|
+
- Health moves through `connecting`, `connected`, then `degraded` on failures.
|
|
218
289
|
|
|
219
|
-
##
|
|
290
|
+
## Troubleshooting
|
|
220
291
|
|
|
221
|
-
|
|
292
|
+
- `getRealtimeState() requires <RealtimeManager> in the component tree.`: Ensure the consuming component is rendered under `<RealtimeManager>`.
|
|
293
|
+
- Endpoint returns `403`: Confirm your `authorize` logic and auth state in `locals`.
|
|
294
|
+
- No messages for a topic: Verify channel name and topic names match your `@inngest/realtime` definitions.
|
|
295
|
+
- `createdAt` is not a `Date`: SSE payloads are JSON-parsed, so `createdAt` arrives as a string.
|
|
222
296
|
|
|
223
|
-
|
|
224
|
-
- `getRealtimeTopicJson()` -> returns topic stream as a `Readable`.
|
|
297
|
+
## Contributing
|
|
225
298
|
|
|
226
|
-
|
|
299
|
+
PRs are welcome. Please include a clear explanation of the behavior you are changing and why.
|
|
227
300
|
|
|
228
|
-
##
|
|
301
|
+
## License
|
|
229
302
|
|
|
230
|
-
|
|
231
|
-
- Ensure the component calling it is rendered under `<RealtimeManager>`.
|
|
232
|
-
- Endpoint returns `403`
|
|
233
|
-
- Check your `authorize` callback and `locals` auth state.
|
|
234
|
-
- No topic messages
|
|
235
|
-
- Confirm `channel` and topic names match your `@inngest/realtime` definitions.
|
|
236
|
-
- `createdAt` is not a `Date`
|
|
237
|
-
- It is a string after JSON parsing; convert it in UI when needed.
|
|
303
|
+
[MIT](./LICENSE)
|