litestar-vite-plugin 0.27.0 → 0.28.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,76 @@
1
+ /**
2
+ * Resilient WebSocket and SSE helpers for Litestar event streams.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { createEventStream } from "litestar-vite-plugin/helpers"
7
+ *
8
+ * const stream = createEventStream({
9
+ * url: "/events",
10
+ * onEvent: (event) => console.log(event),
11
+ * })
12
+ *
13
+ * stream.connect()
14
+ * ```
15
+ *
16
+ * @module
17
+ */
18
+ export interface StreamGap {
19
+ stream: string;
20
+ from: number;
21
+ to: number;
22
+ missing: number;
23
+ }
24
+ export type EventStreamTransport = "websocket" | "sse";
25
+ export type StreamUrl = string | URL | (() => string | URL);
26
+ export interface EventStreamConfig<TFrame = unknown> {
27
+ transport?: "websocket" | "sse";
28
+ sseEvents?: readonly string[];
29
+ onEvent: (frame: TFrame) => void;
30
+ onOpen?: (url: string) => void;
31
+ onClose?: () => void;
32
+ onHealthChange?: (healthy: boolean) => void;
33
+ onReconnect?: () => void;
34
+ onGap?: (gap: StreamGap) => void;
35
+ shouldReconnect?: (closeCode: number) => boolean;
36
+ isHeartbeat?: (frame: TFrame) => boolean;
37
+ getEventKey?: (frame: TFrame) => string | undefined;
38
+ getSequence?: (frame: TFrame) => {
39
+ stream: string;
40
+ value: number;
41
+ } | undefined;
42
+ baseDelayMs?: number;
43
+ maxDelayMs?: number;
44
+ dedupWindow?: number;
45
+ parseFrame?: (data: string) => TFrame;
46
+ WebSocketCtor?: typeof WebSocket;
47
+ EventSourceCtor?: typeof EventSource;
48
+ }
49
+ export type EventStreamOptions<TFrame = unknown> = EventStreamConfig<TFrame> & ({
50
+ url: StreamUrl;
51
+ buildUrl?: never;
52
+ } | {
53
+ url?: never;
54
+ buildUrl: () => string | URL;
55
+ });
56
+ export interface EventStream {
57
+ connect(): void;
58
+ dispose(): void;
59
+ readonly healthy: boolean;
60
+ }
61
+ /**
62
+ * Resolve a stream endpoint against the browser origin.
63
+ *
64
+ * @param value - Absolute or same-origin relative endpoint.
65
+ * @param transport - Transport whose URL protocol should be used.
66
+ * @param baseUrl - Resolution base. Defaults to the current browser location.
67
+ * @returns An absolute transport URL.
68
+ */
69
+ export declare function resolveStreamUrl(value: string | URL, transport: EventStreamTransport, baseUrl?: string | URL): string;
70
+ /**
71
+ * Create a reconnecting, transport-agnostic Litestar event stream.
72
+ *
73
+ * @param options - Stream transport, lifecycle, and frame-processing options.
74
+ * @returns A disposable stream that connects only when `connect()` is called.
75
+ */
76
+ export declare function createEventStream<TFrame = unknown>(options: EventStreamOptions<TFrame>): EventStream;
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Resilient WebSocket and SSE helpers for Litestar event streams.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { createEventStream } from "litestar-vite-plugin/helpers"
7
+ *
8
+ * const stream = createEventStream({
9
+ * url: "/events",
10
+ * onEvent: (event) => console.log(event),
11
+ * })
12
+ *
13
+ * stream.connect()
14
+ * ```
15
+ *
16
+ * @module
17
+ */
18
+ const DEFAULT_BASE_DELAY_MS = 1000;
19
+ const DEFAULT_MAX_DELAY_MS = 10_000;
20
+ const DEFAULT_DEDUP_WINDOW = 1024;
21
+ const DEFAULT_SSE_EVENTS = ["message"];
22
+ function defaultParseFrame(data) {
23
+ try {
24
+ return JSON.parse(data);
25
+ }
26
+ catch {
27
+ return data;
28
+ }
29
+ }
30
+ /**
31
+ * Resolve a stream endpoint against the browser origin.
32
+ *
33
+ * @param value - Absolute or same-origin relative endpoint.
34
+ * @param transport - Transport whose URL protocol should be used.
35
+ * @param baseUrl - Resolution base. Defaults to the current browser location.
36
+ * @returns An absolute transport URL.
37
+ */
38
+ export function resolveStreamUrl(value, transport, baseUrl = window.location.href) {
39
+ const resolved = new URL(value, baseUrl);
40
+ if (transport === "websocket") {
41
+ if (resolved.protocol === "http:") {
42
+ resolved.protocol = "ws:";
43
+ }
44
+ else if (resolved.protocol === "https:") {
45
+ resolved.protocol = "wss:";
46
+ }
47
+ }
48
+ else if (resolved.protocol === "ws:") {
49
+ resolved.protocol = "http:";
50
+ }
51
+ else if (resolved.protocol === "wss:") {
52
+ resolved.protocol = "https:";
53
+ }
54
+ return resolved.toString();
55
+ }
56
+ /**
57
+ * Create a reconnecting, transport-agnostic Litestar event stream.
58
+ *
59
+ * @param options - Stream transport, lifecycle, and frame-processing options.
60
+ * @returns A disposable stream that connects only when `connect()` is called.
61
+ */
62
+ export function createEventStream(options) {
63
+ const { transport = "websocket", sseEvents = DEFAULT_SSE_EVENTS, onEvent, onOpen, onClose, onHealthChange, onReconnect, onGap, shouldReconnect = (closeCode) => closeCode !== 1000, isHeartbeat = () => false, getEventKey = () => undefined, getSequence = () => undefined, baseDelayMs = DEFAULT_BASE_DELAY_MS, maxDelayMs = DEFAULT_MAX_DELAY_MS, dedupWindow = DEFAULT_DEDUP_WINDOW, parseFrame = defaultParseFrame, } = options;
64
+ let connection = null;
65
+ let attempt = 0;
66
+ let timer = null;
67
+ let disposed = false;
68
+ let lastHealthy = null;
69
+ let hasOpened = false;
70
+ const seenKeys = [];
71
+ const seenKeySet = new Set();
72
+ const sequenceByStream = new Map();
73
+ function emitHealth(healthy) {
74
+ if (lastHealthy === healthy) {
75
+ return;
76
+ }
77
+ lastHealthy = healthy;
78
+ onHealthChange?.(healthy);
79
+ }
80
+ function clearTimer() {
81
+ if (timer !== null) {
82
+ clearTimeout(timer);
83
+ timer = null;
84
+ }
85
+ }
86
+ function scheduleReconnect() {
87
+ clearTimer();
88
+ attempt += 1;
89
+ const ceiling = Math.min(baseDelayMs * 2 ** (attempt - 1), maxDelayMs);
90
+ const delay = Math.random() * ceiling;
91
+ timer = setTimeout(() => {
92
+ timer = null;
93
+ if (!disposed) {
94
+ open();
95
+ }
96
+ }, delay);
97
+ }
98
+ function handleOpen(url) {
99
+ attempt = 0;
100
+ clearTimer();
101
+ emitHealth(true);
102
+ onOpen?.(url);
103
+ if (hasOpened) {
104
+ onReconnect?.();
105
+ }
106
+ else {
107
+ hasOpened = true;
108
+ }
109
+ }
110
+ function handleMessage(event) {
111
+ const frame = parseFrame(String(event.data));
112
+ if (isHeartbeat(frame)) {
113
+ return;
114
+ }
115
+ const eventKey = getEventKey(frame);
116
+ if (eventKey !== undefined && dedupWindow > 0) {
117
+ if (seenKeySet.has(eventKey)) {
118
+ return;
119
+ }
120
+ seenKeys.push(eventKey);
121
+ seenKeySet.add(eventKey);
122
+ if (seenKeys.length > dedupWindow) {
123
+ const evicted = seenKeys.shift();
124
+ if (evicted !== undefined) {
125
+ seenKeySet.delete(evicted);
126
+ }
127
+ }
128
+ }
129
+ const sequence = getSequence(frame);
130
+ if (sequence !== undefined && Number.isFinite(sequence.value)) {
131
+ const last = sequenceByStream.get(sequence.stream);
132
+ if (last === undefined) {
133
+ sequenceByStream.set(sequence.stream, sequence.value);
134
+ }
135
+ else if (sequence.value > last) {
136
+ sequenceByStream.set(sequence.stream, sequence.value);
137
+ if (sequence.value > last + 1) {
138
+ onGap?.({
139
+ stream: sequence.stream,
140
+ from: last,
141
+ to: sequence.value,
142
+ missing: sequence.value - last - 1,
143
+ });
144
+ }
145
+ }
146
+ }
147
+ onEvent(frame);
148
+ }
149
+ function buildConnectionUrl() {
150
+ const value = options.buildUrl === undefined ? options.url : options.buildUrl();
151
+ const endpoint = typeof value === "function" ? value() : value;
152
+ return resolveStreamUrl(endpoint, transport);
153
+ }
154
+ function openWebSocket() {
155
+ const WebSocketCtor = options.WebSocketCtor ?? window.WebSocket;
156
+ const url = buildConnectionUrl();
157
+ const next = new WebSocketCtor(url);
158
+ connection = next;
159
+ next.addEventListener("open", () => {
160
+ handleOpen(url);
161
+ });
162
+ next.addEventListener("message", (event) => {
163
+ handleMessage(event);
164
+ });
165
+ next.addEventListener("close", (event) => {
166
+ if (connection !== next) {
167
+ return;
168
+ }
169
+ connection = null;
170
+ onClose?.();
171
+ emitHealth(false);
172
+ if (disposed || !shouldReconnect(event.code)) {
173
+ return;
174
+ }
175
+ scheduleReconnect();
176
+ });
177
+ next.addEventListener("error", () => {
178
+ emitHealth(false);
179
+ });
180
+ }
181
+ function openEventSource() {
182
+ const EventSourceCtor = options.EventSourceCtor ?? window.EventSource;
183
+ const url = buildConnectionUrl();
184
+ const next = new EventSourceCtor(url);
185
+ connection = next;
186
+ next.addEventListener("open", () => {
187
+ handleOpen(url);
188
+ });
189
+ for (const eventType of sseEvents) {
190
+ next.addEventListener(eventType, (event) => {
191
+ handleMessage(event);
192
+ });
193
+ }
194
+ next.addEventListener("error", () => {
195
+ if (connection !== next) {
196
+ return;
197
+ }
198
+ connection = null;
199
+ next.close();
200
+ onClose?.();
201
+ emitHealth(false);
202
+ if (disposed || !shouldReconnect(1006)) {
203
+ return;
204
+ }
205
+ scheduleReconnect();
206
+ });
207
+ }
208
+ function open() {
209
+ if (disposed || typeof window === "undefined") {
210
+ return;
211
+ }
212
+ if (transport === "sse") {
213
+ openEventSource();
214
+ return;
215
+ }
216
+ openWebSocket();
217
+ }
218
+ return {
219
+ connect() {
220
+ if (disposed || typeof window === "undefined") {
221
+ return;
222
+ }
223
+ attempt = 0;
224
+ clearTimer();
225
+ open();
226
+ },
227
+ dispose() {
228
+ disposed = true;
229
+ clearTimer();
230
+ const current = connection;
231
+ connection = null;
232
+ if (current !== null) {
233
+ onClose?.();
234
+ emitHealth(false);
235
+ current.close();
236
+ }
237
+ },
238
+ get healthy() {
239
+ return lastHealthy ?? false;
240
+ },
241
+ };
242
+ }
package/dist/js/index.js CHANGED
@@ -7,6 +7,7 @@ import fullReload from "vite-plugin-full-reload";
7
7
  import { checkBackendAvailability, loadLitestarMeta } from "./litestar-meta.js";
8
8
  import { readBridgeConfig } from "./shared/bridge-schema.js";
9
9
  import { createLogger } from "./shared/logger.js";
10
+ import { installManagedShutdown } from "./shared/managed-shutdown.js";
10
11
  import { resolveLitestarPort } from "./shared/network.js";
11
12
  import { resolveDefaultSdkClientPlugin } from "./shared/typegen-core.js";
12
13
  import { createLitestarTypeGenPlugin, resolveTypesConfig } from "./shared/typegen-plugin.js";
@@ -320,6 +321,9 @@ function resolveLitestarPlugin(pluginConfig) {
320
321
  }
321
322
  });
322
323
  if (!exitHandlersBound) {
324
+ installManagedShutdown(server, () => {
325
+ shuttingDown = true;
326
+ });
323
327
  const clean = () => {
324
328
  if (pluginConfig.hotFile && fs.existsSync(pluginConfig.hotFile)) {
325
329
  fs.rmSync(pluginConfig.hotFile);
package/dist/js/nuxt.js CHANGED
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import colors from "picocolors";
4
4
  import { readBridgeConfig } from "./shared/bridge-schema.js";
5
+ import { installManagedShutdown } from "./shared/managed-shutdown.js";
5
6
  import { normalizeHost, resolveHotFilePath, resolveLitestarPort } from "./shared/network.js";
6
7
  import { createLitestarTypeGenPlugin, resolveTypesConfig } from "./shared/typegen-plugin.js";
7
8
  import { hmrServerConfig } from "./shared/vite-compat.js";
@@ -105,6 +106,7 @@ function createProxyPlugin(config) {
105
106
  };
106
107
  },
107
108
  configureServer(server) {
109
+ installManagedShutdown(server);
108
110
  if (config.verbose) {
109
111
  server.middlewares.use((req, _res, next) => {
110
112
  if (req.url?.startsWith(config.apiPrefix)) {
@@ -186,26 +188,18 @@ function litestarNuxtModule(userOptions, nuxt) {
186
188
  console.log(colors.cyan("[litestar-nuxt]"), "Nitro devProxy configured:");
187
189
  console.log(JSON.stringify(nuxt.options.nitro.devProxy, null, 2));
188
190
  }
189
- if (config.hotFile && config.devPort) {
190
- const rawHost = process.env.NUXT_HOST || process.env.HOST || "127.0.0.1";
191
- const host = normalizeHost(rawHost);
192
- const url = `http://${host}:${config.devPort}`;
193
- fs.mkdirSync(path.dirname(config.hotFile), { recursive: true });
194
- fs.writeFileSync(config.hotFile, url);
195
- if (config.verbose) {
196
- console.log(colors.cyan("[litestar-nuxt]"), colors.dim(`Hotfile written: ${config.hotFile} -> ${url}`));
197
- }
198
- }
199
191
  if (nuxt.hook && config.hotFile) {
192
+ const hotFile = config.hotFile;
200
193
  nuxt.hook("listen", (_server, listener) => {
201
194
  const info = listener;
202
- if (info && typeof info.port === "number") {
203
- const host = normalizeHost(info.host || "127.0.0.1");
204
- const url = `http://${host}:${info.port}`;
205
- fs.mkdirSync(path.dirname(config.hotFile), { recursive: true });
206
- fs.writeFileSync(config.hotFile, url);
195
+ const port = info?.address?.port ?? info?.port;
196
+ if (typeof port === "number") {
197
+ const host = normalizeHost(info.address?.address || info.host || "127.0.0.1");
198
+ const url = `http://${host}:${port}`;
199
+ fs.mkdirSync(path.dirname(hotFile), { recursive: true });
200
+ fs.writeFileSync(hotFile, url);
207
201
  if (config.verbose) {
208
- console.log(colors.cyan("[litestar-nuxt]"), colors.dim(`Hotfile updated via listen hook: ${url}`));
202
+ console.log(colors.cyan("[litestar-nuxt]"), colors.dim(`Hotfile written after listen: ${hotFile} -> ${url}`));
209
203
  }
210
204
  }
211
205
  });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * React bindings for generic and Litestar Queues event streams.
3
+ *
4
+ * @module
5
+ */
6
+ import { type EventStreamOptions, type QueueEventStreamOptions, type StreamGap } from "litestar-vite-plugin/helpers";
7
+ export interface EventStreamState<TFrame> {
8
+ healthy: boolean;
9
+ lastEvent: TFrame | null;
10
+ lastGap: StreamGap | null;
11
+ events: TFrame[];
12
+ }
13
+ export type ReactEventStreamOptions<TFrame> = EventStreamOptions<TFrame> & {
14
+ key: string;
15
+ bufferSize?: number;
16
+ };
17
+ export type ReactQueueEventStreamOptions<TFrame> = QueueEventStreamOptions<TFrame> & {
18
+ key: string;
19
+ bufferSize?: number;
20
+ };
21
+ /**
22
+ * Subscribe a React component to a generic event stream.
23
+ */
24
+ export declare function useEventStream<TFrame = unknown>(options: ReactEventStreamOptions<TFrame>): EventStreamState<TFrame>;
25
+ /**
26
+ * Subscribe a React component to a Litestar Queues event stream.
27
+ */
28
+ export declare function useQueueEventStream<TFrame = unknown>(options: ReactQueueEventStreamOptions<TFrame>): EventStreamState<TFrame>;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * React bindings for generic and Litestar Queues event streams.
3
+ *
4
+ * @module
5
+ */
6
+ import { createEventStream, createQueueEventStream } from "litestar-vite-plugin/helpers";
7
+ import { useEffect, useRef, useState } from "react";
8
+ function initialState() {
9
+ return { events: [], healthy: false, lastEvent: null, lastGap: null };
10
+ }
11
+ function useStream(factory, options) {
12
+ const optionsRef = useRef(options);
13
+ optionsRef.current = options;
14
+ const [state, setState] = useState(initialState);
15
+ const transport = options.transport ?? "websocket";
16
+ useEffect(() => {
17
+ setState(initialState);
18
+ const { bufferSize: _bufferSize, key: _key, ...streamOptions } = optionsRef.current;
19
+ const stream = factory({
20
+ ...streamOptions,
21
+ onEvent: (frame) => {
22
+ optionsRef.current.onEvent(frame);
23
+ setState((current) => {
24
+ const bufferSize = Math.max(0, optionsRef.current.bufferSize ?? 100);
25
+ const events = bufferSize === 0 ? [] : [...current.events, frame].slice(-bufferSize);
26
+ return { ...current, events, lastEvent: frame };
27
+ });
28
+ },
29
+ onGap: (gap) => {
30
+ optionsRef.current.onGap?.(gap);
31
+ setState((current) => ({ ...current, lastGap: gap }));
32
+ },
33
+ onHealthChange: (healthy) => {
34
+ optionsRef.current.onHealthChange?.(healthy);
35
+ setState((current) => ({ ...current, healthy }));
36
+ },
37
+ });
38
+ stream.connect();
39
+ return () => stream.dispose();
40
+ }, [factory, options.key, transport]);
41
+ return state;
42
+ }
43
+ /**
44
+ * Subscribe a React component to a generic event stream.
45
+ */
46
+ export function useEventStream(options) {
47
+ return useStream(createEventStream, options);
48
+ }
49
+ /**
50
+ * Subscribe a React component to a Litestar Queues event stream.
51
+ */
52
+ export function useQueueEventStream(options) {
53
+ return useStream(createQueueEventStream, options);
54
+ }
@@ -37,9 +37,9 @@ export interface BridgeSchema {
37
37
  appUrl: string | null;
38
38
  /**
39
39
  * Litestar dev server port. Used by framework integrations to set
40
- * `vite.server.hmr.clientPort`, ensuring the browser connects to Litestar
41
- * (not the framework dev server) for HMR preserving the single-port
42
- * contract.
40
+ * `vite.server.ws.clientPort` on Vite 8.1+ (`vite.server.hmr.clientPort` on
41
+ * Vite 7 / 8.0), ensuring the browser connects to Litestar (not the framework
42
+ * dev server) for HMR — preserving the single-port contract.
43
43
  */
44
44
  litestarPort: number | null;
45
45
  bundleDir: string;
@@ -0,0 +1,8 @@
1
+ import type { ViteDevServer } from "vite";
2
+ /**
3
+ * Close a Python-managed Vite sidecar when its stdin pipe reaches EOF.
4
+ *
5
+ * @param server - Vite server owned by the Python parent process.
6
+ * @param markShuttingDown - Optional callback for integration-specific shutdown state.
7
+ */
8
+ export declare function installManagedShutdown(server: ViteDevServer, markShuttingDown?: () => void): void;
@@ -0,0 +1,26 @@
1
+ const installedServers = /* @__PURE__ */ new WeakSet();
2
+ function installManagedShutdown(server, markShuttingDown) {
3
+ if (process.env.LITESTAR_VITE_MANAGED !== "1" || installedServers.has(server)) {
4
+ return;
5
+ }
6
+ installedServers.add(server);
7
+ let shuttingDown = false;
8
+ const shutdown = async () => {
9
+ if (shuttingDown) {
10
+ return;
11
+ }
12
+ shuttingDown = true;
13
+ markShuttingDown?.();
14
+ try {
15
+ await server.close();
16
+ } finally {
17
+ process.exit(0);
18
+ }
19
+ };
20
+ process.stdin.on("end", shutdown);
21
+ process.stdin.on("close", shutdown);
22
+ process.stdin.resume();
23
+ }
24
+ export {
25
+ installManagedShutdown
26
+ };
@@ -25,9 +25,10 @@ export declare function normalizeHost(host: string): string;
25
25
  * Resolve the Litestar dev server port for HMR routing.
26
26
  *
27
27
  * Framework integrations (Astro/Nuxt/SvelteKit) need this port to set
28
- * `vite.server.hmr.clientPort` so the browser opens the HMR WebSocket against
29
- * Litestar NOT the framework dev server's portpreserving the
30
- * single-port-via-ASGI contract.
28
+ * `vite.server.ws.clientPort` on Vite 8.1+ (`vite.server.hmr.clientPort` on
29
+ * Vite 7 / 8.0) so the browser opens the HMR WebSocket against Litestar NOT
30
+ * the framework dev server's port — preserving the single-port-via-ASGI
31
+ * contract.
31
32
  *
32
33
  * Resolution order:
33
34
  * 1. `bridge.litestarPort` (preferred; written by Python ≥0.23.0).
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Svelte stores for generic and Litestar Queues event streams.
3
+ *
4
+ * @module
5
+ */
6
+ import { type EventStreamOptions, type QueueEventStreamOptions, type StreamGap } from "litestar-vite-plugin/helpers";
7
+ import { type Readable } from "svelte/store";
8
+ export interface EventStreamStoreState<TFrame> {
9
+ healthy: boolean;
10
+ lastEvent: TFrame | null;
11
+ lastGap: StreamGap | null;
12
+ events: TFrame[];
13
+ }
14
+ export type SvelteEventStreamOptions<TFrame> = EventStreamOptions<TFrame> & {
15
+ bufferSize?: number;
16
+ };
17
+ export type SvelteQueueEventStreamOptions<TFrame> = QueueEventStreamOptions<TFrame> & {
18
+ bufferSize?: number;
19
+ };
20
+ /**
21
+ * Create a readable store backed by a generic event stream.
22
+ */
23
+ export declare function createEventStreamStore<TFrame = unknown>(options: SvelteEventStreamOptions<TFrame>): Readable<EventStreamStoreState<TFrame>>;
24
+ /**
25
+ * Create a readable store backed by a Litestar Queues event stream.
26
+ */
27
+ export declare function createQueueEventStreamStore<TFrame = unknown>(options: SvelteQueueEventStreamOptions<TFrame>): Readable<EventStreamStoreState<TFrame>>;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Svelte stores for generic and Litestar Queues event streams.
3
+ *
4
+ * @module
5
+ */
6
+ import { createEventStream, createQueueEventStream } from "litestar-vite-plugin/helpers";
7
+ import { readable } from "svelte/store";
8
+ function initialState() {
9
+ return { events: [], healthy: false, lastEvent: null, lastGap: null };
10
+ }
11
+ function createStreamStore(factory, options) {
12
+ return readable(initialState(), (set) => {
13
+ let state = initialState();
14
+ const update = (next) => {
15
+ state = { ...state, ...next };
16
+ set(state);
17
+ };
18
+ const { bufferSize: _bufferSize, ...streamOptions } = options;
19
+ const stream = factory({
20
+ ...streamOptions,
21
+ onEvent: (frame) => {
22
+ options.onEvent(frame);
23
+ const bufferSize = Math.max(0, options.bufferSize ?? 100);
24
+ const events = bufferSize === 0 ? [] : [...state.events, frame].slice(-bufferSize);
25
+ update({ events, lastEvent: frame });
26
+ },
27
+ onGap: (gap) => {
28
+ options.onGap?.(gap);
29
+ update({ lastGap: gap });
30
+ },
31
+ onHealthChange: (healthy) => {
32
+ options.onHealthChange?.(healthy);
33
+ update({ healthy });
34
+ },
35
+ });
36
+ stream.connect();
37
+ return () => stream.dispose();
38
+ });
39
+ }
40
+ /**
41
+ * Create a readable store backed by a generic event stream.
42
+ */
43
+ export function createEventStreamStore(options) {
44
+ return createStreamStore(createEventStream, options);
45
+ }
46
+ /**
47
+ * Create a readable store backed by a Litestar Queues event stream.
48
+ */
49
+ export function createQueueEventStreamStore(options) {
50
+ return createStreamStore(createQueueEventStream, options);
51
+ }
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import colors from "picocolors";
4
4
  import { readBridgeConfig } from "./shared/bridge-schema.js";
5
+ import { installManagedShutdown } from "./shared/managed-shutdown.js";
5
6
  import { normalizeHost, resolveHotFilePath, resolveLitestarPort } from "./shared/network.js";
6
7
  import { createLitestarTypeGenPlugin, resolveTypesConfig } from "./shared/typegen-plugin.js";
7
8
  import { hmrServerConfig } from "./shared/vite-compat.js";
@@ -97,6 +98,7 @@ function litestarSvelteKit(userConfig = {}) {
97
98
  };
98
99
  },
99
100
  configureServer(server) {
101
+ installManagedShutdown(server);
100
102
  if (config.verbose) {
101
103
  server.middlewares.use((req, _res, next) => {
102
104
  if (req.url?.startsWith(config.apiPrefix)) {
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Vue bindings for generic and Litestar Queues event streams.
3
+ *
4
+ * @module
5
+ */
6
+ import { type EventStreamOptions, type QueueEventStreamOptions, type StreamGap } from "litestar-vite-plugin/helpers";
7
+ import { type MaybeRefOrGetter, type Ref, type ShallowRef } from "vue";
8
+ export interface VueEventStreamState<TFrame> {
9
+ healthy: Ref<boolean>;
10
+ lastEvent: ShallowRef<TFrame | null>;
11
+ lastGap: ShallowRef<StreamGap | null>;
12
+ events: ShallowRef<TFrame[]>;
13
+ }
14
+ export type VueEventStreamOptions<TFrame> = EventStreamOptions<TFrame> & {
15
+ key: MaybeRefOrGetter<string>;
16
+ bufferSize?: number;
17
+ };
18
+ export type VueQueueEventStreamOptions<TFrame> = QueueEventStreamOptions<TFrame> & {
19
+ key: MaybeRefOrGetter<string>;
20
+ bufferSize?: number;
21
+ };
22
+ /**
23
+ * Subscribe a Vue scope to a generic event stream.
24
+ */
25
+ export declare function useEventStream<TFrame = unknown>(options: VueEventStreamOptions<TFrame>): VueEventStreamState<TFrame>;
26
+ /**
27
+ * Subscribe a Vue scope to a Litestar Queues event stream.
28
+ */
29
+ export declare function useQueueEventStream<TFrame = unknown>(options: VueQueueEventStreamOptions<TFrame>): VueEventStreamState<TFrame>;