@optimystic/db-p2p 0.11.3 → 0.13.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.
Files changed (83) hide show
  1. package/dist/src/cluster/block-transfer-service.d.ts +0 -1
  2. package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
  3. package/dist/src/cluster/block-transfer-service.js +0 -2
  4. package/dist/src/cluster/block-transfer-service.js.map +1 -1
  5. package/dist/src/cluster/cluster-repo.d.ts +1 -2
  6. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  7. package/dist/src/cluster/cluster-repo.js +5 -5
  8. package/dist/src/cluster/cluster-repo.js.map +1 -1
  9. package/dist/src/cluster/partition-detector.d.ts.map +1 -1
  10. package/dist/src/cluster/partition-detector.js +0 -1
  11. package/dist/src/cluster/partition-detector.js.map +1 -1
  12. package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
  13. package/dist/src/cluster/service.d.ts +0 -6
  14. package/dist/src/cluster/service.d.ts.map +1 -1
  15. package/dist/src/cluster/service.js +0 -13
  16. package/dist/src/cluster/service.js.map +1 -1
  17. package/dist/src/dispute/dispute-service.d.ts.map +1 -1
  18. package/dist/src/dispute/dispute-service.js +1 -1
  19. package/dist/src/dispute/dispute-service.js.map +1 -1
  20. package/dist/src/libp2p-key-network.js.map +1 -1
  21. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  22. package/dist/src/libp2p-node-base.js +14 -1
  23. package/dist/src/libp2p-node-base.js.map +1 -1
  24. package/dist/src/network/network-manager-service.js +1 -1
  25. package/dist/src/network/network-manager-service.js.map +1 -1
  26. package/dist/src/repo/cluster-coordinator.d.ts +1 -1
  27. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  28. package/dist/src/repo/cluster-coordinator.js +3 -3
  29. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  30. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  31. package/dist/src/repo/coordinator-repo.js +17 -9
  32. package/dist/src/repo/coordinator-repo.js.map +1 -1
  33. package/dist/src/repo/service.d.ts +0 -1
  34. package/dist/src/repo/service.d.ts.map +1 -1
  35. package/dist/src/repo/service.js +0 -2
  36. package/dist/src/repo/service.js.map +1 -1
  37. package/dist/src/routing/simple-cluster-coordinator.d.ts +2 -2
  38. package/dist/src/routing/simple-cluster-coordinator.d.ts.map +1 -1
  39. package/dist/src/routing/simple-cluster-coordinator.js +2 -2
  40. package/dist/src/routing/simple-cluster-coordinator.js.map +1 -1
  41. package/dist/src/storage/i-raw-storage.d.ts +9 -0
  42. package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
  43. package/dist/src/storage/memory-storage.d.ts +1 -0
  44. package/dist/src/storage/memory-storage.d.ts.map +1 -1
  45. package/dist/src/storage/memory-storage.js +27 -0
  46. package/dist/src/storage/memory-storage.js.map +1 -1
  47. package/dist/src/storage/ring-selector.d.ts.map +1 -1
  48. package/dist/src/storage/ring-selector.js +23 -5
  49. package/dist/src/storage/ring-selector.js.map +1 -1
  50. package/dist/src/storage/storage-monitor.d.ts.map +1 -1
  51. package/dist/src/storage/storage-monitor.js +10 -14
  52. package/dist/src/storage/storage-monitor.js.map +1 -1
  53. package/dist/src/storage/storage-repo.d.ts +2 -2
  54. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  55. package/dist/src/storage/storage-repo.js +2 -2
  56. package/dist/src/storage/storage-repo.js.map +1 -1
  57. package/dist/src/sync/service.d.ts +0 -1
  58. package/dist/src/sync/service.d.ts.map +1 -1
  59. package/dist/src/sync/service.js +0 -2
  60. package/dist/src/sync/service.js.map +1 -1
  61. package/dist/src/testing/mesh-harness.js.map +1 -1
  62. package/package.json +2 -2
  63. package/src/cluster/block-transfer-service.ts +1 -1
  64. package/src/cluster/cluster-repo.ts +5 -3
  65. package/src/cluster/partition-detector.ts +0 -2
  66. package/src/cluster/rebalance-monitor.ts +1 -1
  67. package/src/cluster/service.ts +0 -13
  68. package/src/dispute/dispute-service.ts +1 -2
  69. package/src/dispute/service.ts +1 -1
  70. package/src/libp2p-key-network.ts +1 -1
  71. package/src/libp2p-node-base.ts +13 -3
  72. package/src/network/network-manager-service.ts +1 -1
  73. package/src/repo/cluster-coordinator.ts +4 -4
  74. package/src/repo/coordinator-repo.ts +20 -10
  75. package/src/repo/service.ts +0 -2
  76. package/src/routing/simple-cluster-coordinator.ts +2 -2
  77. package/src/storage/i-raw-storage.ts +10 -0
  78. package/src/storage/memory-storage.ts +28 -0
  79. package/src/storage/ring-selector.ts +26 -5
  80. package/src/storage/storage-monitor.ts +10 -17
  81. package/src/storage/storage-repo.ts +2 -3
  82. package/src/sync/service.ts +2 -3
  83. package/src/testing/mesh-harness.ts +1 -1
@@ -169,7 +169,7 @@ export class NetworkManagerService implements Startable {
169
169
  throw new Error('FRET service not available for seeding keys');
170
170
  }
171
171
  const coord = await hashKey(key);
172
- const _neighbors = fret.getNeighbors(coord, 'both', 1);
172
+ fret.getNeighbors(coord, 'both', 1);
173
173
  }
174
174
 
175
175
  private toCacheKey(key: Uint8Array): string {
@@ -1,5 +1,5 @@
1
1
  import { peerIdFromString } from "@libp2p/peer-id";
2
- import type { ClusterRecord, IKeyNetwork, RepoMessage, BlockId, ClusterPeers, MessageOptions, Signature, ClusterConsensusConfig } from "@optimystic/db-core";
2
+ import type { ClusterRecord, IKeyNetwork, RepoMessage, BlockId, ClusterPeers, MessageOptions, ClusterConsensusConfig } from "@optimystic/db-core";
3
3
  import { base58btc } from "multiformats/bases/base58";
4
4
  import { sha256 } from "multiformats/hashes/sha2";
5
5
  import { ClusterClient } from "../cluster/client.js";
@@ -123,7 +123,7 @@ export class ClusterCoordinator {
123
123
  * Initiates a 2-phase transaction for a specific block ID.
124
124
  * Returns the cluster record and whether the local cluster already executed the operations.
125
125
  */
126
- async executeClusterTransaction(blockId: BlockId, message: RepoMessage, options?: MessageOptions): Promise<{
126
+ async executeClusterTransaction(blockId: BlockId, message: RepoMessage, _options?: MessageOptions): Promise<{
127
127
  record: ClusterRecord;
128
128
  localExecuted: boolean;
129
129
  }> {
@@ -298,7 +298,7 @@ export class ClusterCoordinator {
298
298
  * Validate that a small cluster size is legitimate by querying remote peers
299
299
  * for their network size estimates. Returns true if estimates roughly agree.
300
300
  */
301
- private async validateSmallCluster(localSize: number, peers: ClusterPeers): Promise<boolean> {
301
+ private async validateSmallCluster(localSize: number, _peers: ClusterPeers): Promise<boolean> {
302
302
  // If we have FRET and it shows confident estimate
303
303
  if (this.fretService) {
304
304
  try {
@@ -581,7 +581,7 @@ export class ClusterCoordinator {
581
581
  });
582
582
  }
583
583
 
584
- private scheduleCommitRetry(messageHash: string, record: ClusterRecord, missingPeers: string[]): void {
584
+ private scheduleCommitRetry(messageHash: string, _record: ClusterRecord, missingPeers: string[]): void {
585
585
  const state = this.transactions.get(messageHash);
586
586
  if (!state) {
587
587
  return;
@@ -187,8 +187,25 @@ export class CoordinatorRepo implements IRepo {
187
187
  private async fetchBlockFromCluster(blockId: BlockId, context?: ActionContext): Promise<void> {
188
188
  if (!this.clusterLatestCallback) return;
189
189
 
190
- // Query cluster for the block
191
- const clusterLatest = await this.queryClusterForLatest(blockId, context);
190
+ const blockIdBytes = new TextEncoder().encode(blockId);
191
+ const peers = await this.keyNetwork.findCluster(blockIdBytes);
192
+ const peerIds = peers ? Object.keys(peers) : [];
193
+ if (peerIds.length === 0) return;
194
+
195
+ // Solo-cluster short-circuit: the only responsible peer is us. There is no
196
+ // remote to sync from, so skip the callback entirely. Querying ourselves
197
+ // would dial self via SyncClient — pointless at best, and on nodes without
198
+ // listen addresses (e.g. solo WebSocket-only) the dial can hang.
199
+ if (
200
+ peerIds.length === 1
201
+ && this.localPeerId
202
+ && peerIds[0] === this.localPeerId.toString()
203
+ ) {
204
+ log('cluster-fetch:solo-self-skip', { blockId });
205
+ return;
206
+ }
207
+
208
+ const clusterLatest = await this.queryClusterForLatest(peerIds, blockId, context);
192
209
  if (clusterLatest) {
193
210
  // Found on cluster - trigger restoration to sync the block
194
211
  await this.storageRepo.get({ blockIds: [blockId], context: { committed: [clusterLatest], rev: clusterLatest.rev } });
@@ -199,14 +216,7 @@ export class CoordinatorRepo implements IRepo {
199
216
  /**
200
217
  * Query cluster peers to find the maximum latest revision for a block.
201
218
  */
202
- private async queryClusterForLatest(blockId: BlockId, context?: ActionContext): Promise<ActionRev | undefined> {
203
- const blockIdBytes = new TextEncoder().encode(blockId);
204
- const peers = await this.keyNetwork.findCluster(blockIdBytes);
205
- if (!peers || Object.keys(peers).length === 0) {
206
- return undefined;
207
- }
208
-
209
- const peerIds = Object.keys(peers);
219
+ private async queryClusterForLatest(peerIds: string[], blockId: BlockId, context?: ActionContext): Promise<ActionRev | undefined> {
210
220
  let maxLatest: ActionRev | undefined;
211
221
 
212
222
  // Add timeout wrapper to prevent hanging on unresponsive peers
@@ -63,7 +63,6 @@ export class RepoService implements Startable {
63
63
  private readonly repo: IRepo
64
64
  private readonly components: RepoServiceComponents
65
65
  private running: boolean
66
- private readonly k: number
67
66
  /** Responsibility K - how many peers are responsible for a key (for redirect decisions) */
68
67
  private readonly responsibilityK: number
69
68
 
@@ -76,7 +75,6 @@ export class RepoService implements Startable {
76
75
  this.log = components.logger.forComponent(init.logPrefix ?? 'db-p2p:repo-service')
77
76
  this.repo = components.repo
78
77
  this.running = false
79
- this.k = init.kBucketSize ?? 10
80
78
  this.responsibilityK = init.responsibilityK ?? 1
81
79
  }
82
80
 
@@ -57,13 +57,13 @@ export class ModuloCoordinator implements SimpleClusterCoordinator {
57
57
  * For very small clusters, just replicate everywhere
58
58
  */
59
59
  export class FullReplicationCoordinator implements SimpleClusterCoordinator {
60
- async selectCoordinator(key: Uint8Array, peers: PeerId[]): Promise<PeerId> {
60
+ async selectCoordinator(_key: Uint8Array, peers: PeerId[]): Promise<PeerId> {
61
61
  // Always select first peer as primary
62
62
  if (peers.length === 0) throw new Error('No peers available')
63
63
  return peers[0]!
64
64
  }
65
65
 
66
- async selectReplicas(key: Uint8Array, peers: PeerId[], replicationFactor: number): Promise<PeerId[]> {
66
+ async selectReplicas(_key: Uint8Array, peers: PeerId[], _replicationFactor: number): Promise<PeerId[]> {
67
67
  // Replicate to all peers in small clusters
68
68
  return [...peers]
69
69
  }
@@ -27,4 +27,14 @@ export interface IRawStorage {
27
27
 
28
28
  // Promote a pending action to a committed action
29
29
  promotePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void>;
30
+
31
+ /**
32
+ * Approximate bytes currently stored by this backend.
33
+ *
34
+ * Used by `StorageMonitor` to feed real used-space figures into ring selection.
35
+ * Implementations should return their best cheap estimate (e.g. on-disk size for
36
+ * filesystem backends, tracked footprint for in-memory backends). The result is
37
+ * advisory — `StorageMonitor` treats a missing implementation as 0.
38
+ */
39
+ getApproximateBytesUsed?(): Promise<number>;
30
40
  }
@@ -142,4 +142,32 @@ export class MemoryRawStorage implements IRawStorage {
142
142
  this.pendingActions.delete(key);
143
143
  }
144
144
  }
145
+
146
+ async getApproximateBytesUsed(): Promise<number> {
147
+ let total = 0;
148
+ for (const [blockId, meta] of this.metadata) {
149
+ total += blockId.length + jsonByteSize(meta);
150
+ }
151
+ for (const [key, actionId] of this.revisions) {
152
+ total += key.length + actionId.length;
153
+ }
154
+ for (const [key, transform] of this.pendingActions) {
155
+ total += key.length + jsonByteSize(transform);
156
+ }
157
+ for (const [key, transform] of this.actions) {
158
+ total += key.length + jsonByteSize(transform);
159
+ }
160
+ for (const [key, block] of this.materializedBlocks) {
161
+ total += key.length + jsonByteSize(block);
162
+ }
163
+ return total;
164
+ }
165
+ }
166
+
167
+ function jsonByteSize(value: unknown): number {
168
+ try {
169
+ return JSON.stringify(value)?.length ?? 0;
170
+ } catch {
171
+ return 0;
172
+ }
145
173
  }
@@ -42,11 +42,32 @@ export class RingSelector {
42
42
  return -1;
43
43
  }
44
44
 
45
- // Estimate total network size from FRET
46
- // We use a simple heuristic: assume average block size and typical data distribution
47
- const avgBlockSize = 100 * 1024; // 100KB typical block
48
- const estimatedTotalBlocks = 1000; // Conservative estimate
49
- const estimatedTotalData = estimatedTotalBlocks * avgBlockSize;
45
+ // Estimate total network spare capacity by aggregating observed per-ring stats.
46
+ // See docs/arachnode.md §"Capacity Management and Ring Adjustment": ring selection
47
+ // should reflect observed network demand, not a hard-coded constant. We treat
48
+ // Σ (peerCount × avgCapacity) over known rings as a proxy for total network spare
49
+ // capacity, so `coverage` becomes our share of it. More peers (or richer peers) →
50
+ // smaller share → higher ring depth (more specialization). On bootstrap, when no
51
+ // ring stats are observed yet, we fall back to a conservative fixed estimate so
52
+ // first-boot behavior is deterministic.
53
+ const ringStats = this.fretAdapter.getRingStats();
54
+ let estimatedTotalData: number;
55
+ if (ringStats.length > 0) {
56
+ estimatedTotalData = ringStats.reduce(
57
+ (sum, stat) => sum + stat.peerCount * stat.avgCapacity,
58
+ 0
59
+ );
60
+ } else {
61
+ const avgBlockSize = 100 * 1024; // 100KB typical block
62
+ const estimatedTotalBlocks = 1000; // Conservative estimate
63
+ estimatedTotalData = estimatedTotalBlocks * avgBlockSize;
64
+ }
65
+
66
+ // Guard against pathological zero-aggregate (e.g. all peers report 0 available);
67
+ // fall back to local capacity so coverage = 1.0 (ring 0).
68
+ if (estimatedTotalData <= 0) {
69
+ estimatedTotalData = capacity.available;
70
+ }
50
71
 
51
72
  // Calculate what fraction of keyspace we can cover
52
73
  const coverage = capacity.available / estimatedTotalData;
@@ -28,22 +28,15 @@ export class StorageMonitor {
28
28
  const usedOverride = this.config.usedBytes;
29
29
  const availableOverride = this.config.availableBytes;
30
30
 
31
- if (
32
- usedOverride !== undefined ||
33
- availableOverride !== undefined ||
34
- this.config.totalBytes !== undefined
35
- ) {
36
- const used = usedOverride ?? Math.max(0, total - (availableOverride ?? total));
37
- const available = availableOverride ?? Math.max(0, total - used);
38
- return {
39
- total,
40
- used: Math.min(total, Math.max(0, used)),
41
- available: Math.max(0, Math.min(total, available))
42
- };
43
- }
44
-
45
- const used = await this.estimateUsedSpace();
46
- const available = Math.max(0, total - used);
31
+ // Only short-circuit the backend when used/available is explicitly supplied;
32
+ // a bare `totalBytes` is just the disk-size hint and should still let the
33
+ // backend report actual used bytes.
34
+ const rawUsed = usedOverride
35
+ ?? (availableOverride !== undefined ? total - availableOverride : await this.estimateUsedSpace());
36
+ const used = Math.min(total, Math.max(0, rawUsed));
37
+ const available = availableOverride !== undefined
38
+ ? Math.max(0, Math.min(total, availableOverride))
39
+ : Math.max(0, total - used);
47
40
 
48
41
  return {
49
42
  total,
@@ -53,7 +46,7 @@ export class StorageMonitor {
53
46
  }
54
47
 
55
48
  private async estimateUsedSpace(): Promise<number> {
56
- return 0; // TODO: Implement actual space calculation
49
+ return (await this.storage.getApproximateBytesUsed?.()) ?? 0;
57
50
  }
58
51
  }
59
52
 
@@ -1,7 +1,6 @@
1
1
  import type {
2
2
  IRepo, MessageOptions, BlockId, CommitRequest, CommitResult, GetBlockResults, PendRequest, PendResult, ActionBlocks,
3
3
  ActionId, BlockGets, ActionPending, PendSuccess, ActionTransform, ActionTransforms,
4
- Transforms,
5
4
  GetBlockResult,
6
5
  PendValidationHook
7
6
  } from "@optimystic/db-core";
@@ -30,7 +29,7 @@ export class StorageRepo implements IRepo {
30
29
  this.validatePend = options?.validatePend;
31
30
  }
32
31
 
33
- async get({ blockIds, context }: BlockGets, options?: MessageOptions): Promise<GetBlockResults> {
32
+ async get({ blockIds, context }: BlockGets, _options?: MessageOptions): Promise<GetBlockResults> {
34
33
  const distinctBlockIds = Array.from(new Set(blockIds));
35
34
  log('get blockIds=%d', distinctBlockIds.length);
36
35
  const results = await Promise.all(distinctBlockIds.map(async (blockId) => {
@@ -191,7 +190,7 @@ export class StorageRepo implements IRepo {
191
190
  }));
192
191
  }
193
192
 
194
- async commit(request: CommitRequest, options?: MessageOptions): Promise<CommitResult> {
193
+ async commit(request: CommitRequest, _options?: MessageOptions): Promise<CommitResult> {
195
194
  log('commit actionId=%s rev=%d blockIds=%d', request.actionId, request.rev, request.blockIds.length);
196
195
  const uniqueBlockIds = Array.from(new Set(request.blockIds)).sort();
197
196
  const releases: (() => void)[] = [];
@@ -1,8 +1,7 @@
1
- import type { ComponentLogger, Startable, Stream } from '@libp2p/interface';
1
+ import type { ComponentLogger, Startable } from '@libp2p/interface';
2
2
  import type { IRepo } from '@optimystic/db-core';
3
3
  import { buildSyncProtocol, type SyncRequest, type SyncResponse } from './protocol.js';
4
4
  import { pipe } from 'it-pipe';
5
- import { fromString as u8FromString } from 'uint8arrays/from-string';
6
5
  import { toString as u8ToString } from 'uint8arrays/to-string';
7
6
  import * as lp from 'it-length-prefixed';
8
7
  import type { Uint8ArrayList } from 'uint8arraylist';
@@ -37,7 +36,7 @@ export class SyncService implements Startable {
37
36
  private readonly registrar: { handle: (...args: any[]) => Promise<void>, unhandle: (...args: any[]) => Promise<void> };
38
37
 
39
38
  constructor(
40
- private readonly components: SyncServiceComponents,
39
+ components: SyncServiceComponents,
41
40
  init: SyncServiceInit = {}
42
41
  ) {
43
42
  this.log = components.logger.forComponent('db-p2p:sync-service');
@@ -62,7 +62,7 @@ class MockMeshKeyNetwork implements IKeyNetwork {
62
62
  private readonly failures: MeshFailureConfig = {}
63
63
  ) {}
64
64
 
65
- async findCoordinator<T>(key: Uint8Array, options?: Partial<FindCoordinatorOptions>): Promise<PeerId> {
65
+ async findCoordinator(key: Uint8Array, options?: Partial<FindCoordinatorOptions>): Promise<PeerId> {
66
66
  const excluded = new Set((options?.excludedPeers ?? []).map(p => p.toString()));
67
67
  const sorted = this.sortedByDistance(key);
68
68
  const pick = sorted.find(n => !excluded.has(n.peerId.toString()));