Testing
This commit is contained in:
+6
-1
@@ -22,7 +22,12 @@ PEARDATA_DEFAULT_ROLE=viewer
|
||||
# PEARDATA_TIER1_POINTS=1440
|
||||
# PEARDATA_TIER1_EVERY=60
|
||||
|
||||
# ── Netdata-style REST API ───────────────────────────────────
|
||||
# ── HyperDB (warm history + metadata + linked sync) ──────────
|
||||
# PEARDATA_HYPERDB=1
|
||||
# PEARDATA_SWARM=0
|
||||
# See docs/STORAGE-HYPERDB.md
|
||||
|
||||
# ── agent-style REST API ───────────────────────────────────
|
||||
# PEARDATA_REST=1
|
||||
# PEARDATA_REST_HOST=127.0.0.1
|
||||
# PEARDATA_REST_PORT=19999
|
||||
|
||||
@@ -9,6 +9,8 @@ coverage/
|
||||
.pear/
|
||||
storage/
|
||||
data/
|
||||
tmp-test-data/
|
||||
tmp-hyperdb-test/
|
||||
*.seed
|
||||
.cache/
|
||||
tmp/
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# PearData
|
||||
|
||||
**Decentralized, P2P, Netdata-class real-time monitoring for the Pear / Holepunch ecosystem.**
|
||||
**Decentralized, P2P, real-time monitoring for the Pear / Holepunch ecosystem.**
|
||||
|
||||
Each machine runs a lightweight **PearMonitor agent**. A Pear desktop client dials agents by public key and shows fleet + per-node live dashboards. Agents also expose a **Netdata-compatible REST API** (`/api/v1`, `/api/v2`, `/api/v3`) for scripts, Grafana, and Prometheus.
|
||||
Each machine runs a lightweight **PearMonitor agent**. A Pear desktop client dials agents by public key and shows fleet + per-node live dashboards. Agents also expose an **agent-compatible REST API** (`/api/v1`, `/api/v2`, `/api/v3`) for scripts, Grafana, and Prometheus.
|
||||
|
||||
Built from the [pear-app-template](https://github.com/snxraven) HyperDHT + protomux-rpc patterns used by PearDock-class apps (MIT template — not a copy of PearDock’s AGPL sources).
|
||||
|
||||
@@ -12,7 +12,7 @@ Built from the [pear-app-template](https://github.com/snxraven) HyperDHT + proto
|
||||
| RPC | **protomux-rpc** + compact-encoding JSON |
|
||||
| Metrics | 1s collector → tiered buffers → push + query |
|
||||
| AuthZ | Roles (`viewer` / `operator` / `admin`) + `pd1.` invites + admin seed |
|
||||
| HTTP | Netdata-style REST on `127.0.0.1:19999` |
|
||||
| HTTP | agent-style REST on `127.0.0.1:19999` |
|
||||
| Desktop | **Pear** (`pear-electron` + `pear-bridge` + `<pear-ctrl>`) |
|
||||
|
||||
---
|
||||
@@ -54,12 +54,15 @@ npm run healthcheck
|
||||
peardata/
|
||||
├── app.js / index.html / ui/ # Pear fleet dashboard
|
||||
├── shared/ # Protocol, metrics catalog, schema, crypto
|
||||
├── spec/ # Generated Hyperschema + HyperDB definitions
|
||||
├── scripts/build-db.js # Regenerate spec/ (`npm run build:db`)
|
||||
├── server/ # PearMonitor agent
|
||||
│ ├── server.js # DHT + pipeline + REST boot
|
||||
│ ├── server.js # DHT + HyperDB + pipeline + REST boot
|
||||
│ ├── pipeline.js # collector → store → anomaly → push
|
||||
│ ├── db/ # HyperDB model + Corestore + swarm replicate
|
||||
│ ├── handlers/monitor.js # Domain RPCs
|
||||
│ ├── services/ # collector, store, anomaly, jobs, …
|
||||
│ ├── rest/ # Netdata-compatible HTTP API
|
||||
│ ├── services/ # collector, store, warm-flush, anomaly, jobs
|
||||
│ ├── rest/ # agent-compatible HTTP API
|
||||
│ └── core/ rpc/ utils/ # Auth, ACL, sessions (template)
|
||||
├── client/ # Multi-peer connection manager
|
||||
├── docs/ # Full documentation set
|
||||
@@ -77,6 +80,7 @@ peardata/
|
||||
| `npm run start:server` | PearMonitor agent (P2P + REST) |
|
||||
| `npm test` | brittle unit + integration |
|
||||
| `npm run mint-invite -- [role]` | Offline `pd1.` invite |
|
||||
| `npm run build:db` | Regenerate HyperDB `spec/` |
|
||||
| `npm run healthcheck` | Liveness / remote ping |
|
||||
| `npm run soak` | Load exercise |
|
||||
|
||||
@@ -104,6 +108,7 @@ peardata/
|
||||
| [Data model](./docs/DATA-MODEL.md) | Metrics, anomalies, health |
|
||||
| [REST API](./docs/REST-API.md) | `/api/v1\|v2\|v3` |
|
||||
| [Tech choices](./docs/TECH-CHOICES.md) | Collector, charts, libraries |
|
||||
| [HyperDB storage](./docs/STORAGE-HYPERDB.md) | Warm history, peer links, swarm sync |
|
||||
| [Security](./docs/SECURITY.md) | Threat model & hardening |
|
||||
| [Configuration](./docs/CONFIGURATION.md) | Environment reference |
|
||||
|
||||
|
||||
+11
-2
@@ -1,5 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Server binary entry (symlink-friendly).
|
||||
* PearMonitor agent entry for Node and Bare.
|
||||
*
|
||||
* Under Bare: load bare-node-runtime globals (process, Buffer, fetch, …) first.
|
||||
* package.json `imports` routes Node builtin names → bare-* when packed for Bare/Pear.
|
||||
*/
|
||||
import '../server/server.js'
|
||||
const isBare = typeof globalThis.Bare !== 'undefined'
|
||||
|
||||
if (isBare) {
|
||||
await import('bare-node-runtime/global')
|
||||
}
|
||||
|
||||
await import('../server/server.js')
|
||||
|
||||
+14
-4
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* Persistent client DHT identity for stable peerId across reconnects.
|
||||
* Stored at ~/.config/peardata/identity.json (mode 0600).
|
||||
*
|
||||
* Uses fs/path/os/crypto via package.json import maps → bare-* under Bare/Pear.
|
||||
*/
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
@@ -11,11 +13,19 @@ import b4a from 'b4a'
|
||||
|
||||
const IDENTITY_VERSION = 1
|
||||
|
||||
function envGet(name) {
|
||||
try {
|
||||
return typeof process !== 'undefined' ? process.env?.[name] : undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function getIdentityPath() {
|
||||
const home =
|
||||
process.env.PEARDATA_HOME ||
|
||||
process.env.HOME ||
|
||||
process.env.USERPROFILE ||
|
||||
envGet('PEARDATA_HOME') ||
|
||||
envGet('HOME') ||
|
||||
envGet('USERPROFILE') ||
|
||||
(typeof os.homedir === 'function' ? os.homedir() : '') ||
|
||||
''
|
||||
return path.join(home, '.config', 'peardata', 'identity.json')
|
||||
@@ -37,7 +47,7 @@ export function loadOrCreateClientIdentity() {
|
||||
}
|
||||
|
||||
if (!seedHex) {
|
||||
seedHex = crypto.randomBytes(32).toString('hex')
|
||||
seedHex = b4a.toString(crypto.randomBytes(32), 'hex')
|
||||
try {
|
||||
const dir = path.dirname(filePath)
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
||||
|
||||
+4
-1
@@ -14,7 +14,10 @@ export class ConnectionManager extends EventEmitter {
|
||||
this.active = null
|
||||
/** @type {Map<string, ReturnType<typeof setTimeout>>} */
|
||||
this._reconnectTimers = new Map()
|
||||
this.maxReconnectTries = Number(process.env.PEARDATA_MAX_RECONNECT) || 20
|
||||
this.maxReconnectTries =
|
||||
Number(
|
||||
(typeof process !== 'undefined' && process.env?.PEARDATA_MAX_RECONNECT) || 20
|
||||
) || 20
|
||||
/** @type {Map<string, number>} */
|
||||
this._tries = new Map()
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ Wants=network-online.target
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/peardata
|
||||
EnvironmentFile=-/opt/peardata/.env
|
||||
ExecStart=/usr/bin/node /opt/peardata/server/server.js
|
||||
ExecStart=/usr/bin/node /opt/peardata/bin/peardata-server.mjs
|
||||
ExecStartPost=/usr/bin/node /opt/peardata/scripts/healthcheck.js
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
|
||||
+13
-7
@@ -1,15 +1,16 @@
|
||||
# Architecture
|
||||
|
||||
PearData is a **decentralized, P2P clone of the Netdata real-time monitoring experience**, built on the same HyperDHT + protomux-rpc patterns as PearDock-class apps (via the pear-app template).
|
||||
PearData is a **decentralized, P2P real-time host monitoring stack**, built on the same HyperDHT + protomux-rpc patterns as PearDock-class apps (via the pear-app template).
|
||||
|
||||
## Design goals
|
||||
|
||||
1. **No central control plane** — dial an agent by Ed25519 public key.
|
||||
2. **Instant live truth** — ~1s metric push to connected desktops.
|
||||
3. **Dual API** — P2P RPC for the Pear client; Netdata-style REST for scripts/Grafana.
|
||||
3. **Dual API** — P2P RPC for the Pear client; agent-style REST for scripts/Grafana.
|
||||
4. **Clear AuthZ** — viewer (pubkey) vs operator/admin (`pd1.` invite / seed proof).
|
||||
5. **Low agent overhead** — Node/`os` + `/proc` collectors, ring buffers, hot-path RPCs.
|
||||
6. **Ecosystem-ready** — shared wire contract in `shared/` for PearDock / PearVirt adapters later.
|
||||
5. **Low agent overhead** — `os`/`bare-os` + `/proc` collectors, ring buffers, hot-path RPCs.
|
||||
6. **Bare/Pear-ready** — `package.json` import maps route builtins → `bare-*`; no Node-only core deps in the Pear path.
|
||||
7. **Ecosystem-ready** — shared wire contract in `shared/` for PearDock / PearVirt adapters later.
|
||||
|
||||
## Mapping to PearDock / template components
|
||||
|
||||
@@ -23,7 +24,7 @@ PearData is a **decentralized, P2P clone of the Netdata real-time monitoring exp
|
||||
| Connection manager / multi-peer | `client/manager.js` |
|
||||
| Job tray | `server/services/jobs.js` + desktop actions |
|
||||
| Domain service | **Metrics pipeline** (`collector` → `store` → `anomaly` → pushes) |
|
||||
| Optional HTTP surface | `server/rest/*` (Netdata v1/v2/v3) |
|
||||
| Optional HTTP surface | `server/rest/*` (REST v1/v2/v3) |
|
||||
|
||||
## System context
|
||||
|
||||
@@ -67,7 +68,7 @@ Threshold engine evaluates each batch; transitions emit `push:anomaly` / `push:a
|
||||
|
||||
### 4. Local REST (optional)
|
||||
|
||||
Netdata-compatible HTTP on `127.0.0.1:19999` by default — for local tooling without P2P. Disable with `PEARDATA_REST=0`.
|
||||
agent-compatible HTTP on `127.0.0.1:19999` by default — for local tooling without P2P. Disable with `PEARDATA_REST=0`.
|
||||
|
||||
## Layered stack
|
||||
|
||||
@@ -152,8 +153,13 @@ A heavier agent may subscribe to child agents over P2P, downsample into its own
|
||||
| `server/services/subscriptions.js` | Push fan-out |
|
||||
| `server/services/jobs.js` | On-demand jobs |
|
||||
| `server/handlers/monitor.js` | RPC surface |
|
||||
| `server/rest/*` | Netdata HTTP API |
|
||||
| `server/rest/*` | Agent HTTP API |
|
||||
| `server/pipeline.js` | Wire collector→store→push |
|
||||
| `server/db/*` | HyperDB model, Corestore, swarm replicate |
|
||||
| `spec/` | Generated Hyperschema + HyperDB defs |
|
||||
| `scripts/build-db.js` | Schema codegen |
|
||||
|
||||
HyperDB design: [STORAGE-HYPERDB.md](./STORAGE-HYPERDB.md).
|
||||
|
||||
## Related docs
|
||||
|
||||
|
||||
+21
-3
@@ -57,17 +57,34 @@ Permissions: directory `0700`. Do **not** commit `data/` or `.env`.
|
||||
| `PEARDATA_SAMPLE_MS` | `1000` | Collector interval |
|
||||
| `PEARDATA_TIER0_POINTS` | `3600` | High-res ring size (~1h @ 1s) |
|
||||
| `PEARDATA_TIER1_POINTS` | `1440` | Downsampled ring size |
|
||||
| `PEARDATA_TIER1_EVERY` | `60` | Samples per tier1 average |
|
||||
| `PEARDATA_TIER1_EVERY` | `60` | Samples per tier1 average (also HyperDB warm flush) |
|
||||
|
||||
---
|
||||
|
||||
## REST API (Netdata-style)
|
||||
## HyperDB storage & sync
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `PEARDATA_HYPERDB` | on | `0` / `off` disables HyperDB |
|
||||
| `PEARDATA_SWARM` | off | `1` enables Hyperswarm Corestore replication |
|
||||
|
||||
Storage: `$PEARDATA_DATA_DIR/corestore` (named core `peardata-meta`).
|
||||
|
||||
Full guide: [STORAGE-HYPERDB.md](./STORAGE-HYPERDB.md).
|
||||
|
||||
```bash
|
||||
npm run build:db # regenerate spec/ after schema edits
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## REST API (agent-style)
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `PEARDATA_REST` | on | Set `0` / `off` to disable HTTP API |
|
||||
| `PEARDATA_REST_HOST` | `127.0.0.1` | Bind address (`0.0.0.0` exposes LAN — firewall!) |
|
||||
| `PEARDATA_REST_PORT` | `19999` | HTTP port (Netdata classic) |
|
||||
| `PEARDATA_REST_PORT` | `19999` | HTTP port (classic local agent port) |
|
||||
| `PEARDATA_REST_CORS` | `*` | `Access-Control-Allow-Origin` |
|
||||
|
||||
See [REST-API.md](./REST-API.md).
|
||||
@@ -104,6 +121,7 @@ See [REST-API.md](./REST-API.md).
|
||||
| `npm run start:server:bin` | `bin/peardata-server.mjs` |
|
||||
| `npm test` | brittle suite |
|
||||
| `npm run mint-invite -- [role] [ttlMs]` | Offline `pd1.` invite |
|
||||
| `npm run build:db` | Regenerate HyperDB `spec/` |
|
||||
| `npm run healthcheck` | Liveness |
|
||||
| `npm run soak` | Load exercise |
|
||||
|
||||
|
||||
+23
-12
@@ -26,20 +26,31 @@ Implemented in `shared/data-model.js`, `shared/metrics.js`, and agent services.
|
||||
|
||||
## Metric contexts & charts
|
||||
|
||||
Netdata-inspired IDs:
|
||||
Canonical catalog lives in `shared/metrics.js` (`STATIC_CHART_DEFS` + runtime instance charts). Collector fills these from `/proc` + `/sys` on Linux (`os` / bare-os fallback elsewhere).
|
||||
|
||||
| Context | Chart id | Units | Dimensions (MVP) |
|
||||
|---------|----------|-------|------------------|
|
||||
| `system.cpu` | `system.cpu` | percentage | user, system, nice, iowait, irq, softirq, idle |
|
||||
| `system.ram` | `system.ram` | MiB | used, cached, buffers, free |
|
||||
| `mem.available` | `mem.available` | MiB | avail |
|
||||
| `system.load` | `system.load` | load | load1, load5, load15 |
|
||||
| `system.io` | `system.io` | KiB/s | reads, writes |
|
||||
| `system.net` | `system.net` | kilobits/s | received, sent |
|
||||
| `system.processes` | `system.processes` | processes | running, blocked, total |
|
||||
| `system.uptime` | `system.uptime` | seconds | uptime |
|
||||
### Host-wide (static)
|
||||
|
||||
Chart summary objects mirror Netdata’s `/api/v1/charts` fields (`id`, `context`, `units`, `dimensions`, `update_every`, `first_entry`, `last_entry`, …).
|
||||
| Family | Contexts / charts | Units (typical) |
|
||||
|--------|-------------------|-----------------|
|
||||
| CPU / scheduler | `system.cpu`, `system.intr`, `system.ctxt`, `system.forks`, `system.processes`, `system.active_processes`, `system.load`, `system.uptime`, `system.entropy` | %, interrupts/s, load, seconds |
|
||||
| Memory | `system.ram`, `mem.available`, `mem.swap`, `mem.swap_cached`, `mem.kernel`, `mem.slab`, `mem.writeback`, `mem.committed`, `mem.swapio`, `system.pgpgio`, `system.pgfaults` | MiB, KiB/s, faults/s |
|
||||
| Disk aggregate | `system.io` | KiB/s (`in` / `out`) |
|
||||
| Network aggregate | `system.net`, `system.ip`, `system.ipv6` | kilobits/s |
|
||||
| TCP / IPv4 | `ip.tcppackets`, `ip.tcperrors`, `ip.tcpopens`, `ip.tcpsock`, `ipv4.packets`, `ipv4.errors`, `ipv4.udppackets`, `ipv4.udperrors` | packets/s, connections |
|
||||
| PSI pressure | `system.cpu_some_pressure`, `system.memory_some_pressure`, `system.io_some_pressure` | % (avg10/60/300) |
|
||||
|
||||
`system.cpu` dimensions: `guest_nice`, `guest`, `steal`, `softirq`, `irq`, `user`, `system`, `nice`, `iowait`, `idle`.
|
||||
|
||||
### Instance charts (registered at runtime)
|
||||
|
||||
| Context | Chart id pattern | Notes |
|
||||
|---------|------------------|-------|
|
||||
| `cpu.cpu` | `cpu.cpu{N}` | Per-core utilization (same dims as `system.cpu`) |
|
||||
| `disk.io` / `disk.ops` / `disk.util` | `disk_io.{dev}`, `disk_ops.{dev}`, `disk_util.{dev}` | Per-disk from `/proc/diskstats` |
|
||||
| `disk.space` / `disk.inodes` | `disk_space.{mount}`, `disk_inodes.{mount}` | Per-mount via `statfs` |
|
||||
| `net.net` / `net.packets` / `net.errors` / `net.drops` | `net.{iface}`, … | Per-iface from `/proc/net/dev` (loopback skipped) |
|
||||
|
||||
Chart summary objects use fields: `id`, `context`, `units`, `dimensions`, `update_every`, `first_entry`, `last_entry`, …
|
||||
|
||||
## MetricSample (live push)
|
||||
|
||||
|
||||
+3
-3
@@ -2,9 +2,9 @@
|
||||
|
||||
## Add a chart / context
|
||||
|
||||
1. Define the chart in `shared/metrics.js` (`CHART_DEFS`).
|
||||
1. Define the chart in `shared/metrics.js` (`STATIC_CHART_DEFS`, or `registerChart()` for instances).
|
||||
2. Emit samples from `server/services/collector.js` (or a new collector module).
|
||||
3. Store + REST/RPC pick it up automatically via `CHART_BY_ID`.
|
||||
3. Store + REST/RPC pick it up automatically via `getAllChartDefs()` / `CHART_BY_ID`.
|
||||
4. Document dimensions in [DATA-MODEL.md](./DATA-MODEL.md).
|
||||
5. Optionally add a canvas panel in `index.html` + `app.js`.
|
||||
|
||||
@@ -40,7 +40,7 @@ Update [PROTOCOL.md](./PROTOCOL.md) and add a brittle test.
|
||||
|
||||
## Add a REST route
|
||||
|
||||
Edit `server/rest/routes.js` — keep Netdata path naming when emulating Agent APIs (`/api/v3/...`).
|
||||
Edit `server/rest/routes.js` — keep stable `/api/v3/...` path naming.
|
||||
|
||||
## Add a job
|
||||
|
||||
|
||||
@@ -44,6 +44,22 @@ curl -s 'http://127.0.0.1:19999/api/v3/allmetrics?format=prometheus' | head
|
||||
|
||||
Disable REST: `PEARDATA_REST=0`.
|
||||
|
||||
### HyperDB (warm storage)
|
||||
|
||||
On by default. Agent banner shows `hyperdb:` public key.
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:19999/api/v3/db | jq
|
||||
```
|
||||
|
||||
Warm history is written about once per minute (tier1). Linked-node Hyperswarm sync:
|
||||
|
||||
```bash
|
||||
PEARDATA_SWARM=1 npm run start:server
|
||||
```
|
||||
|
||||
See [STORAGE-HYPERDB.md](./STORAGE-HYPERDB.md).
|
||||
|
||||
## Mint an invite
|
||||
|
||||
```bash
|
||||
|
||||
+13
-2
@@ -48,12 +48,12 @@ Auth modes at handshake: public key (viewer), capability token / `pd1.` invite,
|
||||
|--------|------|------|--------|
|
||||
| `listContexts` | viewer | — | Context catalog |
|
||||
| `getContext` | viewer | `{ id }` | Charts in context |
|
||||
| `listCharts` | viewer | — | Netdata-ish chart map |
|
||||
| `listCharts` | viewer | — | agent-style chart map |
|
||||
| `getChart` | viewer | `{ id }` | Chart summary |
|
||||
| `queryData` | viewer | `{ chart, after, before, points, group, tier }` | Time series |
|
||||
| `getAllMetrics` | viewer | `{ format: json\|prometheus\|shell }` | Latest export |
|
||||
|
||||
`after` / `before`: absolute unix seconds, or relative (negative = relative to `before`/`now`), Netdata-style.
|
||||
`after` / `before`: absolute unix seconds, or relative (negative = relative to `before`/`now`), agent-style.
|
||||
|
||||
### Live subscriptions
|
||||
|
||||
@@ -82,6 +82,17 @@ Auth modes at handshake: public key (viewer), capability token / `pd1.` invite,
|
||||
| `runJob` | operator | `collectOnce`, `snapshot`, `gcBuffers` |
|
||||
| `cancelJob` | operator | By job id |
|
||||
|
||||
### HyperDB / peer links
|
||||
|
||||
| Method | Role | Notes |
|
||||
|--------|------|-------|
|
||||
| `getDbInfo` | viewer | DB + discovery keys, swarm flag |
|
||||
| `listPeerLinks` | viewer | Linked peers from HyperDB |
|
||||
| `linkPeer` | admin | Upsert link; optional swarm join |
|
||||
| `unlinkPeer` | admin | Remove link |
|
||||
|
||||
See [STORAGE-HYPERDB.md](./STORAGE-HYPERDB.md).
|
||||
|
||||
### Admin
|
||||
|
||||
| Method | Role |
|
||||
|
||||
+2
-2
@@ -3,11 +3,11 @@
|
||||
| Doc | Audience | Contents |
|
||||
|-----|----------|----------|
|
||||
| [GETTING-STARTED.md](./GETTING-STARTED.md) | Operators | Install, run agent, REST, desktop, systemd |
|
||||
| [ROADMAP.md](./ROADMAP.md) | Everyone | Phased MVP → advanced Netdata-class features |
|
||||
| [ROADMAP.md](./ROADMAP.md) | Everyone | Phased MVP → advanced real-time features |
|
||||
| [ARCHITECTURE.md](./ARCHITECTURE.md) | Engineers | Planes, PearDock mapping, module map |
|
||||
| [PROTOCOL.md](./PROTOCOL.md) | Protocol owners | RPC methods, pushes, versioning |
|
||||
| [DATA-MODEL.md](./DATA-MODEL.md) | Engineers | Metrics, anomalies, alerts, jobs |
|
||||
| [REST-API.md](./REST-API.md) | Integrators | Netdata-style `/api/v1\|v2\|v3` |
|
||||
| [REST-API.md](./REST-API.md) | Integrators | agent-style `/api/v1\|v2\|v3` |
|
||||
| [TECH-CHOICES.md](./TECH-CHOICES.md) | Engineers | Collector, charts, libraries |
|
||||
| [STORAGE-HYPERDB.md](./STORAGE-HYPERDB.md) | Engineers | HyperDB / Corestore / Autobase sync design |
|
||||
| [DESKTOP.md](./DESKTOP.md) | UI developers | Pear shell, titlebar, identity |
|
||||
|
||||
+16
-15
@@ -1,12 +1,12 @@
|
||||
# REST API (Netdata-compatible)
|
||||
# REST API (agent-compatible)
|
||||
|
||||
PearMonitor agents expose an optional HTTP API modeled on **Netdata Agent** endpoints (`/api/v1`, `/api/v2`, `/api/v3`).
|
||||
PearMonitor agents expose an optional HTTP API for local tooling (`/api/v1`, `/api/v2`, `/api/v3`).
|
||||
|
||||
Default bind: `http://127.0.0.1:19999` (Netdata’s classic port).
|
||||
Default bind: `http://127.0.0.1:19999`.
|
||||
Disable: `PEARDATA_REST=0`.
|
||||
Bind all interfaces (careful): `PEARDATA_REST_HOST=0.0.0.0`.
|
||||
|
||||
This is a **compatibility layer**, not a byte-for-byte Netdata clone. Core query/metadata paths are implemented for scripts, Grafana, and Prometheus scrapers.
|
||||
Core query/metadata paths are implemented for scripts, Grafana, and Prometheus scrapers.
|
||||
|
||||
## Quick examples
|
||||
|
||||
@@ -36,10 +36,11 @@ curl -s http://127.0.0.1:19999/api/v3/health | jq
|
||||
|
||||
| Method | Path | Notes |
|
||||
|--------|------|-------|
|
||||
| GET | `/` or `/api` | Service index + P2P pubkey |
|
||||
| GET | `/` or `/api` | Service index + P2P pubkey + HyperDB keys |
|
||||
| GET | `/api/v1/info` | Agent info |
|
||||
| GET | `/api/v2/info` | same |
|
||||
| GET | `/api/v3/info` | **preferred** |
|
||||
| GET | `/api/v3/info` | **preferred** (includes `peardata.hyperdb`) |
|
||||
| GET | `/api/v3/db` | HyperDB public/discovery keys + collections |
|
||||
| GET | `/api/v3/versions` | Agent / protocol / API versions |
|
||||
| GET | `/api/v3/me` | Anonymous REST identity note |
|
||||
| GET | `/api/v3/settings` | Runtime knobs |
|
||||
@@ -75,7 +76,7 @@ Single-agent MVP returns one node (this host). Parent/fleet aggregation is roadm
|
||||
| GET | `/api/v2/data` | same |
|
||||
| GET | `/api/v1/data` | same (legacy) |
|
||||
|
||||
**Params (Netdata-style)**
|
||||
**Params (agent-style)**
|
||||
|
||||
| Param | Default | Description |
|
||||
|-------|---------|-------------|
|
||||
@@ -121,7 +122,7 @@ Single-agent MVP returns one node (this host). Parent/fleet aggregation is roadm
|
||||
|
||||
## Auth model (REST)
|
||||
|
||||
- **Default:** localhost-only, no bearer required (like a typical Netdata agent bind).
|
||||
- **Default:** localhost-only, no bearer required (like a typical local agent bind).
|
||||
- **P2P remains the secure remote path** (Noise + roles).
|
||||
- If you bind `0.0.0.0`, put REST behind a firewall, reverse proxy, or Holesail tunnel — do not expose raw metrics to the internet.
|
||||
- Future: optional bearer gate (`/api/v3/bearer_protection` parity).
|
||||
@@ -130,15 +131,15 @@ Single-agent MVP returns one node (this host). Parent/fleet aggregation is roadm
|
||||
|
||||
`Access-Control-Allow-Origin` defaults to `*` (override with `PEARDATA_REST_CORS`).
|
||||
|
||||
## Compatibility notes
|
||||
## Scope notes
|
||||
|
||||
| Netdata | PearData MVP |
|
||||
|---------|--------------|
|
||||
| Full ML weights / metric correlations | Simplified health weights |
|
||||
| Area | PearData |
|
||||
|------|----------|
|
||||
| ML weights / metric correlations | Simplified health weights |
|
||||
| Multi-node parent streaming | Single node; parent planned |
|
||||
| Cloud POST `/api/v3/spaces/.../data` | Not implemented (agent GET style only) |
|
||||
| Every chart Netdata ships | Core system charts (expanding) |
|
||||
| Functions execute via HTTP | Listed; run via P2P jobs |
|
||||
| Cloud POST spaces APIs | Not implemented (agent GET style only) |
|
||||
| App/plugin charts (nginx, DB, …) | System/OS charts; plugins later |
|
||||
| Functions via HTTP | Listed; run via P2P jobs |
|
||||
|
||||
## Implementation
|
||||
|
||||
|
||||
+7
-8
@@ -1,13 +1,13 @@
|
||||
# PearData Roadmap
|
||||
|
||||
Phased plan from MVP agent → Netdata-class fleet observability on pure P2P.
|
||||
Phased plan from MVP agent → real-time fleet observability on pure P2P.
|
||||
|
||||
## Guiding principles
|
||||
|
||||
1. **Instant value** — connect a pubkey, see live charts in seconds.
|
||||
2. **Agent efficiency** — stay in Netdata’s ballpark for CPU/RAM overhead.
|
||||
2. **Agent efficiency** — stay in a tight ballpark for CPU/RAM overhead.
|
||||
3. **PearDock patterns** — HyperDHT identity, protomux-rpc, roles, `pd1.` invites.
|
||||
4. **Dual access** — P2P desktop + Netdata-style REST (`/api/v1|v2|v3`).
|
||||
4. **Dual access** — P2P desktop + agent-style REST (`/api/v1|v2|v3`).
|
||||
5. **Ecosystem glue** — ready for PearDock / PearVirt / HoneyPeer / BareOS later.
|
||||
|
||||
---
|
||||
@@ -18,11 +18,11 @@ Phased plan from MVP agent → Netdata-class fleet observability on pure P2P.
|
||||
|------|--------|
|
||||
| Copy pear-app-template → PearData rebrand (`pd1.`, `PEARDATA_*`, `peardata/rpc`) | Done |
|
||||
| Shared protocol + schema + metrics catalog | Done |
|
||||
| Agent collector (CPU/RAM/load/net/disk/processes) @ ~1s | Done |
|
||||
| Agent collector (full system/OS charts + per-cpu/disk/iface/mount) @ ~1s | Done |
|
||||
| In-memory tiered store (1s + downsample) | Done |
|
||||
| Threshold anomaly engine + health | Done |
|
||||
| P2P RPC surface (query, subscribe, alerts, jobs, ACL) | Done |
|
||||
| Netdata-compatible REST v1/v2/v3 (local :19999) | Done |
|
||||
| agent-compatible REST v1/v2/v3 (local :19999) | Done |
|
||||
| Pear desktop fleet overview + live canvas charts | Done |
|
||||
| Docs: architecture, protocol, data model, REST, roadmap, tech choices | Done |
|
||||
| systemd unit + CI skeletons | Done |
|
||||
@@ -43,7 +43,6 @@ Phased plan from MVP agent → Netdata-class fleet observability on pure P2P.
|
||||
- Multi-peer compare mode (overlay 2–4 nodes on one chart)
|
||||
- Reconnection / offline banners with last-known samples
|
||||
- Agent process title / `peardata-agent` binary naming polish
|
||||
- Expand collectors: per-core CPU, per-iface net, mount disk space
|
||||
- Alert silence TTL auto-reenable
|
||||
- brittle tests for collector, store query, REST routes
|
||||
- One-line install script (`curl | bash`) for Linux agents
|
||||
@@ -96,8 +95,8 @@ See **[STORAGE-HYPERDB.md](./STORAGE-HYPERDB.md)** for the full design (from Hol
|
||||
|
||||
## Non-goals (for now)
|
||||
|
||||
- Replacing Netdata Cloud SaaS multi-tenant product
|
||||
- Full byte-identical Netdata internal DB format
|
||||
- Replacing centralized multi-tenant SaaS monitoring products
|
||||
- Third-party proprietary on-disk metric DB formats
|
||||
- Shipping a browser-only public dashboard without auth by default (REST stays localhost unless explicitly bound)
|
||||
|
||||
---
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@
|
||||
## REST API exposure
|
||||
|
||||
- Default bind is **localhost only** (`127.0.0.1:19999`).
|
||||
- REST is intentionally open on that bind (Netdata-agent style) — **do not** set `PEARDATA_REST_HOST=0.0.0.0` without a firewall, reverse proxy, or Holesail tunnel.
|
||||
- REST is intentionally open on that bind (local-agent style) — **do not** set `PEARDATA_REST_HOST=0.0.0.0` without a firewall, reverse proxy, or Holesail tunnel.
|
||||
- Prefer **P2P + roles** for remote multi-operator access; use REST for local scrapers/Grafana.
|
||||
|
||||
## Production checklist
|
||||
|
||||
+256
-238
@@ -1,290 +1,308 @@
|
||||
# HyperDB storage & linked-node sync
|
||||
|
||||
How PearData should adopt Holepunch’s **HyperDB + Corestore + Hyperswarm (+ Autobase)** stack for durable storage and P2P sync between linked agents — based on patterns in local clones under `holepunchto_repos` (`hyperdb`, `hyperdb-workshop`, `hyperdb-autobase-workshop`, `corestore`, `hyperswarm`, `autobee`, `pear-hyperdb`).
|
||||
PearData uses Holepunch **HyperDB + Corestore (+ optional Hyperswarm)** for durable metadata, warm metric history, and P2P sync between linked agents.
|
||||
|
||||
## Why HyperDB (not “just SQLite”)
|
||||
Live 1-second samples stay in the **memory ring** and are pushed over protomux-rpc. HyperDB is for everything that must survive restarts and replicate.
|
||||
|
||||
| Need | HyperDB fit |
|
||||
|------|-------------|
|
||||
| Typed collections + indexes | Hyperschema + `@ns/collection` + secondary indexes |
|
||||
| Local high-perf | `HyperDB.rocks(path, def)` |
|
||||
| P2P replicate | `HyperDB.bee(hypercore, def, { autoUpdate })` over Corestore |
|
||||
| Multi-writer HA parents | Autobase whose **view** is HyperDB (`extension: false`) |
|
||||
| Same query API local + remote | Workshops prove one `Registry` class works for both |
|
||||
---
|
||||
|
||||
PearData’s current `server/services/store.js` is an **in-memory ring**. HyperDB replaces durability + sync; the in-memory tier stays as the **hot 1s path**.
|
||||
## Status (implemented)
|
||||
|
||||
## Stack mapping (from Holepunch repos)
|
||||
| Piece | Status | Location |
|
||||
|-------|--------|----------|
|
||||
| Schema codegen | ✅ | `scripts/build-db.js` → `spec/` |
|
||||
| Collections + indexes | ✅ | `@peardata/*` in `spec/hyperdb` |
|
||||
| `PearDataModel` | ✅ | `server/db/model.js` |
|
||||
| Corestore open/close | ✅ | `server/db/index.js` |
|
||||
| Warm flush from tier1 | ✅ | `server/services/warm-flush.js` + `store` `warm` event |
|
||||
| Query fallback (memory → HyperDB) | ✅ | `MetricStore.query()` |
|
||||
| Agent boot upsert node | ✅ | `server/server.js` |
|
||||
| RPC: `getDbInfo`, `linkPeer`, `unlinkPeer`, `listPeerLinks` | ✅ | `server/handlers/monitor.js` |
|
||||
| REST: `GET /api/v3/db` | ✅ | `server/rest/routes.js` |
|
||||
| Hyperswarm replicate | ✅ stub | `server/db/replicate.js` (`PEARDATA_SWARM=1`) |
|
||||
| Autobase multi-writer parents | ⏳ Phase C | See roadmap |
|
||||
|
||||
Disable HyperDB: `PEARDATA_HYPERDB=0`.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Agent
|
||||
COL[collector 1s] --> HOT[Memory tier0 ring]
|
||||
COL --> DS[Downsample]
|
||||
DS --> HDB[(HyperDB bee/rocks)]
|
||||
POL[peer-policy / alerts / links] --> HDB
|
||||
HOT --> RPC[protomux-rpc + REST]
|
||||
HDB --> RPC
|
||||
end
|
||||
subgraph Sync
|
||||
CS[Corestore] --> HDB
|
||||
SW[Hyperswarm] -->|store.replicate| CS
|
||||
AB[Autobase optional] -->|view| HDB
|
||||
end
|
||||
CHILD[Linked child agent] -.->|discoveryKey| SW
|
||||
PARENT[Parent / peer agent] -.-> SW
|
||||
DESK[Pear desktop cache] -.-> SW
|
||||
COL[collector 1s] --> MEM[Memory tier0 ring]
|
||||
COL --> T1[Memory tier1 downsample]
|
||||
T1 -->|warm event| FLUSH[warm-flush queue]
|
||||
FLUSH --> HDB[(HyperDB.bee peardata-meta)]
|
||||
ANO[anomaly engine] -->|alert events| HDB
|
||||
LINK[linkPeer RPC] --> HDB
|
||||
MEM --> PUSH[push:metrics RPC]
|
||||
MEM --> Q[queryData / REST]
|
||||
HDB --> Q
|
||||
CS[Corestore data/corestore] --> HDB
|
||||
SW[Hyperswarm PEARDATA_SWARM=1] -->|store.replicate| CS
|
||||
```
|
||||
|
||||
| Component | Repo pattern | PearData use |
|
||||
|-----------|--------------|--------------|
|
||||
| **Hyperschema + hyperdb/builder** | `hyperdb-workshop/build.js` | `spec/` codegen for collections |
|
||||
| **HyperDB.bee** | workshop `Registry` | Replicable agent DB |
|
||||
| **HyperDB.rocks** | `hyperdb` README | Optional local-only fast index |
|
||||
| **Corestore** | workshop `bin.js` | Named cores: `metrics-meta`, `alerts`, … |
|
||||
| **Hyperswarm** | `swarm.join(discoveryKey)` + `store.replicate(conn)` | Link nodes / seed DB |
|
||||
| **protomux-rpc** | already in PearData | Control plane (unchanged) |
|
||||
| **Autobase + hyperdispatch** | `hyperdb-autobase-workshop` | Multi-writer **parent** / HA registry |
|
||||
| **autobee** | experimental multiwriter bee | Alternative later; prefer Autobase+HyperDB view for now |
|
||||
| **pear-hyperdb** | Pear-shaped Model wrapper | Optional UX for desktop-local rocks |
|
||||
|
||||
## Critical design rule: don’t put 1s samples in HyperDB txs
|
||||
|
||||
HyperDB is an **indexable document DB** (put/get/find + flush). Writing every chart every second as HyperDB transactions will:
|
||||
|
||||
- Amplify Rocks/Bee write cost
|
||||
- Create huge replication chatter
|
||||
- Fight Netdata-class overhead goals
|
||||
|
||||
**Split planes:**
|
||||
### Planes
|
||||
|
||||
| Plane | Storage | Sync |
|
||||
|-------|---------|------|
|
||||
| **Hot live (≤1h @ 1s)** | Memory ring (current) | P2P `push:metrics` (current) |
|
||||
| **Warm history (downsampled)** | Hypercore append **or** HyperDB rows keyed `(chart, tsBucket)` | Corestore replicate |
|
||||
| **Metadata** (peers, alerts, labels, jobs, ACL cache) | **HyperDB** | Corestore replicate |
|
||||
| **Fleet / parent consensus** | Autobase → HyperDB view | Swarm on autobase discoveryKey |
|
||||
| Hot live (~1h @ 1s) | Memory | `push:metrics` |
|
||||
| Warm history (~1m buckets) | HyperDB `@peardata/metric-point` | Corestore replicate |
|
||||
| Metadata / links / alerts | HyperDB collections | Corestore replicate |
|
||||
| Fleet HA (future) | Autobase → HyperDB view | Swarm on autobase key |
|
||||
|
||||
## Proposed HyperDB schema (`@peardata/*`)
|
||||
### Why not HyperDB for every 1s sample
|
||||
|
||||
Modeled after workshop `build.js` namespaces.
|
||||
HyperDB is transactional + indexed. Flushing every chart every second would inflate write amplification and replication traffic. Tier1 downsample (default every 60 samples ≈ 1 minute) is the durable path.
|
||||
|
||||
---
|
||||
|
||||
## Schema (`@peardata`)
|
||||
|
||||
Defined in `scripts/build-db.js`. **Append-only** — never delete fields from committed `spec/` (Holepunch safety rule).
|
||||
|
||||
### Collections
|
||||
|
||||
```text
|
||||
@peardata/node
|
||||
key: nodeId (string / pubkey hex)
|
||||
fields: hostname, platform, arch, cpus, agentVersion, labels{}, updatedAt
|
||||
|
||||
@peardata/peer-link
|
||||
key: [localNodeId, remotePublicKey]
|
||||
fields: role, alias, discoveryKey?, linkedAt, lastSeen, syncMode (push|pull|both)
|
||||
|
||||
@peardata/alert-config
|
||||
key: id
|
||||
fields: chart, dimension, warn, crit, comparator, enabled, info
|
||||
|
||||
@peardata/alert-event
|
||||
key: [id, ts] # or ulid
|
||||
fields: severity, value, threshold, message, cleared
|
||||
|
||||
@peardata/metric-point # WARM tier only (e.g. 1m buckets)
|
||||
key: [chart, ts]
|
||||
fields: context, values{} (map), tier
|
||||
|
||||
@peardata/job
|
||||
key: id
|
||||
fields: name, status, startedAt, finishedAt, result?
|
||||
```
|
||||
| Collection | Key | Purpose |
|
||||
|------------|-----|---------|
|
||||
| `@peardata/node` | `nodeId` | Agent / host inventory |
|
||||
| `@peardata/peer-link` | `localNodeId` + `remotePublicKey` | Linked peers + sync mode |
|
||||
| `@peardata/alert-config` | `id` | Threshold configs |
|
||||
| `@peardata/alert-event` | `id` + `ts` | Anomaly / alert history |
|
||||
| `@peardata/metric-point` | `chart` + `ts` | Warm downsampled samples (`valuesJson`) |
|
||||
| `@peardata/job` | `id` | Persisted job records (optional use) |
|
||||
|
||||
### Indexes
|
||||
|
||||
```text
|
||||
@peardata/node-by-hostname → node.hostname
|
||||
@peardata/peer-link-by-remote → peer-link.remotePublicKey
|
||||
@peardata/alert-event-by-chart → alert-event.chart + ts
|
||||
@peardata/metric-point-by-context → metric-point.context + ts
|
||||
```
|
||||
| Index | On |
|
||||
|-------|-----|
|
||||
| `@peardata/node-by-hostname` | hostname |
|
||||
| `@peardata/peer-link-by-remote` | remotePublicKey |
|
||||
| `@peardata/alert-event-by-chart` | chart + ts |
|
||||
| `@peardata/metric-point-by-context` | context + ts |
|
||||
| `@peardata/metric-point-by-tier` | tier + chart + ts |
|
||||
|
||||
Rebuild with:
|
||||
### Field notes
|
||||
|
||||
- `valuesJson` / `labelsJson` / `resultJson` — JSON strings for open-ended maps (avoids rigid hyperschema maps).
|
||||
- `tier` on metric-point: `1` = default warm (~1m). Future coarser tiers use `2+`.
|
||||
- `syncMode` on peer-link: `push` | `pull` | `both`.
|
||||
|
||||
### Regenerate after schema edits
|
||||
|
||||
```bash
|
||||
node scripts/build-db.js # Hyperschema + HyperDB.toDisk → spec/
|
||||
npm run build:db
|
||||
# or: node scripts/build-db.js
|
||||
```
|
||||
|
||||
## Agent integration shape
|
||||
Then commit `spec/hyperschema/*` and `spec/hyperdb/*`.
|
||||
|
||||
Follow `hyperdb-workshop` / `pear-hyperdb` Model pattern:
|
||||
---
|
||||
|
||||
## Runtime layout
|
||||
|
||||
```text
|
||||
server/
|
||||
db/
|
||||
build.js # schema codegen
|
||||
spec/hyperschema/
|
||||
spec/hyperdb/
|
||||
model.js # PearDataModel: putNode, linkPeer, queryWarm, …
|
||||
replicate.js # Hyperswarm join + store.replicate
|
||||
services/
|
||||
store.js # HOT memory (keep)
|
||||
store-hyperdb.js # WARM + metadata facade used by queryData/REST
|
||||
$PEARDATA_DATA_DIR/ # default ./data
|
||||
peer-policy.json # existing ACL file
|
||||
audit.log
|
||||
corestore/ # Corestore (HyperDB bee cores)
|
||||
```
|
||||
|
||||
### Boot (single-writer agent — Phase 2)
|
||||
Named core: **`peardata-meta`**.
|
||||
|
||||
```js
|
||||
const store = new Corestore(dataDir + '/corestore')
|
||||
const swarm = new Hyperswarm({ keyPair: await store.createKeyPair('swarm') })
|
||||
swarm.on('connection', (conn) => store.replicate(conn))
|
||||
Banner fields on agent start:
|
||||
|
||||
const metaCore = store.get({ name: 'peardata-meta' })
|
||||
const db = HyperDB.bee(metaCore, spec, { autoUpdate: true })
|
||||
- `hyperdb:` — DB public key (hex) for others to open a read replica
|
||||
- `swarm:` — `on` / `off`
|
||||
|
||||
// announce for linked peers / desktop seeders
|
||||
swarm.join(metaCore.discoveryKey, { server: true, client: true })
|
||||
```
|
||||
---
|
||||
|
||||
Collector path:
|
||||
## Configuration
|
||||
|
||||
1. Ingest → memory tier0 (unchanged)
|
||||
2. Every N samples → downsample → `tx.insert('@peardata/metric-point', …); tx.flush()`
|
||||
3. Alert transitions → `alert-event` collection
|
||||
4. `queryData` / REST: memory first, then HyperDB range scan for older windows
|
||||
| Variable | Default | Meaning |
|
||||
|----------|---------|---------|
|
||||
| `PEARDATA_HYPERDB` | on | `0` / `off` disables HyperDB entirely |
|
||||
| `PEARDATA_DATA_DIR` | `./data` | Policy + corestore root |
|
||||
| `PEARDATA_SWARM` | off | `1` enables Hyperswarm `store.replicate` |
|
||||
| `PEARDATA_TIER1_EVERY` | `60` | Samples per warm bucket (~60s @ 1Hz) |
|
||||
| `PEARDATA_TIER1_POINTS` | `1440` | In-memory tier1 ring (HyperDB keeps longer) |
|
||||
|
||||
### Auth note
|
||||
---
|
||||
|
||||
HyperDB replication shares **capability to read the core**, not PearData RPC roles. Keep:
|
||||
## API surface
|
||||
|
||||
- **Noise + MethodRoles** for mutating RPC (`setAlertConfig`, `runJob`)
|
||||
- Replication topic optionally gated (only invite-linked peers get discoveryKey / capability)
|
||||
- Do **not** announce writable cores to the public swarm without encryption / allowlist
|
||||
### RPC
|
||||
|
||||
## Linked nodes: sync modes
|
||||
| Method | Role | Description |
|
||||
|--------|------|-------------|
|
||||
| `getDbInfo` | viewer | `{ enabled, publicKeyHex, discoveryKeyHex, swarm }` |
|
||||
| `listPeerLinks` | viewer | Linked peers from HyperDB |
|
||||
| `linkPeer` | admin | Upsert link; optionally `joinRemoteTopic` if swarm on |
|
||||
| `unlinkPeer` | admin | Remove link |
|
||||
| `queryData` | viewer | Memory first; HyperDB warm if miss / `tier≥1` |
|
||||
|
||||
PearData “links” are first-class `@peardata/peer-link` rows + swarm topics.
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| **Pull** | Local agent opens remote DB by key (`HyperDB.bee(store.get({ key }), spec, { writable: false, autoUpdate: true })`) and replicates |
|
||||
| **Push** | Remote peers allowed to replicate our meta/warm cores (seed) |
|
||||
| **Both** | Mutual swarm join (homelab mesh) |
|
||||
| **Parent aggregate** | Parent pulls many children; stores namespaced copies or Autobase fleet view |
|
||||
|
||||
Discovery:
|
||||
|
||||
1. Desktop/admin mints link → stores remote pubkey + optional `dbKey` (z32/hex)
|
||||
2. Agent joins `discoveryKey` of that core
|
||||
3. On connection: `store.replicate(conn)` (workshop pattern)
|
||||
4. `clone.watch` / `autoUpdate` refreshes HyperDB indexes for REST/RPC queries
|
||||
|
||||
This is the same pattern as workshop §3.1 Lookups — **read path is swarm + HyperDB**, not constant RPC polling.
|
||||
|
||||
## Parent / HA (Phase 4) — Autobase workshop
|
||||
|
||||
When you need multi-writer fleet registry or HA parents:
|
||||
|
||||
- Autobase bootstrap key shared across parent instances
|
||||
- `open: (store) => HyperDB.bee(store.get('db-view'), spec, { extension: false, autoUpdate: true })`
|
||||
- hyperdispatch ops: `add-writer`, `put-alert`, `put-link`, `ingest-rollup`
|
||||
- RPC (existing PearData / workshop) appends ops; apply mutates HyperDB view
|
||||
- Clients dial **any** writer; view key stays stable
|
||||
|
||||
Do **not** “backup” by copying Corestore folders (workshop warning). Rotate writers via Autobase instead.
|
||||
|
||||
## What stays on protomux-rpc
|
||||
|
||||
| Keep on RPC | Why |
|
||||
|-------------|-----|
|
||||
| Live `push:metrics` | Sub-second UX; not DB-shaped |
|
||||
| `subscribeMetrics` / handshake / ACL | Session security |
|
||||
| `runJob`, mintInvite | Mutating control |
|
||||
| On-demand `queryData` for hot window | Memory path |
|
||||
|
||||
| Move to HyperDB (+ replicate) | Why |
|
||||
|-------------------------------|-----|
|
||||
| Peer links, aliases, labels | Shared fleet truth |
|
||||
| Alert config + history | Durable, queryable |
|
||||
| Warm/cold metric buckets | History without central server |
|
||||
| Node inventory | Parent `/api/v3/nodes` |
|
||||
|
||||
## Phased delivery (recommended)
|
||||
|
||||
### Phase A — Local durability (rocks or bee, no swarm yet)
|
||||
|
||||
1. Add `hyperdb`, `hyperschema`, `corestore` deps
|
||||
2. `scripts/build-db.js` + `spec/`
|
||||
3. Persist alert configs, peer policy, warm downsample into HyperDB
|
||||
4. REST/RPC history falls back to HyperDB after memory miss
|
||||
5. Keep REST localhost semantics
|
||||
|
||||
### Phase B — Link & replicate
|
||||
|
||||
1. Hyperswarm alongside HyperDHT RPC (or reuse connections carefully — often **separate swarm for store.replicate**)
|
||||
2. `linkPeer` / `unlinkPeer` RPCs write `@peardata/peer-link`
|
||||
3. Desktop can seed/cache agent DB by key for offline scrubbing
|
||||
4. Document `pd1.` invite vs **db discovery key** (two layers)
|
||||
|
||||
### Phase C — Parent fleet
|
||||
|
||||
1. Parent process pulls N child DB keys
|
||||
2. Composite REST `/api/v3/nodes` + scoped `/api/v3/data`
|
||||
3. Optional Autobase for parent HA
|
||||
|
||||
### Phase D — Polish
|
||||
|
||||
1. Encryption at rest (`encryptionKey` on cores / autobee)
|
||||
2. Blind-peer seeding for always-on warm history
|
||||
3. Grafana: already have REST Prometheus export; optionally expose hypercore-stats
|
||||
|
||||
## Concrete file plan (when implementing)
|
||||
|
||||
```text
|
||||
peardata/
|
||||
scripts/build-db.js
|
||||
spec/hyperschema/
|
||||
spec/hyperdb/
|
||||
server/db/model.js
|
||||
server/db/replicate.js
|
||||
server/services/store-hyperdb.js
|
||||
docs/STORAGE-HYPERDB.md ← this file
|
||||
```
|
||||
|
||||
Deps (approximate):
|
||||
`linkPeer` args:
|
||||
|
||||
```json
|
||||
"hyperdb": "^6",
|
||||
"hyperschema": "^1",
|
||||
"corestore": "^7",
|
||||
"hyperswarm": "^4",
|
||||
"autobase": "^7",
|
||||
"hyperdispatch": "^1"
|
||||
{
|
||||
"remotePublicKey": "<64 hex>",
|
||||
"alias": "homelab-nas",
|
||||
"dbKeyHex": "<optional remote db key>",
|
||||
"discoveryKeyHex": "<optional topic to pull>",
|
||||
"syncMode": "both",
|
||||
"role": "viewer"
|
||||
}
|
||||
```
|
||||
|
||||
(`autobee` only if you prefer that multiwriter path over Autobase+HyperDB view.)
|
||||
### REST
|
||||
|
||||
## Relationship to current PearData roadmap
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `GET /api/v3/db` | HyperDB keys + collection list |
|
||||
| `GET /api/v3/info` | Includes `peardata.hyperdb` block |
|
||||
| `GET /api/v3/data?...` | Uses async store query (warm fallback) |
|
||||
| Query `source` field | `memory-tier0` \| `memory-tier1` \| `hyperdb-warm` |
|
||||
|
||||
| Roadmap item | HyperDB role |
|
||||
|--------------|--------------|
|
||||
| Phase 2 tiered storage | Warm/cold collections |
|
||||
| Phase 3 PearDock/containers | Extra collections / indexes |
|
||||
| Phase 4 parent peer | Autobase + replicated views |
|
||||
| REST v3 historical queries | `find` ranges on `@peardata/metric-point` |
|
||||
```bash
|
||||
curl -s http://127.0.0.1:19999/api/v3/db | jq
|
||||
curl -s 'http://127.0.0.1:19999/api/v3/data?chart=system.cpu&after=-3600&tier=1&points=120' | jq '.source'
|
||||
```
|
||||
|
||||
## References (local clones)
|
||||
---
|
||||
|
||||
| Path under `holepunchto_repos` | Takeaway |
|
||||
|--------------------------------|----------|
|
||||
| `hyperdb/README.md` | rocks vs bee, find/get/tx, autoUpdate |
|
||||
| `hyperdb-workshop` | Schema builder, Corestore+Swarm replicate, RPC inserts |
|
||||
| `hyperdb-autobase-workshop` | Multi-writer view, hyperdispatch, HA |
|
||||
| `corestore/README.md` | `store.replicate(conn)`, namespacing |
|
||||
| `pear-hyperdb` | Thin Model wrapper pattern for Pear apps |
|
||||
| `autobee` | Experimental multiwriter bee alternative |
|
||||
## How linked-node sync works
|
||||
|
||||
## Decision summary
|
||||
### Phase A (now) — local durability
|
||||
|
||||
1. **Yes — incorporate HyperDB** for metadata + warm history + linked-node sync.
|
||||
2. **Keep memory + RPC pushes** for live 1s Netdata feel.
|
||||
3. **Sync linked nodes via Corestore replication on Hyperswarm**, not by streaming every sample over RPC.
|
||||
4. **Use Autobase+HyperDB** when you need multi-writer parents / HA — same pattern as Holepunch’s own workshops.
|
||||
5. **Treat discovery keys as sensitive** — link only invited peers; RPC AuthZ remains authoritative for mutations.
|
||||
1. Agent opens Corestore + HyperDB on boot.
|
||||
2. Warm points + alert events persist under `data/corestore`.
|
||||
3. Restart retains warm history / links / node row.
|
||||
|
||||
### Phase B — mesh replicate (`PEARDATA_SWARM=1`)
|
||||
|
||||
Workshop pattern (`hyperdb-workshop/bin.js`):
|
||||
|
||||
```js
|
||||
swarm.on('connection', (conn) => store.replicate(conn))
|
||||
swarm.join(db.discoveryKey, { server: true, client: true })
|
||||
```
|
||||
|
||||
1. Enable swarm on agents that should seed/pull.
|
||||
2. Share **db public key** + **discovery key** (`getDbInfo` / `/api/v3/db`).
|
||||
3. Admin calls `linkPeer` with `discoveryKeyHex` + `syncMode: pull|both`.
|
||||
4. Peers replicate Corestore; HyperDB `autoUpdate` refreshes indexes.
|
||||
5. Remote warm history becomes queryable locally (parent path).
|
||||
|
||||
**Security:** replication grants read of the core to anyone who can join the topic. Prefer:
|
||||
|
||||
- Swarm only on trusted LAN / VPN, or
|
||||
- Future: encrypted cores + allowlisted swarm joins
|
||||
- Keep **mutations** on protomux-rpc AuthZ (`linkPeer` is admin)
|
||||
|
||||
HyperDHT RPC identity ≠ Hyperswarm topic access — treat them as two layers.
|
||||
|
||||
### Phase C — Autobase parents (planned)
|
||||
|
||||
Same as `hyperdb-autobase-workshop`:
|
||||
|
||||
- Autobase bootstrap key across parent writers
|
||||
- View = `HyperDB.bee(store.get('db-view'), spec, { extension: false })`
|
||||
- hyperdispatch ops for `put-link`, `put-alert`, rollups
|
||||
- Do **not** “backup” by copying Corestore folders
|
||||
|
||||
---
|
||||
|
||||
## Code map
|
||||
|
||||
| Path | Role |
|
||||
|------|------|
|
||||
| `scripts/build-db.js` | Hyperschema + HyperDB builder |
|
||||
| `spec/hyperschema/` | Generated encodings + `schema.json` |
|
||||
| `spec/hyperdb/` | Generated collections/indexes |
|
||||
| `server/db/model.js` | Typed CRUD facade |
|
||||
| `server/db/index.js` | Singleton open/close |
|
||||
| `server/db/replicate.js` | Optional Hyperswarm |
|
||||
| `server/services/warm-flush.js` | Batch writer |
|
||||
| `server/services/store.js` | Hot ring + `warm` events + query fallback |
|
||||
|
||||
### Model usage example
|
||||
|
||||
```js
|
||||
import { openDb, getDb, closeDb } from './server/db/index.js'
|
||||
|
||||
await openDb()
|
||||
const db = getDb()
|
||||
|
||||
await db.putPeerLink({
|
||||
localNodeId: 'abc123',
|
||||
remotePublicKey: 'ff'.repeat(32),
|
||||
syncMode: 'pull',
|
||||
linkedAt: Date.now(),
|
||||
})
|
||||
|
||||
await db.putMetricPoints([
|
||||
{ chart: 'system.cpu', context: 'system.cpu', ts: Date.now(), values: { user: 12.3 }, tier: 1 },
|
||||
])
|
||||
|
||||
const rows = await db.queryMetricPoints({
|
||||
chart: 'system.cpu',
|
||||
afterMs: Date.now() - 3600_000,
|
||||
beforeMs: Date.now(),
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
From Holepunch stack (see `holepunchto_repos`):
|
||||
|
||||
- `hyperdb` — DB engine (bee + rocks)
|
||||
- `hyperschema` — struct codegen
|
||||
- `corestore` — named hypercores + replicate
|
||||
- `hyperswarm` — topic discovery for store sync
|
||||
- `ready-resource` — open/close lifecycle
|
||||
|
||||
Future: `autobase`, `hyperdispatch` for HA parents.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npm run build:db
|
||||
SKIP_INTEGRATION=1 npm test
|
||||
# includes test/hyperdb.test.js
|
||||
```
|
||||
|
||||
Manual:
|
||||
|
||||
```bash
|
||||
npm run start:server
|
||||
curl -s http://127.0.0.1:19999/api/v3/db | jq
|
||||
# wait ~60s for first warm bucket, then:
|
||||
curl -s 'http://127.0.0.1:19999/api/v3/data?chart=system.cpu&after=-7200&tier=1&points=120' | jq '.source,.points'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Operational checklist
|
||||
|
||||
- [ ] Commit `spec/` after every `build:db`
|
||||
- [ ] Back up **keys** (`SERVER_SEED`, swarm keypair in corestore) — not by zipping live corestore while writing
|
||||
- [ ] Keep REST on localhost; swarm only when linking trusted peers
|
||||
- [ ] Prefer `pd1.` invites for RPC admin; share db discovery keys only with linked nodes
|
||||
- [ ] Monitor disk under `data/corestore` as warm retention grows
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
| Repo (local `holepunchto_repos`) | Takeaway |
|
||||
|----------------------------------|----------|
|
||||
| `hyperdb` | rocks vs bee, tx/flush, autoUpdate |
|
||||
| `hyperdb-workshop` | builder + Corestore + Swarm replicate |
|
||||
| `hyperdb-autobase-workshop` | Multi-writer view for parents |
|
||||
| `corestore` | `store.replicate(conn)` |
|
||||
| `pear-hyperdb` | Pear Model wrapper style |
|
||||
|
||||
+22
-6
@@ -23,13 +23,29 @@ If fleet scale demands it later:
|
||||
|
||||
For MVP, JSON pushes keep the stack simple and debuggable.
|
||||
|
||||
## Bare / Pear runtime (no Node builtins)
|
||||
|
||||
Pear’s native runtime is **Bare**. Do not rely on Node core modules existing at runtime.
|
||||
|
||||
| Pattern | Use |
|
||||
|---------|-----|
|
||||
| `package.json` `imports` map | `fs` / `os` / `path` / `crypto` / `events` / `http` / … → `bare-*` under `"bare"` condition (same idea as PearDock / pear-docs node-compat) |
|
||||
| `bare-node-runtime/global` | Loaded first in Bare entrypoints (`bin/peardata-server.mjs`, `index.js`) for `process` / `Buffer` / `fetch` |
|
||||
| `b4a` | Buffers in `shared/` and wire code (prefer over Node `Buffer`) |
|
||||
| Direct `bare-*` | Optional for new Bare-first modules |
|
||||
|
||||
**Keep bare-safe:** `shared/`, `client/`, Pear `app.js` / `index.js` / `ui/`.
|
||||
**Agent (`server/`):** may keep Node-style import names; under Bare they resolve via the imports map. Boot via `bin/peardata-server.mjs`.
|
||||
|
||||
Do **not** use `if (isBare) require('bare-fs') else require('fs')` branches for packable apps — Bare’s packer walks both sides. Prefer import maps.
|
||||
|
||||
## Collection
|
||||
|
||||
| Option | Verdict |
|
||||
|--------|---------|
|
||||
| **Node `os` + `/proc` (chosen)** | Zero native deps, good enough for MVP, low overhead |
|
||||
| **`os` + `/proc` via bare-os / bare-fs (chosen)** | Hybrid Node+Bare; low overhead |
|
||||
| `node-os-utils` | Convenient but extra dep / less control |
|
||||
| Native bindings (netdata collectors, `systeminformation`) | Higher fidelity; consider Phase 2 for Windows depth |
|
||||
| Native bindings (`systeminformation`, etc.) | Higher fidelity; consider Phase 2 for Windows depth |
|
||||
| Shell out to `vmstat`/`iostat` | Avoid on hot path |
|
||||
|
||||
**Linux:** `/proc/meminfo`, `/proc/loadavg`, `/proc/net/dev`, `/proc/diskstats`
|
||||
@@ -58,20 +74,20 @@ Target overhead: single timer, no child processes per tick, ring buffers only.
|
||||
| Option | Verdict |
|
||||
|--------|---------|
|
||||
| **Canvas sparklines (chosen MVP)** | No extra deps inside Pear; snappy for 1s updates |
|
||||
| **uPlot** | Best next step for Netdata-like interactive charts (tiny, fast) |
|
||||
| **uPlot** | Best next step for interactive charts (tiny, fast) |
|
||||
| Chart.js / ECharts | Heavier; fine for secondary views |
|
||||
| Grafana via REST | External; use `/api/v3/data` + Prometheus export |
|
||||
|
||||
## REST
|
||||
|
||||
Built-in Node `http` (no Express) — small attack surface, enough for Netdata-style GET APIs.
|
||||
`http` module (no Express) — under Bare this is `bare-http1` via import maps. Small attack surface for agent-style GET APIs.
|
||||
|
||||
## Packaging
|
||||
|
||||
| Piece | Approach |
|
||||
|-------|----------|
|
||||
| Agent | Node 20+ , systemd unit `deploy/peardata.service` |
|
||||
| Desktop | Pear (`pear-electron` + `pear-bridge`) |
|
||||
| Agent | Node 20+ or Bare (`bin/peardata-server.mjs`), systemd unit `deploy/peardata.service` |
|
||||
| Desktop | Pear (`pear-electron` + `pear-bridge`) with Bare-ready imports |
|
||||
| Invites | `pd1.` tokens (PearDock-style) |
|
||||
| Installer | Phase 1 one-liner script |
|
||||
|
||||
|
||||
+2
-1
@@ -16,7 +16,8 @@
|
||||
| `test/crypto-auth.test.js` | MAC key, capabilities, admin proof, invites, classify input |
|
||||
| `test/protocol.test.js` | Constants, monitoring `MethodRoles`, schema validators |
|
||||
| `test/store.test.js` | Metric ring buffer ingest + query |
|
||||
| `test/rest.test.js` | Netdata-style `/api/v3` routes |
|
||||
| `test/hyperdb.test.js` | HyperDB model (node, links, warm points, alerts) |
|
||||
| `test/rest.test.js` | agent-style `/api/v3` routes |
|
||||
| `test/integration.test.js` | Live HyperDHT agent + handshake + metrics query |
|
||||
|
||||
```bash
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
<span class="logo">◈</span>
|
||||
<div class="app-brand-text">
|
||||
<strong>PearData</strong>
|
||||
<span class="muted">P2P · Netdata-class observability</span>
|
||||
<span class="muted">P2P · real-time observability</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,16 @@
|
||||
* Pear desktop entrypoint.
|
||||
* Boots pear-electron UI + pear-bridge HTTP for the HTML app shell.
|
||||
*
|
||||
* Under Bare: load bare-node-runtime globals so shared/client code can use
|
||||
* process/Buffer while package.json `imports` maps builtins → bare-*.
|
||||
*
|
||||
* @typedef {import('pear-interface')}
|
||||
*/
|
||||
/* global Pear */
|
||||
if (typeof globalThis.Bare !== 'undefined') {
|
||||
await import('bare-node-runtime/global')
|
||||
}
|
||||
|
||||
import Runtime from 'pear-electron'
|
||||
import Bridge from 'pear-bridge'
|
||||
|
||||
|
||||
Generated
+462
-72
@@ -10,6 +10,34 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"b4a": "^1.8.1",
|
||||
"bare-assert": "^1.1.0",
|
||||
"bare-buffer": "^3.3.1",
|
||||
"bare-console": "^6.0.1",
|
||||
"bare-crypto": "^1.15.3",
|
||||
"bare-dgram": "^1.0.1",
|
||||
"bare-dns": "^2.1.4",
|
||||
"bare-events": "^2.9.1",
|
||||
"bare-fs": "^4.7.4",
|
||||
"bare-http1": "^4.5.7",
|
||||
"bare-https": "^3.0.0",
|
||||
"bare-module": "^6.1.2",
|
||||
"bare-net": "^2.3.2",
|
||||
"bare-node-runtime": "^1.5.0",
|
||||
"bare-os": "^3.9.3",
|
||||
"bare-path": "^3.1.1",
|
||||
"bare-performance": "^2.0.0",
|
||||
"bare-process": "^4.5.1",
|
||||
"bare-querystring": "^1.0.0",
|
||||
"bare-stream": "^2.7.0",
|
||||
"bare-string-decoder": "^1.0.0",
|
||||
"bare-subprocess": "^5.2.3",
|
||||
"bare-timers": "^3.0.0",
|
||||
"bare-tls": "^3.0.0",
|
||||
"bare-tty": "^5.0.0",
|
||||
"bare-url": "^2.4.5",
|
||||
"bare-utils": "^1.5.1",
|
||||
"bare-worker": "^4.0.0",
|
||||
"bare-zlib": "^1.3.1",
|
||||
"compact-encoding": "^3.3.0",
|
||||
"corestore": "^7.11.1",
|
||||
"dotenv": "^17.4.2",
|
||||
@@ -19,6 +47,7 @@
|
||||
"hyperdb": "^6.7.0",
|
||||
"hyperdht": "^6.33.0",
|
||||
"hyperschema": "^1.21.0",
|
||||
"hyperswarm": "^4.17.0",
|
||||
"pear-bridge": "^1.2.5",
|
||||
"pear-electron": "^1.7.28",
|
||||
"pear-pipe": "^1.0.6",
|
||||
@@ -27,6 +56,7 @@
|
||||
"protomux-rpc": "^1.10.0",
|
||||
"ready-resource": "^1.2.0",
|
||||
"safety-catch": "^1.0.3",
|
||||
"which-runtime": "^1.4.0",
|
||||
"z32": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -130,6 +160,15 @@
|
||||
"integrity": "sha512-zdc8l88eB11Jsz5rDd6sCAgv2kUFXgdrZWoMlgU6JMkfAi1/uuGFC3IEHswKbIRQTk5H3T5CMuechsXYxiaHlQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/bare-abort-controller": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-abort-controller/-/bare-abort-controller-1.1.2.tgz",
|
||||
"integrity": "sha512-wk+JZGZEjm7RqaBAU1KuT8TxYYz7h/xxC7+4IVuDZFqK4dGqwrJ/1/yR8hNiSyaAAVHTTFNBJPH4Rzu+rI3IAg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-events": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-addon-resolve": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.1.tgz",
|
||||
@@ -174,11 +213,27 @@
|
||||
"bare-inspect": "^3.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-async-hooks": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-async-hooks/-/bare-async-hooks-0.0.0.tgz",
|
||||
"integrity": "sha512-xNfGwUobaomCGMGAqohAekS3uMCj+4tvI4AoOaJnO7NfpN+dvFdkC5xkeQtmZzs2vxf2TR5J6i5FDd1ImCZERw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/bare-broadcast-channel": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-broadcast-channel/-/bare-broadcast-channel-0.2.0.tgz",
|
||||
"integrity": "sha512-MuAwdKWr4cSjNwqvbE3tA9Wn6w69q6iXYnP2Wb0nlDa6sKNSMSfY7LXkV4FzWlq9JFP9d0HkCAKPboTLKnUfWQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-events": "^2.0.0",
|
||||
"bare-stream": "^2.7.0",
|
||||
"bare-structured-clone": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-buffer": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-buffer/-/bare-buffer-3.6.2.tgz",
|
||||
"integrity": "sha512-WT9xx12FJvWbBCkgfjuAeWfY40RW6BKVr2fK9UGrTEYKbvqhcaW0J9AMkA3Sj36Wgi+DUuGXYkZPxn5jVH61LA==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"bare": ">=1.20.0"
|
||||
@@ -188,7 +243,6 @@
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
|
||||
"integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*",
|
||||
@@ -207,7 +261,6 @@
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/bare-channel/-/bare-channel-5.2.4.tgz",
|
||||
"integrity": "sha512-enkaOtvXUiZsLBSbataxV/QBjehCOK+SUTd2JQWUYW2iIOufpPu9jyZ9bqJqkEquktrK/rpEMVEmKsPzoci/sA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-events": "^2.0.0",
|
||||
@@ -218,6 +271,19 @@
|
||||
"bare": ">=1.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-console": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-console/-/bare-console-6.2.0.tgz",
|
||||
"integrity": "sha512-qQ+Vasa3NwNdVDcUWKIIEG5p7MKO7uABcV/xBR9MDwQg3QklC5ayemaHzBa/ER4h6TKN2PRQN5IppNwUAMtb8Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-format": "^1.0.2",
|
||||
"bare-hrtime": "^2.0.0",
|
||||
"bare-logger": "^2.0.0",
|
||||
"bare-system-logger": "^1.0.2",
|
||||
"bare-type": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-cov": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.2.tgz",
|
||||
@@ -239,7 +305,6 @@
|
||||
"version": "1.15.3",
|
||||
"resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.15.3.tgz",
|
||||
"integrity": "sha512-macV9lbyJTsLPRXJkBtz8ivTGEo3LCyJInLT9IB/PWJ7pRXwvHs/FP4bx/fWw+HZkiepIYCAV2cuU5CR92XWCw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-assert": "^1.2.0",
|
||||
@@ -269,12 +334,27 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
|
||||
"integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-os": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-dgram": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-dgram/-/bare-dgram-1.0.1.tgz",
|
||||
"integrity": "sha512-EdsyRErrkWgN8fENdrDdXFEE9HAuJ/m6ehXz13fVj9JhdCaLWIA+L8o5aYNRLt66x08RlyG2vbrRAZoxGfcdlg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-events": "^2.5.0",
|
||||
"udx-native": "^1.11.2"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-diagnostics-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-diagnostics-channel/-/bare-diagnostics-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-Reu+EQo+eLpB+a5p8UykFEdXndFaRaSgKV38uAMh/qhE2eTeJcdwAwo74hKYyeN8GD3DIFqC9ZlM4bnDc03CIg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/bare-dns": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
|
||||
@@ -288,7 +368,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
|
||||
"integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*"
|
||||
@@ -322,6 +401,44 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-fetch": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-fetch/-/bare-fetch-3.1.0.tgz",
|
||||
"integrity": "sha512-+l0Ex1cXa50M/UaOYQPyWk76UKJ3odkgHkdcFqTQfbUbHuP/32OE0qVMYveHK4Dc6hG8Y3HQvn5RNC21IbJeKw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-form-data": "^1.2.0",
|
||||
"bare-http1": "^4.5.2",
|
||||
"bare-https": "^3.0.0",
|
||||
"bare-mime": "^1.0.0",
|
||||
"bare-performance": "^2.1.1",
|
||||
"bare-stream": "^2.9.1",
|
||||
"bare-url": "^2.4.0",
|
||||
"bare-zlib": "^1.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*",
|
||||
"bare-buffer": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-abort-controller": {
|
||||
"optional": true
|
||||
},
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-form-data": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-form-data/-/bare-form-data-1.2.2.tgz",
|
||||
"integrity": "sha512-DQyAkCf5mgKT07orewuvaJfoalw7RBSHia4wgkrG7+seI6aHLB+r6gMRdCGrlO+BmCqMwgTeHAHxDU2NrOjQnQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-buffer": "^3.6.0",
|
||||
"bare-stream": "^2.6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-format": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.2.tgz",
|
||||
@@ -395,7 +512,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-https/-/bare-https-3.0.0.tgz",
|
||||
"integrity": "sha512-W1GRSCzn+xXKf5bMcPs/hg6Ga1bxPqb7owGfS+tvlBQfPe5Q2STcanRuKZrgU60v5uKrhXH5cgWwM+DLqvXZgQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-http1": "^4.4.0",
|
||||
@@ -420,7 +536,6 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-6.1.0.tgz",
|
||||
"integrity": "sha512-PRxmZ4gF+K3TLzGubgRFvzdECybTCSKackgNsAdd4e7SdvICxMkGj+p5iX7bSKYSmgDnxgCR+2Z6UXmWykKhvw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-events": "^2.1.0",
|
||||
@@ -441,11 +556,25 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-logger": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bare-logger/-/bare-logger-2.0.3.tgz",
|
||||
"integrity": "sha512-U6K7NxHdeAHxEgHFDV8zXvgjgDZKQO6LlCrxQvUvlrZEVTnoezSImMWizi85rbZpVsbeI1ZWcLCRGnoIf5EV8Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-format": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-mime": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-mime/-/bare-mime-1.0.0.tgz",
|
||||
"integrity": "sha512-lUOswzBkfqham4zjLDueKOd4Qj3gS56BiZ3q2f0g0adoFhF+HFNupvTUfZBWoicl7fWJ7Hp2RUZjmkY47dxxOQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/bare-module": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.4.0.tgz",
|
||||
"integrity": "sha512-Yn4V5g5EqGQL4LYUOmt7fjKzj2JPWyJOqE3lPoeZwfUH5rk4CKUfZj6JhDwbzhBYCqqmUgjgQ5aY8cihAPILLA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-bundle": "^1.3.0",
|
||||
@@ -471,7 +600,6 @@
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.6.3.tgz",
|
||||
"integrity": "sha512-NQY7cnPV3GZlHJphX4nXmPdNPER/Tp17pVi9/he2ODw/GNZ7FXzrZlrS7WMF8zbtWigqW/NMc9aQc2BH8UJXqA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"require-addon": "^1.0.2"
|
||||
@@ -502,11 +630,34 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-module-traverse": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/bare-module-traverse/-/bare-module-traverse-2.4.4.tgz",
|
||||
"integrity": "sha512-zK4bDxqeD15Tvu/C5B1Mi/epl9PavjIhU3SVh5RhDb4Hpet6applPVX8Q9rinu7nT80yzgSSlBn0exzJaNXv0A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-addon-resolve": "^1.5.0",
|
||||
"bare-mime": "^1.0.0",
|
||||
"bare-module-lexer": "^1.6.0",
|
||||
"bare-module-resolve": "^1.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*",
|
||||
"bare-url": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
},
|
||||
"bare-url": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-net": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.3.2.tgz",
|
||||
"integrity": "sha512-I+yz+pqbYsBkxDsnu5vkKvy7RSNY9CcAvu2jZT6PsmdXJQG1i3dmD5V7xc3334OVp2absgtUEYLmmuNFlphBzg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-events": "^2.2.2",
|
||||
@@ -515,6 +666,79 @@
|
||||
"bare-tcp": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-node-runtime": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-node-runtime/-/bare-node-runtime-1.5.0.tgz",
|
||||
"integrity": "sha512-eMRq9WDYd+E0IZ8EG/8iCozZJl511z6vp7kxZyxjDw77ggOZ7bThFpJ72Kztjs8sh0hoe+xhFyZmoqAdVE/Ziw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-abort-controller": "^1.0.0",
|
||||
"bare-assert": "^1.1.0",
|
||||
"bare-async-hooks": "^0.0.0",
|
||||
"bare-buffer": "^3.3.1",
|
||||
"bare-console": "^6.0.1",
|
||||
"bare-crypto": "^1.11.2",
|
||||
"bare-dgram": "^1.0.1",
|
||||
"bare-diagnostics-channel": "^1.1.0",
|
||||
"bare-dns": "^2.1.4",
|
||||
"bare-events": "^2.7.0",
|
||||
"bare-fetch": "^3.0.0",
|
||||
"bare-fs": "^4.2.3",
|
||||
"bare-http1": "^4.0.4",
|
||||
"bare-https": "^3.0.0",
|
||||
"bare-inspector": "^6.0.1",
|
||||
"bare-module": "^6.1.2",
|
||||
"bare-net": "^2.0.2",
|
||||
"bare-os": "^3.6.2",
|
||||
"bare-path": "^3.0.0",
|
||||
"bare-performance": "^2.0.0",
|
||||
"bare-process": "^4.2.1",
|
||||
"bare-punycode": "^0.0.0",
|
||||
"bare-querystring": "^1.0.0",
|
||||
"bare-readline": "^1.1.0",
|
||||
"bare-repl": "^6.0.1",
|
||||
"bare-sqlite": "^0.1.4",
|
||||
"bare-stream": "^2.7.0",
|
||||
"bare-string-decoder": "^1.0.0",
|
||||
"bare-subprocess": "^6.0.0",
|
||||
"bare-timers": "^3.0.3",
|
||||
"bare-tls": "^3.0.0",
|
||||
"bare-tty": "^5.0.3",
|
||||
"bare-url": "^2.2.2",
|
||||
"bare-utils": "^1.5.1",
|
||||
"bare-v8": "^1.0.1",
|
||||
"bare-vm": "^1.0.0",
|
||||
"bare-worker": "^4.0.0",
|
||||
"bare-ws": "^3.0.0",
|
||||
"bare-zlib": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-node-runtime/node_modules/bare-subprocess": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-6.1.0.tgz",
|
||||
"integrity": "sha512-8L6KtbmreDy4Fc1BdDqaDo9fg0nbedUmTSFQxYPV0uHIM2BBAX8+E0LyMDKgkk0I+mlKto80WYzOnIhT8VDdOg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-env": "^3.0.0",
|
||||
"bare-events": "^2.5.4",
|
||||
"bare-os": "^3.0.1",
|
||||
"bare-pipe": "^4.2.0",
|
||||
"bare-structured-clone": "^1.5.2",
|
||||
"bare-tcp": "^2.4.1",
|
||||
"bare-url": "^2.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-os": {
|
||||
"version": "3.9.3",
|
||||
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.3.tgz",
|
||||
@@ -530,6 +754,18 @@
|
||||
"integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/bare-performance": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-performance/-/bare-performance-2.1.1.tgz",
|
||||
"integrity": "sha512-nVlulswnYgXS2Fkbk4ZIKgfIWY/rmeG8ljM9aryPYClgPNzpOgOSLQzVSgU/K+ReLJHq7fEUQ9SBdjEs1QTIfw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-events": "^2.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.27.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-pipe": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.2.2.tgz",
|
||||
@@ -565,6 +801,21 @@
|
||||
"bare-stdio": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-punycode": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-punycode/-/bare-punycode-0.0.0.tgz",
|
||||
"integrity": "sha512-PC2Y6mGLytZPJCB9M7CvO5Zb6uWYVUZ+5t3L6vePFuFM6tdC6SsQ+sIsuf0Sa6LHBoWURX7I9yMMbPXMv7TIdQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-querystring": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-querystring/-/bare-querystring-1.1.0.tgz",
|
||||
"integrity": "sha512-pUtEM6JrX53MbEJFwO92F0Ch7BwZ67KD7LyglcB8/tvkkVdwTgN1f7oIklRe+NTT/WCYZgwjDFYS9efBxDSq8g==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/bare-readline": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-readline/-/bare-readline-1.3.1.tgz",
|
||||
@@ -575,6 +826,30 @@
|
||||
"bare-stream": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-realm": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-realm/-/bare-realm-2.0.1.tgz",
|
||||
"integrity": "sha512-kQbYU6AAQu4XBTQesuz2+PpjBx7MJzf5Qw3nSLzQCzbVglx7Ml85MtWEjUe1AeslXqrd+nFPHMEbL55ouISscA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"bare": ">=1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-repl": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-repl/-/bare-repl-6.1.1.tgz",
|
||||
"integrity": "sha512-vV52s+zLcwf9WB6y09KwHrqHrCR0UUgcKz3NPhPAwmO62ctFYbBfjskBch92wWk7vRrF68dHJj3hmYNCe8d18g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-inspect": "^3.0.0",
|
||||
"bare-module": "^6.4.0",
|
||||
"bare-path": "^3.0.0",
|
||||
"bare-pipe": "^4.0.0",
|
||||
"bare-readline": "^1.0.0",
|
||||
"bare-stream": "^2.0.0",
|
||||
"bare-tty": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-semver": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.1.0.tgz",
|
||||
@@ -593,6 +868,20 @@
|
||||
"bare": ">=1.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-sqlite": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/bare-sqlite/-/bare-sqlite-0.1.4.tgz",
|
||||
"integrity": "sha512-h2UdmQohSQG98lrMZ31PnhKIZRNT+1cDUZRzboHmsXKp+l4N4UBSJylMDEGL+D4fw6MJfOHWFIzUG20pITGy8Q==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-stdio": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bare-stdio/-/bare-stdio-1.0.3.tgz",
|
||||
@@ -631,11 +920,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-string-decoder": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-string-decoder/-/bare-string-decoder-1.0.0.tgz",
|
||||
"integrity": "sha512-FjFvfHo88U7borNQSj9ijP2JTb1asqY6K28OZrix4dF9iZf5D2wi1+99CgLlHT3HtYqdwxRhDAiKu8rVstt5rQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"text-decoder": "^1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-structured-clone": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-structured-clone/-/bare-structured-clone-1.6.0.tgz",
|
||||
"integrity": "sha512-AZjEERyqF7kAuudlmgrweT2KwwWM0LsGG4Gx/bWyFwJrKU7E3wNG8c09z2DIrdw93p3vDtfOX8fyHOcXfy5m+g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-buffer": "^3.6.0",
|
||||
@@ -651,7 +956,6 @@
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-stylize/-/bare-stylize-0.0.1.tgz",
|
||||
"integrity": "sha512-l3MjmIl476bWijYWf3RbE+osl4iuXSOMudzp0vAqzIK7gPgn/+G3oAxp8Oin9CFF911KBP0LO9kts8Ci8mGZaQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-ansi-escapes": "^2.2.3",
|
||||
@@ -659,18 +963,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bare-subprocess": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-6.1.0.tgz",
|
||||
"integrity": "sha512-8L6KtbmreDy4Fc1BdDqaDo9fg0nbedUmTSFQxYPV0uHIM2BBAX8+E0LyMDKgkk0I+mlKto80WYzOnIhT8VDdOg==",
|
||||
"dev": true,
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz",
|
||||
"integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-env": "^3.0.0",
|
||||
"bare-events": "^2.5.4",
|
||||
"bare-os": "^3.0.1",
|
||||
"bare-pipe": "^4.2.0",
|
||||
"bare-structured-clone": "^1.5.2",
|
||||
"bare-tcp": "^2.4.1",
|
||||
"bare-pipe": "^4.0.0",
|
||||
"bare-url": "^2.2.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -685,6 +986,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-system-logger": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bare-system-logger/-/bare-system-logger-1.0.3.tgz",
|
||||
"integrity": "sha512-HXTXZyhIIS4ZDYun4J9zKNQZDfLgEkkK8wy9nrTFJE5xq8APsOhDxuEsxP0YcNwC23DW1jaNpqD51zDeXPzIdg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-logger": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-tcp": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.5.2.tgz",
|
||||
@@ -707,11 +1017,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-thread": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/bare-thread/-/bare-thread-1.2.4.tgz",
|
||||
"integrity": "sha512-MnqMCGVp2JJJNXgwUBC8hw+J4FrTeLkLgfCPPl5tQK6MbtV2Efi6LKK5v819VRY0da+AXlCbgssbAVge31NfRg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-bundle": "^1.9.0",
|
||||
"bare-module-resolve": "^1.11.2",
|
||||
"bare-module-traverse": "^2.0.0",
|
||||
"bare-url": "^2.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-timers": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-timers/-/bare-timers-3.2.1.tgz",
|
||||
"integrity": "sha512-O+9e6Jol/BoYsQUzAFXG0gWUW1GWryFMblGcNBfi3smPrD3rJIKQwpw2FJARl2j/1VHne4a8YaHPK1cxDKfiYQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"bare": ">=1.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-abort-controller": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-tls": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-3.1.7.tgz",
|
||||
"integrity": "sha512-AMw8tJlb3LhzAmhgXRcjDrTlNxR3gXXyj6G8eU9iwvCFtiUBD8MxAW7bwunA1gXDukgo40A970jX0APc2jMU7A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-net": "^2.0.1",
|
||||
@@ -748,7 +1086,6 @@
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/bare-type-stripper/-/bare-type-stripper-0.1.4.tgz",
|
||||
"integrity": "sha512-FdZhp9XEnQpj8AWFmIft/sVUyKS9XSmB6PhcxBHhuEDxxZM5Kkt8+kFS7eEpLXR7TkaRkNpSENoGH/8lpAmtkA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"require-addon": "^1.0.2"
|
||||
@@ -775,7 +1112,6 @@
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.6.0.tgz",
|
||||
"integrity": "sha512-WhQEIkkAxkSnW7u1QgrI0AfNm5JpMruETXeYsb5qnkBJ0TTfNKygZmsh6rkoHBANaV+C/7Jed7bJP9OmEHG7rQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-debug-log": "^2.0.0",
|
||||
@@ -786,6 +1122,12 @@
|
||||
"bare-type": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-v8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-v8/-/bare-v8-1.0.1.tgz",
|
||||
"integrity": "sha512-/cR5ZvFWQRdtTZ4tx0j7TKvTWce8UnnLqm88fwHtJmfM7HODIBVjQGDT7KkDLeD2d/eHP2pzB71Y8/QyiMMKrQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/bare-v8-to-istanbul": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
|
||||
@@ -804,11 +1146,33 @@
|
||||
"which-runtime": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-vm": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-vm/-/bare-vm-1.0.1.tgz",
|
||||
"integrity": "sha512-yLnbRvKt3AhRTmtfTIrYfdTHqGEfIJc+Fgb2DcHejE0HJ+p5adGxxPMvd3893Z7iXVYnalxukNARn4oJSZELHQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-realm": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-worker": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-worker/-/bare-worker-4.4.0.tgz",
|
||||
"integrity": "sha512-zSc1biis9ks03nj/24M7tYS2V0CPhefizzRIxVYdglbbUAgA0zakwSgTKLJshZ6P+9NA55M/eG4k/rBALZVbxg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-broadcast-channel": "^0.2.0",
|
||||
"bare-channel": "^5.1.5",
|
||||
"bare-events": "^2.2.1",
|
||||
"bare-module": "^6.4.0",
|
||||
"bare-stream": "^2.13.3",
|
||||
"bare-thread": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-ws": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-3.0.0.tgz",
|
||||
"integrity": "sha512-q2v0UIQ0cFQBXQMp+0FRaoSk1EoYgOhzO7yio0TqBV6Rkfot97mbBYxb1ssw5faVCisVaFxQYY8113SMLYQBow==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-crypto": "^1.2.0",
|
||||
@@ -830,6 +1194,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-zlib": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-zlib/-/bare-zlib-1.4.1.tgz",
|
||||
"integrity": "sha512-CsnQl+XyLaUecB9/OUpjqmemung10M7J2UNXz+6NAVrZAI3HC9c5Kxw34aI0jaU9+gb2yUCD31hOrtPZlnE3bA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-stream": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/big-sparse-array": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz",
|
||||
@@ -934,6 +1315,33 @@
|
||||
"brittle-node": "brittle-node.js"
|
||||
}
|
||||
},
|
||||
"node_modules/brittle/node_modules/bare-subprocess": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-6.1.0.tgz",
|
||||
"integrity": "sha512-8L6KtbmreDy4Fc1BdDqaDo9fg0nbedUmTSFQxYPV0uHIM2BBAX8+E0LyMDKgkk0I+mlKto80WYzOnIhT8VDdOg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-env": "^3.0.0",
|
||||
"bare-events": "^2.5.4",
|
||||
"bare-os": "^3.0.1",
|
||||
"bare-pipe": "^4.2.0",
|
||||
"bare-structured-clone": "^1.5.2",
|
||||
"bare-tcp": "^2.4.1",
|
||||
"bare-url": "^2.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cjs-module-lexer": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
|
||||
@@ -1380,6 +1788,21 @@
|
||||
"generate-string": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/hyperswarm": {
|
||||
"version": "4.17.0",
|
||||
"resolved": "https://registry.npmjs.org/hyperswarm/-/hyperswarm-4.17.0.tgz",
|
||||
"integrity": "sha512-oe86sK961Ueg7rvDN/veFwG8xH+Iv6vObPhGDkPJcDVxk/NduW41ZhAcVDnHzRbm7S0eLU7WaDUvehOYoKSpRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"b4a": "^1.3.1",
|
||||
"bare-events": "^2.2.0",
|
||||
"hyperdht": "^6.21.0",
|
||||
"safety-catch": "^1.0.2",
|
||||
"shuffled-priority-queue": "^2.1.0",
|
||||
"streamx": "^2.22.1",
|
||||
"unslab": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/index-encoder": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/index-encoder/-/index-encoder-3.5.0.tgz",
|
||||
@@ -1636,30 +2059,6 @@
|
||||
"which-runtime": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pear-electron/node_modules/bare-subprocess": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz",
|
||||
"integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-env": "^3.0.0",
|
||||
"bare-events": "^2.5.4",
|
||||
"bare-os": "^3.0.1",
|
||||
"bare-pipe": "^4.0.0",
|
||||
"bare-url": "^2.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pear-electron/node_modules/compact-encoding": {
|
||||
"version": "2.19.2",
|
||||
"resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-2.19.2.tgz",
|
||||
@@ -1835,30 +2234,6 @@
|
||||
"which-runtime": "^1.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/pear-run/node_modules/bare-subprocess": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz",
|
||||
"integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-env": "^3.0.0",
|
||||
"bare-events": "^2.5.4",
|
||||
"bare-os": "^3.0.1",
|
||||
"bare-pipe": "^4.0.0",
|
||||
"bare-url": "^2.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pear-stamp": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pear-stamp/-/pear-stamp-1.0.2.tgz",
|
||||
@@ -2170,6 +2545,15 @@
|
||||
"unix-path-resolve": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/shuffled-priority-queue": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/shuffled-priority-queue/-/shuffled-priority-queue-2.1.0.tgz",
|
||||
"integrity": "sha512-xhdh7fHyMsr0m/w2kDfRJuBFRS96b9l8ZPNWGaQ+PMvnUnZ/Eh+gJJ9NsHBd7P9k0399WYlCLzsy18EaMfyadA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"unordered-set": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-promise": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/signal-promise/-/signal-promise-1.0.3.tgz",
|
||||
@@ -2385,6 +2769,12 @@
|
||||
"integrity": "sha512-kG4g5nobBBaMnH2XbrS4sLUXEpx4nY2J3C6KAlAUcnahG2HChxSPVKWYrqEq76iTo+cyMkLUjqxGaQR2tz097Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unordered-set": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/unordered-set/-/unordered-set-2.0.1.tgz",
|
||||
"integrity": "sha512-eUmNTPzdx+q/WvOHW0bgGYLWvWHNT3PTKEQLg0MAQhc0AHASHVHoP/9YytYd4RBVariqno/mEUhVZN98CmD7bg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unslab": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz",
|
||||
|
||||
+421
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "peardata",
|
||||
"version": "0.1.0",
|
||||
"description": "Decentralized P2P Netdata-class monitoring — PearMonitor agent + Pear desktop + REST v3",
|
||||
"description": "Decentralized P2P real-time monitoring \u2014 PearMonitor agent + Pear desktop + REST v3",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
@@ -36,14 +36,15 @@
|
||||
"scripts": {
|
||||
"dev": "pear run -d .",
|
||||
"start": "pear run -d .",
|
||||
"start:server": "node server/server.js",
|
||||
"server": "node server/server.js",
|
||||
"start:server": "node bin/peardata-server.mjs",
|
||||
"server": "node bin/peardata-server.mjs",
|
||||
"start:server:bin": "node bin/peardata-server.mjs",
|
||||
"test": "brittle-node test/*.test.js",
|
||||
"test:integration": "brittle-node test/integration.test.js",
|
||||
"healthcheck": "node scripts/healthcheck.js",
|
||||
"soak": "node scripts/soak.js",
|
||||
"mint-invite": "node scripts/mint-invite.js",
|
||||
"build:db": "node scripts/build-db.js",
|
||||
"rename": "bash scripts/rename-template.sh",
|
||||
"release:notes": "node -e \"console.log('See docs/RELEASE.md')\""
|
||||
},
|
||||
@@ -58,6 +59,7 @@
|
||||
"hyperdb": "^6.7.0",
|
||||
"hyperdht": "^6.33.0",
|
||||
"hyperschema": "^1.21.0",
|
||||
"hyperswarm": "^4.17.0",
|
||||
"pear-bridge": "^1.2.5",
|
||||
"pear-electron": "^1.7.28",
|
||||
"pear-pipe": "^1.0.6",
|
||||
@@ -66,10 +68,425 @@
|
||||
"protomux-rpc": "^1.10.0",
|
||||
"ready-resource": "^1.2.0",
|
||||
"safety-catch": "^1.0.3",
|
||||
"z32": "^1.1.0"
|
||||
"z32": "^1.1.0",
|
||||
"bare-assert": "^1.1.0",
|
||||
"bare-buffer": "^3.3.1",
|
||||
"bare-console": "^6.0.1",
|
||||
"bare-crypto": "^1.15.3",
|
||||
"bare-dgram": "^1.0.1",
|
||||
"bare-dns": "^2.1.4",
|
||||
"bare-events": "^2.9.1",
|
||||
"bare-fs": "^4.7.4",
|
||||
"bare-http1": "^4.5.7",
|
||||
"bare-https": "^3.0.0",
|
||||
"bare-module": "^6.1.2",
|
||||
"bare-net": "^2.3.2",
|
||||
"bare-node-runtime": "^1.5.0",
|
||||
"bare-os": "^3.9.3",
|
||||
"bare-path": "^3.1.1",
|
||||
"bare-performance": "^2.0.0",
|
||||
"bare-process": "^4.5.1",
|
||||
"bare-querystring": "^1.0.0",
|
||||
"bare-stream": "^2.7.0",
|
||||
"bare-string-decoder": "^1.0.0",
|
||||
"bare-subprocess": "^5.2.3",
|
||||
"bare-timers": "^3.0.0",
|
||||
"bare-tls": "^3.0.0",
|
||||
"bare-tty": "^5.0.0",
|
||||
"bare-url": "^2.4.5",
|
||||
"bare-utils": "^1.5.1",
|
||||
"bare-worker": "^4.0.0",
|
||||
"bare-zlib": "^1.3.1",
|
||||
"which-runtime": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"brittle": "^4.1.0",
|
||||
"pear-interface": "^1.1.0"
|
||||
},
|
||||
"imports": {
|
||||
"assert": {
|
||||
"bare": "bare-assert",
|
||||
"default": "assert"
|
||||
},
|
||||
"node:assert": {
|
||||
"bare": "bare-assert",
|
||||
"default": "assert"
|
||||
},
|
||||
"assert/strict": {
|
||||
"bare": "bare-assert/strict",
|
||||
"default": "assert/strict"
|
||||
},
|
||||
"node:assert/strict": {
|
||||
"bare": "bare-assert/strict",
|
||||
"default": "assert/strict"
|
||||
},
|
||||
"buffer": {
|
||||
"bare": "bare-buffer",
|
||||
"default": "buffer"
|
||||
},
|
||||
"node:buffer": {
|
||||
"bare": "bare-buffer",
|
||||
"default": "buffer"
|
||||
},
|
||||
"child_process": {
|
||||
"bare": "bare-subprocess",
|
||||
"default": "child_process"
|
||||
},
|
||||
"node:child_process": {
|
||||
"bare": "bare-subprocess",
|
||||
"default": "child_process"
|
||||
},
|
||||
"console": {
|
||||
"bare": "bare-console",
|
||||
"default": "console"
|
||||
},
|
||||
"node:console": {
|
||||
"bare": "bare-console",
|
||||
"default": "console"
|
||||
},
|
||||
"crypto": {
|
||||
"bare": "bare-crypto",
|
||||
"default": "crypto"
|
||||
},
|
||||
"node:crypto": {
|
||||
"bare": "bare-crypto",
|
||||
"default": "crypto"
|
||||
},
|
||||
"dgram": {
|
||||
"bare": "bare-dgram",
|
||||
"default": "dgram"
|
||||
},
|
||||
"node:dgram": {
|
||||
"bare": "bare-dgram",
|
||||
"default": "dgram"
|
||||
},
|
||||
"dns": {
|
||||
"bare": "bare-dns",
|
||||
"default": "dns"
|
||||
},
|
||||
"node:dns": {
|
||||
"bare": "bare-dns",
|
||||
"default": "dns"
|
||||
},
|
||||
"dns/promises": {
|
||||
"bare": "bare-dns/promises",
|
||||
"default": "dns/promises"
|
||||
},
|
||||
"node:dns/promises": {
|
||||
"bare": "bare-dns/promises",
|
||||
"default": "dns/promises"
|
||||
},
|
||||
"events": {
|
||||
"bare": "bare-events",
|
||||
"default": "events"
|
||||
},
|
||||
"node:events": {
|
||||
"bare": "bare-events",
|
||||
"default": "events"
|
||||
},
|
||||
"fs": {
|
||||
"bare": "bare-fs",
|
||||
"default": "fs"
|
||||
},
|
||||
"node:fs": {
|
||||
"bare": "bare-fs",
|
||||
"default": "fs"
|
||||
},
|
||||
"fs/promises": {
|
||||
"bare": "bare-fs/promises",
|
||||
"default": "fs/promises"
|
||||
},
|
||||
"node:fs/promises": {
|
||||
"bare": "bare-fs/promises",
|
||||
"default": "fs/promises"
|
||||
},
|
||||
"http": {
|
||||
"bare": "bare-http1",
|
||||
"default": "http"
|
||||
},
|
||||
"node:http": {
|
||||
"bare": "bare-http1",
|
||||
"default": "http"
|
||||
},
|
||||
"https": {
|
||||
"bare": "bare-https",
|
||||
"default": "https"
|
||||
},
|
||||
"node:https": {
|
||||
"bare": "bare-https",
|
||||
"default": "https"
|
||||
},
|
||||
"module": {
|
||||
"bare": "bare-module",
|
||||
"default": "module"
|
||||
},
|
||||
"node:module": {
|
||||
"bare": "bare-module",
|
||||
"default": "module"
|
||||
},
|
||||
"net": {
|
||||
"bare": "bare-net",
|
||||
"default": "net"
|
||||
},
|
||||
"node:net": {
|
||||
"bare": "bare-net",
|
||||
"default": "net"
|
||||
},
|
||||
"os": {
|
||||
"bare": "bare-os",
|
||||
"default": "os"
|
||||
},
|
||||
"node:os": {
|
||||
"bare": "bare-os",
|
||||
"default": "os"
|
||||
},
|
||||
"path": {
|
||||
"bare": "bare-path",
|
||||
"default": "path"
|
||||
},
|
||||
"node:path": {
|
||||
"bare": "bare-path",
|
||||
"default": "path"
|
||||
},
|
||||
"path/posix": {
|
||||
"bare": "bare-path/posix",
|
||||
"default": "path/posix"
|
||||
},
|
||||
"node:path/posix": {
|
||||
"bare": "bare-path/posix",
|
||||
"default": "path/posix"
|
||||
},
|
||||
"path/win32": {
|
||||
"bare": "bare-path/win32",
|
||||
"default": "path/win32"
|
||||
},
|
||||
"node:path/win32": {
|
||||
"bare": "bare-path/win32",
|
||||
"default": "path/win32"
|
||||
},
|
||||
"perf_hooks": {
|
||||
"bare": "bare-performance",
|
||||
"default": "perf_hooks"
|
||||
},
|
||||
"node:perf_hooks": {
|
||||
"bare": "bare-performance",
|
||||
"default": "perf_hooks"
|
||||
},
|
||||
"process": {
|
||||
"bare": "bare-process",
|
||||
"default": "process"
|
||||
},
|
||||
"node:process": {
|
||||
"bare": "bare-process",
|
||||
"default": "process"
|
||||
},
|
||||
"querystring": {
|
||||
"bare": "bare-querystring",
|
||||
"default": "querystring"
|
||||
},
|
||||
"node:querystring": {
|
||||
"bare": "bare-querystring",
|
||||
"default": "querystring"
|
||||
},
|
||||
"stream": {
|
||||
"bare": "bare-stream",
|
||||
"default": "stream"
|
||||
},
|
||||
"node:stream": {
|
||||
"bare": "bare-stream",
|
||||
"default": "stream"
|
||||
},
|
||||
"stream/consumers": {
|
||||
"bare": "bare-stream/consumers",
|
||||
"default": "stream/consumers"
|
||||
},
|
||||
"node:stream/consumers": {
|
||||
"bare": "bare-stream/consumers",
|
||||
"default": "stream/consumers"
|
||||
},
|
||||
"stream/promises": {
|
||||
"bare": "bare-stream/promises",
|
||||
"default": "stream/promises"
|
||||
},
|
||||
"node:stream/promises": {
|
||||
"bare": "bare-stream/promises",
|
||||
"default": "stream/promises"
|
||||
},
|
||||
"stream/web": {
|
||||
"bare": "bare-stream/web",
|
||||
"default": "stream/web"
|
||||
},
|
||||
"node:stream/web": {
|
||||
"bare": "bare-stream/web",
|
||||
"default": "stream/web"
|
||||
},
|
||||
"string_decoder": {
|
||||
"bare": "bare-string-decoder",
|
||||
"default": "string_decoder"
|
||||
},
|
||||
"node:string_decoder": {
|
||||
"bare": "bare-string-decoder",
|
||||
"default": "string_decoder"
|
||||
},
|
||||
"timers": {
|
||||
"bare": "bare-timers",
|
||||
"default": "timers"
|
||||
},
|
||||
"node:timers": {
|
||||
"bare": "bare-timers",
|
||||
"default": "timers"
|
||||
},
|
||||
"timers/promises": {
|
||||
"bare": "bare-timers/promises",
|
||||
"default": "timers/promises"
|
||||
},
|
||||
"node:timers/promises": {
|
||||
"bare": "bare-timers/promises",
|
||||
"default": "timers/promises"
|
||||
},
|
||||
"tls": {
|
||||
"bare": "bare-tls",
|
||||
"default": "tls"
|
||||
},
|
||||
"node:tls": {
|
||||
"bare": "bare-tls",
|
||||
"default": "tls"
|
||||
},
|
||||
"tty": {
|
||||
"bare": "bare-tty",
|
||||
"default": "tty"
|
||||
},
|
||||
"node:tty": {
|
||||
"bare": "bare-tty",
|
||||
"default": "tty"
|
||||
},
|
||||
"url": {
|
||||
"bare": "bare-url",
|
||||
"default": "url"
|
||||
},
|
||||
"node:url": {
|
||||
"bare": "bare-url",
|
||||
"default": "url"
|
||||
},
|
||||
"util": {
|
||||
"bare": "bare-utils",
|
||||
"default": "util"
|
||||
},
|
||||
"node:util": {
|
||||
"bare": "bare-utils",
|
||||
"default": "util"
|
||||
},
|
||||
"util/types": {
|
||||
"bare": "bare-utils/types",
|
||||
"default": "util/types"
|
||||
},
|
||||
"node:util/types": {
|
||||
"bare": "bare-utils/types",
|
||||
"default": "util/types"
|
||||
},
|
||||
"worker_threads": {
|
||||
"bare": "bare-worker",
|
||||
"default": "worker_threads"
|
||||
},
|
||||
"node:worker_threads": {
|
||||
"bare": "bare-worker",
|
||||
"default": "worker_threads"
|
||||
},
|
||||
"zlib": {
|
||||
"bare": "bare-zlib",
|
||||
"default": "zlib"
|
||||
},
|
||||
"node:zlib": {
|
||||
"bare": "bare-zlib",
|
||||
"default": "zlib"
|
||||
},
|
||||
"cluster": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "cluster"
|
||||
},
|
||||
"node:cluster": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "cluster"
|
||||
},
|
||||
"constants": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "constants"
|
||||
},
|
||||
"node:constants": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "constants"
|
||||
},
|
||||
"domain": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "domain"
|
||||
},
|
||||
"node:domain": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "domain"
|
||||
},
|
||||
"http2": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "http2"
|
||||
},
|
||||
"node:http2": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "http2"
|
||||
},
|
||||
"sea": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "sea"
|
||||
},
|
||||
"node:sea": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "sea"
|
||||
},
|
||||
"sys": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "sys"
|
||||
},
|
||||
"node:sys": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "sys"
|
||||
},
|
||||
"test": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "test"
|
||||
},
|
||||
"node:test": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "test"
|
||||
},
|
||||
"test/reporters": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "test/reporters"
|
||||
},
|
||||
"node:test/reporters": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "test/reporters"
|
||||
},
|
||||
"trace_events": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "trace_events"
|
||||
},
|
||||
"node:trace_events": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "trace_events"
|
||||
},
|
||||
"wasi": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "wasi"
|
||||
},
|
||||
"node:wasi": {
|
||||
"bare": "bare-node-runtime/unsupported",
|
||||
"default": "wasi"
|
||||
},
|
||||
"fs/*": {
|
||||
"bare": "bare-fs/*",
|
||||
"default": "fs/*"
|
||||
},
|
||||
"node:fs/*": {
|
||||
"bare": "bare-fs/*",
|
||||
"default": "fs/*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Generate Hyperschema + HyperDB definitions for PearData.
|
||||
*
|
||||
* Usage: node scripts/build-db.js
|
||||
*
|
||||
* SCHEMA SAFETY (Holepunch rules):
|
||||
* 1. Fields are append-only — deprecate instead of removing
|
||||
* 2. Do not delete/reset committed files under spec/
|
||||
* 3. After merge to main, schema changes are permanent
|
||||
*/
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import Hyperschema from 'hyperschema'
|
||||
import HyperDB from 'hyperdb/builder'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = path.join(__dirname, '..')
|
||||
const SCHEMA_DIR = path.join(ROOT, 'spec', 'hyperschema')
|
||||
const DB_DIR = path.join(ROOT, 'spec', 'hyperdb')
|
||||
|
||||
setupSchema()
|
||||
setupDb()
|
||||
console.log('Wrote', SCHEMA_DIR)
|
||||
console.log('Wrote', DB_DIR)
|
||||
|
||||
function setupSchema() {
|
||||
const schema = Hyperschema.from(SCHEMA_DIR)
|
||||
const ns = schema.namespace('peardata')
|
||||
|
||||
ns.register({
|
||||
name: 'node',
|
||||
fields: [
|
||||
{ name: 'nodeId', type: 'string', required: true },
|
||||
{ name: 'hostname', type: 'string', required: true },
|
||||
{ name: 'publicKeyHex', type: 'string', required: false },
|
||||
{ name: 'platform', type: 'string', required: false },
|
||||
{ name: 'arch', type: 'string', required: false },
|
||||
{ name: 'cpus', type: 'uint', required: false },
|
||||
{ name: 'totalMemMiB', type: 'uint', required: false },
|
||||
{ name: 'agentVersion', type: 'string', required: false },
|
||||
{ name: 'labelsJson', type: 'string', required: false },
|
||||
{ name: 'updatedAt', type: 'uint', required: true },
|
||||
],
|
||||
})
|
||||
|
||||
ns.register({
|
||||
name: 'peer-link',
|
||||
fields: [
|
||||
{ name: 'localNodeId', type: 'string', required: true },
|
||||
{ name: 'remotePublicKey', type: 'string', required: true },
|
||||
{ name: 'role', type: 'string', required: false },
|
||||
{ name: 'alias', type: 'string', required: false },
|
||||
{ name: 'dbKeyHex', type: 'string', required: false },
|
||||
{ name: 'syncMode', type: 'string', required: false }, // push | pull | both
|
||||
{ name: 'linkedAt', type: 'uint', required: true },
|
||||
{ name: 'lastSeen', type: 'uint', required: false },
|
||||
],
|
||||
})
|
||||
|
||||
ns.register({
|
||||
name: 'alert-config',
|
||||
fields: [
|
||||
{ name: 'id', type: 'string', required: true },
|
||||
{ name: 'chart', type: 'string', required: true },
|
||||
{ name: 'dimension', type: 'string', required: true },
|
||||
{ name: 'warn', type: 'float64', required: false },
|
||||
{ name: 'crit', type: 'float64', required: false },
|
||||
{ name: 'comparator', type: 'string', required: false },
|
||||
{ name: 'enabled', type: 'bool', required: false },
|
||||
{ name: 'info', type: 'string', required: false },
|
||||
{ name: 'updatedAt', type: 'uint', required: true },
|
||||
],
|
||||
})
|
||||
|
||||
ns.register({
|
||||
name: 'alert-event',
|
||||
fields: [
|
||||
{ name: 'id', type: 'string', required: true },
|
||||
{ name: 'ts', type: 'uint', required: true },
|
||||
{ name: 'chart', type: 'string', required: true },
|
||||
{ name: 'dimension', type: 'string', required: false },
|
||||
{ name: 'severity', type: 'string', required: true },
|
||||
{ name: 'value', type: 'float64', required: false },
|
||||
{ name: 'threshold', type: 'float64', required: false },
|
||||
{ name: 'message', type: 'string', required: false },
|
||||
{ name: 'cleared', type: 'bool', required: false },
|
||||
],
|
||||
})
|
||||
|
||||
// Warm / downsampled metric buckets (NOT 1s hot path)
|
||||
ns.register({
|
||||
name: 'metric-point',
|
||||
fields: [
|
||||
{ name: 'chart', type: 'string', required: true },
|
||||
{ name: 'ts', type: 'uint', required: true }, // unix ms
|
||||
{ name: 'context', type: 'string', required: true },
|
||||
{ name: 'tier', type: 'uint', required: true }, // 1 = ~1m, 2 = future coarser
|
||||
{ name: 'valuesJson', type: 'string', required: true },
|
||||
],
|
||||
})
|
||||
|
||||
ns.register({
|
||||
name: 'job',
|
||||
fields: [
|
||||
{ name: 'id', type: 'string', required: true },
|
||||
{ name: 'name', type: 'string', required: true },
|
||||
{ name: 'status', type: 'string', required: true },
|
||||
{ name: 'startedAt', type: 'uint', required: false },
|
||||
{ name: 'finishedAt', type: 'uint', required: false },
|
||||
{ name: 'resultJson', type: 'string', required: false },
|
||||
{ name: 'error', type: 'string', required: false },
|
||||
],
|
||||
})
|
||||
|
||||
Hyperschema.toDisk(schema, { esm: true })
|
||||
}
|
||||
|
||||
function setupDb() {
|
||||
const db = HyperDB.from(SCHEMA_DIR, DB_DIR)
|
||||
const ns = db.namespace('peardata')
|
||||
|
||||
ns.collections.register({
|
||||
name: 'node',
|
||||
schema: '@peardata/node',
|
||||
key: ['nodeId'],
|
||||
})
|
||||
|
||||
ns.collections.register({
|
||||
name: 'peer-link',
|
||||
schema: '@peardata/peer-link',
|
||||
key: ['localNodeId', 'remotePublicKey'],
|
||||
})
|
||||
|
||||
ns.collections.register({
|
||||
name: 'alert-config',
|
||||
schema: '@peardata/alert-config',
|
||||
key: ['id'],
|
||||
})
|
||||
|
||||
ns.collections.register({
|
||||
name: 'alert-event',
|
||||
schema: '@peardata/alert-event',
|
||||
key: ['id', 'ts'],
|
||||
})
|
||||
|
||||
ns.collections.register({
|
||||
name: 'metric-point',
|
||||
schema: '@peardata/metric-point',
|
||||
key: ['chart', 'ts'],
|
||||
})
|
||||
|
||||
ns.collections.register({
|
||||
name: 'job',
|
||||
schema: '@peardata/job',
|
||||
key: ['id'],
|
||||
})
|
||||
|
||||
ns.indexes.register({
|
||||
name: 'node-by-hostname',
|
||||
collection: '@peardata/node',
|
||||
key: ['hostname'],
|
||||
})
|
||||
|
||||
ns.indexes.register({
|
||||
name: 'peer-link-by-remote',
|
||||
collection: '@peardata/peer-link',
|
||||
key: ['remotePublicKey'],
|
||||
})
|
||||
|
||||
ns.indexes.register({
|
||||
name: 'alert-event-by-chart',
|
||||
collection: '@peardata/alert-event',
|
||||
key: ['chart', 'ts'],
|
||||
})
|
||||
|
||||
ns.indexes.register({
|
||||
name: 'metric-point-by-context',
|
||||
collection: '@peardata/metric-point',
|
||||
key: ['context', 'ts'],
|
||||
})
|
||||
|
||||
ns.indexes.register({
|
||||
name: 'metric-point-by-tier',
|
||||
collection: '@peardata/metric-point',
|
||||
key: ['tier', 'chart', 'ts'],
|
||||
})
|
||||
|
||||
HyperDB.toDisk(db)
|
||||
}
|
||||
@@ -13,6 +13,8 @@ const MUTATING = new Set([
|
||||
'runJob',
|
||||
'cancelJob',
|
||||
'exportSnapshot',
|
||||
'linkPeer',
|
||||
'unlinkPeer',
|
||||
'handshake',
|
||||
])
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Open / close the agent HyperDB (Corestore-backed bee).
|
||||
*
|
||||
* Storage layout (under PEARDATA_DATA_DIR):
|
||||
* data/corestore/ — Corestore rocks/hypercores
|
||||
* Named core: "peardata-meta"
|
||||
*
|
||||
* Disable with PEARDATA_HYPERDB=0
|
||||
*/
|
||||
import path from 'path'
|
||||
import Corestore from 'corestore'
|
||||
import { PearDataModel } from './model.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
const log = logger.child('db')
|
||||
|
||||
/** @type {import('corestore')|null} */
|
||||
let store = null
|
||||
/** @type {PearDataModel|null} */
|
||||
let model = null
|
||||
|
||||
export function isHyperDbEnabled() {
|
||||
const v = process.env.PEARDATA_HYPERDB
|
||||
if (v === '0' || v === 'off' || v === 'false') return false
|
||||
return true
|
||||
}
|
||||
|
||||
export function getDataDir() {
|
||||
return process.env.PEARDATA_DATA_DIR || path.resolve('data')
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<PearDataModel|null>}
|
||||
*/
|
||||
export async function openDb() {
|
||||
if (!isHyperDbEnabled()) {
|
||||
log.info('HyperDB disabled (PEARDATA_HYPERDB=0)')
|
||||
return null
|
||||
}
|
||||
if (model) return model
|
||||
|
||||
const dataDir = getDataDir()
|
||||
const storePath = path.join(dataDir, 'corestore')
|
||||
store = new Corestore(storePath)
|
||||
await store.ready()
|
||||
|
||||
const core = store.get({ name: 'peardata-meta' })
|
||||
model = new PearDataModel(core, { autoUpdate: true })
|
||||
await model.ready()
|
||||
|
||||
log.info('HyperDB ready', {
|
||||
storePath,
|
||||
publicKeyHex: model.publicKeyHex,
|
||||
discoveryKeyHex: model.discoveryKeyHex,
|
||||
})
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {PearDataModel|null}
|
||||
*/
|
||||
export function getDb() {
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import('corestore')|null}
|
||||
*/
|
||||
export function getCorestore() {
|
||||
return store
|
||||
}
|
||||
|
||||
export async function closeDb() {
|
||||
if (model) {
|
||||
await model.close().catch(() => {})
|
||||
model = null
|
||||
}
|
||||
if (store) {
|
||||
await store.close().catch(() => {})
|
||||
store = null
|
||||
}
|
||||
log.info('HyperDB closed')
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* PearDataModel — HyperDB facade for metadata, peer links, alerts, warm metrics.
|
||||
*
|
||||
* Pattern mirrors holepunch hyperdb-workshop Registry:
|
||||
* HyperDB.bee(core, spec, { autoUpdate: true })
|
||||
* exclusiveTransaction / transaction + flush
|
||||
*/
|
||||
import ReadyResource from 'ready-resource'
|
||||
import HyperDB from 'hyperdb'
|
||||
import b4a from 'b4a'
|
||||
import spec from '../../spec/hyperdb/index.js'
|
||||
|
||||
export class PearDataModel extends ReadyResource {
|
||||
/**
|
||||
* @param {import('hypercore')} core
|
||||
* @param {{ autoUpdate?: boolean, writable?: boolean, extension?: boolean }} [opts]
|
||||
*/
|
||||
constructor(core, opts = {}) {
|
||||
super()
|
||||
this.db = HyperDB.bee(core, spec, {
|
||||
autoUpdate: opts.autoUpdate !== false,
|
||||
writable: opts.writable !== false,
|
||||
extension: opts.extension !== false,
|
||||
})
|
||||
}
|
||||
|
||||
get publicKey() {
|
||||
return this.db.core.key
|
||||
}
|
||||
|
||||
get publicKeyHex() {
|
||||
return b4a.toString(this.db.core.key, 'hex')
|
||||
}
|
||||
|
||||
get discoveryKey() {
|
||||
return this.db.core.discoveryKey
|
||||
}
|
||||
|
||||
get discoveryKeyHex() {
|
||||
return b4a.toString(this.db.core.discoveryKey, 'hex')
|
||||
}
|
||||
|
||||
async _open() {
|
||||
await this.db.ready()
|
||||
}
|
||||
|
||||
async _close() {
|
||||
await this.db.close()
|
||||
}
|
||||
|
||||
// ── nodes ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @param {object} node
|
||||
*/
|
||||
async putNode(node) {
|
||||
await this.ready()
|
||||
const tx = await this.db.exclusiveTransaction()
|
||||
try {
|
||||
await tx.insert('@peardata/node', {
|
||||
nodeId: String(node.nodeId),
|
||||
hostname: String(node.hostname || ''),
|
||||
publicKeyHex: node.publicKeyHex || null,
|
||||
platform: node.platform || null,
|
||||
arch: node.arch || null,
|
||||
cpus: node.cpus ?? null,
|
||||
totalMemMiB: node.totalMemMiB != null ? Math.floor(node.totalMemMiB) : null,
|
||||
agentVersion: node.agentVersion || null,
|
||||
labelsJson: node.labels ? JSON.stringify(node.labels) : node.labelsJson || null,
|
||||
updatedAt: node.updatedAt || Date.now(),
|
||||
})
|
||||
await tx.flush()
|
||||
} catch (err) {
|
||||
await tx.close().catch(() => {})
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async getNode(nodeId) {
|
||||
await this.ready()
|
||||
return this.db.get('@peardata/node', { nodeId: String(nodeId) })
|
||||
}
|
||||
|
||||
async listNodes() {
|
||||
await this.ready()
|
||||
return this.db.find('@peardata/node').toArray()
|
||||
}
|
||||
|
||||
// ── peer links ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @param {object} link
|
||||
*/
|
||||
async putPeerLink(link) {
|
||||
await this.ready()
|
||||
const tx = await this.db.exclusiveTransaction()
|
||||
try {
|
||||
await tx.insert('@peardata/peer-link', {
|
||||
localNodeId: String(link.localNodeId),
|
||||
remotePublicKey: String(link.remotePublicKey).toLowerCase(),
|
||||
role: link.role || 'viewer',
|
||||
alias: link.alias || null,
|
||||
dbKeyHex: link.dbKeyHex || null,
|
||||
syncMode: link.syncMode || 'both',
|
||||
linkedAt: link.linkedAt || Date.now(),
|
||||
lastSeen: link.lastSeen || null,
|
||||
})
|
||||
await tx.flush()
|
||||
} catch (err) {
|
||||
await tx.close().catch(() => {})
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async deletePeerLink(localNodeId, remotePublicKey) {
|
||||
await this.ready()
|
||||
const tx = await this.db.exclusiveTransaction()
|
||||
try {
|
||||
await tx.delete('@peardata/peer-link', {
|
||||
localNodeId: String(localNodeId),
|
||||
remotePublicKey: String(remotePublicKey).toLowerCase(),
|
||||
})
|
||||
await tx.flush()
|
||||
} catch (err) {
|
||||
await tx.close().catch(() => {})
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async listPeerLinks(localNodeId = null) {
|
||||
await this.ready()
|
||||
if (!localNodeId) {
|
||||
return this.db.find('@peardata/peer-link').toArray()
|
||||
}
|
||||
return this.db
|
||||
.find('@peardata/peer-link', {
|
||||
gte: { localNodeId: String(localNodeId), remotePublicKey: '' },
|
||||
lte: { localNodeId: String(localNodeId), remotePublicKey: '\uffff' },
|
||||
})
|
||||
.toArray()
|
||||
}
|
||||
|
||||
// ── alert configs ─────────────────────────────────────────
|
||||
|
||||
async putAlertConfig(cfg) {
|
||||
await this.ready()
|
||||
const tx = await this.db.exclusiveTransaction()
|
||||
try {
|
||||
await tx.insert('@peardata/alert-config', {
|
||||
id: String(cfg.id),
|
||||
chart: String(cfg.chart),
|
||||
dimension: String(cfg.dimension),
|
||||
warn: cfg.warn ?? null,
|
||||
crit: cfg.crit ?? null,
|
||||
comparator: cfg.comparator || '>',
|
||||
enabled: cfg.enabled !== false,
|
||||
info: cfg.info || null,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
await tx.flush()
|
||||
} catch (err) {
|
||||
await tx.close().catch(() => {})
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async getAlertConfig(id) {
|
||||
await this.ready()
|
||||
return this.db.get('@peardata/alert-config', { id: String(id) })
|
||||
}
|
||||
|
||||
async listAlertConfigs() {
|
||||
await this.ready()
|
||||
return this.db.find('@peardata/alert-config').toArray()
|
||||
}
|
||||
|
||||
// ── alert events ──────────────────────────────────────────
|
||||
|
||||
async putAlertEvent(ev) {
|
||||
await this.ready()
|
||||
const tx = await this.db.exclusiveTransaction()
|
||||
try {
|
||||
await tx.insert('@peardata/alert-event', {
|
||||
id: String(ev.id),
|
||||
ts: Number(ev.ts) || Date.now(),
|
||||
chart: String(ev.chart || ''),
|
||||
dimension: ev.dimension || null,
|
||||
severity: String(ev.severity || 'warning'),
|
||||
value: ev.value ?? null,
|
||||
threshold: ev.threshold ?? null,
|
||||
message: ev.message || null,
|
||||
cleared: Boolean(ev.cleared),
|
||||
})
|
||||
await tx.flush()
|
||||
} catch (err) {
|
||||
await tx.close().catch(() => {})
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async listAlertEvents({ chart = null, limit = 100 } = {}) {
|
||||
await this.ready()
|
||||
let rows
|
||||
if (chart) {
|
||||
rows = await this.db
|
||||
.find(
|
||||
'@peardata/alert-event-by-chart',
|
||||
{
|
||||
gte: { chart: String(chart), ts: 0 },
|
||||
lte: { chart: String(chart), ts: Number.MAX_SAFE_INTEGER },
|
||||
},
|
||||
{ reverse: true, limit }
|
||||
)
|
||||
.toArray()
|
||||
} else {
|
||||
rows = await this.db
|
||||
.find('@peardata/alert-event', {}, { reverse: true, limit })
|
||||
.toArray()
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// ── warm metric points ────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Batch-insert warm (downsampled) metric points.
|
||||
* @param {Array<{ chart: string, context: string, ts: number, tier?: number, values: object }>} points
|
||||
*/
|
||||
async putMetricPoints(points) {
|
||||
if (!points?.length) return { inserted: 0 }
|
||||
await this.ready()
|
||||
const tx = await this.db.exclusiveTransaction()
|
||||
try {
|
||||
for (const p of points) {
|
||||
await tx.insert('@peardata/metric-point', {
|
||||
chart: String(p.chart),
|
||||
ts: Number(p.ts),
|
||||
context: String(p.context || p.chart),
|
||||
tier: p.tier ?? 1,
|
||||
valuesJson: JSON.stringify(p.values || {}),
|
||||
})
|
||||
}
|
||||
await tx.flush()
|
||||
return { inserted: points.length }
|
||||
} catch (err) {
|
||||
await tx.close().catch(() => {})
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query warm points for a chart in [afterMs, beforeMs].
|
||||
* @param {{ chart: string, afterMs: number, beforeMs: number, limit?: number, tier?: number }} opts
|
||||
*/
|
||||
async queryMetricPoints(opts) {
|
||||
await this.ready()
|
||||
const chart = String(opts.chart)
|
||||
const afterMs = Number(opts.afterMs) || 0
|
||||
const beforeMs = Number(opts.beforeMs) || Date.now()
|
||||
const limit = Math.min(opts.limit || 10_000, 50_000)
|
||||
|
||||
const rows = await this.db
|
||||
.find(
|
||||
'@peardata/metric-point',
|
||||
{
|
||||
gte: { chart, ts: afterMs },
|
||||
lte: { chart, ts: beforeMs },
|
||||
},
|
||||
{ limit }
|
||||
)
|
||||
.toArray()
|
||||
|
||||
return rows
|
||||
.filter((r) => (opts.tier == null ? true : r.tier === opts.tier))
|
||||
.map((r) => ({
|
||||
chart: r.chart,
|
||||
context: r.context,
|
||||
ts: r.ts,
|
||||
tier: r.tier,
|
||||
values: safeJson(r.valuesJson),
|
||||
}))
|
||||
}
|
||||
|
||||
// ── jobs ──────────────────────────────────────────────────
|
||||
|
||||
async putJob(job) {
|
||||
await this.ready()
|
||||
const tx = await this.db.exclusiveTransaction()
|
||||
try {
|
||||
await tx.insert('@peardata/job', {
|
||||
id: String(job.id),
|
||||
name: String(job.name),
|
||||
status: String(job.status),
|
||||
startedAt: job.startedAt ?? null,
|
||||
finishedAt: job.finishedAt ?? null,
|
||||
resultJson: job.result != null ? JSON.stringify(job.result) : job.resultJson || null,
|
||||
error: job.error || null,
|
||||
})
|
||||
await tx.flush()
|
||||
} catch (err) {
|
||||
await tx.close().catch(() => {})
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async listJobs(limit = 50) {
|
||||
await this.ready()
|
||||
const rows = await this.db.find('@peardata/job', {}, { reverse: true, limit }).toArray()
|
||||
return rows
|
||||
}
|
||||
}
|
||||
|
||||
function safeJson(s) {
|
||||
try {
|
||||
return JSON.parse(s || '{}')
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Hyperswarm replication for the agent Corestore / HyperDB.
|
||||
*
|
||||
* Pattern (hyperdb-workshop bin.js):
|
||||
* swarm.on('connection', (conn) => store.replicate(conn))
|
||||
* swarm.join(db.discoveryKey, { server: true, client: true })
|
||||
*
|
||||
* Enable with PEARDATA_SWARM=1 (off by default — Phase B).
|
||||
* Requires HyperDB to be open first.
|
||||
*/
|
||||
import Hyperswarm from 'hyperswarm'
|
||||
import b4a from 'b4a'
|
||||
import { getCorestore, getDb } from './index.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
const log = logger.child('db:replicate')
|
||||
|
||||
/** @type {import('hyperswarm')|null} */
|
||||
let swarm = null
|
||||
|
||||
export function isSwarmEnabled() {
|
||||
const v = process.env.PEARDATA_SWARM
|
||||
return v === '1' || v === 'true' || v === 'on'
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<import('hyperswarm')|null>}
|
||||
*/
|
||||
export async function startReplication() {
|
||||
if (!isSwarmEnabled()) {
|
||||
log.info('Swarm replication disabled (set PEARDATA_SWARM=1 to enable)')
|
||||
return null
|
||||
}
|
||||
|
||||
const store = getCorestore()
|
||||
const model = getDb()
|
||||
if (!store || !model) {
|
||||
log.warn('Cannot start swarm — HyperDB not open')
|
||||
return null
|
||||
}
|
||||
|
||||
if (swarm) return swarm
|
||||
|
||||
swarm = new Hyperswarm({
|
||||
keyPair: await store.createKeyPair('peardata-swarm'),
|
||||
})
|
||||
|
||||
swarm.on('connection', (conn, info) => {
|
||||
const peer = info?.publicKey ? b4a.toString(info.publicKey, 'hex').slice(0, 12) : '?'
|
||||
log.info('Swarm peer connected', { peer })
|
||||
store.replicate(conn)
|
||||
conn.on('close', () => log.info('Swarm peer disconnected', { peer }))
|
||||
conn.on('error', (err) => log.warn('Swarm conn error', { peer, error: err.message }))
|
||||
})
|
||||
|
||||
const topic = model.discoveryKey
|
||||
swarm.join(topic, { server: true, client: true })
|
||||
await swarm.flush()
|
||||
|
||||
log.info('Swarm joined HyperDB topic', {
|
||||
discoveryKeyHex: model.discoveryKeyHex,
|
||||
dbKeyHex: model.publicKeyHex,
|
||||
})
|
||||
return swarm
|
||||
}
|
||||
|
||||
/**
|
||||
* Join an additional remote DB discovery topic (pull mode for linked peers).
|
||||
* @param {string|Buffer} discoveryKeyHexOrBuf
|
||||
*/
|
||||
export async function joinRemoteTopic(discoveryKeyHexOrBuf) {
|
||||
if (!swarm) {
|
||||
throw new Error('Swarm not started — enable PEARDATA_SWARM=1')
|
||||
}
|
||||
const key =
|
||||
typeof discoveryKeyHexOrBuf === 'string'
|
||||
? b4a.from(discoveryKeyHexOrBuf, 'hex')
|
||||
: discoveryKeyHexOrBuf
|
||||
swarm.join(key, { server: false, client: true })
|
||||
log.info('Joined remote topic', {
|
||||
discoveryKeyHex:
|
||||
typeof discoveryKeyHexOrBuf === 'string'
|
||||
? discoveryKeyHexOrBuf
|
||||
: b4a.toString(discoveryKeyHexOrBuf, 'hex'),
|
||||
})
|
||||
}
|
||||
|
||||
export async function stopReplication() {
|
||||
if (!swarm) return
|
||||
try {
|
||||
await swarm.destroy()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
swarm = null
|
||||
log.info('Swarm stopped')
|
||||
}
|
||||
|
||||
export function getSwarm() {
|
||||
return swarm
|
||||
}
|
||||
@@ -10,7 +10,12 @@ import {
|
||||
Roles,
|
||||
} from '../../shared/protocol.js'
|
||||
import { SCHEMA_VERSION } from '../../shared/schema.js'
|
||||
import { CHART_DEFS, CHART_BY_ID, CONTEXT_IDS, chartSummary } from '../../shared/metrics.js'
|
||||
import {
|
||||
getAllChartDefs,
|
||||
getContextIds,
|
||||
CHART_BY_ID,
|
||||
chartSummary,
|
||||
} from '../../shared/metrics.js'
|
||||
import { peers } from '../core/peer-registry.js'
|
||||
import {
|
||||
listPolicyPeers,
|
||||
@@ -37,6 +42,8 @@ import {
|
||||
} from '../services/subscriptions.js'
|
||||
import { getJobs, knownJobNames } from '../services/jobs.js'
|
||||
import { formatAllMetrics } from '../rest/formatters.js'
|
||||
import { getDb } from '../db/index.js'
|
||||
import { joinRemoteTopic, isSwarmEnabled } from '../db/replicate.js'
|
||||
|
||||
/**
|
||||
* @param {import('../rpc/session.js').PeerSession} session
|
||||
@@ -61,11 +68,13 @@ export function registerMonitorHandlers(session) {
|
||||
publicKeyHex: getServerPublicKeyHex(),
|
||||
hostname: os.hostname(),
|
||||
platform: `${os.platform()}/${os.arch()}`,
|
||||
uptimeSec: Math.floor(process.uptime()),
|
||||
uptimeSec: Math.floor(
|
||||
typeof process?.uptime === 'function' ? process.uptime() : 0
|
||||
),
|
||||
connectedPeers: peers.size(),
|
||||
node: process.version,
|
||||
node: typeof process?.version === 'string' ? process.version : 'bare',
|
||||
role: 'agent',
|
||||
charts: CHART_DEFS.length,
|
||||
charts: getAllChartDefs().length,
|
||||
}))
|
||||
|
||||
session.respond('getAuthStatus', async () => ({
|
||||
@@ -88,8 +97,8 @@ export function registerMonitorHandlers(session) {
|
||||
session.respond('getHealth', async () => anomalies.getHealth())
|
||||
|
||||
session.respond('listContexts', async () => ({
|
||||
contexts: CONTEXT_IDS.map((id) => {
|
||||
const charts = CHART_DEFS.filter((c) => c.context === id)
|
||||
contexts: getContextIds().map((id) => {
|
||||
const charts = getAllChartDefs().filter((c) => c.context === id)
|
||||
return {
|
||||
id,
|
||||
family: charts[0]?.family || id.split('.')[0],
|
||||
@@ -100,7 +109,7 @@ export function registerMonitorHandlers(session) {
|
||||
}))
|
||||
|
||||
session.respond('getContext', async (args) => {
|
||||
const charts = CHART_DEFS.filter((c) => c.context === args.id || c.id === args.id)
|
||||
const charts = getAllChartDefs().filter((c) => c.context === args.id || c.id === args.id)
|
||||
if (!charts.length) return { error: 'unknown context', id: args.id }
|
||||
return {
|
||||
id: args.id,
|
||||
@@ -126,6 +135,57 @@ export function registerMonitorHandlers(session) {
|
||||
|
||||
session.respond('queryData', async (args) => store.query(args), { hot: true })
|
||||
|
||||
session.respond('getDbInfo', async () => {
|
||||
const db = getDb()
|
||||
if (!db) return { enabled: false }
|
||||
return {
|
||||
enabled: true,
|
||||
publicKeyHex: db.publicKeyHex,
|
||||
discoveryKeyHex: db.discoveryKeyHex,
|
||||
swarm: isSwarmEnabled(),
|
||||
}
|
||||
})
|
||||
|
||||
session.respond('listPeerLinks', async () => {
|
||||
const db = getDb()
|
||||
if (!db) return { links: [], enabled: false }
|
||||
const links = await db.listPeerLinks(getServerPublicKeyHex().slice(0, 16))
|
||||
return { links, enabled: true }
|
||||
})
|
||||
|
||||
session.respond('linkPeer', async (args) => {
|
||||
const db = getDb()
|
||||
if (!db) return { success: false, error: 'HyperDB disabled' }
|
||||
const localNodeId = getServerPublicKeyHex().slice(0, 16)
|
||||
await db.putPeerLink({
|
||||
localNodeId,
|
||||
remotePublicKey: args.remotePublicKey,
|
||||
role: args.role || 'viewer',
|
||||
alias: args.alias || null,
|
||||
dbKeyHex: args.dbKeyHex || null,
|
||||
syncMode: args.syncMode || 'both',
|
||||
})
|
||||
if (isSwarmEnabled() && args.discoveryKeyHex && (args.syncMode === 'pull' || args.syncMode === 'both')) {
|
||||
try {
|
||||
await joinRemoteTopic(args.discoveryKeyHex)
|
||||
} catch (err) {
|
||||
return {
|
||||
success: true,
|
||||
linked: true,
|
||||
swarmWarning: err.message,
|
||||
}
|
||||
}
|
||||
}
|
||||
return { success: true, linked: true }
|
||||
})
|
||||
|
||||
session.respond('unlinkPeer', async (args) => {
|
||||
const db = getDb()
|
||||
if (!db) return { success: false, error: 'HyperDB disabled' }
|
||||
await db.deletePeerLink(getServerPublicKeyHex().slice(0, 16), args.remotePublicKey)
|
||||
return { success: true }
|
||||
})
|
||||
|
||||
session.respond('getAllMetrics', async (args) => formatAllMetrics(args.format || 'json'))
|
||||
|
||||
session.respond('subscribeMetrics', async (args, s) => subscribeMetrics(s, args), {
|
||||
|
||||
+18
-2
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* Agent data pipeline: collector → store → anomaly → push fan-out.
|
||||
* Agent data pipeline: collector → memory store → anomaly → push
|
||||
* ↘ warm HyperDB flush
|
||||
*/
|
||||
import os from 'os'
|
||||
import { getCollector } from './services/collector.js'
|
||||
@@ -10,18 +11,25 @@ import {
|
||||
broadcastAnomaly,
|
||||
broadcastHealth,
|
||||
} from './services/subscriptions.js'
|
||||
import { enqueueWarmPoint, flushWarmPending, persistAlertEvent } from './services/warm-flush.js'
|
||||
import { getDb } from './db/index.js'
|
||||
import { Pushes } from '../shared/protocol.js'
|
||||
import { peers } from './core/peer-registry.js'
|
||||
import logger from './utils/logger.js'
|
||||
|
||||
const log = logger.child('pipeline')
|
||||
let healthEvery = 0
|
||||
let warmFlushEvery = 0
|
||||
|
||||
export function startPipeline() {
|
||||
const collector = getCollector()
|
||||
const store = getStore()
|
||||
const anomalies = getAnomalyEngine(os.cpus().length)
|
||||
|
||||
store.on('warm', (point) => {
|
||||
enqueueWarmPoint(point)
|
||||
})
|
||||
|
||||
collector.on('samples', (batch) => {
|
||||
store.ingest(batch)
|
||||
broadcastMetrics(batch)
|
||||
@@ -29,6 +37,7 @@ export function startPipeline() {
|
||||
const fired = anomalies.evaluate(batch)
|
||||
for (const ev of fired) {
|
||||
broadcastAnomaly(ev)
|
||||
persistAlertEvent(ev)
|
||||
if (!ev.cleared) {
|
||||
peers.broadcast(Pushes.alert, {
|
||||
id: ev.id,
|
||||
@@ -44,9 +53,16 @@ export function startPipeline() {
|
||||
healthEvery = 0
|
||||
broadcastHealth(anomalies.getHealth())
|
||||
}
|
||||
|
||||
// flush warm HyperDB batch every ~10s
|
||||
warmFlushEvery++
|
||||
if (warmFlushEvery >= 10) {
|
||||
warmFlushEvery = 0
|
||||
flushWarmPending().catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
collector.start()
|
||||
log.info('Metrics pipeline started')
|
||||
log.info('Metrics pipeline started', { hyperdb: Boolean(getDb()) })
|
||||
return { collector, store, anomalies }
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ function toShell(latest) {
|
||||
for (const [chart, point] of Object.entries(latest)) {
|
||||
for (const [dim, val] of Object.entries(point.values)) {
|
||||
if (val == null || Number.isNaN(val)) continue
|
||||
lines.push(`NETDATA_${chart.replace(/\./g, '_').toUpperCase()}_${dim.toUpperCase()}="${val}"`)
|
||||
lines.push(`PEARDATA_${chart.replace(/\./g, '_').toUpperCase()}_${dim.toUpperCase()}="${val}"`)
|
||||
}
|
||||
}
|
||||
return lines.join('\n') + '\n'
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* Optional Netdata-style HTTP API bound to the PearMonitor agent.
|
||||
* Optional agent-style HTTP API bound to the PearMonitor agent.
|
||||
*
|
||||
* Default: 127.0.0.1:19999 (Netdata's classic port) — local only.
|
||||
* Default: 127.0.0.1:19999 — local only.
|
||||
* Disable with PEARDATA_REST=0
|
||||
*/
|
||||
import http from 'http'
|
||||
import b4a from 'b4a'
|
||||
import { handleRest } from './routes.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
@@ -22,7 +23,7 @@ export function startRestServer() {
|
||||
const host = process.env.PEARDATA_REST_HOST || '127.0.0.1'
|
||||
const port = Number(process.env.PEARDATA_REST_PORT) || 19999
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const url = new URL(req.url || '/', `http://${host}:${port}`)
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS') {
|
||||
@@ -36,7 +37,7 @@ export function startRestServer() {
|
||||
return
|
||||
}
|
||||
|
||||
const result = handleRest(url.pathname, url.searchParams)
|
||||
const result = await handleRest(url.pathname, url.searchParams)
|
||||
const headers = {
|
||||
...corsHeaders(),
|
||||
'content-type': result.contentType || 'application/json',
|
||||
@@ -47,7 +48,7 @@ export function startRestServer() {
|
||||
typeof result.body === 'string' ? result.body : JSON.stringify(result.body, null, 0)
|
||||
|
||||
if (req.method === 'HEAD') {
|
||||
headers['content-length'] = Buffer.byteLength(body)
|
||||
headers['content-length'] = b4a.byteLength(body)
|
||||
res.writeHead(result.status || 200, headers)
|
||||
res.end()
|
||||
return
|
||||
|
||||
+55
-18
@@ -1,12 +1,16 @@
|
||||
/**
|
||||
* Netdata-compatible REST route handlers (v1 + v2 + v3).
|
||||
* Agent REST route handlers (v1 + v2 + v3).
|
||||
*
|
||||
* Local-agent style GET endpoints. Not a byte-for-byte clone of every
|
||||
* Netdata field, but intentional compatibility for charts/data/contexts/nodes/info/allmetrics.
|
||||
* Local-agent style GET endpoints for charts/data/contexts/nodes/info/allmetrics.
|
||||
*/
|
||||
import os from 'os'
|
||||
import { APP_NAME, APP_VERSION, PROTOCOL, PROTOCOL_VERSION } from '../../shared/protocol.js'
|
||||
import { CHART_DEFS, CHART_BY_ID, CONTEXT_IDS, chartSummary } from '../../shared/metrics.js'
|
||||
import {
|
||||
getAllChartDefs,
|
||||
getContextIds,
|
||||
CHART_BY_ID,
|
||||
chartSummary,
|
||||
} from '../../shared/metrics.js'
|
||||
import { getStore } from '../services/store.js'
|
||||
import { getCollector } from '../services/collector.js'
|
||||
import { getAnomalyEngine } from '../services/anomaly.js'
|
||||
@@ -14,13 +18,15 @@ import { listAlerts, getAlert } from '../services/alerts.js'
|
||||
import { getServerPublicKeyHex } from '../core/auth-keys.js'
|
||||
import { formatAllMetrics } from './formatters.js'
|
||||
import { peers } from '../core/peer-registry.js'
|
||||
import { getDb, isHyperDbEnabled } from '../db/index.js'
|
||||
import { isSwarmEnabled } from '../db/replicate.js'
|
||||
|
||||
/**
|
||||
* @param {string} pathname
|
||||
* @param {URLSearchParams} query
|
||||
* @returns {{ status: number, contentType: string, body: any }}
|
||||
*/
|
||||
export function handleRest(pathname, query) {
|
||||
export async function handleRest(pathname, query) {
|
||||
const path = pathname.replace(/\/+$/, '') || '/'
|
||||
|
||||
// ── info / versions ──────────────────────────────────────
|
||||
@@ -57,8 +63,8 @@ export function handleRest(pathname, query) {
|
||||
if (path === '/api/v2/contexts' || path === '/api/v3/contexts') {
|
||||
return json({
|
||||
contexts: Object.fromEntries(
|
||||
CONTEXT_IDS.map((id) => {
|
||||
const charts = CHART_DEFS.filter((c) => c.context === id)
|
||||
getContextIds().map((id) => {
|
||||
const charts = getAllChartDefs().filter((c) => c.context === id)
|
||||
return [
|
||||
id,
|
||||
{
|
||||
@@ -74,7 +80,7 @@ export function handleRest(pathname, query) {
|
||||
}
|
||||
if (path === '/api/v3/context' || path === '/api/v2/context') {
|
||||
const id = query.get('context') || query.get('id') || ''
|
||||
const charts = CHART_DEFS.filter((c) => c.context === id || c.id === id)
|
||||
const charts = getAllChartDefs().filter((c) => c.context === id || c.id === id)
|
||||
if (!charts.length) return err(404, 'unknown context')
|
||||
return json({
|
||||
id,
|
||||
@@ -113,8 +119,8 @@ export function handleRest(pathname, query) {
|
||||
const chartId = chart.split(',')[0].trim()
|
||||
const resolved = CHART_BY_ID.has(chartId)
|
||||
? chartId
|
||||
: CHART_DEFS.find((c) => c.context === chartId)?.id || chartId
|
||||
const result = getStore().query({
|
||||
: getAllChartDefs().find((c) => c.context === chartId)?.id || chartId
|
||||
const result = await getStore().query({
|
||||
chart: resolved,
|
||||
after: num(query.get('after'), -60),
|
||||
before: num(query.get('before'), 0),
|
||||
@@ -150,14 +156,16 @@ export function handleRest(pathname, query) {
|
||||
}
|
||||
if (path === '/api/v3/q' || path === '/api/v2/q') {
|
||||
const q = (query.get('q') || query.get('query') || '').toLowerCase()
|
||||
const hits = CHART_DEFS.filter(
|
||||
(c) =>
|
||||
!q ||
|
||||
c.id.includes(q) ||
|
||||
c.title.toLowerCase().includes(q) ||
|
||||
c.context.includes(q) ||
|
||||
c.family.includes(q)
|
||||
).map((c) => ({ type: 'chart', id: c.id, title: c.title, context: c.context }))
|
||||
const hits = getAllChartDefs()
|
||||
.filter(
|
||||
(c) =>
|
||||
!q ||
|
||||
c.id.includes(q) ||
|
||||
c.title.toLowerCase().includes(q) ||
|
||||
c.context.includes(q) ||
|
||||
c.family.includes(q)
|
||||
)
|
||||
.map((c) => ({ type: 'chart', id: c.id, title: c.title, context: c.context }))
|
||||
return json({ results: hits, q })
|
||||
}
|
||||
|
||||
@@ -242,6 +250,25 @@ export function handleRest(pathname, query) {
|
||||
if (path === '/api/v1/health' || path === '/health' || path === '/api/v3/health') {
|
||||
return json(getAnomalyEngine().getHealth())
|
||||
}
|
||||
if (path === '/api/v3/db' || path === '/api/v2/db') {
|
||||
const db = getDb()
|
||||
if (!db) return json({ enabled: false, hyperdb: isHyperDbEnabled() })
|
||||
return json({
|
||||
enabled: true,
|
||||
publicKeyHex: db.publicKeyHex,
|
||||
discoveryKeyHex: db.discoveryKeyHex,
|
||||
swarm: isSwarmEnabled(),
|
||||
collections: [
|
||||
'@peardata/node',
|
||||
'@peardata/peer-link',
|
||||
'@peardata/alert-config',
|
||||
'@peardata/alert-event',
|
||||
'@peardata/metric-point',
|
||||
'@peardata/job',
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
if (path === '/' || path === '/api') {
|
||||
return json({
|
||||
name: APP_NAME,
|
||||
@@ -249,6 +276,9 @@ export function handleRest(pathname, query) {
|
||||
apis: ['/api/v1', '/api/v2', '/api/v3'],
|
||||
docs: 'See docs/REST-API.md',
|
||||
p2p: { protocol: PROTOCOL, publicKeyHex: getServerPublicKeyHex() },
|
||||
hyperdb: getDb()
|
||||
? { publicKeyHex: getDb().publicKeyHex, discoveryKeyHex: getDb().discoveryKeyHex }
|
||||
: null,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -275,6 +305,13 @@ function infoPayload() {
|
||||
protocol: PROTOCOL,
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
publicKeyHex: getServerPublicKeyHex(),
|
||||
hyperdb: getDb()
|
||||
? {
|
||||
publicKeyHex: getDb().publicKeyHex,
|
||||
discoveryKeyHex: getDb().discoveryKeyHex,
|
||||
swarm: isSwarmEnabled(),
|
||||
}
|
||||
: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+37
-2
@@ -2,9 +2,11 @@
|
||||
* PearData / PearMonitor agent entry point.
|
||||
*
|
||||
* - HyperDHT listener + protomux-rpc (P2P control + metric streams)
|
||||
* - Optional Netdata-compatible REST API (default :19999)
|
||||
* - 1s system metrics pipeline
|
||||
* - HyperDB (Corestore) for warm history + metadata
|
||||
* - Optional Hyperswarm replication (PEARDATA_SWARM=1)
|
||||
* - Optional agent-compatible REST API (default :19999)
|
||||
*/
|
||||
import os from 'os'
|
||||
import DHT from 'hyperdht'
|
||||
import b4a from 'b4a'
|
||||
import gracefulGoodbye from 'graceful-goodbye'
|
||||
@@ -16,9 +18,12 @@ import { registerAllHandlers, cleanupSession } from './rpc/register.js'
|
||||
import { isPeerRevoked, loadPeerPolicy } from './core/peer-policy.js'
|
||||
import { isInsecureOpenAdmin } from '../shared/crypto-auth.js'
|
||||
import { APP_NAME, APP_VERSION } from '../shared/protocol.js'
|
||||
import { openDb, closeDb, getDb } from './db/index.js'
|
||||
import { startReplication, stopReplication } from './db/replicate.js'
|
||||
import { startPipeline } from './pipeline.js'
|
||||
import { startRestServer } from './rest/http-server.js'
|
||||
import { getCollector } from './services/collector.js'
|
||||
import { flushWarmPending } from './services/warm-flush.js'
|
||||
import logger from './utils/logger.js'
|
||||
|
||||
const bootStarted = Date.now()
|
||||
@@ -43,6 +48,26 @@ if (isInsecureOpenAdmin()) {
|
||||
)
|
||||
}
|
||||
|
||||
const db = await openDb()
|
||||
if (db) {
|
||||
try {
|
||||
await db.putNode({
|
||||
nodeId: publicKeyHex.slice(0, 16),
|
||||
hostname: os.hostname(),
|
||||
publicKeyHex,
|
||||
platform: os.platform(),
|
||||
arch: os.arch(),
|
||||
cpus: os.cpus().length,
|
||||
totalMemMiB: Math.floor(os.totalmem() / (1024 * 1024)),
|
||||
agentVersion: APP_VERSION,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
} catch (err) {
|
||||
log.warn('Failed to upsert local node record', { error: err.message })
|
||||
}
|
||||
}
|
||||
|
||||
await startReplication()
|
||||
startPipeline()
|
||||
const restServer = startRestServer()
|
||||
|
||||
@@ -91,6 +116,7 @@ await server.listen(keyPair)
|
||||
const bootMs = Date.now() - bootStarted
|
||||
const restPort = Number(process.env.PEARDATA_REST_PORT) || 19999
|
||||
const restHost = process.env.PEARDATA_REST_HOST || '127.0.0.1'
|
||||
const model = getDb()
|
||||
|
||||
logger.banner({
|
||||
title: `${APP_NAME} agent v${APP_VERSION}`,
|
||||
@@ -98,6 +124,8 @@ logger.banner({
|
||||
connect: `Client → dial ${publicKeyHex}`,
|
||||
admin: 'Use SERVER_SEED as admin proof, or mint pd1 invites',
|
||||
rest: restServer ? `http://${restHost}:${restPort}/api/v3/info` : 'disabled',
|
||||
hyperdb: model ? model.publicKeyHex : 'disabled',
|
||||
swarm: process.env.PEARDATA_SWARM === '1' ? 'on' : 'off',
|
||||
insecure: isInsecureOpenAdmin() ? 'OPEN ADMIN (dev)' : 'secure defaults',
|
||||
bootMs: `${bootMs}ms`,
|
||||
})
|
||||
@@ -111,6 +139,11 @@ async function shutdown() {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
await flushWarmPending()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
for (const s of peers.list()) {
|
||||
try {
|
||||
s.destroy()
|
||||
@@ -131,6 +164,8 @@ async function shutdown() {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
await stopReplication()
|
||||
await closeDb()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,26 @@ export const DEFAULT_THRESHOLDS = [
|
||||
enabled: true,
|
||||
info: 'CPU user time high',
|
||||
},
|
||||
{
|
||||
id: 'cpu_iowait_high',
|
||||
chart: 'system.cpu',
|
||||
dimension: 'iowait',
|
||||
warn: 40,
|
||||
crit: 70,
|
||||
comparator: '>',
|
||||
enabled: true,
|
||||
info: 'CPU iowait high',
|
||||
},
|
||||
{
|
||||
id: 'cpu_steal_high',
|
||||
chart: 'system.cpu',
|
||||
dimension: 'steal',
|
||||
warn: 10,
|
||||
crit: 25,
|
||||
comparator: '>',
|
||||
enabled: true,
|
||||
info: 'CPU steal time high (hypervisor contention)',
|
||||
},
|
||||
{
|
||||
id: 'load1_high',
|
||||
chart: 'system.load',
|
||||
|
||||
+880
-181
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,23 @@
|
||||
/**
|
||||
* In-memory tiered metric ring buffers.
|
||||
*
|
||||
* Tier 0: high-res (1s) short retention
|
||||
* Tier 1: downsampled (avg over window) longer retention
|
||||
* Tier 0: high-res (1s) short retention (hot path)
|
||||
* Tier 1: downsampled averages — also flushed to HyperDB warm storage
|
||||
*
|
||||
* Future: Hypercore / disk-backed persistence.
|
||||
* See docs/STORAGE-HYPERDB.md
|
||||
*/
|
||||
import { EventEmitter } from 'events'
|
||||
import { SAMPLE_INTERVAL_MS, CHART_BY_ID, chartSummary } from '../../shared/metrics.js'
|
||||
import { getDb } from '../db/index.js'
|
||||
|
||||
function envInt(name, fallback) {
|
||||
const n = Number(process.env[name])
|
||||
return Number.isFinite(n) && n > 0 ? n : fallback
|
||||
}
|
||||
|
||||
export class MetricStore {
|
||||
export class MetricStore extends EventEmitter {
|
||||
constructor() {
|
||||
super()
|
||||
this.tier0Max = envInt('PEARDATA_TIER0_POINTS', 3600) // 1h @ 1s
|
||||
this.tier1Max = envInt('PEARDATA_TIER1_POINTS', 1440) // 24h @ 1m
|
||||
this.tier1Every = envInt('PEARDATA_TIER1_EVERY', 60) // downsample every N samples
|
||||
@@ -54,12 +57,20 @@ export class MetricStore {
|
||||
for (const [k, v] of Object.entries(entry.acc)) {
|
||||
avg[k] = entry.accCount ? v / entry.accCount : null
|
||||
}
|
||||
entry.tier1.push({ ts: s.ts, values: avg })
|
||||
const warm = { ts: s.ts, values: avg }
|
||||
entry.tier1.push(warm)
|
||||
if (entry.tier1.length > this.tier1Max) {
|
||||
entry.tier1.splice(0, entry.tier1.length - this.tier1Max)
|
||||
}
|
||||
entry.acc = null
|
||||
entry.accCount = 0
|
||||
this.emit('warm', {
|
||||
chart: s.chart,
|
||||
context: s.context,
|
||||
ts: warm.ts,
|
||||
values: warm.values,
|
||||
tier: 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,11 +102,12 @@ export class MetricStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Query points for a chart (Netdata-like after/before/points).
|
||||
* Query points for a chart (after/before/points windowing).
|
||||
* Memory first; optional HyperDB warm fallback when window exceeds hot buffer.
|
||||
*
|
||||
* @param {{ chart: string, after?: number, before?: number, points?: number, group?: string, tier?: number }} opts
|
||||
*/
|
||||
query(opts) {
|
||||
async query(opts) {
|
||||
const chart = opts.chart
|
||||
const entry = this.series.get(chart)
|
||||
const def = CHART_BY_ID.get(chart)
|
||||
@@ -116,10 +128,34 @@ export class MetricStore {
|
||||
const beforeMs = before * 1000
|
||||
|
||||
let windowed = src.filter((p) => p.ts >= afterMs && p.ts <= beforeMs)
|
||||
let source = useTier1 ? 'memory-tier1' : 'memory-tier0'
|
||||
|
||||
// HyperDB warm fallback when memory misses (or explicit tier>=1 with sparse memory)
|
||||
if (!windowed.length || (opts.tier >= 1 && windowed.length < (opts.points || 60) / 2)) {
|
||||
const db = getDb()
|
||||
if (db) {
|
||||
try {
|
||||
const warm = await db.queryMetricPoints({
|
||||
chart,
|
||||
afterMs,
|
||||
beforeMs,
|
||||
limit: opts.points || 10_000,
|
||||
tier: 1,
|
||||
})
|
||||
if (warm.length) {
|
||||
windowed = warm
|
||||
source = 'hyperdb-warm'
|
||||
}
|
||||
} catch {
|
||||
// keep memory result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!windowed.length && src.length) {
|
||||
// fall back to latest N
|
||||
const n = Math.min(opts.points || 60, src.length)
|
||||
windowed = src.slice(-n)
|
||||
source = useTier1 ? 'memory-tier1' : 'memory-tier0'
|
||||
}
|
||||
|
||||
const want = Math.min(opts.points || windowed.length || 60, 10_000)
|
||||
@@ -145,6 +181,7 @@ export class MetricStore {
|
||||
before: before,
|
||||
points: data.length,
|
||||
format: 'json',
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Flush memory tier1 (downsampled) points into HyperDB warm storage.
|
||||
*
|
||||
* Called from the pipeline when the in-memory store rolls a tier1 bucket,
|
||||
* or on a periodic timer as a safety net.
|
||||
*/
|
||||
import { getDb } from '../db/index.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
const log = logger.child('warm-flush')
|
||||
|
||||
/** @type {Array<{ chart: string, context: string, ts: number, values: object }>} */
|
||||
let pending = []
|
||||
let flushing = false
|
||||
|
||||
/**
|
||||
* Queue a warm point for HyperDB persistence.
|
||||
* @param {{ chart: string, context: string, ts: number, values: object }} point
|
||||
*/
|
||||
export function enqueueWarmPoint(point) {
|
||||
pending.push({
|
||||
chart: point.chart,
|
||||
context: point.context || point.chart,
|
||||
ts: point.ts,
|
||||
values: point.values,
|
||||
tier: 1,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush pending warm points to HyperDB.
|
||||
*/
|
||||
export async function flushWarmPending() {
|
||||
const db = getDb()
|
||||
if (!db || !pending.length || flushing) return { inserted: 0 }
|
||||
flushing = true
|
||||
const batch = pending
|
||||
pending = []
|
||||
try {
|
||||
const res = await db.putMetricPoints(batch)
|
||||
log.debug?.('Flushed warm points', res)
|
||||
return res
|
||||
} catch (err) {
|
||||
// re-queue on failure (bounded)
|
||||
pending = batch.concat(pending).slice(-5000)
|
||||
log.error('Warm flush failed', { error: err.message, requeued: batch.length })
|
||||
return { inserted: 0, error: err.message }
|
||||
} finally {
|
||||
flushing = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist an anomaly/alert event to HyperDB.
|
||||
* @param {object} ev
|
||||
*/
|
||||
export async function persistAlertEvent(ev) {
|
||||
const db = getDb()
|
||||
if (!db) return
|
||||
try {
|
||||
await db.putAlertEvent(ev)
|
||||
} catch (err) {
|
||||
log.warn('Failed to persist alert event', { error: err.message })
|
||||
}
|
||||
}
|
||||
+51
-40
@@ -2,6 +2,7 @@
|
||||
* HMAC capability grants + admin seed proof for handshake auth.
|
||||
*
|
||||
* Pure helpers shared by server and client. Never logs secrets.
|
||||
* Uses `b4a` + `crypto` (mapped to bare-crypto under Bare/Pear).
|
||||
*
|
||||
* Capability token format:
|
||||
* base64url(JSON payload) + "." + base64url(HMAC-SHA256(macKey, payloadBytes))
|
||||
@@ -13,32 +14,39 @@
|
||||
* pd1.<base64url JSON { publicKeyHex, capability, role, ... }>
|
||||
*/
|
||||
import crypto from 'crypto'
|
||||
import b4a from 'b4a'
|
||||
import { Roles } from './protocol.js'
|
||||
|
||||
const SALT = Buffer.from('peardata-hmac-v1', 'utf8')
|
||||
const INFO_CAPABILITY = Buffer.from('capability', 'utf8')
|
||||
const ADMIN_PREFIX = Buffer.from('peardata-admin-v1', 'utf8')
|
||||
const SALT = b4a.from('peardata-hmac-v1')
|
||||
const INFO_CAPABILITY = b4a.from('capability')
|
||||
const ADMIN_PREFIX = b4a.from('peardata-admin-v1')
|
||||
const VALID_ROLES = new Set([Roles.viewer, Roles.operator, Roles.admin])
|
||||
|
||||
export const INVITE_PREFIX = 'pd1.'
|
||||
|
||||
function asU8(value) {
|
||||
if (value == null) return b4a.alloc(0)
|
||||
if (value instanceof Uint8Array) return b4a.from(value)
|
||||
return b4a.from(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string|Uint8Array|Buffer} seedHexOrBuf
|
||||
* @returns {Buffer}
|
||||
* @param {string|Uint8Array} seedHexOrBuf
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function deriveMacKey(seedHexOrBuf) {
|
||||
const ikm = toSeedBuffer(seedHexOrBuf)
|
||||
if (typeof crypto.hkdfSync === 'function') {
|
||||
return Buffer.from(crypto.hkdfSync('sha256', ikm, SALT, INFO_CAPABILITY, 32))
|
||||
return asU8(crypto.hkdfSync('sha256', ikm, SALT, INFO_CAPABILITY, 32))
|
||||
}
|
||||
const prk = crypto.createHmac('sha256', SALT).update(ikm).digest()
|
||||
const info = Buffer.concat([INFO_CAPABILITY, Buffer.from([0x01])])
|
||||
return crypto.createHmac('sha256', prk).update(info).digest()
|
||||
const info = b4a.concat([INFO_CAPABILITY, b4a.from([0x01])])
|
||||
return asU8(crypto.createHmac('sha256', prk).update(info).digest())
|
||||
}
|
||||
|
||||
function toSeedBuffer(seedHexOrBuf) {
|
||||
if (Buffer.isBuffer(seedHexOrBuf) || seedHexOrBuf instanceof Uint8Array) {
|
||||
const buf = Buffer.from(seedHexOrBuf)
|
||||
if (seedHexOrBuf instanceof Uint8Array) {
|
||||
const buf = b4a.from(seedHexOrBuf)
|
||||
if (buf.length !== 32) throw new Error('Seed must be 32 bytes')
|
||||
return buf
|
||||
}
|
||||
@@ -48,12 +56,12 @@ function toSeedBuffer(seedHexOrBuf) {
|
||||
if (!/^[0-9a-f]{64}$/.test(hex)) {
|
||||
throw new Error('Seed must be 64 hex characters (32 bytes)')
|
||||
}
|
||||
return Buffer.from(hex, 'hex')
|
||||
return b4a.from(hex, 'hex')
|
||||
}
|
||||
|
||||
export function b64url(buf) {
|
||||
return Buffer.from(buf)
|
||||
.toString('base64')
|
||||
return b4a
|
||||
.toString(asU8(buf), 'base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '')
|
||||
@@ -62,7 +70,7 @@ export function b64url(buf) {
|
||||
export function b64urlDecode(s) {
|
||||
const str = String(s || '').replace(/-/g, '+').replace(/_/g, '/')
|
||||
const pad = str.length % 4 === 0 ? '' : '='.repeat(4 - (str.length % 4))
|
||||
return Buffer.from(str + pad, 'base64')
|
||||
return b4a.from(str + pad, 'base64')
|
||||
}
|
||||
|
||||
export function canonicalizePayload(payload) {
|
||||
@@ -74,32 +82,30 @@ export function canonicalizePayload(payload) {
|
||||
jti: payload.jti,
|
||||
iat: payload.iat,
|
||||
}
|
||||
return Buffer.from(JSON.stringify(ordered), 'utf8')
|
||||
return b4a.from(JSON.stringify(ordered))
|
||||
}
|
||||
|
||||
function resolveMacKey(macKeyOrSeed) {
|
||||
if (Buffer.isBuffer(macKeyOrSeed) || macKeyOrSeed instanceof Uint8Array) {
|
||||
const buf = Buffer.from(macKeyOrSeed)
|
||||
if (macKeyOrSeed instanceof Uint8Array) {
|
||||
const buf = b4a.from(macKeyOrSeed)
|
||||
if (buf.length === 32) return buf
|
||||
return deriveMacKey(macKeyOrSeed)
|
||||
}
|
||||
if (typeof macKeyOrSeed === 'string' && /^[0-9a-fA-F]{64}$/.test(macKeyOrSeed.trim())) {
|
||||
return deriveMacKey(macKeyOrSeed.trim())
|
||||
}
|
||||
if (Buffer.isBuffer(macKeyOrSeed) || macKeyOrSeed instanceof Uint8Array) {
|
||||
return deriveMacKey(macKeyOrSeed)
|
||||
}
|
||||
throw new Error('Invalid mac key or seed')
|
||||
}
|
||||
|
||||
export function safeEqual(a, b) {
|
||||
if (!Buffer.isBuffer(a)) a = Buffer.from(a)
|
||||
if (!Buffer.isBuffer(b)) b = Buffer.from(b)
|
||||
if (a.length !== b.length) return false
|
||||
return crypto.timingSafeEqual(a, b)
|
||||
const aa = asU8(a)
|
||||
const bb = asU8(b)
|
||||
if (aa.length !== bb.length) return false
|
||||
return crypto.timingSafeEqual(aa, bb)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer|string} macKeyOrSeed
|
||||
* @param {Uint8Array|string} macKeyOrSeed
|
||||
* @param {{ role?: string, ttlMs?: number|null, peerId?: string|null, jti?: string, forever?: boolean }} opts
|
||||
*/
|
||||
export function signCapability(macKeyOrSeed, opts = {}) {
|
||||
@@ -124,7 +130,7 @@ export function signCapability(macKeyOrSeed, opts = {}) {
|
||||
role,
|
||||
peerId: opts.peerId ? String(opts.peerId).toLowerCase() : null,
|
||||
exp,
|
||||
jti: opts.jti || crypto.randomBytes(16).toString('hex'),
|
||||
jti: opts.jti || b4a.toString(crypto.randomBytes(16), 'hex'),
|
||||
iat: now,
|
||||
}
|
||||
const body = canonicalizePayload(payload)
|
||||
@@ -133,7 +139,7 @@ export function signCapability(macKeyOrSeed, opts = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer|string} macKeyOrSeed
|
||||
* @param {Uint8Array|string} macKeyOrSeed
|
||||
* @param {string} token
|
||||
* @param {{ peerId?: string, now?: number, allowSpentCheck?: (jti: string) => boolean }} [opts]
|
||||
*/
|
||||
@@ -165,7 +171,7 @@ export function verifyCapability(macKeyOrSeed, token, opts = {}) {
|
||||
|
||||
let payload
|
||||
try {
|
||||
payload = JSON.parse(body.toString('utf8'))
|
||||
payload = JSON.parse(b4a.toString(body, 'utf8'))
|
||||
} catch {
|
||||
return { ok: false, error: 'Capability payload not JSON', code: 'CAPABILITY_INVALID' }
|
||||
}
|
||||
@@ -217,18 +223,18 @@ function hmacAdmin(macKey, nonce, peerId, serverPk) {
|
||||
return crypto
|
||||
.createHmac('sha256', macKey)
|
||||
.update(ADMIN_PREFIX)
|
||||
.update(Buffer.from(nonce, 'utf8'))
|
||||
.update(Buffer.from(peerId, 'utf8'))
|
||||
.update(Buffer.from(serverPk, 'utf8'))
|
||||
.update(b4a.from(nonce))
|
||||
.update(b4a.from(peerId))
|
||||
.update(b4a.from(serverPk))
|
||||
.digest()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer|string} macKeyOrSeed
|
||||
* @param {Uint8Array|string} macKeyOrSeed
|
||||
* @param {{ nonce?: string, peerId: string, serverPublicKeyHex: string }} opts
|
||||
*/
|
||||
export function createAdminProof(macKeyOrSeed, opts) {
|
||||
const nonce = String(opts.nonce || crypto.randomBytes(16).toString('hex'))
|
||||
const nonce = String(opts.nonce || b4a.toString(crypto.randomBytes(16), 'hex'))
|
||||
if (!/^[0-9a-fA-F]{16,64}$/.test(nonce)) {
|
||||
throw new Error('Admin proof nonce must be 16-64 hex characters')
|
||||
}
|
||||
@@ -239,11 +245,11 @@ export function createAdminProof(macKeyOrSeed, opts) {
|
||||
|
||||
const macKey = resolveMacKey(macKeyOrSeed)
|
||||
const mac = hmacAdmin(macKey, nonce, peerId, serverPk)
|
||||
return { nonce: nonce.toLowerCase(), mac: mac.toString('hex') }
|
||||
return { nonce: nonce.toLowerCase(), mac: b4a.toString(asU8(mac), 'hex') }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer|string} macKeyOrSeed
|
||||
* @param {Uint8Array|string} macKeyOrSeed
|
||||
* @param {{ nonce?: string, mac?: string }|null} proof
|
||||
* @param {{ peerId: string, serverPublicKeyHex: string }} ctx
|
||||
*/
|
||||
@@ -264,7 +270,7 @@ export function verifyAdminProof(macKeyOrSeed, proof, ctx) {
|
||||
|
||||
const macKey = resolveMacKey(macKeyOrSeed)
|
||||
const expected = hmacAdmin(macKey, nonce, peerId, serverPk)
|
||||
const got = Buffer.from(macHex, 'hex')
|
||||
const got = b4a.from(macHex, 'hex')
|
||||
if (!safeEqual(got, expected)) {
|
||||
return { ok: false, error: 'Admin proof verification failed', code: 'ADMIN_PROOF_FAILED' }
|
||||
}
|
||||
@@ -291,7 +297,7 @@ export function encodeInvite(pkg) {
|
||||
if (!body.capability || !body.capability.includes('.')) {
|
||||
throw new Error('encodeInvite: invalid capability')
|
||||
}
|
||||
return `${INVITE_PREFIX}${b64url(Buffer.from(JSON.stringify(body), 'utf8'))}`
|
||||
return `${INVITE_PREFIX}${b64url(b4a.from(JSON.stringify(body)))}`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,7 +310,7 @@ export function decodeInvite(invite) {
|
||||
return { ok: false, error: 'Not a pd1 invite', code: 'INVITE_INVALID' }
|
||||
}
|
||||
try {
|
||||
const json = b64urlDecode(s.slice(INVITE_PREFIX.length)).toString('utf8')
|
||||
const json = b4a.toString(b64urlDecode(s.slice(INVITE_PREFIX.length)), 'utf8')
|
||||
const pkg = JSON.parse(json)
|
||||
if (!pkg?.publicKeyHex || !pkg?.capability) {
|
||||
return { ok: false, error: 'Invite missing fields', code: 'INVITE_INVALID' }
|
||||
@@ -343,6 +349,11 @@ export function classifyConnectionInput(input) {
|
||||
}
|
||||
|
||||
export function isInsecureOpenAdmin() {
|
||||
const v = String(process.env.PEARDATA_INSECURE_OPEN_ADMIN || '').toLowerCase()
|
||||
return v === '1' || v === 'true' || v === 'yes'
|
||||
try {
|
||||
const env = typeof process !== 'undefined' ? process.env : null
|
||||
const v = String(env?.PEARDATA_INSECURE_OPEN_ADMIN || '').toLowerCase()
|
||||
return v === '1' || v === 'true' || v === 'yes'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
+718
-69
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Metric contexts, charts, and dimension catalog (Netdata-inspired).
|
||||
* Metric contexts, charts, and dimension catalog.
|
||||
*
|
||||
* Context IDs follow Netdata style: family.metric (e.g. system.cpu).
|
||||
* Chart IDs are unique per agent instance.
|
||||
* Context IDs follow family.metric style (e.g. system.cpu).
|
||||
* Static charts cover host-wide system stats; instance charts
|
||||
* (per-CPU, per-disk, per-iface, per-mount) are registered at runtime.
|
||||
*/
|
||||
|
||||
export const SAMPLE_INTERVAL_MS = 1000
|
||||
@@ -19,11 +20,13 @@ export const SAMPLE_INTERVAL_MS = 1000
|
||||
* chartType: 'line'|'area'|'stacked',
|
||||
* priority: number,
|
||||
* dimensions: DimensionDef[],
|
||||
* plugin?: string,
|
||||
* }} ChartDef
|
||||
*/
|
||||
|
||||
/** @type {ChartDef[]} */
|
||||
export const CHART_DEFS = [
|
||||
export const STATIC_CHART_DEFS = [
|
||||
// ── CPU / scheduler ───────────────────────────────────────
|
||||
{
|
||||
id: 'system.cpu',
|
||||
name: 'system.cpu',
|
||||
@@ -33,74 +36,55 @@ export const CHART_DEFS = [
|
||||
family: 'cpu',
|
||||
chartType: 'stacked',
|
||||
priority: 100,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'guest_nice', name: 'guest_nice', algorithm: 'absolute' },
|
||||
{ id: 'guest', name: 'guest', algorithm: 'absolute' },
|
||||
{ id: 'steal', name: 'steal', algorithm: 'absolute' },
|
||||
{ id: 'softirq', name: 'softirq', algorithm: 'absolute' },
|
||||
{ id: 'irq', name: 'irq', algorithm: 'absolute' },
|
||||
{ id: 'user', name: 'user', algorithm: 'absolute' },
|
||||
{ id: 'system', name: 'system', algorithm: 'absolute' },
|
||||
{ id: 'nice', name: 'nice', algorithm: 'absolute' },
|
||||
{ id: 'iowait', name: 'iowait', algorithm: 'absolute' },
|
||||
{ id: 'irq', name: 'irq', algorithm: 'absolute' },
|
||||
{ id: 'softirq', name: 'softirq', algorithm: 'absolute' },
|
||||
{ id: 'idle', name: 'idle', algorithm: 'absolute' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'system.ram',
|
||||
name: 'system.ram',
|
||||
context: 'system.ram',
|
||||
title: 'System RAM',
|
||||
units: 'MiB',
|
||||
family: 'memory',
|
||||
chartType: 'stacked',
|
||||
priority: 200,
|
||||
dimensions: [
|
||||
{ id: 'used', name: 'used', algorithm: 'absolute' },
|
||||
{ id: 'cached', name: 'cached', algorithm: 'absolute' },
|
||||
{ id: 'buffers', name: 'buffers', algorithm: 'absolute' },
|
||||
{ id: 'free', name: 'free', algorithm: 'absolute' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'system.load',
|
||||
name: 'system.load',
|
||||
context: 'system.load',
|
||||
title: 'System Load Average',
|
||||
units: 'load',
|
||||
family: 'load',
|
||||
id: 'system.intr',
|
||||
name: 'system.intr',
|
||||
context: 'system.intr',
|
||||
title: 'CPU Interrupts',
|
||||
units: 'interrupts/s',
|
||||
family: 'interrupts',
|
||||
chartType: 'line',
|
||||
priority: 300,
|
||||
dimensions: [
|
||||
{ id: 'load1', name: 'load1', algorithm: 'absolute' },
|
||||
{ id: 'load5', name: 'load5', algorithm: 'absolute' },
|
||||
{ id: 'load15', name: 'load15', algorithm: 'absolute' },
|
||||
],
|
||||
priority: 110,
|
||||
plugin: 'proc',
|
||||
dimensions: [{ id: 'interrupts', name: 'interrupts', algorithm: 'incremental' }],
|
||||
},
|
||||
{
|
||||
id: 'system.io',
|
||||
name: 'system.io',
|
||||
context: 'system.io',
|
||||
title: 'Disk I/O',
|
||||
units: 'KiB/s',
|
||||
family: 'disk',
|
||||
chartType: 'area',
|
||||
priority: 400,
|
||||
dimensions: [
|
||||
{ id: 'reads', name: 'reads', algorithm: 'incremental' },
|
||||
{ id: 'writes', name: 'writes', algorithm: 'incremental' },
|
||||
],
|
||||
id: 'system.ctxt',
|
||||
name: 'system.ctxt',
|
||||
context: 'system.ctxt',
|
||||
title: 'CPU Context Switches',
|
||||
units: 'context switches/s',
|
||||
family: 'processes',
|
||||
chartType: 'line',
|
||||
priority: 120,
|
||||
plugin: 'proc',
|
||||
dimensions: [{ id: 'switches', name: 'switches', algorithm: 'incremental' }],
|
||||
},
|
||||
{
|
||||
id: 'system.net',
|
||||
name: 'system.net',
|
||||
context: 'system.net',
|
||||
title: 'Bandwidth',
|
||||
units: 'kilobits/s',
|
||||
family: 'network',
|
||||
chartType: 'area',
|
||||
priority: 500,
|
||||
dimensions: [
|
||||
{ id: 'received', name: 'received', algorithm: 'incremental' },
|
||||
{ id: 'sent', name: 'sent', algorithm: 'incremental' },
|
||||
],
|
||||
id: 'system.forks',
|
||||
name: 'system.forks',
|
||||
context: 'system.forks',
|
||||
title: 'Started Processes',
|
||||
units: 'processes/s',
|
||||
family: 'processes',
|
||||
chartType: 'line',
|
||||
priority: 130,
|
||||
plugin: 'proc',
|
||||
dimensions: [{ id: 'started', name: 'started', algorithm: 'incremental' }],
|
||||
},
|
||||
{
|
||||
id: 'system.processes',
|
||||
@@ -110,11 +94,39 @@ export const CHART_DEFS = [
|
||||
units: 'processes',
|
||||
family: 'processes',
|
||||
chartType: 'line',
|
||||
priority: 600,
|
||||
priority: 140,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'running', name: 'running', algorithm: 'absolute' },
|
||||
{ id: 'blocked', name: 'blocked', algorithm: 'absolute' },
|
||||
{ id: 'total', name: 'total', algorithm: 'absolute' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'system.active_processes',
|
||||
name: 'system.active_processes',
|
||||
context: 'system.active_processes',
|
||||
title: 'System Active Processes',
|
||||
units: 'processes',
|
||||
family: 'processes',
|
||||
chartType: 'line',
|
||||
priority: 145,
|
||||
plugin: 'proc',
|
||||
dimensions: [{ id: 'active', name: 'active', algorithm: 'absolute' }],
|
||||
},
|
||||
{
|
||||
id: 'system.load',
|
||||
name: 'system.load',
|
||||
context: 'system.load',
|
||||
title: 'System Load Average',
|
||||
units: 'load',
|
||||
family: 'load',
|
||||
chartType: 'line',
|
||||
priority: 150,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'load1', name: 'load1', algorithm: 'absolute' },
|
||||
{ id: 'load5', name: 'load5', algorithm: 'absolute' },
|
||||
{ id: 'load15', name: 'load15', algorithm: 'absolute' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -125,37 +137,673 @@ export const CHART_DEFS = [
|
||||
units: 'seconds',
|
||||
family: 'uptime',
|
||||
chartType: 'line',
|
||||
priority: 700,
|
||||
priority: 160,
|
||||
plugin: 'proc',
|
||||
dimensions: [{ id: 'uptime', name: 'uptime', algorithm: 'absolute' }],
|
||||
},
|
||||
{
|
||||
id: 'system.entropy',
|
||||
name: 'system.entropy',
|
||||
context: 'system.entropy',
|
||||
title: 'Available Entropy',
|
||||
units: 'entropy',
|
||||
family: 'entropy',
|
||||
chartType: 'line',
|
||||
priority: 170,
|
||||
plugin: 'proc',
|
||||
dimensions: [{ id: 'entropy', name: 'entropy', algorithm: 'absolute' }],
|
||||
},
|
||||
|
||||
// ── Memory ────────────────────────────────────────────────
|
||||
{
|
||||
id: 'system.ram',
|
||||
name: 'system.ram',
|
||||
context: 'system.ram',
|
||||
title: 'System RAM',
|
||||
units: 'MiB',
|
||||
family: 'ram',
|
||||
chartType: 'stacked',
|
||||
priority: 200,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'free', name: 'free', algorithm: 'absolute' },
|
||||
{ id: 'used', name: 'used', algorithm: 'absolute' },
|
||||
{ id: 'cached', name: 'cached', algorithm: 'absolute' },
|
||||
{ id: 'buffers', name: 'buffers', algorithm: 'absolute' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mem.available',
|
||||
name: 'mem.available',
|
||||
context: 'mem.available',
|
||||
title: 'Available RAM',
|
||||
title: 'Available RAM for applications',
|
||||
units: 'MiB',
|
||||
family: 'memory',
|
||||
family: 'ram',
|
||||
chartType: 'area',
|
||||
priority: 210,
|
||||
plugin: 'proc',
|
||||
dimensions: [{ id: 'avail', name: 'avail', algorithm: 'absolute' }],
|
||||
},
|
||||
{
|
||||
id: 'mem.swap',
|
||||
name: 'mem.swap',
|
||||
context: 'mem.swap',
|
||||
title: 'System Swap',
|
||||
units: 'MiB',
|
||||
family: 'swap',
|
||||
chartType: 'stacked',
|
||||
priority: 220,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'free', name: 'free', algorithm: 'absolute' },
|
||||
{ id: 'used', name: 'used', algorithm: 'absolute' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mem.swap_cached',
|
||||
name: 'mem.swap_cached',
|
||||
context: 'mem.swap_cached',
|
||||
title: 'Swap Cached',
|
||||
units: 'MiB',
|
||||
family: 'swap',
|
||||
chartType: 'area',
|
||||
priority: 225,
|
||||
plugin: 'proc',
|
||||
dimensions: [{ id: 'cached', name: 'cached', algorithm: 'absolute' }],
|
||||
},
|
||||
{
|
||||
id: 'mem.kernel',
|
||||
name: 'mem.kernel',
|
||||
context: 'mem.kernel',
|
||||
title: 'Memory Used by Kernel',
|
||||
units: 'MiB',
|
||||
family: 'kernel',
|
||||
chartType: 'stacked',
|
||||
priority: 230,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'slab', name: 'slab', algorithm: 'absolute' },
|
||||
{ id: 'kernel_stack', name: 'kernel_stack', algorithm: 'absolute' },
|
||||
{ id: 'page_tables', name: 'page_tables', algorithm: 'absolute' },
|
||||
{ id: 'vmalloc_used', name: 'vmalloc_used', algorithm: 'absolute' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mem.slab',
|
||||
name: 'mem.slab',
|
||||
context: 'mem.slab',
|
||||
title: 'Reclaimable Kernel Memory',
|
||||
units: 'MiB',
|
||||
family: 'slab',
|
||||
chartType: 'stacked',
|
||||
priority: 235,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'reclaimable', name: 'reclaimable', algorithm: 'absolute' },
|
||||
{ id: 'unreclaimable', name: 'unreclaimable', algorithm: 'absolute' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mem.writeback',
|
||||
name: 'mem.writeback',
|
||||
context: 'mem.writeback',
|
||||
title: 'Writeback Memory',
|
||||
units: 'MiB',
|
||||
family: 'writeback',
|
||||
chartType: 'line',
|
||||
priority: 240,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'dirty', name: 'dirty', algorithm: 'absolute' },
|
||||
{ id: 'writeback', name: 'writeback', algorithm: 'absolute' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mem.committed',
|
||||
name: 'mem.committed',
|
||||
context: 'mem.committed',
|
||||
title: 'Committed (Allocated) Memory',
|
||||
units: 'MiB',
|
||||
family: 'committed',
|
||||
chartType: 'area',
|
||||
priority: 245,
|
||||
plugin: 'proc',
|
||||
dimensions: [{ id: 'Committed_AS', name: 'Committed_AS', algorithm: 'absolute' }],
|
||||
},
|
||||
{
|
||||
id: 'mem.swapio',
|
||||
name: 'mem.swapio',
|
||||
context: 'mem.swapio',
|
||||
title: 'Swap I/O',
|
||||
units: 'KiB/s',
|
||||
family: 'swap',
|
||||
chartType: 'area',
|
||||
priority: 250,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'in', name: 'in', algorithm: 'incremental' },
|
||||
{ id: 'out', name: 'out', algorithm: 'incremental' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'system.pgpgio',
|
||||
name: 'system.pgpgio',
|
||||
context: 'system.pgpgio',
|
||||
title: 'Memory Page I/O',
|
||||
units: 'KiB/s',
|
||||
family: 'pgpgio',
|
||||
chartType: 'area',
|
||||
priority: 260,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'in', name: 'in', algorithm: 'incremental' },
|
||||
{ id: 'out', name: 'out', algorithm: 'incremental' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'system.pgfaults',
|
||||
name: 'system.pgfaults',
|
||||
context: 'system.pgfaults',
|
||||
title: 'Memory Page Faults',
|
||||
units: 'faults/s',
|
||||
family: 'pgfaults',
|
||||
chartType: 'line',
|
||||
priority: 270,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'minor', name: 'minor', algorithm: 'incremental' },
|
||||
{ id: 'major', name: 'major', algorithm: 'incremental' },
|
||||
],
|
||||
},
|
||||
|
||||
// ── Aggregate disk / network ──────────────────────────────
|
||||
{
|
||||
id: 'system.io',
|
||||
name: 'system.io',
|
||||
context: 'system.io',
|
||||
title: 'Disk I/O',
|
||||
units: 'KiB/s',
|
||||
family: 'disk',
|
||||
chartType: 'area',
|
||||
priority: 400,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'in', name: 'in', algorithm: 'incremental' },
|
||||
{ id: 'out', name: 'out', algorithm: 'incremental' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'system.net',
|
||||
name: 'system.net',
|
||||
context: 'system.net',
|
||||
title: 'Physical Network Interfaces Bandwidth',
|
||||
units: 'kilobits/s',
|
||||
family: 'network',
|
||||
chartType: 'area',
|
||||
priority: 500,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'received', name: 'received', algorithm: 'incremental' },
|
||||
{ id: 'sent', name: 'sent', algorithm: 'incremental' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'system.ip',
|
||||
name: 'system.ip',
|
||||
context: 'system.ip',
|
||||
title: 'IP Bandwidth',
|
||||
units: 'kilobits/s',
|
||||
family: 'ip',
|
||||
chartType: 'area',
|
||||
priority: 600,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'received', name: 'received', algorithm: 'incremental' },
|
||||
{ id: 'sent', name: 'sent', algorithm: 'incremental' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'system.ipv6',
|
||||
name: 'system.ipv6',
|
||||
context: 'system.ipv6',
|
||||
title: 'IPv6 Bandwidth',
|
||||
units: 'kilobits/s',
|
||||
family: 'ipv6',
|
||||
chartType: 'area',
|
||||
priority: 610,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'received', name: 'received', algorithm: 'incremental' },
|
||||
{ id: 'sent', name: 'sent', algorithm: 'incremental' },
|
||||
],
|
||||
},
|
||||
|
||||
// ── IP / TCP / UDP aggregates ─────────────────────────────
|
||||
{
|
||||
id: 'ip.tcppackets',
|
||||
name: 'ip.tcppackets',
|
||||
context: 'ip.tcppackets',
|
||||
title: 'TCP Packets',
|
||||
units: 'packets/s',
|
||||
family: 'tcp',
|
||||
chartType: 'line',
|
||||
priority: 700,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'received', name: 'received', algorithm: 'incremental' },
|
||||
{ id: 'sent', name: 'sent', algorithm: 'incremental' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ip.tcperrors',
|
||||
name: 'ip.tcperrors',
|
||||
context: 'ip.tcperrors',
|
||||
title: 'TCP Errors',
|
||||
units: 'packets/s',
|
||||
family: 'tcp',
|
||||
chartType: 'line',
|
||||
priority: 710,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'InErrs', name: 'InErrs', algorithm: 'incremental' },
|
||||
{ id: 'InCsumErrors', name: 'InCsumErrors', algorithm: 'incremental' },
|
||||
{ id: 'RetransSegs', name: 'RetransSegs', algorithm: 'incremental' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ip.tcpopens',
|
||||
name: 'ip.tcpopens',
|
||||
context: 'ip.tcpopens',
|
||||
title: 'TCP Connections',
|
||||
units: 'connections/s',
|
||||
family: 'tcp',
|
||||
chartType: 'line',
|
||||
priority: 720,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'active', name: 'active', algorithm: 'incremental' },
|
||||
{ id: 'passive', name: 'passive', algorithm: 'incremental' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ip.tcpsock',
|
||||
name: 'ip.tcpsock',
|
||||
context: 'ip.tcpsock',
|
||||
title: 'TCP Connections Current',
|
||||
units: 'connections',
|
||||
family: 'tcp',
|
||||
chartType: 'line',
|
||||
priority: 725,
|
||||
plugin: 'proc',
|
||||
dimensions: [{ id: 'connections', name: 'connections', algorithm: 'absolute' }],
|
||||
},
|
||||
{
|
||||
id: 'ipv4.packets',
|
||||
name: 'ipv4.packets',
|
||||
context: 'ipv4.packets',
|
||||
title: 'IPv4 Packets',
|
||||
units: 'packets/s',
|
||||
family: 'packets',
|
||||
chartType: 'line',
|
||||
priority: 740,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'received', name: 'received', algorithm: 'incremental' },
|
||||
{ id: 'sent', name: 'sent', algorithm: 'incremental' },
|
||||
{ id: 'forwarded', name: 'forwarded', algorithm: 'incremental' },
|
||||
{ id: 'delivered', name: 'delivered', algorithm: 'incremental' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ipv4.errors',
|
||||
name: 'ipv4.errors',
|
||||
context: 'ipv4.errors',
|
||||
title: 'IPv4 Errors',
|
||||
units: 'packets/s',
|
||||
family: 'errors',
|
||||
chartType: 'line',
|
||||
priority: 750,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'InDiscards', name: 'InDiscards', algorithm: 'incremental' },
|
||||
{ id: 'OutDiscards', name: 'OutDiscards', algorithm: 'incremental' },
|
||||
{ id: 'InHdrErrors', name: 'InHdrErrors', algorithm: 'incremental' },
|
||||
{ id: 'OutNoRoutes', name: 'OutNoRoutes', algorithm: 'incremental' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ipv4.udppackets',
|
||||
name: 'ipv4.udppackets',
|
||||
context: 'ipv4.udppackets',
|
||||
title: 'IPv4 UDP Packets',
|
||||
units: 'packets/s',
|
||||
family: 'udp',
|
||||
chartType: 'line',
|
||||
priority: 760,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'received', name: 'received', algorithm: 'incremental' },
|
||||
{ id: 'sent', name: 'sent', algorithm: 'incremental' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ipv4.udperrors',
|
||||
name: 'ipv4.udperrors',
|
||||
context: 'ipv4.udperrors',
|
||||
title: 'IPv4 UDP Errors',
|
||||
units: 'packets/s',
|
||||
family: 'udp',
|
||||
chartType: 'line',
|
||||
priority: 770,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'RcvbufErrors', name: 'RcvbufErrors', algorithm: 'incremental' },
|
||||
{ id: 'SndbufErrors', name: 'SndbufErrors', algorithm: 'incremental' },
|
||||
{ id: 'InErrors', name: 'InErrors', algorithm: 'incremental' },
|
||||
{ id: 'NoPorts', name: 'NoPorts', algorithm: 'incremental' },
|
||||
],
|
||||
},
|
||||
|
||||
// ── Pressure stall (PSI) ──────────────────────────────────
|
||||
{
|
||||
id: 'system.cpu_some_pressure',
|
||||
name: 'system.cpu_some_pressure',
|
||||
context: 'system.cpu_some_pressure',
|
||||
title: 'CPU Some Pressure',
|
||||
units: 'percentage',
|
||||
family: 'pressure',
|
||||
chartType: 'line',
|
||||
priority: 800,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'some10', name: 'some10', algorithm: 'absolute' },
|
||||
{ id: 'some60', name: 'some60', algorithm: 'absolute' },
|
||||
{ id: 'some300', name: 'some300', algorithm: 'absolute' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'system.memory_some_pressure',
|
||||
name: 'system.memory_some_pressure',
|
||||
context: 'system.memory_some_pressure',
|
||||
title: 'Memory Some Pressure',
|
||||
units: 'percentage',
|
||||
family: 'pressure',
|
||||
chartType: 'line',
|
||||
priority: 810,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'some10', name: 'some10', algorithm: 'absolute' },
|
||||
{ id: 'some60', name: 'some60', algorithm: 'absolute' },
|
||||
{ id: 'some300', name: 'some300', algorithm: 'absolute' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'system.io_some_pressure',
|
||||
name: 'system.io_some_pressure',
|
||||
context: 'system.io_some_pressure',
|
||||
title: 'I/O Some Pressure',
|
||||
units: 'percentage',
|
||||
family: 'pressure',
|
||||
chartType: 'line',
|
||||
priority: 820,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'some10', name: 'some10', algorithm: 'absolute' },
|
||||
{ id: 'some60', name: 'some60', algorithm: 'absolute' },
|
||||
{ id: 'some300', name: 'some300', algorithm: 'absolute' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
/** @type {Map<string, ChartDef>} */
|
||||
export const CHART_BY_ID = new Map(CHART_DEFS.map((c) => [c.id, c]))
|
||||
const runtimeCharts = new Map()
|
||||
|
||||
/** Unique context ids */
|
||||
export const CONTEXT_IDS = [...new Set(CHART_DEFS.map((c) => c.context))]
|
||||
/**
|
||||
* Register or refresh a dynamic instance chart (per-cpu / disk / iface / mount).
|
||||
* @param {ChartDef} def
|
||||
*/
|
||||
export function registerChart(def) {
|
||||
runtimeCharts.set(def.id, def)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ChartDef[]}
|
||||
*/
|
||||
export function getAllChartDefs() {
|
||||
return [...STATIC_CHART_DEFS, ...runtimeCharts.values()]
|
||||
}
|
||||
|
||||
/** Alias — prefer getAllChartDefs() when instance charts matter. */
|
||||
export const CHART_DEFS = STATIC_CHART_DEFS
|
||||
|
||||
export const CHART_BY_ID = {
|
||||
get(id) {
|
||||
const sid = String(id)
|
||||
return STATIC_CHART_DEFS.find((c) => c.id === sid) || runtimeCharts.get(sid) || null
|
||||
},
|
||||
has(id) {
|
||||
return this.get(id) != null
|
||||
},
|
||||
keys() {
|
||||
return getAllChartDefs().map((c) => c.id)
|
||||
},
|
||||
values() {
|
||||
return getAllChartDefs()
|
||||
},
|
||||
get size() {
|
||||
return getAllChartDefs().length
|
||||
},
|
||||
}
|
||||
|
||||
export function getContextIds() {
|
||||
return [...new Set(getAllChartDefs().map((c) => c.context))]
|
||||
}
|
||||
|
||||
/** @deprecated use getContextIds() */
|
||||
export const CONTEXT_IDS = {
|
||||
map(fn) {
|
||||
return getContextIds().map(fn)
|
||||
},
|
||||
includes(id) {
|
||||
return getContextIds().includes(id)
|
||||
},
|
||||
[Symbol.iterator]() {
|
||||
return getContextIds()[Symbol.iterator]()
|
||||
},
|
||||
get length() {
|
||||
return getContextIds().length
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} context
|
||||
*/
|
||||
export function chartsForContext(context) {
|
||||
return CHART_DEFS.filter((c) => c.context === context)
|
||||
return getAllChartDefs().filter((c) => c.context === context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Netdata-ish chart summary object.
|
||||
* Instance chart helpers
|
||||
*/
|
||||
export function makeCpuCoreChart(coreId) {
|
||||
return {
|
||||
id: `cpu.cpu${coreId}`,
|
||||
name: `cpu.cpu${coreId}`,
|
||||
context: 'cpu.cpu',
|
||||
title: `Core ${coreId} utilization`,
|
||||
units: 'percentage',
|
||||
family: `cpu${coreId}`,
|
||||
chartType: 'stacked',
|
||||
priority: 1000 + Number(coreId),
|
||||
plugin: 'proc',
|
||||
dimensions: STATIC_CHART_DEFS.find((c) => c.id === 'system.cpu').dimensions.slice(),
|
||||
}
|
||||
}
|
||||
|
||||
export function makeDiskIoChart(disk) {
|
||||
return {
|
||||
id: `disk_io.${disk}`,
|
||||
name: `disk_io.${disk}`,
|
||||
context: 'disk.io',
|
||||
title: `Disk ${disk} I/O`,
|
||||
units: 'KiB/s',
|
||||
family: disk,
|
||||
chartType: 'area',
|
||||
priority: 2000,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'reads', name: 'reads', algorithm: 'incremental' },
|
||||
{ id: 'writes', name: 'writes', algorithm: 'incremental' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export function makeDiskOpsChart(disk) {
|
||||
return {
|
||||
id: `disk_ops.${disk}`,
|
||||
name: `disk_ops.${disk}`,
|
||||
context: 'disk.ops',
|
||||
title: `Disk ${disk} Completed I/O Operations`,
|
||||
units: 'operations/s',
|
||||
family: disk,
|
||||
chartType: 'line',
|
||||
priority: 2010,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'reads', name: 'reads', algorithm: 'incremental' },
|
||||
{ id: 'writes', name: 'writes', algorithm: 'incremental' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export function makeDiskUtilChart(disk) {
|
||||
return {
|
||||
id: `disk_util.${disk}`,
|
||||
name: `disk_util.${disk}`,
|
||||
context: 'disk.util',
|
||||
title: `Disk ${disk} Utilization Time`,
|
||||
units: '% of time working',
|
||||
family: disk,
|
||||
chartType: 'area',
|
||||
priority: 2020,
|
||||
plugin: 'proc',
|
||||
dimensions: [{ id: 'utilization', name: 'utilization', algorithm: 'absolute' }],
|
||||
}
|
||||
}
|
||||
|
||||
export function makeNetChart(iface) {
|
||||
return {
|
||||
id: `net.${iface}`,
|
||||
name: `net.${iface}`,
|
||||
context: 'net.net',
|
||||
title: `Bandwidth ${iface}`,
|
||||
units: 'kilobits/s',
|
||||
family: iface,
|
||||
chartType: 'area',
|
||||
priority: 3000,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'received', name: 'received', algorithm: 'incremental' },
|
||||
{ id: 'sent', name: 'sent', algorithm: 'incremental' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export function makeNetPacketsChart(iface) {
|
||||
return {
|
||||
id: `net_packets.${iface}`,
|
||||
name: `net_packets.${iface}`,
|
||||
context: 'net.packets',
|
||||
title: `Packets ${iface}`,
|
||||
units: 'packets/s',
|
||||
family: iface,
|
||||
chartType: 'line',
|
||||
priority: 3010,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'received', name: 'received', algorithm: 'incremental' },
|
||||
{ id: 'sent', name: 'sent', algorithm: 'incremental' },
|
||||
{ id: 'multicast', name: 'multicast', algorithm: 'incremental' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export function makeNetErrorsChart(iface) {
|
||||
return {
|
||||
id: `net_errors.${iface}`,
|
||||
name: `net_errors.${iface}`,
|
||||
context: 'net.errors',
|
||||
title: `Interface ${iface} Errors`,
|
||||
units: 'errors/s',
|
||||
family: iface,
|
||||
chartType: 'line',
|
||||
priority: 3020,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'inbound', name: 'inbound', algorithm: 'incremental' },
|
||||
{ id: 'outbound', name: 'outbound', algorithm: 'incremental' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export function makeNetDropsChart(iface) {
|
||||
return {
|
||||
id: `net_drops.${iface}`,
|
||||
name: `net_drops.${iface}`,
|
||||
context: 'net.drops',
|
||||
title: `Interface ${iface} Drops`,
|
||||
units: 'drops/s',
|
||||
family: iface,
|
||||
chartType: 'line',
|
||||
priority: 3030,
|
||||
plugin: 'proc',
|
||||
dimensions: [
|
||||
{ id: 'inbound', name: 'inbound', algorithm: 'incremental' },
|
||||
{ id: 'outbound', name: 'outbound', algorithm: 'incremental' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export function makeDiskSpaceChart(mountId, mountPath) {
|
||||
return {
|
||||
id: `disk_space.${mountId}`,
|
||||
name: `disk_space.${mountId}`,
|
||||
context: 'disk.space',
|
||||
title: `Disk Space Usage ${mountPath}`,
|
||||
units: 'GiB',
|
||||
family: mountPath,
|
||||
chartType: 'stacked',
|
||||
priority: 4000,
|
||||
plugin: 'diskspace',
|
||||
dimensions: [
|
||||
{ id: 'avail', name: 'avail', algorithm: 'absolute' },
|
||||
{ id: 'used', name: 'used', algorithm: 'absolute' },
|
||||
{ id: 'reserved_for_root', name: 'reserved_for_root', algorithm: 'absolute' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export function makeDiskInodesChart(mountId, mountPath) {
|
||||
return {
|
||||
id: `disk_inodes.${mountId}`,
|
||||
name: `disk_inodes.${mountId}`,
|
||||
context: 'disk.inodes',
|
||||
title: `Disk Files (inodes) Usage ${mountPath}`,
|
||||
units: 'inodes',
|
||||
family: mountPath,
|
||||
chartType: 'stacked',
|
||||
priority: 4010,
|
||||
plugin: 'diskspace',
|
||||
dimensions: [
|
||||
{ id: 'avail', name: 'avail', algorithm: 'absolute' },
|
||||
{ id: 'used', name: 'used', algorithm: 'absolute' },
|
||||
{ id: 'reserved_for_root', name: 'reserved_for_root', algorithm: 'absolute' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a chart summary object for discovery APIs.
|
||||
* @param {ChartDef} def
|
||||
* @param {{ firstEntry?: number, lastEntry?: number, updateEvery?: number }} [meta]
|
||||
*/
|
||||
@@ -172,13 +820,14 @@ export function chartSummary(def, meta = {}) {
|
||||
return {
|
||||
id: def.id,
|
||||
name: def.name,
|
||||
type: 'system',
|
||||
type: def.plugin || 'system',
|
||||
family: def.family,
|
||||
context: def.context,
|
||||
title: def.title,
|
||||
units: def.units,
|
||||
chart_type: def.chartType,
|
||||
priority: def.priority,
|
||||
plugin: def.plugin || 'proc',
|
||||
update_every: meta.updateEvery ?? SAMPLE_INTERVAL_MS / 1000,
|
||||
first_entry: meta.firstEntry ?? 0,
|
||||
last_entry: meta.lastEntry ?? 0,
|
||||
|
||||
@@ -82,6 +82,12 @@ export const MethodRoles = Object.freeze({
|
||||
runJob: Roles.operator,
|
||||
cancelJob: Roles.operator,
|
||||
|
||||
// HyperDB peer links / db info
|
||||
getDbInfo: Roles.viewer,
|
||||
listPeerLinks: Roles.viewer,
|
||||
linkPeer: Roles.admin,
|
||||
unlinkPeer: Roles.admin,
|
||||
|
||||
// admin / fleet
|
||||
mintInvite: Roles.admin,
|
||||
listPeers: Roles.admin,
|
||||
|
||||
@@ -136,8 +136,30 @@ export function validateMethodArgs(method, args = {}) {
|
||||
}
|
||||
|
||||
case 'exportSnapshot':
|
||||
case 'getDbInfo':
|
||||
case 'listPeerLinks':
|
||||
return { ok: true, args }
|
||||
|
||||
case 'linkPeer': {
|
||||
const remotePublicKey = String(args.remotePublicKey || args.peerId || '').toLowerCase()
|
||||
if (!/^[0-9a-f]{64}$/.test(remotePublicKey)) {
|
||||
return { ok: false, error: 'remotePublicKey must be 64 hex characters' }
|
||||
}
|
||||
const syncMode = String(args.syncMode || 'both')
|
||||
if (!['push', 'pull', 'both'].includes(syncMode)) {
|
||||
return { ok: false, error: 'syncMode must be push|pull|both' }
|
||||
}
|
||||
return { ok: true, args: { ...args, remotePublicKey, syncMode } }
|
||||
}
|
||||
|
||||
case 'unlinkPeer': {
|
||||
const remotePublicKey = String(args.remotePublicKey || args.peerId || '').toLowerCase()
|
||||
if (!/^[0-9a-f]{64}$/.test(remotePublicKey)) {
|
||||
return { ok: false, error: 'remotePublicKey must be 64 hex characters' }
|
||||
}
|
||||
return { ok: true, args: { ...args, remotePublicKey } }
|
||||
}
|
||||
|
||||
default:
|
||||
return { ok: true, args }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
{
|
||||
"version": 1,
|
||||
"offset": 0,
|
||||
"schema": [
|
||||
{
|
||||
"name": "node",
|
||||
"namespace": "peardata",
|
||||
"id": 0,
|
||||
"type": 1,
|
||||
"version": 1,
|
||||
"versionField": null,
|
||||
"indexes": [
|
||||
"@peardata/node-by-hostname"
|
||||
],
|
||||
"schema": "@peardata/node",
|
||||
"derived": false,
|
||||
"key": [
|
||||
"nodeId"
|
||||
],
|
||||
"trigger": null
|
||||
},
|
||||
{
|
||||
"name": "peer-link",
|
||||
"namespace": "peardata",
|
||||
"id": 1,
|
||||
"type": 1,
|
||||
"version": 1,
|
||||
"versionField": null,
|
||||
"indexes": [
|
||||
"@peardata/peer-link-by-remote"
|
||||
],
|
||||
"schema": "@peardata/peer-link",
|
||||
"derived": false,
|
||||
"key": [
|
||||
"localNodeId",
|
||||
"remotePublicKey"
|
||||
],
|
||||
"trigger": null
|
||||
},
|
||||
{
|
||||
"name": "alert-config",
|
||||
"namespace": "peardata",
|
||||
"id": 2,
|
||||
"type": 1,
|
||||
"version": 1,
|
||||
"versionField": null,
|
||||
"indexes": [],
|
||||
"schema": "@peardata/alert-config",
|
||||
"derived": false,
|
||||
"key": [
|
||||
"id"
|
||||
],
|
||||
"trigger": null
|
||||
},
|
||||
{
|
||||
"name": "alert-event",
|
||||
"namespace": "peardata",
|
||||
"id": 3,
|
||||
"type": 1,
|
||||
"version": 1,
|
||||
"versionField": null,
|
||||
"indexes": [
|
||||
"@peardata/alert-event-by-chart"
|
||||
],
|
||||
"schema": "@peardata/alert-event",
|
||||
"derived": false,
|
||||
"key": [
|
||||
"id",
|
||||
"ts"
|
||||
],
|
||||
"trigger": null
|
||||
},
|
||||
{
|
||||
"name": "metric-point",
|
||||
"namespace": "peardata",
|
||||
"id": 4,
|
||||
"type": 1,
|
||||
"version": 1,
|
||||
"versionField": null,
|
||||
"indexes": [
|
||||
"@peardata/metric-point-by-context",
|
||||
"@peardata/metric-point-by-tier"
|
||||
],
|
||||
"schema": "@peardata/metric-point",
|
||||
"derived": false,
|
||||
"key": [
|
||||
"chart",
|
||||
"ts"
|
||||
],
|
||||
"trigger": null
|
||||
},
|
||||
{
|
||||
"name": "job",
|
||||
"namespace": "peardata",
|
||||
"id": 5,
|
||||
"type": 1,
|
||||
"version": 1,
|
||||
"versionField": null,
|
||||
"indexes": [],
|
||||
"schema": "@peardata/job",
|
||||
"derived": false,
|
||||
"key": [
|
||||
"id"
|
||||
],
|
||||
"trigger": null
|
||||
},
|
||||
{
|
||||
"name": "node-by-hostname",
|
||||
"namespace": "peardata",
|
||||
"id": 6,
|
||||
"type": 2,
|
||||
"version": 1,
|
||||
"collection": "@peardata/node",
|
||||
"unique": false,
|
||||
"deprecated": false,
|
||||
"key": [
|
||||
"hostname"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "peer-link-by-remote",
|
||||
"namespace": "peardata",
|
||||
"id": 7,
|
||||
"type": 2,
|
||||
"version": 1,
|
||||
"collection": "@peardata/peer-link",
|
||||
"unique": false,
|
||||
"deprecated": false,
|
||||
"key": [
|
||||
"remotePublicKey"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "alert-event-by-chart",
|
||||
"namespace": "peardata",
|
||||
"id": 8,
|
||||
"type": 2,
|
||||
"version": 1,
|
||||
"collection": "@peardata/alert-event",
|
||||
"unique": false,
|
||||
"deprecated": false,
|
||||
"key": [
|
||||
"chart",
|
||||
"ts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "metric-point-by-context",
|
||||
"namespace": "peardata",
|
||||
"id": 9,
|
||||
"type": 2,
|
||||
"version": 1,
|
||||
"collection": "@peardata/metric-point",
|
||||
"unique": false,
|
||||
"deprecated": false,
|
||||
"key": [
|
||||
"context",
|
||||
"ts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "metric-point-by-tier",
|
||||
"namespace": "peardata",
|
||||
"id": 10,
|
||||
"type": 2,
|
||||
"version": 1,
|
||||
"collection": "@peardata/metric-point",
|
||||
"unique": false,
|
||||
"deprecated": false,
|
||||
"key": [
|
||||
"tier",
|
||||
"chart",
|
||||
"ts"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,757 @@
|
||||
// This file is autogenerated by the hyperdb compiler
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import { IndexEncoder, c, b4a } from 'hyperdb/runtime'
|
||||
import { version, getEncoding, setVersion } from './messages.js'
|
||||
|
||||
const versions = { schema: version, db: 1 }
|
||||
|
||||
// '@peardata/node' collection key
|
||||
const collection0_key = new IndexEncoder([
|
||||
IndexEncoder.STRING
|
||||
], { prefix: 0 })
|
||||
|
||||
function collection0_indexify (record) {
|
||||
const a = record.nodeId
|
||||
return a === undefined ? [] : [a]
|
||||
}
|
||||
|
||||
// '@peardata/node' value encoding
|
||||
const collection0_enc = getEncoding('@peardata/node/hyperdb#0')
|
||||
|
||||
// '@peardata/node' reconstruction function
|
||||
function collection0_reconstruct (schemaVersion, keyBuf, valueBuf) {
|
||||
const key = collection0_key.decode(keyBuf)
|
||||
setVersion(schemaVersion)
|
||||
const state = { start: 0, end: valueBuf.byteLength, buffer: valueBuf }
|
||||
const type = c.uint.decode(state)
|
||||
if (type !== 0) throw new Error('Unknown collection type: ' + type)
|
||||
collection0.decodedVersion = c.uint.decode(state)
|
||||
const record = collection0_enc.decode(state)
|
||||
record.nodeId = key[0]
|
||||
return record
|
||||
}
|
||||
// '@peardata/node' key reconstruction function
|
||||
function collection0_reconstruct_key (keyBuf) {
|
||||
const key = collection0_key.decode(keyBuf)
|
||||
return {
|
||||
nodeId: key[0]
|
||||
}
|
||||
}
|
||||
|
||||
// '@peardata/node'
|
||||
const collection0 = {
|
||||
name: '@peardata/node',
|
||||
id: 0,
|
||||
version: 1,
|
||||
encodeKey (record) {
|
||||
const key = [record.nodeId]
|
||||
return collection0_key.encode(key)
|
||||
},
|
||||
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
|
||||
return collection0_key.encodeRange({
|
||||
gt: gt ? collection0_indexify(gt) : null,
|
||||
lt: lt ? collection0_indexify(lt) : null,
|
||||
gte: gte ? collection0_indexify(gte) : null,
|
||||
lte: lte ? collection0_indexify(lte) : null
|
||||
})
|
||||
},
|
||||
encodeValue (schemaVersion, collectionVersion, record) {
|
||||
setVersion(schemaVersion)
|
||||
const state = { start: 0, end: 2, buffer: null }
|
||||
collection0_enc.preencode(state, record)
|
||||
state.buffer = b4a.allocUnsafe(state.end)
|
||||
state.buffer[state.start++] = 0
|
||||
state.buffer[state.start++] = collectionVersion
|
||||
collection0_enc.encode(state, record)
|
||||
return state.buffer
|
||||
},
|
||||
trigger: null,
|
||||
reconstruct: collection0_reconstruct,
|
||||
reconstructKey: collection0_reconstruct_key,
|
||||
indexes: [],
|
||||
decodedVersion: 0
|
||||
}
|
||||
|
||||
// '@peardata/peer-link' collection key
|
||||
const collection1_key = new IndexEncoder([
|
||||
IndexEncoder.STRING,
|
||||
IndexEncoder.STRING
|
||||
], { prefix: 1 })
|
||||
|
||||
function collection1_indexify (record) {
|
||||
const arr = []
|
||||
|
||||
const a0 = record.localNodeId
|
||||
if (a0 === undefined) return arr
|
||||
arr.push(a0)
|
||||
|
||||
const a1 = record.remotePublicKey
|
||||
if (a1 === undefined) return arr
|
||||
arr.push(a1)
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
// '@peardata/peer-link' value encoding
|
||||
const collection1_enc = getEncoding('@peardata/peer-link/hyperdb#1')
|
||||
|
||||
// '@peardata/peer-link' reconstruction function
|
||||
function collection1_reconstruct (schemaVersion, keyBuf, valueBuf) {
|
||||
const key = collection1_key.decode(keyBuf)
|
||||
setVersion(schemaVersion)
|
||||
const state = { start: 0, end: valueBuf.byteLength, buffer: valueBuf }
|
||||
const type = c.uint.decode(state)
|
||||
if (type !== 0) throw new Error('Unknown collection type: ' + type)
|
||||
collection1.decodedVersion = c.uint.decode(state)
|
||||
const record = collection1_enc.decode(state)
|
||||
record.localNodeId = key[0]
|
||||
record.remotePublicKey = key[1]
|
||||
return record
|
||||
}
|
||||
// '@peardata/peer-link' key reconstruction function
|
||||
function collection1_reconstruct_key (keyBuf) {
|
||||
const key = collection1_key.decode(keyBuf)
|
||||
return {
|
||||
localNodeId: key[0],
|
||||
remotePublicKey: key[1]
|
||||
}
|
||||
}
|
||||
|
||||
// '@peardata/peer-link'
|
||||
const collection1 = {
|
||||
name: '@peardata/peer-link',
|
||||
id: 1,
|
||||
version: 1,
|
||||
encodeKey (record) {
|
||||
const key = [record.localNodeId, record.remotePublicKey]
|
||||
return collection1_key.encode(key)
|
||||
},
|
||||
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
|
||||
return collection1_key.encodeRange({
|
||||
gt: gt ? collection1_indexify(gt) : null,
|
||||
lt: lt ? collection1_indexify(lt) : null,
|
||||
gte: gte ? collection1_indexify(gte) : null,
|
||||
lte: lte ? collection1_indexify(lte) : null
|
||||
})
|
||||
},
|
||||
encodeValue (schemaVersion, collectionVersion, record) {
|
||||
setVersion(schemaVersion)
|
||||
const state = { start: 0, end: 2, buffer: null }
|
||||
collection1_enc.preencode(state, record)
|
||||
state.buffer = b4a.allocUnsafe(state.end)
|
||||
state.buffer[state.start++] = 0
|
||||
state.buffer[state.start++] = collectionVersion
|
||||
collection1_enc.encode(state, record)
|
||||
return state.buffer
|
||||
},
|
||||
trigger: null,
|
||||
reconstruct: collection1_reconstruct,
|
||||
reconstructKey: collection1_reconstruct_key,
|
||||
indexes: [],
|
||||
decodedVersion: 0
|
||||
}
|
||||
|
||||
// '@peardata/alert-config' collection key
|
||||
const collection2_key = new IndexEncoder([
|
||||
IndexEncoder.STRING
|
||||
], { prefix: 2 })
|
||||
|
||||
function collection2_indexify (record) {
|
||||
const a = record.id
|
||||
return a === undefined ? [] : [a]
|
||||
}
|
||||
|
||||
// '@peardata/alert-config' value encoding
|
||||
const collection2_enc = getEncoding('@peardata/alert-config/hyperdb#2')
|
||||
|
||||
// '@peardata/alert-config' reconstruction function
|
||||
function collection2_reconstruct (schemaVersion, keyBuf, valueBuf) {
|
||||
const key = collection2_key.decode(keyBuf)
|
||||
setVersion(schemaVersion)
|
||||
const state = { start: 0, end: valueBuf.byteLength, buffer: valueBuf }
|
||||
const type = c.uint.decode(state)
|
||||
if (type !== 0) throw new Error('Unknown collection type: ' + type)
|
||||
collection2.decodedVersion = c.uint.decode(state)
|
||||
const record = collection2_enc.decode(state)
|
||||
record.id = key[0]
|
||||
return record
|
||||
}
|
||||
// '@peardata/alert-config' key reconstruction function
|
||||
function collection2_reconstruct_key (keyBuf) {
|
||||
const key = collection2_key.decode(keyBuf)
|
||||
return {
|
||||
id: key[0]
|
||||
}
|
||||
}
|
||||
|
||||
// '@peardata/alert-config'
|
||||
const collection2 = {
|
||||
name: '@peardata/alert-config',
|
||||
id: 2,
|
||||
version: 1,
|
||||
encodeKey (record) {
|
||||
const key = [record.id]
|
||||
return collection2_key.encode(key)
|
||||
},
|
||||
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
|
||||
return collection2_key.encodeRange({
|
||||
gt: gt ? collection2_indexify(gt) : null,
|
||||
lt: lt ? collection2_indexify(lt) : null,
|
||||
gte: gte ? collection2_indexify(gte) : null,
|
||||
lte: lte ? collection2_indexify(lte) : null
|
||||
})
|
||||
},
|
||||
encodeValue (schemaVersion, collectionVersion, record) {
|
||||
setVersion(schemaVersion)
|
||||
const state = { start: 0, end: 2, buffer: null }
|
||||
collection2_enc.preencode(state, record)
|
||||
state.buffer = b4a.allocUnsafe(state.end)
|
||||
state.buffer[state.start++] = 0
|
||||
state.buffer[state.start++] = collectionVersion
|
||||
collection2_enc.encode(state, record)
|
||||
return state.buffer
|
||||
},
|
||||
trigger: null,
|
||||
reconstruct: collection2_reconstruct,
|
||||
reconstructKey: collection2_reconstruct_key,
|
||||
indexes: [],
|
||||
decodedVersion: 0
|
||||
}
|
||||
|
||||
// '@peardata/alert-event' collection key
|
||||
const collection3_key = new IndexEncoder([
|
||||
IndexEncoder.STRING,
|
||||
IndexEncoder.UINT
|
||||
], { prefix: 3 })
|
||||
|
||||
function collection3_indexify (record) {
|
||||
const arr = []
|
||||
|
||||
const a0 = record.id
|
||||
if (a0 === undefined) return arr
|
||||
arr.push(a0)
|
||||
|
||||
const a1 = record.ts
|
||||
if (a1 === undefined) return arr
|
||||
arr.push(a1)
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
// '@peardata/alert-event' value encoding
|
||||
const collection3_enc = getEncoding('@peardata/alert-event/hyperdb#3')
|
||||
|
||||
// '@peardata/alert-event' reconstruction function
|
||||
function collection3_reconstruct (schemaVersion, keyBuf, valueBuf) {
|
||||
const key = collection3_key.decode(keyBuf)
|
||||
setVersion(schemaVersion)
|
||||
const state = { start: 0, end: valueBuf.byteLength, buffer: valueBuf }
|
||||
const type = c.uint.decode(state)
|
||||
if (type !== 0) throw new Error('Unknown collection type: ' + type)
|
||||
collection3.decodedVersion = c.uint.decode(state)
|
||||
const record = collection3_enc.decode(state)
|
||||
record.id = key[0]
|
||||
record.ts = key[1]
|
||||
return record
|
||||
}
|
||||
// '@peardata/alert-event' key reconstruction function
|
||||
function collection3_reconstruct_key (keyBuf) {
|
||||
const key = collection3_key.decode(keyBuf)
|
||||
return {
|
||||
id: key[0],
|
||||
ts: key[1]
|
||||
}
|
||||
}
|
||||
|
||||
// '@peardata/alert-event'
|
||||
const collection3 = {
|
||||
name: '@peardata/alert-event',
|
||||
id: 3,
|
||||
version: 1,
|
||||
encodeKey (record) {
|
||||
const key = [record.id, record.ts]
|
||||
return collection3_key.encode(key)
|
||||
},
|
||||
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
|
||||
return collection3_key.encodeRange({
|
||||
gt: gt ? collection3_indexify(gt) : null,
|
||||
lt: lt ? collection3_indexify(lt) : null,
|
||||
gte: gte ? collection3_indexify(gte) : null,
|
||||
lte: lte ? collection3_indexify(lte) : null
|
||||
})
|
||||
},
|
||||
encodeValue (schemaVersion, collectionVersion, record) {
|
||||
setVersion(schemaVersion)
|
||||
const state = { start: 0, end: 2, buffer: null }
|
||||
collection3_enc.preencode(state, record)
|
||||
state.buffer = b4a.allocUnsafe(state.end)
|
||||
state.buffer[state.start++] = 0
|
||||
state.buffer[state.start++] = collectionVersion
|
||||
collection3_enc.encode(state, record)
|
||||
return state.buffer
|
||||
},
|
||||
trigger: null,
|
||||
reconstruct: collection3_reconstruct,
|
||||
reconstructKey: collection3_reconstruct_key,
|
||||
indexes: [],
|
||||
decodedVersion: 0
|
||||
}
|
||||
|
||||
// '@peardata/metric-point' collection key
|
||||
const collection4_key = new IndexEncoder([
|
||||
IndexEncoder.STRING,
|
||||
IndexEncoder.UINT
|
||||
], { prefix: 4 })
|
||||
|
||||
function collection4_indexify (record) {
|
||||
const arr = []
|
||||
|
||||
const a0 = record.chart
|
||||
if (a0 === undefined) return arr
|
||||
arr.push(a0)
|
||||
|
||||
const a1 = record.ts
|
||||
if (a1 === undefined) return arr
|
||||
arr.push(a1)
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
// '@peardata/metric-point' value encoding
|
||||
const collection4_enc = getEncoding('@peardata/metric-point/hyperdb#4')
|
||||
|
||||
// '@peardata/metric-point' reconstruction function
|
||||
function collection4_reconstruct (schemaVersion, keyBuf, valueBuf) {
|
||||
const key = collection4_key.decode(keyBuf)
|
||||
setVersion(schemaVersion)
|
||||
const state = { start: 0, end: valueBuf.byteLength, buffer: valueBuf }
|
||||
const type = c.uint.decode(state)
|
||||
if (type !== 0) throw new Error('Unknown collection type: ' + type)
|
||||
collection4.decodedVersion = c.uint.decode(state)
|
||||
const record = collection4_enc.decode(state)
|
||||
record.chart = key[0]
|
||||
record.ts = key[1]
|
||||
return record
|
||||
}
|
||||
// '@peardata/metric-point' key reconstruction function
|
||||
function collection4_reconstruct_key (keyBuf) {
|
||||
const key = collection4_key.decode(keyBuf)
|
||||
return {
|
||||
chart: key[0],
|
||||
ts: key[1]
|
||||
}
|
||||
}
|
||||
|
||||
// '@peardata/metric-point'
|
||||
const collection4 = {
|
||||
name: '@peardata/metric-point',
|
||||
id: 4,
|
||||
version: 1,
|
||||
encodeKey (record) {
|
||||
const key = [record.chart, record.ts]
|
||||
return collection4_key.encode(key)
|
||||
},
|
||||
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
|
||||
return collection4_key.encodeRange({
|
||||
gt: gt ? collection4_indexify(gt) : null,
|
||||
lt: lt ? collection4_indexify(lt) : null,
|
||||
gte: gte ? collection4_indexify(gte) : null,
|
||||
lte: lte ? collection4_indexify(lte) : null
|
||||
})
|
||||
},
|
||||
encodeValue (schemaVersion, collectionVersion, record) {
|
||||
setVersion(schemaVersion)
|
||||
const state = { start: 0, end: 2, buffer: null }
|
||||
collection4_enc.preencode(state, record)
|
||||
state.buffer = b4a.allocUnsafe(state.end)
|
||||
state.buffer[state.start++] = 0
|
||||
state.buffer[state.start++] = collectionVersion
|
||||
collection4_enc.encode(state, record)
|
||||
return state.buffer
|
||||
},
|
||||
trigger: null,
|
||||
reconstruct: collection4_reconstruct,
|
||||
reconstructKey: collection4_reconstruct_key,
|
||||
indexes: [],
|
||||
decodedVersion: 0
|
||||
}
|
||||
|
||||
// '@peardata/job' collection key
|
||||
const collection5_key = new IndexEncoder([
|
||||
IndexEncoder.STRING
|
||||
], { prefix: 5 })
|
||||
|
||||
function collection5_indexify (record) {
|
||||
const a = record.id
|
||||
return a === undefined ? [] : [a]
|
||||
}
|
||||
|
||||
// '@peardata/job' value encoding
|
||||
const collection5_enc = getEncoding('@peardata/job/hyperdb#5')
|
||||
|
||||
// '@peardata/job' reconstruction function
|
||||
function collection5_reconstruct (schemaVersion, keyBuf, valueBuf) {
|
||||
const key = collection5_key.decode(keyBuf)
|
||||
setVersion(schemaVersion)
|
||||
const state = { start: 0, end: valueBuf.byteLength, buffer: valueBuf }
|
||||
const type = c.uint.decode(state)
|
||||
if (type !== 0) throw new Error('Unknown collection type: ' + type)
|
||||
collection5.decodedVersion = c.uint.decode(state)
|
||||
const record = collection5_enc.decode(state)
|
||||
record.id = key[0]
|
||||
return record
|
||||
}
|
||||
// '@peardata/job' key reconstruction function
|
||||
function collection5_reconstruct_key (keyBuf) {
|
||||
const key = collection5_key.decode(keyBuf)
|
||||
return {
|
||||
id: key[0]
|
||||
}
|
||||
}
|
||||
|
||||
// '@peardata/job'
|
||||
const collection5 = {
|
||||
name: '@peardata/job',
|
||||
id: 5,
|
||||
version: 1,
|
||||
encodeKey (record) {
|
||||
const key = [record.id]
|
||||
return collection5_key.encode(key)
|
||||
},
|
||||
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
|
||||
return collection5_key.encodeRange({
|
||||
gt: gt ? collection5_indexify(gt) : null,
|
||||
lt: lt ? collection5_indexify(lt) : null,
|
||||
gte: gte ? collection5_indexify(gte) : null,
|
||||
lte: lte ? collection5_indexify(lte) : null
|
||||
})
|
||||
},
|
||||
encodeValue (schemaVersion, collectionVersion, record) {
|
||||
setVersion(schemaVersion)
|
||||
const state = { start: 0, end: 2, buffer: null }
|
||||
collection5_enc.preencode(state, record)
|
||||
state.buffer = b4a.allocUnsafe(state.end)
|
||||
state.buffer[state.start++] = 0
|
||||
state.buffer[state.start++] = collectionVersion
|
||||
collection5_enc.encode(state, record)
|
||||
return state.buffer
|
||||
},
|
||||
trigger: null,
|
||||
reconstruct: collection5_reconstruct,
|
||||
reconstructKey: collection5_reconstruct_key,
|
||||
indexes: [],
|
||||
decodedVersion: 0
|
||||
}
|
||||
|
||||
// '@peardata/node-by-hostname' collection key
|
||||
const index6_key = new IndexEncoder([
|
||||
IndexEncoder.STRING,
|
||||
IndexEncoder.STRING
|
||||
], { prefix: 6 })
|
||||
|
||||
function index6_indexify (record) {
|
||||
const arr = []
|
||||
|
||||
const a0 = record.hostname
|
||||
if (a0 === undefined) return arr
|
||||
arr.push(a0)
|
||||
|
||||
const a1 = record.nodeId
|
||||
if (a1 === undefined) return arr
|
||||
arr.push(a1)
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
// '@peardata/node-by-hostname'
|
||||
const index6 = {
|
||||
name: '@peardata/node-by-hostname',
|
||||
version: 1,
|
||||
id: 6,
|
||||
encodeKey (record) {
|
||||
return index6_key.encode(index6_indexify(record))
|
||||
},
|
||||
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
|
||||
return index6_key.encodeRange({
|
||||
gt: gt ? index6_indexify(gt) : null,
|
||||
lt: lt ? index6_indexify(lt) : null,
|
||||
gte: gte ? index6_indexify(gte) : null,
|
||||
lte: lte ? index6_indexify(lte) : null
|
||||
})
|
||||
},
|
||||
encodeValue: (record) => index6.collection.encodeKey(record),
|
||||
encodeIndexKeys (record, context) {
|
||||
return [index6_key.encode([record.hostname, record.nodeId])]
|
||||
},
|
||||
reconstruct: (keyBuf, valueBuf) => valueBuf,
|
||||
offset: collection0.indexes.length,
|
||||
collection: collection0
|
||||
}
|
||||
collection0.indexes.push(index6)
|
||||
|
||||
// '@peardata/peer-link-by-remote' collection key
|
||||
const index7_key = new IndexEncoder([
|
||||
IndexEncoder.STRING,
|
||||
IndexEncoder.STRING,
|
||||
IndexEncoder.STRING
|
||||
], { prefix: 7 })
|
||||
|
||||
function index7_indexify (record) {
|
||||
const arr = []
|
||||
|
||||
const a0 = record.remotePublicKey
|
||||
if (a0 === undefined) return arr
|
||||
arr.push(a0)
|
||||
|
||||
const a1 = record.localNodeId
|
||||
if (a1 === undefined) return arr
|
||||
arr.push(a1)
|
||||
|
||||
const a2 = record.remotePublicKey
|
||||
if (a2 === undefined) return arr
|
||||
arr.push(a2)
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
// '@peardata/peer-link-by-remote'
|
||||
const index7 = {
|
||||
name: '@peardata/peer-link-by-remote',
|
||||
version: 1,
|
||||
id: 7,
|
||||
encodeKey (record) {
|
||||
return index7_key.encode(index7_indexify(record))
|
||||
},
|
||||
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
|
||||
return index7_key.encodeRange({
|
||||
gt: gt ? index7_indexify(gt) : null,
|
||||
lt: lt ? index7_indexify(lt) : null,
|
||||
gte: gte ? index7_indexify(gte) : null,
|
||||
lte: lte ? index7_indexify(lte) : null
|
||||
})
|
||||
},
|
||||
encodeValue: (record) => index7.collection.encodeKey(record),
|
||||
encodeIndexKeys (record, context) {
|
||||
return [index7_key.encode([record.remotePublicKey, record.localNodeId, record.remotePublicKey])]
|
||||
},
|
||||
reconstruct: (keyBuf, valueBuf) => valueBuf,
|
||||
offset: collection1.indexes.length,
|
||||
collection: collection1
|
||||
}
|
||||
collection1.indexes.push(index7)
|
||||
|
||||
// '@peardata/alert-event-by-chart' collection key
|
||||
const index8_key = new IndexEncoder([
|
||||
IndexEncoder.STRING,
|
||||
IndexEncoder.UINT,
|
||||
IndexEncoder.STRING,
|
||||
IndexEncoder.UINT
|
||||
], { prefix: 8 })
|
||||
|
||||
function index8_indexify (record) {
|
||||
const arr = []
|
||||
|
||||
const a0 = record.chart
|
||||
if (a0 === undefined) return arr
|
||||
arr.push(a0)
|
||||
|
||||
const a1 = record.ts
|
||||
if (a1 === undefined) return arr
|
||||
arr.push(a1)
|
||||
|
||||
const a2 = record.id
|
||||
if (a2 === undefined) return arr
|
||||
arr.push(a2)
|
||||
|
||||
const a3 = record.ts
|
||||
if (a3 === undefined) return arr
|
||||
arr.push(a3)
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
// '@peardata/alert-event-by-chart'
|
||||
const index8 = {
|
||||
name: '@peardata/alert-event-by-chart',
|
||||
version: 1,
|
||||
id: 8,
|
||||
encodeKey (record) {
|
||||
return index8_key.encode(index8_indexify(record))
|
||||
},
|
||||
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
|
||||
return index8_key.encodeRange({
|
||||
gt: gt ? index8_indexify(gt) : null,
|
||||
lt: lt ? index8_indexify(lt) : null,
|
||||
gte: gte ? index8_indexify(gte) : null,
|
||||
lte: lte ? index8_indexify(lte) : null
|
||||
})
|
||||
},
|
||||
encodeValue: (record) => index8.collection.encodeKey(record),
|
||||
encodeIndexKeys (record, context) {
|
||||
return [index8_key.encode([record.chart, record.ts, record.id, record.ts])]
|
||||
},
|
||||
reconstruct: (keyBuf, valueBuf) => valueBuf,
|
||||
offset: collection3.indexes.length,
|
||||
collection: collection3
|
||||
}
|
||||
collection3.indexes.push(index8)
|
||||
|
||||
// '@peardata/metric-point-by-context' collection key
|
||||
const index9_key = new IndexEncoder([
|
||||
IndexEncoder.STRING,
|
||||
IndexEncoder.UINT,
|
||||
IndexEncoder.STRING,
|
||||
IndexEncoder.UINT
|
||||
], { prefix: 9 })
|
||||
|
||||
function index9_indexify (record) {
|
||||
const arr = []
|
||||
|
||||
const a0 = record.context
|
||||
if (a0 === undefined) return arr
|
||||
arr.push(a0)
|
||||
|
||||
const a1 = record.ts
|
||||
if (a1 === undefined) return arr
|
||||
arr.push(a1)
|
||||
|
||||
const a2 = record.chart
|
||||
if (a2 === undefined) return arr
|
||||
arr.push(a2)
|
||||
|
||||
const a3 = record.ts
|
||||
if (a3 === undefined) return arr
|
||||
arr.push(a3)
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
// '@peardata/metric-point-by-context'
|
||||
const index9 = {
|
||||
name: '@peardata/metric-point-by-context',
|
||||
version: 1,
|
||||
id: 9,
|
||||
encodeKey (record) {
|
||||
return index9_key.encode(index9_indexify(record))
|
||||
},
|
||||
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
|
||||
return index9_key.encodeRange({
|
||||
gt: gt ? index9_indexify(gt) : null,
|
||||
lt: lt ? index9_indexify(lt) : null,
|
||||
gte: gte ? index9_indexify(gte) : null,
|
||||
lte: lte ? index9_indexify(lte) : null
|
||||
})
|
||||
},
|
||||
encodeValue: (record) => index9.collection.encodeKey(record),
|
||||
encodeIndexKeys (record, context) {
|
||||
return [index9_key.encode([record.context, record.ts, record.chart, record.ts])]
|
||||
},
|
||||
reconstruct: (keyBuf, valueBuf) => valueBuf,
|
||||
offset: collection4.indexes.length,
|
||||
collection: collection4
|
||||
}
|
||||
collection4.indexes.push(index9)
|
||||
|
||||
// '@peardata/metric-point-by-tier' collection key
|
||||
const index10_key = new IndexEncoder([
|
||||
IndexEncoder.UINT,
|
||||
IndexEncoder.STRING,
|
||||
IndexEncoder.UINT,
|
||||
IndexEncoder.STRING,
|
||||
IndexEncoder.UINT
|
||||
], { prefix: 10 })
|
||||
|
||||
function index10_indexify (record) {
|
||||
const arr = []
|
||||
|
||||
const a0 = record.tier
|
||||
if (a0 === undefined) return arr
|
||||
arr.push(a0)
|
||||
|
||||
const a1 = record.chart
|
||||
if (a1 === undefined) return arr
|
||||
arr.push(a1)
|
||||
|
||||
const a2 = record.ts
|
||||
if (a2 === undefined) return arr
|
||||
arr.push(a2)
|
||||
|
||||
const a3 = record.chart
|
||||
if (a3 === undefined) return arr
|
||||
arr.push(a3)
|
||||
|
||||
const a4 = record.ts
|
||||
if (a4 === undefined) return arr
|
||||
arr.push(a4)
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
// '@peardata/metric-point-by-tier'
|
||||
const index10 = {
|
||||
name: '@peardata/metric-point-by-tier',
|
||||
version: 1,
|
||||
id: 10,
|
||||
encodeKey (record) {
|
||||
return index10_key.encode(index10_indexify(record))
|
||||
},
|
||||
encodeKeyRange ({ gt, lt, gte, lte } = {}) {
|
||||
return index10_key.encodeRange({
|
||||
gt: gt ? index10_indexify(gt) : null,
|
||||
lt: lt ? index10_indexify(lt) : null,
|
||||
gte: gte ? index10_indexify(gte) : null,
|
||||
lte: lte ? index10_indexify(lte) : null
|
||||
})
|
||||
},
|
||||
encodeValue: (record) => index10.collection.encodeKey(record),
|
||||
encodeIndexKeys (record, context) {
|
||||
return [index10_key.encode([record.tier, record.chart, record.ts, record.chart, record.ts])]
|
||||
},
|
||||
reconstruct: (keyBuf, valueBuf) => valueBuf,
|
||||
offset: collection4.indexes.length,
|
||||
collection: collection4
|
||||
}
|
||||
collection4.indexes.push(index10)
|
||||
|
||||
const collections = [
|
||||
collection0,
|
||||
collection1,
|
||||
collection2,
|
||||
collection3,
|
||||
collection4,
|
||||
collection5
|
||||
]
|
||||
|
||||
const indexes = [
|
||||
index6,
|
||||
index7,
|
||||
index8,
|
||||
index9,
|
||||
index10
|
||||
]
|
||||
|
||||
export default { versions, collections, indexes, resolveCollection, resolveIndex }
|
||||
|
||||
function resolveCollection (name) {
|
||||
switch (name) {
|
||||
case '@peardata/node': return collection0
|
||||
case '@peardata/peer-link': return collection1
|
||||
case '@peardata/alert-config': return collection2
|
||||
case '@peardata/alert-event': return collection3
|
||||
case '@peardata/metric-point': return collection4
|
||||
case '@peardata/job': return collection5
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
function resolveIndex (name) {
|
||||
switch (name) {
|
||||
case '@peardata/node-by-hostname': return index6
|
||||
case '@peardata/peer-link-by-remote': return index7
|
||||
case '@peardata/alert-event-by-chart': return index8
|
||||
case '@peardata/metric-point-by-context': return index9
|
||||
case '@peardata/metric-point-by-tier': return index10
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
// This file is autogenerated by the hyperschema compiler
|
||||
// Schema Version: 1
|
||||
/* eslint-disable camelcase */
|
||||
/* eslint-disable quotes */
|
||||
/* eslint-disable space-before-function-paren */
|
||||
|
||||
import { c } from 'hyperschema/runtime'
|
||||
|
||||
const VERSION = 1
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let version = VERSION
|
||||
|
||||
// @peardata/node
|
||||
const encoding0 = {
|
||||
preencode(state, m) {
|
||||
c.string.preencode(state, m.nodeId)
|
||||
c.string.preencode(state, m.hostname)
|
||||
state.end++ // max flag is 64 so always one byte
|
||||
|
||||
if (m.publicKeyHex) c.string.preencode(state, m.publicKeyHex)
|
||||
if (m.platform) c.string.preencode(state, m.platform)
|
||||
if (m.arch) c.string.preencode(state, m.arch)
|
||||
if (m.cpus) c.uint.preencode(state, m.cpus)
|
||||
if (m.totalMemMiB) c.uint.preencode(state, m.totalMemMiB)
|
||||
if (m.agentVersion) c.string.preencode(state, m.agentVersion)
|
||||
if (m.labelsJson) c.string.preencode(state, m.labelsJson)
|
||||
c.uint.preencode(state, m.updatedAt)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags =
|
||||
(m.publicKeyHex ? 1 : 0) |
|
||||
(m.platform ? 2 : 0) |
|
||||
(m.arch ? 4 : 0) |
|
||||
(m.cpus ? 8 : 0) |
|
||||
(m.totalMemMiB ? 16 : 0) |
|
||||
(m.agentVersion ? 32 : 0) |
|
||||
(m.labelsJson ? 64 : 0)
|
||||
|
||||
c.string.encode(state, m.nodeId)
|
||||
c.string.encode(state, m.hostname)
|
||||
c.uint.encode(state, flags)
|
||||
|
||||
if (m.publicKeyHex) c.string.encode(state, m.publicKeyHex)
|
||||
if (m.platform) c.string.encode(state, m.platform)
|
||||
if (m.arch) c.string.encode(state, m.arch)
|
||||
if (m.cpus) c.uint.encode(state, m.cpus)
|
||||
if (m.totalMemMiB) c.uint.encode(state, m.totalMemMiB)
|
||||
if (m.agentVersion) c.string.encode(state, m.agentVersion)
|
||||
if (m.labelsJson) c.string.encode(state, m.labelsJson)
|
||||
c.uint.encode(state, m.updatedAt)
|
||||
},
|
||||
decode(state) {
|
||||
const r0 = c.string.decode(state)
|
||||
const r1 = c.string.decode(state)
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
nodeId: r0,
|
||||
hostname: r1,
|
||||
publicKeyHex: (flags & 1) !== 0 ? c.string.decode(state) : null,
|
||||
platform: (flags & 2) !== 0 ? c.string.decode(state) : null,
|
||||
arch: (flags & 4) !== 0 ? c.string.decode(state) : null,
|
||||
cpus: (flags & 8) !== 0 ? c.uint.decode(state) : 0,
|
||||
totalMemMiB: (flags & 16) !== 0 ? c.uint.decode(state) : 0,
|
||||
agentVersion: (flags & 32) !== 0 ? c.string.decode(state) : null,
|
||||
labelsJson: (flags & 64) !== 0 ? c.string.decode(state) : null,
|
||||
updatedAt: c.uint.decode(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @peardata/peer-link
|
||||
const encoding1 = {
|
||||
preencode(state, m) {
|
||||
c.string.preencode(state, m.localNodeId)
|
||||
c.string.preencode(state, m.remotePublicKey)
|
||||
state.end++ // max flag is 16 so always one byte
|
||||
|
||||
if (m.role) c.string.preencode(state, m.role)
|
||||
if (m.alias) c.string.preencode(state, m.alias)
|
||||
if (m.dbKeyHex) c.string.preencode(state, m.dbKeyHex)
|
||||
if (m.syncMode) c.string.preencode(state, m.syncMode)
|
||||
c.uint.preencode(state, m.linkedAt)
|
||||
if (m.lastSeen) c.uint.preencode(state, m.lastSeen)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags =
|
||||
(m.role ? 1 : 0) |
|
||||
(m.alias ? 2 : 0) |
|
||||
(m.dbKeyHex ? 4 : 0) |
|
||||
(m.syncMode ? 8 : 0) |
|
||||
(m.lastSeen ? 16 : 0)
|
||||
|
||||
c.string.encode(state, m.localNodeId)
|
||||
c.string.encode(state, m.remotePublicKey)
|
||||
c.uint.encode(state, flags)
|
||||
|
||||
if (m.role) c.string.encode(state, m.role)
|
||||
if (m.alias) c.string.encode(state, m.alias)
|
||||
if (m.dbKeyHex) c.string.encode(state, m.dbKeyHex)
|
||||
if (m.syncMode) c.string.encode(state, m.syncMode)
|
||||
c.uint.encode(state, m.linkedAt)
|
||||
if (m.lastSeen) c.uint.encode(state, m.lastSeen)
|
||||
},
|
||||
decode(state) {
|
||||
const r0 = c.string.decode(state)
|
||||
const r1 = c.string.decode(state)
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
localNodeId: r0,
|
||||
remotePublicKey: r1,
|
||||
role: (flags & 1) !== 0 ? c.string.decode(state) : null,
|
||||
alias: (flags & 2) !== 0 ? c.string.decode(state) : null,
|
||||
dbKeyHex: (flags & 4) !== 0 ? c.string.decode(state) : null,
|
||||
syncMode: (flags & 8) !== 0 ? c.string.decode(state) : null,
|
||||
linkedAt: c.uint.decode(state),
|
||||
lastSeen: (flags & 16) !== 0 ? c.uint.decode(state) : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @peardata/alert-config
|
||||
const encoding2 = {
|
||||
preencode(state, m) {
|
||||
c.string.preencode(state, m.id)
|
||||
c.string.preencode(state, m.chart)
|
||||
c.string.preencode(state, m.dimension)
|
||||
state.end++ // max flag is 16 so always one byte
|
||||
|
||||
if (m.warn) c.float64.preencode(state, m.warn)
|
||||
if (m.crit) c.float64.preencode(state, m.crit)
|
||||
if (m.comparator) c.string.preencode(state, m.comparator)
|
||||
if (m.info) c.string.preencode(state, m.info)
|
||||
c.uint.preencode(state, m.updatedAt)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags =
|
||||
(m.warn ? 1 : 0) |
|
||||
(m.crit ? 2 : 0) |
|
||||
(m.comparator ? 4 : 0) |
|
||||
(m.enabled ? 8 : 0) |
|
||||
(m.info ? 16 : 0)
|
||||
|
||||
c.string.encode(state, m.id)
|
||||
c.string.encode(state, m.chart)
|
||||
c.string.encode(state, m.dimension)
|
||||
c.uint.encode(state, flags)
|
||||
|
||||
if (m.warn) c.float64.encode(state, m.warn)
|
||||
if (m.crit) c.float64.encode(state, m.crit)
|
||||
if (m.comparator) c.string.encode(state, m.comparator)
|
||||
if (m.info) c.string.encode(state, m.info)
|
||||
c.uint.encode(state, m.updatedAt)
|
||||
},
|
||||
decode(state) {
|
||||
const r0 = c.string.decode(state)
|
||||
const r1 = c.string.decode(state)
|
||||
const r2 = c.string.decode(state)
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
id: r0,
|
||||
chart: r1,
|
||||
dimension: r2,
|
||||
warn: (flags & 1) !== 0 ? c.float64.decode(state) : 0,
|
||||
crit: (flags & 2) !== 0 ? c.float64.decode(state) : 0,
|
||||
comparator: (flags & 4) !== 0 ? c.string.decode(state) : null,
|
||||
enabled: (flags & 8) !== 0,
|
||||
info: (flags & 16) !== 0 ? c.string.decode(state) : null,
|
||||
updatedAt: c.uint.decode(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @peardata/alert-event
|
||||
const encoding3 = {
|
||||
preencode(state, m) {
|
||||
c.string.preencode(state, m.id)
|
||||
c.uint.preencode(state, m.ts)
|
||||
c.string.preencode(state, m.chart)
|
||||
state.end++ // max flag is 16 so always one byte
|
||||
|
||||
if (m.dimension) c.string.preencode(state, m.dimension)
|
||||
c.string.preencode(state, m.severity)
|
||||
if (m.value) c.float64.preencode(state, m.value)
|
||||
if (m.threshold) c.float64.preencode(state, m.threshold)
|
||||
if (m.message) c.string.preencode(state, m.message)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags =
|
||||
(m.dimension ? 1 : 0) |
|
||||
(m.value ? 2 : 0) |
|
||||
(m.threshold ? 4 : 0) |
|
||||
(m.message ? 8 : 0) |
|
||||
(m.cleared ? 16 : 0)
|
||||
|
||||
c.string.encode(state, m.id)
|
||||
c.uint.encode(state, m.ts)
|
||||
c.string.encode(state, m.chart)
|
||||
c.uint.encode(state, flags)
|
||||
|
||||
if (m.dimension) c.string.encode(state, m.dimension)
|
||||
c.string.encode(state, m.severity)
|
||||
if (m.value) c.float64.encode(state, m.value)
|
||||
if (m.threshold) c.float64.encode(state, m.threshold)
|
||||
if (m.message) c.string.encode(state, m.message)
|
||||
},
|
||||
decode(state) {
|
||||
const r0 = c.string.decode(state)
|
||||
const r1 = c.uint.decode(state)
|
||||
const r2 = c.string.decode(state)
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
id: r0,
|
||||
ts: r1,
|
||||
chart: r2,
|
||||
dimension: (flags & 1) !== 0 ? c.string.decode(state) : null,
|
||||
severity: c.string.decode(state),
|
||||
value: (flags & 2) !== 0 ? c.float64.decode(state) : 0,
|
||||
threshold: (flags & 4) !== 0 ? c.float64.decode(state) : 0,
|
||||
message: (flags & 8) !== 0 ? c.string.decode(state) : null,
|
||||
cleared: (flags & 16) !== 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @peardata/metric-point
|
||||
const encoding4 = {
|
||||
preencode(state, m) {
|
||||
c.string.preencode(state, m.chart)
|
||||
c.uint.preencode(state, m.ts)
|
||||
c.string.preencode(state, m.context)
|
||||
c.uint.preencode(state, m.tier)
|
||||
c.string.preencode(state, m.valuesJson)
|
||||
},
|
||||
encode(state, m) {
|
||||
c.string.encode(state, m.chart)
|
||||
c.uint.encode(state, m.ts)
|
||||
c.string.encode(state, m.context)
|
||||
c.uint.encode(state, m.tier)
|
||||
c.string.encode(state, m.valuesJson)
|
||||
},
|
||||
decode(state) {
|
||||
const r0 = c.string.decode(state)
|
||||
const r1 = c.uint.decode(state)
|
||||
const r2 = c.string.decode(state)
|
||||
const r3 = c.uint.decode(state)
|
||||
const r4 = c.string.decode(state)
|
||||
|
||||
return {
|
||||
chart: r0,
|
||||
ts: r1,
|
||||
context: r2,
|
||||
tier: r3,
|
||||
valuesJson: r4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @peardata/job
|
||||
const encoding5 = {
|
||||
preencode(state, m) {
|
||||
c.string.preencode(state, m.id)
|
||||
c.string.preencode(state, m.name)
|
||||
c.string.preencode(state, m.status)
|
||||
state.end++ // max flag is 8 so always one byte
|
||||
|
||||
if (m.startedAt) c.uint.preencode(state, m.startedAt)
|
||||
if (m.finishedAt) c.uint.preencode(state, m.finishedAt)
|
||||
if (m.resultJson) c.string.preencode(state, m.resultJson)
|
||||
if (m.error) c.string.preencode(state, m.error)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags =
|
||||
(m.startedAt ? 1 : 0) | (m.finishedAt ? 2 : 0) | (m.resultJson ? 4 : 0) | (m.error ? 8 : 0)
|
||||
|
||||
c.string.encode(state, m.id)
|
||||
c.string.encode(state, m.name)
|
||||
c.string.encode(state, m.status)
|
||||
c.uint.encode(state, flags)
|
||||
|
||||
if (m.startedAt) c.uint.encode(state, m.startedAt)
|
||||
if (m.finishedAt) c.uint.encode(state, m.finishedAt)
|
||||
if (m.resultJson) c.string.encode(state, m.resultJson)
|
||||
if (m.error) c.string.encode(state, m.error)
|
||||
},
|
||||
decode(state) {
|
||||
const r0 = c.string.decode(state)
|
||||
const r1 = c.string.decode(state)
|
||||
const r2 = c.string.decode(state)
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
id: r0,
|
||||
name: r1,
|
||||
status: r2,
|
||||
startedAt: (flags & 1) !== 0 ? c.uint.decode(state) : 0,
|
||||
finishedAt: (flags & 2) !== 0 ? c.uint.decode(state) : 0,
|
||||
resultJson: (flags & 4) !== 0 ? c.string.decode(state) : null,
|
||||
error: (flags & 8) !== 0 ? c.string.decode(state) : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @peardata/node/hyperdb#0
|
||||
const encoding6 = {
|
||||
preencode(state, m) {
|
||||
c.string.preencode(state, m.hostname)
|
||||
state.end++ // max flag is 64 so always one byte
|
||||
|
||||
if (m.publicKeyHex) c.string.preencode(state, m.publicKeyHex)
|
||||
if (m.platform) c.string.preencode(state, m.platform)
|
||||
if (m.arch) c.string.preencode(state, m.arch)
|
||||
if (m.cpus) c.uint.preencode(state, m.cpus)
|
||||
if (m.totalMemMiB) c.uint.preencode(state, m.totalMemMiB)
|
||||
if (m.agentVersion) c.string.preencode(state, m.agentVersion)
|
||||
if (m.labelsJson) c.string.preencode(state, m.labelsJson)
|
||||
c.uint.preencode(state, m.updatedAt)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags =
|
||||
(m.publicKeyHex ? 1 : 0) |
|
||||
(m.platform ? 2 : 0) |
|
||||
(m.arch ? 4 : 0) |
|
||||
(m.cpus ? 8 : 0) |
|
||||
(m.totalMemMiB ? 16 : 0) |
|
||||
(m.agentVersion ? 32 : 0) |
|
||||
(m.labelsJson ? 64 : 0)
|
||||
|
||||
c.string.encode(state, m.hostname)
|
||||
c.uint.encode(state, flags)
|
||||
|
||||
if (m.publicKeyHex) c.string.encode(state, m.publicKeyHex)
|
||||
if (m.platform) c.string.encode(state, m.platform)
|
||||
if (m.arch) c.string.encode(state, m.arch)
|
||||
if (m.cpus) c.uint.encode(state, m.cpus)
|
||||
if (m.totalMemMiB) c.uint.encode(state, m.totalMemMiB)
|
||||
if (m.agentVersion) c.string.encode(state, m.agentVersion)
|
||||
if (m.labelsJson) c.string.encode(state, m.labelsJson)
|
||||
c.uint.encode(state, m.updatedAt)
|
||||
},
|
||||
decode(state) {
|
||||
const r1 = c.string.decode(state)
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
nodeId: null,
|
||||
hostname: r1,
|
||||
publicKeyHex: (flags & 1) !== 0 ? c.string.decode(state) : null,
|
||||
platform: (flags & 2) !== 0 ? c.string.decode(state) : null,
|
||||
arch: (flags & 4) !== 0 ? c.string.decode(state) : null,
|
||||
cpus: (flags & 8) !== 0 ? c.uint.decode(state) : 0,
|
||||
totalMemMiB: (flags & 16) !== 0 ? c.uint.decode(state) : 0,
|
||||
agentVersion: (flags & 32) !== 0 ? c.string.decode(state) : null,
|
||||
labelsJson: (flags & 64) !== 0 ? c.string.decode(state) : null,
|
||||
updatedAt: c.uint.decode(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @peardata/peer-link/hyperdb#1
|
||||
const encoding7 = {
|
||||
preencode(state, m) {
|
||||
state.end++ // max flag is 16 so always one byte
|
||||
|
||||
if (m.role) c.string.preencode(state, m.role)
|
||||
if (m.alias) c.string.preencode(state, m.alias)
|
||||
if (m.dbKeyHex) c.string.preencode(state, m.dbKeyHex)
|
||||
if (m.syncMode) c.string.preencode(state, m.syncMode)
|
||||
c.uint.preencode(state, m.linkedAt)
|
||||
if (m.lastSeen) c.uint.preencode(state, m.lastSeen)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags =
|
||||
(m.role ? 1 : 0) |
|
||||
(m.alias ? 2 : 0) |
|
||||
(m.dbKeyHex ? 4 : 0) |
|
||||
(m.syncMode ? 8 : 0) |
|
||||
(m.lastSeen ? 16 : 0)
|
||||
|
||||
c.uint.encode(state, flags)
|
||||
|
||||
if (m.role) c.string.encode(state, m.role)
|
||||
if (m.alias) c.string.encode(state, m.alias)
|
||||
if (m.dbKeyHex) c.string.encode(state, m.dbKeyHex)
|
||||
if (m.syncMode) c.string.encode(state, m.syncMode)
|
||||
c.uint.encode(state, m.linkedAt)
|
||||
if (m.lastSeen) c.uint.encode(state, m.lastSeen)
|
||||
},
|
||||
decode(state) {
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
localNodeId: null,
|
||||
remotePublicKey: null,
|
||||
role: (flags & 1) !== 0 ? c.string.decode(state) : null,
|
||||
alias: (flags & 2) !== 0 ? c.string.decode(state) : null,
|
||||
dbKeyHex: (flags & 4) !== 0 ? c.string.decode(state) : null,
|
||||
syncMode: (flags & 8) !== 0 ? c.string.decode(state) : null,
|
||||
linkedAt: c.uint.decode(state),
|
||||
lastSeen: (flags & 16) !== 0 ? c.uint.decode(state) : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @peardata/alert-config/hyperdb#2
|
||||
const encoding8 = {
|
||||
preencode(state, m) {
|
||||
c.string.preencode(state, m.chart)
|
||||
c.string.preencode(state, m.dimension)
|
||||
state.end++ // max flag is 16 so always one byte
|
||||
|
||||
if (m.warn) c.float64.preencode(state, m.warn)
|
||||
if (m.crit) c.float64.preencode(state, m.crit)
|
||||
if (m.comparator) c.string.preencode(state, m.comparator)
|
||||
if (m.info) c.string.preencode(state, m.info)
|
||||
c.uint.preencode(state, m.updatedAt)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags =
|
||||
(m.warn ? 1 : 0) |
|
||||
(m.crit ? 2 : 0) |
|
||||
(m.comparator ? 4 : 0) |
|
||||
(m.enabled ? 8 : 0) |
|
||||
(m.info ? 16 : 0)
|
||||
|
||||
c.string.encode(state, m.chart)
|
||||
c.string.encode(state, m.dimension)
|
||||
c.uint.encode(state, flags)
|
||||
|
||||
if (m.warn) c.float64.encode(state, m.warn)
|
||||
if (m.crit) c.float64.encode(state, m.crit)
|
||||
if (m.comparator) c.string.encode(state, m.comparator)
|
||||
if (m.info) c.string.encode(state, m.info)
|
||||
c.uint.encode(state, m.updatedAt)
|
||||
},
|
||||
decode(state) {
|
||||
const r1 = c.string.decode(state)
|
||||
const r2 = c.string.decode(state)
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
id: null,
|
||||
chart: r1,
|
||||
dimension: r2,
|
||||
warn: (flags & 1) !== 0 ? c.float64.decode(state) : 0,
|
||||
crit: (flags & 2) !== 0 ? c.float64.decode(state) : 0,
|
||||
comparator: (flags & 4) !== 0 ? c.string.decode(state) : null,
|
||||
enabled: (flags & 8) !== 0,
|
||||
info: (flags & 16) !== 0 ? c.string.decode(state) : null,
|
||||
updatedAt: c.uint.decode(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @peardata/alert-event/hyperdb#3
|
||||
const encoding9 = {
|
||||
preencode(state, m) {
|
||||
c.string.preencode(state, m.chart)
|
||||
state.end++ // max flag is 16 so always one byte
|
||||
|
||||
if (m.dimension) c.string.preencode(state, m.dimension)
|
||||
c.string.preencode(state, m.severity)
|
||||
if (m.value) c.float64.preencode(state, m.value)
|
||||
if (m.threshold) c.float64.preencode(state, m.threshold)
|
||||
if (m.message) c.string.preencode(state, m.message)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags =
|
||||
(m.dimension ? 1 : 0) |
|
||||
(m.value ? 2 : 0) |
|
||||
(m.threshold ? 4 : 0) |
|
||||
(m.message ? 8 : 0) |
|
||||
(m.cleared ? 16 : 0)
|
||||
|
||||
c.string.encode(state, m.chart)
|
||||
c.uint.encode(state, flags)
|
||||
|
||||
if (m.dimension) c.string.encode(state, m.dimension)
|
||||
c.string.encode(state, m.severity)
|
||||
if (m.value) c.float64.encode(state, m.value)
|
||||
if (m.threshold) c.float64.encode(state, m.threshold)
|
||||
if (m.message) c.string.encode(state, m.message)
|
||||
},
|
||||
decode(state) {
|
||||
const r2 = c.string.decode(state)
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
id: null,
|
||||
ts: 0,
|
||||
chart: r2,
|
||||
dimension: (flags & 1) !== 0 ? c.string.decode(state) : null,
|
||||
severity: c.string.decode(state),
|
||||
value: (flags & 2) !== 0 ? c.float64.decode(state) : 0,
|
||||
threshold: (flags & 4) !== 0 ? c.float64.decode(state) : 0,
|
||||
message: (flags & 8) !== 0 ? c.string.decode(state) : null,
|
||||
cleared: (flags & 16) !== 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @peardata/metric-point/hyperdb#4
|
||||
const encoding10 = {
|
||||
preencode(state, m) {
|
||||
c.string.preencode(state, m.context)
|
||||
c.uint.preencode(state, m.tier)
|
||||
c.string.preencode(state, m.valuesJson)
|
||||
},
|
||||
encode(state, m) {
|
||||
c.string.encode(state, m.context)
|
||||
c.uint.encode(state, m.tier)
|
||||
c.string.encode(state, m.valuesJson)
|
||||
},
|
||||
decode(state) {
|
||||
const r2 = c.string.decode(state)
|
||||
const r3 = c.uint.decode(state)
|
||||
const r4 = c.string.decode(state)
|
||||
|
||||
return {
|
||||
chart: null,
|
||||
ts: 0,
|
||||
context: r2,
|
||||
tier: r3,
|
||||
valuesJson: r4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @peardata/job/hyperdb#5
|
||||
const encoding11 = {
|
||||
preencode(state, m) {
|
||||
c.string.preencode(state, m.name)
|
||||
c.string.preencode(state, m.status)
|
||||
state.end++ // max flag is 8 so always one byte
|
||||
|
||||
if (m.startedAt) c.uint.preencode(state, m.startedAt)
|
||||
if (m.finishedAt) c.uint.preencode(state, m.finishedAt)
|
||||
if (m.resultJson) c.string.preencode(state, m.resultJson)
|
||||
if (m.error) c.string.preencode(state, m.error)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags =
|
||||
(m.startedAt ? 1 : 0) | (m.finishedAt ? 2 : 0) | (m.resultJson ? 4 : 0) | (m.error ? 8 : 0)
|
||||
|
||||
c.string.encode(state, m.name)
|
||||
c.string.encode(state, m.status)
|
||||
c.uint.encode(state, flags)
|
||||
|
||||
if (m.startedAt) c.uint.encode(state, m.startedAt)
|
||||
if (m.finishedAt) c.uint.encode(state, m.finishedAt)
|
||||
if (m.resultJson) c.string.encode(state, m.resultJson)
|
||||
if (m.error) c.string.encode(state, m.error)
|
||||
},
|
||||
decode(state) {
|
||||
const r1 = c.string.decode(state)
|
||||
const r2 = c.string.decode(state)
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
id: null,
|
||||
name: r1,
|
||||
status: r2,
|
||||
startedAt: (flags & 1) !== 0 ? c.uint.decode(state) : 0,
|
||||
finishedAt: (flags & 2) !== 0 ? c.uint.decode(state) : 0,
|
||||
resultJson: (flags & 4) !== 0 ? c.string.decode(state) : null,
|
||||
error: (flags & 8) !== 0 ? c.string.decode(state) : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setVersion(v) {
|
||||
version = v
|
||||
}
|
||||
|
||||
function encode(name, value, v = VERSION) {
|
||||
version = v
|
||||
return c.encode(getEncoding(name), value)
|
||||
}
|
||||
|
||||
function decode(name, buffer, v = VERSION) {
|
||||
version = v
|
||||
return c.decode(getEncoding(name), buffer)
|
||||
}
|
||||
|
||||
function getEnum(name) {
|
||||
switch (name) {
|
||||
default:
|
||||
throw new Error('Enum not found ' + name)
|
||||
}
|
||||
}
|
||||
|
||||
function getEncoding(name) {
|
||||
switch (name) {
|
||||
case '@peardata/node':
|
||||
return encoding0
|
||||
case '@peardata/peer-link':
|
||||
return encoding1
|
||||
case '@peardata/alert-config':
|
||||
return encoding2
|
||||
case '@peardata/alert-event':
|
||||
return encoding3
|
||||
case '@peardata/metric-point':
|
||||
return encoding4
|
||||
case '@peardata/job':
|
||||
return encoding5
|
||||
case '@peardata/node/hyperdb#0':
|
||||
return encoding6
|
||||
case '@peardata/peer-link/hyperdb#1':
|
||||
return encoding7
|
||||
case '@peardata/alert-config/hyperdb#2':
|
||||
return encoding8
|
||||
case '@peardata/alert-event/hyperdb#3':
|
||||
return encoding9
|
||||
case '@peardata/metric-point/hyperdb#4':
|
||||
return encoding10
|
||||
case '@peardata/job/hyperdb#5':
|
||||
return encoding11
|
||||
default:
|
||||
throw new Error('Encoder not found ' + name)
|
||||
}
|
||||
}
|
||||
|
||||
function getStruct(name, v = VERSION) {
|
||||
const enc = getEncoding(name)
|
||||
return {
|
||||
preencode(state, m) {
|
||||
version = v
|
||||
enc.preencode(state, m)
|
||||
},
|
||||
encode(state, m) {
|
||||
version = v
|
||||
enc.encode(state, m)
|
||||
},
|
||||
decode(state) {
|
||||
version = v
|
||||
return enc.decode(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resolveStruct = getStruct // compat
|
||||
|
||||
export { resolveStruct, getStruct, getEnum, getEncoding, encode, decode, setVersion, version }
|
||||
@@ -0,0 +1,368 @@
|
||||
// This file is autogenerated by the hyperschema compiler
|
||||
// Schema Version: 1
|
||||
/* eslint-disable camelcase */
|
||||
/* eslint-disable quotes */
|
||||
/* eslint-disable space-before-function-paren */
|
||||
|
||||
import { c } from 'hyperschema/runtime'
|
||||
|
||||
const VERSION = 1
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let version = VERSION
|
||||
|
||||
// @peardata/node
|
||||
const encoding0 = {
|
||||
preencode(state, m) {
|
||||
c.string.preencode(state, m.nodeId)
|
||||
c.string.preencode(state, m.hostname)
|
||||
state.end++ // max flag is 64 so always one byte
|
||||
|
||||
if (m.publicKeyHex) c.string.preencode(state, m.publicKeyHex)
|
||||
if (m.platform) c.string.preencode(state, m.platform)
|
||||
if (m.arch) c.string.preencode(state, m.arch)
|
||||
if (m.cpus) c.uint.preencode(state, m.cpus)
|
||||
if (m.totalMemMiB) c.uint.preencode(state, m.totalMemMiB)
|
||||
if (m.agentVersion) c.string.preencode(state, m.agentVersion)
|
||||
if (m.labelsJson) c.string.preencode(state, m.labelsJson)
|
||||
c.uint.preencode(state, m.updatedAt)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags =
|
||||
(m.publicKeyHex ? 1 : 0) |
|
||||
(m.platform ? 2 : 0) |
|
||||
(m.arch ? 4 : 0) |
|
||||
(m.cpus ? 8 : 0) |
|
||||
(m.totalMemMiB ? 16 : 0) |
|
||||
(m.agentVersion ? 32 : 0) |
|
||||
(m.labelsJson ? 64 : 0)
|
||||
|
||||
c.string.encode(state, m.nodeId)
|
||||
c.string.encode(state, m.hostname)
|
||||
c.uint.encode(state, flags)
|
||||
|
||||
if (m.publicKeyHex) c.string.encode(state, m.publicKeyHex)
|
||||
if (m.platform) c.string.encode(state, m.platform)
|
||||
if (m.arch) c.string.encode(state, m.arch)
|
||||
if (m.cpus) c.uint.encode(state, m.cpus)
|
||||
if (m.totalMemMiB) c.uint.encode(state, m.totalMemMiB)
|
||||
if (m.agentVersion) c.string.encode(state, m.agentVersion)
|
||||
if (m.labelsJson) c.string.encode(state, m.labelsJson)
|
||||
c.uint.encode(state, m.updatedAt)
|
||||
},
|
||||
decode(state) {
|
||||
const r0 = c.string.decode(state)
|
||||
const r1 = c.string.decode(state)
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
nodeId: r0,
|
||||
hostname: r1,
|
||||
publicKeyHex: (flags & 1) !== 0 ? c.string.decode(state) : null,
|
||||
platform: (flags & 2) !== 0 ? c.string.decode(state) : null,
|
||||
arch: (flags & 4) !== 0 ? c.string.decode(state) : null,
|
||||
cpus: (flags & 8) !== 0 ? c.uint.decode(state) : 0,
|
||||
totalMemMiB: (flags & 16) !== 0 ? c.uint.decode(state) : 0,
|
||||
agentVersion: (flags & 32) !== 0 ? c.string.decode(state) : null,
|
||||
labelsJson: (flags & 64) !== 0 ? c.string.decode(state) : null,
|
||||
updatedAt: c.uint.decode(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @peardata/peer-link
|
||||
const encoding1 = {
|
||||
preencode(state, m) {
|
||||
c.string.preencode(state, m.localNodeId)
|
||||
c.string.preencode(state, m.remotePublicKey)
|
||||
state.end++ // max flag is 16 so always one byte
|
||||
|
||||
if (m.role) c.string.preencode(state, m.role)
|
||||
if (m.alias) c.string.preencode(state, m.alias)
|
||||
if (m.dbKeyHex) c.string.preencode(state, m.dbKeyHex)
|
||||
if (m.syncMode) c.string.preencode(state, m.syncMode)
|
||||
c.uint.preencode(state, m.linkedAt)
|
||||
if (m.lastSeen) c.uint.preencode(state, m.lastSeen)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags =
|
||||
(m.role ? 1 : 0) |
|
||||
(m.alias ? 2 : 0) |
|
||||
(m.dbKeyHex ? 4 : 0) |
|
||||
(m.syncMode ? 8 : 0) |
|
||||
(m.lastSeen ? 16 : 0)
|
||||
|
||||
c.string.encode(state, m.localNodeId)
|
||||
c.string.encode(state, m.remotePublicKey)
|
||||
c.uint.encode(state, flags)
|
||||
|
||||
if (m.role) c.string.encode(state, m.role)
|
||||
if (m.alias) c.string.encode(state, m.alias)
|
||||
if (m.dbKeyHex) c.string.encode(state, m.dbKeyHex)
|
||||
if (m.syncMode) c.string.encode(state, m.syncMode)
|
||||
c.uint.encode(state, m.linkedAt)
|
||||
if (m.lastSeen) c.uint.encode(state, m.lastSeen)
|
||||
},
|
||||
decode(state) {
|
||||
const r0 = c.string.decode(state)
|
||||
const r1 = c.string.decode(state)
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
localNodeId: r0,
|
||||
remotePublicKey: r1,
|
||||
role: (flags & 1) !== 0 ? c.string.decode(state) : null,
|
||||
alias: (flags & 2) !== 0 ? c.string.decode(state) : null,
|
||||
dbKeyHex: (flags & 4) !== 0 ? c.string.decode(state) : null,
|
||||
syncMode: (flags & 8) !== 0 ? c.string.decode(state) : null,
|
||||
linkedAt: c.uint.decode(state),
|
||||
lastSeen: (flags & 16) !== 0 ? c.uint.decode(state) : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @peardata/alert-config
|
||||
const encoding2 = {
|
||||
preencode(state, m) {
|
||||
c.string.preencode(state, m.id)
|
||||
c.string.preencode(state, m.chart)
|
||||
c.string.preencode(state, m.dimension)
|
||||
state.end++ // max flag is 16 so always one byte
|
||||
|
||||
if (m.warn) c.float64.preencode(state, m.warn)
|
||||
if (m.crit) c.float64.preencode(state, m.crit)
|
||||
if (m.comparator) c.string.preencode(state, m.comparator)
|
||||
if (m.info) c.string.preencode(state, m.info)
|
||||
c.uint.preencode(state, m.updatedAt)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags =
|
||||
(m.warn ? 1 : 0) |
|
||||
(m.crit ? 2 : 0) |
|
||||
(m.comparator ? 4 : 0) |
|
||||
(m.enabled ? 8 : 0) |
|
||||
(m.info ? 16 : 0)
|
||||
|
||||
c.string.encode(state, m.id)
|
||||
c.string.encode(state, m.chart)
|
||||
c.string.encode(state, m.dimension)
|
||||
c.uint.encode(state, flags)
|
||||
|
||||
if (m.warn) c.float64.encode(state, m.warn)
|
||||
if (m.crit) c.float64.encode(state, m.crit)
|
||||
if (m.comparator) c.string.encode(state, m.comparator)
|
||||
if (m.info) c.string.encode(state, m.info)
|
||||
c.uint.encode(state, m.updatedAt)
|
||||
},
|
||||
decode(state) {
|
||||
const r0 = c.string.decode(state)
|
||||
const r1 = c.string.decode(state)
|
||||
const r2 = c.string.decode(state)
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
id: r0,
|
||||
chart: r1,
|
||||
dimension: r2,
|
||||
warn: (flags & 1) !== 0 ? c.float64.decode(state) : 0,
|
||||
crit: (flags & 2) !== 0 ? c.float64.decode(state) : 0,
|
||||
comparator: (flags & 4) !== 0 ? c.string.decode(state) : null,
|
||||
enabled: (flags & 8) !== 0,
|
||||
info: (flags & 16) !== 0 ? c.string.decode(state) : null,
|
||||
updatedAt: c.uint.decode(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @peardata/alert-event
|
||||
const encoding3 = {
|
||||
preencode(state, m) {
|
||||
c.string.preencode(state, m.id)
|
||||
c.uint.preencode(state, m.ts)
|
||||
c.string.preencode(state, m.chart)
|
||||
state.end++ // max flag is 16 so always one byte
|
||||
|
||||
if (m.dimension) c.string.preencode(state, m.dimension)
|
||||
c.string.preencode(state, m.severity)
|
||||
if (m.value) c.float64.preencode(state, m.value)
|
||||
if (m.threshold) c.float64.preencode(state, m.threshold)
|
||||
if (m.message) c.string.preencode(state, m.message)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags =
|
||||
(m.dimension ? 1 : 0) |
|
||||
(m.value ? 2 : 0) |
|
||||
(m.threshold ? 4 : 0) |
|
||||
(m.message ? 8 : 0) |
|
||||
(m.cleared ? 16 : 0)
|
||||
|
||||
c.string.encode(state, m.id)
|
||||
c.uint.encode(state, m.ts)
|
||||
c.string.encode(state, m.chart)
|
||||
c.uint.encode(state, flags)
|
||||
|
||||
if (m.dimension) c.string.encode(state, m.dimension)
|
||||
c.string.encode(state, m.severity)
|
||||
if (m.value) c.float64.encode(state, m.value)
|
||||
if (m.threshold) c.float64.encode(state, m.threshold)
|
||||
if (m.message) c.string.encode(state, m.message)
|
||||
},
|
||||
decode(state) {
|
||||
const r0 = c.string.decode(state)
|
||||
const r1 = c.uint.decode(state)
|
||||
const r2 = c.string.decode(state)
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
id: r0,
|
||||
ts: r1,
|
||||
chart: r2,
|
||||
dimension: (flags & 1) !== 0 ? c.string.decode(state) : null,
|
||||
severity: c.string.decode(state),
|
||||
value: (flags & 2) !== 0 ? c.float64.decode(state) : 0,
|
||||
threshold: (flags & 4) !== 0 ? c.float64.decode(state) : 0,
|
||||
message: (flags & 8) !== 0 ? c.string.decode(state) : null,
|
||||
cleared: (flags & 16) !== 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @peardata/metric-point
|
||||
const encoding4 = {
|
||||
preencode(state, m) {
|
||||
c.string.preencode(state, m.chart)
|
||||
c.uint.preencode(state, m.ts)
|
||||
c.string.preencode(state, m.context)
|
||||
c.uint.preencode(state, m.tier)
|
||||
c.string.preencode(state, m.valuesJson)
|
||||
},
|
||||
encode(state, m) {
|
||||
c.string.encode(state, m.chart)
|
||||
c.uint.encode(state, m.ts)
|
||||
c.string.encode(state, m.context)
|
||||
c.uint.encode(state, m.tier)
|
||||
c.string.encode(state, m.valuesJson)
|
||||
},
|
||||
decode(state) {
|
||||
const r0 = c.string.decode(state)
|
||||
const r1 = c.uint.decode(state)
|
||||
const r2 = c.string.decode(state)
|
||||
const r3 = c.uint.decode(state)
|
||||
const r4 = c.string.decode(state)
|
||||
|
||||
return {
|
||||
chart: r0,
|
||||
ts: r1,
|
||||
context: r2,
|
||||
tier: r3,
|
||||
valuesJson: r4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @peardata/job
|
||||
const encoding5 = {
|
||||
preencode(state, m) {
|
||||
c.string.preencode(state, m.id)
|
||||
c.string.preencode(state, m.name)
|
||||
c.string.preencode(state, m.status)
|
||||
state.end++ // max flag is 8 so always one byte
|
||||
|
||||
if (m.startedAt) c.uint.preencode(state, m.startedAt)
|
||||
if (m.finishedAt) c.uint.preencode(state, m.finishedAt)
|
||||
if (m.resultJson) c.string.preencode(state, m.resultJson)
|
||||
if (m.error) c.string.preencode(state, m.error)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags =
|
||||
(m.startedAt ? 1 : 0) | (m.finishedAt ? 2 : 0) | (m.resultJson ? 4 : 0) | (m.error ? 8 : 0)
|
||||
|
||||
c.string.encode(state, m.id)
|
||||
c.string.encode(state, m.name)
|
||||
c.string.encode(state, m.status)
|
||||
c.uint.encode(state, flags)
|
||||
|
||||
if (m.startedAt) c.uint.encode(state, m.startedAt)
|
||||
if (m.finishedAt) c.uint.encode(state, m.finishedAt)
|
||||
if (m.resultJson) c.string.encode(state, m.resultJson)
|
||||
if (m.error) c.string.encode(state, m.error)
|
||||
},
|
||||
decode(state) {
|
||||
const r0 = c.string.decode(state)
|
||||
const r1 = c.string.decode(state)
|
||||
const r2 = c.string.decode(state)
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
id: r0,
|
||||
name: r1,
|
||||
status: r2,
|
||||
startedAt: (flags & 1) !== 0 ? c.uint.decode(state) : 0,
|
||||
finishedAt: (flags & 2) !== 0 ? c.uint.decode(state) : 0,
|
||||
resultJson: (flags & 4) !== 0 ? c.string.decode(state) : null,
|
||||
error: (flags & 8) !== 0 ? c.string.decode(state) : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setVersion(v) {
|
||||
version = v
|
||||
}
|
||||
|
||||
function encode(name, value, v = VERSION) {
|
||||
version = v
|
||||
return c.encode(getEncoding(name), value)
|
||||
}
|
||||
|
||||
function decode(name, buffer, v = VERSION) {
|
||||
version = v
|
||||
return c.decode(getEncoding(name), buffer)
|
||||
}
|
||||
|
||||
function getEnum(name) {
|
||||
switch (name) {
|
||||
default:
|
||||
throw new Error('Enum not found ' + name)
|
||||
}
|
||||
}
|
||||
|
||||
function getEncoding(name) {
|
||||
switch (name) {
|
||||
case '@peardata/node':
|
||||
return encoding0
|
||||
case '@peardata/peer-link':
|
||||
return encoding1
|
||||
case '@peardata/alert-config':
|
||||
return encoding2
|
||||
case '@peardata/alert-event':
|
||||
return encoding3
|
||||
case '@peardata/metric-point':
|
||||
return encoding4
|
||||
case '@peardata/job':
|
||||
return encoding5
|
||||
default:
|
||||
throw new Error('Encoder not found ' + name)
|
||||
}
|
||||
}
|
||||
|
||||
function getStruct(name, v = VERSION) {
|
||||
const enc = getEncoding(name)
|
||||
return {
|
||||
preencode(state, m) {
|
||||
version = v
|
||||
enc.preencode(state, m)
|
||||
},
|
||||
encode(state, m) {
|
||||
version = v
|
||||
enc.encode(state, m)
|
||||
},
|
||||
decode(state) {
|
||||
version = v
|
||||
return enc.decode(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resolveStruct = getStruct // compat
|
||||
|
||||
export { resolveStruct, getStruct, getEnum, getEncoding, encode, decode, setVersion, version }
|
||||
@@ -0,0 +1,341 @@
|
||||
{
|
||||
"version": 1,
|
||||
"schema": [
|
||||
{
|
||||
"name": "node",
|
||||
"namespace": "peardata",
|
||||
"compact": false,
|
||||
"flagsPosition": 2,
|
||||
"fields": [
|
||||
{
|
||||
"name": "nodeId",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "hostname",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "publicKeyHex",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "platform",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "arch",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "cpus",
|
||||
"required": false,
|
||||
"type": "uint",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "totalMemMiB",
|
||||
"required": false,
|
||||
"type": "uint",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "agentVersion",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "labelsJson",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "updatedAt",
|
||||
"required": true,
|
||||
"type": "uint",
|
||||
"version": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "peer-link",
|
||||
"namespace": "peardata",
|
||||
"compact": false,
|
||||
"flagsPosition": 2,
|
||||
"fields": [
|
||||
{
|
||||
"name": "localNodeId",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "remotePublicKey",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "role",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "alias",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "dbKeyHex",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "syncMode",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "linkedAt",
|
||||
"required": true,
|
||||
"type": "uint",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "lastSeen",
|
||||
"required": false,
|
||||
"type": "uint",
|
||||
"version": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "alert-config",
|
||||
"namespace": "peardata",
|
||||
"compact": false,
|
||||
"flagsPosition": 3,
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "chart",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "dimension",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "warn",
|
||||
"required": false,
|
||||
"type": "float64",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "crit",
|
||||
"required": false,
|
||||
"type": "float64",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "comparator",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"required": false,
|
||||
"type": "bool",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "info",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "updatedAt",
|
||||
"required": true,
|
||||
"type": "uint",
|
||||
"version": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "alert-event",
|
||||
"namespace": "peardata",
|
||||
"compact": false,
|
||||
"flagsPosition": 3,
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "ts",
|
||||
"required": true,
|
||||
"type": "uint",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "chart",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "dimension",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "severity",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"required": false,
|
||||
"type": "float64",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "threshold",
|
||||
"required": false,
|
||||
"type": "float64",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "cleared",
|
||||
"required": false,
|
||||
"type": "bool",
|
||||
"version": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "metric-point",
|
||||
"namespace": "peardata",
|
||||
"compact": false,
|
||||
"flagsPosition": -1,
|
||||
"fields": [
|
||||
{
|
||||
"name": "chart",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "ts",
|
||||
"required": true,
|
||||
"type": "uint",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "context",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "tier",
|
||||
"required": true,
|
||||
"type": "uint",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "valuesJson",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "job",
|
||||
"namespace": "peardata",
|
||||
"compact": false,
|
||||
"flagsPosition": 3,
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "startedAt",
|
||||
"required": false,
|
||||
"type": "uint",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "finishedAt",
|
||||
"required": false,
|
||||
"type": "uint",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "resultJson",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"version": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import test from 'brittle'
|
||||
import { MetricsCollector } from '../server/services/collector.js'
|
||||
import { CHART_BY_ID, getAllChartDefs } from '../shared/metrics.js'
|
||||
|
||||
test('collector emits core system charts', async (t) => {
|
||||
const c = new MetricsCollector({ intervalMs: 50 })
|
||||
/** @type {any[]} */
|
||||
let batch = []
|
||||
c.once('samples', (b) => {
|
||||
batch = b
|
||||
})
|
||||
c.start()
|
||||
await new Promise((r) => setTimeout(r, 80))
|
||||
c.stop()
|
||||
|
||||
t.ok(batch.length > 5)
|
||||
const charts = new Set(batch.map((s) => s.chart))
|
||||
t.ok(charts.has('system.cpu'))
|
||||
t.ok(charts.has('system.ram') || charts.has('mem.available'))
|
||||
t.ok(charts.has('system.load'))
|
||||
t.ok(charts.has('system.uptime'))
|
||||
|
||||
const cpu = batch.find((s) => s.chart === 'system.cpu')
|
||||
t.ok(cpu)
|
||||
for (const dim of ['user', 'system', 'idle', 'iowait', 'steal']) {
|
||||
t.ok(dim in cpu.values, dim)
|
||||
}
|
||||
|
||||
// runtime instance charts should be registered after first tick
|
||||
t.ok(getAllChartDefs().length >= 30)
|
||||
t.ok(CHART_BY_ID.has('system.cpu'))
|
||||
})
|
||||
@@ -0,0 +1,105 @@
|
||||
import test from 'brittle'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { PearDataModel } from '../server/db/model.js'
|
||||
import Corestore from 'corestore'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const TMP = path.join(__dirname, '..', 'tmp-hyperdb-test')
|
||||
|
||||
async function withModel(fn) {
|
||||
fs.rmSync(TMP, { recursive: true, force: true })
|
||||
const store = new Corestore(path.join(TMP, 'corestore'))
|
||||
await store.ready()
|
||||
const core = store.get({ name: 'peardata-meta' })
|
||||
const model = new PearDataModel(core, { autoUpdate: true })
|
||||
await model.ready()
|
||||
try {
|
||||
await fn(model)
|
||||
} finally {
|
||||
await model.close().catch(() => {})
|
||||
await store.close().catch(() => {})
|
||||
fs.rmSync(TMP, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
test('hyperdb put/get node', async (t) => {
|
||||
await withModel(async (db) => {
|
||||
await db.putNode({
|
||||
nodeId: 'node-a',
|
||||
hostname: 'lab',
|
||||
publicKeyHex: 'ab'.repeat(32),
|
||||
cpus: 4,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
const row = await db.getNode('node-a')
|
||||
t.is(row.hostname, 'lab')
|
||||
t.is(row.cpus, 4)
|
||||
})
|
||||
})
|
||||
|
||||
test('hyperdb peer links', async (t) => {
|
||||
await withModel(async (db) => {
|
||||
const remote = 'cd'.repeat(32)
|
||||
await db.putPeerLink({
|
||||
localNodeId: 'local',
|
||||
remotePublicKey: remote,
|
||||
syncMode: 'pull',
|
||||
alias: 'nas',
|
||||
})
|
||||
const links = await db.listPeerLinks('local')
|
||||
t.is(links.length, 1)
|
||||
t.is(links[0].alias, 'nas')
|
||||
await db.deletePeerLink('local', remote)
|
||||
t.is((await db.listPeerLinks('local')).length, 0)
|
||||
})
|
||||
})
|
||||
|
||||
test('hyperdb warm metric points', async (t) => {
|
||||
await withModel(async (db) => {
|
||||
const ts = Date.now()
|
||||
await db.putMetricPoints([
|
||||
{
|
||||
chart: 'system.cpu',
|
||||
context: 'system.cpu',
|
||||
ts: ts - 1000,
|
||||
values: { user: 10, idle: 90 },
|
||||
tier: 1,
|
||||
},
|
||||
{
|
||||
chart: 'system.cpu',
|
||||
context: 'system.cpu',
|
||||
ts,
|
||||
values: { user: 20, idle: 80 },
|
||||
tier: 1,
|
||||
},
|
||||
])
|
||||
const rows = await db.queryMetricPoints({
|
||||
chart: 'system.cpu',
|
||||
afterMs: ts - 5000,
|
||||
beforeMs: ts + 1000,
|
||||
})
|
||||
t.is(rows.length, 2)
|
||||
t.is(rows[1].values.user, 20)
|
||||
})
|
||||
})
|
||||
|
||||
test('hyperdb alert event', async (t) => {
|
||||
await withModel(async (db) => {
|
||||
const ts = Date.now()
|
||||
await db.putAlertEvent({
|
||||
id: 'cpu_user_high',
|
||||
ts,
|
||||
chart: 'system.cpu',
|
||||
dimension: 'user',
|
||||
severity: 'warning',
|
||||
value: 90,
|
||||
threshold: 80,
|
||||
message: 'high',
|
||||
})
|
||||
const evs = await db.listAlertEvents({ chart: 'system.cpu', limit: 10 })
|
||||
t.ok(evs.length >= 1)
|
||||
t.is(evs[0].severity, 'warning')
|
||||
})
|
||||
})
|
||||
+23
-3
@@ -9,7 +9,7 @@ import {
|
||||
Pushes,
|
||||
} from '../shared/protocol.js'
|
||||
import { validateMethodArgs, SCHEMA_VERSION } from '../shared/schema.js'
|
||||
import { CHART_DEFS, CONTEXT_IDS } from '../shared/metrics.js'
|
||||
import { CHART_DEFS, CONTEXT_IDS, getAllChartDefs, getContextIds } from '../shared/metrics.js'
|
||||
|
||||
test('protocol constants', (t) => {
|
||||
t.is(PROTOCOL, 'peardata/rpc')
|
||||
@@ -34,6 +34,9 @@ test('method roles cover monitoring surface', (t) => {
|
||||
'listCharts',
|
||||
'mintInvite',
|
||||
'runJob',
|
||||
'getDbInfo',
|
||||
'linkPeer',
|
||||
'unlinkPeer',
|
||||
]) {
|
||||
t.ok(MethodRoles[m], m)
|
||||
t.is(Methods[m], m)
|
||||
@@ -45,8 +48,25 @@ test('pushes include metrics + anomaly', (t) => {
|
||||
t.ok(Pushes.anomaly.startsWith('push:'))
|
||||
})
|
||||
|
||||
test('metrics catalog non-empty', (t) => {
|
||||
t.ok(CHART_DEFS.length >= 5)
|
||||
test('metrics catalog covers system stats', (t) => {
|
||||
t.ok(CHART_DEFS.length >= 30)
|
||||
t.ok(getAllChartDefs().length >= CHART_DEFS.length)
|
||||
const contexts = getContextIds()
|
||||
for (const id of [
|
||||
'system.cpu',
|
||||
'system.ram',
|
||||
'system.load',
|
||||
'system.io',
|
||||
'system.net',
|
||||
'system.ip',
|
||||
'mem.available',
|
||||
'mem.swap',
|
||||
'ip.tcpsock',
|
||||
'ipv4.packets',
|
||||
'system.cpu_some_pressure',
|
||||
]) {
|
||||
t.ok(contexts.includes(id), id)
|
||||
}
|
||||
t.ok(CONTEXT_IDS.includes('system.cpu'))
|
||||
})
|
||||
|
||||
|
||||
+16
-10
@@ -12,14 +12,14 @@ initAuthKeys({
|
||||
publicKeyHex: b4a.toString(crypto.keyPair(seed).publicKey, 'hex'),
|
||||
})
|
||||
|
||||
test('REST /api/v3/info', (t) => {
|
||||
const res = handleRest('/api/v3/info', new URLSearchParams())
|
||||
test('REST /api/v3/info', async (t) => {
|
||||
const res = await handleRest('/api/v3/info', new URLSearchParams())
|
||||
t.is(res.status, 200)
|
||||
t.ok(res.body.hostname)
|
||||
t.ok(res.body.peardata)
|
||||
})
|
||||
|
||||
test('REST /api/v1/charts after ingest', (t) => {
|
||||
test('REST /api/v1/charts after ingest', async (t) => {
|
||||
getStore().ingest([
|
||||
{
|
||||
chart: 'system.load',
|
||||
@@ -28,12 +28,12 @@ test('REST /api/v1/charts after ingest', (t) => {
|
||||
values: { load1: 0.5, load5: 0.4, load15: 0.3 },
|
||||
},
|
||||
])
|
||||
const res = handleRest('/api/v1/charts', new URLSearchParams())
|
||||
const res = await handleRest('/api/v1/charts', new URLSearchParams())
|
||||
t.is(res.status, 200)
|
||||
t.ok(res.body.charts['system.load'] || res.body.charts['system.cpu'])
|
||||
})
|
||||
|
||||
test('REST /api/v3/data', (t) => {
|
||||
test('REST /api/v3/data', async (t) => {
|
||||
const now = Date.now()
|
||||
for (let i = 0; i < 10; i++) {
|
||||
getStore().ingest([
|
||||
@@ -53,7 +53,7 @@ test('REST /api/v3/data', (t) => {
|
||||
},
|
||||
])
|
||||
}
|
||||
const res = handleRest(
|
||||
const res = await handleRest(
|
||||
'/api/v3/data',
|
||||
new URLSearchParams({ chart: 'system.cpu', after: '-30', points: '10' })
|
||||
)
|
||||
@@ -61,13 +61,19 @@ test('REST /api/v3/data', (t) => {
|
||||
t.ok(Array.isArray(res.body.data))
|
||||
})
|
||||
|
||||
test('REST /api/v3/contexts', (t) => {
|
||||
const res = handleRest('/api/v3/contexts', new URLSearchParams())
|
||||
test('REST /api/v3/contexts', async (t) => {
|
||||
const res = await handleRest('/api/v3/contexts', new URLSearchParams())
|
||||
t.is(res.status, 200)
|
||||
t.ok(res.body.contexts['system.cpu'])
|
||||
})
|
||||
|
||||
test('REST 404', (t) => {
|
||||
const res = handleRest('/api/v9/nope', new URLSearchParams())
|
||||
test('REST /api/v3/db', async (t) => {
|
||||
const res = await handleRest('/api/v3/db', new URLSearchParams())
|
||||
t.is(res.status, 200)
|
||||
t.ok('enabled' in res.body)
|
||||
})
|
||||
|
||||
test('REST 404', async (t) => {
|
||||
const res = await handleRest('/api/v9/nope', new URLSearchParams())
|
||||
t.is(res.status, 404)
|
||||
})
|
||||
|
||||
+5
-4
@@ -1,7 +1,7 @@
|
||||
import test from 'brittle'
|
||||
import { MetricStore } from '../server/services/store.js'
|
||||
|
||||
test('store ingest + query', (t) => {
|
||||
test('store ingest + query', async (t) => {
|
||||
const store = new MetricStore()
|
||||
const now = Date.now()
|
||||
for (let i = 0; i < 30; i++) {
|
||||
@@ -27,15 +27,16 @@ test('store ingest + query', (t) => {
|
||||
t.ok(meta)
|
||||
t.is(meta.context, 'system.cpu')
|
||||
|
||||
const q = store.query({ chart: 'system.cpu', after: -60, before: 0, points: 15 })
|
||||
const q = await store.query({ chart: 'system.cpu', after: -60, before: 0, points: 15 })
|
||||
t.absent(q.error)
|
||||
t.ok(q.data.length > 0)
|
||||
t.ok(q.labels.includes('user'))
|
||||
t.ok(q.labels.includes('time'))
|
||||
t.ok(q.source)
|
||||
})
|
||||
|
||||
test('store unknown chart', (t) => {
|
||||
test('store unknown chart', async (t) => {
|
||||
const store = new MetricStore()
|
||||
const q = store.query({ chart: 'nope.chart', points: 10 })
|
||||
const q = await store.query({ chart: 'nope.chart', points: 10 })
|
||||
t.ok(q.error)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user