yaxa-svelte 2.4.0 → 2.6.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.
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Standard RFC 7807 Problem Details representation.
3
+ * @see https://datatracker.ietf.org/doc/html/rfc7807
4
+ */
5
+ export interface ProblemDetails {
6
+ type: string;
7
+ title: string;
8
+ status: number;
9
+ detail?: string;
10
+ instance?: string;
11
+ invalidParams?: Array<{
12
+ name: string;
13
+ reason: string;
14
+ }>;
15
+ [key: string]: unknown;
16
+ }
17
+ /**
18
+ * Standard Schema or validator representation.
19
+ */
20
+ export type AnyValidator<T = any> = {
21
+ '~standard': {
22
+ validate: (value: unknown) => Promise<{
23
+ value?: T;
24
+ issues?: ReadonlyArray<any>;
25
+ }> | {
26
+ value?: T;
27
+ issues?: ReadonlyArray<any>;
28
+ };
29
+ };
30
+ } | {
31
+ safeParse: (value: unknown) => {
32
+ success: true;
33
+ data: T;
34
+ } | {
35
+ success: false;
36
+ error: any;
37
+ };
38
+ } | {
39
+ parse: (value: unknown) => T;
40
+ } | ((value: unknown) => {
41
+ success: boolean;
42
+ data?: T;
43
+ error?: any;
44
+ });
45
+ /**
46
+ * Infer output type from a schema validator.
47
+ */
48
+ export type InferValidatorOutput<V> = V extends {
49
+ '~standard': {
50
+ validate: (...args: any[]) => Promise<{
51
+ value?: infer T;
52
+ }> | {
53
+ value?: infer T;
54
+ };
55
+ };
56
+ } ? T : V extends {
57
+ safeParse: (...args: any[]) => {
58
+ success: true;
59
+ data: infer T;
60
+ } | {
61
+ success: false;
62
+ error: any;
63
+ };
64
+ } ? T : V extends {
65
+ parse: (...args: any[]) => infer T;
66
+ } ? T : V extends (value: unknown) => {
67
+ success: true;
68
+ data: infer T;
69
+ } ? T : any;
70
+ /**
71
+ * Infer request payload and response types from a yaxaEndpoint handler or config.
72
+ */
73
+ export type InferEndpoint<T> = T extends {
74
+ _yaxaConfig: {
75
+ schema?: {
76
+ body?: infer B;
77
+ query?: infer Q;
78
+ params?: infer P;
79
+ headers?: infer H;
80
+ };
81
+ handle: (...args: any[]) => infer R;
82
+ };
83
+ } ? {
84
+ body: [B] extends [never] ? undefined : InferValidatorOutput<B>;
85
+ query: [Q] extends [never] ? undefined : InferValidatorOutput<Q>;
86
+ params: [P] extends [never] ? undefined : InferValidatorOutput<P>;
87
+ headers: [H] extends [never] ? undefined : InferValidatorOutput<H>;
88
+ response: Awaited<R> extends Response ? unknown : Awaited<R>;
89
+ } : T extends (...args: any[]) => Promise<infer R> | infer R ? {
90
+ body: any;
91
+ query: any;
92
+ params: any;
93
+ headers: any;
94
+ response: Awaited<R> extends Response ? unknown : Awaited<R>;
95
+ } : {
96
+ body: any;
97
+ query: any;
98
+ params: any;
99
+ headers: any;
100
+ response: any;
101
+ };
102
+ /**
103
+ * Strong discriminated union result returned by apiClient calls.
104
+ */
105
+ export type ClientResponse<TData> = {
106
+ data: TData;
107
+ error: null;
108
+ response: Response;
109
+ status: number;
110
+ ok: true;
111
+ } | {
112
+ data: null;
113
+ error: ProblemDetails;
114
+ response: Response;
115
+ status: number;
116
+ ok: false;
117
+ };
118
+ /**
119
+ * Client request options passed to an endpoint method.
120
+ */
121
+ export interface ClientRequestOptions<TBody = unknown, TQuery = Record<string, any>, TParams = Record<string, any>> {
122
+ body?: TBody;
123
+ query?: TQuery;
124
+ params?: TParams;
125
+ headers?: HeadersInit;
126
+ signal?: AbortSignal;
127
+ fetch?: typeof fetch;
128
+ }
129
+ /**
130
+ * Helper to determine if an option object is required based on schema properties.
131
+ */
132
+ export type RequiredKeys<T> = {
133
+ [K in keyof T]-?: Record<string, never> extends Pick<T, K> ? never : K;
134
+ }[keyof T];
135
+ export type ClientMethodFn<TEndpoint> = TEndpoint extends never ? (options?: ClientRequestOptions) => Promise<ClientResponse<any>> : InferEndpoint<TEndpoint> extends infer Inferred ? Inferred extends {
136
+ body: infer B;
137
+ query: infer Q;
138
+ params: infer P;
139
+ response: infer R;
140
+ } ? (options?: {
141
+ body?: B;
142
+ query?: Q;
143
+ params?: P;
144
+ headers?: HeadersInit;
145
+ signal?: AbortSignal;
146
+ fetch?: typeof fetch;
147
+ }) => Promise<ClientResponse<R>> : (options?: ClientRequestOptions) => Promise<ClientResponse<any>> : (options?: ClientRequestOptions) => Promise<ClientResponse<any>>;
148
+ /**
149
+ * HTTP methods supported by a route definition.
150
+ */
151
+ export type RouteMethods<TRoute> = {
152
+ get: TRoute extends {
153
+ GET: infer H;
154
+ } ? ClientMethodFn<H> : (options?: ClientRequestOptions) => Promise<ClientResponse<any>>;
155
+ post: TRoute extends {
156
+ POST: infer H;
157
+ } ? ClientMethodFn<H> : (options?: ClientRequestOptions) => Promise<ClientResponse<any>>;
158
+ put: TRoute extends {
159
+ PUT: infer H;
160
+ } ? ClientMethodFn<H> : (options?: ClientRequestOptions) => Promise<ClientResponse<any>>;
161
+ delete: TRoute extends {
162
+ DELETE: infer H;
163
+ } ? ClientMethodFn<H> : (options?: ClientRequestOptions) => Promise<ClientResponse<any>>;
164
+ patch: TRoute extends {
165
+ PATCH: infer H;
166
+ } ? ClientMethodFn<H> : (options?: ClientRequestOptions) => Promise<ClientResponse<any>>;
167
+ };
168
+ /**
169
+ * Callable route proxy supporting path parameter interpolation.
170
+ * e.g. api['/api/projects/[id]']({ id: '123' }).get()
171
+ */
172
+ export type RouteProxy<TRoute> = RouteMethods<TRoute> & {
173
+ (params: Record<string, any>): RouteMethods<TRoute>;
174
+ };
175
+ /**
176
+ * Global configuration options for creating an API client.
177
+ */
178
+ export interface ApiClientConfig {
179
+ /**
180
+ * Base URL prefix for all requests (e.g. 'https://api.example.com' or '').
181
+ * Defaults to empty string (relative to current origin).
182
+ */
183
+ baseUrl?: string;
184
+ /**
185
+ * Custom fetch implementation (e.g. SvelteKit's event.fetch in load functions).
186
+ * Defaults to globalThis.fetch.
187
+ */
188
+ fetch?: typeof fetch;
189
+ /**
190
+ * Default headers to attach to every request.
191
+ */
192
+ headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
193
+ /**
194
+ * Interceptor hook called before every request.
195
+ */
196
+ onRequest?: (context: {
197
+ url: string;
198
+ options: RequestInit;
199
+ }) => void | Promise<void>;
200
+ /**
201
+ * Interceptor hook called on every response.
202
+ */
203
+ onResponse?: (context: {
204
+ response: Response;
205
+ data: any;
206
+ error: ProblemDetails | null;
207
+ }) => void | Promise<void>;
208
+ }
209
+ /**
210
+ * Dynamic Proxy type mapping paths to methods.
211
+ */
212
+ export type ApiClient<TRoutes = Record<string, any>> = {
213
+ [K in keyof TRoutes]: RouteProxy<TRoutes[K]>;
214
+ } & {
215
+ [path: string]: RouteProxy<any>;
216
+ } & {
217
+ get: <TData = any>(path: string, options?: ClientRequestOptions) => Promise<ClientResponse<TData>>;
218
+ post: <TData = any>(path: string, options?: ClientRequestOptions) => Promise<ClientResponse<TData>>;
219
+ put: <TData = any>(path: string, options?: ClientRequestOptions) => Promise<ClientResponse<TData>>;
220
+ delete: <TData = any>(path: string, options?: ClientRequestOptions) => Promise<ClientResponse<TData>>;
221
+ patch: <TData = any>(path: string, options?: ClientRequestOptions) => Promise<ClientResponse<TData>>;
222
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ export type SSEStatus = 'idle' | 'connecting' | 'connected' | 'error' | 'closed';
2
+ export interface UseSSEOptions {
3
+ /** Whether the SSE connection is active (defaults to true) */
4
+ enabled?: boolean;
5
+ /** Whether to send credentials/cookies with cross-origin requests */
6
+ withCredentials?: boolean;
7
+ /** Enable automatic reconnection on disconnect/error (defaults to true) */
8
+ autoReconnect?: boolean;
9
+ /** Maximum reconnection attempts before stopping (defaults to 10) */
10
+ maxRetries?: number;
11
+ /** Base reconnect delay in ms before exponential backoff (defaults to 1,000ms) */
12
+ baseDelayMs?: number;
13
+ /** Maximum delay between reconnect attempts in ms (defaults to 30,000ms) */
14
+ maxDelayMs?: number;
15
+ /** Callback invoked when the connection opens */
16
+ onOpen?: () => void;
17
+ /** Callback invoked on error */
18
+ onError?: (err: Event) => void;
19
+ /** Callback invoked when any message is received */
20
+ onMessage?: (data: any, event: MessageEvent) => void;
21
+ }
22
+ export type SSEEventHandler<T = any> = (data: T, event: MessageEvent) => void;
23
+ export interface SSEResult {
24
+ readonly status: SSEStatus;
25
+ readonly connected: boolean;
26
+ readonly connecting: boolean;
27
+ readonly data: any;
28
+ readonly lastEvent: string | null;
29
+ readonly error: any;
30
+ on: <T = any>(event: string, handler: SSEEventHandler<T>) => () => void;
31
+ close: () => void;
32
+ reconnect: () => void;
33
+ }
34
+ /**
35
+ * Idiomatic Svelte 5 Composable Rune for Server-Sent Events (SSE).
36
+ * Provides automatic reconnect with jitter, typed event subscribers, and automatic teardown.
37
+ *
38
+ * @example
39
+ * ```svelte
40
+ * <script lang="ts">
41
+ * import { useSSE } from 'yaxa-svelte';
42
+ *
43
+ * const stream = useSSE('/api/events');
44
+ *
45
+ * stream.on('order.created', (order) => {
46
+ * console.log('New order received:', order);
47
+ * });
48
+ * </script>
49
+ *
50
+ * <p>Status: {stream.connected ? 'Live' : 'Connecting...'}</p>
51
+ * ```
52
+ */
53
+ export declare function useSSE(url: string | (() => string), options?: UseSSEOptions): SSEResult;
@@ -0,0 +1,209 @@
1
+ import { SvelteMap, SvelteSet } from 'svelte/reactivity';
2
+ /**
3
+ * Idiomatic Svelte 5 Composable Rune for Server-Sent Events (SSE).
4
+ * Provides automatic reconnect with jitter, typed event subscribers, and automatic teardown.
5
+ *
6
+ * @example
7
+ * ```svelte
8
+ * <script lang="ts">
9
+ * import { useSSE } from 'yaxa-svelte';
10
+ *
11
+ * const stream = useSSE('/api/events');
12
+ *
13
+ * stream.on('order.created', (order) => {
14
+ * console.log('New order received:', order);
15
+ * });
16
+ * </script>
17
+ *
18
+ * <p>Status: {stream.connected ? 'Live' : 'Connecting...'}</p>
19
+ * ```
20
+ */
21
+ export function useSSE(url, options = {}) {
22
+ const { enabled = true, withCredentials = false, autoReconnect = true, maxRetries = 10, baseDelayMs = 1000, maxDelayMs = 30000, onOpen, onError, onMessage } = options;
23
+ let status = $state('idle');
24
+ let data = $state(null);
25
+ let lastEvent = $state(null);
26
+ let error = $state(null);
27
+ const connected = $derived(status === 'connected');
28
+ const connecting = $derived(status === 'connecting');
29
+ const listeners = new SvelteMap();
30
+ let es = null;
31
+ let reconnectTimer = null;
32
+ let retryCount = 0;
33
+ let manualClose = false;
34
+ function parseEventData(raw) {
35
+ try {
36
+ return JSON.parse(raw);
37
+ }
38
+ catch {
39
+ return raw;
40
+ }
41
+ }
42
+ function dispatchToListeners(eventType, parsed, rawEvent) {
43
+ const handlers = listeners.get(eventType);
44
+ if (handlers) {
45
+ for (const handler of handlers) {
46
+ try {
47
+ handler(parsed, rawEvent);
48
+ }
49
+ catch (err) {
50
+ console.error(`[useSSE Handler Error] Event '${eventType}':`, err);
51
+ }
52
+ }
53
+ }
54
+ }
55
+ function cleanupConnection() {
56
+ if (reconnectTimer) {
57
+ clearTimeout(reconnectTimer);
58
+ reconnectTimer = null;
59
+ }
60
+ if (es) {
61
+ es.close();
62
+ es = null;
63
+ }
64
+ }
65
+ function scheduleReconnect(targetUrl) {
66
+ if (manualClose || !autoReconnect || retryCount >= maxRetries) {
67
+ status = 'closed';
68
+ return;
69
+ }
70
+ // Exponential backoff with jitter
71
+ const delay = Math.min(maxDelayMs, baseDelayMs * Math.pow(1.5, retryCount) + Math.random() * 500);
72
+ retryCount++;
73
+ status = 'connecting';
74
+ reconnectTimer = setTimeout(() => {
75
+ connect(targetUrl);
76
+ }, delay);
77
+ }
78
+ function connect(targetUrl) {
79
+ cleanupConnection();
80
+ if (typeof window === 'undefined' || typeof EventSource === 'undefined') {
81
+ status = 'idle';
82
+ return;
83
+ }
84
+ status = 'connecting';
85
+ manualClose = false;
86
+ try {
87
+ const source = new EventSource(targetUrl, { withCredentials });
88
+ es = source;
89
+ source.onopen = () => {
90
+ status = 'connected';
91
+ retryCount = 0;
92
+ error = null;
93
+ onOpen?.();
94
+ };
95
+ source.onmessage = (event) => {
96
+ const parsed = parseEventData(event.data);
97
+ data = parsed;
98
+ lastEvent = 'message';
99
+ onMessage?.(parsed, event);
100
+ dispatchToListeners('message', parsed, event);
101
+ };
102
+ source.onerror = (err) => {
103
+ status = 'error';
104
+ error = err;
105
+ onError?.(err);
106
+ cleanupConnection();
107
+ scheduleReconnect(targetUrl);
108
+ };
109
+ // Re-attach custom event listeners
110
+ for (const [eventName] of listeners) {
111
+ if (eventName !== 'message') {
112
+ source.addEventListener(eventName, (event) => {
113
+ const parsed = parseEventData(event.data);
114
+ data = parsed;
115
+ lastEvent = eventName;
116
+ dispatchToListeners(eventName, parsed, event);
117
+ });
118
+ }
119
+ }
120
+ }
121
+ catch (err) {
122
+ status = 'error';
123
+ error = err;
124
+ scheduleReconnect(targetUrl);
125
+ }
126
+ }
127
+ function close() {
128
+ manualClose = true;
129
+ status = 'closed';
130
+ cleanupConnection();
131
+ }
132
+ function reconnect() {
133
+ manualClose = false;
134
+ retryCount = 0;
135
+ const targetUrl = typeof url === 'function' ? url() : url;
136
+ connect(targetUrl);
137
+ }
138
+ function on(event, handler) {
139
+ let handlers = listeners.get(event);
140
+ if (!handlers) {
141
+ handlers = new SvelteSet();
142
+ listeners.set(event, handlers);
143
+ // If EventSource is currently connected, register native listener for named event
144
+ if (es && event !== 'message') {
145
+ es.addEventListener(event, (evt) => {
146
+ const parsed = parseEventData(evt.data);
147
+ data = parsed;
148
+ lastEvent = event;
149
+ dispatchToListeners(event, parsed, evt);
150
+ });
151
+ }
152
+ }
153
+ handlers.add(handler);
154
+ return () => {
155
+ handlers?.delete(handler);
156
+ if (handlers?.size === 0) {
157
+ listeners.delete(event);
158
+ }
159
+ };
160
+ }
161
+ // Svelte 5 effect for automated lifecycle management if called inside a component/effect root
162
+ try {
163
+ $effect(() => {
164
+ const targetUrl = typeof url === 'function' ? url() : url;
165
+ if (enabled && targetUrl) {
166
+ connect(targetUrl);
167
+ }
168
+ else {
169
+ close();
170
+ }
171
+ return () => {
172
+ manualClose = true;
173
+ cleanupConnection();
174
+ };
175
+ });
176
+ }
177
+ catch {
178
+ // When called outside an effect root (e.g. In unit tests), connect directly
179
+ if (enabled) {
180
+ const targetUrl = typeof url === 'function' ? url() : url;
181
+ if (targetUrl) {
182
+ connect(targetUrl);
183
+ }
184
+ }
185
+ }
186
+ return {
187
+ get status() {
188
+ return status;
189
+ },
190
+ get connected() {
191
+ return connected;
192
+ },
193
+ get connecting() {
194
+ return connecting;
195
+ },
196
+ get data() {
197
+ return data;
198
+ },
199
+ get lastEvent() {
200
+ return lastEvent;
201
+ },
202
+ get error() {
203
+ return error;
204
+ },
205
+ on,
206
+ close,
207
+ reconnect
208
+ };
209
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,122 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { useSSE } from './useSSE.svelte';
3
+ class MockEventSource {
4
+ url;
5
+ withCredentials;
6
+ onopen = null;
7
+ onmessage = null;
8
+ onerror = null;
9
+ listeners = new Map();
10
+ readyState = 0;
11
+ static instances = [];
12
+ constructor(url, init) {
13
+ this.url = url;
14
+ this.withCredentials = init?.withCredentials;
15
+ MockEventSource.instances.push(this);
16
+ }
17
+ addEventListener(type, handler) {
18
+ let handlers = this.listeners.get(type);
19
+ if (!handlers) {
20
+ handlers = new Set();
21
+ this.listeners.set(type, handlers);
22
+ }
23
+ handlers.add(handler);
24
+ }
25
+ removeEventListener(type, handler) {
26
+ this.listeners.get(type)?.delete(handler);
27
+ }
28
+ close() {
29
+ this.readyState = 2;
30
+ }
31
+ simulateOpen() {
32
+ this.readyState = 1;
33
+ this.onopen?.();
34
+ }
35
+ simulateMessage(data, type = 'message') {
36
+ const rawData = typeof data === 'object' ? JSON.stringify(data) : String(data);
37
+ const evt = { data: rawData, type };
38
+ if (type === 'message') {
39
+ this.onmessage?.(evt);
40
+ }
41
+ const handlers = this.listeners.get(type);
42
+ if (handlers) {
43
+ for (const handler of handlers) {
44
+ handler(evt);
45
+ }
46
+ }
47
+ }
48
+ simulateError(err = new Event('error')) {
49
+ this.onerror?.(err);
50
+ }
51
+ }
52
+ describe('useSSE Composable Rune', () => {
53
+ beforeEach(() => {
54
+ MockEventSource.instances = [];
55
+ vi.stubGlobal('EventSource', MockEventSource);
56
+ });
57
+ afterEach(() => {
58
+ vi.unstubAllGlobals();
59
+ vi.restoreAllMocks();
60
+ });
61
+ it('initializes with connecting or connected state when EventSource exists', () => {
62
+ const sse = useSSE('/api/events', { enabled: true });
63
+ expect(sse.status).toBe('connecting');
64
+ expect(sse.connecting).toBe(true);
65
+ expect(sse.connected).toBe(false);
66
+ expect(MockEventSource.instances.length).toBe(1);
67
+ const instance = MockEventSource.instances[0];
68
+ expect(instance.url).toBe('/api/events');
69
+ instance.simulateOpen();
70
+ expect(sse.status).toBe('connected');
71
+ expect(sse.connected).toBe(true);
72
+ expect(sse.connecting).toBe(false);
73
+ });
74
+ it('handles message events and parses JSON data reactively', () => {
75
+ const onMessage = vi.fn();
76
+ const sse = useSSE('/api/events', { onMessage });
77
+ const instance = MockEventSource.instances[0];
78
+ instance.simulateOpen();
79
+ instance.simulateMessage({ user: 'Alice', score: 100 });
80
+ expect(sse.data).toEqual({ user: 'Alice', score: 100 });
81
+ expect(sse.lastEvent).toBe('message');
82
+ expect(onMessage).toHaveBeenCalledWith({ user: 'Alice', score: 100 }, expect.any(Object));
83
+ });
84
+ it('subscribes to custom named events via .on(eventName, handler)', () => {
85
+ const sse = useSSE('/api/events');
86
+ const instance = MockEventSource.instances[0];
87
+ instance.simulateOpen();
88
+ const orderHandler = vi.fn();
89
+ const unsubscribe = sse.on('order.paid', orderHandler);
90
+ instance.simulateMessage({ orderId: 'ord_99', total: 29.5 }, 'order.paid');
91
+ expect(orderHandler).toHaveBeenCalledTimes(1);
92
+ expect(orderHandler).toHaveBeenCalledWith({ orderId: 'ord_99', total: 29.5 }, expect.any(Object));
93
+ expect(sse.lastEvent).toBe('order.paid');
94
+ // Test unsubscribe
95
+ unsubscribe();
96
+ instance.simulateMessage({ orderId: 'ord_100', total: 10 }, 'order.paid');
97
+ expect(orderHandler).toHaveBeenCalledTimes(1);
98
+ });
99
+ it('closes the connection cleanly on .close()', () => {
100
+ const sse = useSSE('/api/events');
101
+ const instance = MockEventSource.instances[0];
102
+ instance.simulateOpen();
103
+ sse.close();
104
+ expect(sse.status).toBe('closed');
105
+ expect(sse.connected).toBe(false);
106
+ expect(instance.readyState).toBe(2);
107
+ });
108
+ it('reconnects when .reconnect() is invoked', () => {
109
+ const sse = useSSE('/api/events');
110
+ expect(MockEventSource.instances.length).toBe(1);
111
+ sse.close();
112
+ expect(sse.status).toBe('closed');
113
+ sse.reconnect();
114
+ expect(MockEventSource.instances.length).toBe(2);
115
+ expect(sse.status).toBe('connecting');
116
+ });
117
+ it('stays idle when enabled is false', () => {
118
+ const sse = useSSE('/api/events', { enabled: false });
119
+ expect(sse.status).toBe('idle');
120
+ expect(MockEventSource.instances.length).toBe(0);
121
+ });
122
+ });
package/dist/index.d.ts CHANGED
@@ -156,6 +156,8 @@ export { useFormAction } from './composables/useFormAction.svelte';
156
156
  export type { UseFormActionOptions } from './composables/useFormAction.svelte';
157
157
  export { useIdle } from './composables/useIdle.svelte';
158
158
  export type { UseIdleOptions } from './composables/useIdle.svelte';
159
+ export { useSSE } from './composables/useSSE.svelte';
160
+ export type { SSEStatus, UseSSEOptions, SSEEventHandler, SSEResult } from './composables/useSSE.svelte';
159
161
  export { default as ThemeStudio } from './components/theme/ThemeStudio.svelte';
160
162
  export type { ThemeStudioProps } from './components/theme/ThemeStudio.svelte';
161
163
  export { theme, themeInitScript, ACCENT_PALETTES, NEUTRAL_PALETTES, FONT_PRESETS, RADIUS_PRESETS, FONT_SIZE_PRESETS, DEFAULT_CHART_COLORS } from './theme/theme.svelte';
@@ -205,5 +207,7 @@ export { default as FeatureGrid, featureGridVariants } from './components/blocks
205
207
  export type { FeatureGridProps, FeatureItem } from './components/blocks/FeatureGrid.svelte';
206
208
  export { createMockSiteConfig, createMockUser, createTestWrapperProps } from './testing';
207
209
  export type { YaxaTestOptions } from './testing';
210
+ export { createApiClient, interpolatePath, serializeQueryParams } from './client';
211
+ export type { ApiClient, ApiClientConfig, ClientRequestOptions, ClientResponse, InferEndpoint, RouteMethods, ProblemDetails as ClientProblemDetails } from './client';
208
212
  export { cn, tv } from './utils/cn';
209
213
  export type { VariantProps } from './utils/cn';
package/dist/index.js CHANGED
@@ -103,6 +103,7 @@ export { Sortable, useSortable } from './composables/useSortable.svelte';
103
103
  export { useDataTable } from './composables/useDataTable.svelte';
104
104
  export { useFormAction } from './composables/useFormAction.svelte';
105
105
  export { useIdle } from './composables/useIdle.svelte';
106
+ export { useSSE } from './composables/useSSE.svelte';
106
107
  // Theming & Theme Studio
107
108
  export { default as ThemeStudio } from './components/theme/ThemeStudio.svelte';
108
109
  export { theme, themeInitScript, ACCENT_PALETTES, NEUTRAL_PALETTES, FONT_PRESETS, RADIUS_PRESETS, FONT_SIZE_PRESETS, DEFAULT_CHART_COLORS } from './theme/theme.svelte';
@@ -137,5 +138,7 @@ export { default as FAQ, faqVariants } from './components/blocks/FAQ.svelte';
137
138
  export { default as FeatureGrid, featureGridVariants } from './components/blocks/FeatureGrid.svelte';
138
139
  // Testing Utilities
139
140
  export { createMockSiteConfig, createMockUser, createTestWrapperProps } from './testing';
141
+ // Type-Safe Client RPC
142
+ export { createApiClient, interpolatePath, serializeQueryParams } from './client';
140
143
  // Utilities
141
144
  export { cn, tv } from './utils/cn';
@@ -28,3 +28,5 @@ export { generateOpenApiSpec, createApiDocsHandler, createApiRegistry, globalApi
28
28
  export type { OpenApiSpec31, OpenApiOperation, OpenApiParameter, OpenApiRequestBody, OpenApiResponse, GenerateOpenApiOptions, ApiDocsOptions, ApiDocsTheme, EndpointLike, EndpointMap } from './openapi/index';
29
29
  export { enqueueJob, defineWorker, createJobRunner, eventBus, EventBus, parseJobDelay } from './jobs/index';
30
30
  export type { JobRecord, JobDelay, EnqueueJobOptions, WorkerContext, WorkerHandler, WorkerDefinition, JobRunnerOptions, JobBatchResult, JobRunner, EventHandler, EventSubscription, IEventBus } from './jobs/index';
31
+ export { yaxaSSE, formatSSEFrame, broadcast, createBroadcaster, globalBroadcaster, MemoryBroadcaster } from './realtime/index';
32
+ export type { SSEMessage, SSEClientContext, YaxaSSEOptions, BroadcastMessage, BroadcastSubscriber, Broadcaster } from './realtime/index';
@@ -24,3 +24,5 @@ export { yaxaEndpoint, createProblemDetails, problemResponse, isProblemDetails,
24
24
  export { generateOpenApiSpec, createApiDocsHandler, createApiRegistry, globalApiRegistry, convertToJsonSchema } from './openapi/index';
25
25
  // Embedded Background Workflows, Job Queue & Domain Events (Milestone 6)
26
26
  export { enqueueJob, defineWorker, createJobRunner, eventBus, EventBus, parseJobDelay } from './jobs/index';
27
+ // Adapter-Agnostic Real-Time Abstractions & Server-Sent Events (Milestone 5)
28
+ export { yaxaSSE, formatSSEFrame, broadcast, createBroadcaster, globalBroadcaster, MemoryBroadcaster } from './realtime/index';
@@ -78,10 +78,12 @@ function renderRedocHtml(specJson, title) {
78
78
  * page (Scalar, Swagger UI, or Redoc) or returns raw OpenAPI 3.1 JSON when ?format=json is requested.
79
79
  */
80
80
  export function createApiDocsHandler(options = {}) {
81
- const theme = options.theme || 'scalar';
81
+ const defaultTheme = options.theme || 'scalar';
82
82
  return async (event) => {
83
83
  const { url, request } = event;
84
84
  const format = url.searchParams.get('format');
85
+ const themeQuery = url.searchParams.get('theme');
86
+ const activeTheme = themeQuery && ['scalar', 'swagger', 'redoc'].includes(themeQuery) ? themeQuery : defaultTheme;
85
87
  const acceptHeader = request.headers.get('accept') || '';
86
88
  const spec = generateOpenApiSpec(options);
87
89
  // If caller requests JSON via query param, accept header, or path ending in .json
@@ -98,10 +100,10 @@ export function createApiDocsHandler(options = {}) {
98
100
  // Render Interactive HTML Docs
99
101
  const specJson = JSON.stringify(spec);
100
102
  let html;
101
- if (theme === 'swagger') {
103
+ if (activeTheme === 'swagger') {
102
104
  html = renderSwaggerHtml(specJson, spec.info.title);
103
105
  }
104
- else if (theme === 'redoc') {
106
+ else if (activeTheme === 'redoc') {
105
107
  html = renderRedocHtml(specJson, spec.info.title);
106
108
  }
107
109
  else {