@novadxhq/sveltekit-inngest 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +160 -94
  2. package/package.json +17 -1
package/README.md CHANGED
@@ -1,43 +1,48 @@
1
1
  # @novadxhq/sveltekit-inngest
2
2
 
3
- Svelte 5 utilities for building typed realtime subscriptions in SvelteKit with Inngest and SSE.
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
- ## What This Library Provides
5
+ It is built for Svelte 5 and leans into state-first reads (`.current`) instead of store syntax.
6
6
 
7
- - A client manager component (`RealtimeManager`) that owns the SSE connection.
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
- ## Requirements
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
- This package is for **Svelte 5 + SvelteKit** projects.
16
+ ## Requirements
16
17
 
17
- Your app must already include these peer dependencies:
18
+ This package is intended for **Svelte 5 + SvelteKit** projects and expects these peer dependencies:
18
19
 
19
- - `svelte` (v5)
20
+ - `svelte`
20
21
  - `@sveltejs/kit`
21
22
  - `sveltekit-sse`
22
23
  - `@inngest/realtime`
23
24
  - `inngest`
24
25
 
25
- ## Install
26
+ ## Installation
26
27
 
27
- ```sh
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
- ## Quick Start (End-to-End)
36
+ ## How to Use
32
37
 
33
- ### 1. Define your channel and topic
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
- export const ordersUpdatedTopic = topic("orders.updated").schema(
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: ({ event, locals, topics, params }) => {
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 your UI with `RealtimeManager`
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 a child component
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 orderUpdated = getRealtimeTopicState<typeof ordersChannel, "orders.updated">(
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 orderUpdated.current}
119
- <pre>{JSON.stringify(orderUpdated.current, null, 2)}</pre>
120
+ {#if ordersUpdated.current}
121
+ <pre>{JSON.stringify(ordersUpdated.current, null, 2)}</pre>
120
122
  {/if}
121
123
  ```
122
124
 
123
- ## Client API
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
- ### `RealtimeManager`
141
+ ### `<RealtimeManager />`
126
142
 
127
- Wraps children and provides realtime context to descendant components.
143
+ Provides realtime context to descendants and owns the client SSE connection.
128
144
 
129
- Common props:
145
+ #### `endpoint`
130
146
 
131
- - `endpoint`: SSE endpoint path (default: `"/api/events"`).
132
- - `channel`: `Realtime.Channel` or channel definition.
133
- - `channelArgs`: args for channel definitions that are factories.
134
- - `topics`: optional subset of topic names.
135
- - `params`: optional metadata sent to server authorize logic.
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 state-first manager context:
179
+ Returns manager context with Svelte 5 state wrappers:
140
180
 
141
- - `health.current`: current health payload (`ok`, `status`, `ts`, optional `detail`).
142
- - `channelId`, `topics`, `select` for lower-level usage.
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
- By default, `.current` is the **full Inngest message envelope**, including fields like:
190
+ #### `options.map(message)`
191
+
192
+ Transforms each parsed message before it is stored.
149
193
 
150
- - `topic`
151
- - `data`
152
- - `runId`
153
- - `createdAt`
154
- - `kind`
155
- - `envId`
156
- - `fnId`
194
+ #### `options.or(payload)`
157
195
 
158
- `createdAt` is a string on the client because SSE payloads are JSON-parsed.
196
+ Fallback parser hook when JSON parsing fails. Receives `{ error, raw, previous }`.
159
197
 
160
- If you only want `data`, map it:
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
- const payloadOnly = getRealtimeTopicState<
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
- ## Server API
230
+ ---
173
231
 
174
- ### `createRealtimeEndpoint(options)`
232
+ #### `options.healthCheck`
175
233
 
176
- Creates a SvelteKit `RequestHandler` for `POST` SSE.
234
+ Controls health tick behavior:
177
235
 
178
- Key options:
236
+ ```ts
237
+ healthCheck?: {
238
+ intervalMs?: number; // default: 5000
239
+ enabled?: boolean; // default: true
240
+ };
241
+ ```
179
242
 
180
- - `inngest`: your Inngest client instance.
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` receives:
245
+ #### `options.authorize(context)`
187
246
 
188
- - `event`: full SvelteKit `RequestEvent`.
189
- - `locals`, `request`, `channelId`, `topics`, `params`.
247
+ Optional authorization hook. Useful for auth checks and topic filtering.
190
248
 
191
- `authorize` return values:
249
+ `context` includes:
192
250
 
193
- - `true`: allow requested topics.
194
- - `false`: deny request (`403` JSON).
195
- - `{ allowedTopics }`: allow only a subset of requested topics.
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: `POST`.
200
- - Request body:
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 by the endpoint:
213
- - `message`: realtime message payloads (JSON stringified).
214
- - `health`: connection health payloads (JSON stringified).
215
- - Unauthorized requests return `403` JSON and no SSE stream.
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
- ## Compatibility APIs (Concise)
290
+ ## Troubleshooting
220
291
 
221
- These alternatives are still available if you prefer store-returning helpers:
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
- - `getRealtime()` -> returns context with `health` as a `Readable`.
224
- - `getRealtimeTopicJson()` -> returns topic stream as a `Readable`.
297
+ ## Contributing
225
298
 
226
- The recommended Svelte 5 path is `getRealtimeState()` and `getRealtimeTopicState()` with `.current`.
299
+ PRs are welcome. Please include a clear explanation of the behavior you are changing and why.
227
300
 
228
- ## Troubleshooting
301
+ ## License
229
302
 
230
- - `getRealtimeState() requires <RealtimeManager>...`
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@novadxhq/sveltekit-inngest",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "license": "MIT",
5
5
  "scripts": {
6
6
  "dev": "bunx vite dev",
@@ -40,8 +40,24 @@
40
40
  },
41
41
  "./server": {
42
42
  "types": "./dist/server/index.d.ts",
43
+ "svelte": "./dist/server/index.js",
43
44
  "default": "./dist/server/index.js"
44
45
  },
46
+ "./server/index": {
47
+ "types": "./dist/server/index.d.ts",
48
+ "svelte": "./dist/server/index.js",
49
+ "default": "./dist/server/index.js"
50
+ },
51
+ "./server/createRealtimeEndpoint": {
52
+ "types": "./dist/server/createRealtimeEndpoint.d.ts",
53
+ "svelte": "./dist/server/createRealtimeEndpoint.js",
54
+ "default": "./dist/server/createRealtimeEndpoint.js"
55
+ },
56
+ "./server/createRealtimeEndpoint.js": {
57
+ "types": "./dist/server/createRealtimeEndpoint.d.ts",
58
+ "svelte": "./dist/server/createRealtimeEndpoint.js",
59
+ "default": "./dist/server/createRealtimeEndpoint.js"
60
+ },
45
61
  "./package.json": "./package.json"
46
62
  },
47
63
  "peerDependencies": {