@klum-db/lobby 0.2.0-pre.30 → 0.2.0-pre.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -28
- package/dist/bin/klum.d.cts +1 -1
- package/dist/bin/klum.d.ts +1 -1
- package/dist/index.cjs +108 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +108 -10
- package/dist/index.js.map +1 -1
- package/dist/{vault-group-BXjO5kHB.d.cts → vault-group-DqEyXbN1.d.cts} +49 -3
- package/dist/{vault-group-BXjO5kHB.d.ts → vault-group-DqEyXbN1.d.ts} +49 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,43 +1,65 @@
|
|
|
1
1
|
# @klum-db/lobby
|
|
2
2
|
|
|
3
|
-
> **
|
|
4
|
-
>
|
|
5
|
-
> **noy-db is the vault (inward). klum-db is the Lobby (outward).**
|
|
6
|
-
> A container runs perfectly alone; the engine orchestrates many. Docker is to a container what the Lobby is to a vault.
|
|
3
|
+
> **Small enough to own, coordinated enough to scale.**
|
|
4
|
+
> `@klum-db/lobby` is the **control plane** for a fleet of sovereign [noy-db](https://github.com/vLannaAi/noy-db) vaults. Each vault is small, **100%-encrypted**, and complete on its own — its own embedded schema, its own query engine, usable **offline**. The Lobby orchestrates many into one coordinated whole, **online or offline**: **coordination without custody** — it drives the fleet but never owns the data. *One-way — klum drives noy; noy never knows klum exists.*
|
|
7
5
|
|
|
8
6
|
`@klum-db/lobby` · status: **preview** · depends on `@noy-db/hub`
|
|
9
7
|
|
|
10
8
|
---
|
|
11
9
|
|
|
12
|
-
##
|
|
10
|
+
## What it is, in 20 seconds
|
|
13
11
|
|
|
14
|
-
|
|
12
|
+
A single vault is complete — its own keys, schema, and truth — but completeness is also a ceiling: alone, a vault can only answer for *itself*. The usual fix is to pour everything into one central store, trading away the sovereignty that made each vault worth trusting. **The Lobby takes the other path — it coordinates the vaults where they stand.**
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
```mermaid
|
|
15
|
+
flowchart TD
|
|
16
|
+
L["<b>CONTROL PLANE</b> · @klum-db/lobby<br/>federate · interchange · custody · surface"]:::cp
|
|
17
|
+
L ==> V1["🔒 vault"]:::dp
|
|
18
|
+
L ==> V2["🔒 vault"]:::dp
|
|
19
|
+
L ==> V3["🔒 vault"]:::dp
|
|
20
|
+
L ==> V4["🔒 vault"]:::dp
|
|
21
|
+
classDef cp fill:#0f766e,stroke:#0f766e,color:#ffffff
|
|
22
|
+
classDef dp fill:#ecfdf5,stroke:#10b981,color:#065f46
|
|
23
|
+
```
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
*DATA PLANE · `@noy-db/hub` — each vault sovereign & 100% encrypted, complete on its own. **klum drives noy one-way; noy never depends on klum.***
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
<details><summary>Text version (npm / non-Mermaid viewers)</summary>
|
|
25
28
|
|
|
26
29
|
```
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
│
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
┌────────────────────────────────────────────────────┐
|
|
31
|
+
│ CONTROL PLANE · @klum-db/lobby │
|
|
32
|
+
│ federate · interchange · custody · surface │
|
|
33
|
+
└────────────────────────┬───────────────────────────┘
|
|
34
|
+
│ drives — one-way (klum → noy)
|
|
35
|
+
┌──────────┬───────┴───────┬──────────┐
|
|
36
|
+
▼ ▼ ▼ ▼
|
|
37
|
+
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
|
|
38
|
+
│vault │ │vault │ │vault │ │vault │
|
|
39
|
+
└──────┘ └──────┘ └──────┘ └──────┘
|
|
40
|
+
DATA PLANE · @noy-db/hub — each vault sovereign & non-fungible,
|
|
41
|
+
complete on its own. noy never depends on klum.
|
|
38
42
|
```
|
|
43
|
+
</details>
|
|
44
|
+
|
|
45
|
+
- **Coordination without custody.** klum drives the fleet — federate, move data, custody, sync — but **never owns the data**: keys, crypto, and records stay sovereign in each vault, and klum binds to one stable contract (`@noy-db/hub/kernel`), never hub internals. *(One-way, and enforced — a build-time guard means no `@noy-db` package can ever import `@klum-db`.)*
|
|
46
|
+
- **Bring the work to the data, not the data to a lake.** Cross-vault queries resolve *across* the group — each vault answers for its own slice under its own keys, nothing pools. No central honeypot to breach.
|
|
47
|
+
- **Small core, coordinated reach.** Each vault stays small, portable, and individually revocable; orchestration recovers the cross-cutting reach you'd otherwise need a monolith for.
|
|
48
|
+
- **A group, not a cluster.** Vaults are sovereign and non-fungible — one subject = one vault, the subject holds the deed. *Joined, not merged; allied, not absorbed.* (Thai *klum* กลุ่ม = a group.)
|
|
49
|
+
|
|
50
|
+
## Why it exists
|
|
51
|
+
|
|
52
|
+
Banking, accounting, health, insurance — the data that matters is **individual, single-subject, and owned by the person it's about**. noy-db makes each of those a small, sovereign, **100%-encrypted** vault — a complete system on its own. *You own your data, in spite of the cloud.* The limit only bites when one actor must work across **many** subjects at once.
|
|
53
|
+
|
|
54
|
+
That's the Lobby's dimension: the efficiency and privacy of a small sovereign dataset at the core, joined with an actor operating across many at once — **governance by default, not by checklist**. Access is **scoped, purpose-limited, and revocable** (the subject can withdraw anytime); the orchestrator coordinates without absorbing. A counterweight to the central data lake — without the lock-in, the lock-out, or the single honeypot.
|
|
39
55
|
|
|
40
|
-
|
|
56
|
+
**Lineage & market context:** klum-db sits in the local-first / data-sovereignty tradition — [Local-first software](https://www.inkandswitch.com/essay/local-first/), [GDPR data portability](https://gdpr-info.eu/art-20-gdpr/), and the full picture in [**docs/positioning.md**](docs/positioning.md).
|
|
57
|
+
|
|
58
|
+
## Reads in a sentence
|
|
59
|
+
|
|
60
|
+
> A firm is a **Custodian** in the **Lobby**: it holds operating grants to client **Vaults** that all reference one shared **Pool**. The client holds the **Deed**. To onboard, the firm **Relocates** a client's vault as a **Bundle**, **Migrates** it to the current schema, and **Merges** the Pool slice by field **Authority** using **Provenance**. The client can **Withdraw** anytime; an abandoned vault can be **Liberated**.
|
|
61
|
+
|
|
62
|
+
Every bold word is a real, shipped capability.
|
|
41
63
|
|
|
42
64
|
## Install
|
|
43
65
|
|
|
@@ -131,17 +153,22 @@ const { bundleBytes, transferKey } = await lobby.exportSurface('payroll-vault',
|
|
|
131
153
|
await lobby.applySurface('tax-vault', surface, bundleBytes, transferKey)
|
|
132
154
|
```
|
|
133
155
|
|
|
156
|
+
### On-ramp · Dock → `graduate()`
|
|
157
|
+
|
|
158
|
+
A foreign, non-noy-db unit (a legacy DB, a raw export) can't be federated directly — the sovereign tier needs a vault's keyring and per-record keys. **Dock** carries it read-only at a lower tier; **`graduate()`** imports it into a fresh sovereign vault, unlocking the full tier (custody, provenance, field-Authority merge). It's the one move that *adds a node to the data plane* rather than coordinating existing ones.
|
|
159
|
+
|
|
134
160
|
---
|
|
135
161
|
|
|
136
162
|
## Relationship with noy-db
|
|
137
163
|
|
|
138
|
-
-
|
|
164
|
+
The one-way law and the kernel seam are covered up top — here are the specifics that matter when you build:
|
|
165
|
+
|
|
139
166
|
- **Custody is a vault-level concern** and lives *in* hub (keyring/CEK/consent primitives); the Lobby **re-exports** it (`createDeedOwner`, `liberateVault`, `CustodyApi`) so consumers have one import surface.
|
|
140
167
|
- **Federation** lives in the Lobby, not in hub — open fleets with `lobby.openVaultGroup` (`@noy-db/hub` no longer ships the `openVaultGroup` / `openStateManagementVault` / `withVaultTemplate` fleet methods).
|
|
141
|
-
-
|
|
168
|
+
- **Enforced, not conventional:** an `@noy-db` package importing `@klum-db` fails noy-db's build-time architecture check.
|
|
142
169
|
|
|
143
170
|
## Status
|
|
144
171
|
|
|
145
172
|
Preview. `@klum-db/lobby` is its own repository and the sole publisher of `@klum-db/*` to npm. It depends on the **published** `@noy-db/*` packages through the stable `@noy-db/hub/kernel` boundary and versions **independently** (`0.2.0-pre.N`, decoupled from noy-db). Pilot-1 (FR-1…FR-9), the dock tier, and `Lobby.graduate()` are complete.
|
|
146
173
|
|
|
147
|
-
See [`PROVENANCE.md`](./PROVENANCE.md) for origin and build history.
|
|
174
|
+
See [`docs/architecture.md`](docs/architecture.md) for the detailed noy-db ↔ klum-db boundary, [`docs/roadmap.md`](docs/roadmap.md) for what's next, and [`PROVENANCE.md`](./PROVENANCE.md) for origin and build history.
|
package/dist/bin/klum.d.cts
CHANGED
package/dist/bin/klum.d.ts
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -1268,6 +1268,43 @@ var init_insight_auto_push = __esm({
|
|
|
1268
1268
|
}
|
|
1269
1269
|
});
|
|
1270
1270
|
|
|
1271
|
+
// src/federation/partial-reduce.ts
|
|
1272
|
+
function canPartialReduce(spec) {
|
|
1273
|
+
return Object.values(spec).every((r) => typeof r.merge === "function");
|
|
1274
|
+
}
|
|
1275
|
+
function reduceToPartial(records, spec) {
|
|
1276
|
+
const out = {};
|
|
1277
|
+
for (const [key, reducer] of Object.entries(spec)) {
|
|
1278
|
+
const r = reducer;
|
|
1279
|
+
let state = r.init();
|
|
1280
|
+
for (const rec of records) state = r.step(state, rec);
|
|
1281
|
+
out[key] = state;
|
|
1282
|
+
}
|
|
1283
|
+
return out;
|
|
1284
|
+
}
|
|
1285
|
+
function mergePartials(spec, partials) {
|
|
1286
|
+
const out = {};
|
|
1287
|
+
for (const [key, reducer] of Object.entries(spec)) {
|
|
1288
|
+
const r = reducer;
|
|
1289
|
+
let acc = r.init();
|
|
1290
|
+
for (const p of partials) acc = r.merge(acc, p[key]);
|
|
1291
|
+
out[key] = acc;
|
|
1292
|
+
}
|
|
1293
|
+
return out;
|
|
1294
|
+
}
|
|
1295
|
+
function finalizePartial(spec, merged) {
|
|
1296
|
+
const out = {};
|
|
1297
|
+
for (const [key, reducer] of Object.entries(spec)) {
|
|
1298
|
+
out[key] = reducer.finalize(merged[key]);
|
|
1299
|
+
}
|
|
1300
|
+
return out;
|
|
1301
|
+
}
|
|
1302
|
+
var init_partial_reduce = __esm({
|
|
1303
|
+
"src/federation/partial-reduce.ts"() {
|
|
1304
|
+
"use strict";
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1271
1308
|
// src/federation/aggregate-across.ts
|
|
1272
1309
|
var import_kernel8, import_kernel9, CrossVaultAggregation, CrossVaultGroupedAggregation;
|
|
1273
1310
|
var init_aggregate_across = __esm({
|
|
@@ -1276,6 +1313,7 @@ var init_aggregate_across = __esm({
|
|
|
1276
1313
|
import_kernel8 = require("@noy-db/hub/kernel");
|
|
1277
1314
|
import_kernel9 = require("@noy-db/hub/kernel");
|
|
1278
1315
|
init_cross_vault_live();
|
|
1316
|
+
init_partial_reduce();
|
|
1279
1317
|
CrossVaultAggregation = class {
|
|
1280
1318
|
constructor(src, spec, bind) {
|
|
1281
1319
|
this.src = src;
|
|
@@ -1286,6 +1324,10 @@ var init_aggregate_across = __esm({
|
|
|
1286
1324
|
spec;
|
|
1287
1325
|
bind;
|
|
1288
1326
|
async run(options = {}) {
|
|
1327
|
+
if (this.src.fanoutReduce && canPartialReduce(this.spec)) {
|
|
1328
|
+
const { partials, skippedVaults: skippedVaults2 } = await this.src.fanoutReduce(this.spec, options);
|
|
1329
|
+
return { result: finalizePartial(this.spec, mergePartials(this.spec, partials)), skippedVaults: skippedVaults2 };
|
|
1330
|
+
}
|
|
1289
1331
|
const { records, skippedVaults } = await this.src.fanoutRecords(options);
|
|
1290
1332
|
return { result: (0, import_kernel8.reduceRecords)(records, this.spec), skippedVaults };
|
|
1291
1333
|
}
|
|
@@ -1412,6 +1454,7 @@ var init_vault_group = __esm({
|
|
|
1412
1454
|
init_cross_vault_live();
|
|
1413
1455
|
init_insight_auto_push();
|
|
1414
1456
|
init_aggregate_across();
|
|
1457
|
+
init_partial_reduce();
|
|
1415
1458
|
SHARD_SEPARATOR = "--";
|
|
1416
1459
|
SAFE_PARTITION_KEY = /^[A-Za-z0-9._-]+$/;
|
|
1417
1460
|
VaultGroup = class {
|
|
@@ -1550,22 +1593,33 @@ var init_vault_group = __esm({
|
|
|
1550
1593
|
collection(collectionName) {
|
|
1551
1594
|
return new ShardedCollection(this, collectionName);
|
|
1552
1595
|
}
|
|
1553
|
-
/** @internal — eligible (openable-candidate) rows + drift/divergence skips. */
|
|
1596
|
+
/** @internal — eligible (openable-candidate) rows + drift/divergence/unreachable skips. */
|
|
1554
1597
|
async resolveEligible(options = {}) {
|
|
1555
1598
|
const rows = await this.allRows();
|
|
1599
|
+
const candidates = options.only ? rows.filter((r) => options.only.includes(r.partitionKey)) : rows;
|
|
1556
1600
|
const skipped = [];
|
|
1557
1601
|
const versionOk = [];
|
|
1558
|
-
for (const row of
|
|
1602
|
+
for (const row of candidates) {
|
|
1559
1603
|
if (options.minVersion !== void 0 && row.schemaVersion < options.minVersion) {
|
|
1560
1604
|
skipped.push({ vaultId: row.vaultId, reason: "schema-drift" });
|
|
1561
1605
|
} else versionOk.push(row);
|
|
1562
1606
|
}
|
|
1563
|
-
const
|
|
1607
|
+
const probes = await Promise.all(
|
|
1608
|
+
versionOk.map(async (row) => {
|
|
1609
|
+
try {
|
|
1610
|
+
return { row, provisioned: await this.db._shardVaultProvisioned(row.vaultId) };
|
|
1611
|
+
} catch (err) {
|
|
1612
|
+
if (options.failFast) throw err;
|
|
1613
|
+
return { row, error: err };
|
|
1614
|
+
}
|
|
1615
|
+
})
|
|
1616
|
+
);
|
|
1564
1617
|
const eligible = [];
|
|
1565
|
-
|
|
1566
|
-
if (
|
|
1567
|
-
else
|
|
1568
|
-
|
|
1618
|
+
for (const p of probes) {
|
|
1619
|
+
if ("error" in p) skipped.push({ vaultId: p.row.vaultId, reason: "error", error: p.error });
|
|
1620
|
+
else if (p.provisioned) eligible.push(p.row);
|
|
1621
|
+
else skipped.push({ vaultId: p.row.vaultId, reason: "error", error: new import_kernel10.ShardProvisioningError(p.row.vaultId, p.row.partitionKey) });
|
|
1622
|
+
}
|
|
1569
1623
|
return { eligible, skipped };
|
|
1570
1624
|
}
|
|
1571
1625
|
/** @internal — registered push-model cross-vault derivations (#271 Insight Vault). */
|
|
@@ -1625,12 +1679,19 @@ var init_vault_group = __esm({
|
|
|
1625
1679
|
* Insight Vault keyed by partition key. Shards behind `minVersion`,
|
|
1626
1680
|
* unprovisioned, or whose read errors are reported in `skippedVaults` and
|
|
1627
1681
|
* are not written (a stale summary is never left behind for a failed shard).
|
|
1682
|
+
* A shard whose backend is unreachable (provisioning probe or read throws) is
|
|
1683
|
+
* **skipped** (`reason: 'error'`) and the pass continues for the others — its
|
|
1684
|
+
* prior summary is left intact. Pass `failFast: true` for the legacy
|
|
1685
|
+
* all-or-nothing throw. Reconcile a lagging shard later with
|
|
1686
|
+
* `refreshInsights({ only: [pk] })` or `refreshDerivation(pk)`.
|
|
1628
1687
|
*/
|
|
1629
1688
|
async refreshInsights(options = {}) {
|
|
1630
1689
|
if (this.crossVaultDerivations.length === 0) return { written: 0, skippedVaults: [] };
|
|
1631
|
-
const { eligible, skipped } = await this.resolveEligible(
|
|
1632
|
-
options.minVersion !== void 0 ? { minVersion: options.minVersion } : {}
|
|
1633
|
-
|
|
1690
|
+
const { eligible, skipped } = await this.resolveEligible({
|
|
1691
|
+
...options.minVersion !== void 0 ? { minVersion: options.minVersion } : {},
|
|
1692
|
+
...options.only !== void 0 ? { only: options.only } : {},
|
|
1693
|
+
...options.failFast !== void 0 ? { failFast: options.failFast } : {}
|
|
1694
|
+
});
|
|
1634
1695
|
let written = 0;
|
|
1635
1696
|
for (const spec of this.crossVaultDerivations) {
|
|
1636
1697
|
const results = await this.db.queryAcross(
|
|
@@ -1647,6 +1708,7 @@ var init_vault_group = __esm({
|
|
|
1647
1708
|
const row = eligible[i];
|
|
1648
1709
|
const res = results[i];
|
|
1649
1710
|
if (!res || res.result === void 0) {
|
|
1711
|
+
if (options.failFast && res?.error) throw res.error;
|
|
1650
1712
|
skipped.push({ vaultId: row.vaultId, reason: "error", ...res?.error ? { error: res.error } : {} });
|
|
1651
1713
|
continue;
|
|
1652
1714
|
}
|
|
@@ -1662,6 +1724,14 @@ var init_vault_group = __esm({
|
|
|
1662
1724
|
}
|
|
1663
1725
|
return { written, skippedVaults: skipped };
|
|
1664
1726
|
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Reconcile one shard's Insight summaries after its backend was unreachable.
|
|
1729
|
+
* Equivalent to `refreshInsights({ only: [partitionKey] })` — runs every
|
|
1730
|
+
* registered derivation (autoPush or not) for just this shard.
|
|
1731
|
+
*/
|
|
1732
|
+
async refreshDerivation(partitionKey) {
|
|
1733
|
+
return this.refreshInsights({ only: [partitionKey] });
|
|
1734
|
+
}
|
|
1665
1735
|
/** @internal — re-derive + push every autoPush derivation's summary for one shard. */
|
|
1666
1736
|
async _recomputeShardInsights(partitionKey) {
|
|
1667
1737
|
const row = await this.registry.get(this.registryId(partitionKey));
|
|
@@ -1897,6 +1967,34 @@ var init_vault_group = __esm({
|
|
|
1897
1967
|
}
|
|
1898
1968
|
return { records: results, skippedVaults: skipped };
|
|
1899
1969
|
}
|
|
1970
|
+
/**
|
|
1971
|
+
* @internal — distributed partial-reduce (#8). Fan out across eligible shards,
|
|
1972
|
+
* but fold each shard's where-filtered records to a partial reducer STATE
|
|
1973
|
+
* inside the per-shard callback (never returning the rows). Only the scalar
|
|
1974
|
+
* `.aggregate()` path calls this, and that path rejects join legs upstream.
|
|
1975
|
+
*/
|
|
1976
|
+
async fanoutReduce(spec, options = {}) {
|
|
1977
|
+
const { eligible, skipped } = await this.group.resolveEligible(options);
|
|
1978
|
+
const across = await this.group.db.queryAcross(
|
|
1979
|
+
eligible.map((r) => r.vaultId),
|
|
1980
|
+
async (vault) => {
|
|
1981
|
+
this.group.template.configure(vault);
|
|
1982
|
+
const coll = vault.collection(this.collectionName);
|
|
1983
|
+
await coll.list();
|
|
1984
|
+
let q = coll.query();
|
|
1985
|
+
for (const c of this.clauses) q = q.where(c.field, c.op, c.value);
|
|
1986
|
+
const rows = q.toArray();
|
|
1987
|
+
return reduceToPartial(rows, spec);
|
|
1988
|
+
},
|
|
1989
|
+
{ concurrency: options.concurrency ?? 1, create: false }
|
|
1990
|
+
);
|
|
1991
|
+
const partials = [];
|
|
1992
|
+
for (const r of across) {
|
|
1993
|
+
if (r.error) skipped.push({ vaultId: r.vault, reason: classifyShardSkip(r.error), error: r.error });
|
|
1994
|
+
else partials.push(r.result);
|
|
1995
|
+
}
|
|
1996
|
+
return { partials, skippedVaults: skipped };
|
|
1997
|
+
}
|
|
1900
1998
|
/** Fan out across eligible shards, merge, then apply any broadcast dimension legs. */
|
|
1901
1999
|
async toArray(options = {}) {
|
|
1902
2000
|
const { records, skippedVaults } = await this.fanoutRecords(options);
|