@klum-db/lobby 0.2.0-pre.27 → 0.2.0-pre.29
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 +4 -4
- package/dist/bin/klum.cjs +168 -0
- package/dist/bin/klum.cjs.map +1 -0
- package/dist/bin/klum.d.cts +25 -0
- package/dist/bin/klum.d.ts +25 -0
- package/dist/bin/klum.js +139 -0
- package/dist/bin/klum.js.map +1 -0
- package/dist/index.cjs +77 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -766
- package/dist/index.d.ts +58 -766
- package/dist/index.js +75 -11
- package/dist/index.js.map +1 -1
- package/dist/vault-group-BXjO5kHB.d.cts +763 -0
- package/dist/vault-group-BXjO5kHB.d.ts +763 -0
- package/package.json +14 -6
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ lobby.withVaultTemplate('client', { version: 1, configure: v => v.collection('in
|
|
|
67
67
|
const group = await lobby.openVaultGroup('clients', { registry, sharding: { keyOf, vaultTemplate: 'client', autoCreate: true } })
|
|
68
68
|
await group.shard('acme-co').collection('invoices').put('i1', { id: 'i1', total: '1200.00' })
|
|
69
69
|
const all = await group.queryAcross(/* … */) // fan-out read across shards
|
|
70
|
-
await group.
|
|
70
|
+
await group.rolloutSchema({ batchSize: 4 }) // resumable, registry-tracked
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
### 2 · Interchange — move data between vaults, safely
|
|
@@ -137,11 +137,11 @@ await lobby.applySurface('tax-vault', surface, bundleBytes, transferKey)
|
|
|
137
137
|
|
|
138
138
|
- **Depends on `@noy-db/hub`**, binds to the stable **`@noy-db/hub/kernel`** subpath — never reaches into hub internals.
|
|
139
139
|
- **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
|
-
- **Federation**
|
|
140
|
+
- **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
141
|
- The dependency is enforced one-way at build time; an `@noy-db` package importing `@klum-db` fails the architecture check.
|
|
142
142
|
|
|
143
143
|
## Status
|
|
144
144
|
|
|
145
|
-
Preview
|
|
145
|
+
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
146
|
|
|
147
|
-
|
|
147
|
+
See [`PROVENANCE.md`](./PROVENANCE.md) for origin and build history.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/bin/klum.ts
|
|
21
|
+
var klum_exports = {};
|
|
22
|
+
__export(klum_exports, {
|
|
23
|
+
main: () => main,
|
|
24
|
+
parseArgs: () => parseArgs,
|
|
25
|
+
runInspectGroup: () => runInspectGroup,
|
|
26
|
+
runMeterGroup: () => runMeterGroup
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(klum_exports);
|
|
29
|
+
var import_in_devtools = require("@noy-db/in-devtools");
|
|
30
|
+
|
|
31
|
+
// src/federation/group-inspector.ts
|
|
32
|
+
function groupInspector(group) {
|
|
33
|
+
let shardIds = /* @__PURE__ */ new Set();
|
|
34
|
+
const refresh = async () => {
|
|
35
|
+
const rows = await group.allRows();
|
|
36
|
+
shardIds = new Set(rows.map((r) => r.vaultId));
|
|
37
|
+
return rows;
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
async listAccessibleVaults() {
|
|
41
|
+
const rows = await refresh();
|
|
42
|
+
return rows.map((r) => ({ id: r.vaultId, role: "owner" }));
|
|
43
|
+
},
|
|
44
|
+
async openVault(name) {
|
|
45
|
+
const vault = await group.db.openVault(name);
|
|
46
|
+
group.template.configure(vault);
|
|
47
|
+
return vault;
|
|
48
|
+
},
|
|
49
|
+
onAfterWrite(handler) {
|
|
50
|
+
return group.db.onAfterWrite((event) => {
|
|
51
|
+
if (shardIds.has(event.vault)) return handler(event);
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
onWriteConflict(handler) {
|
|
55
|
+
return group.db.onWriteConflict((c) => {
|
|
56
|
+
if (shardIds.has(c.vault)) handler(c);
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
get writeQueue() {
|
|
60
|
+
return group.db.writeQueue;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/federation/meter-group.ts
|
|
66
|
+
async function meterGroup(group, opts = {}) {
|
|
67
|
+
const { eligible, skipped } = await group.resolveEligible(
|
|
68
|
+
opts.minVersion !== void 0 ? { minVersion: opts.minVersion } : {}
|
|
69
|
+
);
|
|
70
|
+
const perShard = [];
|
|
71
|
+
const names = /* @__PURE__ */ new Set();
|
|
72
|
+
let records = 0;
|
|
73
|
+
for (const row of eligible) {
|
|
74
|
+
const vault = await group.shard(row.partitionKey);
|
|
75
|
+
const collNames = await vault.collections();
|
|
76
|
+
let shardRecords = 0;
|
|
77
|
+
for (const n of collNames) {
|
|
78
|
+
names.add(n);
|
|
79
|
+
shardRecords += await vault.collection(n).count();
|
|
80
|
+
}
|
|
81
|
+
records += shardRecords;
|
|
82
|
+
perShard.push({
|
|
83
|
+
vaultId: row.vaultId,
|
|
84
|
+
partitionKey: row.partitionKey,
|
|
85
|
+
schemaVersion: row.schemaVersion,
|
|
86
|
+
collections: collNames.length,
|
|
87
|
+
records: shardRecords
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return { vaults: eligible.length, collections: names.size, records, perShard, skipped };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/bin/klum.ts
|
|
94
|
+
var import_meta = {};
|
|
95
|
+
function parseArgs(argv) {
|
|
96
|
+
const out = { command: argv[0] ?? "", meter: false };
|
|
97
|
+
for (const a of argv.slice(1)) {
|
|
98
|
+
if (a.startsWith("--group=")) out.group = a.slice("--group=".length);
|
|
99
|
+
else if (a.startsWith("--vault=")) out.vault = a.slice("--vault=".length);
|
|
100
|
+
else if (a === "--meter") out.meter = true;
|
|
101
|
+
else if (!a.startsWith("--")) out.configPath = a;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
async function loadGroup(args) {
|
|
106
|
+
const mod = await import(args.configPath);
|
|
107
|
+
return mod.default(args.group);
|
|
108
|
+
}
|
|
109
|
+
async function runInspectGroup(args, log) {
|
|
110
|
+
if (!args.configPath) {
|
|
111
|
+
log("usage: klum inspect-group <config> --group=<name> [--vault=<id>]");
|
|
112
|
+
return 2;
|
|
113
|
+
}
|
|
114
|
+
const group = await loadGroup(args);
|
|
115
|
+
const inspector = (0, import_in_devtools.createInspector)(groupInspector(group));
|
|
116
|
+
const vaults = await inspector.listVaults();
|
|
117
|
+
log(`group "${args.group ?? ""}" \u2014 ${vaults.length} shard(s):`);
|
|
118
|
+
for (const v of vaults) log(` ${v.id} [${v.role}]`);
|
|
119
|
+
if (args.vault) {
|
|
120
|
+
const vault = await group.db.openVault(args.vault);
|
|
121
|
+
group.template.configure(vault);
|
|
122
|
+
const snap = await inspector.snapshot(vault);
|
|
123
|
+
log(` collections in ${args.vault}: ${snap.collections.map((c) => c.name).join(", ")}`);
|
|
124
|
+
}
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
async function runMeterGroup(args, log) {
|
|
128
|
+
if (!args.configPath) {
|
|
129
|
+
log("usage: klum meter-group <config> --group=<name>");
|
|
130
|
+
return 2;
|
|
131
|
+
}
|
|
132
|
+
const group = await loadGroup(args);
|
|
133
|
+
const r = await meterGroup(group);
|
|
134
|
+
log(`group "${args.group ?? ""}" \u2014 ${r.vaults} vault(s), ${r.collections} collection(s), ${r.records} record(s)`);
|
|
135
|
+
for (const s of r.perShard) {
|
|
136
|
+
log(` ${s.vaultId} (${s.partitionKey}) v${s.schemaVersion}: ${s.collections} coll, ${s.records} rec`);
|
|
137
|
+
}
|
|
138
|
+
if (r.skipped.length) log(` skipped: ${r.skipped.length} shard(s)`);
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
async function main(argv, log = console.log) {
|
|
142
|
+
const args = parseArgs(argv);
|
|
143
|
+
switch (args.command) {
|
|
144
|
+
case "inspect-group":
|
|
145
|
+
return runInspectGroup(args, log);
|
|
146
|
+
case "meter-group":
|
|
147
|
+
return runMeterGroup(args, log);
|
|
148
|
+
default:
|
|
149
|
+
log("klum <inspect-group|meter-group> <config> --group=<name> [--vault=<id>]");
|
|
150
|
+
return args.command ? 1 : 0;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (process.argv[1] && import_meta.url === `file://${process.argv[1]}`) {
|
|
154
|
+
main(process.argv.slice(2)).then((code) => {
|
|
155
|
+
process.exitCode = code;
|
|
156
|
+
}).catch((e) => {
|
|
157
|
+
console.error(e);
|
|
158
|
+
process.exitCode = 1;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
162
|
+
0 && (module.exports = {
|
|
163
|
+
main,
|
|
164
|
+
parseArgs,
|
|
165
|
+
runInspectGroup,
|
|
166
|
+
runMeterGroup
|
|
167
|
+
});
|
|
168
|
+
//# sourceMappingURL=klum.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/bin/klum.ts","../../src/federation/group-inspector.ts","../../src/federation/meter-group.ts"],"sourcesContent":["import { createInspector } from '@noy-db/in-devtools'\nimport { groupInspector } from '../federation/group-inspector.js'\nimport { meterGroup } from '../federation/meter-group.js'\nimport type { VaultGroup } from '../federation/vault-group.js'\n\n/**\n * A config module for the `klum` CLI default-exports this factory: given an\n * optional group name, it returns an opened VaultGroup (the user's module owns\n * the store, templates, and sharding config — the CLI stays agnostic of them).\n */\nexport type GroupFactory = (groupName?: string) => Promise<VaultGroup<unknown>>\n\ntype Log = (s: string) => void\n\nexport interface ParsedArgs {\n command: string\n configPath?: string\n group?: string\n vault?: string\n meter: boolean\n}\n\nexport function parseArgs(argv: readonly string[]): ParsedArgs {\n const out: ParsedArgs = { command: argv[0] ?? '', meter: false }\n for (const a of argv.slice(1)) {\n if (a.startsWith('--group=')) out.group = a.slice('--group='.length)\n else if (a.startsWith('--vault=')) out.vault = a.slice('--vault='.length)\n else if (a === '--meter') out.meter = true\n else if (!a.startsWith('--')) out.configPath = a\n }\n return out\n}\n\nasync function loadGroup(args: ParsedArgs): Promise<VaultGroup<unknown>> {\n const mod = (await import(args.configPath!)) as { default: GroupFactory }\n return mod.default(args.group)\n}\n\nexport async function runInspectGroup(args: ParsedArgs, log: Log): Promise<number> {\n if (!args.configPath) {\n log('usage: klum inspect-group <config> --group=<name> [--vault=<id>]')\n return 2\n }\n const group = await loadGroup(args)\n const inspector = createInspector(groupInspector(group))\n const vaults = await inspector.listVaults()\n log(`group \"${args.group ?? ''}\" — ${vaults.length} shard(s):`)\n for (const v of vaults) log(` ${v.id} [${v.role}]`)\n if (args.vault) {\n const vault = await group.db.openVault(args.vault)\n group.template.configure(vault)\n const snap = await inspector.snapshot(vault)\n log(` collections in ${args.vault}: ${snap.collections.map((c) => c.name).join(', ')}`)\n }\n return 0\n}\n\nexport async function runMeterGroup(args: ParsedArgs, log: Log): Promise<number> {\n if (!args.configPath) {\n log('usage: klum meter-group <config> --group=<name>')\n return 2\n }\n const group = await loadGroup(args)\n const r = await meterGroup(group)\n log(`group \"${args.group ?? ''}\" — ${r.vaults} vault(s), ${r.collections} collection(s), ${r.records} record(s)`)\n for (const s of r.perShard) {\n log(` ${s.vaultId} (${s.partitionKey}) v${s.schemaVersion}: ${s.collections} coll, ${s.records} rec`)\n }\n if (r.skipped.length) log(` skipped: ${r.skipped.length} shard(s)`)\n return 0\n}\n\nexport async function main(argv: readonly string[], log: Log = console.log): Promise<number> {\n const args = parseArgs(argv)\n switch (args.command) {\n case 'inspect-group':\n return runInspectGroup(args, log)\n case 'meter-group':\n return runMeterGroup(args, log)\n default:\n log('klum <inspect-group|meter-group> <config> --group=<name> [--vault=<id>]')\n return args.command ? 1 : 0\n }\n}\n\n// bin entrypoint — only runs when executed directly, not when imported in tests.\nif (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {\n main(process.argv.slice(2))\n .then((code) => {\n process.exitCode = code\n })\n .catch((e: unknown) => {\n console.error(e)\n process.exitCode = 1\n })\n}\n","import type { InspectableContainer } from '@noy-db/in-devtools'\nimport type { AccessibleVault, Vault, WriteHook, WriteConflict, WriteQueue, Unsubscribe } from '@noy-db/hub'\nimport type { VaultGroup } from './vault-group.js'\n\n/**\n * Adapt a federation {@link VaultGroup} to the dev-tools `InspectableContainer`\n * contract from `@noy-db/in-devtools`, so the inspector / TUI can browse a\n * whole fleet exactly like a single instance.\n *\n * Built entirely on the group's public surface (`allRows`, `db`, `template`) —\n * no `VaultGroup` changes, and (critically) no `@klum-db` import lands in any\n * `@noy-db` package: the dependency runs one way, klum → noy.\n *\n * Write-event scoping: `group.db` may host vaults outside this group, so write\n * and conflict events are filtered to the group's shard ids. The id set is\n * primed/refreshed on every `listAccessibleVaults()` call — drive `listVaults()`\n * (the inspector's normal first step) before relying on event scoping.\n */\nexport function groupInspector<T>(group: VaultGroup<T>): InspectableContainer {\n let shardIds = new Set<string>()\n const refresh = async () => {\n const rows = await group.allRows()\n shardIds = new Set(rows.map((r) => r.vaultId))\n return rows\n }\n return {\n async listAccessibleVaults(): Promise<readonly AccessibleVault[]> {\n const rows = await refresh()\n return rows.map((r): AccessibleVault => ({ id: r.vaultId, role: 'owner' }))\n },\n async openVault(name: string): Promise<Vault> {\n const vault = await group.db.openVault(name)\n group.template.configure(vault)\n return vault\n },\n onAfterWrite(handler: WriteHook): Unsubscribe {\n return group.db.onAfterWrite((event) => {\n if (shardIds.has(event.vault)) return handler(event)\n })\n },\n onWriteConflict(handler: (c: WriteConflict) => void): Unsubscribe {\n return group.db.onWriteConflict((c) => {\n if (shardIds.has(c.vault)) handler(c)\n })\n },\n get writeQueue(): WriteQueue {\n return group.db.writeQueue\n },\n }\n}\n","import type { VaultGroup } from './vault-group.js'\nimport type { SkippedVault } from './types.js'\n\nexport interface GroupShardMetrics {\n readonly vaultId: string\n readonly partitionKey: string\n readonly schemaVersion: number\n readonly collections: number\n readonly records: number\n}\n\nexport interface GroupMeterReport {\n /** Number of eligible shards measured. */\n readonly vaults: number\n /** Distinct collection names across the group. */\n readonly collections: number\n /** Total record count summed across shards. */\n readonly records: number\n readonly perShard: ReadonlyArray<GroupShardMetrics>\n /** Drifted / provisioning-failed shards — surfaced, never counted or hidden. */\n readonly skipped: ReadonlyArray<SkippedVault>\n}\n\n/**\n * Fan shape-metrics (collection count + record count) across the group's\n * ELIGIBLE shards. Skipped shards (schema-drift / provisioning failures) are\n * reported in `skipped`, never silently dropped. Reuses the per-vault pattern\n * from `multi-bundle.ts` (`vault.collections()` → `collection(n).count()`).\n *\n * Operational store metrics (calls, CAS conflicts) are a separate concern and\n * are already group-wide via `@noy-db/to-meter` on the underlying store.\n */\nexport async function meterGroup<T>(\n group: VaultGroup<T>,\n opts: { minVersion?: number } = {},\n): Promise<GroupMeterReport> {\n const { eligible, skipped } = await group.resolveEligible(\n opts.minVersion !== undefined ? { minVersion: opts.minVersion } : {},\n )\n const perShard: GroupShardMetrics[] = []\n const names = new Set<string>()\n let records = 0\n for (const row of eligible) {\n const vault = await group.shard(row.partitionKey)\n const collNames = await vault.collections()\n let shardRecords = 0\n for (const n of collNames) {\n names.add(n)\n shardRecords += await vault.collection(n).count()\n }\n records += shardRecords\n perShard.push({\n vaultId: row.vaultId,\n partitionKey: row.partitionKey,\n schemaVersion: row.schemaVersion,\n collections: collNames.length,\n records: shardRecords,\n })\n }\n return { vaults: eligible.length, collections: names.size, records, perShard, skipped }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { V as VaultGroup } from '../vault-group-BXjO5kHB.cjs';
|
|
2
|
+
import '@noy-db/hub/kernel';
|
|
3
|
+
import '@noy-db/hub';
|
|
4
|
+
import '@noy-db/hub/bundle';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A config module for the `klum` CLI default-exports this factory: given an
|
|
8
|
+
* optional group name, it returns an opened VaultGroup (the user's module owns
|
|
9
|
+
* the store, templates, and sharding config — the CLI stays agnostic of them).
|
|
10
|
+
*/
|
|
11
|
+
type GroupFactory = (groupName?: string) => Promise<VaultGroup<unknown>>;
|
|
12
|
+
type Log = (s: string) => void;
|
|
13
|
+
interface ParsedArgs {
|
|
14
|
+
command: string;
|
|
15
|
+
configPath?: string;
|
|
16
|
+
group?: string;
|
|
17
|
+
vault?: string;
|
|
18
|
+
meter: boolean;
|
|
19
|
+
}
|
|
20
|
+
declare function parseArgs(argv: readonly string[]): ParsedArgs;
|
|
21
|
+
declare function runInspectGroup(args: ParsedArgs, log: Log): Promise<number>;
|
|
22
|
+
declare function runMeterGroup(args: ParsedArgs, log: Log): Promise<number>;
|
|
23
|
+
declare function main(argv: readonly string[], log?: Log): Promise<number>;
|
|
24
|
+
|
|
25
|
+
export { type GroupFactory, type ParsedArgs, main, parseArgs, runInspectGroup, runMeterGroup };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { V as VaultGroup } from '../vault-group-BXjO5kHB.js';
|
|
2
|
+
import '@noy-db/hub/kernel';
|
|
3
|
+
import '@noy-db/hub';
|
|
4
|
+
import '@noy-db/hub/bundle';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A config module for the `klum` CLI default-exports this factory: given an
|
|
8
|
+
* optional group name, it returns an opened VaultGroup (the user's module owns
|
|
9
|
+
* the store, templates, and sharding config — the CLI stays agnostic of them).
|
|
10
|
+
*/
|
|
11
|
+
type GroupFactory = (groupName?: string) => Promise<VaultGroup<unknown>>;
|
|
12
|
+
type Log = (s: string) => void;
|
|
13
|
+
interface ParsedArgs {
|
|
14
|
+
command: string;
|
|
15
|
+
configPath?: string;
|
|
16
|
+
group?: string;
|
|
17
|
+
vault?: string;
|
|
18
|
+
meter: boolean;
|
|
19
|
+
}
|
|
20
|
+
declare function parseArgs(argv: readonly string[]): ParsedArgs;
|
|
21
|
+
declare function runInspectGroup(args: ParsedArgs, log: Log): Promise<number>;
|
|
22
|
+
declare function runMeterGroup(args: ParsedArgs, log: Log): Promise<number>;
|
|
23
|
+
declare function main(argv: readonly string[], log?: Log): Promise<number>;
|
|
24
|
+
|
|
25
|
+
export { type GroupFactory, type ParsedArgs, main, parseArgs, runInspectGroup, runMeterGroup };
|
package/dist/bin/klum.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// src/bin/klum.ts
|
|
2
|
+
import { createInspector } from "@noy-db/in-devtools";
|
|
3
|
+
|
|
4
|
+
// src/federation/group-inspector.ts
|
|
5
|
+
function groupInspector(group) {
|
|
6
|
+
let shardIds = /* @__PURE__ */ new Set();
|
|
7
|
+
const refresh = async () => {
|
|
8
|
+
const rows = await group.allRows();
|
|
9
|
+
shardIds = new Set(rows.map((r) => r.vaultId));
|
|
10
|
+
return rows;
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
async listAccessibleVaults() {
|
|
14
|
+
const rows = await refresh();
|
|
15
|
+
return rows.map((r) => ({ id: r.vaultId, role: "owner" }));
|
|
16
|
+
},
|
|
17
|
+
async openVault(name) {
|
|
18
|
+
const vault = await group.db.openVault(name);
|
|
19
|
+
group.template.configure(vault);
|
|
20
|
+
return vault;
|
|
21
|
+
},
|
|
22
|
+
onAfterWrite(handler) {
|
|
23
|
+
return group.db.onAfterWrite((event) => {
|
|
24
|
+
if (shardIds.has(event.vault)) return handler(event);
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
onWriteConflict(handler) {
|
|
28
|
+
return group.db.onWriteConflict((c) => {
|
|
29
|
+
if (shardIds.has(c.vault)) handler(c);
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
get writeQueue() {
|
|
33
|
+
return group.db.writeQueue;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/federation/meter-group.ts
|
|
39
|
+
async function meterGroup(group, opts = {}) {
|
|
40
|
+
const { eligible, skipped } = await group.resolveEligible(
|
|
41
|
+
opts.minVersion !== void 0 ? { minVersion: opts.minVersion } : {}
|
|
42
|
+
);
|
|
43
|
+
const perShard = [];
|
|
44
|
+
const names = /* @__PURE__ */ new Set();
|
|
45
|
+
let records = 0;
|
|
46
|
+
for (const row of eligible) {
|
|
47
|
+
const vault = await group.shard(row.partitionKey);
|
|
48
|
+
const collNames = await vault.collections();
|
|
49
|
+
let shardRecords = 0;
|
|
50
|
+
for (const n of collNames) {
|
|
51
|
+
names.add(n);
|
|
52
|
+
shardRecords += await vault.collection(n).count();
|
|
53
|
+
}
|
|
54
|
+
records += shardRecords;
|
|
55
|
+
perShard.push({
|
|
56
|
+
vaultId: row.vaultId,
|
|
57
|
+
partitionKey: row.partitionKey,
|
|
58
|
+
schemaVersion: row.schemaVersion,
|
|
59
|
+
collections: collNames.length,
|
|
60
|
+
records: shardRecords
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return { vaults: eligible.length, collections: names.size, records, perShard, skipped };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/bin/klum.ts
|
|
67
|
+
function parseArgs(argv) {
|
|
68
|
+
const out = { command: argv[0] ?? "", meter: false };
|
|
69
|
+
for (const a of argv.slice(1)) {
|
|
70
|
+
if (a.startsWith("--group=")) out.group = a.slice("--group=".length);
|
|
71
|
+
else if (a.startsWith("--vault=")) out.vault = a.slice("--vault=".length);
|
|
72
|
+
else if (a === "--meter") out.meter = true;
|
|
73
|
+
else if (!a.startsWith("--")) out.configPath = a;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
async function loadGroup(args) {
|
|
78
|
+
const mod = await import(args.configPath);
|
|
79
|
+
return mod.default(args.group);
|
|
80
|
+
}
|
|
81
|
+
async function runInspectGroup(args, log) {
|
|
82
|
+
if (!args.configPath) {
|
|
83
|
+
log("usage: klum inspect-group <config> --group=<name> [--vault=<id>]");
|
|
84
|
+
return 2;
|
|
85
|
+
}
|
|
86
|
+
const group = await loadGroup(args);
|
|
87
|
+
const inspector = createInspector(groupInspector(group));
|
|
88
|
+
const vaults = await inspector.listVaults();
|
|
89
|
+
log(`group "${args.group ?? ""}" \u2014 ${vaults.length} shard(s):`);
|
|
90
|
+
for (const v of vaults) log(` ${v.id} [${v.role}]`);
|
|
91
|
+
if (args.vault) {
|
|
92
|
+
const vault = await group.db.openVault(args.vault);
|
|
93
|
+
group.template.configure(vault);
|
|
94
|
+
const snap = await inspector.snapshot(vault);
|
|
95
|
+
log(` collections in ${args.vault}: ${snap.collections.map((c) => c.name).join(", ")}`);
|
|
96
|
+
}
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
async function runMeterGroup(args, log) {
|
|
100
|
+
if (!args.configPath) {
|
|
101
|
+
log("usage: klum meter-group <config> --group=<name>");
|
|
102
|
+
return 2;
|
|
103
|
+
}
|
|
104
|
+
const group = await loadGroup(args);
|
|
105
|
+
const r = await meterGroup(group);
|
|
106
|
+
log(`group "${args.group ?? ""}" \u2014 ${r.vaults} vault(s), ${r.collections} collection(s), ${r.records} record(s)`);
|
|
107
|
+
for (const s of r.perShard) {
|
|
108
|
+
log(` ${s.vaultId} (${s.partitionKey}) v${s.schemaVersion}: ${s.collections} coll, ${s.records} rec`);
|
|
109
|
+
}
|
|
110
|
+
if (r.skipped.length) log(` skipped: ${r.skipped.length} shard(s)`);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
async function main(argv, log = console.log) {
|
|
114
|
+
const args = parseArgs(argv);
|
|
115
|
+
switch (args.command) {
|
|
116
|
+
case "inspect-group":
|
|
117
|
+
return runInspectGroup(args, log);
|
|
118
|
+
case "meter-group":
|
|
119
|
+
return runMeterGroup(args, log);
|
|
120
|
+
default:
|
|
121
|
+
log("klum <inspect-group|meter-group> <config> --group=<name> [--vault=<id>]");
|
|
122
|
+
return args.command ? 1 : 0;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
|
|
126
|
+
main(process.argv.slice(2)).then((code) => {
|
|
127
|
+
process.exitCode = code;
|
|
128
|
+
}).catch((e) => {
|
|
129
|
+
console.error(e);
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
export {
|
|
134
|
+
main,
|
|
135
|
+
parseArgs,
|
|
136
|
+
runInspectGroup,
|
|
137
|
+
runMeterGroup
|
|
138
|
+
};
|
|
139
|
+
//# sourceMappingURL=klum.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/bin/klum.ts","../../src/federation/group-inspector.ts","../../src/federation/meter-group.ts"],"sourcesContent":["import { createInspector } from '@noy-db/in-devtools'\nimport { groupInspector } from '../federation/group-inspector.js'\nimport { meterGroup } from '../federation/meter-group.js'\nimport type { VaultGroup } from '../federation/vault-group.js'\n\n/**\n * A config module for the `klum` CLI default-exports this factory: given an\n * optional group name, it returns an opened VaultGroup (the user's module owns\n * the store, templates, and sharding config — the CLI stays agnostic of them).\n */\nexport type GroupFactory = (groupName?: string) => Promise<VaultGroup<unknown>>\n\ntype Log = (s: string) => void\n\nexport interface ParsedArgs {\n command: string\n configPath?: string\n group?: string\n vault?: string\n meter: boolean\n}\n\nexport function parseArgs(argv: readonly string[]): ParsedArgs {\n const out: ParsedArgs = { command: argv[0] ?? '', meter: false }\n for (const a of argv.slice(1)) {\n if (a.startsWith('--group=')) out.group = a.slice('--group='.length)\n else if (a.startsWith('--vault=')) out.vault = a.slice('--vault='.length)\n else if (a === '--meter') out.meter = true\n else if (!a.startsWith('--')) out.configPath = a\n }\n return out\n}\n\nasync function loadGroup(args: ParsedArgs): Promise<VaultGroup<unknown>> {\n const mod = (await import(args.configPath!)) as { default: GroupFactory }\n return mod.default(args.group)\n}\n\nexport async function runInspectGroup(args: ParsedArgs, log: Log): Promise<number> {\n if (!args.configPath) {\n log('usage: klum inspect-group <config> --group=<name> [--vault=<id>]')\n return 2\n }\n const group = await loadGroup(args)\n const inspector = createInspector(groupInspector(group))\n const vaults = await inspector.listVaults()\n log(`group \"${args.group ?? ''}\" — ${vaults.length} shard(s):`)\n for (const v of vaults) log(` ${v.id} [${v.role}]`)\n if (args.vault) {\n const vault = await group.db.openVault(args.vault)\n group.template.configure(vault)\n const snap = await inspector.snapshot(vault)\n log(` collections in ${args.vault}: ${snap.collections.map((c) => c.name).join(', ')}`)\n }\n return 0\n}\n\nexport async function runMeterGroup(args: ParsedArgs, log: Log): Promise<number> {\n if (!args.configPath) {\n log('usage: klum meter-group <config> --group=<name>')\n return 2\n }\n const group = await loadGroup(args)\n const r = await meterGroup(group)\n log(`group \"${args.group ?? ''}\" — ${r.vaults} vault(s), ${r.collections} collection(s), ${r.records} record(s)`)\n for (const s of r.perShard) {\n log(` ${s.vaultId} (${s.partitionKey}) v${s.schemaVersion}: ${s.collections} coll, ${s.records} rec`)\n }\n if (r.skipped.length) log(` skipped: ${r.skipped.length} shard(s)`)\n return 0\n}\n\nexport async function main(argv: readonly string[], log: Log = console.log): Promise<number> {\n const args = parseArgs(argv)\n switch (args.command) {\n case 'inspect-group':\n return runInspectGroup(args, log)\n case 'meter-group':\n return runMeterGroup(args, log)\n default:\n log('klum <inspect-group|meter-group> <config> --group=<name> [--vault=<id>]')\n return args.command ? 1 : 0\n }\n}\n\n// bin entrypoint — only runs when executed directly, not when imported in tests.\nif (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {\n main(process.argv.slice(2))\n .then((code) => {\n process.exitCode = code\n })\n .catch((e: unknown) => {\n console.error(e)\n process.exitCode = 1\n })\n}\n","import type { InspectableContainer } from '@noy-db/in-devtools'\nimport type { AccessibleVault, Vault, WriteHook, WriteConflict, WriteQueue, Unsubscribe } from '@noy-db/hub'\nimport type { VaultGroup } from './vault-group.js'\n\n/**\n * Adapt a federation {@link VaultGroup} to the dev-tools `InspectableContainer`\n * contract from `@noy-db/in-devtools`, so the inspector / TUI can browse a\n * whole fleet exactly like a single instance.\n *\n * Built entirely on the group's public surface (`allRows`, `db`, `template`) —\n * no `VaultGroup` changes, and (critically) no `@klum-db` import lands in any\n * `@noy-db` package: the dependency runs one way, klum → noy.\n *\n * Write-event scoping: `group.db` may host vaults outside this group, so write\n * and conflict events are filtered to the group's shard ids. The id set is\n * primed/refreshed on every `listAccessibleVaults()` call — drive `listVaults()`\n * (the inspector's normal first step) before relying on event scoping.\n */\nexport function groupInspector<T>(group: VaultGroup<T>): InspectableContainer {\n let shardIds = new Set<string>()\n const refresh = async () => {\n const rows = await group.allRows()\n shardIds = new Set(rows.map((r) => r.vaultId))\n return rows\n }\n return {\n async listAccessibleVaults(): Promise<readonly AccessibleVault[]> {\n const rows = await refresh()\n return rows.map((r): AccessibleVault => ({ id: r.vaultId, role: 'owner' }))\n },\n async openVault(name: string): Promise<Vault> {\n const vault = await group.db.openVault(name)\n group.template.configure(vault)\n return vault\n },\n onAfterWrite(handler: WriteHook): Unsubscribe {\n return group.db.onAfterWrite((event) => {\n if (shardIds.has(event.vault)) return handler(event)\n })\n },\n onWriteConflict(handler: (c: WriteConflict) => void): Unsubscribe {\n return group.db.onWriteConflict((c) => {\n if (shardIds.has(c.vault)) handler(c)\n })\n },\n get writeQueue(): WriteQueue {\n return group.db.writeQueue\n },\n }\n}\n","import type { VaultGroup } from './vault-group.js'\nimport type { SkippedVault } from './types.js'\n\nexport interface GroupShardMetrics {\n readonly vaultId: string\n readonly partitionKey: string\n readonly schemaVersion: number\n readonly collections: number\n readonly records: number\n}\n\nexport interface GroupMeterReport {\n /** Number of eligible shards measured. */\n readonly vaults: number\n /** Distinct collection names across the group. */\n readonly collections: number\n /** Total record count summed across shards. */\n readonly records: number\n readonly perShard: ReadonlyArray<GroupShardMetrics>\n /** Drifted / provisioning-failed shards — surfaced, never counted or hidden. */\n readonly skipped: ReadonlyArray<SkippedVault>\n}\n\n/**\n * Fan shape-metrics (collection count + record count) across the group's\n * ELIGIBLE shards. Skipped shards (schema-drift / provisioning failures) are\n * reported in `skipped`, never silently dropped. Reuses the per-vault pattern\n * from `multi-bundle.ts` (`vault.collections()` → `collection(n).count()`).\n *\n * Operational store metrics (calls, CAS conflicts) are a separate concern and\n * are already group-wide via `@noy-db/to-meter` on the underlying store.\n */\nexport async function meterGroup<T>(\n group: VaultGroup<T>,\n opts: { minVersion?: number } = {},\n): Promise<GroupMeterReport> {\n const { eligible, skipped } = await group.resolveEligible(\n opts.minVersion !== undefined ? { minVersion: opts.minVersion } : {},\n )\n const perShard: GroupShardMetrics[] = []\n const names = new Set<string>()\n let records = 0\n for (const row of eligible) {\n const vault = await group.shard(row.partitionKey)\n const collNames = await vault.collections()\n let shardRecords = 0\n for (const n of collNames) {\n names.add(n)\n shardRecords += await vault.collection(n).count()\n }\n records += shardRecords\n perShard.push({\n vaultId: row.vaultId,\n partitionKey: row.partitionKey,\n schemaVersion: row.schemaVersion,\n collections: collNames.length,\n records: shardRecords,\n })\n }\n return { vaults: eligible.length, collections: names.size, records, perShard, skipped }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -1415,13 +1415,13 @@ var init_vault_group = __esm({
|
|
|
1415
1415
|
SHARD_SEPARATOR = "--";
|
|
1416
1416
|
SAFE_PARTITION_KEY = /^[A-Za-z0-9._-]+$/;
|
|
1417
1417
|
VaultGroup = class {
|
|
1418
|
-
constructor(db, name, registry, sharding, template,
|
|
1418
|
+
constructor(db, name, registry, sharding, template, cutoverOnOpen = false) {
|
|
1419
1419
|
this.db = db;
|
|
1420
1420
|
this.name = name;
|
|
1421
1421
|
this.registry = registry;
|
|
1422
1422
|
this.sharding = sharding;
|
|
1423
1423
|
this.template = template;
|
|
1424
|
-
this.
|
|
1424
|
+
this.cutoverOnOpen = cutoverOnOpen;
|
|
1425
1425
|
if (name.includes(SHARD_SEPARATOR)) {
|
|
1426
1426
|
throw new import_kernel10.ValidationError(
|
|
1427
1427
|
`VaultGroup name "${name}" must not contain "--" (reserved shard vault-id separator).`
|
|
@@ -1433,7 +1433,7 @@ var init_vault_group = __esm({
|
|
|
1433
1433
|
registry;
|
|
1434
1434
|
sharding;
|
|
1435
1435
|
template;
|
|
1436
|
-
|
|
1436
|
+
cutoverOnOpen;
|
|
1437
1437
|
/** @internal — set when the group is managed (no explicit registry). */
|
|
1438
1438
|
stateVault;
|
|
1439
1439
|
/** @internal */
|
|
@@ -1468,20 +1468,20 @@ var init_vault_group = __esm({
|
|
|
1468
1468
|
return rows.filter((r) => r.group === this.name);
|
|
1469
1469
|
}
|
|
1470
1470
|
/**
|
|
1471
|
-
* Open an existing shard and apply the template. When `
|
|
1471
|
+
* Open an existing shard and apply the template. When `cutoverOnOpen` is set
|
|
1472
1472
|
* (#271) and the shard's registry version is behind the template, its cutover
|
|
1473
1473
|
* runs inline first — so a behind shard never surfaces a stale handle.
|
|
1474
1474
|
*/
|
|
1475
1475
|
async openShard(partitionKey) {
|
|
1476
|
-
if (this.
|
|
1476
|
+
if (this.cutoverOnOpen) {
|
|
1477
1477
|
const row = await this.registry.get(this.registryId(partitionKey));
|
|
1478
1478
|
if (row && row.schemaVersion < this.template.version) {
|
|
1479
|
-
await this.
|
|
1479
|
+
await this.cutoverShard(partitionKey);
|
|
1480
1480
|
}
|
|
1481
1481
|
}
|
|
1482
1482
|
return this._openShardRaw(partitionKey);
|
|
1483
1483
|
}
|
|
1484
|
-
/** @internal — open + configure with no
|
|
1484
|
+
/** @internal — open + configure with no cutover-on-open hook (used by the migration path itself to avoid recursion). */
|
|
1485
1485
|
async _openShardRaw(partitionKey) {
|
|
1486
1486
|
const vault = await this.db.openVault(this.shardVaultId(partitionKey), { create: false });
|
|
1487
1487
|
this.template.configure(vault);
|
|
@@ -1703,7 +1703,7 @@ var init_vault_group = __esm({
|
|
|
1703
1703
|
* Never throws on a cutover failure — it records `status: 'failed'` and
|
|
1704
1704
|
* returns the row, so a fleet run continues past a bad shard.
|
|
1705
1705
|
*/
|
|
1706
|
-
async
|
|
1706
|
+
async cutoverShard(partitionKey) {
|
|
1707
1707
|
const vaultId = this.shardVaultId(partitionKey);
|
|
1708
1708
|
const row = await this.registry.get(this.registryId(partitionKey));
|
|
1709
1709
|
if (!row) throw new import_kernel10.UnknownShardError(partitionKey, this.name);
|
|
@@ -1754,7 +1754,7 @@ var init_vault_group = __esm({
|
|
|
1754
1754
|
* - `batchSize` — max shards migrated concurrently per batch (back-pressure).
|
|
1755
1755
|
* Default 4. Batches run sequentially; shards within a batch run in parallel.
|
|
1756
1756
|
*/
|
|
1757
|
-
async
|
|
1757
|
+
async rolloutSchema(options = {}) {
|
|
1758
1758
|
const target = this.template.version;
|
|
1759
1759
|
const rows = await this.allRows();
|
|
1760
1760
|
const cohort = options.cohort;
|
|
@@ -1766,7 +1766,7 @@ var init_vault_group = __esm({
|
|
|
1766
1766
|
const failed = [];
|
|
1767
1767
|
for (let i = 0; i < todo.length; i += batchSize) {
|
|
1768
1768
|
const batch = todo.slice(i, i + batchSize);
|
|
1769
|
-
const settled = await Promise.all(batch.map((r) => this.
|
|
1769
|
+
const settled = await Promise.all(batch.map((r) => this.cutoverShard(r.partitionKey)));
|
|
1770
1770
|
for (const res of settled) {
|
|
1771
1771
|
if (res.status === "done") migrated.push(res.vaultId);
|
|
1772
1772
|
else failed.push({ vaultId: res.vaultId, error: res.error ?? "unknown" });
|
|
@@ -2015,6 +2015,7 @@ __export(index_exports, {
|
|
|
2015
2015
|
encodeMultiBundle: () => encodeMultiBundle,
|
|
2016
2016
|
exportSurface: () => exportSurface,
|
|
2017
2017
|
extractCrossVaultPartition: () => extractCrossVaultPartition,
|
|
2018
|
+
groupInspector: () => groupInspector,
|
|
2018
2019
|
isDeedVault: () => import_hub6.isDeedVault,
|
|
2019
2020
|
isSurfaceDue: () => isSurfaceDue,
|
|
2020
2021
|
liberateVault: () => import_hub6.liberateVault,
|
|
@@ -2023,6 +2024,7 @@ __export(index_exports, {
|
|
|
2023
2024
|
markSynced: () => markSynced,
|
|
2024
2025
|
mergeCompartment: () => mergeCompartment,
|
|
2025
2026
|
mergeDecryptedRecords: () => mergeDecryptedRecords,
|
|
2027
|
+
meterGroup: () => meterGroup,
|
|
2026
2028
|
migrateThenMerge: () => migrateThenMerge,
|
|
2027
2029
|
proposeSurface: () => proposeSurface,
|
|
2028
2030
|
readMultiVaultBundleCompartment: () => readMultiVaultBundleCompartment,
|
|
@@ -2053,6 +2055,68 @@ var DockedUnit = class {
|
|
|
2053
2055
|
}
|
|
2054
2056
|
};
|
|
2055
2057
|
|
|
2058
|
+
// src/federation/group-inspector.ts
|
|
2059
|
+
function groupInspector(group) {
|
|
2060
|
+
let shardIds = /* @__PURE__ */ new Set();
|
|
2061
|
+
const refresh = async () => {
|
|
2062
|
+
const rows = await group.allRows();
|
|
2063
|
+
shardIds = new Set(rows.map((r) => r.vaultId));
|
|
2064
|
+
return rows;
|
|
2065
|
+
};
|
|
2066
|
+
return {
|
|
2067
|
+
async listAccessibleVaults() {
|
|
2068
|
+
const rows = await refresh();
|
|
2069
|
+
return rows.map((r) => ({ id: r.vaultId, role: "owner" }));
|
|
2070
|
+
},
|
|
2071
|
+
async openVault(name) {
|
|
2072
|
+
const vault = await group.db.openVault(name);
|
|
2073
|
+
group.template.configure(vault);
|
|
2074
|
+
return vault;
|
|
2075
|
+
},
|
|
2076
|
+
onAfterWrite(handler) {
|
|
2077
|
+
return group.db.onAfterWrite((event) => {
|
|
2078
|
+
if (shardIds.has(event.vault)) return handler(event);
|
|
2079
|
+
});
|
|
2080
|
+
},
|
|
2081
|
+
onWriteConflict(handler) {
|
|
2082
|
+
return group.db.onWriteConflict((c) => {
|
|
2083
|
+
if (shardIds.has(c.vault)) handler(c);
|
|
2084
|
+
});
|
|
2085
|
+
},
|
|
2086
|
+
get writeQueue() {
|
|
2087
|
+
return group.db.writeQueue;
|
|
2088
|
+
}
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// src/federation/meter-group.ts
|
|
2093
|
+
async function meterGroup(group, opts = {}) {
|
|
2094
|
+
const { eligible, skipped } = await group.resolveEligible(
|
|
2095
|
+
opts.minVersion !== void 0 ? { minVersion: opts.minVersion } : {}
|
|
2096
|
+
);
|
|
2097
|
+
const perShard = [];
|
|
2098
|
+
const names = /* @__PURE__ */ new Set();
|
|
2099
|
+
let records = 0;
|
|
2100
|
+
for (const row of eligible) {
|
|
2101
|
+
const vault = await group.shard(row.partitionKey);
|
|
2102
|
+
const collNames = await vault.collections();
|
|
2103
|
+
let shardRecords = 0;
|
|
2104
|
+
for (const n of collNames) {
|
|
2105
|
+
names.add(n);
|
|
2106
|
+
shardRecords += await vault.collection(n).count();
|
|
2107
|
+
}
|
|
2108
|
+
records += shardRecords;
|
|
2109
|
+
perShard.push({
|
|
2110
|
+
vaultId: row.vaultId,
|
|
2111
|
+
partitionKey: row.partitionKey,
|
|
2112
|
+
schemaVersion: row.schemaVersion,
|
|
2113
|
+
collections: collNames.length,
|
|
2114
|
+
records: shardRecords
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
return { vaults: eligible.length, collections: names.size, records, perShard, skipped };
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2056
2120
|
// src/index.ts
|
|
2057
2121
|
var import_kernel12 = require("@noy-db/hub/kernel");
|
|
2058
2122
|
init_multi_bundle();
|
|
@@ -2150,7 +2214,7 @@ var Lobby = class {
|
|
|
2150
2214
|
const { StateManagementVault: StateManagementVault2 } = await Promise.resolve().then(() => (init_state_vault(), state_vault_exports));
|
|
2151
2215
|
const stateVault = opts.registry ? void 0 : await StateManagementVault2.open(db);
|
|
2152
2216
|
const registry = opts.registry ?? stateVault.registry;
|
|
2153
|
-
const group = new VaultGroup2(db, name, registry, opts.sharding, template, opts.
|
|
2217
|
+
const group = new VaultGroup2(db, name, registry, opts.sharding, template, opts.cutoverOnOpen ?? false);
|
|
2154
2218
|
if (stateVault) {
|
|
2155
2219
|
group._attachStateVault(stateVault);
|
|
2156
2220
|
await stateVault.recordManifest(opts.sharding.vaultTemplate, template);
|
|
@@ -2283,6 +2347,7 @@ function createLobby(noydb) {
|
|
|
2283
2347
|
encodeMultiBundle,
|
|
2284
2348
|
exportSurface,
|
|
2285
2349
|
extractCrossVaultPartition,
|
|
2350
|
+
groupInspector,
|
|
2286
2351
|
isDeedVault,
|
|
2287
2352
|
isSurfaceDue,
|
|
2288
2353
|
liberateVault,
|
|
@@ -2291,6 +2356,7 @@ function createLobby(noydb) {
|
|
|
2291
2356
|
markSynced,
|
|
2292
2357
|
mergeCompartment,
|
|
2293
2358
|
mergeDecryptedRecords,
|
|
2359
|
+
meterGroup,
|
|
2294
2360
|
migrateThenMerge,
|
|
2295
2361
|
proposeSurface,
|
|
2296
2362
|
readMultiVaultBundleCompartment,
|