@net-mesh/sdk 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Capability aggregation surface — Phase 6c of
3
+ * `MULTIFOLD_PHASE_6C_CAPACITY_AGGREGATION.md`.
4
+ *
5
+ * Three composable primitives map onto the Rust core's
6
+ * `Fold::aggregate` and `Fold::capacity_ranking`:
7
+ *
8
+ * - {@link TagMatcher} — picks which entries the aggregation
9
+ * walks. Tagged-union: `kind: 'exact' | 'prefix' | 'axis' |
10
+ * 'axisKey' | 'regex' | 'versionRange'`.
11
+ * - {@link GroupBy} — buckets matching entries.
12
+ * - {@link Aggregation} — reduces each bucket to a number.
13
+ *
14
+ * {@link CapacityQuery} composes a matcher + groupBy + optional RTT
15
+ * filter + optional summed-capacity axis into a single
16
+ * `capacityRanking(...)` call; the materialized view returns
17
+ * per-bucket state breakdown sorted by available capacity.
18
+ *
19
+ * The Rust core takes JSON-encoded tagged unions over the napi
20
+ * boundary; these helpers handle the conversion so TS callers work
21
+ * with idiomatic discriminated unions.
22
+ */
23
+ import type { TaxonomyAxis } from './capability-enhancements';
24
+ export type { TaxonomyAxis };
25
+ /**
26
+ * Pre-grouping filter. An entry is included if any of its tags
27
+ * matches the matcher.
28
+ */
29
+ export type TagMatcher = {
30
+ kind: 'exact';
31
+ value: string;
32
+ } | {
33
+ kind: 'prefix';
34
+ value: string;
35
+ } | {
36
+ kind: 'axis';
37
+ axis: TaxonomyAxis;
38
+ } | {
39
+ kind: 'axisKey';
40
+ axis: TaxonomyAxis;
41
+ key: string;
42
+ } | {
43
+ kind: 'regex';
44
+ pattern: string;
45
+ } | {
46
+ kind: 'versionRange';
47
+ axisKey: string;
48
+ min?: string;
49
+ max?: string;
50
+ };
51
+ /** Bucket-key derivation. */
52
+ export type GroupBy = {
53
+ kind: 'class';
54
+ } | {
55
+ kind: 'state';
56
+ } | {
57
+ kind: 'region';
58
+ } | {
59
+ kind: 'publisher';
60
+ } | {
61
+ kind: 'tagStem';
62
+ prefix: string;
63
+ } | {
64
+ kind: 'tagValue';
65
+ axis: TaxonomyAxis;
66
+ key: string;
67
+ };
68
+ /** Per-bucket reduction. */
69
+ export type Aggregation = {
70
+ kind: 'count';
71
+ } | {
72
+ kind: 'distinctPublishers';
73
+ } | {
74
+ kind: 'distinctValues';
75
+ axis: TaxonomyAxis;
76
+ key: string;
77
+ } | {
78
+ kind: 'sumNumericTag';
79
+ axisKey: string;
80
+ } | {
81
+ kind: 'minNumericTag';
82
+ axisKey: string;
83
+ } | {
84
+ kind: 'maxNumericTag';
85
+ axisKey: string;
86
+ };
87
+ /** Composed capacity-ranking query. */
88
+ export interface CapacityQuery {
89
+ /** Optional pre-filter. */
90
+ matcher?: TagMatcher;
91
+ /** How to bucket matching entries. */
92
+ groupBy: GroupBy;
93
+ /**
94
+ * Drop entries whose publisher's RTT exceeds this. `undefined`
95
+ * disables the RTT filter (the `rttEntries` argument to
96
+ * {@link MeshNode.capabilityCapacityRanking} is unused regardless).
97
+ */
98
+ maxRttMs?: number;
99
+ /**
100
+ * Canonical `<axis>.<key>` of a numeric tag to sum across each
101
+ * bucket (e.g. `"hardware.gpu.count"`).
102
+ */
103
+ sumAxisKey?: string;
104
+ /** Top-N buckets by `available` descending. `0` = no truncation. */
105
+ limit: number;
106
+ }
107
+ /** One row of an aggregate result. */
108
+ export interface AggregateRow {
109
+ bucket: string;
110
+ value: bigint;
111
+ }
112
+ /** One row of a capacity-ranking result. */
113
+ export interface CapacityRow {
114
+ /** Bucket key. */
115
+ bucket: string;
116
+ /** Entries in `Idle` that pass the matcher + RTT gates. */
117
+ idle: bigint;
118
+ /** Entries in `Busy` that pass. */
119
+ busy: bigint;
120
+ /** Entries in `Reserved` that pass. */
121
+ reserved: bigint;
122
+ /** `idle + busy + reserved`. Faulty entries are always excluded. */
123
+ available: bigint;
124
+ /**
125
+ * Sum of the `sumAxisKey` numeric tag across the bucket;
126
+ * `undefined` when no `sumAxisKey` was requested.
127
+ */
128
+ summedCapacity?: bigint;
129
+ }
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ /**
3
+ * Capability aggregation surface — Phase 6c of
4
+ * `MULTIFOLD_PHASE_6C_CAPACITY_AGGREGATION.md`.
5
+ *
6
+ * Three composable primitives map onto the Rust core's
7
+ * `Fold::aggregate` and `Fold::capacity_ranking`:
8
+ *
9
+ * - {@link TagMatcher} — picks which entries the aggregation
10
+ * walks. Tagged-union: `kind: 'exact' | 'prefix' | 'axis' |
11
+ * 'axisKey' | 'regex' | 'versionRange'`.
12
+ * - {@link GroupBy} — buckets matching entries.
13
+ * - {@link Aggregation} — reduces each bucket to a number.
14
+ *
15
+ * {@link CapacityQuery} composes a matcher + groupBy + optional RTT
16
+ * filter + optional summed-capacity axis into a single
17
+ * `capacityRanking(...)` call; the materialized view returns
18
+ * per-bucket state breakdown sorted by available capacity.
19
+ *
20
+ * The Rust core takes JSON-encoded tagged unions over the napi
21
+ * boundary; these helpers handle the conversion so TS callers work
22
+ * with idiomatic discriminated unions.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.tagMatcherToJson = tagMatcherToJson;
26
+ exports.groupByToJson = groupByToJson;
27
+ exports.aggregationToJson = aggregationToJson;
28
+ exports.capacityQueryToJson = capacityQueryToJson;
29
+ // =====================================================
30
+ // JSON encoding for the napi boundary
31
+ // =====================================================
32
+ //
33
+ // The Rust core's serde uses snake_case kind values + snake_case
34
+ // field names. The TS surface above uses camelCase per ergonomics;
35
+ // these helpers translate.
36
+ /** @internal */
37
+ function tagMatcherToJson(matcher) {
38
+ switch (matcher.kind) {
39
+ case 'exact':
40
+ return JSON.stringify({ kind: 'exact', value: matcher.value });
41
+ case 'prefix':
42
+ return JSON.stringify({ kind: 'prefix', value: matcher.value });
43
+ case 'axis':
44
+ return JSON.stringify({ kind: 'axis', axis: matcher.axis });
45
+ case 'axisKey':
46
+ return JSON.stringify({
47
+ kind: 'axis_key',
48
+ axis: matcher.axis,
49
+ key: matcher.key,
50
+ });
51
+ case 'regex':
52
+ return JSON.stringify({ kind: 'regex', pattern: matcher.pattern });
53
+ case 'versionRange':
54
+ return JSON.stringify({
55
+ kind: 'version_range',
56
+ axis_key: matcher.axisKey,
57
+ min: matcher.min ?? null,
58
+ max: matcher.max ?? null,
59
+ });
60
+ }
61
+ }
62
+ /** @internal */
63
+ function groupByToJson(groupBy) {
64
+ switch (groupBy.kind) {
65
+ case 'class':
66
+ return JSON.stringify({ kind: 'class' });
67
+ case 'state':
68
+ return JSON.stringify({ kind: 'state' });
69
+ case 'region':
70
+ return JSON.stringify({ kind: 'region' });
71
+ case 'publisher':
72
+ return JSON.stringify({ kind: 'publisher' });
73
+ case 'tagStem':
74
+ return JSON.stringify({ kind: 'tag_stem', prefix: groupBy.prefix });
75
+ case 'tagValue':
76
+ return JSON.stringify({
77
+ kind: 'tag_value',
78
+ axis: groupBy.axis,
79
+ key: groupBy.key,
80
+ });
81
+ }
82
+ }
83
+ /** @internal */
84
+ function aggregationToJson(agg) {
85
+ switch (agg.kind) {
86
+ case 'count':
87
+ return JSON.stringify({ kind: 'count' });
88
+ case 'distinctPublishers':
89
+ return JSON.stringify({ kind: 'distinct_publishers' });
90
+ case 'distinctValues':
91
+ return JSON.stringify({
92
+ kind: 'distinct_values',
93
+ axis: agg.axis,
94
+ key: agg.key,
95
+ });
96
+ case 'sumNumericTag':
97
+ return JSON.stringify({
98
+ kind: 'sum_numeric_tag',
99
+ axis_key: agg.axisKey,
100
+ });
101
+ case 'minNumericTag':
102
+ return JSON.stringify({
103
+ kind: 'min_numeric_tag',
104
+ axis_key: agg.axisKey,
105
+ });
106
+ case 'maxNumericTag':
107
+ return JSON.stringify({
108
+ kind: 'max_numeric_tag',
109
+ axis_key: agg.axisKey,
110
+ });
111
+ }
112
+ }
113
+ /** @internal */
114
+ function capacityQueryToJson(query) {
115
+ return JSON.stringify({
116
+ matcher: query.matcher
117
+ ? JSON.parse(tagMatcherToJson(query.matcher))
118
+ : null,
119
+ group_by: JSON.parse(groupByToJson(query.groupBy)),
120
+ max_rtt_ms: query.maxRttMs ?? null,
121
+ sum_axis_key: query.sumAxisKey ?? null,
122
+ limit: query.limit,
123
+ });
124
+ }
package/dist/index.d.ts CHANGED
@@ -43,6 +43,7 @@ export type { AxisEntry, AxisSchema, KeyEntry, KeyShape, KeyShapeKind, SchemaErr
43
43
  export { AXIS_SCHEMA, METADATA_RESERVED_KEYS, METADATA_RESERVED_PREFIXES, METADATA_SOFT_CAP_BYTES, isReportClean, isReportValid, validateCapabilities, } from './capability-schema';
44
44
  export { subnetId, GLOBAL_SUBNET } from './subnets';
45
45
  export type { SubnetId, SubnetRule, SubnetPolicy } from './subnets';
46
+ export type { Aggregation, AggregateRow, CapacityQuery, CapacityRow, GroupBy, TagMatcher, } from './capability-aggregation';
46
47
  export { DaemonRuntime, DaemonHandle, DaemonError, MigrationHandle, MigrationError, } from './compute';
47
48
  export type { CausalEvent, MeshDaemon, DaemonFactory, DaemonHostConfig, DaemonStats, MigrationPhase, MigrationOptions, MigrationErrorKind, } from './compute';
48
49
  export { DisposableMeshQueryRunner, InMemoryChainReader, MeshQuery, MeshQueryRunner, MeshQueryStream, QueryBuilder, parseMeshDbErrorKind, } from './meshdb';
package/dist/mesh.d.ts CHANGED
@@ -36,6 +36,7 @@
36
36
  * ```
37
37
  */
38
38
  import { type CapabilityFilter, type CapabilitySet, type ScopeFilter } from './capabilities';
39
+ import { type Aggregation, type AggregateRow, type CapacityQuery, type CapacityRow, type GroupBy, type TagMatcher } from './capability-aggregation';
39
40
  import type { SubnetId, SubnetPolicy } from './subnets';
40
41
  import type { Token } from './identity';
41
42
  /** Reliability mode chosen at stream-open time. */
@@ -299,6 +300,64 @@ export declare class MeshNode {
299
300
  * ```
300
301
  */
301
302
  findNodesScoped(filter: CapabilityFilter, scope: ScopeFilter): bigint[];
303
+ /**
304
+ * Bucketed aggregation over the local capability fold —
305
+ * `Fold::aggregate(matcher, groupBy, agg)`. Composes a matcher,
306
+ * a bucket-derivation, and a per-bucket reduction into a
307
+ * lex-sorted `[bucket, value][]`. Phase 6c-A of
308
+ * `MULTIFOLD_PHASE_6C_CAPACITY_AGGREGATION.md`.
309
+ *
310
+ * `matcher === undefined` walks every entry.
311
+ *
312
+ * @example
313
+ * ```typescript
314
+ * // Top GPU types by count.
315
+ * const rows = node.capabilityAggregate(
316
+ * { kind: 'prefix', value: 'hardware.gpu' },
317
+ * { kind: 'tagStem', prefix: 'hardware.gpu' },
318
+ * { kind: 'count' },
319
+ * );
320
+ * for (const r of rows) console.log(r.bucket, r.value);
321
+ * ```
322
+ */
323
+ capabilityAggregate(matcher: TagMatcher | undefined, groupBy: GroupBy, aggregation: Aggregation): AggregateRow[];
324
+ /**
325
+ * Capacity-ranked materialized view —
326
+ * `Fold::capacity_ranking(query, rttLookup)`. Per-bucket state
327
+ * breakdown + latency gate + optional summed numeric capacity,
328
+ * sorted by `available` desc (ties broken on bucket asc) and
329
+ * truncated to `query.limit`. Phase 6c-B.
330
+ *
331
+ * `rttEntries` is the materialized RTT map. `undefined`/empty
332
+ * disables the RTT filter regardless of `query.maxRttMs`. Per
333
+ * the plan, a `ThreadsafeFunction` closure variant is a follow-
334
+ * up; the map shape matches the Go / C wrappers and lines up
335
+ * with what operators typically have cached from the proximity
336
+ * graph.
337
+ *
338
+ * @example
339
+ * ```typescript
340
+ * // Top 5 GPU types available within 50 ms latency.
341
+ * const rttMap = [
342
+ * { nodeId: 0x1234n, rttMs: 25 },
343
+ * { nodeId: 0x5678n, rttMs: 75 },
344
+ * ];
345
+ * const rows = node.capabilityCapacityRanking(
346
+ * {
347
+ * matcher: { kind: 'prefix', value: 'hardware.gpu' },
348
+ * groupBy: { kind: 'tagStem', prefix: 'hardware.gpu' },
349
+ * maxRttMs: 50,
350
+ * sumAxisKey: 'hardware.gpu.count',
351
+ * limit: 5,
352
+ * },
353
+ * rttMap,
354
+ * );
355
+ * ```
356
+ */
357
+ capabilityCapacityRanking(query: CapacityQuery, rttEntries?: Array<{
358
+ nodeId: bigint;
359
+ rttMs: number;
360
+ }>): CapacityRow[];
302
361
  /** Shutdown the mesh node. */
303
362
  shutdown(): Promise<void>;
304
363
  }
package/dist/mesh.js CHANGED
@@ -41,6 +41,7 @@ exports.ChannelAuthError = exports.ChannelError = exports.MeshNode = exports.Not
41
41
  const core_1 = require("@net-mesh/core");
42
42
  const _internal_js_1 = require("./_internal.js");
43
43
  const capabilities_1 = require("./capabilities");
44
+ const capability_aggregation_1 = require("./capability-aggregation");
44
45
  /**
45
46
  * Thrown by {@link MeshNode.sendOnStream} / `sendWithRetry` /
46
47
  * `sendBlocking` when the stream's per-stream in-flight window is
@@ -122,6 +123,21 @@ class MeshNode {
122
123
  }
123
124
  native.testInjectSyntheticPeer(nodeId);
124
125
  }
126
+ /**
127
+ * Test-only — same shape as {@link _testInjectSyntheticPeer} but
128
+ * stages the synthetic peer with the supplied canonical tag
129
+ * strings. Used by the Phase 6c aggregation smoke tests to set
130
+ * up multi-bucket fixtures without spinning up multiple meshes.
131
+ *
132
+ * @internal
133
+ */
134
+ _testInjectSyntheticPeerWithTags(nodeId, tags) {
135
+ const native = this.native;
136
+ if (typeof native.testInjectSyntheticPeerWithTags !== 'function') {
137
+ throw new Error('testInjectSyntheticPeerWithTags: NAPI build missing `test-helpers` feature');
138
+ }
139
+ native.testInjectSyntheticPeerWithTags(nodeId, tags);
140
+ }
125
141
  /** Create and configure a new mesh node. */
126
142
  static async create(config) {
127
143
  const native = await core_1.NetMesh.create({
@@ -389,6 +405,69 @@ class MeshNode {
389
405
  findNodesScoped(filter, scope) {
390
406
  return this.native.findNodesScoped((0, capabilities_1.capabilityFilterToNapi)(filter), (0, capabilities_1.scopeFilterToNapi)(scope));
391
407
  }
408
+ /**
409
+ * Bucketed aggregation over the local capability fold —
410
+ * `Fold::aggregate(matcher, groupBy, agg)`. Composes a matcher,
411
+ * a bucket-derivation, and a per-bucket reduction into a
412
+ * lex-sorted `[bucket, value][]`. Phase 6c-A of
413
+ * `MULTIFOLD_PHASE_6C_CAPACITY_AGGREGATION.md`.
414
+ *
415
+ * `matcher === undefined` walks every entry.
416
+ *
417
+ * @example
418
+ * ```typescript
419
+ * // Top GPU types by count.
420
+ * const rows = node.capabilityAggregate(
421
+ * { kind: 'prefix', value: 'hardware.gpu' },
422
+ * { kind: 'tagStem', prefix: 'hardware.gpu' },
423
+ * { kind: 'count' },
424
+ * );
425
+ * for (const r of rows) console.log(r.bucket, r.value);
426
+ * ```
427
+ */
428
+ capabilityAggregate(matcher, groupBy, aggregation) {
429
+ const matcherJson = matcher ? (0, capability_aggregation_1.tagMatcherToJson)(matcher) : null;
430
+ const groupByJson = (0, capability_aggregation_1.groupByToJson)(groupBy);
431
+ const aggregationJson = (0, capability_aggregation_1.aggregationToJson)(aggregation);
432
+ return this.native.capabilityAggregate(matcherJson, groupByJson, aggregationJson);
433
+ }
434
+ /**
435
+ * Capacity-ranked materialized view —
436
+ * `Fold::capacity_ranking(query, rttLookup)`. Per-bucket state
437
+ * breakdown + latency gate + optional summed numeric capacity,
438
+ * sorted by `available` desc (ties broken on bucket asc) and
439
+ * truncated to `query.limit`. Phase 6c-B.
440
+ *
441
+ * `rttEntries` is the materialized RTT map. `undefined`/empty
442
+ * disables the RTT filter regardless of `query.maxRttMs`. Per
443
+ * the plan, a `ThreadsafeFunction` closure variant is a follow-
444
+ * up; the map shape matches the Go / C wrappers and lines up
445
+ * with what operators typically have cached from the proximity
446
+ * graph.
447
+ *
448
+ * @example
449
+ * ```typescript
450
+ * // Top 5 GPU types available within 50 ms latency.
451
+ * const rttMap = [
452
+ * { nodeId: 0x1234n, rttMs: 25 },
453
+ * { nodeId: 0x5678n, rttMs: 75 },
454
+ * ];
455
+ * const rows = node.capabilityCapacityRanking(
456
+ * {
457
+ * matcher: { kind: 'prefix', value: 'hardware.gpu' },
458
+ * groupBy: { kind: 'tagStem', prefix: 'hardware.gpu' },
459
+ * maxRttMs: 50,
460
+ * sumAxisKey: 'hardware.gpu.count',
461
+ * limit: 5,
462
+ * },
463
+ * rttMap,
464
+ * );
465
+ * ```
466
+ */
467
+ capabilityCapacityRanking(query, rttEntries) {
468
+ const queryJson = (0, capability_aggregation_1.capacityQueryToJson)(query);
469
+ return this.native.capabilityCapacityRanking(queryJson, rttEntries ?? null);
470
+ }
392
471
  /** Shutdown the mesh node. */
393
472
  async shutdown() {
394
473
  await this.native.shutdown();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@net-mesh/sdk",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "Ergonomic TypeScript SDK for the Net mesh network",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -29,7 +29,7 @@
29
29
  "test:watch": "vitest"
30
30
  },
31
31
  "peerDependencies": {
32
- "@net-mesh/core": ">=0.21.0"
32
+ "@net-mesh/core": ">=0.23.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@net-mesh/core": "file:../bindings/node",