vh-dev-client 0.0.0-stage → 0.1.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/dist/browser/index.d.ts +1 -0
- package/dist/browser/index.js +335 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +335 -0
- package/dist/index.js.map +1 -0
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.js +335 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +39 -4
- package/README.md +0 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { VHClient, VHEvent, VHLogEvent, VHLogLevel, VHNetworkRequestEvent, VHNetworkResponseEvent, VHOptions, createVH } from '../index.js';
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// src/core/id.ts
|
|
2
|
+
function createId() {
|
|
3
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
4
|
+
return globalThis.crypto.randomUUID();
|
|
5
|
+
}
|
|
6
|
+
throw new Error(
|
|
7
|
+
"Secure randomUUID is unavailable. Use a modern runtime with Web Crypto support."
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/core/batcher.ts
|
|
12
|
+
var EventBatcher = class {
|
|
13
|
+
queue = [];
|
|
14
|
+
maxBatchSize;
|
|
15
|
+
flushIntervalMs;
|
|
16
|
+
maxQueueSize;
|
|
17
|
+
onFlush;
|
|
18
|
+
timer;
|
|
19
|
+
flushing = false;
|
|
20
|
+
flushRequested = false;
|
|
21
|
+
destroyed = false;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.maxBatchSize = options.maxBatchSize ?? 50;
|
|
24
|
+
this.flushIntervalMs = options.flushIntervalMs ?? 5e3;
|
|
25
|
+
this.maxQueueSize = options.maxQueueSize ?? 5e3;
|
|
26
|
+
this.onFlush = options.onFlush;
|
|
27
|
+
this.timer = setInterval(() => {
|
|
28
|
+
void this.flush();
|
|
29
|
+
}, this.flushIntervalMs);
|
|
30
|
+
}
|
|
31
|
+
enqueue(event) {
|
|
32
|
+
if (this.destroyed) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (this.queue.length >= this.maxQueueSize) {
|
|
36
|
+
this.queue.shift();
|
|
37
|
+
}
|
|
38
|
+
this.queue.push(event);
|
|
39
|
+
if (this.queue.length >= this.maxBatchSize) {
|
|
40
|
+
void this.flush();
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
async flush() {
|
|
45
|
+
if (this.destroyed && this.queue.length === 0) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (this.flushing) {
|
|
49
|
+
this.flushRequested = true;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.flushing = true;
|
|
53
|
+
try {
|
|
54
|
+
do {
|
|
55
|
+
this.flushRequested = false;
|
|
56
|
+
if (this.queue.length === 0) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
const batch = this.queue.splice(0, this.maxBatchSize);
|
|
60
|
+
try {
|
|
61
|
+
await this.onFlush(batch);
|
|
62
|
+
} catch {
|
|
63
|
+
this.queue.unshift(...batch);
|
|
64
|
+
if (this.queue.length > this.maxQueueSize) {
|
|
65
|
+
this.queue.length = this.maxQueueSize;
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
} while (this.flushRequested || this.queue.length >= this.maxBatchSize);
|
|
70
|
+
} finally {
|
|
71
|
+
this.flushing = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async destroy() {
|
|
75
|
+
if (this.timer) {
|
|
76
|
+
clearInterval(this.timer);
|
|
77
|
+
this.timer = void 0;
|
|
78
|
+
}
|
|
79
|
+
await this.flush();
|
|
80
|
+
this.destroyed = true;
|
|
81
|
+
}
|
|
82
|
+
get size() {
|
|
83
|
+
return this.queue.length;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// src/core/fingerprint.ts
|
|
88
|
+
function stableStringify(value) {
|
|
89
|
+
if (value === null || typeof value !== "object") {
|
|
90
|
+
return JSON.stringify(value) ?? "undefined";
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(value)) {
|
|
93
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
94
|
+
}
|
|
95
|
+
const record = value;
|
|
96
|
+
const keys = Object.keys(record).sort();
|
|
97
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
|
|
98
|
+
}
|
|
99
|
+
async function createFingerprint(level, message, data) {
|
|
100
|
+
const input = stableStringify({
|
|
101
|
+
level,
|
|
102
|
+
message,
|
|
103
|
+
data: data ?? null
|
|
104
|
+
});
|
|
105
|
+
const bytes = new TextEncoder().encode(input);
|
|
106
|
+
const hash = await crypto.subtle.digest("SHA-256", bytes);
|
|
107
|
+
return Array.from(new Uint8Array(hash)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/core/transport.ts
|
|
111
|
+
import { io } from "socket.io-client";
|
|
112
|
+
var VHTransport = class {
|
|
113
|
+
socket;
|
|
114
|
+
timeoutMs;
|
|
115
|
+
constructor(options) {
|
|
116
|
+
this.timeoutMs = options.timeoutMs ?? 5e3;
|
|
117
|
+
this.socket = io(options.serverUrl, {
|
|
118
|
+
autoConnect: true,
|
|
119
|
+
reconnection: true,
|
|
120
|
+
reconnectionAttempts: Infinity,
|
|
121
|
+
reconnectionDelay: 500,
|
|
122
|
+
reconnectionDelayMax: 5e3,
|
|
123
|
+
timeout: this.timeoutMs
|
|
124
|
+
});
|
|
125
|
+
this.socket.on("connect", () => {
|
|
126
|
+
console.log("[VH Client] Connected:", this.socket.id);
|
|
127
|
+
});
|
|
128
|
+
this.socket.on("connect_error", (error) => {
|
|
129
|
+
console.error("[VH Client] Connection error:", error.message);
|
|
130
|
+
});
|
|
131
|
+
this.socket.on("disconnect", (reason) => {
|
|
132
|
+
console.log("[VH Client] Disconnected:", reason);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
waitForConnection() {
|
|
136
|
+
if (this.socket.connected) {
|
|
137
|
+
return Promise.resolve();
|
|
138
|
+
}
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const cleanup = () => {
|
|
141
|
+
clearTimeout(timeout);
|
|
142
|
+
this.socket.off("connect", onConnect);
|
|
143
|
+
this.socket.off("connect_error", onConnectError);
|
|
144
|
+
};
|
|
145
|
+
const onConnect = () => {
|
|
146
|
+
cleanup();
|
|
147
|
+
resolve();
|
|
148
|
+
};
|
|
149
|
+
const onConnectError = (error) => {
|
|
150
|
+
cleanup();
|
|
151
|
+
reject(new Error(`Unable to connect to VH server: ${error.message}`));
|
|
152
|
+
};
|
|
153
|
+
const timeout = setTimeout(() => {
|
|
154
|
+
cleanup();
|
|
155
|
+
reject(
|
|
156
|
+
new Error(`VH server connection timed out after ${this.timeoutMs}ms`)
|
|
157
|
+
);
|
|
158
|
+
}, this.timeoutMs);
|
|
159
|
+
this.socket.once("connect", onConnect);
|
|
160
|
+
this.socket.once("connect_error", onConnectError);
|
|
161
|
+
if (this.socket.connected) {
|
|
162
|
+
onConnect();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async sendBatch(events) {
|
|
167
|
+
if (events.length === 0) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await this.waitForConnection();
|
|
171
|
+
await new Promise((resolve, reject) => {
|
|
172
|
+
let settled = false;
|
|
173
|
+
const timeout = setTimeout(() => {
|
|
174
|
+
finish(
|
|
175
|
+
new Error(
|
|
176
|
+
`VH server acknowledgement timed out after ${this.timeoutMs}ms`
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
}, this.timeoutMs);
|
|
180
|
+
const finish = (error) => {
|
|
181
|
+
if (settled) return;
|
|
182
|
+
settled = true;
|
|
183
|
+
clearTimeout(timeout);
|
|
184
|
+
if (error) {
|
|
185
|
+
reject(error);
|
|
186
|
+
} else {
|
|
187
|
+
resolve();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
this.socket.emit("vh:events", events, (ack) => {
|
|
191
|
+
if (!ack?.ok) {
|
|
192
|
+
finish(new Error(ack?.error ?? "VH server rejected the batch"));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
console.log(`[VH Client] Server acknowledged ${events.length} events`);
|
|
196
|
+
finish();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
async destroy() {
|
|
201
|
+
this.socket.disconnect();
|
|
202
|
+
}
|
|
203
|
+
get connected() {
|
|
204
|
+
return this.socket.connected;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// src/index.ts
|
|
209
|
+
function createVH(options) {
|
|
210
|
+
const appName = options.appName.trim();
|
|
211
|
+
const serverUrls = [
|
|
212
|
+
...new Set(
|
|
213
|
+
(Array.isArray(options.serverUrl) ? options.serverUrl : [options.serverUrl]).map((url) => url.trim())
|
|
214
|
+
)
|
|
215
|
+
];
|
|
216
|
+
if (!appName) {
|
|
217
|
+
throw new Error("appName is required");
|
|
218
|
+
}
|
|
219
|
+
if (serverUrls.length === 0 || serverUrls.some((url) => {
|
|
220
|
+
try {
|
|
221
|
+
const parsed = new URL(url);
|
|
222
|
+
return !["http:", "https:"].includes(parsed.protocol);
|
|
223
|
+
} catch {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
})) {
|
|
227
|
+
throw new Error("serverUrl must contain valid http:// or https:// URLs");
|
|
228
|
+
}
|
|
229
|
+
const showConsoleOnLog = options.showConsoleOnLog ?? true;
|
|
230
|
+
const transports = serverUrls.map(
|
|
231
|
+
(serverUrl) => new VHTransport({ serverUrl })
|
|
232
|
+
);
|
|
233
|
+
const batcher = new EventBatcher({
|
|
234
|
+
maxBatchSize: 50,
|
|
235
|
+
flushIntervalMs: 5e3,
|
|
236
|
+
maxQueueSize: 5e3,
|
|
237
|
+
async onFlush(events) {
|
|
238
|
+
await Promise.all(
|
|
239
|
+
transports.map((transport) => transport.sendBatch(events))
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
let destroyed = false;
|
|
244
|
+
const pendingLogs = /* @__PURE__ */ new Set();
|
|
245
|
+
async function waitForPendingLogs() {
|
|
246
|
+
while (pendingLogs.size > 0) {
|
|
247
|
+
await Promise.all([...pendingLogs]);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function enqueue(event) {
|
|
251
|
+
if (destroyed) {
|
|
252
|
+
throw new Error("VH client has been destroyed");
|
|
253
|
+
}
|
|
254
|
+
batcher.enqueue(event);
|
|
255
|
+
}
|
|
256
|
+
function writeLog(level, message, data) {
|
|
257
|
+
if (destroyed) {
|
|
258
|
+
throw new Error("VH client has been destroyed");
|
|
259
|
+
}
|
|
260
|
+
if (showConsoleOnLog) {
|
|
261
|
+
console[level](`[${appName}] ${message}`, data ?? "");
|
|
262
|
+
}
|
|
263
|
+
const pending = createFingerprint(level, message, data).then((fingerprint) => {
|
|
264
|
+
if (destroyed) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
enqueue({
|
|
268
|
+
type: "log",
|
|
269
|
+
eventId: createId(),
|
|
270
|
+
fingerprint,
|
|
271
|
+
appName,
|
|
272
|
+
level,
|
|
273
|
+
message,
|
|
274
|
+
...data === void 0 ? {} : { data },
|
|
275
|
+
timestamp: Date.now()
|
|
276
|
+
});
|
|
277
|
+
}).catch((error) => {
|
|
278
|
+
console.error("[vh-dev-client] Failed to create log event", error);
|
|
279
|
+
}).then(() => {
|
|
280
|
+
pendingLogs.delete(pending);
|
|
281
|
+
});
|
|
282
|
+
pendingLogs.add(pending);
|
|
283
|
+
}
|
|
284
|
+
const log = {
|
|
285
|
+
debug: (message, data) => writeLog("debug", message, data),
|
|
286
|
+
info: (message, data) => writeLog("info", message, data),
|
|
287
|
+
warn: (message, data) => writeLog("warn", message, data),
|
|
288
|
+
error: (message, data) => writeLog("error", message, data)
|
|
289
|
+
};
|
|
290
|
+
return {
|
|
291
|
+
log,
|
|
292
|
+
network: {
|
|
293
|
+
req(input) {
|
|
294
|
+
enqueue({
|
|
295
|
+
type: "network:req",
|
|
296
|
+
eventId: createId(),
|
|
297
|
+
appName,
|
|
298
|
+
requestId: input.requestId,
|
|
299
|
+
method: input.method,
|
|
300
|
+
url: input.url,
|
|
301
|
+
timestamp: Date.now()
|
|
302
|
+
});
|
|
303
|
+
},
|
|
304
|
+
res(input) {
|
|
305
|
+
enqueue({
|
|
306
|
+
type: "network:res",
|
|
307
|
+
eventId: createId(),
|
|
308
|
+
appName,
|
|
309
|
+
requestId: input.requestId,
|
|
310
|
+
...input.status === void 0 ? {} : { status: input.status },
|
|
311
|
+
...input.durationMs === void 0 ? {} : { durationMs: input.durationMs },
|
|
312
|
+
...input.error === void 0 ? {} : { error: input.error },
|
|
313
|
+
timestamp: Date.now()
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
async flush() {
|
|
318
|
+
await waitForPendingLogs();
|
|
319
|
+
await batcher.flush();
|
|
320
|
+
},
|
|
321
|
+
async destroy() {
|
|
322
|
+
if (destroyed) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
await waitForPendingLogs();
|
|
326
|
+
await batcher.destroy();
|
|
327
|
+
destroyed = true;
|
|
328
|
+
await Promise.all(transports.map((transport) => transport.destroy()));
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
export {
|
|
333
|
+
createVH
|
|
334
|
+
};
|
|
335
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/id.ts","../../src/core/batcher.ts","../../src/core/fingerprint.ts","../../src/core/transport.ts","../../src/index.ts"],"sourcesContent":["export function createId(): string {\n if (typeof globalThis.crypto?.randomUUID === \"function\") {\n return globalThis.crypto.randomUUID();\n }\n\n throw new Error(\n \"Secure randomUUID is unavailable. Use a modern runtime with Web Crypto support.\",\n );\n}\n","import type { VHEvent } from \"./types.js\";\n\nexport interface BatcherOptions {\n maxBatchSize?: number;\n flushIntervalMs?: number;\n maxQueueSize?: number;\n onFlush: (events: VHEvent[]) => Promise<void>;\n}\n\nexport class EventBatcher {\n private queue: VHEvent[] = [];\n private readonly maxBatchSize: number;\n private readonly flushIntervalMs: number;\n private readonly maxQueueSize: number;\n private readonly onFlush: BatcherOptions[\"onFlush\"];\n\n private timer: ReturnType<typeof setInterval> | undefined;\n private flushing = false;\n private flushRequested = false;\n private destroyed = false;\n\n constructor(options: BatcherOptions) {\n this.maxBatchSize = options.maxBatchSize ?? 50;\n this.flushIntervalMs = options.flushIntervalMs ?? 5000;\n this.maxQueueSize = options.maxQueueSize ?? 5000;\n this.onFlush = options.onFlush;\n\n this.timer = setInterval(() => {\n void this.flush();\n }, this.flushIntervalMs);\n }\n\n enqueue(event: VHEvent): boolean {\n if (this.destroyed) {\n return false;\n }\n\n if (this.queue.length >= this.maxQueueSize) {\n // Drop the oldest queued event to keep memory bounded.\n this.queue.shift();\n }\n\n this.queue.push(event);\n\n if (this.queue.length >= this.maxBatchSize) {\n void this.flush();\n }\n\n return true;\n }\n\n async flush(): Promise<void> {\n if (this.destroyed && this.queue.length === 0) {\n return;\n }\n\n if (this.flushing) {\n this.flushRequested = true;\n return;\n }\n\n this.flushing = true;\n\n try {\n do {\n this.flushRequested = false;\n\n if (this.queue.length === 0) {\n break;\n }\n\n const batch = this.queue.splice(0, this.maxBatchSize);\n\n try {\n await this.onFlush(batch);\n } catch {\n // Restore failed events to the front of the queue.\n this.queue.unshift(...batch);\n\n if (this.queue.length > this.maxQueueSize) {\n this.queue.length = this.maxQueueSize;\n }\n\n break;\n }\n } while (this.flushRequested || this.queue.length >= this.maxBatchSize);\n } finally {\n this.flushing = false;\n }\n }\n\n async destroy(): Promise<void> {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = undefined;\n }\n\n await this.flush();\n this.destroyed = true;\n }\n\n get size(): number {\n return this.queue.length;\n }\n}\n","import type { VHLogLevel } from \"./types.js\";\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value) ?? \"undefined\";\n }\n\n if (Array.isArray(value)) {\n return `[${value.map(stableStringify).join(\",\")}]`;\n }\n\n const record = value as Record<string, unknown>;\n\n const keys = Object.keys(record).sort();\n\n return `{${keys\n .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)\n .join(\",\")}}`;\n}\n\nexport async function createFingerprint(\n level: VHLogLevel,\n message: string,\n data?: Record<string, unknown>,\n): Promise<string> {\n const input = stableStringify({\n level,\n message,\n data: data ?? null,\n });\n\n const bytes = new TextEncoder().encode(input);\n const hash = await crypto.subtle.digest(\"SHA-256\", bytes);\n\n return Array.from(new Uint8Array(hash))\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n","import { io, type Socket } from \"socket.io-client\";\nimport type { VHEvent } from \"./types.js\";\n\nexport interface TransportOptions {\n serverUrl: string;\n timeoutMs?: number;\n}\n\ninterface BatchAck {\n ok: boolean;\n eventIds?: string[];\n error?: string;\n}\n\nexport class VHTransport {\n private readonly socket: Socket;\n private readonly timeoutMs: number;\n\n constructor(options: TransportOptions) {\n this.timeoutMs = options.timeoutMs ?? 5000;\n\n this.socket = io(options.serverUrl, {\n autoConnect: true,\n reconnection: true,\n reconnectionAttempts: Infinity,\n reconnectionDelay: 500,\n reconnectionDelayMax: 5000,\n timeout: this.timeoutMs,\n });\n\n this.socket.on(\"connect\", () => {\n console.log(\"[VH Client] Connected:\", this.socket.id);\n });\n\n this.socket.on(\"connect_error\", (error) => {\n console.error(\"[VH Client] Connection error:\", error.message);\n });\n\n this.socket.on(\"disconnect\", (reason) => {\n console.log(\"[VH Client] Disconnected:\", reason);\n });\n }\n\n private waitForConnection(): Promise<void> {\n if (this.socket.connected) {\n return Promise.resolve();\n }\n\n return new Promise((resolve, reject) => {\n const cleanup = () => {\n clearTimeout(timeout);\n this.socket.off(\"connect\", onConnect);\n this.socket.off(\"connect_error\", onConnectError);\n };\n\n const onConnect = () => {\n cleanup();\n resolve();\n };\n\n const onConnectError = (error: Error) => {\n // Socket.IO may reconnect after this; this send attempt fails\n // rather than waiting indefinitely.\n cleanup();\n reject(new Error(`Unable to connect to VH server: ${error.message}`));\n };\n\n const timeout = setTimeout(() => {\n cleanup();\n reject(\n new Error(`VH server connection timed out after ${this.timeoutMs}ms`),\n );\n }, this.timeoutMs);\n\n this.socket.once(\"connect\", onConnect);\n this.socket.once(\"connect_error\", onConnectError);\n\n // Recheck after registering listeners to avoid missing a connection\n // that happens between the initial check and listener registration.\n if (this.socket.connected) {\n onConnect();\n }\n });\n }\n\n async sendBatch(events: VHEvent[]): Promise<void> {\n if (events.length === 0) {\n return;\n }\n\n await this.waitForConnection();\n\n await new Promise<void>((resolve, reject) => {\n let settled = false;\n\n const timeout = setTimeout(() => {\n finish(\n new Error(\n `VH server acknowledgement timed out after ${this.timeoutMs}ms`,\n ),\n );\n }, this.timeoutMs);\n\n const finish = (error?: Error) => {\n if (settled) return;\n settled = true;\n clearTimeout(timeout);\n\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n };\n\n this.socket.emit(\"vh:events\", events, (ack?: BatchAck) => {\n if (!ack?.ok) {\n finish(new Error(ack?.error ?? \"VH server rejected the batch\"));\n return;\n }\n\n console.log(`[VH Client] Server acknowledged ${events.length} events`);\n\n finish();\n });\n });\n }\n\n async destroy(): Promise<void> {\n this.socket.disconnect();\n }\n\n get connected(): boolean {\n return this.socket.connected;\n }\n}\n","import { createId } from \"./core/id.js\";\nimport { EventBatcher } from \"./core/batcher.js\";\nimport { createFingerprint } from \"./core/fingerprint.js\";\nimport { VHTransport } from \"./core/transport.js\";\n\nimport type {\n VHEvent,\n VHLogLevel,\n VHNetworkRequestInput,\n VHNetworkResponseInput,\n VHOptions,\n} from \"./core/types.js\";\n\nexport type {\n VHEvent,\n VHLogLevel,\n VHLogEvent,\n VHNetworkRequestEvent,\n VHNetworkResponseEvent,\n VHOptions,\n} from \"./core/types.js\";\n\nexport interface VHClient {\n log: Record<\n VHLogLevel,\n (message: string, data?: Record<string, unknown>) => void\n >;\n\n network: {\n req(input: VHNetworkRequestInput): void;\n res(input: VHNetworkResponseInput): void;\n };\n\n flush(): Promise<void>;\n destroy(): Promise<void>;\n}\n\nexport function createVH(options: VHOptions): VHClient {\n const appName = options.appName.trim();\n\n const serverUrls = [\n ...new Set(\n (Array.isArray(options.serverUrl)\n ? options.serverUrl\n : [options.serverUrl]\n ).map((url) => url.trim()),\n ),\n ];\n\n if (!appName) {\n throw new Error(\"appName is required\");\n }\n\n if (\n serverUrls.length === 0 ||\n serverUrls.some((url) => {\n try {\n const parsed = new URL(url);\n\n return ![\"http:\", \"https:\"].includes(parsed.protocol);\n } catch {\n return true;\n }\n })\n ) {\n throw new Error(\"serverUrl must contain valid http:// or https:// URLs\");\n }\n\n const showConsoleOnLog = options.showConsoleOnLog ?? true;\n\n const transports = serverUrls.map(\n (serverUrl) => new VHTransport({ serverUrl }),\n );\n\n const batcher = new EventBatcher({\n maxBatchSize: 50,\n flushIntervalMs: 5000,\n maxQueueSize: 5000,\n\n async onFlush(events) {\n await Promise.all(\n transports.map((transport) => transport.sendBatch(events)),\n );\n },\n });\n\n let destroyed = false;\n\n const pendingLogs = new Set<Promise<void>>();\n\n async function waitForPendingLogs(): Promise<void> {\n while (pendingLogs.size > 0) {\n await Promise.all([...pendingLogs]);\n }\n }\n\n function enqueue(event: VHEvent): void {\n if (destroyed) {\n throw new Error(\"VH client has been destroyed\");\n }\n\n batcher.enqueue(event);\n }\n\n function writeLog(\n level: VHLogLevel,\n message: string,\n data?: Record<string, unknown>,\n ): void {\n if (destroyed) {\n throw new Error(\"VH client has been destroyed\");\n }\n\n if (showConsoleOnLog) {\n console[level](`[${appName}] ${message}`, data ?? \"\");\n }\n\n const pending = createFingerprint(level, message, data)\n .then((fingerprint) => {\n if (destroyed) {\n return;\n }\n\n enqueue({\n type: \"log\",\n eventId: createId(),\n fingerprint,\n appName,\n level,\n message,\n ...(data === undefined ? {} : { data }),\n timestamp: Date.now(),\n });\n })\n .catch((error: unknown) => {\n console.error(\"[vh-dev-client] Failed to create log event\", error);\n })\n .then(() => {\n pendingLogs.delete(pending);\n });\n\n pendingLogs.add(pending);\n }\n\n const log: VHClient[\"log\"] = {\n debug: (message, data) => writeLog(\"debug\", message, data),\n info: (message, data) => writeLog(\"info\", message, data),\n warn: (message, data) => writeLog(\"warn\", message, data),\n error: (message, data) => writeLog(\"error\", message, data),\n };\n\n return {\n log,\n\n network: {\n req(input) {\n enqueue({\n type: \"network:req\",\n eventId: createId(),\n appName,\n requestId: input.requestId,\n method: input.method,\n url: input.url,\n timestamp: Date.now(),\n });\n },\n\n res(input) {\n enqueue({\n type: \"network:res\",\n eventId: createId(),\n appName,\n requestId: input.requestId,\n ...(input.status === undefined ? {} : { status: input.status }),\n ...(input.durationMs === undefined\n ? {}\n : { durationMs: input.durationMs }),\n ...(input.error === undefined ? {} : { error: input.error }),\n timestamp: Date.now(),\n });\n },\n },\n\n async flush() {\n await waitForPendingLogs();\n await batcher.flush();\n },\n\n async destroy() {\n if (destroyed) {\n return;\n }\n\n await waitForPendingLogs();\n await batcher.destroy();\n\n destroyed = true;\n\n await Promise.all(transports.map((transport) => transport.destroy()));\n },\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
type VHLogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
+
interface VHBaseEvent {
|
|
3
|
+
eventId: string;
|
|
4
|
+
appName: string;
|
|
5
|
+
timestamp: number;
|
|
6
|
+
}
|
|
7
|
+
interface VHLogEvent extends VHBaseEvent {
|
|
8
|
+
type: "log";
|
|
9
|
+
level: VHLogLevel;
|
|
10
|
+
message: string;
|
|
11
|
+
data?: Record<string, unknown>;
|
|
12
|
+
fingerprint: string;
|
|
13
|
+
}
|
|
14
|
+
interface VHNetworkRequestEvent extends VHBaseEvent {
|
|
15
|
+
type: "network:req";
|
|
16
|
+
requestId: string;
|
|
17
|
+
method: string;
|
|
18
|
+
url: string;
|
|
19
|
+
}
|
|
20
|
+
interface VHNetworkResponseEvent extends VHBaseEvent {
|
|
21
|
+
type: "network:res";
|
|
22
|
+
requestId: string;
|
|
23
|
+
status?: number;
|
|
24
|
+
durationMs?: number;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
type VHEvent = VHLogEvent | VHNetworkRequestEvent | VHNetworkResponseEvent;
|
|
28
|
+
interface VHOptions {
|
|
29
|
+
appName: string;
|
|
30
|
+
serverUrl: string | string[];
|
|
31
|
+
showConsoleOnLog?: boolean;
|
|
32
|
+
}
|
|
33
|
+
interface VHNetworkRequestInput {
|
|
34
|
+
requestId: string;
|
|
35
|
+
method: string;
|
|
36
|
+
url: string;
|
|
37
|
+
}
|
|
38
|
+
interface VHNetworkResponseInput {
|
|
39
|
+
requestId: string;
|
|
40
|
+
status?: number;
|
|
41
|
+
durationMs?: number;
|
|
42
|
+
error?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface VHClient {
|
|
46
|
+
log: Record<VHLogLevel, (message: string, data?: Record<string, unknown>) => void>;
|
|
47
|
+
network: {
|
|
48
|
+
req(input: VHNetworkRequestInput): void;
|
|
49
|
+
res(input: VHNetworkResponseInput): void;
|
|
50
|
+
};
|
|
51
|
+
flush(): Promise<void>;
|
|
52
|
+
destroy(): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
declare function createVH(options: VHOptions): VHClient;
|
|
55
|
+
|
|
56
|
+
export { type VHClient, type VHEvent, type VHLogEvent, type VHLogLevel, type VHNetworkRequestEvent, type VHNetworkResponseEvent, type VHOptions, createVH };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// src/core/id.ts
|
|
2
|
+
function createId() {
|
|
3
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
4
|
+
return globalThis.crypto.randomUUID();
|
|
5
|
+
}
|
|
6
|
+
throw new Error(
|
|
7
|
+
"Secure randomUUID is unavailable. Use a modern runtime with Web Crypto support."
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/core/batcher.ts
|
|
12
|
+
var EventBatcher = class {
|
|
13
|
+
queue = [];
|
|
14
|
+
maxBatchSize;
|
|
15
|
+
flushIntervalMs;
|
|
16
|
+
maxQueueSize;
|
|
17
|
+
onFlush;
|
|
18
|
+
timer;
|
|
19
|
+
flushing = false;
|
|
20
|
+
flushRequested = false;
|
|
21
|
+
destroyed = false;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.maxBatchSize = options.maxBatchSize ?? 50;
|
|
24
|
+
this.flushIntervalMs = options.flushIntervalMs ?? 5e3;
|
|
25
|
+
this.maxQueueSize = options.maxQueueSize ?? 5e3;
|
|
26
|
+
this.onFlush = options.onFlush;
|
|
27
|
+
this.timer = setInterval(() => {
|
|
28
|
+
void this.flush();
|
|
29
|
+
}, this.flushIntervalMs);
|
|
30
|
+
}
|
|
31
|
+
enqueue(event) {
|
|
32
|
+
if (this.destroyed) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (this.queue.length >= this.maxQueueSize) {
|
|
36
|
+
this.queue.shift();
|
|
37
|
+
}
|
|
38
|
+
this.queue.push(event);
|
|
39
|
+
if (this.queue.length >= this.maxBatchSize) {
|
|
40
|
+
void this.flush();
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
async flush() {
|
|
45
|
+
if (this.destroyed && this.queue.length === 0) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (this.flushing) {
|
|
49
|
+
this.flushRequested = true;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.flushing = true;
|
|
53
|
+
try {
|
|
54
|
+
do {
|
|
55
|
+
this.flushRequested = false;
|
|
56
|
+
if (this.queue.length === 0) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
const batch = this.queue.splice(0, this.maxBatchSize);
|
|
60
|
+
try {
|
|
61
|
+
await this.onFlush(batch);
|
|
62
|
+
} catch {
|
|
63
|
+
this.queue.unshift(...batch);
|
|
64
|
+
if (this.queue.length > this.maxQueueSize) {
|
|
65
|
+
this.queue.length = this.maxQueueSize;
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
} while (this.flushRequested || this.queue.length >= this.maxBatchSize);
|
|
70
|
+
} finally {
|
|
71
|
+
this.flushing = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async destroy() {
|
|
75
|
+
if (this.timer) {
|
|
76
|
+
clearInterval(this.timer);
|
|
77
|
+
this.timer = void 0;
|
|
78
|
+
}
|
|
79
|
+
await this.flush();
|
|
80
|
+
this.destroyed = true;
|
|
81
|
+
}
|
|
82
|
+
get size() {
|
|
83
|
+
return this.queue.length;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// src/core/fingerprint.ts
|
|
88
|
+
function stableStringify(value) {
|
|
89
|
+
if (value === null || typeof value !== "object") {
|
|
90
|
+
return JSON.stringify(value) ?? "undefined";
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(value)) {
|
|
93
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
94
|
+
}
|
|
95
|
+
const record = value;
|
|
96
|
+
const keys = Object.keys(record).sort();
|
|
97
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
|
|
98
|
+
}
|
|
99
|
+
async function createFingerprint(level, message, data) {
|
|
100
|
+
const input = stableStringify({
|
|
101
|
+
level,
|
|
102
|
+
message,
|
|
103
|
+
data: data ?? null
|
|
104
|
+
});
|
|
105
|
+
const bytes = new TextEncoder().encode(input);
|
|
106
|
+
const hash = await crypto.subtle.digest("SHA-256", bytes);
|
|
107
|
+
return Array.from(new Uint8Array(hash)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/core/transport.ts
|
|
111
|
+
import { io } from "socket.io-client";
|
|
112
|
+
var VHTransport = class {
|
|
113
|
+
socket;
|
|
114
|
+
timeoutMs;
|
|
115
|
+
constructor(options) {
|
|
116
|
+
this.timeoutMs = options.timeoutMs ?? 5e3;
|
|
117
|
+
this.socket = io(options.serverUrl, {
|
|
118
|
+
autoConnect: true,
|
|
119
|
+
reconnection: true,
|
|
120
|
+
reconnectionAttempts: Infinity,
|
|
121
|
+
reconnectionDelay: 500,
|
|
122
|
+
reconnectionDelayMax: 5e3,
|
|
123
|
+
timeout: this.timeoutMs
|
|
124
|
+
});
|
|
125
|
+
this.socket.on("connect", () => {
|
|
126
|
+
console.log("[VH Client] Connected:", this.socket.id);
|
|
127
|
+
});
|
|
128
|
+
this.socket.on("connect_error", (error) => {
|
|
129
|
+
console.error("[VH Client] Connection error:", error.message);
|
|
130
|
+
});
|
|
131
|
+
this.socket.on("disconnect", (reason) => {
|
|
132
|
+
console.log("[VH Client] Disconnected:", reason);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
waitForConnection() {
|
|
136
|
+
if (this.socket.connected) {
|
|
137
|
+
return Promise.resolve();
|
|
138
|
+
}
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const cleanup = () => {
|
|
141
|
+
clearTimeout(timeout);
|
|
142
|
+
this.socket.off("connect", onConnect);
|
|
143
|
+
this.socket.off("connect_error", onConnectError);
|
|
144
|
+
};
|
|
145
|
+
const onConnect = () => {
|
|
146
|
+
cleanup();
|
|
147
|
+
resolve();
|
|
148
|
+
};
|
|
149
|
+
const onConnectError = (error) => {
|
|
150
|
+
cleanup();
|
|
151
|
+
reject(new Error(`Unable to connect to VH server: ${error.message}`));
|
|
152
|
+
};
|
|
153
|
+
const timeout = setTimeout(() => {
|
|
154
|
+
cleanup();
|
|
155
|
+
reject(
|
|
156
|
+
new Error(`VH server connection timed out after ${this.timeoutMs}ms`)
|
|
157
|
+
);
|
|
158
|
+
}, this.timeoutMs);
|
|
159
|
+
this.socket.once("connect", onConnect);
|
|
160
|
+
this.socket.once("connect_error", onConnectError);
|
|
161
|
+
if (this.socket.connected) {
|
|
162
|
+
onConnect();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async sendBatch(events) {
|
|
167
|
+
if (events.length === 0) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await this.waitForConnection();
|
|
171
|
+
await new Promise((resolve, reject) => {
|
|
172
|
+
let settled = false;
|
|
173
|
+
const timeout = setTimeout(() => {
|
|
174
|
+
finish(
|
|
175
|
+
new Error(
|
|
176
|
+
`VH server acknowledgement timed out after ${this.timeoutMs}ms`
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
}, this.timeoutMs);
|
|
180
|
+
const finish = (error) => {
|
|
181
|
+
if (settled) return;
|
|
182
|
+
settled = true;
|
|
183
|
+
clearTimeout(timeout);
|
|
184
|
+
if (error) {
|
|
185
|
+
reject(error);
|
|
186
|
+
} else {
|
|
187
|
+
resolve();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
this.socket.emit("vh:events", events, (ack) => {
|
|
191
|
+
if (!ack?.ok) {
|
|
192
|
+
finish(new Error(ack?.error ?? "VH server rejected the batch"));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
console.log(`[VH Client] Server acknowledged ${events.length} events`);
|
|
196
|
+
finish();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
async destroy() {
|
|
201
|
+
this.socket.disconnect();
|
|
202
|
+
}
|
|
203
|
+
get connected() {
|
|
204
|
+
return this.socket.connected;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// src/index.ts
|
|
209
|
+
function createVH(options) {
|
|
210
|
+
const appName = options.appName.trim();
|
|
211
|
+
const serverUrls = [
|
|
212
|
+
...new Set(
|
|
213
|
+
(Array.isArray(options.serverUrl) ? options.serverUrl : [options.serverUrl]).map((url) => url.trim())
|
|
214
|
+
)
|
|
215
|
+
];
|
|
216
|
+
if (!appName) {
|
|
217
|
+
throw new Error("appName is required");
|
|
218
|
+
}
|
|
219
|
+
if (serverUrls.length === 0 || serverUrls.some((url) => {
|
|
220
|
+
try {
|
|
221
|
+
const parsed = new URL(url);
|
|
222
|
+
return !["http:", "https:"].includes(parsed.protocol);
|
|
223
|
+
} catch {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
})) {
|
|
227
|
+
throw new Error("serverUrl must contain valid http:// or https:// URLs");
|
|
228
|
+
}
|
|
229
|
+
const showConsoleOnLog = options.showConsoleOnLog ?? true;
|
|
230
|
+
const transports = serverUrls.map(
|
|
231
|
+
(serverUrl) => new VHTransport({ serverUrl })
|
|
232
|
+
);
|
|
233
|
+
const batcher = new EventBatcher({
|
|
234
|
+
maxBatchSize: 50,
|
|
235
|
+
flushIntervalMs: 5e3,
|
|
236
|
+
maxQueueSize: 5e3,
|
|
237
|
+
async onFlush(events) {
|
|
238
|
+
await Promise.all(
|
|
239
|
+
transports.map((transport) => transport.sendBatch(events))
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
let destroyed = false;
|
|
244
|
+
const pendingLogs = /* @__PURE__ */ new Set();
|
|
245
|
+
async function waitForPendingLogs() {
|
|
246
|
+
while (pendingLogs.size > 0) {
|
|
247
|
+
await Promise.all([...pendingLogs]);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function enqueue(event) {
|
|
251
|
+
if (destroyed) {
|
|
252
|
+
throw new Error("VH client has been destroyed");
|
|
253
|
+
}
|
|
254
|
+
batcher.enqueue(event);
|
|
255
|
+
}
|
|
256
|
+
function writeLog(level, message, data) {
|
|
257
|
+
if (destroyed) {
|
|
258
|
+
throw new Error("VH client has been destroyed");
|
|
259
|
+
}
|
|
260
|
+
if (showConsoleOnLog) {
|
|
261
|
+
console[level](`[${appName}] ${message}`, data ?? "");
|
|
262
|
+
}
|
|
263
|
+
const pending = createFingerprint(level, message, data).then((fingerprint) => {
|
|
264
|
+
if (destroyed) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
enqueue({
|
|
268
|
+
type: "log",
|
|
269
|
+
eventId: createId(),
|
|
270
|
+
fingerprint,
|
|
271
|
+
appName,
|
|
272
|
+
level,
|
|
273
|
+
message,
|
|
274
|
+
...data === void 0 ? {} : { data },
|
|
275
|
+
timestamp: Date.now()
|
|
276
|
+
});
|
|
277
|
+
}).catch((error) => {
|
|
278
|
+
console.error("[vh-dev-client] Failed to create log event", error);
|
|
279
|
+
}).then(() => {
|
|
280
|
+
pendingLogs.delete(pending);
|
|
281
|
+
});
|
|
282
|
+
pendingLogs.add(pending);
|
|
283
|
+
}
|
|
284
|
+
const log = {
|
|
285
|
+
debug: (message, data) => writeLog("debug", message, data),
|
|
286
|
+
info: (message, data) => writeLog("info", message, data),
|
|
287
|
+
warn: (message, data) => writeLog("warn", message, data),
|
|
288
|
+
error: (message, data) => writeLog("error", message, data)
|
|
289
|
+
};
|
|
290
|
+
return {
|
|
291
|
+
log,
|
|
292
|
+
network: {
|
|
293
|
+
req(input) {
|
|
294
|
+
enqueue({
|
|
295
|
+
type: "network:req",
|
|
296
|
+
eventId: createId(),
|
|
297
|
+
appName,
|
|
298
|
+
requestId: input.requestId,
|
|
299
|
+
method: input.method,
|
|
300
|
+
url: input.url,
|
|
301
|
+
timestamp: Date.now()
|
|
302
|
+
});
|
|
303
|
+
},
|
|
304
|
+
res(input) {
|
|
305
|
+
enqueue({
|
|
306
|
+
type: "network:res",
|
|
307
|
+
eventId: createId(),
|
|
308
|
+
appName,
|
|
309
|
+
requestId: input.requestId,
|
|
310
|
+
...input.status === void 0 ? {} : { status: input.status },
|
|
311
|
+
...input.durationMs === void 0 ? {} : { durationMs: input.durationMs },
|
|
312
|
+
...input.error === void 0 ? {} : { error: input.error },
|
|
313
|
+
timestamp: Date.now()
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
async flush() {
|
|
318
|
+
await waitForPendingLogs();
|
|
319
|
+
await batcher.flush();
|
|
320
|
+
},
|
|
321
|
+
async destroy() {
|
|
322
|
+
if (destroyed) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
await waitForPendingLogs();
|
|
326
|
+
await batcher.destroy();
|
|
327
|
+
destroyed = true;
|
|
328
|
+
await Promise.all(transports.map((transport) => transport.destroy()));
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
export {
|
|
333
|
+
createVH
|
|
334
|
+
};
|
|
335
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/id.ts","../src/core/batcher.ts","../src/core/fingerprint.ts","../src/core/transport.ts","../src/index.ts"],"sourcesContent":["export function createId(): string {\n if (typeof globalThis.crypto?.randomUUID === \"function\") {\n return globalThis.crypto.randomUUID();\n }\n\n throw new Error(\n \"Secure randomUUID is unavailable. Use a modern runtime with Web Crypto support.\",\n );\n}\n","import type { VHEvent } from \"./types.js\";\n\nexport interface BatcherOptions {\n maxBatchSize?: number;\n flushIntervalMs?: number;\n maxQueueSize?: number;\n onFlush: (events: VHEvent[]) => Promise<void>;\n}\n\nexport class EventBatcher {\n private queue: VHEvent[] = [];\n private readonly maxBatchSize: number;\n private readonly flushIntervalMs: number;\n private readonly maxQueueSize: number;\n private readonly onFlush: BatcherOptions[\"onFlush\"];\n\n private timer: ReturnType<typeof setInterval> | undefined;\n private flushing = false;\n private flushRequested = false;\n private destroyed = false;\n\n constructor(options: BatcherOptions) {\n this.maxBatchSize = options.maxBatchSize ?? 50;\n this.flushIntervalMs = options.flushIntervalMs ?? 5000;\n this.maxQueueSize = options.maxQueueSize ?? 5000;\n this.onFlush = options.onFlush;\n\n this.timer = setInterval(() => {\n void this.flush();\n }, this.flushIntervalMs);\n }\n\n enqueue(event: VHEvent): boolean {\n if (this.destroyed) {\n return false;\n }\n\n if (this.queue.length >= this.maxQueueSize) {\n // Drop the oldest queued event to keep memory bounded.\n this.queue.shift();\n }\n\n this.queue.push(event);\n\n if (this.queue.length >= this.maxBatchSize) {\n void this.flush();\n }\n\n return true;\n }\n\n async flush(): Promise<void> {\n if (this.destroyed && this.queue.length === 0) {\n return;\n }\n\n if (this.flushing) {\n this.flushRequested = true;\n return;\n }\n\n this.flushing = true;\n\n try {\n do {\n this.flushRequested = false;\n\n if (this.queue.length === 0) {\n break;\n }\n\n const batch = this.queue.splice(0, this.maxBatchSize);\n\n try {\n await this.onFlush(batch);\n } catch {\n // Restore failed events to the front of the queue.\n this.queue.unshift(...batch);\n\n if (this.queue.length > this.maxQueueSize) {\n this.queue.length = this.maxQueueSize;\n }\n\n break;\n }\n } while (this.flushRequested || this.queue.length >= this.maxBatchSize);\n } finally {\n this.flushing = false;\n }\n }\n\n async destroy(): Promise<void> {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = undefined;\n }\n\n await this.flush();\n this.destroyed = true;\n }\n\n get size(): number {\n return this.queue.length;\n }\n}\n","import type { VHLogLevel } from \"./types.js\";\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value) ?? \"undefined\";\n }\n\n if (Array.isArray(value)) {\n return `[${value.map(stableStringify).join(\",\")}]`;\n }\n\n const record = value as Record<string, unknown>;\n\n const keys = Object.keys(record).sort();\n\n return `{${keys\n .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)\n .join(\",\")}}`;\n}\n\nexport async function createFingerprint(\n level: VHLogLevel,\n message: string,\n data?: Record<string, unknown>,\n): Promise<string> {\n const input = stableStringify({\n level,\n message,\n data: data ?? null,\n });\n\n const bytes = new TextEncoder().encode(input);\n const hash = await crypto.subtle.digest(\"SHA-256\", bytes);\n\n return Array.from(new Uint8Array(hash))\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n","import { io, type Socket } from \"socket.io-client\";\nimport type { VHEvent } from \"./types.js\";\n\nexport interface TransportOptions {\n serverUrl: string;\n timeoutMs?: number;\n}\n\ninterface BatchAck {\n ok: boolean;\n eventIds?: string[];\n error?: string;\n}\n\nexport class VHTransport {\n private readonly socket: Socket;\n private readonly timeoutMs: number;\n\n constructor(options: TransportOptions) {\n this.timeoutMs = options.timeoutMs ?? 5000;\n\n this.socket = io(options.serverUrl, {\n autoConnect: true,\n reconnection: true,\n reconnectionAttempts: Infinity,\n reconnectionDelay: 500,\n reconnectionDelayMax: 5000,\n timeout: this.timeoutMs,\n });\n\n this.socket.on(\"connect\", () => {\n console.log(\"[VH Client] Connected:\", this.socket.id);\n });\n\n this.socket.on(\"connect_error\", (error) => {\n console.error(\"[VH Client] Connection error:\", error.message);\n });\n\n this.socket.on(\"disconnect\", (reason) => {\n console.log(\"[VH Client] Disconnected:\", reason);\n });\n }\n\n private waitForConnection(): Promise<void> {\n if (this.socket.connected) {\n return Promise.resolve();\n }\n\n return new Promise((resolve, reject) => {\n const cleanup = () => {\n clearTimeout(timeout);\n this.socket.off(\"connect\", onConnect);\n this.socket.off(\"connect_error\", onConnectError);\n };\n\n const onConnect = () => {\n cleanup();\n resolve();\n };\n\n const onConnectError = (error: Error) => {\n // Socket.IO may reconnect after this; this send attempt fails\n // rather than waiting indefinitely.\n cleanup();\n reject(new Error(`Unable to connect to VH server: ${error.message}`));\n };\n\n const timeout = setTimeout(() => {\n cleanup();\n reject(\n new Error(`VH server connection timed out after ${this.timeoutMs}ms`),\n );\n }, this.timeoutMs);\n\n this.socket.once(\"connect\", onConnect);\n this.socket.once(\"connect_error\", onConnectError);\n\n // Recheck after registering listeners to avoid missing a connection\n // that happens between the initial check and listener registration.\n if (this.socket.connected) {\n onConnect();\n }\n });\n }\n\n async sendBatch(events: VHEvent[]): Promise<void> {\n if (events.length === 0) {\n return;\n }\n\n await this.waitForConnection();\n\n await new Promise<void>((resolve, reject) => {\n let settled = false;\n\n const timeout = setTimeout(() => {\n finish(\n new Error(\n `VH server acknowledgement timed out after ${this.timeoutMs}ms`,\n ),\n );\n }, this.timeoutMs);\n\n const finish = (error?: Error) => {\n if (settled) return;\n settled = true;\n clearTimeout(timeout);\n\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n };\n\n this.socket.emit(\"vh:events\", events, (ack?: BatchAck) => {\n if (!ack?.ok) {\n finish(new Error(ack?.error ?? \"VH server rejected the batch\"));\n return;\n }\n\n console.log(`[VH Client] Server acknowledged ${events.length} events`);\n\n finish();\n });\n });\n }\n\n async destroy(): Promise<void> {\n this.socket.disconnect();\n }\n\n get connected(): boolean {\n return this.socket.connected;\n }\n}\n","import { createId } from \"./core/id.js\";\nimport { EventBatcher } from \"./core/batcher.js\";\nimport { createFingerprint } from \"./core/fingerprint.js\";\nimport { VHTransport } from \"./core/transport.js\";\n\nimport type {\n VHEvent,\n VHLogLevel,\n VHNetworkRequestInput,\n VHNetworkResponseInput,\n VHOptions,\n} from \"./core/types.js\";\n\nexport type {\n VHEvent,\n VHLogLevel,\n VHLogEvent,\n VHNetworkRequestEvent,\n VHNetworkResponseEvent,\n VHOptions,\n} from \"./core/types.js\";\n\nexport interface VHClient {\n log: Record<\n VHLogLevel,\n (message: string, data?: Record<string, unknown>) => void\n >;\n\n network: {\n req(input: VHNetworkRequestInput): void;\n res(input: VHNetworkResponseInput): void;\n };\n\n flush(): Promise<void>;\n destroy(): Promise<void>;\n}\n\nexport function createVH(options: VHOptions): VHClient {\n const appName = options.appName.trim();\n\n const serverUrls = [\n ...new Set(\n (Array.isArray(options.serverUrl)\n ? options.serverUrl\n : [options.serverUrl]\n ).map((url) => url.trim()),\n ),\n ];\n\n if (!appName) {\n throw new Error(\"appName is required\");\n }\n\n if (\n serverUrls.length === 0 ||\n serverUrls.some((url) => {\n try {\n const parsed = new URL(url);\n\n return ![\"http:\", \"https:\"].includes(parsed.protocol);\n } catch {\n return true;\n }\n })\n ) {\n throw new Error(\"serverUrl must contain valid http:// or https:// URLs\");\n }\n\n const showConsoleOnLog = options.showConsoleOnLog ?? true;\n\n const transports = serverUrls.map(\n (serverUrl) => new VHTransport({ serverUrl }),\n );\n\n const batcher = new EventBatcher({\n maxBatchSize: 50,\n flushIntervalMs: 5000,\n maxQueueSize: 5000,\n\n async onFlush(events) {\n await Promise.all(\n transports.map((transport) => transport.sendBatch(events)),\n );\n },\n });\n\n let destroyed = false;\n\n const pendingLogs = new Set<Promise<void>>();\n\n async function waitForPendingLogs(): Promise<void> {\n while (pendingLogs.size > 0) {\n await Promise.all([...pendingLogs]);\n }\n }\n\n function enqueue(event: VHEvent): void {\n if (destroyed) {\n throw new Error(\"VH client has been destroyed\");\n }\n\n batcher.enqueue(event);\n }\n\n function writeLog(\n level: VHLogLevel,\n message: string,\n data?: Record<string, unknown>,\n ): void {\n if (destroyed) {\n throw new Error(\"VH client has been destroyed\");\n }\n\n if (showConsoleOnLog) {\n console[level](`[${appName}] ${message}`, data ?? \"\");\n }\n\n const pending = createFingerprint(level, message, data)\n .then((fingerprint) => {\n if (destroyed) {\n return;\n }\n\n enqueue({\n type: \"log\",\n eventId: createId(),\n fingerprint,\n appName,\n level,\n message,\n ...(data === undefined ? {} : { data }),\n timestamp: Date.now(),\n });\n })\n .catch((error: unknown) => {\n console.error(\"[vh-dev-client] Failed to create log event\", error);\n })\n .then(() => {\n pendingLogs.delete(pending);\n });\n\n pendingLogs.add(pending);\n }\n\n const log: VHClient[\"log\"] = {\n debug: (message, data) => writeLog(\"debug\", message, data),\n info: (message, data) => writeLog(\"info\", message, data),\n warn: (message, data) => writeLog(\"warn\", message, data),\n error: (message, data) => writeLog(\"error\", message, data),\n };\n\n return {\n log,\n\n network: {\n req(input) {\n enqueue({\n type: \"network:req\",\n eventId: createId(),\n appName,\n requestId: input.requestId,\n method: input.method,\n url: input.url,\n timestamp: Date.now(),\n });\n },\n\n res(input) {\n enqueue({\n type: \"network:res\",\n eventId: createId(),\n appName,\n requestId: input.requestId,\n ...(input.status === undefined ? {} : { status: input.status }),\n ...(input.durationMs === undefined\n ? {}\n : { durationMs: input.durationMs }),\n ...(input.error === undefined ? {} : { error: input.error }),\n timestamp: Date.now(),\n });\n },\n },\n\n async flush() {\n await waitForPendingLogs();\n await batcher.flush();\n },\n\n async destroy() {\n if (destroyed) {\n return;\n }\n\n await waitForPendingLogs();\n await batcher.destroy();\n\n destroyed = true;\n\n await Promise.all(transports.map((transport) => transport.destroy()));\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { VHClient, VHEvent, VHLogEvent, VHLogLevel, VHNetworkRequestEvent, VHNetworkResponseEvent, VHOptions, createVH } from '../index.js';
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// src/core/id.ts
|
|
2
|
+
function createId() {
|
|
3
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
4
|
+
return globalThis.crypto.randomUUID();
|
|
5
|
+
}
|
|
6
|
+
throw new Error(
|
|
7
|
+
"Secure randomUUID is unavailable. Use a modern runtime with Web Crypto support."
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/core/batcher.ts
|
|
12
|
+
var EventBatcher = class {
|
|
13
|
+
queue = [];
|
|
14
|
+
maxBatchSize;
|
|
15
|
+
flushIntervalMs;
|
|
16
|
+
maxQueueSize;
|
|
17
|
+
onFlush;
|
|
18
|
+
timer;
|
|
19
|
+
flushing = false;
|
|
20
|
+
flushRequested = false;
|
|
21
|
+
destroyed = false;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.maxBatchSize = options.maxBatchSize ?? 50;
|
|
24
|
+
this.flushIntervalMs = options.flushIntervalMs ?? 5e3;
|
|
25
|
+
this.maxQueueSize = options.maxQueueSize ?? 5e3;
|
|
26
|
+
this.onFlush = options.onFlush;
|
|
27
|
+
this.timer = setInterval(() => {
|
|
28
|
+
void this.flush();
|
|
29
|
+
}, this.flushIntervalMs);
|
|
30
|
+
}
|
|
31
|
+
enqueue(event) {
|
|
32
|
+
if (this.destroyed) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (this.queue.length >= this.maxQueueSize) {
|
|
36
|
+
this.queue.shift();
|
|
37
|
+
}
|
|
38
|
+
this.queue.push(event);
|
|
39
|
+
if (this.queue.length >= this.maxBatchSize) {
|
|
40
|
+
void this.flush();
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
async flush() {
|
|
45
|
+
if (this.destroyed && this.queue.length === 0) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (this.flushing) {
|
|
49
|
+
this.flushRequested = true;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.flushing = true;
|
|
53
|
+
try {
|
|
54
|
+
do {
|
|
55
|
+
this.flushRequested = false;
|
|
56
|
+
if (this.queue.length === 0) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
const batch = this.queue.splice(0, this.maxBatchSize);
|
|
60
|
+
try {
|
|
61
|
+
await this.onFlush(batch);
|
|
62
|
+
} catch {
|
|
63
|
+
this.queue.unshift(...batch);
|
|
64
|
+
if (this.queue.length > this.maxQueueSize) {
|
|
65
|
+
this.queue.length = this.maxQueueSize;
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
} while (this.flushRequested || this.queue.length >= this.maxBatchSize);
|
|
70
|
+
} finally {
|
|
71
|
+
this.flushing = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async destroy() {
|
|
75
|
+
if (this.timer) {
|
|
76
|
+
clearInterval(this.timer);
|
|
77
|
+
this.timer = void 0;
|
|
78
|
+
}
|
|
79
|
+
await this.flush();
|
|
80
|
+
this.destroyed = true;
|
|
81
|
+
}
|
|
82
|
+
get size() {
|
|
83
|
+
return this.queue.length;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// src/core/fingerprint.ts
|
|
88
|
+
function stableStringify(value) {
|
|
89
|
+
if (value === null || typeof value !== "object") {
|
|
90
|
+
return JSON.stringify(value) ?? "undefined";
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(value)) {
|
|
93
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
94
|
+
}
|
|
95
|
+
const record = value;
|
|
96
|
+
const keys = Object.keys(record).sort();
|
|
97
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
|
|
98
|
+
}
|
|
99
|
+
async function createFingerprint(level, message, data) {
|
|
100
|
+
const input = stableStringify({
|
|
101
|
+
level,
|
|
102
|
+
message,
|
|
103
|
+
data: data ?? null
|
|
104
|
+
});
|
|
105
|
+
const bytes = new TextEncoder().encode(input);
|
|
106
|
+
const hash = await crypto.subtle.digest("SHA-256", bytes);
|
|
107
|
+
return Array.from(new Uint8Array(hash)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/core/transport.ts
|
|
111
|
+
import { io } from "socket.io-client";
|
|
112
|
+
var VHTransport = class {
|
|
113
|
+
socket;
|
|
114
|
+
timeoutMs;
|
|
115
|
+
constructor(options) {
|
|
116
|
+
this.timeoutMs = options.timeoutMs ?? 5e3;
|
|
117
|
+
this.socket = io(options.serverUrl, {
|
|
118
|
+
autoConnect: true,
|
|
119
|
+
reconnection: true,
|
|
120
|
+
reconnectionAttempts: Infinity,
|
|
121
|
+
reconnectionDelay: 500,
|
|
122
|
+
reconnectionDelayMax: 5e3,
|
|
123
|
+
timeout: this.timeoutMs
|
|
124
|
+
});
|
|
125
|
+
this.socket.on("connect", () => {
|
|
126
|
+
console.log("[VH Client] Connected:", this.socket.id);
|
|
127
|
+
});
|
|
128
|
+
this.socket.on("connect_error", (error) => {
|
|
129
|
+
console.error("[VH Client] Connection error:", error.message);
|
|
130
|
+
});
|
|
131
|
+
this.socket.on("disconnect", (reason) => {
|
|
132
|
+
console.log("[VH Client] Disconnected:", reason);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
waitForConnection() {
|
|
136
|
+
if (this.socket.connected) {
|
|
137
|
+
return Promise.resolve();
|
|
138
|
+
}
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const cleanup = () => {
|
|
141
|
+
clearTimeout(timeout);
|
|
142
|
+
this.socket.off("connect", onConnect);
|
|
143
|
+
this.socket.off("connect_error", onConnectError);
|
|
144
|
+
};
|
|
145
|
+
const onConnect = () => {
|
|
146
|
+
cleanup();
|
|
147
|
+
resolve();
|
|
148
|
+
};
|
|
149
|
+
const onConnectError = (error) => {
|
|
150
|
+
cleanup();
|
|
151
|
+
reject(new Error(`Unable to connect to VH server: ${error.message}`));
|
|
152
|
+
};
|
|
153
|
+
const timeout = setTimeout(() => {
|
|
154
|
+
cleanup();
|
|
155
|
+
reject(
|
|
156
|
+
new Error(`VH server connection timed out after ${this.timeoutMs}ms`)
|
|
157
|
+
);
|
|
158
|
+
}, this.timeoutMs);
|
|
159
|
+
this.socket.once("connect", onConnect);
|
|
160
|
+
this.socket.once("connect_error", onConnectError);
|
|
161
|
+
if (this.socket.connected) {
|
|
162
|
+
onConnect();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async sendBatch(events) {
|
|
167
|
+
if (events.length === 0) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await this.waitForConnection();
|
|
171
|
+
await new Promise((resolve, reject) => {
|
|
172
|
+
let settled = false;
|
|
173
|
+
const timeout = setTimeout(() => {
|
|
174
|
+
finish(
|
|
175
|
+
new Error(
|
|
176
|
+
`VH server acknowledgement timed out after ${this.timeoutMs}ms`
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
}, this.timeoutMs);
|
|
180
|
+
const finish = (error) => {
|
|
181
|
+
if (settled) return;
|
|
182
|
+
settled = true;
|
|
183
|
+
clearTimeout(timeout);
|
|
184
|
+
if (error) {
|
|
185
|
+
reject(error);
|
|
186
|
+
} else {
|
|
187
|
+
resolve();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
this.socket.emit("vh:events", events, (ack) => {
|
|
191
|
+
if (!ack?.ok) {
|
|
192
|
+
finish(new Error(ack?.error ?? "VH server rejected the batch"));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
console.log(`[VH Client] Server acknowledged ${events.length} events`);
|
|
196
|
+
finish();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
async destroy() {
|
|
201
|
+
this.socket.disconnect();
|
|
202
|
+
}
|
|
203
|
+
get connected() {
|
|
204
|
+
return this.socket.connected;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// src/index.ts
|
|
209
|
+
function createVH(options) {
|
|
210
|
+
const appName = options.appName.trim();
|
|
211
|
+
const serverUrls = [
|
|
212
|
+
...new Set(
|
|
213
|
+
(Array.isArray(options.serverUrl) ? options.serverUrl : [options.serverUrl]).map((url) => url.trim())
|
|
214
|
+
)
|
|
215
|
+
];
|
|
216
|
+
if (!appName) {
|
|
217
|
+
throw new Error("appName is required");
|
|
218
|
+
}
|
|
219
|
+
if (serverUrls.length === 0 || serverUrls.some((url) => {
|
|
220
|
+
try {
|
|
221
|
+
const parsed = new URL(url);
|
|
222
|
+
return !["http:", "https:"].includes(parsed.protocol);
|
|
223
|
+
} catch {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
})) {
|
|
227
|
+
throw new Error("serverUrl must contain valid http:// or https:// URLs");
|
|
228
|
+
}
|
|
229
|
+
const showConsoleOnLog = options.showConsoleOnLog ?? true;
|
|
230
|
+
const transports = serverUrls.map(
|
|
231
|
+
(serverUrl) => new VHTransport({ serverUrl })
|
|
232
|
+
);
|
|
233
|
+
const batcher = new EventBatcher({
|
|
234
|
+
maxBatchSize: 50,
|
|
235
|
+
flushIntervalMs: 5e3,
|
|
236
|
+
maxQueueSize: 5e3,
|
|
237
|
+
async onFlush(events) {
|
|
238
|
+
await Promise.all(
|
|
239
|
+
transports.map((transport) => transport.sendBatch(events))
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
let destroyed = false;
|
|
244
|
+
const pendingLogs = /* @__PURE__ */ new Set();
|
|
245
|
+
async function waitForPendingLogs() {
|
|
246
|
+
while (pendingLogs.size > 0) {
|
|
247
|
+
await Promise.all([...pendingLogs]);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function enqueue(event) {
|
|
251
|
+
if (destroyed) {
|
|
252
|
+
throw new Error("VH client has been destroyed");
|
|
253
|
+
}
|
|
254
|
+
batcher.enqueue(event);
|
|
255
|
+
}
|
|
256
|
+
function writeLog(level, message, data) {
|
|
257
|
+
if (destroyed) {
|
|
258
|
+
throw new Error("VH client has been destroyed");
|
|
259
|
+
}
|
|
260
|
+
if (showConsoleOnLog) {
|
|
261
|
+
console[level](`[${appName}] ${message}`, data ?? "");
|
|
262
|
+
}
|
|
263
|
+
const pending = createFingerprint(level, message, data).then((fingerprint) => {
|
|
264
|
+
if (destroyed) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
enqueue({
|
|
268
|
+
type: "log",
|
|
269
|
+
eventId: createId(),
|
|
270
|
+
fingerprint,
|
|
271
|
+
appName,
|
|
272
|
+
level,
|
|
273
|
+
message,
|
|
274
|
+
...data === void 0 ? {} : { data },
|
|
275
|
+
timestamp: Date.now()
|
|
276
|
+
});
|
|
277
|
+
}).catch((error) => {
|
|
278
|
+
console.error("[vh-dev-client] Failed to create log event", error);
|
|
279
|
+
}).then(() => {
|
|
280
|
+
pendingLogs.delete(pending);
|
|
281
|
+
});
|
|
282
|
+
pendingLogs.add(pending);
|
|
283
|
+
}
|
|
284
|
+
const log = {
|
|
285
|
+
debug: (message, data) => writeLog("debug", message, data),
|
|
286
|
+
info: (message, data) => writeLog("info", message, data),
|
|
287
|
+
warn: (message, data) => writeLog("warn", message, data),
|
|
288
|
+
error: (message, data) => writeLog("error", message, data)
|
|
289
|
+
};
|
|
290
|
+
return {
|
|
291
|
+
log,
|
|
292
|
+
network: {
|
|
293
|
+
req(input) {
|
|
294
|
+
enqueue({
|
|
295
|
+
type: "network:req",
|
|
296
|
+
eventId: createId(),
|
|
297
|
+
appName,
|
|
298
|
+
requestId: input.requestId,
|
|
299
|
+
method: input.method,
|
|
300
|
+
url: input.url,
|
|
301
|
+
timestamp: Date.now()
|
|
302
|
+
});
|
|
303
|
+
},
|
|
304
|
+
res(input) {
|
|
305
|
+
enqueue({
|
|
306
|
+
type: "network:res",
|
|
307
|
+
eventId: createId(),
|
|
308
|
+
appName,
|
|
309
|
+
requestId: input.requestId,
|
|
310
|
+
...input.status === void 0 ? {} : { status: input.status },
|
|
311
|
+
...input.durationMs === void 0 ? {} : { durationMs: input.durationMs },
|
|
312
|
+
...input.error === void 0 ? {} : { error: input.error },
|
|
313
|
+
timestamp: Date.now()
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
async flush() {
|
|
318
|
+
await waitForPendingLogs();
|
|
319
|
+
await batcher.flush();
|
|
320
|
+
},
|
|
321
|
+
async destroy() {
|
|
322
|
+
if (destroyed) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
await waitForPendingLogs();
|
|
326
|
+
await batcher.destroy();
|
|
327
|
+
destroyed = true;
|
|
328
|
+
await Promise.all(transports.map((transport) => transport.destroy()));
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
export {
|
|
333
|
+
createVH
|
|
334
|
+
};
|
|
335
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/id.ts","../../src/core/batcher.ts","../../src/core/fingerprint.ts","../../src/core/transport.ts","../../src/index.ts"],"sourcesContent":["export function createId(): string {\n if (typeof globalThis.crypto?.randomUUID === \"function\") {\n return globalThis.crypto.randomUUID();\n }\n\n throw new Error(\n \"Secure randomUUID is unavailable. Use a modern runtime with Web Crypto support.\",\n );\n}\n","import type { VHEvent } from \"./types.js\";\n\nexport interface BatcherOptions {\n maxBatchSize?: number;\n flushIntervalMs?: number;\n maxQueueSize?: number;\n onFlush: (events: VHEvent[]) => Promise<void>;\n}\n\nexport class EventBatcher {\n private queue: VHEvent[] = [];\n private readonly maxBatchSize: number;\n private readonly flushIntervalMs: number;\n private readonly maxQueueSize: number;\n private readonly onFlush: BatcherOptions[\"onFlush\"];\n\n private timer: ReturnType<typeof setInterval> | undefined;\n private flushing = false;\n private flushRequested = false;\n private destroyed = false;\n\n constructor(options: BatcherOptions) {\n this.maxBatchSize = options.maxBatchSize ?? 50;\n this.flushIntervalMs = options.flushIntervalMs ?? 5000;\n this.maxQueueSize = options.maxQueueSize ?? 5000;\n this.onFlush = options.onFlush;\n\n this.timer = setInterval(() => {\n void this.flush();\n }, this.flushIntervalMs);\n }\n\n enqueue(event: VHEvent): boolean {\n if (this.destroyed) {\n return false;\n }\n\n if (this.queue.length >= this.maxQueueSize) {\n // Drop the oldest queued event to keep memory bounded.\n this.queue.shift();\n }\n\n this.queue.push(event);\n\n if (this.queue.length >= this.maxBatchSize) {\n void this.flush();\n }\n\n return true;\n }\n\n async flush(): Promise<void> {\n if (this.destroyed && this.queue.length === 0) {\n return;\n }\n\n if (this.flushing) {\n this.flushRequested = true;\n return;\n }\n\n this.flushing = true;\n\n try {\n do {\n this.flushRequested = false;\n\n if (this.queue.length === 0) {\n break;\n }\n\n const batch = this.queue.splice(0, this.maxBatchSize);\n\n try {\n await this.onFlush(batch);\n } catch {\n // Restore failed events to the front of the queue.\n this.queue.unshift(...batch);\n\n if (this.queue.length > this.maxQueueSize) {\n this.queue.length = this.maxQueueSize;\n }\n\n break;\n }\n } while (this.flushRequested || this.queue.length >= this.maxBatchSize);\n } finally {\n this.flushing = false;\n }\n }\n\n async destroy(): Promise<void> {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = undefined;\n }\n\n await this.flush();\n this.destroyed = true;\n }\n\n get size(): number {\n return this.queue.length;\n }\n}\n","import type { VHLogLevel } from \"./types.js\";\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value) ?? \"undefined\";\n }\n\n if (Array.isArray(value)) {\n return `[${value.map(stableStringify).join(\",\")}]`;\n }\n\n const record = value as Record<string, unknown>;\n\n const keys = Object.keys(record).sort();\n\n return `{${keys\n .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)\n .join(\",\")}}`;\n}\n\nexport async function createFingerprint(\n level: VHLogLevel,\n message: string,\n data?: Record<string, unknown>,\n): Promise<string> {\n const input = stableStringify({\n level,\n message,\n data: data ?? null,\n });\n\n const bytes = new TextEncoder().encode(input);\n const hash = await crypto.subtle.digest(\"SHA-256\", bytes);\n\n return Array.from(new Uint8Array(hash))\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n","import { io, type Socket } from \"socket.io-client\";\nimport type { VHEvent } from \"./types.js\";\n\nexport interface TransportOptions {\n serverUrl: string;\n timeoutMs?: number;\n}\n\ninterface BatchAck {\n ok: boolean;\n eventIds?: string[];\n error?: string;\n}\n\nexport class VHTransport {\n private readonly socket: Socket;\n private readonly timeoutMs: number;\n\n constructor(options: TransportOptions) {\n this.timeoutMs = options.timeoutMs ?? 5000;\n\n this.socket = io(options.serverUrl, {\n autoConnect: true,\n reconnection: true,\n reconnectionAttempts: Infinity,\n reconnectionDelay: 500,\n reconnectionDelayMax: 5000,\n timeout: this.timeoutMs,\n });\n\n this.socket.on(\"connect\", () => {\n console.log(\"[VH Client] Connected:\", this.socket.id);\n });\n\n this.socket.on(\"connect_error\", (error) => {\n console.error(\"[VH Client] Connection error:\", error.message);\n });\n\n this.socket.on(\"disconnect\", (reason) => {\n console.log(\"[VH Client] Disconnected:\", reason);\n });\n }\n\n private waitForConnection(): Promise<void> {\n if (this.socket.connected) {\n return Promise.resolve();\n }\n\n return new Promise((resolve, reject) => {\n const cleanup = () => {\n clearTimeout(timeout);\n this.socket.off(\"connect\", onConnect);\n this.socket.off(\"connect_error\", onConnectError);\n };\n\n const onConnect = () => {\n cleanup();\n resolve();\n };\n\n const onConnectError = (error: Error) => {\n // Socket.IO may reconnect after this; this send attempt fails\n // rather than waiting indefinitely.\n cleanup();\n reject(new Error(`Unable to connect to VH server: ${error.message}`));\n };\n\n const timeout = setTimeout(() => {\n cleanup();\n reject(\n new Error(`VH server connection timed out after ${this.timeoutMs}ms`),\n );\n }, this.timeoutMs);\n\n this.socket.once(\"connect\", onConnect);\n this.socket.once(\"connect_error\", onConnectError);\n\n // Recheck after registering listeners to avoid missing a connection\n // that happens between the initial check and listener registration.\n if (this.socket.connected) {\n onConnect();\n }\n });\n }\n\n async sendBatch(events: VHEvent[]): Promise<void> {\n if (events.length === 0) {\n return;\n }\n\n await this.waitForConnection();\n\n await new Promise<void>((resolve, reject) => {\n let settled = false;\n\n const timeout = setTimeout(() => {\n finish(\n new Error(\n `VH server acknowledgement timed out after ${this.timeoutMs}ms`,\n ),\n );\n }, this.timeoutMs);\n\n const finish = (error?: Error) => {\n if (settled) return;\n settled = true;\n clearTimeout(timeout);\n\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n };\n\n this.socket.emit(\"vh:events\", events, (ack?: BatchAck) => {\n if (!ack?.ok) {\n finish(new Error(ack?.error ?? \"VH server rejected the batch\"));\n return;\n }\n\n console.log(`[VH Client] Server acknowledged ${events.length} events`);\n\n finish();\n });\n });\n }\n\n async destroy(): Promise<void> {\n this.socket.disconnect();\n }\n\n get connected(): boolean {\n return this.socket.connected;\n }\n}\n","import { createId } from \"./core/id.js\";\nimport { EventBatcher } from \"./core/batcher.js\";\nimport { createFingerprint } from \"./core/fingerprint.js\";\nimport { VHTransport } from \"./core/transport.js\";\n\nimport type {\n VHEvent,\n VHLogLevel,\n VHNetworkRequestInput,\n VHNetworkResponseInput,\n VHOptions,\n} from \"./core/types.js\";\n\nexport type {\n VHEvent,\n VHLogLevel,\n VHLogEvent,\n VHNetworkRequestEvent,\n VHNetworkResponseEvent,\n VHOptions,\n} from \"./core/types.js\";\n\nexport interface VHClient {\n log: Record<\n VHLogLevel,\n (message: string, data?: Record<string, unknown>) => void\n >;\n\n network: {\n req(input: VHNetworkRequestInput): void;\n res(input: VHNetworkResponseInput): void;\n };\n\n flush(): Promise<void>;\n destroy(): Promise<void>;\n}\n\nexport function createVH(options: VHOptions): VHClient {\n const appName = options.appName.trim();\n\n const serverUrls = [\n ...new Set(\n (Array.isArray(options.serverUrl)\n ? options.serverUrl\n : [options.serverUrl]\n ).map((url) => url.trim()),\n ),\n ];\n\n if (!appName) {\n throw new Error(\"appName is required\");\n }\n\n if (\n serverUrls.length === 0 ||\n serverUrls.some((url) => {\n try {\n const parsed = new URL(url);\n\n return ![\"http:\", \"https:\"].includes(parsed.protocol);\n } catch {\n return true;\n }\n })\n ) {\n throw new Error(\"serverUrl must contain valid http:// or https:// URLs\");\n }\n\n const showConsoleOnLog = options.showConsoleOnLog ?? true;\n\n const transports = serverUrls.map(\n (serverUrl) => new VHTransport({ serverUrl }),\n );\n\n const batcher = new EventBatcher({\n maxBatchSize: 50,\n flushIntervalMs: 5000,\n maxQueueSize: 5000,\n\n async onFlush(events) {\n await Promise.all(\n transports.map((transport) => transport.sendBatch(events)),\n );\n },\n });\n\n let destroyed = false;\n\n const pendingLogs = new Set<Promise<void>>();\n\n async function waitForPendingLogs(): Promise<void> {\n while (pendingLogs.size > 0) {\n await Promise.all([...pendingLogs]);\n }\n }\n\n function enqueue(event: VHEvent): void {\n if (destroyed) {\n throw new Error(\"VH client has been destroyed\");\n }\n\n batcher.enqueue(event);\n }\n\n function writeLog(\n level: VHLogLevel,\n message: string,\n data?: Record<string, unknown>,\n ): void {\n if (destroyed) {\n throw new Error(\"VH client has been destroyed\");\n }\n\n if (showConsoleOnLog) {\n console[level](`[${appName}] ${message}`, data ?? \"\");\n }\n\n const pending = createFingerprint(level, message, data)\n .then((fingerprint) => {\n if (destroyed) {\n return;\n }\n\n enqueue({\n type: \"log\",\n eventId: createId(),\n fingerprint,\n appName,\n level,\n message,\n ...(data === undefined ? {} : { data }),\n timestamp: Date.now(),\n });\n })\n .catch((error: unknown) => {\n console.error(\"[vh-dev-client] Failed to create log event\", error);\n })\n .then(() => {\n pendingLogs.delete(pending);\n });\n\n pendingLogs.add(pending);\n }\n\n const log: VHClient[\"log\"] = {\n debug: (message, data) => writeLog(\"debug\", message, data),\n info: (message, data) => writeLog(\"info\", message, data),\n warn: (message, data) => writeLog(\"warn\", message, data),\n error: (message, data) => writeLog(\"error\", message, data),\n };\n\n return {\n log,\n\n network: {\n req(input) {\n enqueue({\n type: \"network:req\",\n eventId: createId(),\n appName,\n requestId: input.requestId,\n method: input.method,\n url: input.url,\n timestamp: Date.now(),\n });\n },\n\n res(input) {\n enqueue({\n type: \"network:res\",\n eventId: createId(),\n appName,\n requestId: input.requestId,\n ...(input.status === undefined ? {} : { status: input.status }),\n ...(input.durationMs === undefined\n ? {}\n : { durationMs: input.durationMs }),\n ...(input.error === undefined ? {} : { error: input.error }),\n timestamp: Date.now(),\n });\n },\n },\n\n async flush() {\n await waitForPendingLogs();\n await batcher.flush();\n },\n\n async destroy() {\n if (destroyed) {\n return;\n }\n\n await waitForPendingLogs();\n await batcher.destroy();\n\n destroyed = true;\n\n await Promise.all(transports.map((transport) => transport.destroy()));\n },\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vh-dev-client",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Development logging and network monitoring SDK for VH Dev Helper",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./browser": {
|
|
15
|
+
"types": "./dist/browser/index.d.ts",
|
|
16
|
+
"import": "./dist/browser/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./node": {
|
|
19
|
+
"types": "./dist/node/index.d.ts",
|
|
20
|
+
"import": "./dist/node/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"prepublishOnly": "npm run build && npm run typecheck"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [],
|
|
32
|
+
"author": "",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"socket.io-client": "^4.8.4"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^26.6.5",
|
|
38
|
+
"tsup": "^8.5.1",
|
|
39
|
+
"typescript": "^5.9.3"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/README.md
DELETED