@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.
- package/README.md +57 -0
- package/dist/src/eager-cache.d.ts +69 -0
- package/dist/src/eager-cache.d.ts.map +1 -0
- package/dist/src/eager-cache.js +167 -0
- package/dist/src/eager-cache.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p.d.ts +4 -3
- package/dist/src/libp2p.d.ts.map +1 -1
- package/dist/src/libp2p.js +6 -0
- package/dist/src/libp2p.js.map +1 -1
- package/dist/src/remote.d.ts +34 -6
- package/dist/src/remote.d.ts.map +1 -1
- package/dist/src/remote.js +271 -42
- package/dist/src/remote.js.map +1 -1
- package/package.json +5 -5
- package/src/eager-cache.ts +256 -0
- package/src/index.ts +1 -0
- package/src/libp2p.ts +10 -1
- package/src/remote.ts +359 -60
package/src/remote.ts
CHANGED
|
@@ -3,10 +3,12 @@ import { TypedEventEmitter } from "@libp2p/interface";
|
|
|
3
3
|
import {
|
|
4
4
|
type GetOptions,
|
|
5
5
|
type Blocks as IBlocks,
|
|
6
|
-
checkDecodeBlock,
|
|
7
6
|
cidifyString,
|
|
8
7
|
codecCodes,
|
|
8
|
+
codecMap,
|
|
9
|
+
defaultHasher,
|
|
9
10
|
stringifyCid,
|
|
11
|
+
verifyBlockBytes,
|
|
10
12
|
} from "@peerbit/blocks-interface";
|
|
11
13
|
import { Cache } from "@peerbit/cache";
|
|
12
14
|
import { PublicSignKey } from "@peerbit/crypto";
|
|
@@ -18,6 +20,8 @@ import {
|
|
|
18
20
|
dontThrowIfDeliveryError,
|
|
19
21
|
} from "@peerbit/stream";
|
|
20
22
|
import {
|
|
23
|
+
BACKGROUND_MESSAGE_PRIORITY,
|
|
24
|
+
FOREGROUND_READ_MESSAGE_PRIORITY,
|
|
21
25
|
type PeerRefs,
|
|
22
26
|
type RequestTransportContext,
|
|
23
27
|
SilentDelivery,
|
|
@@ -29,6 +33,14 @@ import { AbortError } from "@peerbit/time";
|
|
|
29
33
|
import { CID } from "multiformats";
|
|
30
34
|
import { type Block } from "multiformats/block";
|
|
31
35
|
import PQueue from "p-queue";
|
|
36
|
+
import {
|
|
37
|
+
BoundedEagerBlockCache,
|
|
38
|
+
type EagerBlockCache,
|
|
39
|
+
type EagerBlocksSetting,
|
|
40
|
+
MAX_EAGER_BLOCK_CID_LENGTH,
|
|
41
|
+
type NormalizedEagerBlocksOptions,
|
|
42
|
+
normalizeEagerBlocksOptions,
|
|
43
|
+
} from "./eager-cache.js";
|
|
32
44
|
import type { BlockStore } from "./interface.js";
|
|
33
45
|
|
|
34
46
|
export const logger = loggerFn("peerbit:transport:blocks");
|
|
@@ -70,23 +82,46 @@ type BlockMessageContext = {
|
|
|
70
82
|
type InFlightRead = {
|
|
71
83
|
promise: Promise<Block<any, any, any, 1> | undefined>;
|
|
72
84
|
addProviders: (providers: string[]) => void;
|
|
85
|
+
promotePriority: (priority: number | undefined) => void;
|
|
73
86
|
};
|
|
74
87
|
|
|
75
88
|
type RemoteReadOptions = Exclude<GetOptions["remote"], boolean | undefined> & {
|
|
76
89
|
hasher?: any;
|
|
77
90
|
};
|
|
78
91
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
export type EagerBlockCacheTelemetry = {
|
|
93
|
+
entries: number;
|
|
94
|
+
bytes: number;
|
|
95
|
+
peakEntries: number;
|
|
96
|
+
peakBytes: number;
|
|
97
|
+
evictions: number;
|
|
98
|
+
expirations: number;
|
|
99
|
+
pendingEntries: number;
|
|
100
|
+
pendingBytes: number;
|
|
101
|
+
peakPendingEntries: number;
|
|
102
|
+
peakPendingBytes: number;
|
|
103
|
+
admitted: number;
|
|
104
|
+
hits: number;
|
|
105
|
+
rejectedCid: number;
|
|
106
|
+
rejectedCodec: number;
|
|
107
|
+
rejectedSize: number;
|
|
108
|
+
rejectedPending: number;
|
|
109
|
+
rejectedIntegrity: number;
|
|
110
|
+
rejectedLifecycle: number;
|
|
111
|
+
limits: NormalizedEagerBlocksOptions;
|
|
88
112
|
};
|
|
89
113
|
|
|
114
|
+
type EagerAdmissionCounters = Omit<
|
|
115
|
+
EagerBlockCacheTelemetry,
|
|
116
|
+
| "entries"
|
|
117
|
+
| "bytes"
|
|
118
|
+
| "peakEntries"
|
|
119
|
+
| "peakBytes"
|
|
120
|
+
| "evictions"
|
|
121
|
+
| "expirations"
|
|
122
|
+
| "limits"
|
|
123
|
+
>;
|
|
124
|
+
|
|
90
125
|
export class RemoteBlocks implements IBlocks {
|
|
91
126
|
localStore: BlockStore;
|
|
92
127
|
|
|
@@ -117,8 +152,27 @@ export class RemoteBlocks implements IBlocks {
|
|
|
117
152
|
data: BlockMessage,
|
|
118
153
|
context?: BlockMessageContext,
|
|
119
154
|
) => any;
|
|
120
|
-
private _resolvers: Map<
|
|
155
|
+
private _resolvers: Map<
|
|
156
|
+
string,
|
|
157
|
+
(data: Uint8Array, providerHash?: string) => Promise<void>
|
|
158
|
+
>;
|
|
121
159
|
private _blockCache?: EagerBlockCache;
|
|
160
|
+
private _eagerBlocksOptions?: NormalizedEagerBlocksOptions;
|
|
161
|
+
private _eagerValidationQueue?: PQueue;
|
|
162
|
+
private _eagerAdmission: EagerAdmissionCounters = {
|
|
163
|
+
pendingEntries: 0,
|
|
164
|
+
pendingBytes: 0,
|
|
165
|
+
peakPendingEntries: 0,
|
|
166
|
+
peakPendingBytes: 0,
|
|
167
|
+
admitted: 0,
|
|
168
|
+
hits: 0,
|
|
169
|
+
rejectedCid: 0,
|
|
170
|
+
rejectedCodec: 0,
|
|
171
|
+
rejectedSize: 0,
|
|
172
|
+
rejectedPending: 0,
|
|
173
|
+
rejectedIntegrity: 0,
|
|
174
|
+
rejectedLifecycle: 0,
|
|
175
|
+
};
|
|
122
176
|
private _providerCache?: Cache<string[]>;
|
|
123
177
|
private _rustProviderCache?: RustBlockProviderCache;
|
|
124
178
|
private readonly rustExchange?: RustBlockExchange;
|
|
@@ -127,6 +181,7 @@ export class RemoteBlocks implements IBlocks {
|
|
|
127
181
|
private readonly maxRequeryOnReachable: number;
|
|
128
182
|
|
|
129
183
|
private _loadFetchQueue: PQueue;
|
|
184
|
+
private _backgroundLoadFetchQueue: PQueue;
|
|
130
185
|
private _readFromPeersPromises: Map<string, InFlightRead>;
|
|
131
186
|
private _deferredStoredNotificationCids?: Set<string>;
|
|
132
187
|
private _deferredStoredNotificationTimer?: ReturnType<typeof setTimeout>;
|
|
@@ -143,7 +198,7 @@ export class RemoteBlocks implements IBlocks {
|
|
|
143
198
|
localTimeout?: number;
|
|
144
199
|
messageProcessingConcurrency?: number;
|
|
145
200
|
publicKey: PublicSignKey;
|
|
146
|
-
eagerBlocks?:
|
|
201
|
+
eagerBlocks?: EagerBlocksSetting;
|
|
147
202
|
/**
|
|
148
203
|
* Optional provider resolver used when `remote: true` is used without `remote.from`.
|
|
149
204
|
*
|
|
@@ -219,8 +274,19 @@ export class RemoteBlocks implements IBlocks {
|
|
|
219
274
|
const localTimeout = options?.localTimeout || 1000;
|
|
220
275
|
this.publicKeyHash = options.publicKey.hashcode();
|
|
221
276
|
this.rustExchange = options.rust?.exchange;
|
|
277
|
+
const messageProcessingConcurrency =
|
|
278
|
+
options?.messageProcessingConcurrency || 10;
|
|
222
279
|
this._loadFetchQueue = new PQueue({
|
|
223
|
-
concurrency:
|
|
280
|
+
concurrency: messageProcessingConcurrency,
|
|
281
|
+
});
|
|
282
|
+
// A provider handler includes the response publication. When every handler
|
|
283
|
+
// is publishing bulk/background blocks under backpressure, a single priority
|
|
284
|
+
// queue cannot preempt that already-active work. Admit at most N-1 background
|
|
285
|
+
// handlers so foreground reads can use the reserved slot whenever the
|
|
286
|
+
// configured concurrency is at least two. Foreground-only traffic can still
|
|
287
|
+
// use the full configured concurrency.
|
|
288
|
+
this._backgroundLoadFetchQueue = new PQueue({
|
|
289
|
+
concurrency: Math.max(1, messageProcessingConcurrency - 1),
|
|
224
290
|
});
|
|
225
291
|
this.localStore = options?.local;
|
|
226
292
|
const localPutKnownManyColumns = (
|
|
@@ -314,13 +380,23 @@ export class RemoteBlocks implements IBlocks {
|
|
|
314
380
|
this._resolvers = new Map();
|
|
315
381
|
this._readFromPeersPromises = new Map();
|
|
316
382
|
if (options?.eagerBlocks) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
383
|
+
this._eagerBlocksOptions = normalizeEagerBlocksOptions(
|
|
384
|
+
options.eagerBlocks,
|
|
385
|
+
);
|
|
386
|
+
this._eagerValidationQueue = new PQueue({
|
|
387
|
+
concurrency: this._eagerBlocksOptions.validationConcurrency,
|
|
388
|
+
});
|
|
321
389
|
this._blockCache = this.rustExchange
|
|
322
|
-
? this.rustExchange.createEagerCache({
|
|
323
|
-
|
|
390
|
+
? this.rustExchange.createEagerCache({
|
|
391
|
+
maxEntries: this._eagerBlocksOptions.maxEntries,
|
|
392
|
+
maxBytes: this._eagerBlocksOptions.maxBytes,
|
|
393
|
+
ttlMs: this._eagerBlocksOptions.ttlMs,
|
|
394
|
+
})
|
|
395
|
+
: new BoundedEagerBlockCache({
|
|
396
|
+
maxEntries: this._eagerBlocksOptions.maxEntries,
|
|
397
|
+
maxBytes: this._eagerBlocksOptions.maxBytes,
|
|
398
|
+
ttlMs: this._eagerBlocksOptions.ttlMs,
|
|
399
|
+
});
|
|
324
400
|
}
|
|
325
401
|
type ProviderCacheOptions = {
|
|
326
402
|
maxEntries?: number;
|
|
@@ -357,28 +433,40 @@ export class RemoteBlocks implements IBlocks {
|
|
|
357
433
|
) => {
|
|
358
434
|
try {
|
|
359
435
|
if (message instanceof BlockRequest && this.localStore) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
436
|
+
const priority =
|
|
437
|
+
context?.transport?.requestPriority ?? BACKGROUND_MESSAGE_PRIORITY;
|
|
438
|
+
const queueSignal = this.closeController.signal;
|
|
439
|
+
const run = () =>
|
|
440
|
+
this._loadFetchQueue.add(
|
|
441
|
+
() =>
|
|
442
|
+
queueSignal.aborted
|
|
443
|
+
? undefined
|
|
444
|
+
: this.handleFetchRequest(message, localTimeout, context),
|
|
445
|
+
{
|
|
446
|
+
priority,
|
|
447
|
+
},
|
|
448
|
+
);
|
|
449
|
+
const scheduled =
|
|
450
|
+
priority >= FOREGROUND_READ_MESSAGE_PRIORITY
|
|
451
|
+
? run()
|
|
452
|
+
: this._backgroundLoadFetchQueue.add(
|
|
453
|
+
() => (queueSignal.aborted ? undefined : run()),
|
|
454
|
+
{ priority },
|
|
455
|
+
);
|
|
456
|
+
scheduled.catch((e) => {
|
|
457
|
+
if (queueSignal.aborted) return;
|
|
458
|
+
try {
|
|
459
|
+
dontThrowIfDeliveryError(e);
|
|
460
|
+
} catch (error) {
|
|
461
|
+
logger.error("Got error for libp2p block transport: ", error);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
369
464
|
} else if (message instanceof BlockResponse) {
|
|
370
|
-
|
|
371
|
-
if (context?.from) {
|
|
372
|
-
this.rememberProvider(message.cid, context.from);
|
|
373
|
-
}
|
|
374
|
-
let resolver = this._resolvers.get(message.cid);
|
|
465
|
+
const resolver = this._resolvers.get(message.cid);
|
|
375
466
|
if (!resolver) {
|
|
376
|
-
|
|
377
|
-
// wait for the resolve to exist
|
|
378
|
-
this._blockCache!.add(message.cid, message.bytes);
|
|
379
|
-
}
|
|
467
|
+
this.queueEagerBlock(message.cid, message.bytes, context?.from);
|
|
380
468
|
} else {
|
|
381
|
-
await resolver(message.bytes);
|
|
469
|
+
await resolver(message.bytes, context?.from);
|
|
382
470
|
}
|
|
383
471
|
}
|
|
384
472
|
} catch (error) {
|
|
@@ -392,6 +480,170 @@ export class RemoteBlocks implements IBlocks {
|
|
|
392
480
|
return this.localStore.getNativeLogBlockStoreHandle?.();
|
|
393
481
|
}
|
|
394
482
|
|
|
483
|
+
/** Snapshot of bounded eager-response admission and retention state. */
|
|
484
|
+
getEagerBlockCacheTelemetry(): EagerBlockCacheTelemetry | undefined {
|
|
485
|
+
if (!this._blockCache || !this._eagerBlocksOptions) return undefined;
|
|
486
|
+
return {
|
|
487
|
+
...this._blockCache.stats(),
|
|
488
|
+
...this._eagerAdmission,
|
|
489
|
+
limits: { ...this._eagerBlocksOptions },
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Primarily useful for diagnostics and deterministic tests. */
|
|
494
|
+
waitForEagerBlockValidation(): Promise<void> {
|
|
495
|
+
return this._eagerValidationQueue?.onIdle() ?? Promise.resolve();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private queueEagerBlock(
|
|
499
|
+
cidString: string,
|
|
500
|
+
incomingBytes: Uint8Array,
|
|
501
|
+
providerHash?: string,
|
|
502
|
+
): void {
|
|
503
|
+
const limits = this._eagerBlocksOptions;
|
|
504
|
+
const queue = this._eagerValidationQueue;
|
|
505
|
+
const cache = this._blockCache;
|
|
506
|
+
if (!limits || !queue || !cache) return;
|
|
507
|
+
|
|
508
|
+
const generationSignal = this.closeController.signal;
|
|
509
|
+
if (generationSignal.aborted) {
|
|
510
|
+
this._eagerAdmission.rejectedLifecycle += 1;
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (!cidString || cidString.length > MAX_EAGER_BLOCK_CID_LENGTH) {
|
|
514
|
+
this._eagerAdmission.rejectedCid += 1;
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
let cidObject: CID;
|
|
519
|
+
let canonicalCid: string;
|
|
520
|
+
try {
|
|
521
|
+
cidObject = cidifyString(cidString);
|
|
522
|
+
canonicalCid = stringifyCid(cidObject);
|
|
523
|
+
if (
|
|
524
|
+
cidObject.multihash.code !== defaultHasher.code ||
|
|
525
|
+
canonicalCid.length > MAX_EAGER_BLOCK_CID_LENGTH
|
|
526
|
+
) {
|
|
527
|
+
throw new Error("unsupported eager block cid");
|
|
528
|
+
}
|
|
529
|
+
} catch {
|
|
530
|
+
this._eagerAdmission.rejectedCid += 1;
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
// Logical DAG-CBOR decoding can materialize even hash-valid attacker-
|
|
534
|
+
// controlled object graphs and expand a small wire payload by orders of
|
|
535
|
+
// magnitude. Eager admission therefore verifies only raw bytes; all other
|
|
536
|
+
// codecs remain available through the requested-response path.
|
|
537
|
+
if (cidObject.code !== codecMap.raw.code) {
|
|
538
|
+
this._eagerAdmission.rejectedCodec += 1;
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const codec = codecMap.raw;
|
|
542
|
+
|
|
543
|
+
const byteLength = incomingBytes.byteLength;
|
|
544
|
+
if (
|
|
545
|
+
byteLength > limits.maxBlockBytes ||
|
|
546
|
+
byteLength > limits.maxBytes ||
|
|
547
|
+
byteLength > limits.maxPendingBytes
|
|
548
|
+
) {
|
|
549
|
+
this._eagerAdmission.rejectedSize += 1;
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (
|
|
553
|
+
this._eagerAdmission.pendingEntries >= limits.maxPendingEntries ||
|
|
554
|
+
this._eagerAdmission.pendingBytes + byteLength > limits.maxPendingBytes
|
|
555
|
+
) {
|
|
556
|
+
this._eagerAdmission.rejectedPending += 1;
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// The decoder commonly returns a subarray into the complete network frame.
|
|
561
|
+
// Copy exactly the block bytes before queuing so no larger backing buffer or
|
|
562
|
+
// response object remains retained while integrity validation is pending.
|
|
563
|
+
const bytes = new Uint8Array(byteLength);
|
|
564
|
+
bytes.set(incomingBytes);
|
|
565
|
+
this._eagerAdmission.pendingEntries += 1;
|
|
566
|
+
this._eagerAdmission.pendingBytes += byteLength;
|
|
567
|
+
this._eagerAdmission.peakPendingEntries = Math.max(
|
|
568
|
+
this._eagerAdmission.peakPendingEntries,
|
|
569
|
+
this._eagerAdmission.pendingEntries,
|
|
570
|
+
);
|
|
571
|
+
this._eagerAdmission.peakPendingBytes = Math.max(
|
|
572
|
+
this._eagerAdmission.peakPendingBytes,
|
|
573
|
+
this._eagerAdmission.pendingBytes,
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
let released = false;
|
|
577
|
+
const releaseReservation = () => {
|
|
578
|
+
if (released) return;
|
|
579
|
+
released = true;
|
|
580
|
+
this._eagerAdmission.pendingEntries -= 1;
|
|
581
|
+
this._eagerAdmission.pendingBytes -= byteLength;
|
|
582
|
+
};
|
|
583
|
+
const validateAndAdmit = async () => {
|
|
584
|
+
try {
|
|
585
|
+
if (generationSignal.aborted) {
|
|
586
|
+
this._eagerAdmission.rejectedLifecycle += 1;
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
try {
|
|
590
|
+
await this.validateEagerBlock(cidObject, bytes, codec);
|
|
591
|
+
} catch {
|
|
592
|
+
this._eagerAdmission.rejectedIntegrity += 1;
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (generationSignal.aborted) {
|
|
596
|
+
this._eagerAdmission.rejectedLifecycle += 1;
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const resolver =
|
|
600
|
+
this._resolvers.get(canonicalCid) ?? this._resolvers.get(cidString);
|
|
601
|
+
if (resolver) {
|
|
602
|
+
try {
|
|
603
|
+
// A read can install its resolver while eager validation is in
|
|
604
|
+
// progress. Let that resolver enforce its own (possibly custom)
|
|
605
|
+
// hasher contract and learn the provider only if it succeeds.
|
|
606
|
+
await resolver(bytes, providerHash);
|
|
607
|
+
} catch {
|
|
608
|
+
// Match an invalid active response: drop it and leave the read open.
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (!cache.add(canonicalCid, bytes)) {
|
|
613
|
+
this._eagerAdmission.rejectedSize += 1;
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
this._eagerAdmission.admitted += 1;
|
|
617
|
+
if (providerHash) {
|
|
618
|
+
// An unsolicited response is only a provider signal after both its
|
|
619
|
+
// CID and payload have passed the integrity gate.
|
|
620
|
+
this.rememberProvider(canonicalCid, providerHash);
|
|
621
|
+
}
|
|
622
|
+
} finally {
|
|
623
|
+
releaseReservation();
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
try {
|
|
627
|
+
void queue.add(validateAndAdmit).catch(() => {
|
|
628
|
+
releaseReservation();
|
|
629
|
+
if (!generationSignal.aborted) {
|
|
630
|
+
this._eagerAdmission.rejectedLifecycle += 1;
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
} catch {
|
|
634
|
+
releaseReservation();
|
|
635
|
+
this._eagerAdmission.rejectedLifecycle += 1;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private validateEagerBlock(
|
|
640
|
+
cid: CID,
|
|
641
|
+
bytes: Uint8Array,
|
|
642
|
+
codec: (typeof codecCodes)[keyof typeof codecCodes],
|
|
643
|
+
) {
|
|
644
|
+
return verifyBlockBytes(cid, bytes, { codec });
|
|
645
|
+
}
|
|
646
|
+
|
|
395
647
|
private normalizeProviderHints(
|
|
396
648
|
providers: string[] | undefined,
|
|
397
649
|
limit = this.maxProviderHintsPerCid || 8,
|
|
@@ -663,7 +915,9 @@ export class RemoteBlocks implements IBlocks {
|
|
|
663
915
|
const cidObject = cidifyString(cid);
|
|
664
916
|
value = await this._readFromPeers(cid, cidObject, remoteOptions);
|
|
665
917
|
if (remoteOptions?.replicate && value) {
|
|
666
|
-
|
|
918
|
+
// _readFromPeers verifies the response bytes against cid before it
|
|
919
|
+
// resolves, so avoid hashing the full block a second time here.
|
|
920
|
+
await this.putKnown(cid, value);
|
|
667
921
|
}
|
|
668
922
|
}
|
|
669
923
|
}
|
|
@@ -792,6 +1046,7 @@ export class RemoteBlocks implements IBlocks {
|
|
|
792
1046
|
signal: controller.signal,
|
|
793
1047
|
timeout: proxyTimeoutMs,
|
|
794
1048
|
from: providers,
|
|
1049
|
+
priority: context?.transport?.requestPriority,
|
|
795
1050
|
});
|
|
796
1051
|
}
|
|
797
1052
|
} finally {
|
|
@@ -834,26 +1089,38 @@ export class RemoteBlocks implements IBlocks {
|
|
|
834
1089
|
|
|
835
1090
|
const codec = codecCodes[cidObject.code as keyof typeof codecCodes];
|
|
836
1091
|
|
|
837
|
-
const
|
|
838
|
-
const
|
|
1092
|
+
const tryVerify = async (bytes: Uint8Array) => {
|
|
1093
|
+
const verifiedCid = await verifyBlockBytes(cidObject, bytes, {
|
|
839
1094
|
codec,
|
|
840
1095
|
hasher: options?.hasher,
|
|
841
1096
|
});
|
|
842
|
-
|
|
843
|
-
|
|
1097
|
+
// This block-shaped value is internal bookkeeping only. RemoteBlocks moves
|
|
1098
|
+
// opaque bytes and must not invoke the logical codec at this boundary.
|
|
1099
|
+
return {
|
|
1100
|
+
bytes,
|
|
1101
|
+
cid: verifiedCid,
|
|
1102
|
+
value: bytes,
|
|
1103
|
+
} as Block<Uint8Array, any, any, 1>;
|
|
844
1104
|
};
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
return
|
|
853
|
-
} catch (error) {
|
|
854
|
-
// ignore
|
|
1105
|
+
const eagerCacheKey = stringifyCid(cidObject);
|
|
1106
|
+
const consumeCachedValue = (): Uint8Array | undefined => {
|
|
1107
|
+
if (options.hasher && options.hasher !== defaultHasher) {
|
|
1108
|
+
// Eager admission proves the built-in SHA-256 contract only. Keep this
|
|
1109
|
+
// lookup synchronous and leave custom hashers on the requested-response
|
|
1110
|
+
// path; asynchronously revalidating cached entries would reopen a race
|
|
1111
|
+
// before the read resolver is installed.
|
|
1112
|
+
return undefined;
|
|
855
1113
|
}
|
|
856
|
-
|
|
1114
|
+
const cachedValue = this._blockCache?.get(eagerCacheKey);
|
|
1115
|
+
if (cachedValue === undefined) return undefined;
|
|
1116
|
+
this._blockCache!.del(eagerCacheKey);
|
|
1117
|
+
this._eagerAdmission.hits += 1;
|
|
1118
|
+
// Eager entries are inserted only after verifyBlockBytes succeeds. The
|
|
1119
|
+
// one-shot cache hit therefore does not hash the full block a second time.
|
|
1120
|
+
return cachedValue;
|
|
1121
|
+
};
|
|
1122
|
+
let cachedResult = consumeCachedValue();
|
|
1123
|
+
if (cachedResult) return cachedResult;
|
|
857
1124
|
|
|
858
1125
|
const explicitFrom = this.normalizeProviderHints(options.from);
|
|
859
1126
|
let providers =
|
|
@@ -865,6 +1132,11 @@ export class RemoteBlocks implements IBlocks {
|
|
|
865
1132
|
// A resolver may observe abort and still complete normally. Do not create a
|
|
866
1133
|
// timeout-backed read from the candidates it returns after shutdown.
|
|
867
1134
|
throwIfReadWasAborted();
|
|
1135
|
+
// Provider resolution is asynchronous. An eager validation can complete and
|
|
1136
|
+
// cache the response during that await, so consume it before either returning
|
|
1137
|
+
// for lack of providers or installing a resolver that would otherwise wait.
|
|
1138
|
+
cachedResult = consumeCachedValue();
|
|
1139
|
+
if (cachedResult) return cachedResult;
|
|
868
1140
|
const canResolveLater = typeof this.options.resolveProviders === "function";
|
|
869
1141
|
if (providers.length === 0 && !canResolveLater) {
|
|
870
1142
|
// Without an explicit provider set (or a resolver), we intentionally do not
|
|
@@ -878,11 +1150,14 @@ export class RemoteBlocks implements IBlocks {
|
|
|
878
1150
|
|
|
879
1151
|
let inFlight = this._readFromPeersPromises.get(cidString);
|
|
880
1152
|
if (!inFlight) {
|
|
1153
|
+
let requestPriority = options.priority ?? BACKGROUND_MESSAGE_PRIORITY;
|
|
881
1154
|
let publishAdditionalProviders: (providers: string[]) => void = () => {};
|
|
882
1155
|
const promise = new Promise<Block<any, any, any, 1> | undefined>(
|
|
883
1156
|
(resolve, reject) => {
|
|
884
1157
|
let timeoutCallback: ReturnType<typeof setTimeout> | undefined;
|
|
885
|
-
let resolver:
|
|
1158
|
+
let resolver:
|
|
1159
|
+
| ((bytes: Uint8Array, providerHash?: string) => Promise<void>)
|
|
1160
|
+
| undefined;
|
|
886
1161
|
let settled = false;
|
|
887
1162
|
const abortHandler = () => {
|
|
888
1163
|
cleanup();
|
|
@@ -921,9 +1196,14 @@ export class RemoteBlocks implements IBlocks {
|
|
|
921
1196
|
options.timeout || 30 * 1000,
|
|
922
1197
|
);
|
|
923
1198
|
|
|
924
|
-
resolver = async (bytes: Uint8Array) => {
|
|
925
|
-
const value = await
|
|
1199
|
+
resolver = async (bytes: Uint8Array, providerHash?: string) => {
|
|
1200
|
+
const value = await tryVerify(bytes);
|
|
926
1201
|
if (settled) return;
|
|
1202
|
+
if (providerHash) {
|
|
1203
|
+
// A response is only evidence that its sender can provide this CID
|
|
1204
|
+
// after the payload has passed the requested CID's integrity check.
|
|
1205
|
+
this.rememberProvider(cidString, providerHash);
|
|
1206
|
+
}
|
|
927
1207
|
settled = true;
|
|
928
1208
|
cleanup();
|
|
929
1209
|
resolve(value);
|
|
@@ -976,8 +1256,8 @@ export class RemoteBlocks implements IBlocks {
|
|
|
976
1256
|
: this.pickRequestBatch(providers, requeryCount);
|
|
977
1257
|
if (requestProviders.length === 0) return;
|
|
978
1258
|
await this.options.publish(new BlockRequest(cidString), {
|
|
979
|
-
priority:
|
|
980
|
-
responsePriority:
|
|
1259
|
+
priority: requestPriority,
|
|
1260
|
+
responsePriority: requestPriority,
|
|
981
1261
|
expiresAt,
|
|
982
1262
|
mode: new SilentDelivery({
|
|
983
1263
|
to: requestProviders,
|
|
@@ -1017,6 +1297,20 @@ export class RemoteBlocks implements IBlocks {
|
|
|
1017
1297
|
promise,
|
|
1018
1298
|
addProviders: (nextProviders) =>
|
|
1019
1299
|
publishAdditionalProviders(nextProviders),
|
|
1300
|
+
promotePriority: (nextPriority) => {
|
|
1301
|
+
if (!this._resolvers.has(cidString)) return;
|
|
1302
|
+
// A background replication read may win CID coalescing before a
|
|
1303
|
+
// foreground waiter arrives. Reissue only on a strict priority upgrade.
|
|
1304
|
+
const promotedPriority = nextPriority ?? BACKGROUND_MESSAGE_PRIORITY;
|
|
1305
|
+
if (
|
|
1306
|
+
!Number.isFinite(promotedPriority) ||
|
|
1307
|
+
promotedPriority <= requestPriority
|
|
1308
|
+
) {
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
requestPriority = promotedPriority;
|
|
1312
|
+
tryPublishRequest({ force: true }).catch(dontThrowIfDeliveryError);
|
|
1313
|
+
},
|
|
1020
1314
|
};
|
|
1021
1315
|
this._readFromPeersPromises.set(cidString, inFlight);
|
|
1022
1316
|
|
|
@@ -1101,6 +1395,7 @@ export class RemoteBlocks implements IBlocks {
|
|
|
1101
1395
|
}
|
|
1102
1396
|
}
|
|
1103
1397
|
} else {
|
|
1398
|
+
inFlight.promotePriority(options.priority);
|
|
1104
1399
|
if (providers.length > 0) {
|
|
1105
1400
|
inFlight.addProviders(providers);
|
|
1106
1401
|
}
|
|
@@ -1174,8 +1469,12 @@ export class RemoteBlocks implements IBlocks {
|
|
|
1174
1469
|
this._deferredStoredNotificationTimer = undefined;
|
|
1175
1470
|
}
|
|
1176
1471
|
await capture(() => this.flushDeferredStoredNotifications());
|
|
1177
|
-
|
|
1472
|
+
// Queued wrappers observe the aborted generation and drain without starting
|
|
1473
|
+
// new work. Avoid PQueue.clear(): it would strand promises already returned
|
|
1474
|
+
// by the nested background-admission queue.
|
|
1475
|
+
await capture(() => this._backgroundLoadFetchQueue.onIdle());
|
|
1178
1476
|
await capture(() => this._loadFetchQueue.onIdle());
|
|
1477
|
+
await capture(() => this._eagerValidationQueue?.onIdle());
|
|
1179
1478
|
await capture(() => this.localStore?.stop());
|
|
1180
1479
|
this._readFromPeersPromises.clear();
|
|
1181
1480
|
this._resolvers.clear();
|