@peerbit/blocks 4.2.5 → 4.2.6

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,256 @@
1
+ import type {
2
+ RustEagerBlockCache,
3
+ RustEagerBlockCacheStats,
4
+ } from "@peerbit/stream";
5
+
6
+ /** Recipient-side eager-response limits. `cacheSize` is kept for API compatibility. */
7
+ export type EagerBlocksOptions = {
8
+ /** Maximum number of validated blocks retained. */
9
+ cacheSize?: number;
10
+ /** Maximum combined byte length of retained blocks. */
11
+ maxBytes?: number;
12
+ /** Maximum byte length of one unsolicited block. */
13
+ maxBlockBytes?: number;
14
+ /** Time-to-live for a validated eager block, in milliseconds. */
15
+ ttlMs?: number;
16
+ /** Maximum number of simultaneous eager integrity checks. */
17
+ validationConcurrency?: number;
18
+ /** Maximum copied bytes waiting for, or undergoing, integrity checks. */
19
+ maxPendingBytes?: number;
20
+ /** Maximum blocks waiting for, or undergoing, integrity checks. */
21
+ maxPendingEntries?: number;
22
+ };
23
+
24
+ export type EagerBlocksSetting = boolean | EagerBlocksOptions;
25
+
26
+ export const DEFAULT_EAGER_BLOCK_CACHE_ENTRIES = 1_000;
27
+ export const DEFAULT_EAGER_BLOCK_CACHE_BYTES = 32 * 1024 * 1024;
28
+ export const DEFAULT_EAGER_BLOCK_MAX_BYTES = 10 * 1024 * 1024;
29
+ export const DEFAULT_EAGER_BLOCK_TTL_MS = 10_000;
30
+ export const DEFAULT_EAGER_BLOCK_VALIDATION_CONCURRENCY = 2;
31
+ export const DEFAULT_EAGER_BLOCK_PENDING_BYTES = 20 * 1024 * 1024;
32
+ export const DEFAULT_EAGER_BLOCK_PENDING_ENTRIES = 64;
33
+ export const MAX_EAGER_BLOCK_CID_LENGTH = 256;
34
+ export const MAX_EAGER_BLOCK_TTL_MS = 0x7fff_ffff;
35
+
36
+ export type NormalizedEagerBlocksOptions = {
37
+ maxEntries: number;
38
+ maxBytes: number;
39
+ maxBlockBytes: number;
40
+ ttlMs: number;
41
+ validationConcurrency: number;
42
+ maxPendingBytes: number;
43
+ maxPendingEntries: number;
44
+ };
45
+
46
+ const positiveSafeInteger = (value: number, name: string): number => {
47
+ if (!Number.isSafeInteger(value) || value <= 0) {
48
+ throw new RangeError(`${name} must be a positive safe integer`);
49
+ }
50
+ return value;
51
+ };
52
+
53
+ const positiveUint32 = (value: number, name: string): number => {
54
+ positiveSafeInteger(value, name);
55
+ if (value > 0xffff_ffff) {
56
+ throw new RangeError(`${name} must be at most 4294967295`);
57
+ }
58
+ return value;
59
+ };
60
+
61
+ const positiveTimerDelay = (value: number, name: string): number => {
62
+ positiveSafeInteger(value, name);
63
+ if (value > MAX_EAGER_BLOCK_TTL_MS) {
64
+ throw new RangeError(`${name} must be at most ${MAX_EAGER_BLOCK_TTL_MS}`);
65
+ }
66
+ return value;
67
+ };
68
+
69
+ export const normalizeEagerBlocksOptions = (
70
+ setting: Exclude<EagerBlocksSetting, false>,
71
+ ): NormalizedEagerBlocksOptions => {
72
+ const options = typeof setting === "boolean" ? {} : setting;
73
+ const maxEntries = positiveUint32(
74
+ options.cacheSize ?? DEFAULT_EAGER_BLOCK_CACHE_ENTRIES,
75
+ "eagerBlocks.cacheSize",
76
+ );
77
+ const maxBytes = positiveUint32(
78
+ options.maxBytes ?? DEFAULT_EAGER_BLOCK_CACHE_BYTES,
79
+ "eagerBlocks.maxBytes",
80
+ );
81
+ const maxBlockBytes = positiveUint32(
82
+ options.maxBlockBytes ?? DEFAULT_EAGER_BLOCK_MAX_BYTES,
83
+ "eagerBlocks.maxBlockBytes",
84
+ );
85
+ const ttlMs = positiveTimerDelay(
86
+ options.ttlMs ?? DEFAULT_EAGER_BLOCK_TTL_MS,
87
+ "eagerBlocks.ttlMs",
88
+ );
89
+ const validationConcurrency = positiveSafeInteger(
90
+ options.validationConcurrency ?? DEFAULT_EAGER_BLOCK_VALIDATION_CONCURRENCY,
91
+ "eagerBlocks.validationConcurrency",
92
+ );
93
+ const maxPendingBytes = positiveSafeInteger(
94
+ options.maxPendingBytes ?? DEFAULT_EAGER_BLOCK_PENDING_BYTES,
95
+ "eagerBlocks.maxPendingBytes",
96
+ );
97
+ const maxPendingEntries = positiveSafeInteger(
98
+ options.maxPendingEntries ?? DEFAULT_EAGER_BLOCK_PENDING_ENTRIES,
99
+ "eagerBlocks.maxPendingEntries",
100
+ );
101
+ return {
102
+ maxEntries,
103
+ maxBytes,
104
+ maxBlockBytes,
105
+ ttlMs,
106
+ validationConcurrency,
107
+ maxPendingBytes,
108
+ maxPendingEntries,
109
+ };
110
+ };
111
+
112
+ export interface EagerBlockCache extends RustEagerBlockCache {}
113
+
114
+ type CacheEntry = {
115
+ bytes: Uint8Array;
116
+ expiresAt: number;
117
+ };
118
+
119
+ /**
120
+ * Exact FIFO/TTL cache used when the native block-exchange core is disabled.
121
+ * Deletion releases the byte buffer immediately and both entry and byte
122
+ * accounting remain exact across replacement and delete/re-add cycles.
123
+ */
124
+ export class BoundedEagerBlockCache implements EagerBlockCache {
125
+ private readonly entries = new Map<string, CacheEntry>();
126
+ private currentBytes = 0;
127
+ private peakEntries = 0;
128
+ private peakBytes = 0;
129
+ private evictions = 0;
130
+ private expirations = 0;
131
+ private expiryTimer?: ReturnType<typeof setTimeout>;
132
+
133
+ constructor(
134
+ private readonly options: {
135
+ maxEntries: number;
136
+ maxBytes: number;
137
+ ttlMs: number;
138
+ },
139
+ ) {
140
+ positiveUint32(options.maxEntries, "maxEntries");
141
+ positiveUint32(options.maxBytes, "maxBytes");
142
+ positiveTimerDelay(options.ttlMs, "ttlMs");
143
+ }
144
+
145
+ add(cid: string, bytes: Uint8Array): boolean {
146
+ this.sweepExpired(Date.now());
147
+ if (bytes.byteLength > this.options.maxBytes) {
148
+ return false;
149
+ }
150
+ const backing = bytes.buffer as ArrayBufferLike & {
151
+ readonly resizable?: boolean;
152
+ readonly growable?: boolean;
153
+ };
154
+ const retainedBytes =
155
+ bytes.byteOffset === 0 &&
156
+ backing.byteLength === bytes.byteLength &&
157
+ backing.resizable !== true &&
158
+ backing.growable !== true
159
+ ? bytes
160
+ : bytes.slice();
161
+
162
+ this.remove(cid);
163
+ while (
164
+ this.entries.size >= this.options.maxEntries ||
165
+ this.currentBytes + retainedBytes.byteLength > this.options.maxBytes
166
+ ) {
167
+ const oldest = this.entries.keys().next().value as string | undefined;
168
+ if (oldest == null) break;
169
+ this.remove(oldest);
170
+ this.evictions += 1;
171
+ }
172
+
173
+ this.entries.set(cid, {
174
+ bytes: retainedBytes,
175
+ expiresAt: Date.now() + this.options.ttlMs,
176
+ });
177
+ this.currentBytes += retainedBytes.byteLength;
178
+ this.peakEntries = Math.max(this.peakEntries, this.entries.size);
179
+ this.peakBytes = Math.max(this.peakBytes, this.currentBytes);
180
+ this.scheduleExpiry();
181
+ return true;
182
+ }
183
+
184
+ get(cid: string): Uint8Array | undefined {
185
+ this.sweepExpired(Date.now());
186
+ return this.entries.get(cid)?.bytes;
187
+ }
188
+
189
+ del(cid: string): void {
190
+ if (this.remove(cid)) {
191
+ this.scheduleExpiry();
192
+ }
193
+ }
194
+
195
+ clear(): void {
196
+ if (this.expiryTimer) {
197
+ clearTimeout(this.expiryTimer);
198
+ this.expiryTimer = undefined;
199
+ }
200
+ this.entries.clear();
201
+ this.currentBytes = 0;
202
+ }
203
+
204
+ stats(): RustEagerBlockCacheStats {
205
+ this.sweepExpired(Date.now());
206
+ return {
207
+ entries: this.entries.size,
208
+ bytes: this.currentBytes,
209
+ peakEntries: this.peakEntries,
210
+ peakBytes: this.peakBytes,
211
+ evictions: this.evictions,
212
+ expirations: this.expirations,
213
+ };
214
+ }
215
+
216
+ private remove(cid: string): boolean {
217
+ const entry = this.entries.get(cid);
218
+ if (!entry) return false;
219
+ this.entries.delete(cid);
220
+ this.currentBytes -= entry.bytes.byteLength;
221
+ return true;
222
+ }
223
+
224
+ private sweepExpired(now: number): void {
225
+ let expired = 0;
226
+ for (const [cid, entry] of this.entries) {
227
+ if (entry.expiresAt > now) break;
228
+ this.remove(cid);
229
+ expired += 1;
230
+ }
231
+ if (expired > 0) {
232
+ this.expirations += expired;
233
+ this.scheduleExpiry();
234
+ }
235
+ }
236
+
237
+ private scheduleExpiry(): void {
238
+ if (this.expiryTimer) {
239
+ clearTimeout(this.expiryTimer);
240
+ this.expiryTimer = undefined;
241
+ }
242
+ const oldest = this.entries.values().next().value as CacheEntry | undefined;
243
+ if (!oldest) return;
244
+ this.expiryTimer = setTimeout(
245
+ () => {
246
+ this.expiryTimer = undefined;
247
+ this.sweepExpired(Date.now());
248
+ this.scheduleExpiry();
249
+ },
250
+ Math.max(0, oldest.expiresAt - Date.now()),
251
+ );
252
+ if (typeof this.expiryTimer === "object" && "unref" in this.expiryTimer) {
253
+ this.expiryTimer.unref();
254
+ }
255
+ }
256
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { DirectBlock } from "./libp2p.js";
2
2
  export * from "./interface.js";
3
3
  export * from "./any-blockstore.js";
4
+ export * from "./eager-cache.js";
4
5
  export * from "./libp2p.js";
5
6
  export * from "./remote.js";
package/src/libp2p.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  } from "@peerbit/stream-interface";
16
16
  import type { Block } from "multiformats/block";
17
17
  import { AnyBlockStore } from "./any-blockstore.js";
18
+ import type { EagerBlocksSetting } from "./eager-cache.js";
18
19
  import { BlockMessage, BlockRequest, BlockResponse, RemoteBlocks } from "./remote.js";
19
20
 
20
21
  export type DirectBlockComponents = DirectStreamComponents;
@@ -32,7 +33,7 @@ export class DirectBlock extends DirectStream implements IBlocks {
32
33
  canRelayMessage?: boolean;
33
34
  localTimeout?: number;
34
35
  messageProcessingConcurrency?: number;
35
- eagerBlocks?: boolean | { cacheSize?: number };
36
+ eagerBlocks?: EagerBlocksSetting;
36
37
  resolveProviders?: (
37
38
  cid: string,
38
39
  options?: { signal?: AbortSignal },
@@ -175,6 +176,14 @@ export class DirectBlock extends DirectStream implements IBlocks {
175
176
  this.remoteBlocks.onReachable(evt.detail);
176
177
  }
177
178
 
179
+ getEagerBlockCacheTelemetry() {
180
+ return this.remoteBlocks.getEagerBlockCacheTelemetry();
181
+ }
182
+
183
+ waitForEagerBlockValidation(): Promise<void> {
184
+ return this.remoteBlocks.waitForEagerBlockValidation();
185
+ }
186
+
178
187
  private encodeBlockMessage(message: BlockRequest | BlockResponse): Uint8Array {
179
188
  const blockExchange = this.rustCore?.blockExchange;
180
189
  if (blockExchange) {