@optimystic/db-p2p 0.28.0 → 0.29.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 (67) hide show
  1. package/dist/src/cluster/block-transfer-service.d.ts +0 -10
  2. package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
  3. package/dist/src/cluster/block-transfer-service.js +4 -2
  4. package/dist/src/cluster/block-transfer-service.js.map +1 -1
  5. package/dist/src/cluster/cluster-policy.d.ts +101 -18
  6. package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
  7. package/dist/src/cluster/cluster-policy.js +153 -30
  8. package/dist/src/cluster/cluster-policy.js.map +1 -1
  9. package/dist/src/cluster/quorum-restore.d.ts +4 -2
  10. package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
  11. package/dist/src/cluster/quorum-restore.js +4 -2
  12. package/dist/src/cluster/quorum-restore.js.map +1 -1
  13. package/dist/src/cluster/service.d.ts +12 -9
  14. package/dist/src/cluster/service.d.ts.map +1 -1
  15. package/dist/src/cluster/service.js +6 -6
  16. package/dist/src/cluster/service.js.map +1 -1
  17. package/dist/src/dispute/service.d.ts +1 -4
  18. package/dist/src/dispute/service.d.ts.map +1 -1
  19. package/dist/src/dispute/service.js +2 -1
  20. package/dist/src/dispute/service.js.map +1 -1
  21. package/dist/src/libp2p-key-network.d.ts +8 -2
  22. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  23. package/dist/src/libp2p-key-network.js +8 -2
  24. package/dist/src/libp2p-key-network.js.map +1 -1
  25. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  26. package/dist/src/libp2p-node-base.js +22 -19
  27. package/dist/src/libp2p-node-base.js.map +1 -1
  28. package/dist/src/logger.d.ts +28 -1
  29. package/dist/src/logger.d.ts.map +1 -1
  30. package/dist/src/logger.js +143 -1
  31. package/dist/src/logger.js.map +1 -1
  32. package/dist/src/network/network-manager-service.d.ts +1 -4
  33. package/dist/src/network/network-manager-service.d.ts.map +1 -1
  34. package/dist/src/network/network-manager-service.js +2 -1
  35. package/dist/src/network/network-manager-service.js.map +1 -1
  36. package/dist/src/repo/coordinator-repo.d.ts +183 -13
  37. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  38. package/dist/src/repo/coordinator-repo.js +686 -107
  39. package/dist/src/repo/coordinator-repo.js.map +1 -1
  40. package/dist/src/repo/service.d.ts +9 -6
  41. package/dist/src/repo/service.d.ts.map +1 -1
  42. package/dist/src/repo/service.js +4 -5
  43. package/dist/src/repo/service.js.map +1 -1
  44. package/dist/src/sync/service.d.ts +1 -2
  45. package/dist/src/sync/service.d.ts.map +1 -1
  46. package/dist/src/sync/service.js +2 -1
  47. package/dist/src/sync/service.js.map +1 -1
  48. package/dist/src/testing/mesh-harness.d.ts +7 -1
  49. package/dist/src/testing/mesh-harness.d.ts.map +1 -1
  50. package/dist/src/testing/mesh-harness.js +2 -1
  51. package/dist/src/testing/mesh-harness.js.map +1 -1
  52. package/package.json +2 -2
  53. package/readme.md +19 -0
  54. package/src/cluster/block-transfer-service.ts +4 -8
  55. package/src/cluster/cluster-policy.ts +196 -36
  56. package/src/cluster/quorum-restore.ts +4 -2
  57. package/src/cluster/service.ts +14 -9
  58. package/src/dispute/service.ts +3 -3
  59. package/src/libp2p-key-network.ts +8 -2
  60. package/src/libp2p-node-base.ts +22 -19
  61. package/src/logger.ts +196 -2
  62. package/src/network/network-manager-service.ts +414 -414
  63. package/src/protocol-client.ts +196 -196
  64. package/src/repo/coordinator-repo.ts +833 -122
  65. package/src/repo/service.ts +12 -9
  66. package/src/sync/service.ts +3 -5
  67. package/src/testing/mesh-harness.ts +8 -1
@@ -1,196 +1,196 @@
1
- import { pipe } from 'it-pipe';
2
- import { encode as lpEncode, decode as lpDecode } from 'it-length-prefixed';
3
- import type { Stream as Libp2pStream } from '@libp2p/interface';
4
- import type { PeerId, IPeerNetwork } from '@optimystic/db-core';
5
- import { first } from './it-utility.js';
6
- import { createLogger } from './logger.js';
7
- import { MAX_BLOCK_MESSAGE_BYTES } from './protocol-limits.js';
8
-
9
- const log = createLogger('protocol-client');
10
-
11
- /**
12
- * Thrown when the per-peer dial deadline expires before a stream is established.
13
- * Distinct from a libp2p dial failure (no route, refused, etc.) so the
14
- * batch-retry loop and diagnostic surfaces can identify a slow/unreachable peer
15
- * specifically. `.code === DIAL_TIMEOUT_ERROR_CODE`.
16
- */
17
- export const DIAL_TIMEOUT_ERROR_CODE = 'DIAL_TIMEOUT';
18
-
19
- export class DialTimeoutError extends Error {
20
- readonly code = DIAL_TIMEOUT_ERROR_CODE;
21
- constructor(peer: string, protocol: string, ms: number) {
22
- super(`dial timeout: peer=${peer} protocol=${protocol} after ${ms}ms`);
23
- this.name = 'DialTimeoutError';
24
- }
25
- }
26
-
27
- /**
28
- * Thrown when a peer dialed successfully but the response-read deadline expired
29
- * before it wrote a reply (it connected, then went silent). Distinct from
30
- * {@link DialTimeoutError} (never connected) and from a parent cancellation
31
- * (`options.signal`), so callers/diagnostics can tell "peer went quiet" apart
32
- * from "peer was unreachable" and "we cancelled". `.code === RESPONSE_TIMEOUT_ERROR_CODE`.
33
- */
34
- export const RESPONSE_TIMEOUT_ERROR_CODE = 'RESPONSE_TIMEOUT';
35
-
36
- export class ResponseTimeoutError extends Error {
37
- readonly code = RESPONSE_TIMEOUT_ERROR_CODE;
38
- constructor(peer: string, protocol: string, ms: number) {
39
- super(`response timeout: peer=${peer} protocol=${protocol} after ${ms}ms`);
40
- this.name = 'ResponseTimeoutError';
41
- }
42
- }
43
-
44
- /** Base class for clients that communicate via a libp2p protocol */
45
- export class ProtocolClient {
46
- constructor(
47
- protected readonly peerId: PeerId,
48
- protected readonly peerNetwork: IPeerNetwork,
49
- ) { }
50
-
51
- protected async processMessage<T>(
52
- message: unknown,
53
- protocol: string,
54
- options?: { signal?: AbortSignal; correlationId?: string; dialTimeoutMs?: number; responseTimeoutMs?: number; maxDataLength?: number }
55
- ): Promise<T> {
56
- const peer = this.peerId.toString();
57
- const cid = options?.correlationId;
58
- log('dial peer=%s protocol=%s%s', peer, protocol, cid ? ` cid=${cid}` : '');
59
- const t0 = Date.now();
60
-
61
- // Per-peer dial deadline. When set, an unreachable peer fails fast so the
62
- // caller can re-pick a different coordinator — independent of any overall
63
- // transaction budget the caller may also be enforcing.
64
- const dialTimeoutMs = options?.dialTimeoutMs;
65
- const dialController = dialTimeoutMs && dialTimeoutMs > 0 ? new AbortController() : undefined;
66
- let dialTimer: ReturnType<typeof setTimeout> | undefined;
67
- const onParentAbort = () => dialController?.abort(options?.signal?.reason);
68
- if (dialController) {
69
- dialTimer = setTimeout(() => {
70
- dialController.abort(new DialTimeoutError(peer, protocol, dialTimeoutMs!));
71
- }, dialTimeoutMs);
72
- if (options?.signal) {
73
- if (options.signal.aborted) dialController.abort(options.signal.reason);
74
- else options.signal.addEventListener('abort', onParentAbort, { once: true });
75
- }
76
- }
77
- const dialSignal = dialController?.signal ?? options?.signal;
78
-
79
- let stream: Libp2pStream;
80
- try {
81
- stream = await this.peerNetwork.connect(
82
- this.peerId,
83
- protocol,
84
- { signal: dialSignal }
85
- ) as unknown as Libp2pStream;
86
- } catch (err) {
87
- const elapsed = Date.now() - t0;
88
- // If the dial AbortController fired due to our own timer, surface the
89
- // dial-timeout error rather than the underlying AbortError so callers
90
- // can distinguish "peer was slow" from "user/parent cancelled".
91
- if (dialController?.signal.aborted && dialController.signal.reason instanceof DialTimeoutError) {
92
- log('dial:timeout peer=%s protocol=%s ms=%d%s', peer, protocol, elapsed, cid ? ` cid=${cid}` : '');
93
- throw dialController.signal.reason;
94
- }
95
- const errCode = (err as { code?: unknown })?.code;
96
- const errMessage = err instanceof Error ? err.message : String(err);
97
- const truncatedMsg = errMessage.length > 200 ? errMessage.slice(0, 200) + '…' : errMessage;
98
- log('dial:fail peer=%s protocol=%s ms=%d code=%s msg=%s%s',
99
- peer, protocol, elapsed,
100
- typeof errCode === 'string' && errCode.length > 0 ? errCode : 'none',
101
- truncatedMsg,
102
- cid ? ` cid=${cid}` : ''
103
- );
104
- throw err;
105
- } finally {
106
- if (dialTimer) clearTimeout(dialTimer);
107
- if (options?.signal) options.signal.removeEventListener('abort', onParentAbort);
108
- }
109
- log('dial:ok peer=%s ms=%d%s', peer, Date.now() - t0, cid ? ` cid=${cid}` : '');
110
-
111
- // Per-peer response deadline. The dial controller is already cleared once a
112
- // stream is established, so a peer that connects but then never writes a frame
113
- // (and never closes the stream) would hang the `first(...)` read below forever.
114
- // Setting a timer alone is not enough — the underlying `for await` over the
115
- // libp2p stream keeps awaiting regardless. The decisive action is to actively
116
- // `stream.abort(...)`, which rejects the stream's async iterator and unblocks
117
- // the read. A parent `options.signal` abort is forwarded the same way so a
118
- // cancelled caller tears the stream down rather than leaking it. When neither
119
- // `responseTimeoutMs` nor `signal` is supplied, no cap is imposed — preserving
120
- // every existing caller (mirrors how omitting `dialTimeoutMs` imposes no dial cap).
121
- const responseTimeoutMs = options?.responseTimeoutMs;
122
- let responseTimer: ReturnType<typeof setTimeout> | undefined;
123
- let responseTimeoutError: ResponseTimeoutError | undefined;
124
- const onParentAbortResponse = () => {
125
- try { stream.abort(options?.signal?.reason ?? new Error('aborted')); } catch { /* already torn down */ }
126
- };
127
- try {
128
- if (responseTimeoutMs && responseTimeoutMs > 0) {
129
- responseTimer = setTimeout(() => {
130
- responseTimeoutError = new ResponseTimeoutError(peer, protocol, responseTimeoutMs);
131
- try { stream.abort(responseTimeoutError); } catch { /* already torn down */ }
132
- }, responseTimeoutMs);
133
- }
134
- if (options?.signal) {
135
- if (options.signal.aborted) onParentAbortResponse();
136
- else options.signal.addEventListener('abort', onParentAbortResponse, { once: true });
137
- }
138
-
139
- // Send the request using length-prefixed encoding
140
- const encoded = pipe(
141
- [new TextEncoder().encode(JSON.stringify(message))],
142
- lpEncode
143
- );
144
- for await (const chunk of encoded) {
145
- stream.send(chunk);
146
- }
147
-
148
- // Read the response from the stream (which is now directly AsyncIterable).
149
- // Cap the response frame size so a peer can't flood the client with an
150
- // oversized reply; each caller passes the cap matching its response shape
151
- // (control vs block), defaulting to the block cap so no existing caller
152
- // regresses. An oversized frame rejects at the length-prefix (before
153
- // allocation) and surfaces through the read's catch, tearing the stream down.
154
- const maxDataLength = options?.maxDataLength ?? MAX_BLOCK_MESSAGE_BYTES;
155
- let firstByte = true;
156
- const source = pipe(
157
- stream,
158
- (source) => lpDecode(source, { maxDataLength }),
159
- async function* (source) {
160
- for await (const data of source) {
161
- if (firstByte) {
162
- log('first-byte peer=%s ms=%d%s', peer, Date.now() - t0, cid ? ` cid=${cid}` : '');
163
- firstByte = false;
164
- }
165
- const decoded = new TextDecoder().decode(data.subarray());
166
- const parsed = JSON.parse(decoded);
167
- yield parsed;
168
- }
169
- }
170
- ) as AsyncIterable<T>;
171
-
172
- let result: T;
173
- try {
174
- result = await first(() => source, () => { throw new Error('No response received') });
175
- } catch (err) {
176
- // Aborting the stream (our timer or a parent abort) surfaces here as an
177
- // iterator error. Translate it so callers see why the read ended.
178
- if (responseTimeoutError) {
179
- log('response:timeout peer=%s protocol=%s ms=%d%s', peer, protocol, Date.now() - t0, cid ? ` cid=${cid}` : '');
180
- throw responseTimeoutError;
181
- }
182
- if (options?.signal?.aborted) {
183
- throw options.signal.reason;
184
- }
185
- throw err;
186
- }
187
- log('response peer=%s protocol=%s ms=%d%s', peer, protocol, Date.now() - t0, cid ? ` cid=${cid}` : '');
188
- return result;
189
- } finally {
190
- if (responseTimer) clearTimeout(responseTimer);
191
- if (options?.signal) options.signal.removeEventListener('abort', onParentAbortResponse);
192
- // Closing an already-aborted stream must be safe.
193
- try { await stream.close(); } catch { /* already torn down */ }
194
- }
195
- }
196
- }
1
+ import { pipe } from 'it-pipe';
2
+ import { encode as lpEncode, decode as lpDecode } from 'it-length-prefixed';
3
+ import type { Stream as Libp2pStream } from '@libp2p/interface';
4
+ import type { PeerId, IPeerNetwork } from '@optimystic/db-core';
5
+ import { first } from './it-utility.js';
6
+ import { createLogger } from './logger.js';
7
+ import { MAX_BLOCK_MESSAGE_BYTES } from './protocol-limits.js';
8
+
9
+ const log = createLogger('protocol-client');
10
+
11
+ /**
12
+ * Thrown when the per-peer dial deadline expires before a stream is established.
13
+ * Distinct from a libp2p dial failure (no route, refused, etc.) so the
14
+ * batch-retry loop and diagnostic surfaces can identify a slow/unreachable peer
15
+ * specifically. `.code === DIAL_TIMEOUT_ERROR_CODE`.
16
+ */
17
+ export const DIAL_TIMEOUT_ERROR_CODE = 'DIAL_TIMEOUT';
18
+
19
+ export class DialTimeoutError extends Error {
20
+ readonly code = DIAL_TIMEOUT_ERROR_CODE;
21
+ constructor(peer: string, protocol: string, ms: number) {
22
+ super(`dial timeout: peer=${peer} protocol=${protocol} after ${ms}ms`);
23
+ this.name = 'DialTimeoutError';
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Thrown when a peer dialed successfully but the response-read deadline expired
29
+ * before it wrote a reply (it connected, then went silent). Distinct from
30
+ * {@link DialTimeoutError} (never connected) and from a parent cancellation
31
+ * (`options.signal`), so callers/diagnostics can tell "peer went quiet" apart
32
+ * from "peer was unreachable" and "we cancelled". `.code === RESPONSE_TIMEOUT_ERROR_CODE`.
33
+ */
34
+ export const RESPONSE_TIMEOUT_ERROR_CODE = 'RESPONSE_TIMEOUT';
35
+
36
+ export class ResponseTimeoutError extends Error {
37
+ readonly code = RESPONSE_TIMEOUT_ERROR_CODE;
38
+ constructor(peer: string, protocol: string, ms: number) {
39
+ super(`response timeout: peer=${peer} protocol=${protocol} after ${ms}ms`);
40
+ this.name = 'ResponseTimeoutError';
41
+ }
42
+ }
43
+
44
+ /** Base class for clients that communicate via a libp2p protocol */
45
+ export class ProtocolClient {
46
+ constructor(
47
+ protected readonly peerId: PeerId,
48
+ protected readonly peerNetwork: IPeerNetwork,
49
+ ) { }
50
+
51
+ protected async processMessage<T>(
52
+ message: unknown,
53
+ protocol: string,
54
+ options?: { signal?: AbortSignal; correlationId?: string; dialTimeoutMs?: number; responseTimeoutMs?: number; maxDataLength?: number }
55
+ ): Promise<T> {
56
+ const peer = this.peerId.toString();
57
+ const cid = options?.correlationId;
58
+ log('dial peer=%s protocol=%s%s', peer, protocol, cid ? ` cid=${cid}` : '');
59
+ const t0 = Date.now();
60
+
61
+ // Per-peer dial deadline. When set, an unreachable peer fails fast so the
62
+ // caller can re-pick a different coordinator — independent of any overall
63
+ // transaction budget the caller may also be enforcing.
64
+ const dialTimeoutMs = options?.dialTimeoutMs;
65
+ const dialController = dialTimeoutMs && dialTimeoutMs > 0 ? new AbortController() : undefined;
66
+ let dialTimer: ReturnType<typeof setTimeout> | undefined;
67
+ const onParentAbort = () => dialController?.abort(options?.signal?.reason);
68
+ if (dialController) {
69
+ dialTimer = setTimeout(() => {
70
+ dialController.abort(new DialTimeoutError(peer, protocol, dialTimeoutMs!));
71
+ }, dialTimeoutMs);
72
+ if (options?.signal) {
73
+ if (options.signal.aborted) dialController.abort(options.signal.reason);
74
+ else options.signal.addEventListener('abort', onParentAbort, { once: true });
75
+ }
76
+ }
77
+ const dialSignal = dialController?.signal ?? options?.signal;
78
+
79
+ let stream: Libp2pStream;
80
+ try {
81
+ stream = await this.peerNetwork.connect(
82
+ this.peerId,
83
+ protocol,
84
+ { signal: dialSignal }
85
+ ) as unknown as Libp2pStream;
86
+ } catch (err) {
87
+ const elapsed = Date.now() - t0;
88
+ // If the dial AbortController fired due to our own timer, surface the
89
+ // dial-timeout error rather than the underlying AbortError so callers
90
+ // can distinguish "peer was slow" from "user/parent cancelled".
91
+ if (dialController?.signal.aborted && dialController.signal.reason instanceof DialTimeoutError) {
92
+ log('dial:timeout peer=%s protocol=%s ms=%d%s', peer, protocol, elapsed, cid ? ` cid=${cid}` : '');
93
+ throw dialController.signal.reason;
94
+ }
95
+ const errCode = (err as { code?: unknown })?.code;
96
+ const errMessage = err instanceof Error ? err.message : String(err);
97
+ const truncatedMsg = errMessage.length > 200 ? errMessage.slice(0, 200) + '…' : errMessage;
98
+ log('dial:fail peer=%s protocol=%s ms=%d code=%s msg=%s%s',
99
+ peer, protocol, elapsed,
100
+ typeof errCode === 'string' && errCode.length > 0 ? errCode : 'none',
101
+ truncatedMsg,
102
+ cid ? ` cid=${cid}` : ''
103
+ );
104
+ throw err;
105
+ } finally {
106
+ if (dialTimer) clearTimeout(dialTimer);
107
+ if (options?.signal) options.signal.removeEventListener('abort', onParentAbort);
108
+ }
109
+ log('dial:ok peer=%s ms=%d%s', peer, Date.now() - t0, cid ? ` cid=${cid}` : '');
110
+
111
+ // Per-peer response deadline. The dial controller is already cleared once a
112
+ // stream is established, so a peer that connects but then never writes a frame
113
+ // (and never closes the stream) would hang the `first(...)` read below forever.
114
+ // Setting a timer alone is not enough — the underlying `for await` over the
115
+ // libp2p stream keeps awaiting regardless. The decisive action is to actively
116
+ // `stream.abort(...)`, which rejects the stream's async iterator and unblocks
117
+ // the read. A parent `options.signal` abort is forwarded the same way so a
118
+ // cancelled caller tears the stream down rather than leaking it. When neither
119
+ // `responseTimeoutMs` nor `signal` is supplied, no cap is imposed — preserving
120
+ // every existing caller (mirrors how omitting `dialTimeoutMs` imposes no dial cap).
121
+ const responseTimeoutMs = options?.responseTimeoutMs;
122
+ let responseTimer: ReturnType<typeof setTimeout> | undefined;
123
+ let responseTimeoutError: ResponseTimeoutError | undefined;
124
+ const onParentAbortResponse = () => {
125
+ try { stream.abort(options?.signal?.reason ?? new Error('aborted')); } catch { /* already torn down */ }
126
+ };
127
+ try {
128
+ if (responseTimeoutMs && responseTimeoutMs > 0) {
129
+ responseTimer = setTimeout(() => {
130
+ responseTimeoutError = new ResponseTimeoutError(peer, protocol, responseTimeoutMs);
131
+ try { stream.abort(responseTimeoutError); } catch { /* already torn down */ }
132
+ }, responseTimeoutMs);
133
+ }
134
+ if (options?.signal) {
135
+ if (options.signal.aborted) onParentAbortResponse();
136
+ else options.signal.addEventListener('abort', onParentAbortResponse, { once: true });
137
+ }
138
+
139
+ // Send the request using length-prefixed encoding
140
+ const encoded = pipe(
141
+ [new TextEncoder().encode(JSON.stringify(message))],
142
+ lpEncode
143
+ );
144
+ for await (const chunk of encoded) {
145
+ stream.send(chunk);
146
+ }
147
+
148
+ // Read the response from the stream (which is now directly AsyncIterable).
149
+ // Cap the response frame size so a peer can't flood the client with an
150
+ // oversized reply; each caller passes the cap matching its response shape
151
+ // (control vs block), defaulting to the block cap so no existing caller
152
+ // regresses. An oversized frame rejects at the length-prefix (before
153
+ // allocation) and surfaces through the read's catch, tearing the stream down.
154
+ const maxDataLength = options?.maxDataLength ?? MAX_BLOCK_MESSAGE_BYTES;
155
+ let firstByte = true;
156
+ const source = pipe(
157
+ stream,
158
+ (source) => lpDecode(source, { maxDataLength }),
159
+ async function* (source) {
160
+ for await (const data of source) {
161
+ if (firstByte) {
162
+ log('first-byte peer=%s ms=%d%s', peer, Date.now() - t0, cid ? ` cid=${cid}` : '');
163
+ firstByte = false;
164
+ }
165
+ const decoded = new TextDecoder().decode(data.subarray());
166
+ const parsed = JSON.parse(decoded);
167
+ yield parsed;
168
+ }
169
+ }
170
+ ) as AsyncIterable<T>;
171
+
172
+ let result: T;
173
+ try {
174
+ result = await first(() => source, () => { throw new Error('No response received') });
175
+ } catch (err) {
176
+ // Aborting the stream (our timer or a parent abort) surfaces here as an
177
+ // iterator error. Translate it so callers see why the read ended.
178
+ if (responseTimeoutError) {
179
+ log('response:timeout peer=%s protocol=%s ms=%d%s', peer, protocol, Date.now() - t0, cid ? ` cid=${cid}` : '');
180
+ throw responseTimeoutError;
181
+ }
182
+ if (options?.signal?.aborted) {
183
+ throw options.signal.reason;
184
+ }
185
+ throw err;
186
+ }
187
+ log('response peer=%s protocol=%s ms=%d%s', peer, protocol, Date.now() - t0, cid ? ` cid=${cid}` : '');
188
+ return result;
189
+ } finally {
190
+ if (responseTimer) clearTimeout(responseTimer);
191
+ if (options?.signal) options.signal.removeEventListener('abort', onParentAbortResponse);
192
+ // Closing an already-aborted stream must be safe.
193
+ try { await stream.close(); } catch { /* already torn down */ }
194
+ }
195
+ }
196
+ }