ugly-app 0.1.519 → 0.1.521
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/.pgdata/base/16384/pg_internal.init +0 -0
- package/.pgdata/global/pg_control +0 -0
- package/.pgdata/global/pg_internal.init +0 -0
- package/.pgdata/pg_wal/000000010000000000000004 +0 -0
- package/.pgdata/postmaster.pid +2 -2
- package/dist/cli/dev.d.ts.map +1 -1
- package/dist/cli/dev.js +0 -3
- package/dist/cli/dev.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +31 -66
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.js +8 -66
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/migrate.js +0 -4
- package/dist/cli/migrate.js.map +1 -1
- package/dist/cli/schemaGen.d.ts.map +1 -1
- package/dist/cli/schemaGen.js +0 -4
- package/dist/cli/schemaGen.js.map +1 -1
- package/dist/cli/schemaStatus.d.ts.map +1 -1
- package/dist/cli/schemaStatus.js +0 -4
- package/dist/cli/schemaStatus.js.map +1 -1
- package/dist/cli/serverLogQueryApi.d.ts +5 -4
- package/dist/cli/serverLogQueryApi.d.ts.map +1 -1
- package/dist/cli/serverLogQueryApi.js +55 -115
- package/dist/cli/serverLogQueryApi.js.map +1 -1
- package/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/client/auth/MagicLinkCallback.d.ts.map +1 -1
- package/dist/client/auth/MagicLinkCallback.js +8 -2
- package/dist/client/auth/MagicLinkCallback.js.map +1 -1
- package/dist/client/auth/MagicLinkForm.d.ts.map +1 -1
- package/dist/client/auth/MagicLinkForm.js +6 -1
- package/dist/client/auth/MagicLinkForm.js.map +1 -1
- package/dist/server/App.d.ts.map +1 -1
- package/dist/server/App.js +1 -2
- package/dist/server/App.js.map +1 -1
- package/dist/server/Logging.d.ts +5 -8
- package/dist/server/Logging.d.ts.map +1 -1
- package/dist/server/Logging.js +21 -97
- package/dist/server/Logging.js.map +1 -1
- package/dist/server/auth/magicLinkProvider.d.ts.map +1 -1
- package/dist/server/auth/magicLinkProvider.js +55 -6
- package/dist/server/auth/magicLinkProvider.js.map +1 -1
- package/dist/server/auth/workersMagicLink.d.ts.map +1 -1
- package/dist/server/auth/workersMagicLink.js +78 -6
- package/dist/server/auth/workersMagicLink.js.map +1 -1
- package/dist/server/bootstrap.d.ts +5 -21
- package/dist/server/bootstrap.d.ts.map +1 -1
- package/dist/server/bootstrap.js +5 -27
- package/dist/server/bootstrap.js.map +1 -1
- package/dist/shared/DB.d.ts +0 -9
- package/dist/shared/DB.d.ts.map +1 -1
- package/dist/shared/DB.js +46 -10
- package/dist/shared/DB.js.map +1 -1
- package/dist/vite/index.d.ts +0 -1
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +0 -1
- package/dist/vite/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/dev.ts +0 -4
- package/src/cli/doctor.ts +31 -63
- package/src/cli/index.ts +9 -81
- package/src/cli/migrate.ts +0 -4
- package/src/cli/schemaGen.ts +0 -4
- package/src/cli/schemaStatus.ts +0 -4
- package/src/cli/serverLogQueryApi.ts +53 -136
- package/src/cli/version.ts +1 -1
- package/src/client/auth/MagicLinkCallback.tsx +10 -2
- package/src/client/auth/MagicLinkForm.tsx +7 -1
- package/src/server/App.ts +1 -2
- package/src/server/Logging.ts +21 -97
- package/src/server/auth/magicLinkProvider.ts +61 -6
- package/src/server/auth/workersMagicLink.ts +75 -6
- package/src/server/bootstrap.ts +5 -29
- package/src/shared/DB.ts +46 -17
- package/src/vite/index.ts +0 -1
- package/.pgdata/base/1/pg_internal.init +0 -0
- package/.pgdata/base/17702/pg_internal.init +0 -0
- package/.pgdata/base/5/pg_internal.init +0 -0
- package/dist/cli/devTunnel.d.ts +0 -37
- package/dist/cli/devTunnel.d.ts.map +0 -1
- package/dist/cli/devTunnel.js +0 -470
- package/dist/cli/devTunnel.js.map +0 -1
- package/dist/cli/ensureInfra.d.ts +0 -9
- package/dist/cli/ensureInfra.d.ts.map +0 -1
- package/dist/cli/ensureInfra.js +0 -86
- package/dist/cli/ensureInfra.js.map +0 -1
- package/dist/cli/sidecar.d.ts +0 -21
- package/dist/cli/sidecar.d.ts.map +0 -1
- package/dist/cli/sidecar.js +0 -294
- package/dist/cli/sidecar.js.map +0 -1
- package/dist/cli/sidecarHealth.d.ts +0 -13
- package/dist/cli/sidecarHealth.d.ts.map +0 -1
- package/dist/cli/sidecarHealth.js +0 -42
- package/dist/cli/sidecarHealth.js.map +0 -1
- package/dist/cli/sidecarProtocol.d.ts +0 -41
- package/dist/cli/sidecarProtocol.d.ts.map +0 -1
- package/dist/cli/sidecarProtocol.js +0 -61
- package/dist/cli/sidecarProtocol.js.map +0 -1
- package/dist/vite/tunnelPlugin.d.ts +0 -10
- package/dist/vite/tunnelPlugin.d.ts.map +0 -1
- package/dist/vite/tunnelPlugin.js +0 -30
- package/dist/vite/tunnelPlugin.js.map +0 -1
- package/src/cli/devTunnel.ts +0 -589
- package/src/cli/ensureInfra.ts +0 -90
- package/src/cli/sidecar.ts +0 -350
- package/src/cli/sidecarHealth.ts +0 -58
- package/src/cli/sidecarProtocol.ts +0 -68
- package/src/vite/tunnelPlugin.ts +0 -32
package/src/cli/sidecar.ts
DELETED
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* sidecar.ts
|
|
3
|
-
*
|
|
4
|
-
* Local TCP multiplexer that tunnels 5 service ports over a single WSS connection
|
|
5
|
-
* to the ugly.bot dev-tunnel endpoint.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import net from 'net';
|
|
9
|
-
import WebSocket from 'ws';
|
|
10
|
-
import { getUglyBotWsUrl } from '../shared/uglyBotUrl.js';
|
|
11
|
-
import type { SidecarPorts } from './sidecarHealth.js';
|
|
12
|
-
import {
|
|
13
|
-
SERVICE,
|
|
14
|
-
SERVICE_NAME,
|
|
15
|
-
FLAGS,
|
|
16
|
-
type ServiceId,
|
|
17
|
-
encodeFrame,
|
|
18
|
-
decodeFrame,
|
|
19
|
-
} from './sidecarProtocol.js';
|
|
20
|
-
|
|
21
|
-
export interface StartSidecarOpts {
|
|
22
|
-
projectId: string;
|
|
23
|
-
token: string;
|
|
24
|
-
/** Defaults to 'wss://ugly.bot' */
|
|
25
|
-
serverUrl?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const BACKPRESSURE_THRESHOLD = 1024 * 1024; // 1 MB
|
|
29
|
-
const SERVICE_LIST: ServiceId[] = [
|
|
30
|
-
SERVICE.POSTGRES,
|
|
31
|
-
SERVICE.NATS,
|
|
32
|
-
SERVICE.S3,
|
|
33
|
-
SERVICE.QDRANT_HTTP,
|
|
34
|
-
SERVICE.QDRANT_GRPC,
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
interface SidecarState {
|
|
38
|
-
ws: WebSocket | null;
|
|
39
|
-
servers: Map<ServiceId, net.Server>;
|
|
40
|
-
streams: Map<number, net.Socket>;
|
|
41
|
-
nextStreamId: number;
|
|
42
|
-
reconnectTimer: ReturnType<typeof setTimeout> | null;
|
|
43
|
-
reconnectDelay: number;
|
|
44
|
-
connected: boolean;
|
|
45
|
-
stopped: boolean;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
let state: SidecarState | null = null;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Start the sidecar: opens WSS tunnel and binds ephemeral TCP ports.
|
|
52
|
-
* Returns the actual ports bound.
|
|
53
|
-
*/
|
|
54
|
-
export async function startSidecar(opts: StartSidecarOpts): Promise<SidecarPorts> {
|
|
55
|
-
if (state && !state.stopped) {
|
|
56
|
-
throw new Error('Sidecar already running');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
state = {
|
|
60
|
-
ws: null,
|
|
61
|
-
servers: new Map(),
|
|
62
|
-
streams: new Map(),
|
|
63
|
-
nextStreamId: 1,
|
|
64
|
-
reconnectTimer: null,
|
|
65
|
-
reconnectDelay: 1000,
|
|
66
|
-
connected: false,
|
|
67
|
-
stopped: false,
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// Create TCP servers for each service on port 0 (ephemeral)
|
|
71
|
-
const portResults = new Map<ServiceId, number>();
|
|
72
|
-
|
|
73
|
-
for (const serviceId of SERVICE_LIST) {
|
|
74
|
-
const server = net.createServer((socket) => {
|
|
75
|
-
handleTcpConnection(serviceId, socket);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
await new Promise<void>((resolve, reject) => {
|
|
79
|
-
server.listen(0, '127.0.0.1', () => resolve());
|
|
80
|
-
server.on('error', reject);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const addr = server.address() as net.AddressInfo;
|
|
84
|
-
portResults.set(serviceId, addr.port);
|
|
85
|
-
state.servers.set(serviceId, server);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Connect WebSocket
|
|
89
|
-
await connectWs(opts);
|
|
90
|
-
|
|
91
|
-
const ports: SidecarPorts = {
|
|
92
|
-
postgres: portResults.get(SERVICE.POSTGRES)!,
|
|
93
|
-
nats: portResults.get(SERVICE.NATS)!,
|
|
94
|
-
s3: portResults.get(SERVICE.S3)!,
|
|
95
|
-
qdrantHttp: portResults.get(SERVICE.QDRANT_HTTP)!,
|
|
96
|
-
qdrantGrpc: portResults.get(SERVICE.QDRANT_GRPC)!,
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
console.log('[ugly-app] Sidecar listening:');
|
|
100
|
-
for (const [serviceId, port] of portResults) {
|
|
101
|
-
console.log(` ${SERVICE_NAME[serviceId]}: localhost:${port}`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return ports;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function connectWs(opts: StartSidecarOpts): Promise<void> {
|
|
108
|
-
if (!state || state.stopped) return Promise.resolve();
|
|
109
|
-
|
|
110
|
-
const serverUrl = opts.serverUrl ?? getUglyBotWsUrl();
|
|
111
|
-
|
|
112
|
-
return new Promise<void>((resolve, reject) => {
|
|
113
|
-
let resolved = false;
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
const ws = new WebSocket(`${serverUrl}/dev-tunnel`, {
|
|
117
|
-
headers: {
|
|
118
|
-
'Authorization': `Bearer ${opts.token}`,
|
|
119
|
-
'X-Project-Id': opts.projectId,
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
ws.binaryType = 'nodebuffer';
|
|
124
|
-
|
|
125
|
-
ws.on('open', () => {
|
|
126
|
-
if (!state) return;
|
|
127
|
-
state.ws = ws;
|
|
128
|
-
state.reconnectDelay = 1000;
|
|
129
|
-
// Don't resolve yet — wait for 'ready' message from server.
|
|
130
|
-
// The server needs time to spin up per-client proxy instances.
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
ws.on('message', (data: Buffer | string) => {
|
|
134
|
-
if (!state) return;
|
|
135
|
-
|
|
136
|
-
// Handle JSON control messages (text frames from server)
|
|
137
|
-
if (typeof data === 'string' || (Buffer.isBuffer(data) && data[0] === 0x7b /* '{' */)) {
|
|
138
|
-
try {
|
|
139
|
-
const msg = JSON.parse(typeof data === 'string' ? data : data.toString('utf8')) as { type: string };
|
|
140
|
-
if (msg.type === 'ready') {
|
|
141
|
-
state.connected = true;
|
|
142
|
-
if (!resolved) {
|
|
143
|
-
resolved = true;
|
|
144
|
-
resolve();
|
|
145
|
-
}
|
|
146
|
-
} else if (msg.type === 'provisioning') {
|
|
147
|
-
console.log('[ugly-app] Cloud DB being provisioned — first-time setup...');
|
|
148
|
-
}
|
|
149
|
-
} catch {
|
|
150
|
-
// Not JSON — fall through to binary handling
|
|
151
|
-
}
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Handle binary tunnel frames
|
|
156
|
-
try {
|
|
157
|
-
const { streamId, flags, payload } = decodeFrame(Buffer.from(data));
|
|
158
|
-
|
|
159
|
-
if (flags & FLAGS.RST) {
|
|
160
|
-
const socket = state.streams.get(streamId);
|
|
161
|
-
if (socket) {
|
|
162
|
-
state.streams.delete(streamId);
|
|
163
|
-
socket.destroy();
|
|
164
|
-
}
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Write payload before checking FIN (last data chunk)
|
|
169
|
-
if (payload.length > 0) {
|
|
170
|
-
const socket = state.streams.get(streamId);
|
|
171
|
-
if (socket && !socket.destroyed) {
|
|
172
|
-
socket.write(payload);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (flags & FLAGS.FIN) {
|
|
177
|
-
const socket = state.streams.get(streamId);
|
|
178
|
-
if (socket) {
|
|
179
|
-
state.streams.delete(streamId);
|
|
180
|
-
socket.end();
|
|
181
|
-
}
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
} catch {
|
|
185
|
-
// Ignore malformed frames
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
ws.on('close', (code) => {
|
|
190
|
-
if (!state || state.stopped) return;
|
|
191
|
-
state.connected = false;
|
|
192
|
-
state.ws = null;
|
|
193
|
-
|
|
194
|
-
if (code === 4001) {
|
|
195
|
-
console.error('[ugly-app] Auth expired — run `npx ugly-app login` then restart');
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// RST all active streams
|
|
199
|
-
for (const [id, socket] of state.streams) {
|
|
200
|
-
socket.destroy();
|
|
201
|
-
state.streams.delete(id);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (!resolved) {
|
|
205
|
-
// Initial connect failed — clean up and reject
|
|
206
|
-
resolved = true;
|
|
207
|
-
cleanupServers();
|
|
208
|
-
reject(new Error('[ugly-app] Cannot reach ugly.bot — check your connection'));
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
console.warn('[ugly-app] Tunnel disconnected, reconnecting...');
|
|
213
|
-
scheduleReconnect(opts);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
ws.on('error', () => {
|
|
217
|
-
// error is always followed by close — handled there
|
|
218
|
-
});
|
|
219
|
-
} catch (err) {
|
|
220
|
-
if (!resolved) {
|
|
221
|
-
resolved = true;
|
|
222
|
-
cleanupServers();
|
|
223
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function cleanupServers(): void {
|
|
230
|
-
if (!state) return;
|
|
231
|
-
for (const server of state.servers.values()) {
|
|
232
|
-
server.close();
|
|
233
|
-
}
|
|
234
|
-
state.servers.clear();
|
|
235
|
-
for (const socket of state.streams.values()) {
|
|
236
|
-
socket.destroy();
|
|
237
|
-
}
|
|
238
|
-
state.streams.clear();
|
|
239
|
-
state = null;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function scheduleReconnect(opts: StartSidecarOpts): void {
|
|
243
|
-
if (!state || state.stopped || state.reconnectTimer) return;
|
|
244
|
-
|
|
245
|
-
state.reconnectTimer = setTimeout(() => {
|
|
246
|
-
if (!state || state.stopped) return;
|
|
247
|
-
state.reconnectTimer = null;
|
|
248
|
-
state.reconnectDelay = Math.min(state.reconnectDelay * 2, 30_000);
|
|
249
|
-
connectWs(opts).then(() => {
|
|
250
|
-
console.log('[ugly-app] Tunnel reconnected');
|
|
251
|
-
}).catch(() => {
|
|
252
|
-
scheduleReconnect(opts);
|
|
253
|
-
});
|
|
254
|
-
}, state.reconnectDelay);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function handleTcpConnection(serviceId: ServiceId, socket: net.Socket): void {
|
|
258
|
-
if (!state) { socket.destroy(); return; }
|
|
259
|
-
|
|
260
|
-
if (!state.connected || !state.ws) {
|
|
261
|
-
console.warn(`[ugly-app] Tunnel reconnecting — rejecting ${SERVICE_NAME[serviceId]} connection`);
|
|
262
|
-
socket.destroy();
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const streamId = state.nextStreamId++;
|
|
267
|
-
// Wrap around at uint32 max
|
|
268
|
-
if (state.nextStreamId > 0xFFFFFFFF) {
|
|
269
|
-
state.nextStreamId = 1;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
state.streams.set(streamId, socket);
|
|
273
|
-
const serviceFlags = serviceId << 2;
|
|
274
|
-
|
|
275
|
-
// Send an empty data frame to signal "stream opened" — the server needs this
|
|
276
|
-
// to create the upstream connection. Without it, server-speaks-first protocols
|
|
277
|
-
// (NATS INFO, Postgres auth challenge) would hang because the server never
|
|
278
|
-
// knows to connect upstream.
|
|
279
|
-
if (state.ws?.readyState === WebSocket.OPEN) {
|
|
280
|
-
state.ws.send(encodeFrame(streamId, serviceFlags, Buffer.alloc(0)));
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
socket.on('data', (chunk: Buffer) => {
|
|
284
|
-
if (state?.ws?.readyState !== WebSocket.OPEN) return;
|
|
285
|
-
|
|
286
|
-
const frame = encodeFrame(streamId, serviceFlags, chunk);
|
|
287
|
-
state.ws.send(frame);
|
|
288
|
-
|
|
289
|
-
// Backpressure: pause this socket if WS buffer is too full
|
|
290
|
-
if (state.ws.bufferedAmount > BACKPRESSURE_THRESHOLD) {
|
|
291
|
-
socket.pause();
|
|
292
|
-
const checkDrain = () => {
|
|
293
|
-
if (!state?.ws) return;
|
|
294
|
-
if (state.ws.bufferedAmount <= BACKPRESSURE_THRESHOLD) {
|
|
295
|
-
socket.resume();
|
|
296
|
-
} else {
|
|
297
|
-
setTimeout(checkDrain, 50);
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
setTimeout(checkDrain, 50);
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
socket.on('end', () => {
|
|
305
|
-
if (!state) return;
|
|
306
|
-
state.streams.delete(streamId);
|
|
307
|
-
if (state.ws?.readyState === WebSocket.OPEN) {
|
|
308
|
-
state.ws.send(encodeFrame(streamId, serviceFlags | FLAGS.FIN, Buffer.alloc(0)));
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
socket.on('error', () => {
|
|
313
|
-
if (!state) return;
|
|
314
|
-
state.streams.delete(streamId);
|
|
315
|
-
if (state.ws?.readyState === WebSocket.OPEN) {
|
|
316
|
-
state.ws.send(encodeFrame(streamId, serviceFlags | FLAGS.RST, Buffer.alloc(0)));
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
socket.on('close', () => {
|
|
321
|
-
state?.streams.delete(streamId);
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/** Stop the sidecar: close WS, all TCP servers and sockets. */
|
|
326
|
-
export function stopSidecar(): void {
|
|
327
|
-
if (!state) return;
|
|
328
|
-
state.stopped = true;
|
|
329
|
-
|
|
330
|
-
if (state.reconnectTimer) {
|
|
331
|
-
clearTimeout(state.reconnectTimer);
|
|
332
|
-
state.reconnectTimer = null;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (state.ws && state.ws.readyState <= WebSocket.OPEN) {
|
|
336
|
-
state.ws.close(1000, 'Sidecar stopped');
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
for (const socket of state.streams.values()) {
|
|
340
|
-
socket.destroy();
|
|
341
|
-
}
|
|
342
|
-
state.streams.clear();
|
|
343
|
-
|
|
344
|
-
for (const server of state.servers.values()) {
|
|
345
|
-
server.close();
|
|
346
|
-
}
|
|
347
|
-
state.servers.clear();
|
|
348
|
-
|
|
349
|
-
state = null;
|
|
350
|
-
}
|
package/src/cli/sidecarHealth.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import net from 'net';
|
|
2
|
-
|
|
3
|
-
/** Ports returned by startSidecar — the actual ephemeral ports bound. */
|
|
4
|
-
export interface SidecarPorts {
|
|
5
|
-
postgres: number;
|
|
6
|
-
nats: number;
|
|
7
|
-
s3: number;
|
|
8
|
-
qdrantHttp: number;
|
|
9
|
-
qdrantGrpc: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Try a TCP connect to a single port. Resolves true if connection succeeds within timeoutMs. */
|
|
13
|
-
function checkPort(port: number, timeoutMs = 2000): Promise<boolean> {
|
|
14
|
-
return new Promise((resolve) => {
|
|
15
|
-
const sock = net.createConnection({ port, host: '127.0.0.1' });
|
|
16
|
-
const timer = setTimeout(() => {
|
|
17
|
-
sock.destroy();
|
|
18
|
-
resolve(false);
|
|
19
|
-
}, timeoutMs);
|
|
20
|
-
sock.once('connect', () => {
|
|
21
|
-
clearTimeout(timer);
|
|
22
|
-
sock.destroy();
|
|
23
|
-
resolve(true);
|
|
24
|
-
});
|
|
25
|
-
sock.once('error', () => {
|
|
26
|
-
clearTimeout(timer);
|
|
27
|
-
sock.destroy();
|
|
28
|
-
resolve(false);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Returns true if all sidecar ports accept TCP connections. */
|
|
34
|
-
export async function isSidecarHealthy(ports: SidecarPorts): Promise<boolean> {
|
|
35
|
-
const results = await Promise.all([
|
|
36
|
-
checkPort(ports.postgres),
|
|
37
|
-
checkPort(ports.nats),
|
|
38
|
-
checkPort(ports.s3),
|
|
39
|
-
checkPort(ports.qdrantHttp),
|
|
40
|
-
checkPort(ports.qdrantGrpc),
|
|
41
|
-
]);
|
|
42
|
-
return results.every(Boolean);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Poll isSidecarHealthy until all ports are reachable. Throws after retries exhausted. */
|
|
46
|
-
export async function waitForSidecar(
|
|
47
|
-
ports: SidecarPorts,
|
|
48
|
-
retries = 15,
|
|
49
|
-
intervalMs = 500,
|
|
50
|
-
): Promise<void> {
|
|
51
|
-
for (let i = 0; i < retries; i++) {
|
|
52
|
-
if (await isSidecarHealthy(ports)) return;
|
|
53
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
54
|
-
}
|
|
55
|
-
throw new Error(
|
|
56
|
-
`[ugly-app] Sidecar not healthy after ${Math.round((retries * intervalMs) / 1000)}s`,
|
|
57
|
-
);
|
|
58
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* sidecarProtocol.ts
|
|
3
|
-
*
|
|
4
|
-
* Wire protocol for the TCP-over-WebSocket multiplexer (sidecar proxy).
|
|
5
|
-
*
|
|
6
|
-
* Frame layout (5-byte header + payload):
|
|
7
|
-
* Bytes 0-3 : streamId (uint32 big-endian)
|
|
8
|
-
* Byte 4 : flags
|
|
9
|
-
* Bytes 5+ : payload
|
|
10
|
-
*
|
|
11
|
-
* Flags byte layout:
|
|
12
|
-
* bit 0 : FIN
|
|
13
|
-
* bit 1 : RST
|
|
14
|
-
* bits 2-4 : serviceId (extracted with (flags >> 2) & 0x07)
|
|
15
|
-
* bit 5 : WINDOW_UPDATE
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
export const SERVICE = {
|
|
19
|
-
POSTGRES: 0,
|
|
20
|
-
NATS: 1,
|
|
21
|
-
S3: 2,
|
|
22
|
-
QDRANT_HTTP: 3,
|
|
23
|
-
QDRANT_GRPC: 4,
|
|
24
|
-
} as const;
|
|
25
|
-
|
|
26
|
-
export type ServiceId = (typeof SERVICE)[keyof typeof SERVICE];
|
|
27
|
-
|
|
28
|
-
export const FLAGS = {
|
|
29
|
-
FIN: 0x01,
|
|
30
|
-
RST: 0x02,
|
|
31
|
-
WINDOW_UPDATE: 0x20,
|
|
32
|
-
} as const;
|
|
33
|
-
|
|
34
|
-
export const SERVICE_NAME: Record<ServiceId, string> = {
|
|
35
|
-
[SERVICE.POSTGRES]: 'postgres',
|
|
36
|
-
[SERVICE.NATS]: 'nats',
|
|
37
|
-
[SERVICE.S3]: 's3',
|
|
38
|
-
[SERVICE.QDRANT_HTTP]: 'qdrant-http',
|
|
39
|
-
[SERVICE.QDRANT_GRPC]: 'qdrant-grpc',
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/** Extract the service identifier from bits 2-4 of the flags byte. Throws on invalid service. */
|
|
43
|
-
export function serviceFromFlags(flags: number): ServiceId {
|
|
44
|
-
const id = (flags >> 2) & 0x07;
|
|
45
|
-
if (id > SERVICE.QDRANT_GRPC) {
|
|
46
|
-
throw new Error(`sidecarProtocol: invalid serviceId ${id} in flags byte 0x${flags.toString(16)}`);
|
|
47
|
-
}
|
|
48
|
-
return id as ServiceId;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Encode a frame into a Buffer with a 5-byte header followed by payload. */
|
|
52
|
-
export function encodeFrame(streamId: number, flags: number, payload: Buffer): Buffer {
|
|
53
|
-
const header = Buffer.allocUnsafe(5);
|
|
54
|
-
header.writeUInt32BE(streamId >>> 0, 0);
|
|
55
|
-
header[4] = flags & 0xff;
|
|
56
|
-
return Buffer.concat([header, payload]);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Decode a frame buffer. Throws if the buffer is shorter than 5 bytes. */
|
|
60
|
-
export function decodeFrame(buf: Buffer): { streamId: number; flags: number; payload: Buffer } {
|
|
61
|
-
if (buf.length < 5) {
|
|
62
|
-
throw new Error(`sidecarProtocol: buffer too short to decode frame (got ${buf.length} bytes, need at least 5)`);
|
|
63
|
-
}
|
|
64
|
-
const streamId = buf.readUInt32BE(0);
|
|
65
|
-
const flags = buf[4];
|
|
66
|
-
const payload = buf.subarray(5);
|
|
67
|
-
return { streamId, flags, payload };
|
|
68
|
-
}
|
package/src/vite/tunnelPlugin.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { Plugin } from 'vite';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Vite plugin for ugly-app dev mode.
|
|
5
|
-
*
|
|
6
|
-
* The tunnel relay (HTTP/WS proxy for public dev URLs) has been moved to
|
|
7
|
-
* devTunnel.ts in the CLI process. This plugin now only provides Vite
|
|
8
|
-
* config optimizations for linked packages.
|
|
9
|
-
*/
|
|
10
|
-
export function tunnelPlugin(): Plugin {
|
|
11
|
-
return {
|
|
12
|
-
name: 'ugly-app-tunnel',
|
|
13
|
-
apply: 'serve',
|
|
14
|
-
|
|
15
|
-
config() {
|
|
16
|
-
// Each module served through the tunnel is a separate HTTP request
|
|
17
|
-
// relayed via WebSocket. Vite excludes linked (file:) packages from
|
|
18
|
-
// dep optimization, serving every sub-module individually — this can
|
|
19
|
-
// easily exceed 100 requests per page load. Force linked deps into
|
|
20
|
-
// the optimization bundle so they become a single request each.
|
|
21
|
-
return {
|
|
22
|
-
optimizeDeps: {
|
|
23
|
-
include: [
|
|
24
|
-
'ugly-app',
|
|
25
|
-
'ugly-app/client',
|
|
26
|
-
'ugly-app/shared',
|
|
27
|
-
],
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
}
|