@noy-db/to-meter 0.1.0-pre.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vLannaAi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @noy-db/to-meter
2
+
3
+ [![npm](https://img.shields.io/npm/v/%40noy-db/to-meter.svg)](https://www.npmjs.com/package/@noy-db/to-meter)
4
+
5
+ > Pass-through meter for @noy-db/to-* stores
6
+
7
+ Part of [**`@noy-db/hub`**](https://www.npmjs.com/package/@noy-db/hub) — the zero-knowledge, offline-first, encrypted document store.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add @noy-db/hub @noy-db/to-meter
13
+ ```
14
+
15
+ ## What it is
16
+
17
+ Pass-through meter for @noy-db/to-* stores — wraps any NoydbStore and records per-method latency percentiles, error rates, and byte counts on real traffic. Optional synthetic liveness probe emits degraded / restored events. No synthetic benchmarks (see @noy-db/to-probe for that) — this measures what your app is actually doing.
18
+
19
+ ## Status
20
+
21
+ **Pre-release** (`0.1.0-pre.1`). API may change before `1.0`.
22
+
23
+ ## Documentation
24
+
25
+ See the [main repository](https://github.com/vLannaAi/noy-db#readme) for setup, examples, and the full subsystem catalog.
26
+
27
+ - Source — [`packages/to-meter`](https://github.com/vLannaAi/noy-db/tree/main/packages/to-meter)
28
+ - Issues — [github.com/vLannaAi/noy-db/issues](https://github.com/vLannaAi/noy-db/issues)
29
+ - Spec — [`SPEC.md`](https://github.com/vLannaAi/noy-db/blob/main/SPEC.md)
30
+
31
+ ## License
32
+
33
+ [MIT](./LICENSE) © vLannaAi
package/dist/index.cjs ADDED
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ toMeter: () => toMeter
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ var import_hub = require("@noy-db/hub");
27
+ var METHODS = ["get", "put", "delete", "list", "loadAll", "saveAll"];
28
+ function toMeter(inner, options = {}) {
29
+ const sampleLimit = options.sampleLimit ?? 1024;
30
+ const degradedMs = options.degradedMs ?? 500;
31
+ const samples = {
32
+ get: [],
33
+ put: [],
34
+ delete: [],
35
+ list: [],
36
+ loadAll: [],
37
+ saveAll: []
38
+ };
39
+ const counts = {
40
+ get: 0,
41
+ put: 0,
42
+ delete: 0,
43
+ list: 0,
44
+ loadAll: 0,
45
+ saveAll: 0
46
+ };
47
+ const errors = {
48
+ get: 0,
49
+ put: 0,
50
+ delete: 0,
51
+ list: 0,
52
+ loadAll: 0,
53
+ saveAll: 0
54
+ };
55
+ let casConflicts = 0;
56
+ let windowStart = Date.now();
57
+ let currentStatus = "ok";
58
+ const listeners = /* @__PURE__ */ new Set();
59
+ function recordOp(method, durationMs, success, error) {
60
+ counts[method]++;
61
+ if (!success) {
62
+ errors[method]++;
63
+ if (error instanceof import_hub.ConflictError) casConflicts++;
64
+ }
65
+ const arr = samples[method];
66
+ arr.push(durationMs);
67
+ if (arr.length > sampleLimit) {
68
+ arr.splice(0, arr.length - sampleLimit);
69
+ }
70
+ if (method === "put" && counts.put >= 10) {
71
+ const put = computeMethodStats(samples.put, counts.put, errors.put);
72
+ const breached = put.p99 > degradedMs;
73
+ if (breached && currentStatus === "ok") transition("degraded", method, put.p99, `put p99 ${put.p99}ms > ${degradedMs}ms`);
74
+ else if (!breached && currentStatus === "degraded") transition("ok", method, put.p99, `put p99 recovered to ${put.p99}ms`);
75
+ }
76
+ }
77
+ function transition(next, method, p99, reason = "") {
78
+ if (next === currentStatus) return;
79
+ const prior = currentStatus;
80
+ currentStatus = next;
81
+ const event = {
82
+ type: next === "ok" ? "restored" : "degraded",
83
+ status: next,
84
+ ...method !== void 0 ? { method } : {},
85
+ ...p99 !== void 0 ? { p99 } : {},
86
+ reason,
87
+ at: (/* @__PURE__ */ new Date()).toISOString()
88
+ };
89
+ for (const l of listeners) {
90
+ try {
91
+ l(event);
92
+ } catch {
93
+ }
94
+ }
95
+ if (next === "degraded" && prior !== "degraded") options.onDegraded?.(event);
96
+ if (next === "ok" && prior !== "ok") options.onRestored?.(event);
97
+ }
98
+ const metrics = (0, import_hub.wrapStore)(
99
+ inner,
100
+ (0, import_hub.withMetrics)({
101
+ onOperation(op) {
102
+ recordOp(op.method, op.durationMs, op.success, op.error);
103
+ }
104
+ })
105
+ );
106
+ const livenessTimer = options.liveness ? startLiveness(inner, options.liveness, transition) : null;
107
+ const handle = {
108
+ snapshot() {
109
+ const byMethod = {};
110
+ let total = 0;
111
+ for (const m of METHODS) {
112
+ byMethod[m] = computeMethodStats(samples[m], counts[m], errors[m]);
113
+ total += counts[m];
114
+ }
115
+ return {
116
+ byMethod,
117
+ status: currentStatus,
118
+ casConflicts,
119
+ totalCalls: total,
120
+ windowMs: Date.now() - windowStart,
121
+ collectedAt: (/* @__PURE__ */ new Date()).toISOString()
122
+ };
123
+ },
124
+ reset() {
125
+ for (const m of METHODS) {
126
+ samples[m].length = 0;
127
+ counts[m] = 0;
128
+ errors[m] = 0;
129
+ }
130
+ casConflicts = 0;
131
+ windowStart = Date.now();
132
+ },
133
+ subscribe(listener) {
134
+ listeners.add(listener);
135
+ return () => {
136
+ listeners.delete(listener);
137
+ };
138
+ },
139
+ close() {
140
+ if (livenessTimer) clearInterval(livenessTimer);
141
+ listeners.clear();
142
+ }
143
+ };
144
+ const renamed = {
145
+ ...metrics,
146
+ name: inner.name ? `meter(${inner.name})` : "meter"
147
+ };
148
+ return { store: renamed, meter: handle };
149
+ }
150
+ function computeMethodStats(sorted, count, errorCount) {
151
+ if (count === 0) {
152
+ return { count: 0, errors: 0, p50: 0, p90: 0, p99: 0, max: 0, avg: 0 };
153
+ }
154
+ const s = [...sorted].sort((a, b) => a - b);
155
+ const pct = (q) => s[Math.min(s.length - 1, Math.floor(q * s.length))];
156
+ const sum = s.reduce((a, b) => a + b, 0);
157
+ return {
158
+ count,
159
+ errors: errorCount,
160
+ p50: pct(0.5),
161
+ p90: pct(0.9),
162
+ p99: pct(0.99),
163
+ max: s[s.length - 1],
164
+ avg: Math.round(sum / s.length)
165
+ };
166
+ }
167
+ function startLiveness(inner, opts, transition) {
168
+ const vault = opts.vault ?? "probe-vault";
169
+ const collection = opts.collection ?? "probe-liveness";
170
+ const pingId = "liveness";
171
+ const timer = setInterval(() => {
172
+ void tick();
173
+ }, opts.interval);
174
+ async function tick() {
175
+ try {
176
+ if (typeof inner.ping === "function") {
177
+ const ok = await inner.ping();
178
+ if (!ok) return transition("unreachable", void 0, void 0, "ping returned false");
179
+ } else {
180
+ await inner.put(vault, collection, pingId, {
181
+ _noydb: 1,
182
+ _v: 1,
183
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
184
+ _iv: "AAAAAAAAAAAAAAAA",
185
+ _data: "cHJvYmU="
186
+ });
187
+ await inner.delete(vault, collection, pingId);
188
+ }
189
+ transition("ok", void 0, void 0, "liveness check succeeded");
190
+ } catch (err) {
191
+ transition("unreachable", void 0, void 0, `liveness error: ${err.message}`);
192
+ }
193
+ }
194
+ return timer;
195
+ }
196
+ // Annotate the CommonJS export names for ESM import in node:
197
+ 0 && (module.exports = {
198
+ toMeter
199
+ });
200
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/to-meter** — pass-through meter for `@noy-db/to-*` stores.\n *\n * Wraps any `NoydbStore` and returns a new store that behaves\n * identically but records per-method timing, error rates, byte\n * counts, and (optionally) periodic liveness status. The meter is\n * itself a `NoydbStore`, so it slots anywhere a store fits:\n *\n * ```ts\n * import { toMeter } from '@noy-db/to-meter'\n * import { awsDynamoStore } from '@noy-db/to-aws-dynamo'\n *\n * const dynamo = awsDynamoStore({ table: 'live' })\n * const { store, meter } = toMeter(dynamo, {\n * liveness: { interval: 60_000 }, // optional synthetic pings\n * degradedMs: 200, // p99 threshold for `degraded` event\n * onDegraded: (e) => console.warn(e),\n * })\n *\n * const db = await createNoydb({ store })\n *\n * // at any time\n * console.log(meter.snapshot())\n * // {\n * // byMethod: {\n * // get: { count: 142, p50: 3, p99: 28, errors: 0 },\n * // put: { count: 43, p50: 11, p99: 92, errors: 1 },\n * // ...\n * // },\n * // status: 'ok' | 'degraded' | 'unreachable',\n * // casConflicts: 2,\n * // totalCalls: 230,\n * // windowMs: 45_280,\n * // }\n * ```\n *\n * ## Relation to `withMetrics`\n *\n * This package **uses** hub's `withMetrics` middleware internally —\n * don't think of it as a replacement. `withMetrics` is the raw event\n * stream (one callback per op); `toMeter` is the aggregator that\n * bucketises events into percentiles + a health verdict.\n *\n * ## Relation to `to-probe`\n *\n * - `to-probe` runs **synthetic** benchmarks on an empty store —\n * answers \"should I adopt this store?\".\n * - `to-meter` observes **real traffic** through the live store —\n * answers \"how is this store performing right now?\".\n *\n * Composable: `toMeter(probe-recommended-store)` after a probe pass\n * validates adoption.\n *\n * @packageDocumentation\n */\nimport type { NoydbStore } from '@noy-db/hub'\nimport { ConflictError, wrapStore, withMetrics } from '@noy-db/hub'\n\n// ── Types ───────────────────────────────────────────────────────────────\n\nexport type MethodName = 'get' | 'put' | 'delete' | 'list' | 'loadAll' | 'saveAll'\n\nexport type MeterStatus = 'ok' | 'degraded' | 'unreachable'\n\n/** Latency + counts for a single store method. */\nexport interface MethodStats {\n readonly count: number\n readonly errors: number\n readonly p50: number\n readonly p90: number\n readonly p99: number\n readonly max: number\n readonly avg: number\n}\n\n/** Full snapshot of meter state at one moment. */\nexport interface MeterSnapshot {\n readonly byMethod: Record<MethodName, MethodStats>\n readonly status: MeterStatus\n readonly casConflicts: number\n readonly totalCalls: number\n readonly windowMs: number\n readonly collectedAt: string\n}\n\n/** Degraded/restored event. */\nexport interface MeterEvent {\n readonly type: 'degraded' | 'restored'\n readonly status: MeterStatus\n readonly method?: MethodName\n readonly p99?: number\n readonly reason: string\n readonly at: string\n}\n\nexport interface LivenessOptions {\n /** Milliseconds between synthetic health checks. */\n readonly interval: number\n /** Vault to use for the liveness `put`/`delete` pair. Default `'probe-vault'`. */\n readonly vault?: string\n /** Collection to use. Default `'probe-liveness'`. Do NOT use a `_`-prefixed name. */\n readonly collection?: string\n}\n\nexport interface MeterOptions {\n /**\n * Upper bound on retained latency samples per method. When the\n * sample array grows past this, oldest entries are dropped. Default\n * 1024 — keeps p50/p99 reasonably accurate with bounded memory.\n */\n readonly sampleLimit?: number\n /**\n * Optional periodic liveness ping. Uses the store's `ping()` if\n * available, otherwise falls back to a `put`/`delete` pair on a\n * dedicated collection.\n */\n readonly liveness?: LivenessOptions\n /**\n * p99 latency threshold (ms) for `put` — if crossed, emit a\n * `degraded` event. Default 500.\n */\n readonly degradedMs?: number\n /** Called when the meter transitions to `degraded`. */\n readonly onDegraded?: (event: MeterEvent) => void\n /** Called when the meter transitions back to `ok`. */\n readonly onRestored?: (event: MeterEvent) => void\n}\n\n/** Handle returned alongside the wrapped store. */\nexport interface MeterHandle {\n /** Current snapshot. Safe to call frequently — O(k log k) on sample sizes. */\n snapshot(): MeterSnapshot\n /** Reset all counters and drop samples. Handy for per-request metering. */\n reset(): void\n /** Subscribe to degraded/restored transitions. Returns an unsubscribe fn. */\n subscribe(listener: (event: MeterEvent) => void): () => void\n /** Stop the liveness timer (if any) and release resources. */\n close(): void\n}\n\nexport interface ToMeterResult {\n readonly store: NoydbStore\n readonly meter: MeterHandle\n}\n\n// ── Implementation ──────────────────────────────────────────────────────\n\nconst METHODS: readonly MethodName[] = ['get', 'put', 'delete', 'list', 'loadAll', 'saveAll']\n\n/**\n * Wrap a store so every call is timed + counted. Returns the wrapped\n * store and a handle for inspecting the aggregate.\n *\n * The wrapped store is a drop-in replacement for the inner store —\n * same 6 methods, same types, same behaviour on success and error. The\n * meter adds zero semantic changes: errors still throw, conflicts\n * still surface as {@link ConflictError}.\n */\nexport function toMeter(inner: NoydbStore, options: MeterOptions = {}): ToMeterResult {\n const sampleLimit = options.sampleLimit ?? 1024\n const degradedMs = options.degradedMs ?? 500\n\n const samples: Record<MethodName, number[]> = {\n get: [], put: [], delete: [], list: [], loadAll: [], saveAll: [],\n }\n const counts: Record<MethodName, number> = {\n get: 0, put: 0, delete: 0, list: 0, loadAll: 0, saveAll: 0,\n }\n const errors: Record<MethodName, number> = {\n get: 0, put: 0, delete: 0, list: 0, loadAll: 0, saveAll: 0,\n }\n let casConflicts = 0\n let windowStart = Date.now()\n let currentStatus: MeterStatus = 'ok'\n const listeners = new Set<(e: MeterEvent) => void>()\n\n function recordOp(method: MethodName, durationMs: number, success: boolean, error?: Error): void {\n counts[method]++\n if (!success) {\n errors[method]++\n if (error instanceof ConflictError) casConflicts++\n }\n const arr = samples[method]\n arr.push(durationMs)\n if (arr.length > sampleLimit) {\n arr.splice(0, arr.length - sampleLimit)\n }\n // Status transition check — only for put-method degraded thresholds\n if (method === 'put' && counts.put >= 10) {\n const put = computeMethodStats(samples.put, counts.put, errors.put)\n const breached = put.p99 > degradedMs\n if (breached && currentStatus === 'ok') transition('degraded', method, put.p99, `put p99 ${put.p99}ms > ${degradedMs}ms`)\n else if (!breached && currentStatus === 'degraded') transition('ok', method, put.p99, `put p99 recovered to ${put.p99}ms`)\n }\n }\n\n function transition(next: MeterStatus, method?: MethodName, p99?: number, reason = ''): void {\n if (next === currentStatus) return\n const prior = currentStatus\n currentStatus = next\n const event: MeterEvent = {\n type: next === 'ok' ? 'restored' : 'degraded',\n status: next,\n ...(method !== undefined ? { method } : {}),\n ...(p99 !== undefined ? { p99 } : {}),\n reason, at: new Date().toISOString(),\n }\n for (const l of listeners) {\n try { l(event) } catch { /* isolate listener errors */ }\n }\n if (next === 'degraded' && prior !== 'degraded') options.onDegraded?.(event)\n if (next === 'ok' && prior !== 'ok') options.onRestored?.(event)\n }\n\n // Build the wrapped store via hub's withMetrics middleware (one event\n // per op, already includes success/error + duration).\n const metrics = wrapStore(\n inner,\n withMetrics({\n onOperation(op) {\n recordOp(op.method, op.durationMs, op.success, op.error)\n },\n }),\n )\n\n // Optional synthetic liveness timer\n const livenessTimer = options.liveness\n ? startLiveness(inner, options.liveness, transition)\n : null\n\n const handle: MeterHandle = {\n snapshot(): MeterSnapshot {\n const byMethod = {} as Record<MethodName, MethodStats>\n let total = 0\n for (const m of METHODS) {\n byMethod[m] = computeMethodStats(samples[m], counts[m], errors[m])\n total += counts[m]\n }\n return {\n byMethod,\n status: currentStatus,\n casConflicts,\n totalCalls: total,\n windowMs: Date.now() - windowStart,\n collectedAt: new Date().toISOString(),\n }\n },\n reset(): void {\n for (const m of METHODS) {\n samples[m].length = 0\n counts[m] = 0\n errors[m] = 0\n }\n casConflicts = 0\n windowStart = Date.now()\n },\n subscribe(listener): () => void {\n listeners.add(listener)\n return () => { listeners.delete(listener) }\n },\n close(): void {\n if (livenessTimer) clearInterval(livenessTimer)\n listeners.clear()\n },\n }\n\n // Preserve the store name so routing/logging continues to identify\n // the underlying backend.\n const renamed: NoydbStore = {\n ...metrics,\n name: inner.name ? `meter(${inner.name})` : 'meter',\n }\n\n return { store: renamed, meter: handle }\n}\n\n// ── Internals ───────────────────────────────────────────────────────────\n\nfunction computeMethodStats(sorted: number[], count: number, errorCount: number): MethodStats {\n if (count === 0) {\n return { count: 0, errors: 0, p50: 0, p90: 0, p99: 0, max: 0, avg: 0 }\n }\n // Sort a copy so reads don't disturb the FIFO buffer\n const s = [...sorted].sort((a, b) => a - b)\n const pct = (q: number): number => s[Math.min(s.length - 1, Math.floor(q * s.length))]!\n const sum = s.reduce((a, b) => a + b, 0)\n return {\n count,\n errors: errorCount,\n p50: pct(0.5),\n p90: pct(0.9),\n p99: pct(0.99),\n max: s[s.length - 1]!,\n avg: Math.round(sum / s.length),\n }\n}\n\nfunction startLiveness(\n inner: NoydbStore,\n opts: LivenessOptions,\n transition: (status: MeterStatus, method?: MethodName, p99?: number, reason?: string) => void,\n): ReturnType<typeof setInterval> {\n const vault = opts.vault ?? 'probe-vault'\n const collection = opts.collection ?? 'probe-liveness'\n const pingId = 'liveness'\n\n const timer = setInterval(() => {\n void tick()\n }, opts.interval)\n\n async function tick(): Promise<void> {\n try {\n if (typeof inner.ping === 'function') {\n const ok = await inner.ping()\n if (!ok) return transition('unreachable', undefined, undefined, 'ping returned false')\n } else {\n // Fallback: put + delete — exercises the write path\n await inner.put(vault, collection, pingId, {\n _noydb: 1, _v: 1,\n _ts: new Date().toISOString(),\n _iv: 'AAAAAAAAAAAAAAAA',\n _data: 'cHJvYmU=',\n })\n await inner.delete(vault, collection, pingId)\n }\n // On a successful check, transition back to ok if we were unreachable\n transition('ok', undefined, undefined, 'liveness check succeeded')\n } catch (err) {\n transition('unreachable', undefined, undefined, `liveness error: ${(err as Error).message}`)\n }\n }\n\n return timer\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,146 @@
1
+ import { NoydbStore } from '@noy-db/hub';
2
+
3
+ /**
4
+ * **@noy-db/to-meter** — pass-through meter for `@noy-db/to-*` stores.
5
+ *
6
+ * Wraps any `NoydbStore` and returns a new store that behaves
7
+ * identically but records per-method timing, error rates, byte
8
+ * counts, and (optionally) periodic liveness status. The meter is
9
+ * itself a `NoydbStore`, so it slots anywhere a store fits:
10
+ *
11
+ * ```ts
12
+ * import { toMeter } from '@noy-db/to-meter'
13
+ * import { awsDynamoStore } from '@noy-db/to-aws-dynamo'
14
+ *
15
+ * const dynamo = awsDynamoStore({ table: 'live' })
16
+ * const { store, meter } = toMeter(dynamo, {
17
+ * liveness: { interval: 60_000 }, // optional synthetic pings
18
+ * degradedMs: 200, // p99 threshold for `degraded` event
19
+ * onDegraded: (e) => console.warn(e),
20
+ * })
21
+ *
22
+ * const db = await createNoydb({ store })
23
+ *
24
+ * // at any time
25
+ * console.log(meter.snapshot())
26
+ * // {
27
+ * // byMethod: {
28
+ * // get: { count: 142, p50: 3, p99: 28, errors: 0 },
29
+ * // put: { count: 43, p50: 11, p99: 92, errors: 1 },
30
+ * // ...
31
+ * // },
32
+ * // status: 'ok' | 'degraded' | 'unreachable',
33
+ * // casConflicts: 2,
34
+ * // totalCalls: 230,
35
+ * // windowMs: 45_280,
36
+ * // }
37
+ * ```
38
+ *
39
+ * ## Relation to `withMetrics`
40
+ *
41
+ * This package **uses** hub's `withMetrics` middleware internally —
42
+ * don't think of it as a replacement. `withMetrics` is the raw event
43
+ * stream (one callback per op); `toMeter` is the aggregator that
44
+ * bucketises events into percentiles + a health verdict.
45
+ *
46
+ * ## Relation to `to-probe`
47
+ *
48
+ * - `to-probe` runs **synthetic** benchmarks on an empty store —
49
+ * answers "should I adopt this store?".
50
+ * - `to-meter` observes **real traffic** through the live store —
51
+ * answers "how is this store performing right now?".
52
+ *
53
+ * Composable: `toMeter(probe-recommended-store)` after a probe pass
54
+ * validates adoption.
55
+ *
56
+ * @packageDocumentation
57
+ */
58
+
59
+ type MethodName = 'get' | 'put' | 'delete' | 'list' | 'loadAll' | 'saveAll';
60
+ type MeterStatus = 'ok' | 'degraded' | 'unreachable';
61
+ /** Latency + counts for a single store method. */
62
+ interface MethodStats {
63
+ readonly count: number;
64
+ readonly errors: number;
65
+ readonly p50: number;
66
+ readonly p90: number;
67
+ readonly p99: number;
68
+ readonly max: number;
69
+ readonly avg: number;
70
+ }
71
+ /** Full snapshot of meter state at one moment. */
72
+ interface MeterSnapshot {
73
+ readonly byMethod: Record<MethodName, MethodStats>;
74
+ readonly status: MeterStatus;
75
+ readonly casConflicts: number;
76
+ readonly totalCalls: number;
77
+ readonly windowMs: number;
78
+ readonly collectedAt: string;
79
+ }
80
+ /** Degraded/restored event. */
81
+ interface MeterEvent {
82
+ readonly type: 'degraded' | 'restored';
83
+ readonly status: MeterStatus;
84
+ readonly method?: MethodName;
85
+ readonly p99?: number;
86
+ readonly reason: string;
87
+ readonly at: string;
88
+ }
89
+ interface LivenessOptions {
90
+ /** Milliseconds between synthetic health checks. */
91
+ readonly interval: number;
92
+ /** Vault to use for the liveness `put`/`delete` pair. Default `'probe-vault'`. */
93
+ readonly vault?: string;
94
+ /** Collection to use. Default `'probe-liveness'`. Do NOT use a `_`-prefixed name. */
95
+ readonly collection?: string;
96
+ }
97
+ interface MeterOptions {
98
+ /**
99
+ * Upper bound on retained latency samples per method. When the
100
+ * sample array grows past this, oldest entries are dropped. Default
101
+ * 1024 — keeps p50/p99 reasonably accurate with bounded memory.
102
+ */
103
+ readonly sampleLimit?: number;
104
+ /**
105
+ * Optional periodic liveness ping. Uses the store's `ping()` if
106
+ * available, otherwise falls back to a `put`/`delete` pair on a
107
+ * dedicated collection.
108
+ */
109
+ readonly liveness?: LivenessOptions;
110
+ /**
111
+ * p99 latency threshold (ms) for `put` — if crossed, emit a
112
+ * `degraded` event. Default 500.
113
+ */
114
+ readonly degradedMs?: number;
115
+ /** Called when the meter transitions to `degraded`. */
116
+ readonly onDegraded?: (event: MeterEvent) => void;
117
+ /** Called when the meter transitions back to `ok`. */
118
+ readonly onRestored?: (event: MeterEvent) => void;
119
+ }
120
+ /** Handle returned alongside the wrapped store. */
121
+ interface MeterHandle {
122
+ /** Current snapshot. Safe to call frequently — O(k log k) on sample sizes. */
123
+ snapshot(): MeterSnapshot;
124
+ /** Reset all counters and drop samples. Handy for per-request metering. */
125
+ reset(): void;
126
+ /** Subscribe to degraded/restored transitions. Returns an unsubscribe fn. */
127
+ subscribe(listener: (event: MeterEvent) => void): () => void;
128
+ /** Stop the liveness timer (if any) and release resources. */
129
+ close(): void;
130
+ }
131
+ interface ToMeterResult {
132
+ readonly store: NoydbStore;
133
+ readonly meter: MeterHandle;
134
+ }
135
+ /**
136
+ * Wrap a store so every call is timed + counted. Returns the wrapped
137
+ * store and a handle for inspecting the aggregate.
138
+ *
139
+ * The wrapped store is a drop-in replacement for the inner store —
140
+ * same 6 methods, same types, same behaviour on success and error. The
141
+ * meter adds zero semantic changes: errors still throw, conflicts
142
+ * still surface as {@link ConflictError}.
143
+ */
144
+ declare function toMeter(inner: NoydbStore, options?: MeterOptions): ToMeterResult;
145
+
146
+ export { type LivenessOptions, type MeterEvent, type MeterHandle, type MeterOptions, type MeterSnapshot, type MeterStatus, type MethodName, type MethodStats, type ToMeterResult, toMeter };
@@ -0,0 +1,146 @@
1
+ import { NoydbStore } from '@noy-db/hub';
2
+
3
+ /**
4
+ * **@noy-db/to-meter** — pass-through meter for `@noy-db/to-*` stores.
5
+ *
6
+ * Wraps any `NoydbStore` and returns a new store that behaves
7
+ * identically but records per-method timing, error rates, byte
8
+ * counts, and (optionally) periodic liveness status. The meter is
9
+ * itself a `NoydbStore`, so it slots anywhere a store fits:
10
+ *
11
+ * ```ts
12
+ * import { toMeter } from '@noy-db/to-meter'
13
+ * import { awsDynamoStore } from '@noy-db/to-aws-dynamo'
14
+ *
15
+ * const dynamo = awsDynamoStore({ table: 'live' })
16
+ * const { store, meter } = toMeter(dynamo, {
17
+ * liveness: { interval: 60_000 }, // optional synthetic pings
18
+ * degradedMs: 200, // p99 threshold for `degraded` event
19
+ * onDegraded: (e) => console.warn(e),
20
+ * })
21
+ *
22
+ * const db = await createNoydb({ store })
23
+ *
24
+ * // at any time
25
+ * console.log(meter.snapshot())
26
+ * // {
27
+ * // byMethod: {
28
+ * // get: { count: 142, p50: 3, p99: 28, errors: 0 },
29
+ * // put: { count: 43, p50: 11, p99: 92, errors: 1 },
30
+ * // ...
31
+ * // },
32
+ * // status: 'ok' | 'degraded' | 'unreachable',
33
+ * // casConflicts: 2,
34
+ * // totalCalls: 230,
35
+ * // windowMs: 45_280,
36
+ * // }
37
+ * ```
38
+ *
39
+ * ## Relation to `withMetrics`
40
+ *
41
+ * This package **uses** hub's `withMetrics` middleware internally —
42
+ * don't think of it as a replacement. `withMetrics` is the raw event
43
+ * stream (one callback per op); `toMeter` is the aggregator that
44
+ * bucketises events into percentiles + a health verdict.
45
+ *
46
+ * ## Relation to `to-probe`
47
+ *
48
+ * - `to-probe` runs **synthetic** benchmarks on an empty store —
49
+ * answers "should I adopt this store?".
50
+ * - `to-meter` observes **real traffic** through the live store —
51
+ * answers "how is this store performing right now?".
52
+ *
53
+ * Composable: `toMeter(probe-recommended-store)` after a probe pass
54
+ * validates adoption.
55
+ *
56
+ * @packageDocumentation
57
+ */
58
+
59
+ type MethodName = 'get' | 'put' | 'delete' | 'list' | 'loadAll' | 'saveAll';
60
+ type MeterStatus = 'ok' | 'degraded' | 'unreachable';
61
+ /** Latency + counts for a single store method. */
62
+ interface MethodStats {
63
+ readonly count: number;
64
+ readonly errors: number;
65
+ readonly p50: number;
66
+ readonly p90: number;
67
+ readonly p99: number;
68
+ readonly max: number;
69
+ readonly avg: number;
70
+ }
71
+ /** Full snapshot of meter state at one moment. */
72
+ interface MeterSnapshot {
73
+ readonly byMethod: Record<MethodName, MethodStats>;
74
+ readonly status: MeterStatus;
75
+ readonly casConflicts: number;
76
+ readonly totalCalls: number;
77
+ readonly windowMs: number;
78
+ readonly collectedAt: string;
79
+ }
80
+ /** Degraded/restored event. */
81
+ interface MeterEvent {
82
+ readonly type: 'degraded' | 'restored';
83
+ readonly status: MeterStatus;
84
+ readonly method?: MethodName;
85
+ readonly p99?: number;
86
+ readonly reason: string;
87
+ readonly at: string;
88
+ }
89
+ interface LivenessOptions {
90
+ /** Milliseconds between synthetic health checks. */
91
+ readonly interval: number;
92
+ /** Vault to use for the liveness `put`/`delete` pair. Default `'probe-vault'`. */
93
+ readonly vault?: string;
94
+ /** Collection to use. Default `'probe-liveness'`. Do NOT use a `_`-prefixed name. */
95
+ readonly collection?: string;
96
+ }
97
+ interface MeterOptions {
98
+ /**
99
+ * Upper bound on retained latency samples per method. When the
100
+ * sample array grows past this, oldest entries are dropped. Default
101
+ * 1024 — keeps p50/p99 reasonably accurate with bounded memory.
102
+ */
103
+ readonly sampleLimit?: number;
104
+ /**
105
+ * Optional periodic liveness ping. Uses the store's `ping()` if
106
+ * available, otherwise falls back to a `put`/`delete` pair on a
107
+ * dedicated collection.
108
+ */
109
+ readonly liveness?: LivenessOptions;
110
+ /**
111
+ * p99 latency threshold (ms) for `put` — if crossed, emit a
112
+ * `degraded` event. Default 500.
113
+ */
114
+ readonly degradedMs?: number;
115
+ /** Called when the meter transitions to `degraded`. */
116
+ readonly onDegraded?: (event: MeterEvent) => void;
117
+ /** Called when the meter transitions back to `ok`. */
118
+ readonly onRestored?: (event: MeterEvent) => void;
119
+ }
120
+ /** Handle returned alongside the wrapped store. */
121
+ interface MeterHandle {
122
+ /** Current snapshot. Safe to call frequently — O(k log k) on sample sizes. */
123
+ snapshot(): MeterSnapshot;
124
+ /** Reset all counters and drop samples. Handy for per-request metering. */
125
+ reset(): void;
126
+ /** Subscribe to degraded/restored transitions. Returns an unsubscribe fn. */
127
+ subscribe(listener: (event: MeterEvent) => void): () => void;
128
+ /** Stop the liveness timer (if any) and release resources. */
129
+ close(): void;
130
+ }
131
+ interface ToMeterResult {
132
+ readonly store: NoydbStore;
133
+ readonly meter: MeterHandle;
134
+ }
135
+ /**
136
+ * Wrap a store so every call is timed + counted. Returns the wrapped
137
+ * store and a handle for inspecting the aggregate.
138
+ *
139
+ * The wrapped store is a drop-in replacement for the inner store —
140
+ * same 6 methods, same types, same behaviour on success and error. The
141
+ * meter adds zero semantic changes: errors still throw, conflicts
142
+ * still surface as {@link ConflictError}.
143
+ */
144
+ declare function toMeter(inner: NoydbStore, options?: MeterOptions): ToMeterResult;
145
+
146
+ export { type LivenessOptions, type MeterEvent, type MeterHandle, type MeterOptions, type MeterSnapshot, type MeterStatus, type MethodName, type MethodStats, type ToMeterResult, toMeter };
package/dist/index.js ADDED
@@ -0,0 +1,175 @@
1
+ // src/index.ts
2
+ import { ConflictError, wrapStore, withMetrics } from "@noy-db/hub";
3
+ var METHODS = ["get", "put", "delete", "list", "loadAll", "saveAll"];
4
+ function toMeter(inner, options = {}) {
5
+ const sampleLimit = options.sampleLimit ?? 1024;
6
+ const degradedMs = options.degradedMs ?? 500;
7
+ const samples = {
8
+ get: [],
9
+ put: [],
10
+ delete: [],
11
+ list: [],
12
+ loadAll: [],
13
+ saveAll: []
14
+ };
15
+ const counts = {
16
+ get: 0,
17
+ put: 0,
18
+ delete: 0,
19
+ list: 0,
20
+ loadAll: 0,
21
+ saveAll: 0
22
+ };
23
+ const errors = {
24
+ get: 0,
25
+ put: 0,
26
+ delete: 0,
27
+ list: 0,
28
+ loadAll: 0,
29
+ saveAll: 0
30
+ };
31
+ let casConflicts = 0;
32
+ let windowStart = Date.now();
33
+ let currentStatus = "ok";
34
+ const listeners = /* @__PURE__ */ new Set();
35
+ function recordOp(method, durationMs, success, error) {
36
+ counts[method]++;
37
+ if (!success) {
38
+ errors[method]++;
39
+ if (error instanceof ConflictError) casConflicts++;
40
+ }
41
+ const arr = samples[method];
42
+ arr.push(durationMs);
43
+ if (arr.length > sampleLimit) {
44
+ arr.splice(0, arr.length - sampleLimit);
45
+ }
46
+ if (method === "put" && counts.put >= 10) {
47
+ const put = computeMethodStats(samples.put, counts.put, errors.put);
48
+ const breached = put.p99 > degradedMs;
49
+ if (breached && currentStatus === "ok") transition("degraded", method, put.p99, `put p99 ${put.p99}ms > ${degradedMs}ms`);
50
+ else if (!breached && currentStatus === "degraded") transition("ok", method, put.p99, `put p99 recovered to ${put.p99}ms`);
51
+ }
52
+ }
53
+ function transition(next, method, p99, reason = "") {
54
+ if (next === currentStatus) return;
55
+ const prior = currentStatus;
56
+ currentStatus = next;
57
+ const event = {
58
+ type: next === "ok" ? "restored" : "degraded",
59
+ status: next,
60
+ ...method !== void 0 ? { method } : {},
61
+ ...p99 !== void 0 ? { p99 } : {},
62
+ reason,
63
+ at: (/* @__PURE__ */ new Date()).toISOString()
64
+ };
65
+ for (const l of listeners) {
66
+ try {
67
+ l(event);
68
+ } catch {
69
+ }
70
+ }
71
+ if (next === "degraded" && prior !== "degraded") options.onDegraded?.(event);
72
+ if (next === "ok" && prior !== "ok") options.onRestored?.(event);
73
+ }
74
+ const metrics = wrapStore(
75
+ inner,
76
+ withMetrics({
77
+ onOperation(op) {
78
+ recordOp(op.method, op.durationMs, op.success, op.error);
79
+ }
80
+ })
81
+ );
82
+ const livenessTimer = options.liveness ? startLiveness(inner, options.liveness, transition) : null;
83
+ const handle = {
84
+ snapshot() {
85
+ const byMethod = {};
86
+ let total = 0;
87
+ for (const m of METHODS) {
88
+ byMethod[m] = computeMethodStats(samples[m], counts[m], errors[m]);
89
+ total += counts[m];
90
+ }
91
+ return {
92
+ byMethod,
93
+ status: currentStatus,
94
+ casConflicts,
95
+ totalCalls: total,
96
+ windowMs: Date.now() - windowStart,
97
+ collectedAt: (/* @__PURE__ */ new Date()).toISOString()
98
+ };
99
+ },
100
+ reset() {
101
+ for (const m of METHODS) {
102
+ samples[m].length = 0;
103
+ counts[m] = 0;
104
+ errors[m] = 0;
105
+ }
106
+ casConflicts = 0;
107
+ windowStart = Date.now();
108
+ },
109
+ subscribe(listener) {
110
+ listeners.add(listener);
111
+ return () => {
112
+ listeners.delete(listener);
113
+ };
114
+ },
115
+ close() {
116
+ if (livenessTimer) clearInterval(livenessTimer);
117
+ listeners.clear();
118
+ }
119
+ };
120
+ const renamed = {
121
+ ...metrics,
122
+ name: inner.name ? `meter(${inner.name})` : "meter"
123
+ };
124
+ return { store: renamed, meter: handle };
125
+ }
126
+ function computeMethodStats(sorted, count, errorCount) {
127
+ if (count === 0) {
128
+ return { count: 0, errors: 0, p50: 0, p90: 0, p99: 0, max: 0, avg: 0 };
129
+ }
130
+ const s = [...sorted].sort((a, b) => a - b);
131
+ const pct = (q) => s[Math.min(s.length - 1, Math.floor(q * s.length))];
132
+ const sum = s.reduce((a, b) => a + b, 0);
133
+ return {
134
+ count,
135
+ errors: errorCount,
136
+ p50: pct(0.5),
137
+ p90: pct(0.9),
138
+ p99: pct(0.99),
139
+ max: s[s.length - 1],
140
+ avg: Math.round(sum / s.length)
141
+ };
142
+ }
143
+ function startLiveness(inner, opts, transition) {
144
+ const vault = opts.vault ?? "probe-vault";
145
+ const collection = opts.collection ?? "probe-liveness";
146
+ const pingId = "liveness";
147
+ const timer = setInterval(() => {
148
+ void tick();
149
+ }, opts.interval);
150
+ async function tick() {
151
+ try {
152
+ if (typeof inner.ping === "function") {
153
+ const ok = await inner.ping();
154
+ if (!ok) return transition("unreachable", void 0, void 0, "ping returned false");
155
+ } else {
156
+ await inner.put(vault, collection, pingId, {
157
+ _noydb: 1,
158
+ _v: 1,
159
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
160
+ _iv: "AAAAAAAAAAAAAAAA",
161
+ _data: "cHJvYmU="
162
+ });
163
+ await inner.delete(vault, collection, pingId);
164
+ }
165
+ transition("ok", void 0, void 0, "liveness check succeeded");
166
+ } catch (err) {
167
+ transition("unreachable", void 0, void 0, `liveness error: ${err.message}`);
168
+ }
169
+ }
170
+ return timer;
171
+ }
172
+ export {
173
+ toMeter
174
+ };
175
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/to-meter** — pass-through meter for `@noy-db/to-*` stores.\n *\n * Wraps any `NoydbStore` and returns a new store that behaves\n * identically but records per-method timing, error rates, byte\n * counts, and (optionally) periodic liveness status. The meter is\n * itself a `NoydbStore`, so it slots anywhere a store fits:\n *\n * ```ts\n * import { toMeter } from '@noy-db/to-meter'\n * import { awsDynamoStore } from '@noy-db/to-aws-dynamo'\n *\n * const dynamo = awsDynamoStore({ table: 'live' })\n * const { store, meter } = toMeter(dynamo, {\n * liveness: { interval: 60_000 }, // optional synthetic pings\n * degradedMs: 200, // p99 threshold for `degraded` event\n * onDegraded: (e) => console.warn(e),\n * })\n *\n * const db = await createNoydb({ store })\n *\n * // at any time\n * console.log(meter.snapshot())\n * // {\n * // byMethod: {\n * // get: { count: 142, p50: 3, p99: 28, errors: 0 },\n * // put: { count: 43, p50: 11, p99: 92, errors: 1 },\n * // ...\n * // },\n * // status: 'ok' | 'degraded' | 'unreachable',\n * // casConflicts: 2,\n * // totalCalls: 230,\n * // windowMs: 45_280,\n * // }\n * ```\n *\n * ## Relation to `withMetrics`\n *\n * This package **uses** hub's `withMetrics` middleware internally —\n * don't think of it as a replacement. `withMetrics` is the raw event\n * stream (one callback per op); `toMeter` is the aggregator that\n * bucketises events into percentiles + a health verdict.\n *\n * ## Relation to `to-probe`\n *\n * - `to-probe` runs **synthetic** benchmarks on an empty store —\n * answers \"should I adopt this store?\".\n * - `to-meter` observes **real traffic** through the live store —\n * answers \"how is this store performing right now?\".\n *\n * Composable: `toMeter(probe-recommended-store)` after a probe pass\n * validates adoption.\n *\n * @packageDocumentation\n */\nimport type { NoydbStore } from '@noy-db/hub'\nimport { ConflictError, wrapStore, withMetrics } from '@noy-db/hub'\n\n// ── Types ───────────────────────────────────────────────────────────────\n\nexport type MethodName = 'get' | 'put' | 'delete' | 'list' | 'loadAll' | 'saveAll'\n\nexport type MeterStatus = 'ok' | 'degraded' | 'unreachable'\n\n/** Latency + counts for a single store method. */\nexport interface MethodStats {\n readonly count: number\n readonly errors: number\n readonly p50: number\n readonly p90: number\n readonly p99: number\n readonly max: number\n readonly avg: number\n}\n\n/** Full snapshot of meter state at one moment. */\nexport interface MeterSnapshot {\n readonly byMethod: Record<MethodName, MethodStats>\n readonly status: MeterStatus\n readonly casConflicts: number\n readonly totalCalls: number\n readonly windowMs: number\n readonly collectedAt: string\n}\n\n/** Degraded/restored event. */\nexport interface MeterEvent {\n readonly type: 'degraded' | 'restored'\n readonly status: MeterStatus\n readonly method?: MethodName\n readonly p99?: number\n readonly reason: string\n readonly at: string\n}\n\nexport interface LivenessOptions {\n /** Milliseconds between synthetic health checks. */\n readonly interval: number\n /** Vault to use for the liveness `put`/`delete` pair. Default `'probe-vault'`. */\n readonly vault?: string\n /** Collection to use. Default `'probe-liveness'`. Do NOT use a `_`-prefixed name. */\n readonly collection?: string\n}\n\nexport interface MeterOptions {\n /**\n * Upper bound on retained latency samples per method. When the\n * sample array grows past this, oldest entries are dropped. Default\n * 1024 — keeps p50/p99 reasonably accurate with bounded memory.\n */\n readonly sampleLimit?: number\n /**\n * Optional periodic liveness ping. Uses the store's `ping()` if\n * available, otherwise falls back to a `put`/`delete` pair on a\n * dedicated collection.\n */\n readonly liveness?: LivenessOptions\n /**\n * p99 latency threshold (ms) for `put` — if crossed, emit a\n * `degraded` event. Default 500.\n */\n readonly degradedMs?: number\n /** Called when the meter transitions to `degraded`. */\n readonly onDegraded?: (event: MeterEvent) => void\n /** Called when the meter transitions back to `ok`. */\n readonly onRestored?: (event: MeterEvent) => void\n}\n\n/** Handle returned alongside the wrapped store. */\nexport interface MeterHandle {\n /** Current snapshot. Safe to call frequently — O(k log k) on sample sizes. */\n snapshot(): MeterSnapshot\n /** Reset all counters and drop samples. Handy for per-request metering. */\n reset(): void\n /** Subscribe to degraded/restored transitions. Returns an unsubscribe fn. */\n subscribe(listener: (event: MeterEvent) => void): () => void\n /** Stop the liveness timer (if any) and release resources. */\n close(): void\n}\n\nexport interface ToMeterResult {\n readonly store: NoydbStore\n readonly meter: MeterHandle\n}\n\n// ── Implementation ──────────────────────────────────────────────────────\n\nconst METHODS: readonly MethodName[] = ['get', 'put', 'delete', 'list', 'loadAll', 'saveAll']\n\n/**\n * Wrap a store so every call is timed + counted. Returns the wrapped\n * store and a handle for inspecting the aggregate.\n *\n * The wrapped store is a drop-in replacement for the inner store —\n * same 6 methods, same types, same behaviour on success and error. The\n * meter adds zero semantic changes: errors still throw, conflicts\n * still surface as {@link ConflictError}.\n */\nexport function toMeter(inner: NoydbStore, options: MeterOptions = {}): ToMeterResult {\n const sampleLimit = options.sampleLimit ?? 1024\n const degradedMs = options.degradedMs ?? 500\n\n const samples: Record<MethodName, number[]> = {\n get: [], put: [], delete: [], list: [], loadAll: [], saveAll: [],\n }\n const counts: Record<MethodName, number> = {\n get: 0, put: 0, delete: 0, list: 0, loadAll: 0, saveAll: 0,\n }\n const errors: Record<MethodName, number> = {\n get: 0, put: 0, delete: 0, list: 0, loadAll: 0, saveAll: 0,\n }\n let casConflicts = 0\n let windowStart = Date.now()\n let currentStatus: MeterStatus = 'ok'\n const listeners = new Set<(e: MeterEvent) => void>()\n\n function recordOp(method: MethodName, durationMs: number, success: boolean, error?: Error): void {\n counts[method]++\n if (!success) {\n errors[method]++\n if (error instanceof ConflictError) casConflicts++\n }\n const arr = samples[method]\n arr.push(durationMs)\n if (arr.length > sampleLimit) {\n arr.splice(0, arr.length - sampleLimit)\n }\n // Status transition check — only for put-method degraded thresholds\n if (method === 'put' && counts.put >= 10) {\n const put = computeMethodStats(samples.put, counts.put, errors.put)\n const breached = put.p99 > degradedMs\n if (breached && currentStatus === 'ok') transition('degraded', method, put.p99, `put p99 ${put.p99}ms > ${degradedMs}ms`)\n else if (!breached && currentStatus === 'degraded') transition('ok', method, put.p99, `put p99 recovered to ${put.p99}ms`)\n }\n }\n\n function transition(next: MeterStatus, method?: MethodName, p99?: number, reason = ''): void {\n if (next === currentStatus) return\n const prior = currentStatus\n currentStatus = next\n const event: MeterEvent = {\n type: next === 'ok' ? 'restored' : 'degraded',\n status: next,\n ...(method !== undefined ? { method } : {}),\n ...(p99 !== undefined ? { p99 } : {}),\n reason, at: new Date().toISOString(),\n }\n for (const l of listeners) {\n try { l(event) } catch { /* isolate listener errors */ }\n }\n if (next === 'degraded' && prior !== 'degraded') options.onDegraded?.(event)\n if (next === 'ok' && prior !== 'ok') options.onRestored?.(event)\n }\n\n // Build the wrapped store via hub's withMetrics middleware (one event\n // per op, already includes success/error + duration).\n const metrics = wrapStore(\n inner,\n withMetrics({\n onOperation(op) {\n recordOp(op.method, op.durationMs, op.success, op.error)\n },\n }),\n )\n\n // Optional synthetic liveness timer\n const livenessTimer = options.liveness\n ? startLiveness(inner, options.liveness, transition)\n : null\n\n const handle: MeterHandle = {\n snapshot(): MeterSnapshot {\n const byMethod = {} as Record<MethodName, MethodStats>\n let total = 0\n for (const m of METHODS) {\n byMethod[m] = computeMethodStats(samples[m], counts[m], errors[m])\n total += counts[m]\n }\n return {\n byMethod,\n status: currentStatus,\n casConflicts,\n totalCalls: total,\n windowMs: Date.now() - windowStart,\n collectedAt: new Date().toISOString(),\n }\n },\n reset(): void {\n for (const m of METHODS) {\n samples[m].length = 0\n counts[m] = 0\n errors[m] = 0\n }\n casConflicts = 0\n windowStart = Date.now()\n },\n subscribe(listener): () => void {\n listeners.add(listener)\n return () => { listeners.delete(listener) }\n },\n close(): void {\n if (livenessTimer) clearInterval(livenessTimer)\n listeners.clear()\n },\n }\n\n // Preserve the store name so routing/logging continues to identify\n // the underlying backend.\n const renamed: NoydbStore = {\n ...metrics,\n name: inner.name ? `meter(${inner.name})` : 'meter',\n }\n\n return { store: renamed, meter: handle }\n}\n\n// ── Internals ───────────────────────────────────────────────────────────\n\nfunction computeMethodStats(sorted: number[], count: number, errorCount: number): MethodStats {\n if (count === 0) {\n return { count: 0, errors: 0, p50: 0, p90: 0, p99: 0, max: 0, avg: 0 }\n }\n // Sort a copy so reads don't disturb the FIFO buffer\n const s = [...sorted].sort((a, b) => a - b)\n const pct = (q: number): number => s[Math.min(s.length - 1, Math.floor(q * s.length))]!\n const sum = s.reduce((a, b) => a + b, 0)\n return {\n count,\n errors: errorCount,\n p50: pct(0.5),\n p90: pct(0.9),\n p99: pct(0.99),\n max: s[s.length - 1]!,\n avg: Math.round(sum / s.length),\n }\n}\n\nfunction startLiveness(\n inner: NoydbStore,\n opts: LivenessOptions,\n transition: (status: MeterStatus, method?: MethodName, p99?: number, reason?: string) => void,\n): ReturnType<typeof setInterval> {\n const vault = opts.vault ?? 'probe-vault'\n const collection = opts.collection ?? 'probe-liveness'\n const pingId = 'liveness'\n\n const timer = setInterval(() => {\n void tick()\n }, opts.interval)\n\n async function tick(): Promise<void> {\n try {\n if (typeof inner.ping === 'function') {\n const ok = await inner.ping()\n if (!ok) return transition('unreachable', undefined, undefined, 'ping returned false')\n } else {\n // Fallback: put + delete — exercises the write path\n await inner.put(vault, collection, pingId, {\n _noydb: 1, _v: 1,\n _ts: new Date().toISOString(),\n _iv: 'AAAAAAAAAAAAAAAA',\n _data: 'cHJvYmU=',\n })\n await inner.delete(vault, collection, pingId)\n }\n // On a successful check, transition back to ok if we were unreachable\n transition('ok', undefined, undefined, 'liveness check succeeded')\n } catch (err) {\n transition('unreachable', undefined, undefined, `liveness error: ${(err as Error).message}`)\n }\n }\n\n return timer\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@noy-db/to-meter",
3
+ "version": "0.1.0-pre.3",
4
+ "description": "Pass-through meter for @noy-db/to-* stores — wraps any NoydbStore and records per-method latency percentiles, error rates, and byte counts on real traffic. Optional synthetic liveness probe emits degraded / restored events. No synthetic benchmarks (see @noy-db/to-probe for that) — this measures what your app is actually doing.",
5
+ "license": "MIT",
6
+ "author": "vLannaAi <vicio@lanna.ai>",
7
+ "homepage": "https://github.com/vLannaAi/noy-db/tree/main/packages/to-meter#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/vLannaAi/noy-db.git",
11
+ "directory": "packages/to-meter"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/vLannaAi/noy-db/issues"
15
+ },
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "exports": {
19
+ ".": {
20
+ "import": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "require": {
25
+ "types": "./dist/index.d.cts",
26
+ "default": "./dist/index.cjs"
27
+ }
28
+ }
29
+ },
30
+ "main": "./dist/index.cjs",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "engines": {
39
+ "node": ">=18.0.0"
40
+ },
41
+ "peerDependencies": {
42
+ "@noy-db/hub": "0.1.0-pre.3"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^22.0.0",
46
+ "@noy-db/hub": "0.1.0-pre.3"
47
+ },
48
+ "keywords": [
49
+ "noy-db",
50
+ "to-meter",
51
+ "metrics",
52
+ "percentiles",
53
+ "observability",
54
+ "monitoring",
55
+ "pass-through",
56
+ "zero-knowledge"
57
+ ],
58
+ "publishConfig": {
59
+ "access": "public",
60
+ "tag": "latest"
61
+ },
62
+ "scripts": {
63
+ "build": "tsup",
64
+ "test": "vitest run",
65
+ "lint": "eslint src/",
66
+ "typecheck": "tsc --noEmit"
67
+ }
68
+ }