@peerbit/pubsub 5.2.11 → 5.3.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.
- package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.d.ts +3 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.d.ts.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.js +90 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.js.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-eval.d.ts +2 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-eval.d.ts.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-eval.js +712 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-eval.js.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.d.ts +2 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.d.ts.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.js +1591 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.js.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-prepush.d.ts +2 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-prepush.d.ts.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-prepush.js +297 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-prepush.js.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-preset.d.ts +73 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-preset.d.ts.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-preset.js +183 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-preset.js.map +1 -0
- package/dist/benchmark/fanout-tree-sim-lib.d.ts +101 -0
- package/dist/benchmark/fanout-tree-sim-lib.d.ts.map +1 -1
- package/dist/benchmark/fanout-tree-sim-lib.js +540 -38
- package/dist/benchmark/fanout-tree-sim-lib.js.map +1 -1
- package/dist/benchmark/fanout-tree-sim.d.ts +0 -9
- package/dist/benchmark/fanout-tree-sim.d.ts.map +1 -1
- package/dist/benchmark/fanout-tree-sim.js +369 -28
- package/dist/benchmark/fanout-tree-sim.js.map +1 -1
- package/dist/benchmark/index.js +20 -0
- package/dist/benchmark/index.js.map +1 -1
- package/dist/benchmark/pubsub-topic-sim-lib.d.ts +4 -0
- package/dist/benchmark/pubsub-topic-sim-lib.d.ts.map +1 -1
- package/dist/benchmark/pubsub-topic-sim-lib.js +58 -3
- package/dist/benchmark/pubsub-topic-sim-lib.js.map +1 -1
- package/dist/benchmark/pubsub-topic-sim.js +18 -0
- package/dist/benchmark/pubsub-topic-sim.js.map +1 -1
- package/dist/src/debounced-set.d.ts +1 -1
- package/dist/src/debounced-set.d.ts.map +1 -1
- package/dist/src/debounced-set.js +5 -1
- package/dist/src/debounced-set.js.map +1 -1
- package/dist/src/fanout-tree-codec.d.ts +330 -0
- package/dist/src/fanout-tree-codec.d.ts.map +1 -0
- package/dist/src/fanout-tree-codec.js +1209 -0
- package/dist/src/fanout-tree-codec.js.map +1 -0
- package/dist/src/fanout-tree-parent-upgrade.d.ts +117 -0
- package/dist/src/fanout-tree-parent-upgrade.d.ts.map +1 -0
- package/dist/src/fanout-tree-parent-upgrade.js +135 -0
- package/dist/src/fanout-tree-parent-upgrade.js.map +1 -0
- package/dist/src/fanout-tree.d.ts +241 -73
- package/dist/src/fanout-tree.d.ts.map +1 -1
- package/dist/src/fanout-tree.js +2224 -1050
- package/dist/src/fanout-tree.js.map +1 -1
- package/dist/src/index.d.ts +57 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +293 -41
- package/dist/src/index.js.map +1 -1
- package/dist/src/topic-root-control-plane.d.ts +13 -0
- package/dist/src/topic-root-control-plane.d.ts.map +1 -1
- package/dist/src/topic-root-control-plane.js +45 -0
- package/dist/src/topic-root-control-plane.js.map +1 -1
- package/package.json +7 -7
- package/src/debounced-set.ts +5 -0
- package/src/fanout-tree-codec.ts +1580 -0
- package/src/fanout-tree-parent-upgrade.ts +353 -0
- package/src/fanout-tree.ts +4467 -2204
- package/src/index.ts +392 -56
- package/src/topic-root-control-plane.ts +48 -0
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type DirectStreamComponents,
|
|
29
29
|
type DirectStreamOptions,
|
|
30
30
|
type PeerStreams,
|
|
31
|
+
type RustTopicControl,
|
|
31
32
|
} from "@peerbit/stream";
|
|
32
33
|
import {
|
|
33
34
|
AcknowledgeAnyWhere,
|
|
@@ -64,12 +65,30 @@ import type {
|
|
|
64
65
|
import { TopicRootControlPlane } from "./topic-root-control-plane.js";
|
|
65
66
|
|
|
66
67
|
export * from "./fanout-tree.js";
|
|
68
|
+
// The complete /peerbit/fanout-tree/0.5.0 wire codec and the
|
|
69
|
+
// parent-upgrade decision module (the TS references the native rust-core
|
|
70
|
+
// implementations are parity-tested against).
|
|
71
|
+
export * as fanoutWire from "./fanout-tree-codec.js";
|
|
72
|
+
export * as fanoutParentUpgrade from "./fanout-tree-parent-upgrade.js";
|
|
67
73
|
export * from "./fanout-channel.js";
|
|
68
74
|
export * from "./topic-root-control-plane.js";
|
|
69
75
|
export { toUint8Array } from "./bytes.js";
|
|
70
76
|
|
|
71
77
|
export const logger = loggerFn("peerbit:transport:topic-control-plane");
|
|
72
78
|
const warn = logger.newScope("warn");
|
|
79
|
+
|
|
80
|
+
const utf8Encoder = new TextEncoder();
|
|
81
|
+
/**
|
|
82
|
+
* Offset of the `data` bytes inside a borsh-serialized `PubSubData`
|
|
83
|
+
* (variant byte + topics vec + strict bool + data length prefix).
|
|
84
|
+
*/
|
|
85
|
+
const preEncodedPubSubDataOffset = (topics: string[]): number => {
|
|
86
|
+
let offset = 1 + 4; // variant + topics vec length
|
|
87
|
+
for (const topic of topics) {
|
|
88
|
+
offset += 4 + utf8Encoder.encode(topic).byteLength;
|
|
89
|
+
}
|
|
90
|
+
return offset + 1 + 4; // strict + data length prefix
|
|
91
|
+
};
|
|
73
92
|
const logError = (e?: { message: string }) => {
|
|
74
93
|
logger.error(e?.message);
|
|
75
94
|
};
|
|
@@ -238,6 +257,21 @@ export class TopicControlPlane
|
|
|
238
257
|
public subscriptions: Map<string, { counter: number }>;
|
|
239
258
|
// Local topics requested via debounced subscribe, not yet applied in `subscriptions`.
|
|
240
259
|
private pendingSubscriptions: Set<string>;
|
|
260
|
+
/**
|
|
261
|
+
* Per-shard batching of Subscribe{requestSubscribers} responses. When many
|
|
262
|
+
* peers announce subscriptions in a burst (group subscribe), answering each
|
|
263
|
+
* announcer with a routed unicast is quadratic in subscriber count and
|
|
264
|
+
* melts the fanout route-resolution machinery (#983); a short window lets
|
|
265
|
+
* one broadcast answer all concurrent announcers instead.
|
|
266
|
+
*/
|
|
267
|
+
private pendingSubscriberResponses: Map<
|
|
268
|
+
string,
|
|
269
|
+
{
|
|
270
|
+
targets: Set<string>;
|
|
271
|
+
topics: Set<string>;
|
|
272
|
+
timer: ReturnType<typeof setTimeout>;
|
|
273
|
+
}
|
|
274
|
+
> = new Map();
|
|
241
275
|
public lastSubscriptionMessages: Map<
|
|
242
276
|
string,
|
|
243
277
|
Map<string, { session: bigint; timestamp: bigint }>
|
|
@@ -246,6 +280,13 @@ export class TopicControlPlane
|
|
|
246
280
|
public readonly topicRootControlPlane: TopicRootControlPlane;
|
|
247
281
|
public readonly subscriberCacheMaxEntries: number;
|
|
248
282
|
public readonly fanout: FanoutTree;
|
|
283
|
+
// Native topic-control components of the rust-core DirectStream engine
|
|
284
|
+
// (`@peerbit/network-rust`). When set, the PubSubMessage codec, the topic
|
|
285
|
+
// hashing behind shard mapping and root selection, the root-directory
|
|
286
|
+
// state and the subscribe-state convergence rules run natively; the
|
|
287
|
+
// observable subscription maps and all events stay unchanged. Unset (the
|
|
288
|
+
// default) leaves every code path as-is.
|
|
289
|
+
private readonly nativeTopicControl?: RustTopicControl;
|
|
249
290
|
|
|
250
291
|
private debounceSubscribeAggregator: DebouncedAccumulatorCounterMap;
|
|
251
292
|
private debounceUnsubscribeAggregator: DebouncedAccumulatorCounterMap;
|
|
@@ -253,10 +294,7 @@ export class TopicControlPlane
|
|
|
253
294
|
private readonly shardCount: number;
|
|
254
295
|
private readonly shardTopicPrefix: string;
|
|
255
296
|
private readonly hostShards: boolean;
|
|
256
|
-
private readonly shardRootCache = new Map<
|
|
257
|
-
string,
|
|
258
|
-
{ root: string; authoritative: boolean }
|
|
259
|
-
>();
|
|
297
|
+
private readonly shardRootCache = new Map<string, { root: string; authoritative: boolean }>();
|
|
260
298
|
private readonly shardTopicCache = new Map<string, string>();
|
|
261
299
|
private readonly shardRefCounts = new Map<string, number>();
|
|
262
300
|
private readonly pinnedShards = new Set<string>();
|
|
@@ -325,6 +363,12 @@ export class TopicControlPlane
|
|
|
325
363
|
|
|
326
364
|
this.topicRootControlPlane =
|
|
327
365
|
props?.topicRootControlPlane || new TopicRootControlPlane();
|
|
366
|
+
this.nativeTopicControl = this.rustCore?.topicControl;
|
|
367
|
+
if (this.nativeTopicControl) {
|
|
368
|
+
this.topicRootControlPlane.adoptNativeDirectoryState(
|
|
369
|
+
this.nativeTopicControl.createRootDirectoryState(),
|
|
370
|
+
);
|
|
371
|
+
}
|
|
328
372
|
this.dispatchEventOnSelfPublish =
|
|
329
373
|
props?.dispatchEventOnSelfPublish || false;
|
|
330
374
|
|
|
@@ -393,12 +437,18 @@ export class TopicControlPlane
|
|
|
393
437
|
this.debounceSubscribeAggregator = debouncedAccumulatorSetCounter(
|
|
394
438
|
(set) => this._subscribe([...set.values()]),
|
|
395
439
|
props?.subscriptionDebounceDelay ?? 50,
|
|
440
|
+
(error) =>
|
|
441
|
+
logger.error(`Debounced subscribe failed: ${error?.message ?? error}`),
|
|
396
442
|
);
|
|
397
443
|
// NOTE: Unsubscribe should update local state immediately and batch only the
|
|
398
444
|
// best-effort network announcements to avoid teardown stalls (program close).
|
|
399
445
|
this.debounceUnsubscribeAggregator = debouncedAccumulatorSetCounter(
|
|
400
446
|
(set) => this._announceUnsubscribe([...set.values()]),
|
|
401
447
|
props?.subscriptionDebounceDelay ?? 50,
|
|
448
|
+
(error) =>
|
|
449
|
+
logger.error(
|
|
450
|
+
`Debounced unsubscribe announce failed: ${error?.message ?? error}`,
|
|
451
|
+
),
|
|
402
452
|
);
|
|
403
453
|
}
|
|
404
454
|
|
|
@@ -497,6 +547,10 @@ export class TopicControlPlane
|
|
|
497
547
|
|
|
498
548
|
this.subscriptions.clear();
|
|
499
549
|
this.pendingSubscriptions.clear();
|
|
550
|
+
for (const entry of this.pendingSubscriberResponses.values()) {
|
|
551
|
+
clearTimeout(entry.timer);
|
|
552
|
+
}
|
|
553
|
+
this.pendingSubscriberResponses.clear();
|
|
500
554
|
this.topics.clear();
|
|
501
555
|
this.peerToTopic.clear();
|
|
502
556
|
this.lastSubscriptionMessages.clear();
|
|
@@ -611,6 +665,12 @@ export class TopicControlPlane
|
|
|
611
665
|
}
|
|
612
666
|
|
|
613
667
|
private normalizeAutoTopicRootCandidates(candidates: string[]): string[] {
|
|
668
|
+
if (this.nativeTopicControl) {
|
|
669
|
+
return this.nativeTopicControl.normalizeAutoCandidates(
|
|
670
|
+
candidates,
|
|
671
|
+
this.publicKeyHash,
|
|
672
|
+
);
|
|
673
|
+
}
|
|
614
674
|
const unique = new Set<string>();
|
|
615
675
|
for (const c of candidates) {
|
|
616
676
|
if (!c) continue;
|
|
@@ -680,7 +740,7 @@ export class TopicControlPlane
|
|
|
680
740
|
if (candidates.length === 0) return;
|
|
681
741
|
|
|
682
742
|
const msg = new TopicRootCandidates({ candidates });
|
|
683
|
-
const embedded = await this.createMessage(
|
|
743
|
+
const embedded = await this.createMessage(this.encodePubSubMessage(msg), {
|
|
684
744
|
mode: new AnyWhere(),
|
|
685
745
|
priority: 1,
|
|
686
746
|
skipRecipientValidation: true,
|
|
@@ -776,7 +836,7 @@ export class TopicControlPlane
|
|
|
776
836
|
topics: userTopics,
|
|
777
837
|
requestSubscribers: true,
|
|
778
838
|
});
|
|
779
|
-
const embedded = await this.createMessage(
|
|
839
|
+
const embedded = await this.createMessage(this.encodePubSubMessage(msg), {
|
|
780
840
|
mode: new AnyWhere(),
|
|
781
841
|
priority: 1,
|
|
782
842
|
skipRecipientValidation: true,
|
|
@@ -925,12 +985,118 @@ export class TopicControlPlane
|
|
|
925
985
|
const t = topic.toString();
|
|
926
986
|
const cached = this.shardTopicCache.get(t);
|
|
927
987
|
if (cached) return cached;
|
|
928
|
-
const
|
|
929
|
-
|
|
988
|
+
const shardTopic = this.nativeTopicControl
|
|
989
|
+
? this.nativeTopicControl.shardTopic(
|
|
990
|
+
t,
|
|
991
|
+
this.shardCount,
|
|
992
|
+
this.shardTopicPrefix,
|
|
993
|
+
)
|
|
994
|
+
: `${this.shardTopicPrefix}${topicHash32(t) % this.shardCount}`;
|
|
930
995
|
this.shardTopicCache.set(t, shardTopic);
|
|
931
996
|
return shardTopic;
|
|
932
997
|
}
|
|
933
998
|
|
|
999
|
+
/**
|
|
1000
|
+
* Serialize a control-plane message (borsh `PubSubMessage`); rust-core
|
|
1001
|
+
* mode encodes natively (byte-identical, covered by the parity suite).
|
|
1002
|
+
*/
|
|
1003
|
+
private encodePubSubMessage(message: PubSubMessage): Uint8Array {
|
|
1004
|
+
const native = this.nativeTopicControl;
|
|
1005
|
+
if (native) {
|
|
1006
|
+
if (message instanceof PubSubData) {
|
|
1007
|
+
return native.encodePubSubData(
|
|
1008
|
+
message.topics,
|
|
1009
|
+
message.strict,
|
|
1010
|
+
message.data,
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
if (message instanceof Subscribe) {
|
|
1014
|
+
return native.encodeSubscribe(
|
|
1015
|
+
message.topics,
|
|
1016
|
+
message.requestSubscribers,
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
if (message instanceof Unsubscribe) {
|
|
1020
|
+
return native.encodeUnsubscribe(message.topics);
|
|
1021
|
+
}
|
|
1022
|
+
if (message instanceof GetSubscribers) {
|
|
1023
|
+
return native.encodeGetSubscribers(message.topics);
|
|
1024
|
+
}
|
|
1025
|
+
if (message instanceof TopicRootCandidates) {
|
|
1026
|
+
return native.encodeTopicRootCandidates(message.candidates);
|
|
1027
|
+
}
|
|
1028
|
+
if (message instanceof PeerUnavailable) {
|
|
1029
|
+
return native.encodePeerUnavailable(
|
|
1030
|
+
message.publicKeyHash,
|
|
1031
|
+
message.session,
|
|
1032
|
+
message.timestamp,
|
|
1033
|
+
message.topics,
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
if (message instanceof TopicRootQuery) {
|
|
1037
|
+
return native.encodeTopicRootQuery(message.requestId, message.topic);
|
|
1038
|
+
}
|
|
1039
|
+
if (message instanceof TopicRootQueryResponse) {
|
|
1040
|
+
return native.encodeTopicRootQueryResponse(
|
|
1041
|
+
message.requestId,
|
|
1042
|
+
message.topic,
|
|
1043
|
+
message.root,
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return toUint8Array(message.bytes());
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Parse a control-plane payload; rust-core mode decodes natively into the
|
|
1052
|
+
* same message classes (decode failures throw either way, so callers keep
|
|
1053
|
+
* their fallback behavior).
|
|
1054
|
+
*/
|
|
1055
|
+
private decodePubSubMessage(bytes: Uint8Array): PubSubMessage {
|
|
1056
|
+
const native = this.nativeTopicControl;
|
|
1057
|
+
if (native) {
|
|
1058
|
+
const decoded = native.decodePubSubMessage(bytes);
|
|
1059
|
+
switch (decoded.type) {
|
|
1060
|
+
case "data":
|
|
1061
|
+
return new PubSubData({
|
|
1062
|
+
topics: decoded.topics,
|
|
1063
|
+
data: decoded.data,
|
|
1064
|
+
strict: decoded.strict,
|
|
1065
|
+
});
|
|
1066
|
+
case "subscribe":
|
|
1067
|
+
return new Subscribe({
|
|
1068
|
+
topics: decoded.topics,
|
|
1069
|
+
requestSubscribers: decoded.requestSubscribers,
|
|
1070
|
+
});
|
|
1071
|
+
case "unsubscribe":
|
|
1072
|
+
return new Unsubscribe({ topics: decoded.topics });
|
|
1073
|
+
case "get-subscribers":
|
|
1074
|
+
return new GetSubscribers({ topics: decoded.topics });
|
|
1075
|
+
case "topic-root-candidates":
|
|
1076
|
+
return new TopicRootCandidates({ candidates: decoded.candidates });
|
|
1077
|
+
case "peer-unavailable":
|
|
1078
|
+
return new PeerUnavailable({
|
|
1079
|
+
publicKeyHash: decoded.publicKeyHash,
|
|
1080
|
+
session: decoded.session,
|
|
1081
|
+
timestamp: decoded.timestamp,
|
|
1082
|
+
topics: decoded.topics,
|
|
1083
|
+
});
|
|
1084
|
+
case "topic-root-query":
|
|
1085
|
+
return new TopicRootQuery({
|
|
1086
|
+
requestId: decoded.requestId,
|
|
1087
|
+
topic: decoded.topic,
|
|
1088
|
+
});
|
|
1089
|
+
case "topic-root-query-response":
|
|
1090
|
+
return new TopicRootQueryResponse({
|
|
1091
|
+
requestId: decoded.requestId,
|
|
1092
|
+
topic: decoded.topic,
|
|
1093
|
+
root: decoded.root,
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return PubSubMessage.from(bytes);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
934
1100
|
private async resolveTopicRootState(
|
|
935
1101
|
topic: string,
|
|
936
1102
|
): Promise<{ root?: string; authoritative: boolean }> {
|
|
@@ -1024,7 +1190,7 @@ export class TopicControlPlane
|
|
|
1024
1190
|
peer: PeerStreams,
|
|
1025
1191
|
pubsubMessage: PubSubMessage,
|
|
1026
1192
|
) {
|
|
1027
|
-
const embedded = await this.createMessage(
|
|
1193
|
+
const embedded = await this.createMessage(this.encodePubSubMessage(pubsubMessage), {
|
|
1028
1194
|
mode: new SilentDelivery({
|
|
1029
1195
|
to: [peer.publicKey.hashcode()],
|
|
1030
1196
|
redundancy: 1,
|
|
@@ -1137,6 +1303,7 @@ export class TopicControlPlane
|
|
|
1137
1303
|
signal?: AbortSignal;
|
|
1138
1304
|
},
|
|
1139
1305
|
): Promise<void> {
|
|
1306
|
+
if (!this.started) throw new NotStartedError();
|
|
1140
1307
|
const t = shardTopic.toString();
|
|
1141
1308
|
const pin = options?.pin === true;
|
|
1142
1309
|
const wantEphemeral = options?.ephemeral === true;
|
|
@@ -1186,7 +1353,7 @@ export class TopicControlPlane
|
|
|
1186
1353
|
|
|
1187
1354
|
let pubsubMessage: PubSubMessage;
|
|
1188
1355
|
try {
|
|
1189
|
-
pubsubMessage =
|
|
1356
|
+
pubsubMessage = this.decodePubSubMessage(dm.data);
|
|
1190
1357
|
} catch {
|
|
1191
1358
|
return;
|
|
1192
1359
|
}
|
|
@@ -1232,6 +1399,9 @@ export class TopicControlPlane
|
|
|
1232
1399
|
this.seenCache.add(msgId, seen ? seen + 1 : 1);
|
|
1233
1400
|
if (seen) return;
|
|
1234
1401
|
|
|
1402
|
+
// fanout-delivered frames bypass the inbound stream decode, so
|
|
1403
|
+
// the nested envelope is verified natively here when possible
|
|
1404
|
+
this.seedNativeWireVerification(dm, payload);
|
|
1235
1405
|
if ((await this.verifyAndProcess(dm)) === false) {
|
|
1236
1406
|
return;
|
|
1237
1407
|
}
|
|
@@ -1419,7 +1589,7 @@ export class TopicControlPlane
|
|
|
1419
1589
|
topics: userTopics,
|
|
1420
1590
|
requestSubscribers: true,
|
|
1421
1591
|
});
|
|
1422
|
-
const embedded = await this.createMessage(
|
|
1592
|
+
const embedded = await this.createMessage(this.encodePubSubMessage(msg), {
|
|
1423
1593
|
mode: new AnyWhere(),
|
|
1424
1594
|
priority: 1,
|
|
1425
1595
|
skipRecipientValidation: true,
|
|
@@ -1498,7 +1668,7 @@ export class TopicControlPlane
|
|
|
1498
1668
|
// Announce first.
|
|
1499
1669
|
try {
|
|
1500
1670
|
const msg = new Unsubscribe({ topics: userTopics });
|
|
1501
|
-
const embedded = await this.createMessage(
|
|
1671
|
+
const embedded = await this.createMessage(this.encodePubSubMessage(msg), {
|
|
1502
1672
|
mode: new AnyWhere(),
|
|
1503
1673
|
priority: 1,
|
|
1504
1674
|
skipRecipientValidation: true,
|
|
@@ -1571,7 +1741,7 @@ export class TopicControlPlane
|
|
|
1571
1741
|
timestamp: batch.timestamp,
|
|
1572
1742
|
topics: batch.topics,
|
|
1573
1743
|
});
|
|
1574
|
-
const embedded = await this.createMessage(
|
|
1744
|
+
const embedded = await this.createMessage(this.encodePubSubMessage(msg), {
|
|
1575
1745
|
mode: new AnyWhere(),
|
|
1576
1746
|
priority: 1,
|
|
1577
1747
|
skipRecipientValidation: true,
|
|
@@ -1601,7 +1771,7 @@ export class TopicControlPlane
|
|
|
1601
1771
|
timestamp: 0n,
|
|
1602
1772
|
topics: [],
|
|
1603
1773
|
});
|
|
1604
|
-
const embedded = await this.createMessage(
|
|
1774
|
+
const embedded = await this.createMessage(this.encodePubSubMessage(msg), {
|
|
1605
1775
|
mode: new AnyWhere(),
|
|
1606
1776
|
priority: 1,
|
|
1607
1777
|
skipRecipientValidation: true,
|
|
@@ -1702,7 +1872,7 @@ export class TopicControlPlane
|
|
|
1702
1872
|
const persistent = (this.shardRefCounts.get(shardTopic) ?? 0) > 0;
|
|
1703
1873
|
await this.ensureFanoutChannel(shardTopic, { ephemeral: !persistent });
|
|
1704
1874
|
|
|
1705
|
-
const embedded = await this.createMessage(
|
|
1875
|
+
const embedded = await this.createMessage(this.encodePubSubMessage(msg), {
|
|
1706
1876
|
mode: new AnyWhere(),
|
|
1707
1877
|
priority: 1,
|
|
1708
1878
|
skipRecipientValidation: true,
|
|
@@ -1754,10 +1924,13 @@ export class TopicControlPlane
|
|
|
1754
1924
|
const msg = data
|
|
1755
1925
|
? new PubSubData({ topics: topicsAll, data, strict: true })
|
|
1756
1926
|
: undefined;
|
|
1757
|
-
const message = await this.createMessage(
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1927
|
+
const message = await this.createMessage(
|
|
1928
|
+
msg ? this.encodePubSubMessage(msg) : undefined,
|
|
1929
|
+
{
|
|
1930
|
+
...options,
|
|
1931
|
+
skipRecipientValidation: this.dispatchEventOnSelfPublish,
|
|
1932
|
+
},
|
|
1933
|
+
);
|
|
1761
1934
|
|
|
1762
1935
|
if (msg) {
|
|
1763
1936
|
this.dispatchEvent(
|
|
@@ -1800,7 +1973,7 @@ export class TopicControlPlane
|
|
|
1800
1973
|
}
|
|
1801
1974
|
|
|
1802
1975
|
const msg = new PubSubData({ topics: topicsAll, data, strict: false });
|
|
1803
|
-
const embedded = await this.createMessage(
|
|
1976
|
+
const embedded = await this.createMessage(this.encodePubSubMessage(msg), {
|
|
1804
1977
|
mode: new AnyWhere(),
|
|
1805
1978
|
priority: options?.priority,
|
|
1806
1979
|
responsePriority: options?.responsePriority,
|
|
@@ -1862,6 +2035,76 @@ export class TopicControlPlane
|
|
|
1862
2035
|
return embedded.id;
|
|
1863
2036
|
}
|
|
1864
2037
|
|
|
2038
|
+
/**
|
|
2039
|
+
* Publish a pre-encoded `PubSubData` payload to explicit recipients.
|
|
2040
|
+
*
|
|
2041
|
+
* `payload` must be the exact borsh serialization of
|
|
2042
|
+
* `new PubSubData({ topics, data, strict: true })` — callers that encode
|
|
2043
|
+
* the payload elsewhere (e.g. the shared-log fused send path serializing
|
|
2044
|
+
* inside wasm) hand the finished bytes here so the wrapping `PubSubData`
|
|
2045
|
+
* copy of the regular `publish` path is skipped. Everything else matches
|
|
2046
|
+
* the explicit-recipient branch of {@link publish}: the payload becomes a
|
|
2047
|
+
* signed `DataMessage` delivered over DirectStream.
|
|
2048
|
+
*/
|
|
2049
|
+
async publishPreEncodedData(
|
|
2050
|
+
payload: Uint8Array,
|
|
2051
|
+
properties: {
|
|
2052
|
+
topics: string[];
|
|
2053
|
+
},
|
|
2054
|
+
options: {
|
|
2055
|
+
mode: SilentDelivery | AcknowledgeDelivery;
|
|
2056
|
+
} & { client?: string } & PriorityOptions &
|
|
2057
|
+
ResponsePriorityOptions &
|
|
2058
|
+
ExpiresAtOptions &
|
|
2059
|
+
IdOptions &
|
|
2060
|
+
WithExtraSigners & { signal?: AbortSignal },
|
|
2061
|
+
): Promise<Uint8Array | undefined> {
|
|
2062
|
+
if (!this.started) throw new NotStartedError();
|
|
2063
|
+
if (!options?.mode || !deliveryModeHasReceiver(options.mode)) {
|
|
2064
|
+
throw new Error(
|
|
2065
|
+
"publishPreEncodedData requires a delivery mode with explicit recipients",
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
const message = await this.createMessage(payload, {
|
|
2070
|
+
...options,
|
|
2071
|
+
skipRecipientValidation: this.dispatchEventOnSelfPublish,
|
|
2072
|
+
});
|
|
2073
|
+
|
|
2074
|
+
this.dispatchEvent(
|
|
2075
|
+
new CustomEvent("publish", {
|
|
2076
|
+
detail: new PublishEvent({
|
|
2077
|
+
client: options?.client,
|
|
2078
|
+
data: new PubSubData({
|
|
2079
|
+
topics: properties.topics,
|
|
2080
|
+
data: payload.subarray(
|
|
2081
|
+
preEncodedPubSubDataOffset(properties.topics),
|
|
2082
|
+
),
|
|
2083
|
+
strict: true,
|
|
2084
|
+
}),
|
|
2085
|
+
message,
|
|
2086
|
+
}),
|
|
2087
|
+
}),
|
|
2088
|
+
);
|
|
2089
|
+
|
|
2090
|
+
const silentDelivery = options.mode instanceof SilentDelivery;
|
|
2091
|
+
try {
|
|
2092
|
+
await this.publishMessage(
|
|
2093
|
+
this.publicKey,
|
|
2094
|
+
message,
|
|
2095
|
+
undefined,
|
|
2096
|
+
undefined,
|
|
2097
|
+
options?.signal,
|
|
2098
|
+
);
|
|
2099
|
+
} catch (error) {
|
|
2100
|
+
if (error instanceof DeliveryError && silentDelivery !== false) {
|
|
2101
|
+
return message.id;
|
|
2102
|
+
}
|
|
2103
|
+
throw error;
|
|
2104
|
+
}
|
|
2105
|
+
return message.id;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
1865
2108
|
public onPeerSession(key: PublicSignKey, _session: number): void {
|
|
1866
2109
|
if (!Number.isFinite(_session) || _session < 0) {
|
|
1867
2110
|
return;
|
|
@@ -1999,23 +2242,44 @@ export class TopicControlPlane
|
|
|
1999
2242
|
timestamp: bigint,
|
|
2000
2243
|
relevantTopics: string[],
|
|
2001
2244
|
) {
|
|
2002
|
-
|
|
2003
|
-
const
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2245
|
+
if (this.nativeTopicControl) {
|
|
2246
|
+
const lasts: bigint[] = [];
|
|
2247
|
+
for (const topic of relevantTopics) {
|
|
2248
|
+
const last = this.lastSubscriptionMessages
|
|
2249
|
+
.get(subscriberKey)
|
|
2250
|
+
?.get(topic);
|
|
2251
|
+
if (last) {
|
|
2252
|
+
lasts.push(last.session, last.timestamp);
|
|
2253
|
+
}
|
|
2011
2254
|
}
|
|
2012
2255
|
if (
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2256
|
+
!this.nativeTopicControl.subscriptionIsLatest(
|
|
2257
|
+
BigUint64Array.from(lasts),
|
|
2258
|
+
session,
|
|
2259
|
+
timestamp,
|
|
2260
|
+
)
|
|
2016
2261
|
) {
|
|
2017
2262
|
return false;
|
|
2018
2263
|
}
|
|
2264
|
+
} else {
|
|
2265
|
+
for (const topic of relevantTopics) {
|
|
2266
|
+
const last = this.lastSubscriptionMessages
|
|
2267
|
+
.get(subscriberKey)
|
|
2268
|
+
?.get(topic);
|
|
2269
|
+
if (!last) {
|
|
2270
|
+
continue;
|
|
2271
|
+
}
|
|
2272
|
+
if (last.session > session) {
|
|
2273
|
+
return false;
|
|
2274
|
+
}
|
|
2275
|
+
if (
|
|
2276
|
+
timestamp !== 0n &&
|
|
2277
|
+
last.session === session &&
|
|
2278
|
+
last.timestamp > timestamp
|
|
2279
|
+
) {
|
|
2280
|
+
return false;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2019
2283
|
}
|
|
2020
2284
|
|
|
2021
2285
|
for (const topic of relevantTopics) {
|
|
@@ -2032,6 +2296,23 @@ export class TopicControlPlane
|
|
|
2032
2296
|
return true;
|
|
2033
2297
|
}
|
|
2034
2298
|
|
|
2299
|
+
/**
|
|
2300
|
+
* `Subscribe` replaces tracked subscription data for new subscribers or
|
|
2301
|
+
* strictly newer sessions; equal/older sessions only refresh cache order.
|
|
2302
|
+
*/
|
|
2303
|
+
private subscribeShouldReplace(
|
|
2304
|
+
existing: SubscriptionData | undefined,
|
|
2305
|
+
session: bigint,
|
|
2306
|
+
): boolean {
|
|
2307
|
+
if (this.nativeTopicControl) {
|
|
2308
|
+
return this.nativeTopicControl.subscribeShouldReplace(
|
|
2309
|
+
existing?.session,
|
|
2310
|
+
session,
|
|
2311
|
+
);
|
|
2312
|
+
}
|
|
2313
|
+
return !existing || existing.session < session;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2035
2316
|
private subscriptionMessageIsLatest(
|
|
2036
2317
|
message: DataMessage,
|
|
2037
2318
|
_pubsubMessage: Subscribe | Unsubscribe,
|
|
@@ -2046,6 +2327,78 @@ export class TopicControlPlane
|
|
|
2046
2327
|
);
|
|
2047
2328
|
}
|
|
2048
2329
|
|
|
2330
|
+
/**
|
|
2331
|
+
* Answer Subscribe{requestSubscribers} announces without letting bursts go
|
|
2332
|
+
* quadratic (#983). Leading edge: the first announcer is answered
|
|
2333
|
+
* immediately with a targeted unicast — a lone joiner sees no added
|
|
2334
|
+
* latency. Announcers arriving within the trailing window that this opens
|
|
2335
|
+
* are batched and answered with ONE broadcast, instead of S(S-1) routed
|
|
2336
|
+
* unicasts whose cold route resolutions flood the shard tree.
|
|
2337
|
+
*/
|
|
2338
|
+
private queueSubscriberResponse(
|
|
2339
|
+
shardTopic: string,
|
|
2340
|
+
targetHash: string,
|
|
2341
|
+
topics: string[],
|
|
2342
|
+
) {
|
|
2343
|
+
let entry = this.pendingSubscriberResponses.get(shardTopic);
|
|
2344
|
+
if (!entry) {
|
|
2345
|
+
const created = {
|
|
2346
|
+
targets: new Set<string>(),
|
|
2347
|
+
topics: new Set<string>(),
|
|
2348
|
+
timer: setTimeout(
|
|
2349
|
+
() => {
|
|
2350
|
+
this.pendingSubscriberResponses.delete(shardTopic);
|
|
2351
|
+
if (created.targets.size === 0) return;
|
|
2352
|
+
void this.flushSubscriberResponses(shardTopic, created).catch(
|
|
2353
|
+
logErrorIfStarted,
|
|
2354
|
+
);
|
|
2355
|
+
},
|
|
2356
|
+
150 + Math.floor(Math.random() * 150),
|
|
2357
|
+
),
|
|
2358
|
+
};
|
|
2359
|
+
this.pendingSubscriberResponses.set(shardTopic, created);
|
|
2360
|
+
// Leading edge: respond to the burst opener right away.
|
|
2361
|
+
void this.flushSubscriberResponses(shardTopic, {
|
|
2362
|
+
targets: new Set([targetHash]),
|
|
2363
|
+
topics: new Set(topics),
|
|
2364
|
+
}).catch(logErrorIfStarted);
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
entry.targets.add(targetHash);
|
|
2368
|
+
for (const t of topics) entry.topics.add(t);
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
private async flushSubscriberResponses(
|
|
2372
|
+
shardTopic: string,
|
|
2373
|
+
entry: { targets: Set<string>; topics: Set<string> },
|
|
2374
|
+
) {
|
|
2375
|
+
if (!this.started) return;
|
|
2376
|
+
// Re-filter: our subscriptions may have changed within the window.
|
|
2377
|
+
const topics = this.getSubscriptionOverlap([...entry.topics]);
|
|
2378
|
+
if (topics.length === 0) return;
|
|
2379
|
+
const response = new Subscribe({
|
|
2380
|
+
topics,
|
|
2381
|
+
requestSubscribers: false,
|
|
2382
|
+
});
|
|
2383
|
+
const embedded = await this.createMessage(
|
|
2384
|
+
this.encodePubSubMessage(response),
|
|
2385
|
+
{
|
|
2386
|
+
mode: new AnyWhere(),
|
|
2387
|
+
priority: 1,
|
|
2388
|
+
skipRecipientValidation: true,
|
|
2389
|
+
} as any,
|
|
2390
|
+
);
|
|
2391
|
+
const payload = toUint8Array(embedded.bytes());
|
|
2392
|
+
const [firstTarget] = entry.targets;
|
|
2393
|
+
if (entry.targets.size === 1 && firstTarget) {
|
|
2394
|
+
await this.sendFanoutUnicastOrBroadcast(shardTopic, firstTarget, payload);
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
const st = this.fanoutChannels.get(shardTopic);
|
|
2398
|
+
if (!st) return;
|
|
2399
|
+
await st.channel.publishMaybe(payload);
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2049
2402
|
private async sendFanoutUnicastOrBroadcast(
|
|
2050
2403
|
shardTopic: string,
|
|
2051
2404
|
targetHash: string,
|
|
@@ -2127,7 +2480,7 @@ export class TopicControlPlane
|
|
|
2127
2480
|
this.initializePeer(sender);
|
|
2128
2481
|
|
|
2129
2482
|
const existing = peers.get(senderKey);
|
|
2130
|
-
if (
|
|
2483
|
+
if (this.subscribeShouldReplace(existing, message.header.session)) {
|
|
2131
2484
|
peers.delete(senderKey);
|
|
2132
2485
|
peers.set(
|
|
2133
2486
|
senderKey,
|
|
@@ -2140,7 +2493,7 @@ export class TopicControlPlane
|
|
|
2140
2493
|
changed.push(topic);
|
|
2141
2494
|
} else {
|
|
2142
2495
|
peers.delete(senderKey);
|
|
2143
|
-
peers.set(senderKey, existing);
|
|
2496
|
+
peers.set(senderKey, existing!);
|
|
2144
2497
|
}
|
|
2145
2498
|
|
|
2146
2499
|
if (!existing) {
|
|
@@ -2236,7 +2589,7 @@ export class TopicControlPlane
|
|
|
2236
2589
|
this.initializePeer(sender);
|
|
2237
2590
|
|
|
2238
2591
|
const existing = peers.get(senderKey);
|
|
2239
|
-
if (
|
|
2592
|
+
if (this.subscribeShouldReplace(existing, message.header.session)) {
|
|
2240
2593
|
peers.delete(senderKey);
|
|
2241
2594
|
peers.set(
|
|
2242
2595
|
senderKey,
|
|
@@ -2249,7 +2602,7 @@ export class TopicControlPlane
|
|
|
2249
2602
|
changed.push(topic);
|
|
2250
2603
|
} else {
|
|
2251
2604
|
peers.delete(senderKey);
|
|
2252
|
-
peers.set(senderKey, existing);
|
|
2605
|
+
peers.set(senderKey, existing!);
|
|
2253
2606
|
}
|
|
2254
2607
|
|
|
2255
2608
|
if (!existing) {
|
|
@@ -2270,24 +2623,7 @@ export class TopicControlPlane
|
|
|
2270
2623
|
if (pubsubMessage.requestSubscribers) {
|
|
2271
2624
|
const overlap = this.getSubscriptionOverlap(pubsubMessage.topics);
|
|
2272
2625
|
if (overlap.length > 0) {
|
|
2273
|
-
|
|
2274
|
-
topics: overlap,
|
|
2275
|
-
requestSubscribers: false,
|
|
2276
|
-
});
|
|
2277
|
-
const embedded = await this.createMessage(
|
|
2278
|
-
toUint8Array(response.bytes()),
|
|
2279
|
-
{
|
|
2280
|
-
mode: new AnyWhere(),
|
|
2281
|
-
priority: 1,
|
|
2282
|
-
skipRecipientValidation: true,
|
|
2283
|
-
} as any,
|
|
2284
|
-
);
|
|
2285
|
-
const payload = toUint8Array(embedded.bytes());
|
|
2286
|
-
await this.sendFanoutUnicastOrBroadcast(
|
|
2287
|
-
shardTopic,
|
|
2288
|
-
senderKey,
|
|
2289
|
-
payload,
|
|
2290
|
-
);
|
|
2626
|
+
this.queueSubscriberResponse(shardTopic, senderKey, overlap);
|
|
2291
2627
|
}
|
|
2292
2628
|
}
|
|
2293
2629
|
return;
|
|
@@ -2406,7 +2742,7 @@ export class TopicControlPlane
|
|
|
2406
2742
|
requestSubscribers: false,
|
|
2407
2743
|
});
|
|
2408
2744
|
const embedded = await this.createMessage(
|
|
2409
|
-
|
|
2745
|
+
this.encodePubSubMessage(response),
|
|
2410
2746
|
{
|
|
2411
2747
|
mode: new AnyWhere(),
|
|
2412
2748
|
priority: 1,
|
|
@@ -2432,7 +2768,7 @@ export class TopicControlPlane
|
|
|
2432
2768
|
|
|
2433
2769
|
let pubsubMessage: PubSubMessage;
|
|
2434
2770
|
try {
|
|
2435
|
-
pubsubMessage =
|
|
2771
|
+
pubsubMessage = this.decodePubSubMessage(message.data);
|
|
2436
2772
|
} catch {
|
|
2437
2773
|
return super.onDataMessage(from, stream, message, seenBefore);
|
|
2438
2774
|
}
|