@noy-db/to-meter 0.4.0-pre.7 → 0.4.0-pre.8
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/README.md +1 -1
- package/dist/index.d.ts +216 -13
- package/dist/index.js +359 -10
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ pnpm add @noy-db/hub @noy-db/to-meter
|
|
|
14
14
|
|
|
15
15
|
## What it is
|
|
16
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
|
|
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 — this measures what your app is actually doing.
|
|
18
18
|
|
|
19
19
|
## Status
|
|
20
20
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,190 @@
|
|
|
1
|
-
import { NoydbStore } from '@noy-db/hub';
|
|
1
|
+
import { StoreCapabilities, NoydbStore, SyncTargetRole } from '@noy-db/hub';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared types for the store-probe diagnostics (absorbed into `@noy-db/to-meter`, #845).
|
|
5
|
+
*
|
|
6
|
+
* Both `runStoreProbe()` and `probeTopology()` produce structured
|
|
7
|
+
* reports with the same vocabulary: a fixed set of per-axis measurement
|
|
8
|
+
* blocks, a `ProbeRisk[]` list with severity and a machine-readable
|
|
9
|
+
* `code`, and a `SuitabilityScore` triple (primary / sync-peer / backup)
|
|
10
|
+
* summarising whether the store is safe to use in that role.
|
|
11
|
+
*
|
|
12
|
+
* The `code` strings are the identifiers adopters pass to
|
|
13
|
+
* `createNoydb({ acknowledgeRisks: [...] })` to silence a known risk.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Role a store is being considered for. */
|
|
19
|
+
type ProbeRole = 'primary' | 'sync-peer' | 'backup' | 'archive';
|
|
20
|
+
/** Machine-readable risk identifiers. Keep this list closed — adopters
|
|
21
|
+
* pass these exact strings to `acknowledgeRisks`. */
|
|
22
|
+
type ProbeRiskCode = 'slow-write-p99' | 'slow-hydration' | 'slow-sync' | 'cas-mismatch' | 'cas-unsupported' | 'no-ping' | 'hydration-blocked' | 'bundle-as-sync-peer' | 'no-atomic-cas-sync-peer' | 'primary-slower-than-peer' | 'archive-pull-configured';
|
|
23
|
+
interface ProbeRisk {
|
|
24
|
+
readonly code: ProbeRiskCode;
|
|
25
|
+
readonly severity: 'warn' | 'error';
|
|
26
|
+
readonly message: string;
|
|
27
|
+
}
|
|
28
|
+
/** Per-axis latency measurement — all numbers in milliseconds. */
|
|
29
|
+
interface LatencyStats {
|
|
30
|
+
readonly count: number;
|
|
31
|
+
readonly p50: number;
|
|
32
|
+
readonly p99: number;
|
|
33
|
+
readonly max: number;
|
|
34
|
+
}
|
|
35
|
+
interface WriteAxis {
|
|
36
|
+
readonly serial: LatencyStats;
|
|
37
|
+
readonly concurrent: LatencyStats;
|
|
38
|
+
readonly coldStart: number;
|
|
39
|
+
}
|
|
40
|
+
interface CasAxis {
|
|
41
|
+
readonly concurrent: number;
|
|
42
|
+
readonly successes: number;
|
|
43
|
+
readonly rejections: number;
|
|
44
|
+
readonly expected: 'exactly-one' | 'multiple-ok';
|
|
45
|
+
}
|
|
46
|
+
interface HydrationAxis {
|
|
47
|
+
readonly records: number;
|
|
48
|
+
readonly loadAllMs: number;
|
|
49
|
+
readonly perRecordBytes: number;
|
|
50
|
+
readonly totalBytes: number;
|
|
51
|
+
}
|
|
52
|
+
interface SyncAxis {
|
|
53
|
+
readonly singlePushMs: number;
|
|
54
|
+
readonly batchPushMs: number;
|
|
55
|
+
readonly batchSize: number;
|
|
56
|
+
readonly bytesPerPush: number;
|
|
57
|
+
}
|
|
58
|
+
interface NetworkAxis {
|
|
59
|
+
readonly pingSupported: boolean;
|
|
60
|
+
readonly pingMs: number | null;
|
|
61
|
+
}
|
|
62
|
+
/** Suitability decision per role. */
|
|
63
|
+
interface SuitabilityScore {
|
|
64
|
+
/** Roles the store passes (no error-severity risks apply). */
|
|
65
|
+
readonly recommended: readonly ProbeRole[];
|
|
66
|
+
/** Risks that caller may choose to acknowledge. */
|
|
67
|
+
readonly risks: readonly ProbeRisk[];
|
|
68
|
+
}
|
|
69
|
+
/** Full report produced by `runStoreProbe()`. */
|
|
70
|
+
interface StoreProbeReport {
|
|
71
|
+
readonly store: string;
|
|
72
|
+
readonly capabilities: StoreCapabilities | null;
|
|
73
|
+
readonly write: WriteAxis;
|
|
74
|
+
readonly cas: CasAxis;
|
|
75
|
+
readonly hydration: HydrationAxis;
|
|
76
|
+
readonly sync: SyncAxis;
|
|
77
|
+
readonly network: NetworkAxis;
|
|
78
|
+
readonly suitability: SuitabilityScore;
|
|
79
|
+
readonly durationMs: number;
|
|
80
|
+
readonly probedAt: string;
|
|
81
|
+
}
|
|
82
|
+
/** Options for `runStoreProbe()`. */
|
|
83
|
+
interface ProbeOptions {
|
|
84
|
+
/**
|
|
85
|
+
* Probe vault name. Isolated from real data — cleaned up at the
|
|
86
|
+
* end of the probe. Default `'probe-vault'`. Avoid `_`-prefixed
|
|
87
|
+
* values: several stores hide `_`-collections from `loadAll`,
|
|
88
|
+
* which would make D3 (hydration) measure zero records.
|
|
89
|
+
*/
|
|
90
|
+
readonly vault?: string;
|
|
91
|
+
/**
|
|
92
|
+
* Collection used for probe writes. Default `'probe-benchmark'`.
|
|
93
|
+
* Leftover envelopes may persist if the probe is interrupted —
|
|
94
|
+
* adopters can safely delete anything under this name.
|
|
95
|
+
*/
|
|
96
|
+
readonly collection?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Declared capabilities of the store (for `casAtomic` verification).
|
|
99
|
+
* Stores in this codebase don't attach capabilities to the `NoydbStore`
|
|
100
|
+
* object itself — pass them explicitly so the probe can compare
|
|
101
|
+
* declared vs. measured behaviour.
|
|
102
|
+
*/
|
|
103
|
+
readonly capabilities?: StoreCapabilities;
|
|
104
|
+
/** Number of serial writes in the D1 latency sample. Default 20. */
|
|
105
|
+
readonly writeSampleSize?: number;
|
|
106
|
+
/** Number of parallel writers in the D2 CAS test. Default 10. */
|
|
107
|
+
readonly casConcurrency?: number;
|
|
108
|
+
/** Records to populate before measuring loadAll. Default 100. */
|
|
109
|
+
readonly hydrationRecords?: number;
|
|
110
|
+
/** Batch size for D4 sync economics. Default 50. */
|
|
111
|
+
readonly syncBatchSize?: number;
|
|
112
|
+
/** p99 write-latency threshold (ms). Above this → `slow-write-p99`. Default 100. */
|
|
113
|
+
readonly slowWriteMs?: number;
|
|
114
|
+
/** loadAll threshold (ms). Above this → `slow-hydration`. Default 500. */
|
|
115
|
+
readonly slowHydrationMs?: number;
|
|
116
|
+
/** Single-record push threshold (ms). Above this → `slow-sync`. Default 250. */
|
|
117
|
+
readonly slowSyncMs?: number;
|
|
118
|
+
}
|
|
119
|
+
/** Input for `probeTopology()`. */
|
|
120
|
+
interface TopologyProbeOptions extends ProbeOptions {
|
|
121
|
+
readonly store: NoydbStore;
|
|
122
|
+
readonly sync?: ReadonlyArray<{
|
|
123
|
+
readonly store: NoydbStore;
|
|
124
|
+
readonly role: SyncTargetRole;
|
|
125
|
+
readonly label?: string;
|
|
126
|
+
readonly hasPullPolicy?: boolean;
|
|
127
|
+
}>;
|
|
128
|
+
/** Expected number of concurrent human users. Default 1. */
|
|
129
|
+
readonly expectedUsers?: number;
|
|
130
|
+
}
|
|
131
|
+
interface TopologyRisk extends ProbeRisk {
|
|
132
|
+
/** Target label (or 'primary'). */
|
|
133
|
+
readonly target: string;
|
|
134
|
+
}
|
|
135
|
+
interface TopologyTargetReport extends StoreProbeReport {
|
|
136
|
+
readonly role: SyncTargetRole;
|
|
137
|
+
readonly label: string;
|
|
138
|
+
}
|
|
139
|
+
interface TopologyProbeReport {
|
|
140
|
+
readonly primary: StoreProbeReport;
|
|
141
|
+
readonly targets: readonly TopologyTargetReport[];
|
|
142
|
+
readonly topology: readonly TopologyRisk[];
|
|
143
|
+
/** `true` iff there are no error-severity risks across primary + targets + topology. */
|
|
144
|
+
readonly recommended: boolean;
|
|
145
|
+
readonly durationMs: number;
|
|
146
|
+
readonly probedAt: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* `runStoreProbe()` — setup-time suitability test for a `NoydbStore`.
|
|
151
|
+
*
|
|
152
|
+
* Five measurement axes (D1-D5 per spec in issue ):
|
|
153
|
+
*
|
|
154
|
+
* | Axis | Measures |
|
|
155
|
+
* |------|----------|
|
|
156
|
+
* | D1 — Write responsiveness | serial + concurrent put p50/p99, cold-start |
|
|
157
|
+
* | D2 — Conflict integrity | N parallel puts with same `expectedVersion` |
|
|
158
|
+
* | D3 — Hydration cost | `loadAll()` time and record-size footprint |
|
|
159
|
+
* | D4 — Sync economics | single + batch `put` cost, bytes/push |
|
|
160
|
+
* | D5 — Network resilience | `ping()` support + latency |
|
|
161
|
+
*
|
|
162
|
+
* Writes happen to an isolated `_probe / _probe` collection that the
|
|
163
|
+
* probe cleans up on completion. The probe does not mutate real
|
|
164
|
+
* application data — but if a probe is interrupted, stray envelopes
|
|
165
|
+
* may remain under that collection. Adopters can safely delete
|
|
166
|
+
* anything under the `_probe` vault.
|
|
167
|
+
*
|
|
168
|
+
* The probe never decrypts anything. It operates at the `NoydbStore`
|
|
169
|
+
* layer with handcrafted {@link EncryptedEnvelope}-shaped payloads — a
|
|
170
|
+
* probe run produces no keyring, no DEK, and no plaintext the store
|
|
171
|
+
* can see.
|
|
172
|
+
*
|
|
173
|
+
* @module
|
|
174
|
+
*/
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Run the full 5-axis probe against `store`. Returns a structured
|
|
178
|
+
* report with per-axis measurements and a {@link SuitabilityScore}.
|
|
179
|
+
*
|
|
180
|
+
* The probe is **idempotent-per-run**: it picks unique record IDs per
|
|
181
|
+
* invocation using a monotonically increasing counter seeded by
|
|
182
|
+
* `Date.now()`, so concurrent probe runs against the same store do
|
|
183
|
+
* not collide.
|
|
184
|
+
*/
|
|
185
|
+
declare function runStoreProbe(store: NoydbStore, options?: ProbeOptions): Promise<StoreProbeReport>;
|
|
186
|
+
|
|
187
|
+
declare function probeTopology(options: TopologyProbeOptions): Promise<TopologyProbeReport>;
|
|
2
188
|
|
|
3
189
|
/**
|
|
4
190
|
* **@noy-db/to-meter** — pass-through meter for `@noy-db/to-*` stores.
|
|
@@ -43,20 +229,21 @@ import { NoydbStore } from '@noy-db/hub';
|
|
|
43
229
|
* stream (one callback per op); `toMeter` is the aggregator that
|
|
44
230
|
* bucketises events into percentiles + a health verdict.
|
|
45
231
|
*
|
|
46
|
-
* ##
|
|
232
|
+
* ## Two modes, one package (#845)
|
|
47
233
|
*
|
|
48
|
-
* - `
|
|
49
|
-
*
|
|
50
|
-
* -
|
|
51
|
-
*
|
|
234
|
+
* - `runStoreProbe()` / `probeTopology()` run **synthetic** benchmarks on an
|
|
235
|
+
* empty store — they answer "should I adopt this store?". Absorbed here from
|
|
236
|
+
* the retired `@noy-db/to-probe`, which exported no store and so never fitted
|
|
237
|
+
* the `to<Backend>()` store-factory contract.
|
|
238
|
+
* - `toMeter()` observes **real traffic** through the live store — it answers
|
|
239
|
+
* "how is this store performing right now?".
|
|
52
240
|
*
|
|
53
|
-
* Composable: `toMeter(
|
|
54
|
-
* validates adoption.
|
|
241
|
+
* Composable: probe first to choose, then `toMeter(chosen)` to keep watching.
|
|
55
242
|
*
|
|
56
243
|
* @packageDocumentation
|
|
57
244
|
*/
|
|
58
245
|
|
|
59
|
-
type MethodName = 'get' | 'put' | 'delete' | 'list' | 'loadAll' | 'saveAll';
|
|
246
|
+
type MethodName = 'get' | 'put' | 'delete' | 'list' | 'loadAll' | 'saveAll' | 'listPage' | 'getStoreTime' | 'tx';
|
|
60
247
|
type MeterStatus = 'ok' | 'degraded' | 'unreachable';
|
|
61
248
|
/** Latency + counts for a single store method. */
|
|
62
249
|
interface MethodStats {
|
|
@@ -128,8 +315,24 @@ interface MeterHandle {
|
|
|
128
315
|
/** Stop the liveness timer (if any) and release resources. */
|
|
129
316
|
close(): void;
|
|
130
317
|
}
|
|
131
|
-
|
|
132
|
-
|
|
318
|
+
/**
|
|
319
|
+
* What {@link toMeter} returns: a fully-conformant {@link NoydbStore} that also
|
|
320
|
+
* carries its own {@link MeterHandle}.
|
|
321
|
+
*
|
|
322
|
+
* Shaped after `RoutedNoydbStore` (hub's `routeStore`), which is likewise a
|
|
323
|
+
* store plus a control surface. Being a store rather than a `{ store, meter }`
|
|
324
|
+
* tuple is what lets a meter sit anywhere a store can — including nested inside
|
|
325
|
+
* `routeStore`, so each backend in a compound topology can be metered
|
|
326
|
+
* independently:
|
|
327
|
+
*
|
|
328
|
+
* ```ts
|
|
329
|
+
* const pg = toMeter(toPostgres({ … }))
|
|
330
|
+
* const s3 = toMeter(toAwsS3({ … }))
|
|
331
|
+
* const db = await createNoydb({ store: routeStore({ default: pg, blobs: s3 }) })
|
|
332
|
+
* pg.meter.snapshot() // per-backend timings, no extra plumbing
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
interface MeteredNoydbStore extends NoydbStore {
|
|
133
336
|
readonly meter: MeterHandle;
|
|
134
337
|
}
|
|
135
338
|
/**
|
|
@@ -141,6 +344,6 @@ interface ToMeterResult {
|
|
|
141
344
|
* meter adds zero semantic changes: errors still throw, conflicts
|
|
142
345
|
* still surface as {@link ConflictError}.
|
|
143
346
|
*/
|
|
144
|
-
declare function toMeter(inner
|
|
347
|
+
declare function toMeter(inner?: NoydbStore, options?: MeterOptions): MeteredNoydbStore;
|
|
145
348
|
|
|
146
|
-
export { type LivenessOptions, type MeterEvent, type MeterHandle, type MeterOptions, type MeterSnapshot, type MeterStatus, type MethodName, type MethodStats, type
|
|
349
|
+
export { type CasAxis, type HydrationAxis, type LatencyStats, type LivenessOptions, type MeterEvent, type MeterHandle, type MeterOptions, type MeterSnapshot, type MeterStatus, type MeteredNoydbStore, type MethodName, type MethodStats, type NetworkAxis, type ProbeOptions, type ProbeRisk, type ProbeRiskCode, type ProbeRole, type StoreProbeReport, type SuitabilityScore, type SyncAxis, type TopologyProbeOptions, type TopologyProbeReport, type TopologyRisk, type TopologyTargetReport, type WriteAxis, probeTopology, runStoreProbe, toMeter };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,319 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { ConflictError, wrapStore, withMetrics } from "@noy-db/hub";
|
|
3
|
-
|
|
2
|
+
import { ConflictError, wrapStore, withMetrics, memoryStore } from "@noy-db/hub";
|
|
3
|
+
|
|
4
|
+
// src/probe.ts
|
|
5
|
+
var PROBE_VAULT = "probe-vault";
|
|
6
|
+
var PROBE_COLLECTION = "probe-benchmark";
|
|
7
|
+
async function runStoreProbe(store, options = {}) {
|
|
8
|
+
const started = Date.now();
|
|
9
|
+
const vault = options.vault ?? PROBE_VAULT;
|
|
10
|
+
const collection = options.collection ?? PROBE_COLLECTION;
|
|
11
|
+
const runId = Date.now().toString(36);
|
|
12
|
+
const write = await probeWrite(store, vault, collection, runId, options);
|
|
13
|
+
const cas = await probeCas(store, vault, collection, runId, options);
|
|
14
|
+
const hydration = await probeHydration(store, vault, collection, runId, options);
|
|
15
|
+
const sync = await probeSync(store, vault, collection, runId, options);
|
|
16
|
+
const network = await probeNetwork(store);
|
|
17
|
+
const capabilities = options.capabilities ?? null;
|
|
18
|
+
const risks = collectRisks(options, write, cas, hydration, sync, network, capabilities);
|
|
19
|
+
const suitability = score(risks);
|
|
20
|
+
await bestEffortCleanup(store, vault, collection);
|
|
21
|
+
return {
|
|
22
|
+
store: store.name ?? "unnamed",
|
|
23
|
+
capabilities,
|
|
24
|
+
write,
|
|
25
|
+
cas,
|
|
26
|
+
hydration,
|
|
27
|
+
sync,
|
|
28
|
+
network,
|
|
29
|
+
suitability,
|
|
30
|
+
durationMs: Date.now() - started,
|
|
31
|
+
probedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function probeWrite(store, vault, collection, runId, options) {
|
|
35
|
+
const n = options.writeSampleSize ?? 20;
|
|
36
|
+
const coldId = `w-${runId}-cold`;
|
|
37
|
+
const coldStart = Date.now();
|
|
38
|
+
await store.put(vault, collection, coldId, envelope(1));
|
|
39
|
+
const coldMs = Date.now() - coldStart;
|
|
40
|
+
const serialSamples = [];
|
|
41
|
+
for (let i = 0; i < n; i++) {
|
|
42
|
+
const t0 = Date.now();
|
|
43
|
+
await store.put(vault, collection, `w-${runId}-s-${i}`, envelope(1));
|
|
44
|
+
serialSamples.push(Date.now() - t0);
|
|
45
|
+
}
|
|
46
|
+
const concurrentSamples = [];
|
|
47
|
+
for (let batch = 0; batch < 5; batch++) {
|
|
48
|
+
const t0 = Date.now();
|
|
49
|
+
await Promise.all(
|
|
50
|
+
Array.from(
|
|
51
|
+
{ length: 10 },
|
|
52
|
+
(_, j) => store.put(vault, collection, `w-${runId}-c-${batch}-${j}`, envelope(1))
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
concurrentSamples.push(Date.now() - t0);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
coldStart: coldMs,
|
|
59
|
+
serial: stats(serialSamples),
|
|
60
|
+
concurrent: stats(concurrentSamples)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function probeCas(store, vault, collection, runId, options) {
|
|
64
|
+
const concurrency = options.casConcurrency ?? 10;
|
|
65
|
+
const id = `cas-${runId}`;
|
|
66
|
+
await store.put(vault, collection, id, envelope(1));
|
|
67
|
+
const settled = await Promise.allSettled(
|
|
68
|
+
Array.from(
|
|
69
|
+
{ length: concurrency },
|
|
70
|
+
(_, i) => store.put(vault, collection, id, envelope(2, i), 1)
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
const successes = settled.filter((r) => r.status === "fulfilled").length;
|
|
74
|
+
const rejections = settled.length - successes;
|
|
75
|
+
const declaredAtomic = options.capabilities?.casAtomic ?? null;
|
|
76
|
+
const expected = declaredAtomic === false ? "multiple-ok" : "exactly-one";
|
|
77
|
+
return { concurrent: concurrency, successes, rejections, expected };
|
|
78
|
+
}
|
|
79
|
+
async function probeHydration(store, vault, collection, runId, options) {
|
|
80
|
+
const records = options.hydrationRecords ?? 100;
|
|
81
|
+
const existing = await store.list(vault, collection);
|
|
82
|
+
for (let i = existing.length; i < records; i++) {
|
|
83
|
+
await store.put(vault, collection, `h-${runId}-${i}`, envelope(1));
|
|
84
|
+
}
|
|
85
|
+
const t0 = Date.now();
|
|
86
|
+
const snapshot = await store.loadAll(vault);
|
|
87
|
+
const loadAllMs = Date.now() - t0;
|
|
88
|
+
const totalBytes = estimateBytes(snapshot);
|
|
89
|
+
const loaded = Object.values(snapshot).reduce(
|
|
90
|
+
(sum, coll) => sum + Object.keys(coll).length,
|
|
91
|
+
0
|
|
92
|
+
);
|
|
93
|
+
const perRecordBytes = loaded > 0 ? Math.round(totalBytes / loaded) : 0;
|
|
94
|
+
return { records: loaded, loadAllMs, totalBytes, perRecordBytes };
|
|
95
|
+
}
|
|
96
|
+
async function probeSync(store, vault, collection, runId, options) {
|
|
97
|
+
const batchSize = options.syncBatchSize ?? 50;
|
|
98
|
+
const singleStart = Date.now();
|
|
99
|
+
await store.put(vault, collection, `sync-${runId}-single`, envelope(1));
|
|
100
|
+
const singlePushMs = Date.now() - singleStart;
|
|
101
|
+
const t0 = Date.now();
|
|
102
|
+
for (let i = 0; i < batchSize; i++) {
|
|
103
|
+
await store.put(vault, collection, `sync-${runId}-b-${i}`, envelope(1));
|
|
104
|
+
}
|
|
105
|
+
const batchPushMs = Date.now() - t0;
|
|
106
|
+
const bytesPerPush = approxEnvelopeBytes();
|
|
107
|
+
return { singlePushMs, batchPushMs, batchSize, bytesPerPush };
|
|
108
|
+
}
|
|
109
|
+
async function probeNetwork(store) {
|
|
110
|
+
if (typeof store.ping !== "function") {
|
|
111
|
+
return { pingSupported: false, pingMs: null };
|
|
112
|
+
}
|
|
113
|
+
const t0 = Date.now();
|
|
114
|
+
try {
|
|
115
|
+
await store.ping();
|
|
116
|
+
return { pingSupported: true, pingMs: Date.now() - t0 };
|
|
117
|
+
} catch {
|
|
118
|
+
return { pingSupported: true, pingMs: null };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function collectRisks(options, write, cas, hydration, sync, network, capabilities) {
|
|
122
|
+
const risks = [];
|
|
123
|
+
const slowWriteMs = options.slowWriteMs ?? 100;
|
|
124
|
+
const slowHydrationMs = options.slowHydrationMs ?? 500;
|
|
125
|
+
const slowSyncMs = options.slowSyncMs ?? 250;
|
|
126
|
+
if (write.serial.p99 > slowWriteMs) {
|
|
127
|
+
risks.push({
|
|
128
|
+
code: "slow-write-p99",
|
|
129
|
+
severity: "warn",
|
|
130
|
+
message: `Serial write p99 ${write.serial.p99}ms exceeds threshold ${slowWriteMs}ms`
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (hydration.loadAllMs > slowHydrationMs) {
|
|
134
|
+
risks.push({
|
|
135
|
+
code: "slow-hydration",
|
|
136
|
+
severity: "warn",
|
|
137
|
+
message: `loadAll(${hydration.records}) took ${hydration.loadAllMs}ms (threshold ${slowHydrationMs}ms)`
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (sync.singlePushMs > slowSyncMs) {
|
|
141
|
+
risks.push({
|
|
142
|
+
code: "slow-sync",
|
|
143
|
+
severity: "warn",
|
|
144
|
+
message: `Single-record push ${sync.singlePushMs}ms exceeds ${slowSyncMs}ms`
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (capabilities?.casAtomic === true && cas.successes > 1) {
|
|
148
|
+
risks.push({
|
|
149
|
+
code: "cas-mismatch",
|
|
150
|
+
severity: "error",
|
|
151
|
+
message: `Store declared casAtomic:true but ${cas.successes}/${cas.concurrent} concurrent puts succeeded (expected exactly 1)`
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (capabilities?.casAtomic === false) {
|
|
155
|
+
risks.push({
|
|
156
|
+
code: "cas-unsupported",
|
|
157
|
+
severity: "warn",
|
|
158
|
+
message: "Store lacks atomic CAS \u2014 unsafe for multi-writer sync-peer role"
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (!network.pingSupported) {
|
|
162
|
+
risks.push({
|
|
163
|
+
code: "no-ping",
|
|
164
|
+
severity: "warn",
|
|
165
|
+
message: "Store has no ping() \u2014 runtime monitor will rely on list() as liveness check"
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return risks;
|
|
169
|
+
}
|
|
170
|
+
function score(risks) {
|
|
171
|
+
const hasError = risks.some((r) => r.severity === "error");
|
|
172
|
+
const casUnsupported = risks.some((r) => r.code === "cas-unsupported");
|
|
173
|
+
const slowWrite = risks.some((r) => r.code === "slow-write-p99");
|
|
174
|
+
const recommended = [];
|
|
175
|
+
if (!hasError) {
|
|
176
|
+
if (!slowWrite) recommended.push("primary");
|
|
177
|
+
if (!casUnsupported) recommended.push("sync-peer");
|
|
178
|
+
recommended.push("backup", "archive");
|
|
179
|
+
}
|
|
180
|
+
return { recommended, risks };
|
|
181
|
+
}
|
|
182
|
+
function envelope(version, seed = 0) {
|
|
183
|
+
const data = `probe-${version}-${seed}`.padEnd(64, "x");
|
|
184
|
+
const b64 = base64Encode(data);
|
|
185
|
+
return {
|
|
186
|
+
_noydb: 1,
|
|
187
|
+
_v: version,
|
|
188
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
189
|
+
_iv: base64Encode("0".repeat(12)),
|
|
190
|
+
_data: b64
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function base64Encode(s) {
|
|
194
|
+
if (typeof Buffer !== "undefined") return Buffer.from(s, "utf-8").toString("base64");
|
|
195
|
+
return btoa(unescape(encodeURIComponent(s)));
|
|
196
|
+
}
|
|
197
|
+
function approxEnvelopeBytes() {
|
|
198
|
+
return JSON.stringify(envelope(1)).length;
|
|
199
|
+
}
|
|
200
|
+
function estimateBytes(snapshot) {
|
|
201
|
+
let total = 0;
|
|
202
|
+
for (const coll of Object.values(snapshot)) {
|
|
203
|
+
for (const rec of Object.values(coll)) {
|
|
204
|
+
total += JSON.stringify(rec).length;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return total;
|
|
208
|
+
}
|
|
209
|
+
function stats(samples) {
|
|
210
|
+
if (samples.length === 0) return { count: 0, p50: 0, p99: 0, max: 0 };
|
|
211
|
+
const sorted = [...samples].sort((a, b) => a - b);
|
|
212
|
+
return {
|
|
213
|
+
count: sorted.length,
|
|
214
|
+
p50: percentile(sorted, 0.5),
|
|
215
|
+
p99: percentile(sorted, 0.99),
|
|
216
|
+
max: sorted[sorted.length - 1]
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function percentile(sorted, q) {
|
|
220
|
+
const idx = Math.min(sorted.length - 1, Math.floor(q * sorted.length));
|
|
221
|
+
return sorted[idx];
|
|
222
|
+
}
|
|
223
|
+
async function bestEffortCleanup(store, vault, collection) {
|
|
224
|
+
try {
|
|
225
|
+
const ids = await store.list(vault, collection);
|
|
226
|
+
await Promise.all(ids.map((id) => store.delete(vault, collection, id).catch(() => {
|
|
227
|
+
})));
|
|
228
|
+
} catch {
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/topology.ts
|
|
233
|
+
async function probeTopology(options) {
|
|
234
|
+
const started = Date.now();
|
|
235
|
+
const expectedUsers = options.expectedUsers ?? 1;
|
|
236
|
+
const primary = await runStoreProbe(options.store, options);
|
|
237
|
+
const targets = [];
|
|
238
|
+
for (const t of options.sync ?? []) {
|
|
239
|
+
const label = t.label ?? t.store.name ?? t.role;
|
|
240
|
+
const report = await runStoreProbe(t.store, { ...options, vault: `_probe-${label}` });
|
|
241
|
+
targets.push({ ...report, role: t.role, label });
|
|
242
|
+
}
|
|
243
|
+
const topology = evaluateTopology(options.store, primary, targets, options.sync, expectedUsers);
|
|
244
|
+
const allErrors = [
|
|
245
|
+
...primary.suitability.risks,
|
|
246
|
+
...targets.flatMap((t) => t.suitability.risks),
|
|
247
|
+
...topology
|
|
248
|
+
].filter((r) => r.severity === "error");
|
|
249
|
+
return {
|
|
250
|
+
primary,
|
|
251
|
+
targets,
|
|
252
|
+
topology,
|
|
253
|
+
recommended: allErrors.length === 0,
|
|
254
|
+
durationMs: Date.now() - started,
|
|
255
|
+
probedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function evaluateTopology(_primaryStore, primary, targets, syncTargets = [], expectedUsers) {
|
|
259
|
+
const risks = [];
|
|
260
|
+
targets.forEach((target, i) => {
|
|
261
|
+
const input = syncTargets[i];
|
|
262
|
+
const label = target.label;
|
|
263
|
+
if (target.role === "sync-peer" && looksLikeBundleStore(target.store)) {
|
|
264
|
+
risks.push({
|
|
265
|
+
target: label,
|
|
266
|
+
code: "bundle-as-sync-peer",
|
|
267
|
+
severity: "warn",
|
|
268
|
+
message: `"${label}" looks bundle-shaped \u2014 use role 'backup' or 'archive' for push-only semantics`
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (target.role === "sync-peer" && expectedUsers > 1 && target.capabilities?.casAtomic === false) {
|
|
272
|
+
risks.push({
|
|
273
|
+
target: label,
|
|
274
|
+
code: "no-atomic-cas-sync-peer",
|
|
275
|
+
severity: "error",
|
|
276
|
+
message: `"${label}" has casAtomic:false \u2014 unsafe as sync-peer for ${expectedUsers} concurrent users`
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (target.role === "sync-peer" && primary.write.serial.p99 > target.write.serial.p99 * 2) {
|
|
280
|
+
risks.push({
|
|
281
|
+
target: label,
|
|
282
|
+
code: "primary-slower-than-peer",
|
|
283
|
+
severity: "warn",
|
|
284
|
+
message: `Primary p99 ${primary.write.serial.p99}ms is >2\xD7 peer "${label}" p99 ${target.write.serial.p99}ms \u2014 unusual topology`
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
if (target.role === "archive" && input?.hasPullPolicy === true) {
|
|
288
|
+
risks.push({
|
|
289
|
+
target: label,
|
|
290
|
+
code: "archive-pull-configured",
|
|
291
|
+
severity: "error",
|
|
292
|
+
message: `"${label}" is an archive target but has a pull policy \u2014 archives are push-only`
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
return risks;
|
|
297
|
+
}
|
|
298
|
+
function looksLikeBundleStore(name) {
|
|
299
|
+
const n = name.toLowerCase();
|
|
300
|
+
return /drive|webdav|git|bundle/.test(n);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/index.ts
|
|
304
|
+
var METHODS = [
|
|
305
|
+
"get",
|
|
306
|
+
"put",
|
|
307
|
+
"delete",
|
|
308
|
+
"list",
|
|
309
|
+
"loadAll",
|
|
310
|
+
"saveAll",
|
|
311
|
+
"listPage",
|
|
312
|
+
"getStoreTime",
|
|
313
|
+
"tx"
|
|
314
|
+
];
|
|
4
315
|
function toMeter(inner, options = {}) {
|
|
316
|
+
const target = inner ?? memoryStore();
|
|
5
317
|
const sampleLimit = options.sampleLimit ?? 1024;
|
|
6
318
|
const degradedMs = options.degradedMs ?? 500;
|
|
7
319
|
const samples = {
|
|
@@ -10,7 +322,10 @@ function toMeter(inner, options = {}) {
|
|
|
10
322
|
delete: [],
|
|
11
323
|
list: [],
|
|
12
324
|
loadAll: [],
|
|
13
|
-
saveAll: []
|
|
325
|
+
saveAll: [],
|
|
326
|
+
listPage: [],
|
|
327
|
+
getStoreTime: [],
|
|
328
|
+
tx: []
|
|
14
329
|
};
|
|
15
330
|
const counts = {
|
|
16
331
|
get: 0,
|
|
@@ -18,7 +333,10 @@ function toMeter(inner, options = {}) {
|
|
|
18
333
|
delete: 0,
|
|
19
334
|
list: 0,
|
|
20
335
|
loadAll: 0,
|
|
21
|
-
saveAll: 0
|
|
336
|
+
saveAll: 0,
|
|
337
|
+
listPage: 0,
|
|
338
|
+
getStoreTime: 0,
|
|
339
|
+
tx: 0
|
|
22
340
|
};
|
|
23
341
|
const errors = {
|
|
24
342
|
get: 0,
|
|
@@ -26,7 +344,10 @@ function toMeter(inner, options = {}) {
|
|
|
26
344
|
delete: 0,
|
|
27
345
|
list: 0,
|
|
28
346
|
loadAll: 0,
|
|
29
|
-
saveAll: 0
|
|
347
|
+
saveAll: 0,
|
|
348
|
+
listPage: 0,
|
|
349
|
+
getStoreTime: 0,
|
|
350
|
+
tx: 0
|
|
30
351
|
};
|
|
31
352
|
let casConflicts = 0;
|
|
32
353
|
let windowStart = Date.now();
|
|
@@ -72,14 +393,14 @@ function toMeter(inner, options = {}) {
|
|
|
72
393
|
if (next === "ok" && prior !== "ok") options.onRestored?.(event);
|
|
73
394
|
}
|
|
74
395
|
const metrics = wrapStore(
|
|
75
|
-
|
|
396
|
+
target,
|
|
76
397
|
withMetrics({
|
|
77
398
|
onOperation(op) {
|
|
78
399
|
recordOp(op.method, op.durationMs, op.success, op.error);
|
|
79
400
|
}
|
|
80
401
|
})
|
|
81
402
|
);
|
|
82
|
-
const livenessTimer = options.liveness ? startLiveness(
|
|
403
|
+
const livenessTimer = options.liveness ? startLiveness(target, options.liveness, transition) : null;
|
|
83
404
|
const handle = {
|
|
84
405
|
snapshot() {
|
|
85
406
|
const byMethod = {};
|
|
@@ -117,11 +438,37 @@ function toMeter(inner, options = {}) {
|
|
|
117
438
|
listeners.clear();
|
|
118
439
|
}
|
|
119
440
|
};
|
|
120
|
-
|
|
441
|
+
return {
|
|
121
442
|
...metrics,
|
|
122
|
-
|
|
443
|
+
...meteredOptional(target, recordOp),
|
|
444
|
+
// Preserve the inner name so routing/logging still identifies the backend.
|
|
445
|
+
name: target.name ? `meter(${target.name})` : "meter",
|
|
446
|
+
meter: handle
|
|
123
447
|
};
|
|
124
|
-
|
|
448
|
+
}
|
|
449
|
+
function meteredOptional(target, record) {
|
|
450
|
+
const time = async (m, fn) => {
|
|
451
|
+
const start = Date.now();
|
|
452
|
+
try {
|
|
453
|
+
const out2 = await fn();
|
|
454
|
+
record(m, Date.now() - start, true);
|
|
455
|
+
return out2;
|
|
456
|
+
} catch (err) {
|
|
457
|
+
record(m, Date.now() - start, false, err);
|
|
458
|
+
throw err;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
const out = {};
|
|
462
|
+
if (typeof target.listPage === "function") {
|
|
463
|
+
out.listPage = (v, c, cur, lim) => time("listPage", () => target.listPage(v, c, cur, lim));
|
|
464
|
+
}
|
|
465
|
+
if (typeof target.getStoreTime === "function") {
|
|
466
|
+
out.getStoreTime = () => time("getStoreTime", () => target.getStoreTime());
|
|
467
|
+
}
|
|
468
|
+
if (typeof target.tx === "function") {
|
|
469
|
+
out.tx = (ops) => time("tx", () => target.tx(ops));
|
|
470
|
+
}
|
|
471
|
+
return out;
|
|
125
472
|
}
|
|
126
473
|
function computeMethodStats(sorted, count, errorCount) {
|
|
127
474
|
if (count === 0) {
|
|
@@ -170,6 +517,8 @@ function startLiveness(inner, opts, transition) {
|
|
|
170
517
|
return timer;
|
|
171
518
|
}
|
|
172
519
|
export {
|
|
520
|
+
probeTopology,
|
|
521
|
+
runStoreProbe,
|
|
173
522
|
toMeter
|
|
174
523
|
};
|
|
175
524
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/probe.ts","../src/topology.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 * ## Two modes, one package (#845)\n *\n * - `runStoreProbe()` / `probeTopology()` run **synthetic** benchmarks on an\n * empty store — they answer \"should I adopt this store?\". Absorbed here from\n * the retired `@noy-db/to-probe`, which exported no store and so never fitted\n * the `to<Backend>()` store-factory contract.\n * - `toMeter()` observes **real traffic** through the live store — it answers\n * \"how is this store performing right now?\".\n *\n * Composable: probe first to choose, then `toMeter(chosen)` to keep watching.\n *\n * @packageDocumentation\n */\nimport type { NoydbStore } from '@noy-db/hub'\nimport { ConflictError, wrapStore, withMetrics, memoryStore } from '@noy-db/hub'\n\n// ── Types ───────────────────────────────────────────────────────────────\n\nexport type MethodName =\n | 'get' | 'put' | 'delete' | 'list' | 'loadAll' | 'saveAll'\n // #845 — the optional surface is where the time usually goes (`listPage`\n // paginates, `tx` batches), so it is metered too. Absent on a given inner\n // store simply means the counter stays at zero.\n | 'listPage' | 'getStoreTime' | 'tx'\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\n/**\n * What {@link toMeter} returns: a fully-conformant {@link NoydbStore} that also\n * carries its own {@link MeterHandle}.\n *\n * Shaped after `RoutedNoydbStore` (hub's `routeStore`), which is likewise a\n * store plus a control surface. Being a store rather than a `{ store, meter }`\n * tuple is what lets a meter sit anywhere a store can — including nested inside\n * `routeStore`, so each backend in a compound topology can be metered\n * independently:\n *\n * ```ts\n * const pg = toMeter(toPostgres({ … }))\n * const s3 = toMeter(toAwsS3({ … }))\n * const db = await createNoydb({ store: routeStore({ default: pg, blobs: s3 }) })\n * pg.meter.snapshot() // per-backend timings, no extra plumbing\n * ```\n */\nexport interface MeteredNoydbStore extends NoydbStore {\n readonly meter: MeterHandle\n}\n\n// ── Implementation ──────────────────────────────────────────────────────\n\nconst METHODS: readonly MethodName[] = [\n 'get', 'put', 'delete', 'list', 'loadAll', 'saveAll',\n 'listPage', 'getStoreTime', 'tx',\n]\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 = {}): MeteredNoydbStore {\n // Omitting `inner` yields a self-contained metered in-memory store — the\n // test/debug case in one call, still composable for the real one.\n const target: NoydbStore = inner ?? memoryStore()\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 listPage: [], getStoreTime: [], tx: [],\n }\n const counts: Record<MethodName, number> = {\n get: 0, put: 0, delete: 0, list: 0, loadAll: 0, saveAll: 0,\n listPage: 0, getStoreTime: 0, tx: 0,\n }\n const errors: Record<MethodName, number> = {\n get: 0, put: 0, delete: 0, list: 0, loadAll: 0, saveAll: 0,\n listPage: 0, getStoreTime: 0, tx: 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 target,\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(target, 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 return {\n ...metrics,\n ...meteredOptional(target, recordOp),\n // Preserve the inner name so routing/logging still identifies the backend.\n name: target.name ? `meter(${target.name})` : 'meter',\n meter: handle,\n }\n}\n\n// ── Internals ───────────────────────────────────────────────────────────\n\n/**\n * Time the OPTIONAL store methods. `withMetrics` covers only the 6-method core,\n * so `listPage` / `getStoreTime` / `tx` previously passed through the wrap\n * unmeasured — invisible to a tool whose whole job is finding where time goes.\n *\n * Each is wrapped only when the inner store actually implements it, so an inner\n * store without `tx()` stays without `tx()` and its capability surface is\n * unchanged (a store must never gain a method by being metered).\n */\nfunction meteredOptional(\n target: NoydbStore,\n record: (m: MethodName, ms: number, ok: boolean, err?: Error) => void,\n): Partial<NoydbStore> {\n const time = async <T>(m: MethodName, fn: () => Promise<T>): Promise<T> => {\n const start = Date.now()\n try {\n const out = await fn()\n record(m, Date.now() - start, true)\n return out\n } catch (err) {\n record(m, Date.now() - start, false, err as Error)\n throw err\n }\n }\n const out: Record<string, unknown> = {}\n if (typeof target.listPage === 'function') {\n out.listPage = (v: string, c: string, cur?: string, lim?: number) =>\n time('listPage', () => target.listPage!(v, c, cur, lim))\n }\n if (typeof target.getStoreTime === 'function') {\n out.getStoreTime = () => time('getStoreTime', () => target.getStoreTime!())\n }\n if (typeof target.tx === 'function') {\n out.tx = (ops: Parameters<NonNullable<NoydbStore['tx']>>[0]) =>\n time('tx', () => target.tx!(ops))\n }\n return out as Partial<NoydbStore>\n}\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\n// ── Store diagnostics (absorbed from @noy-db/to-probe, #845) ────────────\n//\n// `to-probe` exported no store — it was a diagnostic suite, so it never fit\n// the `to<Backend>()` store-factory contract. Both packages answer the same\n// question (\"how is this store actually behaving?\"), one live and one as a\n// one-shot report, so they now ship together. `@noy-db/to-probe` is retired.\n\nexport { runStoreProbe } from './probe.js'\nexport { probeTopology } from './topology.js'\n\nexport type {\n ProbeOptions,\n ProbeRisk,\n ProbeRiskCode,\n ProbeRole,\n StoreProbeReport,\n SuitabilityScore,\n LatencyStats,\n WriteAxis,\n CasAxis,\n HydrationAxis,\n SyncAxis,\n NetworkAxis,\n TopologyProbeOptions,\n TopologyProbeReport,\n TopologyRisk,\n TopologyTargetReport,\n} from './probe-types.js'\n","/**\n * `runStoreProbe()` — setup-time suitability test for a `NoydbStore`.\n *\n * Five measurement axes (D1-D5 per spec in issue ):\n *\n * | Axis | Measures |\n * |------|----------|\n * | D1 — Write responsiveness | serial + concurrent put p50/p99, cold-start |\n * | D2 — Conflict integrity | N parallel puts with same `expectedVersion` |\n * | D3 — Hydration cost | `loadAll()` time and record-size footprint |\n * | D4 — Sync economics | single + batch `put` cost, bytes/push |\n * | D5 — Network resilience | `ping()` support + latency |\n *\n * Writes happen to an isolated `_probe / _probe` collection that the\n * probe cleans up on completion. The probe does not mutate real\n * application data — but if a probe is interrupted, stray envelopes\n * may remain under that collection. Adopters can safely delete\n * anything under the `_probe` vault.\n *\n * The probe never decrypts anything. It operates at the `NoydbStore`\n * layer with handcrafted {@link EncryptedEnvelope}-shaped payloads — a\n * probe run produces no keyring, no DEK, and no plaintext the store\n * can see.\n *\n * @module\n */\nimport type { EncryptedEnvelope, NoydbStore, StoreCapabilities, VaultSnapshot } from '@noy-db/hub'\nimport type {\n CasAxis,\n HydrationAxis,\n LatencyStats,\n NetworkAxis,\n ProbeOptions,\n ProbeRisk,\n ProbeRole,\n StoreProbeReport,\n SuitabilityScore,\n SyncAxis,\n WriteAxis,\n} from './probe-types.js'\n\nconst PROBE_VAULT = 'probe-vault'\nconst PROBE_COLLECTION = 'probe-benchmark'\n\n/**\n * Run the full 5-axis probe against `store`. Returns a structured\n * report with per-axis measurements and a {@link SuitabilityScore}.\n *\n * The probe is **idempotent-per-run**: it picks unique record IDs per\n * invocation using a monotonically increasing counter seeded by\n * `Date.now()`, so concurrent probe runs against the same store do\n * not collide.\n */\nexport async function runStoreProbe(\n store: NoydbStore,\n options: ProbeOptions = {},\n): Promise<StoreProbeReport> {\n const started = Date.now()\n const vault = options.vault ?? PROBE_VAULT\n const collection = options.collection ?? PROBE_COLLECTION\n const runId = Date.now().toString(36)\n\n const write = await probeWrite(store, vault, collection, runId, options)\n const cas = await probeCas(store, vault, collection, runId, options)\n const hydration = await probeHydration(store, vault, collection, runId, options)\n const sync = await probeSync(store, vault, collection, runId, options)\n const network = await probeNetwork(store)\n\n const capabilities = options.capabilities ?? null\n const risks = collectRisks(options, write, cas, hydration, sync, network, capabilities)\n const suitability = score(risks)\n\n await bestEffortCleanup(store, vault, collection)\n\n return {\n store: store.name ?? 'unnamed',\n capabilities,\n write, cas, hydration, sync, network,\n suitability,\n durationMs: Date.now() - started,\n probedAt: new Date().toISOString(),\n }\n}\n\n// ── D1 · write latency ────────────────────────────────────────────────────\n\nasync function probeWrite(\n store: NoydbStore,\n vault: string,\n collection: string,\n runId: string,\n options: ProbeOptions,\n): Promise<WriteAxis> {\n const n = options.writeSampleSize ?? 20\n\n // Cold start — single isolated write\n const coldId = `w-${runId}-cold`\n const coldStart = Date.now()\n await store.put(vault, collection, coldId, envelope(1))\n const coldMs = Date.now() - coldStart\n\n // Serial sample\n const serialSamples: number[] = []\n for (let i = 0; i < n; i++) {\n const t0 = Date.now()\n await store.put(vault, collection, `w-${runId}-s-${i}`, envelope(1))\n serialSamples.push(Date.now() - t0)\n }\n\n // Concurrent sample: 5 batches of 10, measured per-batch\n const concurrentSamples: number[] = []\n for (let batch = 0; batch < 5; batch++) {\n const t0 = Date.now()\n await Promise.all(\n Array.from({ length: 10 }, (_, j) =>\n store.put(vault, collection, `w-${runId}-c-${batch}-${j}`, envelope(1)),\n ),\n )\n concurrentSamples.push(Date.now() - t0)\n }\n\n return {\n coldStart: coldMs,\n serial: stats(serialSamples),\n concurrent: stats(concurrentSamples),\n }\n}\n\n// ── D2 · CAS integrity ────────────────────────────────────────────────────\n\nasync function probeCas(\n store: NoydbStore,\n vault: string,\n collection: string,\n runId: string,\n options: ProbeOptions,\n): Promise<CasAxis> {\n const concurrency = options.casConcurrency ?? 10\n const id = `cas-${runId}`\n\n // Seed with version 1\n await store.put(vault, collection, id, envelope(1))\n\n // Fire N concurrent puts all with expectedVersion=1. For a casAtomic\n // store: exactly one should succeed; the rest should reject with\n // ConflictError.\n const settled = await Promise.allSettled(\n Array.from({ length: concurrency }, (_, i) =>\n store.put(vault, collection, id, envelope(2, i), 1),\n ),\n )\n const successes = settled.filter((r) => r.status === 'fulfilled').length\n const rejections = settled.length - successes\n\n // What the store promised\n const declaredAtomic = options.capabilities?.casAtomic ?? null\n const expected = declaredAtomic === false ? 'multiple-ok' : 'exactly-one'\n\n return { concurrent: concurrency, successes, rejections, expected }\n}\n\n// ── D3 · hydration ────────────────────────────────────────────────────────\n\nasync function probeHydration(\n store: NoydbStore,\n vault: string,\n collection: string,\n runId: string,\n options: ProbeOptions,\n): Promise<HydrationAxis> {\n const records = options.hydrationRecords ?? 100\n\n // Fill the probe collection to the target record count. Writes from\n // D1/D2 already contributed some envelopes; we top up the rest.\n const existing = await store.list(vault, collection)\n for (let i = existing.length; i < records; i++) {\n await store.put(vault, collection, `h-${runId}-${i}`, envelope(1))\n }\n\n const t0 = Date.now()\n const snapshot = await store.loadAll(vault)\n const loadAllMs = Date.now() - t0\n\n const totalBytes = estimateBytes(snapshot)\n const loaded = Object.values(snapshot).reduce(\n (sum, coll) => sum + Object.keys(coll).length,\n 0,\n )\n const perRecordBytes = loaded > 0 ? Math.round(totalBytes / loaded) : 0\n\n return { records: loaded, loadAllMs, totalBytes, perRecordBytes }\n}\n\n// ── D4 · sync economics ───────────────────────────────────────────────────\n\nasync function probeSync(\n store: NoydbStore,\n vault: string,\n collection: string,\n runId: string,\n options: ProbeOptions,\n): Promise<SyncAxis> {\n const batchSize = options.syncBatchSize ?? 50\n\n // Single-record push\n const singleStart = Date.now()\n await store.put(vault, collection, `sync-${runId}-single`, envelope(1))\n const singlePushMs = Date.now() - singleStart\n\n // Batch push (simulated — sequential writes since the contract has no\n // bulk put; saveAll would also rewrite existing data)\n const t0 = Date.now()\n for (let i = 0; i < batchSize; i++) {\n await store.put(vault, collection, `sync-${runId}-b-${i}`, envelope(1))\n }\n const batchPushMs = Date.now() - t0\n\n // Rough bytes-per-push — envelope size plus keys\n const bytesPerPush = approxEnvelopeBytes()\n\n return { singlePushMs, batchPushMs, batchSize, bytesPerPush }\n}\n\n// ── D5 · network resilience ───────────────────────────────────────────────\n\nasync function probeNetwork(store: NoydbStore): Promise<NetworkAxis> {\n if (typeof store.ping !== 'function') {\n return { pingSupported: false, pingMs: null }\n }\n const t0 = Date.now()\n try {\n await store.ping()\n return { pingSupported: true, pingMs: Date.now() - t0 }\n } catch {\n return { pingSupported: true, pingMs: null }\n }\n}\n\n// ── Risk aggregation + scoring ────────────────────────────────────────────\n\nfunction collectRisks(\n options: ProbeOptions,\n write: WriteAxis,\n cas: CasAxis,\n hydration: HydrationAxis,\n sync: SyncAxis,\n network: NetworkAxis,\n capabilities: StoreCapabilities | null,\n): ProbeRisk[] {\n const risks: ProbeRisk[] = []\n const slowWriteMs = options.slowWriteMs ?? 100\n const slowHydrationMs = options.slowHydrationMs ?? 500\n const slowSyncMs = options.slowSyncMs ?? 250\n\n if (write.serial.p99 > slowWriteMs) {\n risks.push({\n code: 'slow-write-p99',\n severity: 'warn',\n message: `Serial write p99 ${write.serial.p99}ms exceeds threshold ${slowWriteMs}ms`,\n })\n }\n if (hydration.loadAllMs > slowHydrationMs) {\n risks.push({\n code: 'slow-hydration',\n severity: 'warn',\n message: `loadAll(${hydration.records}) took ${hydration.loadAllMs}ms (threshold ${slowHydrationMs}ms)`,\n })\n }\n if (sync.singlePushMs > slowSyncMs) {\n risks.push({\n code: 'slow-sync',\n severity: 'warn',\n message: `Single-record push ${sync.singlePushMs}ms exceeds ${slowSyncMs}ms`,\n })\n }\n if (capabilities?.casAtomic === true && cas.successes > 1) {\n risks.push({\n code: 'cas-mismatch',\n severity: 'error',\n message: `Store declared casAtomic:true but ${cas.successes}/${cas.concurrent} concurrent puts succeeded (expected exactly 1)`,\n })\n }\n if (capabilities?.casAtomic === false) {\n risks.push({\n code: 'cas-unsupported',\n severity: 'warn',\n message: 'Store lacks atomic CAS — unsafe for multi-writer sync-peer role',\n })\n }\n if (!network.pingSupported) {\n risks.push({\n code: 'no-ping',\n severity: 'warn',\n message: 'Store has no ping() — runtime monitor will rely on list() as liveness check',\n })\n }\n\n return risks\n}\n\nfunction score(risks: readonly ProbeRisk[]): SuitabilityScore {\n const hasError = risks.some((r) => r.severity === 'error')\n const casUnsupported = risks.some((r) => r.code === 'cas-unsupported')\n const slowWrite = risks.some((r) => r.code === 'slow-write-p99')\n\n const recommended: ProbeRole[] = []\n if (!hasError) {\n if (!slowWrite) recommended.push('primary')\n if (!casUnsupported) recommended.push('sync-peer')\n recommended.push('backup', 'archive')\n }\n return { recommended, risks }\n}\n\n// ── helpers ───────────────────────────────────────────────────────────────\n\n/** Build a synthetic envelope with a tiny ciphertext payload. Safe —\n * the store never decrypts, so the `_data` just needs to parse through\n * whatever JSON round-tripping the store does. */\nfunction envelope(version: number, seed = 0): EncryptedEnvelope {\n const data = `probe-${version}-${seed}`.padEnd(64, 'x')\n // base64-encode a deterministic marker so stores that assert\n // base64-shape on persist don't explode\n const b64 = base64Encode(data)\n return {\n _noydb: 1,\n _v: version,\n _ts: new Date().toISOString(),\n _iv: base64Encode('0'.repeat(12)),\n _data: b64,\n }\n}\n\nfunction base64Encode(s: string): string {\n if (typeof Buffer !== 'undefined') return Buffer.from(s, 'utf-8').toString('base64')\n return btoa(unescape(encodeURIComponent(s)))\n}\n\nfunction approxEnvelopeBytes(): number {\n return JSON.stringify(envelope(1)).length\n}\n\nfunction estimateBytes(snapshot: VaultSnapshot): number {\n let total = 0\n for (const coll of Object.values(snapshot)) {\n for (const rec of Object.values(coll)) {\n total += JSON.stringify(rec).length\n }\n }\n return total\n}\n\nfunction stats(samples: number[]): LatencyStats {\n if (samples.length === 0) return { count: 0, p50: 0, p99: 0, max: 0 }\n const sorted = [...samples].sort((a, b) => a - b)\n return {\n count: sorted.length,\n p50: percentile(sorted, 0.5),\n p99: percentile(sorted, 0.99),\n max: sorted[sorted.length - 1]!,\n }\n}\n\nfunction percentile(sorted: number[], q: number): number {\n const idx = Math.min(sorted.length - 1, Math.floor(q * sorted.length))\n return sorted[idx]!\n}\n\nasync function bestEffortCleanup(\n store: NoydbStore,\n vault: string,\n collection: string,\n): Promise<void> {\n try {\n const ids = await store.list(vault, collection)\n await Promise.all(ids.map((id) => store.delete(vault, collection, id).catch(() => {})))\n } catch {\n // Silent — cleanup failure is not a probe failure\n }\n}\n","/**\n * `probeTopology()` — multi-backend health + suitability check.\n *\n * Runs {@link runStoreProbe} independently on the primary store and\n * every sync target, then layers topology-level rules that only make\n * sense across the whole graph:\n *\n * | Rule | Condition | Severity |\n * |------|-----------|----------|\n * | `bundle-as-sync-peer` | Bundle-shaped store used as `sync-peer` | warn |\n * | `no-atomic-cas-sync-peer` | Non-atomic-CAS store used as `sync-peer` with >1 user | error |\n * | `primary-slower-than-peer` | Primary p99 > sync-peer p99 × 2 | warn |\n * | `archive-pull-configured` | `archive` target declared with a pull policy | error |\n *\n * Only one probe pass per store — if two targets happen to point at\n * the same backend, both get probed (the target identifies the\n * configuration, not the backend instance).\n *\n * @module\n */\nimport type { NoydbStore } from '@noy-db/hub'\nimport { runStoreProbe } from './probe.js'\nimport type {\n StoreProbeReport,\n TopologyProbeOptions,\n TopologyProbeReport,\n TopologyRisk,\n TopologyTargetReport,\n} from './probe-types.js'\n\nexport async function probeTopology(\n options: TopologyProbeOptions,\n): Promise<TopologyProbeReport> {\n const started = Date.now()\n const expectedUsers = options.expectedUsers ?? 1\n\n const primary = await runStoreProbe(options.store, options)\n const targets: TopologyTargetReport[] = []\n\n for (const t of options.sync ?? []) {\n const label = t.label ?? t.store.name ?? t.role\n const report = await runStoreProbe(t.store, { ...options, vault: `_probe-${label}` })\n targets.push({ ...report, role: t.role, label })\n }\n\n const topology = evaluateTopology(options.store, primary, targets, options.sync, expectedUsers)\n const allErrors = [\n ...primary.suitability.risks,\n ...targets.flatMap((t) => t.suitability.risks),\n ...topology,\n ].filter((r) => r.severity === 'error')\n\n return {\n primary, targets, topology,\n recommended: allErrors.length === 0,\n durationMs: Date.now() - started,\n probedAt: new Date().toISOString(),\n }\n}\n\nfunction evaluateTopology(\n _primaryStore: NoydbStore,\n primary: StoreProbeReport,\n targets: readonly TopologyTargetReport[],\n syncTargets: TopologyProbeOptions['sync'] = [],\n expectedUsers: number,\n): TopologyRisk[] {\n const risks: TopologyRisk[] = []\n\n targets.forEach((target, i) => {\n const input = syncTargets[i]\n const label = target.label\n\n // Bundle-shaped stores (drive/webdav/git) don't have atomic CAS\n // and surface as sync-peer-unsuitable. For we detect by\n // name heuristics; future hub work can annotate StoreCapabilities\n // with a `shape: 'kv' | 'bundle'` field.\n if (target.role === 'sync-peer' && looksLikeBundleStore(target.store)) {\n risks.push({\n target: label,\n code: 'bundle-as-sync-peer',\n severity: 'warn',\n message: `\"${label}\" looks bundle-shaped — use role 'backup' or 'archive' for push-only semantics`,\n })\n }\n\n if (\n target.role === 'sync-peer' &&\n expectedUsers > 1 &&\n target.capabilities?.casAtomic === false\n ) {\n risks.push({\n target: label,\n code: 'no-atomic-cas-sync-peer',\n severity: 'error',\n message: `\"${label}\" has casAtomic:false — unsafe as sync-peer for ${expectedUsers} concurrent users`,\n })\n }\n\n if (target.role === 'sync-peer' && primary.write.serial.p99 > target.write.serial.p99 * 2) {\n risks.push({\n target: label,\n code: 'primary-slower-than-peer',\n severity: 'warn',\n message: `Primary p99 ${primary.write.serial.p99}ms is >2× peer \"${label}\" p99 ${target.write.serial.p99}ms — unusual topology`,\n })\n }\n\n if (target.role === 'archive' && input?.hasPullPolicy === true) {\n risks.push({\n target: label,\n code: 'archive-pull-configured',\n severity: 'error',\n message: `\"${label}\" is an archive target but has a pull policy — archives are push-only`,\n })\n }\n })\n\n return risks\n}\n\n/** Heuristic bundle detection: name includes 'drive' / 'webdav' / 'git'\n * / 'bundle'. Adopters who wrap a bundle store under a custom name\n * can silence this via `acknowledgeRisks: ['bundle-as-sync-peer']`. */\nfunction looksLikeBundleStore(name: string): boolean {\n const n = name.toLowerCase()\n return /drive|webdav|git|bundle/.test(n)\n}\n"],"mappings":"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","names":["out"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noy-db/to-meter",
|
|
3
|
-
"version": "0.4.0-pre.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.0-pre.8",
|
|
4
|
+
"description": "Store observability for noy-db — a metered pass-through store that records per-method latency percentiles, error rates and liveness on real traffic, plus one-shot synthetic probes (runStoreProbe / probeTopology) for suitability reports.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vLannaAi <vicio@lanna.ai>",
|
|
7
7
|
"homepage": "https://github.com/vLannaAi/noy-db/tree/main/packages/to-meter#readme",
|
|
@@ -32,11 +32,12 @@
|
|
|
32
32
|
"node": ">=22.0.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@noy-db/hub": "0.4.0-pre.
|
|
35
|
+
"@noy-db/hub": "0.4.0-pre.8"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^22.0.0",
|
|
39
|
-
"@noy-db/hub": "0.4.0-pre.
|
|
39
|
+
"@noy-db/hub": "0.4.0-pre.8",
|
|
40
|
+
"@noy-db/to-memory": "0.4.0-pre.8"
|
|
40
41
|
},
|
|
41
42
|
"keywords": [
|
|
42
43
|
"noy-db",
|