@noy-db/to-meter 0.6.0-pre.8 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // src/index.ts
2
- import { ConflictError, wrapStore, withMetrics, memoryStore } from "@noy-db/hub";
2
+ import { ConflictError, wrapStore, withMetrics, memoryStore, NOYDB_FORMAT_VERSION as NOYDB_FORMAT_VERSION2 } from "@noy-db/hub";
3
3
 
4
4
  // src/probe.ts
5
+ import { NOYDB_FORMAT_VERSION } from "@noy-db/hub";
5
6
  var PROBE_VAULT = "probe-vault";
6
7
  var PROBE_COLLECTION = "probe-benchmark";
7
8
  async function runStoreProbe(store, options = {}) {
@@ -183,7 +184,7 @@ function envelope(version, seed = 0) {
183
184
  const data = `probe-${version}-${seed}`.padEnd(64, "x");
184
185
  const b64 = base64Encode(data);
185
186
  return {
186
- _noydb: 1,
187
+ _noydb: NOYDB_FORMAT_VERSION,
187
188
  _v: version,
188
189
  _ts: (/* @__PURE__ */ new Date()).toISOString(),
189
190
  _iv: base64Encode("0".repeat(12)),
@@ -515,7 +516,7 @@ function startLiveness(inner, opts, transition) {
515
516
  if (!ok) return transition("unreachable", void 0, void 0, "ping returned false");
516
517
  } else {
517
518
  await inner.put(vault, collection, pingId, {
518
- _noydb: 1,
519
+ _noydb: NOYDB_FORMAT_VERSION2,
519
520
  _v: 1,
520
521
  _ts: (/* @__PURE__ */ new Date()).toISOString(),
521
522
  _iv: "AAAAAAAAAAAAAAAA",
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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 // #889 — `listVaults` is a full enumeration on a remote store, and `ping`\n // isolates round-trip time from work, so both are worth timing.\n | 'listVaults' | 'ping'\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', 'listVaults', 'ping',\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: [], listVaults: [], ping: [],\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, listVaults: 0, ping: 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, listVaults: 0, ping: 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 if (typeof target.listVaults === 'function') {\n out.listVaults = () => time('listVaults', () => target.listVaults!())\n }\n if (typeof target.ping === 'function') {\n // NOTE: the synthetic `liveness` poller calls the INNER store directly\n // (see startLiveness), so these counters stay \"what the app did\" rather\n // than being inflated by our own health checks.\n out.ping = () => time('ping', () => target.ping!())\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"]}
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, NOYDB_FORMAT_VERSION } 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 // #889 — `listVaults` is a full enumeration on a remote store, and `ping`\n // isolates round-trip time from work, so both are worth timing.\n | 'listVaults' | 'ping'\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', 'listVaults', 'ping',\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: [], listVaults: [], ping: [],\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, listVaults: 0, ping: 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, listVaults: 0, ping: 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 if (typeof target.listVaults === 'function') {\n out.listVaults = () => time('listVaults', () => target.listVaults!())\n }\n if (typeof target.ping === 'function') {\n // NOTE: the synthetic `liveness` poller calls the INNER store directly\n // (see startLiveness), so these counters stay \"what the app did\" rather\n // than being inflated by our own health checks.\n out.ping = () => time('ping', () => target.ping!())\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: NOYDB_FORMAT_VERSION, _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 { NOYDB_FORMAT_VERSION } 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: NOYDB_FORMAT_VERSION,\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":["NOYDB_FORMAT_VERSION","out","NOYDB_FORMAT_VERSION"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noy-db/to-meter",
3
- "version": "0.6.0-pre.8",
3
+ "version": "0.6.0",
4
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>",
@@ -32,12 +32,12 @@
32
32
  "node": ">=22.0.0"
33
33
  },
34
34
  "peerDependencies": {
35
- "@noy-db/hub": "0.6.0-pre.8"
35
+ "@noy-db/hub": "0.6.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^22.0.0",
39
- "@noy-db/hub": "0.6.0-pre.8",
40
- "@noy-db/to-memory": "0.6.0-pre.8"
39
+ "@noy-db/hub": "0.6.0",
40
+ "@noy-db/to-memory": "0.6.0"
41
41
  },
42
42
  "keywords": [
43
43
  "noy-db",