@klum-db/lobby 0.2.0-pre.30 → 0.2.0-pre.32
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 +243 -40
- 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 +215 -12
- package/dist/index.js.map +1 -1
- package/dist/{vault-group-BXjO5kHB.d.cts → vault-group-BchRCDm5.d.cts} +110 -6
- package/dist/{vault-group-BXjO5kHB.d.ts → vault-group-BchRCDm5.d.ts} +110 -6
- package/package.json +11 -11
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
|
}
|
|
@@ -1374,6 +1416,73 @@ var init_aggregate_across = __esm({
|
|
|
1374
1416
|
}
|
|
1375
1417
|
});
|
|
1376
1418
|
|
|
1419
|
+
// src/federation/retrieve-across.ts
|
|
1420
|
+
async function retrieveAcross(group, collectionName, query, opts = {}) {
|
|
1421
|
+
const { eligible, skipped } = await group.resolveEligible({
|
|
1422
|
+
...opts.minVersion !== void 0 ? { minVersion: opts.minVersion } : {},
|
|
1423
|
+
...opts.failFast !== void 0 ? { failFast: opts.failFast } : {}
|
|
1424
|
+
});
|
|
1425
|
+
const across = await group.db.queryAcross(
|
|
1426
|
+
eligible.map((r) => r.vaultId),
|
|
1427
|
+
async (vault) => {
|
|
1428
|
+
group.template.configure(vault);
|
|
1429
|
+
const coll = vault.collection(collectionName);
|
|
1430
|
+
let within;
|
|
1431
|
+
if (opts.where?.length) {
|
|
1432
|
+
let q = coll.query();
|
|
1433
|
+
for (const [f, op, v] of opts.where) q = q.where(f, op, v);
|
|
1434
|
+
within = q;
|
|
1435
|
+
}
|
|
1436
|
+
return coll.retrieve(query, {
|
|
1437
|
+
...opts.mode ? { mode: opts.mode } : {},
|
|
1438
|
+
...opts.limit !== void 0 ? { limit: opts.limit } : {},
|
|
1439
|
+
...opts.minScore !== void 0 ? { minScore: opts.minScore } : {},
|
|
1440
|
+
...opts.fields ? { fields: opts.fields } : {},
|
|
1441
|
+
...opts.match ? { match: opts.match } : {},
|
|
1442
|
+
...opts.prefix ? { prefix: opts.prefix } : {},
|
|
1443
|
+
...opts.snippetWindow !== void 0 ? { snippetWindow: opts.snippetWindow } : {},
|
|
1444
|
+
...opts.includeRecord ? { includeRecord: true } : {},
|
|
1445
|
+
...within ? { within } : {}
|
|
1446
|
+
});
|
|
1447
|
+
},
|
|
1448
|
+
{ concurrency: opts.concurrency ?? 1, create: false }
|
|
1449
|
+
);
|
|
1450
|
+
const skippedVaults = [...skipped];
|
|
1451
|
+
const perVault = [];
|
|
1452
|
+
for (const r of across) {
|
|
1453
|
+
if (r.error) {
|
|
1454
|
+
if (opts.failFast) throw r.error;
|
|
1455
|
+
skippedVaults.push({ vaultId: r.vault, reason: classifyShardSkip(r.error), error: r.error });
|
|
1456
|
+
} else {
|
|
1457
|
+
perVault.push({ vault: r.vault, hits: r.result });
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
const lists = perVault.map((pv) => {
|
|
1461
|
+
if (pv.vault.includes(SEP)) {
|
|
1462
|
+
throw new Error(`retrieveAcross: vault id "${pv.vault}" contains the reserved NUL separator`);
|
|
1463
|
+
}
|
|
1464
|
+
return pv.hits.map((h) => ({ ...h, id: pv.vault + SEP + h.id }));
|
|
1465
|
+
});
|
|
1466
|
+
const fused = (0, import_kernel10.fuseRetrieval)(lists, {
|
|
1467
|
+
...opts.limit !== void 0 ? { limit: opts.limit } : {},
|
|
1468
|
+
...opts.rrfK !== void 0 ? { k: opts.rrfK } : {}
|
|
1469
|
+
});
|
|
1470
|
+
const hits = fused.map((h) => {
|
|
1471
|
+
const i = h.id.indexOf(SEP);
|
|
1472
|
+
return { ...h, vault: h.id.slice(0, i), id: h.id.slice(i + 1) };
|
|
1473
|
+
});
|
|
1474
|
+
return { hits, skippedVaults };
|
|
1475
|
+
}
|
|
1476
|
+
var import_kernel10, SEP;
|
|
1477
|
+
var init_retrieve_across = __esm({
|
|
1478
|
+
"src/federation/retrieve-across.ts"() {
|
|
1479
|
+
"use strict";
|
|
1480
|
+
import_kernel10 = require("@noy-db/hub/kernel");
|
|
1481
|
+
init_classify_skip();
|
|
1482
|
+
SEP = "\0";
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1377
1486
|
// src/federation/vault-group.ts
|
|
1378
1487
|
var vault_group_exports = {};
|
|
1379
1488
|
__export(vault_group_exports, {
|
|
@@ -1384,46 +1493,49 @@ __export(vault_group_exports, {
|
|
|
1384
1493
|
});
|
|
1385
1494
|
function assertSafePartitionKey(partitionKey) {
|
|
1386
1495
|
if (partitionKey.length === 0) {
|
|
1387
|
-
throw new
|
|
1496
|
+
throw new import_kernel11.ValidationError("partitionKey must be a non-empty string");
|
|
1388
1497
|
}
|
|
1389
1498
|
if (partitionKey === import_hub4.STATE_VAULT_NAME) {
|
|
1390
|
-
throw new
|
|
1499
|
+
throw new import_kernel11.ReservedVaultNameError(partitionKey);
|
|
1391
1500
|
}
|
|
1392
1501
|
if (!SAFE_PARTITION_KEY.test(partitionKey)) {
|
|
1393
|
-
throw new
|
|
1502
|
+
throw new import_kernel11.ValidationError(
|
|
1394
1503
|
`partitionKey "${partitionKey}" contains characters outside [A-Za-z0-9._-]. Map your records to a store-safe key in sharding.keyOf.`
|
|
1395
1504
|
);
|
|
1396
1505
|
}
|
|
1397
1506
|
if (partitionKey.includes(SHARD_SEPARATOR)) {
|
|
1398
|
-
throw new
|
|
1507
|
+
throw new import_kernel11.ValidationError(
|
|
1399
1508
|
`partitionKey "${partitionKey}" must not contain "--" \u2014 it is reserved as the shard vault-id separator and would risk shard-id collisions.`
|
|
1400
1509
|
);
|
|
1401
1510
|
}
|
|
1402
1511
|
}
|
|
1403
|
-
var
|
|
1512
|
+
var import_kernel11, SHARD_SEPARATOR, SAFE_PARTITION_KEY, VaultGroup, ShardedCollection, ShardedQuery, ShardedGroupedQuery;
|
|
1404
1513
|
var init_vault_group = __esm({
|
|
1405
1514
|
"src/federation/vault-group.ts"() {
|
|
1406
1515
|
"use strict";
|
|
1407
1516
|
init_state_vault();
|
|
1408
|
-
|
|
1517
|
+
import_kernel11 = require("@noy-db/hub/kernel");
|
|
1409
1518
|
init_constants();
|
|
1410
1519
|
init_classify_skip();
|
|
1411
1520
|
init_cross_shard_join();
|
|
1412
1521
|
init_cross_vault_live();
|
|
1413
1522
|
init_insight_auto_push();
|
|
1414
1523
|
init_aggregate_across();
|
|
1524
|
+
init_retrieve_across();
|
|
1525
|
+
init_partial_reduce();
|
|
1415
1526
|
SHARD_SEPARATOR = "--";
|
|
1416
1527
|
SAFE_PARTITION_KEY = /^[A-Za-z0-9._-]+$/;
|
|
1417
1528
|
VaultGroup = class {
|
|
1418
|
-
constructor(db, name, registry, sharding, template, cutoverOnOpen = false) {
|
|
1529
|
+
constructor(db, name, registry, sharding, template, cutoverOnOpen = false, meta) {
|
|
1419
1530
|
this.db = db;
|
|
1420
1531
|
this.name = name;
|
|
1421
1532
|
this.registry = registry;
|
|
1422
1533
|
this.sharding = sharding;
|
|
1423
1534
|
this.template = template;
|
|
1424
1535
|
this.cutoverOnOpen = cutoverOnOpen;
|
|
1536
|
+
this.meta = meta;
|
|
1425
1537
|
if (name.includes(SHARD_SEPARATOR)) {
|
|
1426
|
-
throw new
|
|
1538
|
+
throw new import_kernel11.ValidationError(
|
|
1427
1539
|
`VaultGroup name "${name}" must not contain "--" (reserved shard vault-id separator).`
|
|
1428
1540
|
);
|
|
1429
1541
|
}
|
|
@@ -1434,6 +1546,7 @@ var init_vault_group = __esm({
|
|
|
1434
1546
|
sharding;
|
|
1435
1547
|
template;
|
|
1436
1548
|
cutoverOnOpen;
|
|
1549
|
+
meta;
|
|
1437
1550
|
/** @internal — set when the group is managed (no explicit registry). */
|
|
1438
1551
|
stateVault;
|
|
1439
1552
|
/** @internal */
|
|
@@ -1467,6 +1580,30 @@ var init_vault_group = __esm({
|
|
|
1467
1580
|
const rows = this.registry.query().toArray();
|
|
1468
1581
|
return rows.filter((r) => r.group === this.name);
|
|
1469
1582
|
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Federation-level descriptive metadata (#27): this group's `meta` plus each
|
|
1585
|
+
* member vault's `vaultMeta` (read via `getMeta()`). Best-effort per shard — a
|
|
1586
|
+
* shard that fails to open yields `meta: undefined` for that entry, never
|
|
1587
|
+
* throwing. Member meta is transient per-open (noy-db sets it at `openVault`,
|
|
1588
|
+
* does not persist it); this surfaces whatever each vault was opened with.
|
|
1589
|
+
*/
|
|
1590
|
+
async federationMeta() {
|
|
1591
|
+
const rows = await this.allRows();
|
|
1592
|
+
const vaults = await Promise.all(
|
|
1593
|
+
rows.map(async (row) => {
|
|
1594
|
+
let meta;
|
|
1595
|
+
try {
|
|
1596
|
+
const vault = await this.db.openVault(row.vaultId);
|
|
1597
|
+
this.template.configure(vault);
|
|
1598
|
+
meta = vault.getMeta();
|
|
1599
|
+
} catch {
|
|
1600
|
+
meta = void 0;
|
|
1601
|
+
}
|
|
1602
|
+
return { vaultId: row.vaultId, partitionKey: row.partitionKey, meta };
|
|
1603
|
+
})
|
|
1604
|
+
);
|
|
1605
|
+
return { meta: this.meta, vaults };
|
|
1606
|
+
}
|
|
1470
1607
|
/**
|
|
1471
1608
|
* Open an existing shard and apply the template. When `cutoverOnOpen` is set
|
|
1472
1609
|
* (#271) and the shard's registry version is behind the template, its cutover
|
|
@@ -1503,11 +1640,11 @@ var init_vault_group = __esm({
|
|
|
1503
1640
|
const vaultId = this.shardVaultId(partitionKey);
|
|
1504
1641
|
const row = await this.registry.get(this.registryId(partitionKey));
|
|
1505
1642
|
const provisioned = await this.db._shardVaultProvisioned(vaultId);
|
|
1506
|
-
if (row && !provisioned) throw new
|
|
1643
|
+
if (row && !provisioned) throw new import_kernel11.ShardProvisioningError(vaultId, partitionKey);
|
|
1507
1644
|
if (row && provisioned) return this.openShard(partitionKey);
|
|
1508
1645
|
if (region !== void 0) {
|
|
1509
1646
|
const backendRegion = this.db._resolveBackend(vaultId).capabilities?.region;
|
|
1510
|
-
if (backendRegion !== region) throw new
|
|
1647
|
+
if (backendRegion !== region) throw new import_kernel11.DataResidencyError(vaultId, region, backendRegion);
|
|
1511
1648
|
}
|
|
1512
1649
|
const vault = await this.db.openVault(vaultId);
|
|
1513
1650
|
this.template.configure(vault);
|
|
@@ -1541,31 +1678,42 @@ var init_vault_group = __esm({
|
|
|
1541
1678
|
async shard(partitionKey) {
|
|
1542
1679
|
const vaultId = this.shardVaultId(partitionKey);
|
|
1543
1680
|
const row = await this.registry.get(this.registryId(partitionKey));
|
|
1544
|
-
if (!row) throw new
|
|
1681
|
+
if (!row) throw new import_kernel11.UnknownShardError(partitionKey, this.name);
|
|
1545
1682
|
const provisioned = await this.db._shardVaultProvisioned(vaultId);
|
|
1546
|
-
if (!provisioned) throw new
|
|
1683
|
+
if (!provisioned) throw new import_kernel11.ShardProvisioningError(vaultId, partitionKey);
|
|
1547
1684
|
return this.openShard(partitionKey);
|
|
1548
1685
|
}
|
|
1549
1686
|
/** A sharded view over one logical collection across all shards. */
|
|
1550
1687
|
collection(collectionName) {
|
|
1551
1688
|
return new ShardedCollection(this, collectionName);
|
|
1552
1689
|
}
|
|
1553
|
-
/** @internal — eligible (openable-candidate) rows + drift/divergence skips. */
|
|
1690
|
+
/** @internal — eligible (openable-candidate) rows + drift/divergence/unreachable skips. */
|
|
1554
1691
|
async resolveEligible(options = {}) {
|
|
1555
1692
|
const rows = await this.allRows();
|
|
1693
|
+
const candidates = options.only ? rows.filter((r) => options.only.includes(r.partitionKey)) : rows;
|
|
1556
1694
|
const skipped = [];
|
|
1557
1695
|
const versionOk = [];
|
|
1558
|
-
for (const row of
|
|
1696
|
+
for (const row of candidates) {
|
|
1559
1697
|
if (options.minVersion !== void 0 && row.schemaVersion < options.minVersion) {
|
|
1560
1698
|
skipped.push({ vaultId: row.vaultId, reason: "schema-drift" });
|
|
1561
1699
|
} else versionOk.push(row);
|
|
1562
1700
|
}
|
|
1563
|
-
const
|
|
1701
|
+
const probes = await Promise.all(
|
|
1702
|
+
versionOk.map(async (row) => {
|
|
1703
|
+
try {
|
|
1704
|
+
return { row, provisioned: await this.db._shardVaultProvisioned(row.vaultId) };
|
|
1705
|
+
} catch (err) {
|
|
1706
|
+
if (options.failFast) throw err;
|
|
1707
|
+
return { row, error: err };
|
|
1708
|
+
}
|
|
1709
|
+
})
|
|
1710
|
+
);
|
|
1564
1711
|
const eligible = [];
|
|
1565
|
-
|
|
1566
|
-
if (
|
|
1567
|
-
else
|
|
1568
|
-
|
|
1712
|
+
for (const p of probes) {
|
|
1713
|
+
if ("error" in p) skipped.push({ vaultId: p.row.vaultId, reason: "error", error: p.error });
|
|
1714
|
+
else if (p.provisioned) eligible.push(p.row);
|
|
1715
|
+
else skipped.push({ vaultId: p.row.vaultId, reason: "error", error: new import_kernel11.ShardProvisioningError(p.row.vaultId, p.row.partitionKey) });
|
|
1716
|
+
}
|
|
1569
1717
|
return { eligible, skipped };
|
|
1570
1718
|
}
|
|
1571
1719
|
/** @internal — registered push-model cross-vault derivations (#271 Insight Vault). */
|
|
@@ -1601,7 +1749,7 @@ var init_vault_group = __esm({
|
|
|
1601
1749
|
withCrossVaultDerivation(spec) {
|
|
1602
1750
|
const target = spec.target.vault;
|
|
1603
1751
|
if (target === this.name || target.startsWith(`${this.name}${SHARD_SEPARATOR}`)) {
|
|
1604
|
-
throw new
|
|
1752
|
+
throw new import_kernel11.ValidationError(
|
|
1605
1753
|
`withCrossVaultDerivation: target.vault "${target}" is the "${this.name}" group itself or one of its shards \u2014 an Insight summary must target a SEPARATE analytics vault, never write back into client-shard data (it would breach the per-shard DEK boundary). Use a distinct vault name.`
|
|
1606
1754
|
);
|
|
1607
1755
|
}
|
|
@@ -1625,12 +1773,19 @@ var init_vault_group = __esm({
|
|
|
1625
1773
|
* Insight Vault keyed by partition key. Shards behind `minVersion`,
|
|
1626
1774
|
* unprovisioned, or whose read errors are reported in `skippedVaults` and
|
|
1627
1775
|
* are not written (a stale summary is never left behind for a failed shard).
|
|
1776
|
+
* A shard whose backend is unreachable (provisioning probe or read throws) is
|
|
1777
|
+
* **skipped** (`reason: 'error'`) and the pass continues for the others — its
|
|
1778
|
+
* prior summary is left intact. Pass `failFast: true` for the legacy
|
|
1779
|
+
* all-or-nothing throw. Reconcile a lagging shard later with
|
|
1780
|
+
* `refreshInsights({ only: [pk] })` or `refreshDerivation(pk)`.
|
|
1628
1781
|
*/
|
|
1629
1782
|
async refreshInsights(options = {}) {
|
|
1630
1783
|
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
|
-
|
|
1784
|
+
const { eligible, skipped } = await this.resolveEligible({
|
|
1785
|
+
...options.minVersion !== void 0 ? { minVersion: options.minVersion } : {},
|
|
1786
|
+
...options.only !== void 0 ? { only: options.only } : {},
|
|
1787
|
+
...options.failFast !== void 0 ? { failFast: options.failFast } : {}
|
|
1788
|
+
});
|
|
1634
1789
|
let written = 0;
|
|
1635
1790
|
for (const spec of this.crossVaultDerivations) {
|
|
1636
1791
|
const results = await this.db.queryAcross(
|
|
@@ -1647,6 +1802,7 @@ var init_vault_group = __esm({
|
|
|
1647
1802
|
const row = eligible[i];
|
|
1648
1803
|
const res = results[i];
|
|
1649
1804
|
if (!res || res.result === void 0) {
|
|
1805
|
+
if (options.failFast && res?.error) throw res.error;
|
|
1650
1806
|
skipped.push({ vaultId: row.vaultId, reason: "error", ...res?.error ? { error: res.error } : {} });
|
|
1651
1807
|
continue;
|
|
1652
1808
|
}
|
|
@@ -1662,6 +1818,14 @@ var init_vault_group = __esm({
|
|
|
1662
1818
|
}
|
|
1663
1819
|
return { written, skippedVaults: skipped };
|
|
1664
1820
|
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Reconcile one shard's Insight summaries after its backend was unreachable.
|
|
1823
|
+
* Equivalent to `refreshInsights({ only: [partitionKey] })` — runs every
|
|
1824
|
+
* registered derivation (autoPush or not) for just this shard.
|
|
1825
|
+
*/
|
|
1826
|
+
async refreshDerivation(partitionKey) {
|
|
1827
|
+
return this.refreshInsights({ only: [partitionKey] });
|
|
1828
|
+
}
|
|
1665
1829
|
/** @internal — re-derive + push every autoPush derivation's summary for one shard. */
|
|
1666
1830
|
async _recomputeShardInsights(partitionKey) {
|
|
1667
1831
|
const row = await this.registry.get(this.registryId(partitionKey));
|
|
@@ -1706,7 +1870,7 @@ var init_vault_group = __esm({
|
|
|
1706
1870
|
async cutoverShard(partitionKey) {
|
|
1707
1871
|
const vaultId = this.shardVaultId(partitionKey);
|
|
1708
1872
|
const row = await this.registry.get(this.registryId(partitionKey));
|
|
1709
|
-
if (!row) throw new
|
|
1873
|
+
if (!row) throw new import_kernel11.UnknownShardError(partitionKey, this.name);
|
|
1710
1874
|
const target = this.template.version;
|
|
1711
1875
|
const sv = await this.ensureStateVault();
|
|
1712
1876
|
const base = { vaultId, group: this.name, currentVersion: row.schemaVersion, targetVersion: target };
|
|
@@ -1789,7 +1953,7 @@ var init_vault_group = __esm({
|
|
|
1789
1953
|
let vault;
|
|
1790
1954
|
if (!row) {
|
|
1791
1955
|
if (this.group.sharding.autoCreate === false) {
|
|
1792
|
-
throw new
|
|
1956
|
+
throw new import_kernel11.UnknownShardError(key, this.group.name);
|
|
1793
1957
|
}
|
|
1794
1958
|
vault = await this.group.createShard(key, this.group.sharding.regionOf?.(record));
|
|
1795
1959
|
} else {
|
|
@@ -1801,6 +1965,17 @@ var init_vault_group = __esm({
|
|
|
1801
1965
|
query() {
|
|
1802
1966
|
return new ShardedQuery(this.group, this.collectionName, []);
|
|
1803
1967
|
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Cross-vault federated retrieval (#26): scatter-gather across all eligible
|
|
1970
|
+
* shards — each shard runs its own trusted-tier `retrieve()`, then results are
|
|
1971
|
+
* RRF-fused by rank only (no cross-vault statistics cross the DEK boundary).
|
|
1972
|
+
* Every hit carries its originating `vault`. An unreachable, schema-drifted, or
|
|
1973
|
+
* un-indexed shard is skipped (`skippedVaults`); pass `failFast` to re-throw
|
|
1974
|
+
* the first per-shard error instead.
|
|
1975
|
+
*/
|
|
1976
|
+
async retrieve(query, opts = {}) {
|
|
1977
|
+
return retrieveAcross(this.group, this.collectionName, query, opts);
|
|
1978
|
+
}
|
|
1804
1979
|
};
|
|
1805
1980
|
ShardedQuery = class _ShardedQuery {
|
|
1806
1981
|
constructor(group, collectionName, clauses, coPartitionedLegs = [], broadcastLegs = []) {
|
|
@@ -1861,7 +2036,7 @@ var init_vault_group = __esm({
|
|
|
1861
2036
|
this.group.template.configure(probe);
|
|
1862
2037
|
for (const leg of this.coPartitionedLegs) {
|
|
1863
2038
|
if (!probe.resolveRef(this.collectionName, leg.field)) {
|
|
1864
|
-
throw new
|
|
2039
|
+
throw new import_kernel11.CrossShardJoinError(
|
|
1865
2040
|
`crossShardJoin("${leg.field}"): no ref() declared for "${leg.field}" on collection "${this.collectionName}" in template "${this.group.sharding.vaultTemplate}". Add refs: { ${leg.field}: ref('<target>') } to the template's collection options.`
|
|
1866
2041
|
);
|
|
1867
2042
|
}
|
|
@@ -1897,6 +2072,34 @@ var init_vault_group = __esm({
|
|
|
1897
2072
|
}
|
|
1898
2073
|
return { records: results, skippedVaults: skipped };
|
|
1899
2074
|
}
|
|
2075
|
+
/**
|
|
2076
|
+
* @internal — distributed partial-reduce (#8). Fan out across eligible shards,
|
|
2077
|
+
* but fold each shard's where-filtered records to a partial reducer STATE
|
|
2078
|
+
* inside the per-shard callback (never returning the rows). Only the scalar
|
|
2079
|
+
* `.aggregate()` path calls this, and that path rejects join legs upstream.
|
|
2080
|
+
*/
|
|
2081
|
+
async fanoutReduce(spec, options = {}) {
|
|
2082
|
+
const { eligible, skipped } = await this.group.resolveEligible(options);
|
|
2083
|
+
const across = await this.group.db.queryAcross(
|
|
2084
|
+
eligible.map((r) => r.vaultId),
|
|
2085
|
+
async (vault) => {
|
|
2086
|
+
this.group.template.configure(vault);
|
|
2087
|
+
const coll = vault.collection(this.collectionName);
|
|
2088
|
+
await coll.list();
|
|
2089
|
+
let q = coll.query();
|
|
2090
|
+
for (const c of this.clauses) q = q.where(c.field, c.op, c.value);
|
|
2091
|
+
const rows = q.toArray();
|
|
2092
|
+
return reduceToPartial(rows, spec);
|
|
2093
|
+
},
|
|
2094
|
+
{ concurrency: options.concurrency ?? 1, create: false }
|
|
2095
|
+
);
|
|
2096
|
+
const partials = [];
|
|
2097
|
+
for (const r of across) {
|
|
2098
|
+
if (r.error) skipped.push({ vaultId: r.vault, reason: classifyShardSkip(r.error), error: r.error });
|
|
2099
|
+
else partials.push(r.result);
|
|
2100
|
+
}
|
|
2101
|
+
return { partials, skippedVaults: skipped };
|
|
2102
|
+
}
|
|
1900
2103
|
/** Fan out across eligible shards, merge, then apply any broadcast dimension legs. */
|
|
1901
2104
|
async toArray(options = {}) {
|
|
1902
2105
|
const { records, skippedVaults } = await this.fanoutRecords(options);
|
|
@@ -1918,7 +2121,7 @@ var init_vault_group = __esm({
|
|
|
1918
2121
|
/** @internal — joined queries don't support reactive/aggregate surfaces in v1. */
|
|
1919
2122
|
assertNoJoinLegs(surface) {
|
|
1920
2123
|
if (this.coPartitionedLegs.length || this.broadcastLegs.length) {
|
|
1921
|
-
throw new
|
|
2124
|
+
throw new import_kernel11.CrossShardJoinError(
|
|
1922
2125
|
`${surface}() is not supported on a ShardedQuery with crossShardJoin/broadcastJoin legs in v1. Use toArray() for joined cross-shard queries.`
|
|
1923
2126
|
);
|
|
1924
2127
|
}
|
|
@@ -1984,10 +2187,10 @@ var init_vault_group = __esm({
|
|
|
1984
2187
|
// src/index.ts
|
|
1985
2188
|
var index_exports = {};
|
|
1986
2189
|
__export(index_exports, {
|
|
1987
|
-
CrossShardJoinError: () =>
|
|
2190
|
+
CrossShardJoinError: () => import_kernel13.CrossShardJoinError,
|
|
1988
2191
|
CrossVaultDanglingRefError: () => CrossVaultDanglingRefError,
|
|
1989
2192
|
CustodyApi: () => import_hub6.CustodyApi,
|
|
1990
|
-
DataResidencyError: () =>
|
|
2193
|
+
DataResidencyError: () => import_kernel13.DataResidencyError,
|
|
1991
2194
|
DockedUnit: () => DockedUnit,
|
|
1992
2195
|
FieldAuthorityPolicyMissingError: () => FieldAuthorityPolicyMissingError,
|
|
1993
2196
|
FieldLevelDeferredError: () => FieldLevelDeferredError,
|
|
@@ -1998,14 +2201,14 @@ __export(index_exports, {
|
|
|
1998
2201
|
NOYDB_MULTI_BUNDLE_MAGIC: () => NOYDB_MULTI_BUNDLE_MAGIC,
|
|
1999
2202
|
NOYDB_MULTI_BUNDLE_PREFIX_BYTES: () => NOYDB_MULTI_BUNDLE_PREFIX_BYTES,
|
|
2000
2203
|
NOYDB_MULTI_BUNDLE_VERSION: () => NOYDB_MULTI_BUNDLE_VERSION,
|
|
2001
|
-
ReservedVaultNameError: () =>
|
|
2002
|
-
ShardProvisioningError: () =>
|
|
2204
|
+
ReservedVaultNameError: () => import_kernel13.ReservedVaultNameError,
|
|
2205
|
+
ShardProvisioningError: () => import_kernel13.ShardProvisioningError,
|
|
2003
2206
|
SurfaceCadenceScheduler: () => SurfaceCadenceScheduler,
|
|
2004
2207
|
SurfaceNotFoundError: () => SurfaceNotFoundError,
|
|
2005
2208
|
SurfaceStateError: () => SurfaceStateError,
|
|
2006
2209
|
UnitGraduationError: () => UnitGraduationError,
|
|
2007
|
-
UnknownShardError: () =>
|
|
2008
|
-
VaultTemplateNotFoundError: () =>
|
|
2210
|
+
UnknownShardError: () => import_kernel13.UnknownShardError,
|
|
2211
|
+
VaultTemplateNotFoundError: () => import_kernel13.VaultTemplateNotFoundError,
|
|
2009
2212
|
agreeSurface: () => agreeSurface,
|
|
2010
2213
|
applySurface: () => applySurface,
|
|
2011
2214
|
createDeedOwner: () => import_hub6.createDeedOwner,
|
|
@@ -2035,7 +2238,7 @@ __export(index_exports, {
|
|
|
2035
2238
|
writeMultiVaultBundle: () => writeMultiVaultBundle
|
|
2036
2239
|
});
|
|
2037
2240
|
module.exports = __toCommonJS(index_exports);
|
|
2038
|
-
var
|
|
2241
|
+
var import_kernel12 = require("@noy-db/hub/kernel");
|
|
2039
2242
|
var import_hub5 = require("@noy-db/hub");
|
|
2040
2243
|
|
|
2041
2244
|
// src/dock/docked-unit.ts
|
|
@@ -2118,7 +2321,7 @@ async function meterGroup(group, opts = {}) {
|
|
|
2118
2321
|
}
|
|
2119
2322
|
|
|
2120
2323
|
// src/index.ts
|
|
2121
|
-
var
|
|
2324
|
+
var import_kernel13 = require("@noy-db/hub/kernel");
|
|
2122
2325
|
init_multi_bundle();
|
|
2123
2326
|
init_extract_cross_vault();
|
|
2124
2327
|
init_merge_compartment();
|
|
@@ -2206,15 +2409,15 @@ var Lobby = class {
|
|
|
2206
2409
|
}
|
|
2207
2410
|
async openVaultGroup(name, opts) {
|
|
2208
2411
|
const db = this.noydb;
|
|
2209
|
-
if (db.isClosed) throw new
|
|
2210
|
-
if (name === import_hub5.STATE_VAULT_NAME) throw new
|
|
2412
|
+
if (db.isClosed) throw new import_kernel12.ValidationError("Instance is closed");
|
|
2413
|
+
if (name === import_hub5.STATE_VAULT_NAME) throw new import_kernel12.ReservedVaultNameError(name);
|
|
2211
2414
|
const template = this.vaultTemplates.get(opts.sharding.vaultTemplate);
|
|
2212
|
-
if (!template) throw new
|
|
2415
|
+
if (!template) throw new import_kernel12.VaultTemplateNotFoundError(opts.sharding.vaultTemplate);
|
|
2213
2416
|
const { VaultGroup: VaultGroup2 } = await Promise.resolve().then(() => (init_vault_group(), vault_group_exports));
|
|
2214
2417
|
const { StateManagementVault: StateManagementVault2 } = await Promise.resolve().then(() => (init_state_vault(), state_vault_exports));
|
|
2215
2418
|
const stateVault = opts.registry ? void 0 : await StateManagementVault2.open(db);
|
|
2216
2419
|
const registry = opts.registry ?? stateVault.registry;
|
|
2217
|
-
const group = new VaultGroup2(db, name, registry, opts.sharding, template, opts.cutoverOnOpen ?? false);
|
|
2420
|
+
const group = new VaultGroup2(db, name, registry, opts.sharding, template, opts.cutoverOnOpen ?? false, opts.meta);
|
|
2218
2421
|
if (stateVault) {
|
|
2219
2422
|
group._attachStateVault(stateVault);
|
|
2220
2423
|
await stateVault.recordManifest(opts.sharding.vaultTemplate, template);
|
|
@@ -2228,7 +2431,7 @@ var Lobby = class {
|
|
|
2228
2431
|
}
|
|
2229
2432
|
async openStateManagementVault() {
|
|
2230
2433
|
const db = this.noydb;
|
|
2231
|
-
if (db.isClosed) throw new
|
|
2434
|
+
if (db.isClosed) throw new import_kernel12.ValidationError("Instance is closed");
|
|
2232
2435
|
const { StateManagementVault: StateManagementVault2 } = await Promise.resolve().then(() => (init_state_vault(), state_vault_exports));
|
|
2233
2436
|
return StateManagementVault2.open(db);
|
|
2234
2437
|
}
|