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.
- package/README.md +37 -18
- package/dist/js/astro.js +4 -0
- package/dist/js/helpers/channels.d.ts +31 -0
- package/dist/js/helpers/channels.js +37 -0
- package/dist/js/helpers/index.d.ts +4 -0
- package/dist/js/helpers/index.js +8 -0
- package/dist/js/helpers/queues.d.ts +61 -0
- package/dist/js/helpers/queues.js +117 -0
- package/dist/js/helpers/stream-element.d.ts +79 -0
- package/dist/js/helpers/stream-element.js +229 -0
- package/dist/js/helpers/stream.d.ts +76 -0
- package/dist/js/helpers/stream.js +242 -0
- package/dist/js/index.js +4 -0
- package/dist/js/nuxt.js +10 -16
- package/dist/js/react/index.d.ts +28 -0
- package/dist/js/react/index.js +54 -0
- package/dist/js/shared/bridge-schema.d.ts +3 -3
- package/dist/js/shared/managed-shutdown.d.ts +8 -0
- package/dist/js/shared/managed-shutdown.js +26 -0
- package/dist/js/shared/network.d.ts +4 -3
- package/dist/js/svelte/index.d.ts +27 -0
- package/dist/js/svelte/index.js +51 -0
- package/dist/js/sveltekit.js +2 -0
- package/dist/js/vue/index.d.ts +29 -0
- package/dist/js/vue/index.js +57 -0
- package/package.json +47 -4
|
@@ -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
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
fs.
|
|
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
|
|
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.
|
|
41
|
-
*
|
|
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.
|
|
29
|
-
*
|
|
30
|
-
* single-port-via-ASGI
|
|
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
|
+
}
|
package/dist/js/sveltekit.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";
|
|
@@ -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>;
|