yaxa-svelte 2.3.0 → 2.5.0

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 (41) hide show
  1. package/README.md +107 -15
  2. package/dist/composables/useSSE.svelte.d.ts +53 -0
  3. package/dist/composables/useSSE.svelte.js +209 -0
  4. package/dist/composables/useSSE.test.d.ts +1 -0
  5. package/dist/composables/useSSE.test.js +122 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +1 -0
  8. package/dist/server/db/schema-pg.d.ts +262 -0
  9. package/dist/server/db/schema-pg.js +20 -0
  10. package/dist/server/db/schema-sqlite.d.ts +276 -0
  11. package/dist/server/db/schema-sqlite.js +20 -0
  12. package/dist/server/db/types.d.ts +18 -0
  13. package/dist/server/index.d.ts +4 -0
  14. package/dist/server/index.js +4 -0
  15. package/dist/server/jobs/events.d.ts +29 -0
  16. package/dist/server/jobs/events.js +74 -0
  17. package/dist/server/jobs/index.d.ts +4 -0
  18. package/dist/server/jobs/index.js +3 -0
  19. package/dist/server/jobs/jobs.test.d.ts +1 -0
  20. package/dist/server/jobs/jobs.test.js +189 -0
  21. package/dist/server/jobs/queue.d.ts +14 -0
  22. package/dist/server/jobs/queue.js +90 -0
  23. package/dist/server/jobs/runner.d.ts +12 -0
  24. package/dist/server/jobs/runner.js +176 -0
  25. package/dist/server/jobs/types.d.ts +136 -0
  26. package/dist/server/jobs/types.js +1 -0
  27. package/dist/server/openapi/handler.js +5 -3
  28. package/dist/server/openapi/openapi.test.js +13 -0
  29. package/dist/server/realtime/broadcaster.d.ts +31 -0
  30. package/dist/server/realtime/broadcaster.js +76 -0
  31. package/dist/server/realtime/index.d.ts +3 -0
  32. package/dist/server/realtime/index.js +3 -0
  33. package/dist/server/realtime/realtime.test.d.ts +1 -0
  34. package/dist/server/realtime/realtime.test.js +210 -0
  35. package/dist/server/realtime/sse.d.ts +28 -0
  36. package/dist/server/realtime/sse.js +225 -0
  37. package/dist/server/realtime/types.d.ts +116 -0
  38. package/dist/server/realtime/types.js +1 -0
  39. package/dist/testing/index.d.ts +2 -0
  40. package/dist/testing/index.js +7 -5
  41. package/package.json +1 -1
@@ -0,0 +1,225 @@
1
+ import { globalBroadcaster } from './broadcaster';
2
+ import { problemResponse } from '../endpoint/problem';
3
+ /**
4
+ * Formats a payload into a valid Server-Sent Events text frame.
5
+ */
6
+ export function formatSSEFrame(message) {
7
+ let output = '';
8
+ if (message.id !== undefined) {
9
+ output += `id: ${message.id}\n`;
10
+ }
11
+ if (message.event !== undefined) {
12
+ output += `event: ${message.event}\n`;
13
+ }
14
+ if (message.retry !== undefined) {
15
+ output += `retry: ${message.retry}\n`;
16
+ }
17
+ const dataStr = typeof message.data === 'object' && message.data !== null
18
+ ? JSON.stringify(message.data)
19
+ : String(message.data ?? '');
20
+ // SSE specification requires multiple 'data: ' lines if data contains newlines
21
+ const lines = dataStr.split(/\r\n|\r|\n/);
22
+ for (const line of lines) {
23
+ output += `data: ${line}\n`;
24
+ }
25
+ output += '\n';
26
+ return output;
27
+ }
28
+ /**
29
+ * Creates a turnkey SvelteKit Server-Sent Events (SSE) RequestHandler with
30
+ * guard evaluation, automatic keep-alive pings, and channel broadcasting.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * // src/routes/api/events/+server.ts
35
+ * import { yaxaSSE, requireAuth } from 'yaxa-svelte/server';
36
+ *
37
+ * export const GET = yaxaSSE({
38
+ * guards: [requireAuth()],
39
+ * channel: (event) => `org:${event.locals.orgId}`,
40
+ * keepAliveIntervalMs: 15000,
41
+ * onConnect: ({ send }) => {
42
+ * send('welcome', { time: Date.now() });
43
+ * }
44
+ * });
45
+ * ```
46
+ */
47
+ export function yaxaSSE(options = {}) {
48
+ const keepAliveIntervalMs = options.keepAliveIntervalMs ?? 15_000;
49
+ const keepAliveComment = options.keepAliveComment ?? ': ping\n\n';
50
+ const broadcaster = options.broadcaster ?? globalBroadcaster;
51
+ const sseHandler = async (event) => {
52
+ const { request, locals } = event;
53
+ // 1. Evaluate guards if configured
54
+ if (options.guards && options.guards.length > 0) {
55
+ for (const guard of options.guards) {
56
+ const result = await guard(event);
57
+ if (result instanceof Response) {
58
+ return result;
59
+ }
60
+ if (result && typeof result === 'object' && 'status' in result && 'title' in result) {
61
+ return problemResponse(result);
62
+ }
63
+ if (result === false) {
64
+ return problemResponse({
65
+ status: 403,
66
+ title: 'Forbidden',
67
+ detail: 'Access denied by endpoint guard.'
68
+ });
69
+ }
70
+ }
71
+ }
72
+ // 2. Resolve target channel
73
+ let channel = 'default';
74
+ if (typeof options.channel === 'function') {
75
+ channel = await options.channel(event);
76
+ }
77
+ else if (typeof options.channel === 'string' && options.channel.length > 0) {
78
+ channel = options.channel;
79
+ }
80
+ const user = locals?.user ?? null;
81
+ const orgId = locals?.orgId ?? locals?.org?.id ?? null;
82
+ const connectionId = `sse_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
83
+ const encoder = new TextEncoder();
84
+ let unsubscribe = null;
85
+ let pingTimer = null;
86
+ let isClosed = false;
87
+ let clientCloseFn = null;
88
+ const stream = new ReadableStream({
89
+ start(controller) {
90
+ const safeEnqueue = (text) => {
91
+ if (isClosed)
92
+ return false;
93
+ try {
94
+ controller.enqueue(encoder.encode(text));
95
+ return true;
96
+ }
97
+ catch {
98
+ isClosed = true;
99
+ return false;
100
+ }
101
+ };
102
+ const clientSend = (eventOrMessage, data) => {
103
+ if (isClosed)
104
+ return false;
105
+ let msg;
106
+ if (typeof eventOrMessage === 'string') {
107
+ msg = { event: eventOrMessage, data: data };
108
+ }
109
+ else {
110
+ msg = eventOrMessage;
111
+ }
112
+ return safeEnqueue(formatSSEFrame(msg));
113
+ };
114
+ const clientClose = () => {
115
+ if (isClosed)
116
+ return;
117
+ isClosed = true;
118
+ if (pingTimer) {
119
+ clearInterval(pingTimer);
120
+ pingTimer = null;
121
+ }
122
+ if (unsubscribe) {
123
+ unsubscribe();
124
+ unsubscribe = null;
125
+ }
126
+ try {
127
+ controller.close();
128
+ }
129
+ catch {
130
+ // Stream might already be closed by client
131
+ }
132
+ if (options.onDisconnect) {
133
+ Promise.resolve(options.onDisconnect({
134
+ connectionId,
135
+ channel,
136
+ event,
137
+ user,
138
+ orgId
139
+ })).catch((err) => {
140
+ console.error('[Yaxa SSE onDisconnect Error]:', err);
141
+ });
142
+ }
143
+ };
144
+ clientCloseFn = clientClose;
145
+ // Subscribe client to channel broadcast events
146
+ unsubscribe = broadcaster.subscribe(channel, (msg) => {
147
+ clientSend({
148
+ event: msg.event,
149
+ data: msg.payload,
150
+ id: msg.id
151
+ });
152
+ });
153
+ // Heartbeat loop to keep connections open through proxies/firewalls
154
+ if (keepAliveIntervalMs > 0) {
155
+ pingTimer = setInterval(() => {
156
+ safeEnqueue(keepAliveComment);
157
+ }, keepAliveIntervalMs);
158
+ }
159
+ // Abort handling when client closes connection or navigates away
160
+ if (request.signal) {
161
+ request.signal.addEventListener('abort', clientClose, { once: true });
162
+ }
163
+ // Context provided to onConnect
164
+ const clientContext = {
165
+ connectionId,
166
+ channel,
167
+ event,
168
+ user,
169
+ orgId,
170
+ send: clientSend,
171
+ close: clientClose
172
+ };
173
+ // Invoke onConnect callback
174
+ if (options.onConnect) {
175
+ Promise.resolve(options.onConnect(clientContext)).catch((err) => {
176
+ console.error('[Yaxa SSE onConnect Error]:', err);
177
+ });
178
+ }
179
+ },
180
+ cancel() {
181
+ if (clientCloseFn) {
182
+ clientCloseFn();
183
+ }
184
+ else {
185
+ isClosed = true;
186
+ if (pingTimer) {
187
+ clearInterval(pingTimer);
188
+ pingTimer = null;
189
+ }
190
+ if (unsubscribe) {
191
+ unsubscribe();
192
+ unsubscribe = null;
193
+ }
194
+ }
195
+ }
196
+ });
197
+ return new Response(stream, {
198
+ status: 200,
199
+ headers: {
200
+ 'content-type': 'text/event-stream; charset=utf-8',
201
+ 'cache-control': 'no-cache, no-transform',
202
+ connection: 'keep-alive',
203
+ 'x-accel-buffering': 'no'
204
+ }
205
+ });
206
+ };
207
+ sseHandler._yaxaConfig = {
208
+ summary: options.summary || 'Server-Sent Events Stream',
209
+ description: options.description ||
210
+ 'Real-time Server-Sent Events stream with multi-channel pub/sub and keep-alive heartbeats.',
211
+ guards: options.guards,
212
+ status: 200,
213
+ responses: {
214
+ '200': {
215
+ description: 'Active event stream',
216
+ content: {
217
+ 'text/event-stream': {
218
+ schema: { type: 'string' }
219
+ }
220
+ }
221
+ }
222
+ }
223
+ };
224
+ return sseHandler;
225
+ }
@@ -0,0 +1,116 @@
1
+ import type { RequestEvent } from '@sveltejs/kit';
2
+ import type { Guard } from '../endpoint/types';
3
+ /**
4
+ * Structured Server-Sent Event message payload.
5
+ */
6
+ export interface SSEMessage<T = unknown> {
7
+ /** Optional unique event ID for client Last-Event-ID tracking */
8
+ id?: string;
9
+ /** Optional named event type (defaults to 'message') */
10
+ event?: string;
11
+ /** Payload data to transmit. Objects/arrays are JSON-encoded automatically */
12
+ data: T;
13
+ /** Optional reconnection delay in milliseconds suggested to the client */
14
+ retry?: number;
15
+ }
16
+ /**
17
+ * Context provided to onConnect and event callbacks.
18
+ */
19
+ export interface SSEClientContext {
20
+ /** Unique connection ID */
21
+ connectionId: string;
22
+ /** Active channel name */
23
+ channel: string;
24
+ /** The incoming SvelteKit RequestEvent */
25
+ event: RequestEvent;
26
+ /** Resolved user from guards/locals */
27
+ user?: any;
28
+ /** Active organization ID from guards/locals */
29
+ orgId?: string | null;
30
+ /** Send an SSE message directly to this client */
31
+ send: <T = unknown>(eventOrMessage: string | SSEMessage<T>, data?: T) => boolean;
32
+ /** Explicitly close this SSE connection */
33
+ close: () => void;
34
+ }
35
+ /**
36
+ * Configuration options for yaxaSSE endpoint handler.
37
+ */
38
+ export interface YaxaSSEOptions {
39
+ /** Summary for OpenAPI documentation */
40
+ summary?: string;
41
+ /** Description for OpenAPI documentation */
42
+ description?: string;
43
+ /**
44
+ * Declarative guards executed before establishing the SSE connection.
45
+ * If a guard fails, an RFC 7807 Problem Details response (e.g. 401, 403, 429) is returned.
46
+ */
47
+ guards?: Guard[];
48
+ /**
49
+ * Channel name to subscribe this client to. Can be a static string or a dynamic function
50
+ * of the incoming RequestEvent (e.g. `(event) => \`org:\${event.locals.orgId}\``).
51
+ * Defaults to `'default'`.
52
+ */
53
+ channel?: string | ((event: RequestEvent) => string | Promise<string>);
54
+ /**
55
+ * Keep-alive ping interval in milliseconds to prevent reverse proxies, firewalls,
56
+ * and serverless platforms (e.g. Vercel) from terminating idle connections.
57
+ * Set to 0 to disable. Defaults to 15,000ms (15s).
58
+ */
59
+ keepAliveIntervalMs?: number;
60
+ /**
61
+ * Custom comment or payload for keep-alive pings.
62
+ * Defaults to `: ping\n\n`.
63
+ */
64
+ keepAliveComment?: string;
65
+ /**
66
+ * Hook called immediately after connection establishment and headers transmission.
67
+ */
68
+ onConnect?: (ctx: SSEClientContext) => void | Promise<void>;
69
+ /**
70
+ * Hook called when the client disconnects or the request is aborted.
71
+ */
72
+ onDisconnect?: (ctx: Omit<SSEClientContext, 'send' | 'close'>) => void | Promise<void>;
73
+ /**
74
+ * Custom broadcaster instance. If omitted, uses the global broadcaster singleton.
75
+ */
76
+ broadcaster?: Broadcaster;
77
+ }
78
+ /**
79
+ * Broadcast message contract.
80
+ */
81
+ export interface BroadcastMessage<T = unknown> {
82
+ channel: string;
83
+ event: string;
84
+ payload: T;
85
+ id?: string;
86
+ }
87
+ /**
88
+ * Broadcaster subscription callback.
89
+ */
90
+ export type BroadcastSubscriber<T = unknown> = (msg: BroadcastMessage<T>) => void;
91
+ /**
92
+ * Broadcaster adapter interface.
93
+ * Can be implemented by MemoryBroadcaster, RedisBroadcaster, ValkeyBroadcaster, etc.
94
+ */
95
+ export interface Broadcaster {
96
+ /**
97
+ * Publish an event to all subscribers listening on the specified channel.
98
+ */
99
+ publish: <T = unknown>(channel: string, event: string, payload: T, id?: string) => Promise<void> | void;
100
+ /**
101
+ * Subscribe to events on a specific channel. Returns an unsubscribe function.
102
+ */
103
+ subscribe: <T = unknown>(channel: string, subscriber: BroadcastSubscriber<T>) => () => void;
104
+ /**
105
+ * Unsubscribe a callback from a channel.
106
+ */
107
+ unsubscribe: <T = unknown>(channel: string, subscriber: BroadcastSubscriber<T>) => void;
108
+ /**
109
+ * Returns current number of active subscribers on a channel (optional).
110
+ */
111
+ subscriberCount?: (channel: string) => number;
112
+ /**
113
+ * Remove all subscribers across all channels.
114
+ */
115
+ clear?: () => void;
116
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -85,6 +85,8 @@ export interface MockRequestEventOptions {
85
85
  params?: Record<string, string>;
86
86
  locals?: Record<string, any>;
87
87
  cookies?: Record<string, string>;
88
+ signal?: AbortSignal;
89
+ request?: Request;
88
90
  }
89
91
  /**
90
92
  * Creates a mocked typed SvelteKit RequestEvent for unit testing endpoints and server hooks.
@@ -124,11 +124,13 @@ export function createMockRequestEvent(options = {}) {
124
124
  }
125
125
  }
126
126
  }
127
- const request = new Request(url.toString(), {
128
- method,
129
- headers,
130
- body: method !== 'GET' && method !== 'HEAD' ? bodyContent : undefined
131
- });
127
+ const request = options.request ||
128
+ new Request(url.toString(), {
129
+ method,
130
+ headers,
131
+ body: method !== 'GET' && method !== 'HEAD' ? bodyContent : undefined,
132
+ signal: options.signal
133
+ });
132
134
  const cookieStore = new Map(Object.entries(options.cookies || {}));
133
135
  const cookies = {
134
136
  get: (name) => cookieStore.get(name),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaxa-svelte",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "The Intuitive Svelte UI Library — Nuxt UI v4 equivalent for SvelteKit 2 & 3 & Svelte 5 with automated SEO, dynamic OpenGraph cards, sitemaps, robots.txt, and Tailwind CSS v4.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://yaxa.vercel.app",