@mauroandre/velojs 0.0.57 → 0.0.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ import i from "node:fs";
2
+ import d from "node:os";
3
+ import c from "node:path";
4
+ import { parseDotEnv as m, initTelemetry as w, byteLength as h } from "./telemetry.js";
5
+ function $(t = process.cwd()) {
6
+ try {
7
+ return m(i.readFileSync(c.join(t, ".env"), "utf-8"));
8
+ } catch {
9
+ return {};
10
+ }
11
+ }
12
+ function v() {
13
+ const t = d.hostname().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
14
+ return t ? `${process.pid}-${t}` : String(process.pid);
15
+ }
16
+ function S(t = process.cwd()) {
17
+ try {
18
+ const e = i.readFileSync(c.join(t, "package.json"), "utf-8"), o = JSON.parse(e);
19
+ return typeof o.version == "string" ? o.version : null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+ function j(t) {
25
+ try {
26
+ return i.statSync(t).size;
27
+ } catch {
28
+ return 0;
29
+ }
30
+ }
31
+ async function k(t) {
32
+ const e = t.file, o = t.sink;
33
+ let s = null, a = 0, l = 0;
34
+ const p = async () => {
35
+ s || !e || (await i.promises.mkdir(c.dirname(e), { recursive: !0 }), a = j(e), s = await i.promises.open(e, "a"));
36
+ }, f = async () => {
37
+ if (!e) return;
38
+ s && (await s.close().catch(() => {
39
+ }), s = null);
40
+ let r = l + 1;
41
+ for (; i.existsSync(`${e}.${r}`); ) r += 1;
42
+ await i.promises.rename(e, `${e}.${r}`), l = r, a = 0, s = await i.promises.open(e, "a");
43
+ }, y = async (r) => {
44
+ if (e)
45
+ try {
46
+ await p();
47
+ const n = h(r) + 1;
48
+ a > 0 && a + n > t.maxBytes && await f(), await s.write(`${r}
49
+ `), a += n;
50
+ } catch (n) {
51
+ console.error(`[velojs] telemetry file destination failed (${e}):`, n);
52
+ }
53
+ }, u = async (r) => {
54
+ if (o)
55
+ try {
56
+ const n = await fetch(o, {
57
+ method: "POST",
58
+ headers: { "content-type": "application/json" },
59
+ body: r
60
+ });
61
+ await n.text().catch(() => ""), n.ok || console.error(`[velojs] telemetry sink responded ${n.status} (${o})`);
62
+ } catch (n) {
63
+ console.error(`[velojs] telemetry sink destination failed (${o}):`, n);
64
+ }
65
+ };
66
+ return {
67
+ file: e,
68
+ sink: o,
69
+ async write(r) {
70
+ const n = [];
71
+ e && n.push(y(r)), o && n.push(u(r)), await Promise.allSettled(n);
72
+ },
73
+ async close() {
74
+ s && (await s.close().catch(() => {
75
+ }), s = null);
76
+ }
77
+ };
78
+ }
79
+ async function z(t) {
80
+ await w({
81
+ writer: k,
82
+ instanceLabel: t?.instanceLabel ?? v(),
83
+ version: S(),
84
+ fileEnv: $(process.cwd())
85
+ });
86
+ }
87
+ export {
88
+ z as initNodeTelemetry,
89
+ v as nodeInstanceLabel,
90
+ k as nodeTelemetryWriter,
91
+ S as readAppVersion,
92
+ $ as readProjectEnv
93
+ };
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Native telemetry — boundary traces, first-class JSONL (client-safe core).
3
+ *
4
+ * The framework owns a finite set of bottlenecks (page SSR, `?_data=1` refetch,
5
+ * action, declarative endpoint, stream connect, socket setup, channel connect)
6
+ * plus the emit chain that runs outside any request. This module turns each of
7
+ * them into one JSONL event with duration, status, response bytes, child spans
8
+ * and — on long connections — one delivery line per frame delivered and a final
9
+ * close line. The file is the production artifact; `velojs telemetry` ranks it.
10
+ *
11
+ * Activation is by destination, never by a flag: `VELO_TELEMETRY_FILE` and/or
12
+ * `VELO_TELEMETRY_SINK_URL` present and non-empty turn collection on; none of
13
+ * them (absent or empty) leaves the whole module inert — no file, no event, no
14
+ * cost — in any environment, dev included. The values expand `~` and `$VAR`,
15
+ * the effective file carries an instance suffix (pid/hostname) so replicas
16
+ * sharing a volume never corrupt each other, and `VELO_TELEMETRY_MAX_MB`
17
+ * (default 10) is the only pruning mechanism: size-based rotation.
18
+ *
19
+ * This core is import-safe on the client (no Node builtin anywhere): it holds
20
+ * the event model, the trace API and the destination resolution. The writer
21
+ * that touches the filesystem and the network lives in `telemetry-node.ts`,
22
+ * injected at boot — the client bundle never reaches it.
23
+ */
24
+ /** Every line of the JSONL carries one of these types. */
25
+ export type TelemetryEventType = "boot" | "page" | "data" | "action" | "endpoint" | "stream-connect" | "socket-connect" | "channel-connect" | "emit" | "delivery" | "close";
26
+ export type TelemetryStatus = "ok" | "error";
27
+ /** One child span: name, start relative to the trace, own duration and status. */
28
+ export interface TelemetrySpan {
29
+ name: string;
30
+ /** Milliseconds since the trace started. */
31
+ start: number;
32
+ duration: number;
33
+ status: TelemetryStatus;
34
+ }
35
+ /** The open field set shared by the trace handle and every emitted line. */
36
+ export interface TelemetryEventFields {
37
+ status?: TelemetryStatus | undefined;
38
+ duration?: number | undefined;
39
+ bytes?: number | undefined;
40
+ route?: string | undefined;
41
+ module?: string | undefined;
42
+ name?: string | undefined;
43
+ method?: string | undefined;
44
+ channel?: string | undefined;
45
+ partition?: string | null | undefined;
46
+ mode?: "invalidate" | "slice" | undefined;
47
+ connections?: number | undefined;
48
+ }
49
+ /** One JSONL line. Optional fields exist only when the type has them. */
50
+ export interface TelemetryEvent extends TelemetryEventFields {
51
+ trace: string;
52
+ ts: string;
53
+ type: TelemetryEventType;
54
+ /** Cumulative ordinal on `delivery` lines; total on `close` lines. */
55
+ deliveries?: number | undefined;
56
+ spans?: TelemetrySpan[] | undefined;
57
+ version?: string | null | undefined;
58
+ env?: string | null | undefined;
59
+ file?: string | null | undefined;
60
+ sink?: string | null | undefined;
61
+ maxMb?: number | undefined;
62
+ }
63
+ export interface TelemetrySpanHandle {
64
+ /** Closes the span; first call wins. */
65
+ end(status?: TelemetryStatus): void;
66
+ }
67
+ /** A span collector detached from any trace — used for coalesced emit rounds. */
68
+ export interface TelemetrySpanRecorder {
69
+ readonly spans: TelemetrySpan[];
70
+ span(name: string): TelemetrySpanHandle;
71
+ }
72
+ /**
73
+ * One open trace. `end()` writes the main line once; `delivery()` and
74
+ * `close()` write the light lines of a long connection, all carrying the same
75
+ * trace id. A disabled trace is a shared no-op.
76
+ */
77
+ export interface TelemetryTrace extends TelemetrySpanRecorder {
78
+ readonly id: string;
79
+ readonly enabled: boolean;
80
+ addSpans(spans: readonly TelemetrySpan[]): void;
81
+ setFields(fields: TelemetryEventFields): void;
82
+ end(fields?: TelemetryEventFields): void;
83
+ delivery(bytes?: number): void;
84
+ close(fields?: TelemetryEventFields): void;
85
+ }
86
+ /** The destination model: env values resolved, defaults applied. */
87
+ export interface TelemetrySettings {
88
+ /** `VELO_TELEMETRY_FILE` expanded (instance suffix NOT applied), or null. */
89
+ file: string | null;
90
+ /** `VELO_TELEMETRY_SINK_URL` expanded, or null. */
91
+ sink: string | null;
92
+ /** `VELO_TELEMETRY_MAX_MB`, default 10. */
93
+ maxMb: number;
94
+ }
95
+ /** Read-only view of the runtime state — the disabled-mode assertion. */
96
+ export interface TelemetryState {
97
+ enabled: boolean;
98
+ file: string | null;
99
+ sink: string | null;
100
+ maxMb: number;
101
+ /** Events handed to the destinations since the last reset. */
102
+ events: number;
103
+ }
104
+ /**
105
+ * Where the events go. The core never touches the filesystem: the Node adapter
106
+ * (`telemetry-node.ts`) implements this and is injected at boot.
107
+ */
108
+ export interface TelemetryWriter {
109
+ /** Effective file path the writer appends to (instance suffix applied). */
110
+ readonly file: string | null;
111
+ /** Sink URL the writer POSTs to. */
112
+ readonly sink: string | null;
113
+ write(line: string): Promise<void>;
114
+ close(): Promise<void>;
115
+ }
116
+ /** What the writer factory needs to build a destination writer. */
117
+ export interface TelemetryWriterInput {
118
+ /** Effective file path (instance suffix applied) or null. */
119
+ file: string | null;
120
+ sink: string | null;
121
+ maxBytes: number;
122
+ }
123
+ export interface TelemetryInitOptions {
124
+ /** Builds the destination writer (the Node adapter, injected). */
125
+ writer: (input: TelemetryWriterInput) => Promise<TelemetryWriter>;
126
+ /** Overrides the pid/hostname suffix (tests only). */
127
+ instanceLabel?: string | undefined;
128
+ /** The app version served — goes into the boot record. */
129
+ version?: string | null | undefined;
130
+ /**
131
+ * The project's `.env` values, the fallback for the destination keys — the
132
+ * Node adapter reads them; the process environment always wins.
133
+ */
134
+ fileEnv?: Record<string, string> | undefined;
135
+ }
136
+ /** The default rotation limit, in MB. */
137
+ export declare const DEFAULT_MAX_MB = 10;
138
+ /**
139
+ * Expands `~` (home) and `$VAR` / `${VAR}` in a destination value, against the
140
+ * merged environment (process env wins over `.env`). An undefined reference is
141
+ * kept literally — a silently wrong path is worse than a visible one, and the
142
+ * boot record shows the effective destination for diagnosis.
143
+ */
144
+ export declare function expandTelemetryValue(value: string, env: Record<string, string | undefined>): string;
145
+ /**
146
+ * Parses a `.env` file body. Deliberately a small subset — `KEY=value` (with
147
+ * optional `export`), double/single quotes, `#` comments and inline comments
148
+ * after whitespace — which is all the framework consumes: the telemetry keys
149
+ * of the project, and nothing else.
150
+ */
151
+ export declare function parseDotEnv(content: string): Record<string, string>;
152
+ /**
153
+ * Resolves the destination model from the environment. `process env` values
154
+ * win over the project's `.env` (the same precedence the release script uses);
155
+ * a key present but empty counts as unset-at-that-level. Returns `null` when
156
+ * neither destination is configured — the whole collection is off.
157
+ */
158
+ export declare function resolveTelemetrySettings(env: Record<string, string | undefined>, fileEnv?: Record<string, string>): TelemetrySettings | null;
159
+ /**
160
+ * The effective file of one instance: the instance suffix goes before the
161
+ * extension, so several processes pointing the same destination write
162
+ * different files and an append never interleaves with another process's.
163
+ */
164
+ export declare function effectiveTelemetryFile(baseFile: string, instance: string): string;
165
+ /**
166
+ * Starts the collection for this process: resolves the destination from the
167
+ * environment and writes the boot record — the first event of every active
168
+ * destination, the file's first line and the sink's first POST. Without a
169
+ * destination this is a no-op; with one it runs once per process (idempotent).
170
+ *
171
+ * The writer comes from the caller: `initNodeTelemetry()` (the Node adapter)
172
+ * is what the server boot uses.
173
+ */
174
+ export declare function initTelemetry(options: TelemetryInitOptions): Promise<void>;
175
+ /** Waits until every queued event reached its destinations. */
176
+ export declare function flushTelemetry(): Promise<void>;
177
+ /** Whether a destination is active in this process. */
178
+ export declare function isTelemetryEnabled(): boolean;
179
+ /** Read-only snapshot of the runtime state. */
180
+ export declare function telemetryState(): TelemetryState;
181
+ /**
182
+ * Test-only. Closes the writer and drops the runtime, so the next
183
+ * `initTelemetry()` re-reads the environment from scratch.
184
+ */
185
+ export declare function __resetTelemetry(): Promise<void>;
186
+ /** Milliseconds of a string as UTF-8, without pulling Buffer into the client. */
187
+ export declare function byteLength(value: string): number;
188
+ /** The JSON byte size of a value, when it serializes at all. */
189
+ export declare function jsonBytes(value: unknown): number | undefined;
190
+ /** A detached span collector (the coalesced emit round fills it). */
191
+ export declare function spanRecorder(): TelemetrySpanRecorder;
192
+ /**
193
+ * Opens one trace. Every finished line carries the trace id and its own
194
+ * timestamp; the fields given here (route, module, channel, …) travel on the
195
+ * main line and on the delivery/close lines of the same trace.
196
+ */
197
+ export declare function trace(type: TelemetryEventType, fields?: TelemetryEventFields, startedAt?: number): TelemetryTrace;
198
+ /**
199
+ * The size of a response body, when it has one that can be measured without
200
+ * disturbing it: declared `content-length` wins; a stream (chunked transfer or
201
+ * an event stream) has no body size — the delivery lines of a long connection
202
+ * account for its frames instead.
203
+ */
204
+ export declare function responseBytes(response: Response): Promise<number | undefined>;
@@ -0,0 +1,240 @@
1
+ const R = 10;
2
+ function _(e, i) {
3
+ let n = e;
4
+ return n === "~" ? n = w(i) : n.startsWith("~/") && (n = `${w(i)}${n.slice(1)}`), n.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (s, r, o) => {
5
+ const l = i[r ?? o];
6
+ return l === void 0 ? s : l;
7
+ });
8
+ }
9
+ function w(e) {
10
+ return (e.HOME ?? e.USERPROFILE ?? "").replace(/\/+$/, "");
11
+ }
12
+ function D(e) {
13
+ const i = {};
14
+ for (const n of e.split(/\r?\n/)) {
15
+ const s = n.trim();
16
+ if (!s || s.startsWith("#")) continue;
17
+ const r = s.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
18
+ if (!r) continue;
19
+ const o = r[1];
20
+ let t = r[2].trim();
21
+ if (t.length >= 2 && t.startsWith('"') && t.endsWith('"'))
22
+ t = t.slice(1, -1).replace(/\\n/g, `
23
+ `).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
24
+ else if (t.length >= 2 && t.startsWith("'") && t.endsWith("'"))
25
+ t = t.slice(1, -1);
26
+ else {
27
+ const l = t.indexOf(" #");
28
+ l !== -1 && (t = t.slice(0, l).trim());
29
+ }
30
+ i[o] = t;
31
+ }
32
+ return i;
33
+ }
34
+ function S(e, i = {}) {
35
+ const n = (d) => {
36
+ const c = e[d];
37
+ return c !== void 0 ? c.trim() : (i[d] ?? "").trim();
38
+ }, s = n("VELO_TELEMETRY_FILE"), r = n("VELO_TELEMETRY_SINK_URL");
39
+ if (!s && !r) return null;
40
+ const o = { ...i, ...e }, t = n("VELO_TELEMETRY_MAX_MB"), l = Number(t), h = Number.isFinite(l) && l > 0 ? l : 10;
41
+ return {
42
+ file: s ? _(s, o) : null,
43
+ sink: r ? _(r, o) : null,
44
+ maxMb: h
45
+ };
46
+ }
47
+ function $(e, i) {
48
+ const n = Math.max(e.lastIndexOf("/"), e.lastIndexOf("\\")), s = n === -1 ? "" : e.slice(0, n + 1), r = n === -1 ? e : e.slice(n + 1), o = r.lastIndexOf("."), t = o > 0 ? `${r.slice(0, o)}.${i}${r.slice(o)}` : `${r}.${i}`;
49
+ return `${s}${t}`;
50
+ }
51
+ let u = null, E = !1, f = null, M = 0;
52
+ function x() {
53
+ return M += 1, `${Date.now().toString(36)}-${M.toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
54
+ }
55
+ function p(e, i) {
56
+ for (const [n, s] of Object.entries(i))
57
+ s !== void 0 && (e[n] = s);
58
+ return e;
59
+ }
60
+ const y = (e) => Math.round(e * 100) / 100, v = () => typeof performance < "u" ? performance.now() : Date.now(), b = () => (/* @__PURE__ */ new Date()).toISOString();
61
+ function L() {
62
+ return typeof process < "u" ? process.env : {};
63
+ }
64
+ async function B(e) {
65
+ if (!E) {
66
+ if (f) {
67
+ await f;
68
+ return;
69
+ }
70
+ f = (async () => {
71
+ const i = L(), n = S(i, e.fileEnv ?? {});
72
+ if (!n) return;
73
+ const s = e.instanceLabel ?? `pid-${String(globalThis.process?.pid ?? "0")}`, r = n.file ? $(n.file, s) : null, o = await e.writer({
74
+ file: r,
75
+ sink: n.sink,
76
+ maxBytes: n.maxMb * 1024 * 1024
77
+ }), t = {
78
+ settings: n,
79
+ writer: o,
80
+ file: o.file ?? r,
81
+ sink: o.sink ?? n.sink,
82
+ version: e.version ?? null,
83
+ env: i.NODE_ENV ?? null,
84
+ chain: Promise.resolve(),
85
+ events: 0
86
+ };
87
+ u = t, E = !0;
88
+ const l = { trace: x(), ts: b(), type: "boot" };
89
+ p(l, {
90
+ version: t.version,
91
+ env: t.env,
92
+ file: t.file,
93
+ sink: t.sink,
94
+ maxMb: t.settings.maxMb
95
+ }), await m(t, l);
96
+ })(), await f, f = null;
97
+ }
98
+ }
99
+ function m(e, i) {
100
+ return e.events += 1, e.chain = e.chain.then(() => e.writer.write(JSON.stringify(i))).catch(() => {
101
+ }), e.chain;
102
+ }
103
+ async function F() {
104
+ await u?.chain;
105
+ }
106
+ function I() {
107
+ return u !== null;
108
+ }
109
+ function z() {
110
+ return {
111
+ enabled: u !== null,
112
+ file: u?.file ?? null,
113
+ sink: u?.sink ?? null,
114
+ maxMb: u?.settings.maxMb ?? 10,
115
+ events: u?.events ?? 0
116
+ };
117
+ }
118
+ async function P() {
119
+ f && await f;
120
+ const e = u;
121
+ u = null, E = !1, f = null, e && (await e.chain.catch(() => {
122
+ }), await e.writer.close().catch(() => {
123
+ }));
124
+ }
125
+ const T = { end: () => {
126
+ } };
127
+ function O(e) {
128
+ return typeof Buffer < "u" ? Buffer.byteLength(e) : new TextEncoder().encode(e).length;
129
+ }
130
+ function W(e) {
131
+ try {
132
+ const i = JSON.stringify(e);
133
+ return i === void 0 ? void 0 : O(i);
134
+ } catch {
135
+ return;
136
+ }
137
+ }
138
+ function k(e, i, n) {
139
+ const s = v();
140
+ let r = !1;
141
+ return {
142
+ end(o = "ok") {
143
+ r || (r = !0, e.push({
144
+ name: n,
145
+ start: y(s - i),
146
+ duration: y(v() - s),
147
+ status: o
148
+ }));
149
+ }
150
+ };
151
+ }
152
+ const N = {
153
+ id: "",
154
+ enabled: !1,
155
+ spans: [],
156
+ span: () => T,
157
+ addSpans: () => {
158
+ },
159
+ setFields: () => {
160
+ },
161
+ end: () => {
162
+ },
163
+ delivery: () => {
164
+ },
165
+ close: () => {
166
+ }
167
+ }, A = {
168
+ spans: [],
169
+ span: () => T
170
+ };
171
+ function Z() {
172
+ if (!u) return A;
173
+ const e = [];
174
+ return { spans: e, span: (i) => k(e, v(), i) };
175
+ }
176
+ function U(e, i = {}, n = v()) {
177
+ const s = u;
178
+ if (!s) return N;
179
+ const r = x(), o = { ...i }, t = [];
180
+ let l = !1, h = 0;
181
+ const d = (c, a) => {
182
+ const g = { trace: r, ts: b(), type: c };
183
+ return p(g, o), p(g, a), g;
184
+ };
185
+ return {
186
+ id: r,
187
+ enabled: !0,
188
+ spans: t,
189
+ span: (c) => k(t, n, c),
190
+ addSpans(c) {
191
+ for (const a of c) t.push(a);
192
+ },
193
+ setFields(c) {
194
+ p(o, c);
195
+ },
196
+ end(c = {}) {
197
+ if (l) return;
198
+ l = !0;
199
+ const a = d(e, c);
200
+ a.status === void 0 && (a.status = "ok"), a.duration === void 0 && (a.duration = y(v() - n)), t.length > 0 && (a.spans = [...t]), m(s, a);
201
+ },
202
+ delivery(c) {
203
+ h += 1, m(s, d("delivery", { deliveries: h, ...c === void 0 ? {} : { bytes: c } }));
204
+ },
205
+ close(c = {}) {
206
+ const a = d("close", c);
207
+ a.deliveries = h, a.status === void 0 && (a.status = "ok"), a.duration === void 0 && (a.duration = y(v() - n)), m(s, a);
208
+ }
209
+ };
210
+ }
211
+ async function V(e) {
212
+ if (!e.body || (e.headers.get("content-type") ?? "").startsWith("text/event-stream") || (e.headers.get("transfer-encoding") ?? "").includes("chunked")) return;
213
+ const n = e.headers.get("content-length");
214
+ if (n !== null) {
215
+ const s = Number(n);
216
+ if (Number.isFinite(s) && s >= 0) return s;
217
+ }
218
+ try {
219
+ return O(await e.clone().text());
220
+ } catch {
221
+ return;
222
+ }
223
+ }
224
+ export {
225
+ R as DEFAULT_MAX_MB,
226
+ P as __resetTelemetry,
227
+ O as byteLength,
228
+ $ as effectiveTelemetryFile,
229
+ _ as expandTelemetryValue,
230
+ F as flushTelemetry,
231
+ B as initTelemetry,
232
+ I as isTelemetryEnabled,
233
+ W as jsonBytes,
234
+ D as parseDotEnv,
235
+ S as resolveTelemetrySettings,
236
+ V as responseBytes,
237
+ Z as spanRecorder,
238
+ z as telemetryState,
239
+ U as trace
240
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mauroandre/velojs",
3
- "version": "0.0.57",
3
+ "version": "0.0.59",
4
4
  "type": "module",
5
5
  "description": "Fullstack web framework built on Hono + Preact with Vite-powered AST transforms",
6
6
  "keywords": [