@peerbit/pubsub 5.4.7 → 5.4.9
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/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +66 -28
- package/dist/src/index.js.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +108 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peerbit/pubsub",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.9",
|
|
4
4
|
"description": "Direct streaming for libp2p",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/src/index.d.ts",
|
|
@@ -85,13 +85,13 @@
|
|
|
85
85
|
"p-defer": "^4.0.0",
|
|
86
86
|
"uint8arraylist": "^3.0.2",
|
|
87
87
|
"uint8arrays": "^5.1.0",
|
|
88
|
-
"@peerbit/crypto": "3.1.6",
|
|
89
88
|
"@peerbit/any-store-interface": "1.1.4",
|
|
90
89
|
"@peerbit/logger": "2.0.2",
|
|
91
|
-
"@peerbit/pubsub-interface": "5.2.2",
|
|
92
90
|
"@peerbit/stream": "5.2.3",
|
|
93
|
-
"@peerbit/
|
|
94
|
-
"@peerbit/
|
|
91
|
+
"@peerbit/pubsub-interface": "5.2.2",
|
|
92
|
+
"@peerbit/crypto": "3.1.6",
|
|
93
|
+
"@peerbit/time": "3.0.1",
|
|
94
|
+
"@peerbit/stream-interface": "6.0.16"
|
|
95
95
|
},
|
|
96
96
|
"scripts": {
|
|
97
97
|
"bench": "TS_NODE_PROJECT=../../../tsconfig.json node --loader ts-node/esm ./benchmark/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -417,8 +417,9 @@ export class TopicControlPlane
|
|
|
417
417
|
// default) leaves every code path as-is.
|
|
418
418
|
private readonly nativeTopicControl?: RustTopicControl;
|
|
419
419
|
|
|
420
|
-
private debounceSubscribeAggregator
|
|
421
|
-
private debounceUnsubscribeAggregator
|
|
420
|
+
private debounceSubscribeAggregator!: DebouncedAccumulatorCounterMap;
|
|
421
|
+
private debounceUnsubscribeAggregator!: DebouncedAccumulatorCounterMap;
|
|
422
|
+
private readonly subscriptionDebounceDelay: number;
|
|
422
423
|
|
|
423
424
|
private readonly shardCount: number;
|
|
424
425
|
private readonly shardTopicPrefix: string;
|
|
@@ -637,17 +638,23 @@ export class TopicControlPlane
|
|
|
637
638
|
Math.max(1, Math.floor(requestedSubscriberCacheMaxEntries)),
|
|
638
639
|
);
|
|
639
640
|
|
|
641
|
+
this.subscriptionDebounceDelay = props?.subscriptionDebounceDelay ?? 50;
|
|
642
|
+
this.createSubscriptionAggregators();
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
private createSubscriptionAggregators(): void {
|
|
646
|
+
const lifecycleRevision = this.topicControlPlaneLifecycleRevision;
|
|
640
647
|
this.debounceSubscribeAggregator = debouncedAccumulatorSetCounter(
|
|
641
|
-
(set) => this._subscribe([...set.values()]),
|
|
642
|
-
|
|
648
|
+
(set) => this._subscribe([...set.values()], lifecycleRevision),
|
|
649
|
+
this.subscriptionDebounceDelay,
|
|
643
650
|
(error) =>
|
|
644
651
|
logger.error(`Debounced subscribe failed: ${error?.message ?? error}`),
|
|
645
652
|
);
|
|
646
653
|
// NOTE: Unsubscribe should update local state immediately and batch only the
|
|
647
654
|
// best-effort network announcements to avoid teardown stalls (program close).
|
|
648
655
|
this.debounceUnsubscribeAggregator = debouncedAccumulatorSetCounter(
|
|
649
|
-
(set) => this._announceUnsubscribe([...set.values()]),
|
|
650
|
-
|
|
656
|
+
(set) => this._announceUnsubscribe([...set.values()], lifecycleRevision),
|
|
657
|
+
this.subscriptionDebounceDelay,
|
|
651
658
|
(error) =>
|
|
652
659
|
logger.error(
|
|
653
660
|
`Debounced unsubscribe announce failed: ${error?.message ?? error}`,
|
|
@@ -713,6 +720,9 @@ export class TopicControlPlane
|
|
|
713
720
|
// candidates. In particular, a repeated start must not restore unsigned
|
|
714
721
|
// self while a signed-protocol peer is connected.
|
|
715
722
|
if (this.started) return;
|
|
723
|
+
// close() is terminal. Only a completed stop/start gets a fresh pair;
|
|
724
|
+
// repeated start() must preserve the active pair's queued reference counts.
|
|
725
|
+
if (this.topicControlPlaneStopping) this.createSubscriptionAggregators();
|
|
716
726
|
this.topicControlPlaneStopping = false;
|
|
717
727
|
if (
|
|
718
728
|
this.autoTopicRootCandidates &&
|
|
@@ -765,6 +775,8 @@ export class TopicControlPlane
|
|
|
765
775
|
this.clearAutoTopicRootCandidateUpdateSchedule();
|
|
766
776
|
this.clearSignedTopicRootCandidateState();
|
|
767
777
|
this.topicControlPlaneLifecycleRevision += 1;
|
|
778
|
+
this.debounceSubscribeAggregator.close();
|
|
779
|
+
this.debounceUnsubscribeAggregator.close();
|
|
768
780
|
this.reconcileShardOverlaysDirty = false;
|
|
769
781
|
for (const opening of this.ensureFanoutChannelInFlight.values()) {
|
|
770
782
|
opening.abortController.abort(
|
|
@@ -779,7 +791,7 @@ export class TopicControlPlane
|
|
|
779
791
|
);
|
|
780
792
|
}
|
|
781
793
|
for (const st of this.fanoutChannels.values()) {
|
|
782
|
-
|
|
794
|
+
this.clearFanoutIdleClose(st);
|
|
783
795
|
try {
|
|
784
796
|
st.channel.removeEventListener("data", st.onData as any);
|
|
785
797
|
} catch {
|
|
@@ -823,8 +835,6 @@ export class TopicControlPlane
|
|
|
823
835
|
}
|
|
824
836
|
this.pendingTopicRootQueries.clear();
|
|
825
837
|
|
|
826
|
-
this.debounceSubscribeAggregator.close();
|
|
827
|
-
this.debounceUnsubscribeAggregator.close();
|
|
828
838
|
await super.stop();
|
|
829
839
|
this.reconcileShardOverlaysDirty = false;
|
|
830
840
|
}
|
|
@@ -1765,6 +1775,7 @@ export class TopicControlPlane
|
|
|
1765
1775
|
|
|
1766
1776
|
private async reconcileShardOverlays() {
|
|
1767
1777
|
if (!this.started || this.topicControlPlaneStopping) return;
|
|
1778
|
+
const lifecycleRevision = this.topicControlPlaneLifecycleRevision;
|
|
1768
1779
|
|
|
1769
1780
|
const byShard = new Map<string, string[]>();
|
|
1770
1781
|
for (const topic of this.subscriptions.keys()) {
|
|
@@ -1794,7 +1805,7 @@ export class TopicControlPlane
|
|
|
1794
1805
|
const st = this.fanoutChannels.get(shardTopic);
|
|
1795
1806
|
if (!st) return;
|
|
1796
1807
|
await st.channel.publish(toUint8Array(embedded.bytes()));
|
|
1797
|
-
this.touchFanoutChannel(shardTopic);
|
|
1808
|
+
this.touchFanoutChannel(shardTopic, st.channel, lifecycleRevision);
|
|
1798
1809
|
}),
|
|
1799
1810
|
);
|
|
1800
1811
|
}
|
|
@@ -1883,22 +1894,42 @@ export class TopicControlPlane
|
|
|
1883
1894
|
|
|
1884
1895
|
private scheduleFanoutIdleClose(topic: string) {
|
|
1885
1896
|
const st = this.fanoutChannels.get(topic);
|
|
1886
|
-
|
|
1897
|
+
const lifecycleRevision = this.topicControlPlaneLifecycleRevision;
|
|
1898
|
+
if (
|
|
1899
|
+
!st ||
|
|
1900
|
+
!st.ephemeral ||
|
|
1901
|
+
!this.isTopicControlPlaneActive(lifecycleRevision)
|
|
1902
|
+
)
|
|
1903
|
+
return;
|
|
1887
1904
|
this.clearFanoutIdleClose(st);
|
|
1888
1905
|
if (this.fanoutPublishIdleCloseMs <= 0) return;
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1906
|
+
const timer = setTimeout(() => {
|
|
1907
|
+
// Cancellation can race a queued callback. It must still own this exact
|
|
1908
|
+
// timer, channel state and lifecycle before acting on the topic again.
|
|
1909
|
+
if (
|
|
1910
|
+
!this.isTopicControlPlaneActive(lifecycleRevision) ||
|
|
1911
|
+
this.fanoutChannels.get(topic) !== st ||
|
|
1912
|
+
st.idleCloseTimeout !== timer ||
|
|
1913
|
+
!st.ephemeral
|
|
1914
|
+
)
|
|
1915
|
+
return;
|
|
1916
|
+
st.idleCloseTimeout = undefined;
|
|
1917
|
+
const idleMs = Date.now() - st.lastUsedAt;
|
|
1893
1918
|
if (idleMs >= this.fanoutPublishIdleCloseMs) {
|
|
1894
1919
|
void this.closeFanoutChannel(topic);
|
|
1895
1920
|
return;
|
|
1896
1921
|
}
|
|
1897
1922
|
this.scheduleFanoutIdleClose(topic);
|
|
1898
1923
|
}, this.fanoutPublishIdleCloseMs);
|
|
1924
|
+
st.idleCloseTimeout = timer;
|
|
1899
1925
|
}
|
|
1900
1926
|
|
|
1901
|
-
private touchFanoutChannel(
|
|
1927
|
+
private touchFanoutChannel(
|
|
1928
|
+
topic: string,
|
|
1929
|
+
channel: FanoutChannel,
|
|
1930
|
+
lifecycleRevision: number,
|
|
1931
|
+
) {
|
|
1932
|
+
if (!this.isFanoutChannelCurrent(topic, channel, lifecycleRevision)) return;
|
|
1902
1933
|
const st = this.fanoutChannels.get(topic);
|
|
1903
1934
|
if (!st) return;
|
|
1904
1935
|
st.lastUsedAt = Date.now();
|
|
@@ -3043,7 +3074,7 @@ export class TopicControlPlane
|
|
|
3043
3074
|
join
|
|
3044
3075
|
.then(() => {
|
|
3045
3076
|
const st = this.fanoutChannels.get(t);
|
|
3046
|
-
if (st === channelState && st.ephemeral) {
|
|
3077
|
+
if (st === channelState && st.ephemeral && isCurrent()) {
|
|
3047
3078
|
this.scheduleFanoutIdleClose(t);
|
|
3048
3079
|
}
|
|
3049
3080
|
})
|
|
@@ -3187,8 +3218,12 @@ export class TopicControlPlane
|
|
|
3187
3218
|
return this.debounceSubscribeAggregator.add({ key: topic });
|
|
3188
3219
|
}
|
|
3189
3220
|
|
|
3190
|
-
private async _subscribe(
|
|
3221
|
+
private async _subscribe(
|
|
3222
|
+
topics: { key: string; counter: number }[],
|
|
3223
|
+
lifecycleRevision = this.topicControlPlaneLifecycleRevision,
|
|
3224
|
+
) {
|
|
3191
3225
|
if (!this.started) throw new NotStartedError();
|
|
3226
|
+
if (!this.isTopicControlPlaneActive(lifecycleRevision)) return;
|
|
3192
3227
|
if (topics.length === 0) return;
|
|
3193
3228
|
|
|
3194
3229
|
const byShard = new Map<string, string[]>();
|
|
@@ -3214,11 +3249,15 @@ export class TopicControlPlane
|
|
|
3214
3249
|
}
|
|
3215
3250
|
|
|
3216
3251
|
await Promise.all(joins);
|
|
3252
|
+
if (!this.isTopicControlPlaneActive(lifecycleRevision)) return;
|
|
3217
3253
|
|
|
3218
3254
|
// Announce subscriptions per shard overlay.
|
|
3219
3255
|
await Promise.all(
|
|
3220
3256
|
[...byShard.entries()].map(async ([shardTopic, userTopics]) => {
|
|
3221
3257
|
if (userTopics.length === 0) return;
|
|
3258
|
+
const st = this.fanoutChannels.get(shardTopic);
|
|
3259
|
+
if (!st)
|
|
3260
|
+
throw new Error(`Fanout channel missing for shard: ${shardTopic}`);
|
|
3222
3261
|
const msg = new Subscribe({
|
|
3223
3262
|
topics: userTopics,
|
|
3224
3263
|
requestSubscribers: true,
|
|
@@ -3231,11 +3270,19 @@ export class TopicControlPlane
|
|
|
3231
3270
|
skipRecipientValidation: true,
|
|
3232
3271
|
} as any,
|
|
3233
3272
|
);
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3273
|
+
if (
|
|
3274
|
+
!this.isFanoutChannelCurrent(
|
|
3275
|
+
shardTopic,
|
|
3276
|
+
st.channel,
|
|
3277
|
+
lifecycleRevision,
|
|
3278
|
+
)
|
|
3279
|
+
)
|
|
3280
|
+
return;
|
|
3237
3281
|
await st.channel.publish(toUint8Array(embedded.bytes()));
|
|
3238
|
-
|
|
3282
|
+
if (
|
|
3283
|
+
this.isFanoutChannelCurrent(shardTopic, st.channel, lifecycleRevision)
|
|
3284
|
+
)
|
|
3285
|
+
this.touchFanoutChannel(shardTopic, st.channel, lifecycleRevision);
|
|
3239
3286
|
}),
|
|
3240
3287
|
);
|
|
3241
3288
|
}
|
|
@@ -3291,8 +3338,10 @@ export class TopicControlPlane
|
|
|
3291
3338
|
|
|
3292
3339
|
private async _announceUnsubscribe(
|
|
3293
3340
|
topics: { key: string; counter: number }[],
|
|
3341
|
+
lifecycleRevision = this.topicControlPlaneLifecycleRevision,
|
|
3294
3342
|
) {
|
|
3295
3343
|
if (!this.started) throw new NotStartedError();
|
|
3344
|
+
if (!this.isTopicControlPlaneActive(lifecycleRevision)) return;
|
|
3296
3345
|
|
|
3297
3346
|
const byShard = new Map<string, string[]>();
|
|
3298
3347
|
for (const { key: topic } of topics) {
|
|
@@ -3305,6 +3354,8 @@ export class TopicControlPlane
|
|
|
3305
3354
|
await Promise.all(
|
|
3306
3355
|
[...byShard.entries()].map(async ([shardTopic, userTopics]) => {
|
|
3307
3356
|
if (userTopics.length === 0) return;
|
|
3357
|
+
const st = this.fanoutChannels.get(shardTopic);
|
|
3358
|
+
if (!st) return;
|
|
3308
3359
|
|
|
3309
3360
|
// Announce first.
|
|
3310
3361
|
try {
|
|
@@ -3317,20 +3368,43 @@ export class TopicControlPlane
|
|
|
3317
3368
|
skipRecipientValidation: true,
|
|
3318
3369
|
} as any,
|
|
3319
3370
|
);
|
|
3320
|
-
|
|
3321
|
-
|
|
3371
|
+
if (
|
|
3372
|
+
this.isFanoutChannelCurrent(
|
|
3373
|
+
shardTopic,
|
|
3374
|
+
st.channel,
|
|
3375
|
+
lifecycleRevision,
|
|
3376
|
+
)
|
|
3377
|
+
) {
|
|
3322
3378
|
// Best-effort: do not let a stuck proxy publish stall teardown.
|
|
3323
3379
|
void st.channel
|
|
3324
3380
|
.publish(toUint8Array(embedded.bytes()))
|
|
3325
3381
|
.catch(() => {});
|
|
3326
|
-
|
|
3382
|
+
if (
|
|
3383
|
+
this.isFanoutChannelCurrent(
|
|
3384
|
+
shardTopic,
|
|
3385
|
+
st.channel,
|
|
3386
|
+
lifecycleRevision,
|
|
3387
|
+
)
|
|
3388
|
+
)
|
|
3389
|
+
this.touchFanoutChannel(
|
|
3390
|
+
shardTopic,
|
|
3391
|
+
st.channel,
|
|
3392
|
+
lifecycleRevision,
|
|
3393
|
+
);
|
|
3327
3394
|
}
|
|
3328
3395
|
} catch {
|
|
3329
3396
|
// best-effort
|
|
3330
3397
|
}
|
|
3331
3398
|
|
|
3332
3399
|
// Close shard overlay if no local topics remain.
|
|
3333
|
-
if (
|
|
3400
|
+
if (
|
|
3401
|
+
this.isFanoutChannelCurrent(
|
|
3402
|
+
shardTopic,
|
|
3403
|
+
st.channel,
|
|
3404
|
+
lifecycleRevision,
|
|
3405
|
+
) &&
|
|
3406
|
+
(this.shardRefCounts.get(shardTopic) ?? 0) <= 0
|
|
3407
|
+
) {
|
|
3334
3408
|
try {
|
|
3335
3409
|
// Shutdown should be bounded and not depend on network I/O.
|
|
3336
3410
|
await this.closeFanoutChannel(shardTopic);
|
|
@@ -3347,6 +3421,7 @@ export class TopicControlPlane
|
|
|
3347
3421
|
batches: { session: bigint; timestamp: bigint; topics: string[] }[],
|
|
3348
3422
|
) {
|
|
3349
3423
|
if (!this.started) throw new NotStartedError();
|
|
3424
|
+
const lifecycleRevision = this.topicControlPlaneLifecycleRevision;
|
|
3350
3425
|
|
|
3351
3426
|
const byShard = new Map<
|
|
3352
3427
|
string,
|
|
@@ -3397,7 +3472,7 @@ export class TopicControlPlane
|
|
|
3397
3472
|
const st = this.fanoutChannels.get(shardTopic);
|
|
3398
3473
|
if (st) {
|
|
3399
3474
|
void st.channel.publishMaybe(toUint8Array(embedded.bytes()));
|
|
3400
|
-
this.touchFanoutChannel(shardTopic);
|
|
3475
|
+
this.touchFanoutChannel(shardTopic, st.channel, lifecycleRevision);
|
|
3401
3476
|
}
|
|
3402
3477
|
} catch {
|
|
3403
3478
|
// best-effort
|
|
@@ -3411,6 +3486,7 @@ export class TopicControlPlane
|
|
|
3411
3486
|
shardTopic: string,
|
|
3412
3487
|
) {
|
|
3413
3488
|
if (!this.started) throw new NotStartedError();
|
|
3489
|
+
const lifecycleRevision = this.topicControlPlaneLifecycleRevision;
|
|
3414
3490
|
try {
|
|
3415
3491
|
const msg = new PeerUnavailable({
|
|
3416
3492
|
publicKeyHash,
|
|
@@ -3427,7 +3503,7 @@ export class TopicControlPlane
|
|
|
3427
3503
|
const st = this.fanoutChannels.get(shardTopic);
|
|
3428
3504
|
if (st) {
|
|
3429
3505
|
void st.channel.publishMaybe(toUint8Array(embedded.bytes()));
|
|
3430
|
-
this.touchFanoutChannel(shardTopic);
|
|
3506
|
+
this.touchFanoutChannel(shardTopic, st.channel, lifecycleRevision);
|
|
3431
3507
|
}
|
|
3432
3508
|
} catch {
|
|
3433
3509
|
// best-effort
|
|
@@ -3494,6 +3570,7 @@ export class TopicControlPlane
|
|
|
3494
3570
|
to?: PublicSignKey,
|
|
3495
3571
|
): Promise<void> {
|
|
3496
3572
|
if (!this.started) throw new NotStartedError();
|
|
3573
|
+
const lifecycleRevision = this.topicControlPlaneLifecycleRevision;
|
|
3497
3574
|
if (topic == null) throw new Error("ERR_NOT_VALID_TOPIC");
|
|
3498
3575
|
if (topic.length === 0) return;
|
|
3499
3576
|
|
|
@@ -3545,7 +3622,7 @@ export class TopicControlPlane
|
|
|
3545
3622
|
} else {
|
|
3546
3623
|
await st.channel.publish(payload);
|
|
3547
3624
|
}
|
|
3548
|
-
this.touchFanoutChannel(shardTopic);
|
|
3625
|
+
this.touchFanoutChannel(shardTopic, st.channel, lifecycleRevision);
|
|
3549
3626
|
}),
|
|
3550
3627
|
);
|
|
3551
3628
|
}
|
|
@@ -3563,6 +3640,7 @@ export class TopicControlPlane
|
|
|
3563
3640
|
WithExtraSigners & { signal?: AbortSignal },
|
|
3564
3641
|
): Promise<Uint8Array | undefined> {
|
|
3565
3642
|
if (!this.started) throw new NotStartedError();
|
|
3643
|
+
const lifecycleRevision = this.topicControlPlaneLifecycleRevision;
|
|
3566
3644
|
|
|
3567
3645
|
const topicsAll =
|
|
3568
3646
|
(options as { topics: string[] }).topics?.map((x) => x.toString()) || [];
|
|
@@ -3670,7 +3748,7 @@ export class TopicControlPlane
|
|
|
3670
3748
|
throw new Error(`Fanout channel missing for shard: ${shardTopic}`);
|
|
3671
3749
|
}
|
|
3672
3750
|
await withAbort(st.channel.publish(payload), options?.signal);
|
|
3673
|
-
this.touchFanoutChannel(shardTopic);
|
|
3751
|
+
this.touchFanoutChannel(shardTopic, st.channel, lifecycleRevision);
|
|
3674
3752
|
}),
|
|
3675
3753
|
);
|
|
3676
3754
|
|