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