@peerbit/pubsub 5.2.10 → 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.
Files changed (67) hide show
  1. package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.d.ts +3 -0
  2. package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.d.ts.map +1 -0
  3. package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.js +90 -0
  4. package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.js.map +1 -0
  5. package/dist/benchmark/fanout-tree-parent-upgrade-eval.d.ts +2 -0
  6. package/dist/benchmark/fanout-tree-parent-upgrade-eval.d.ts.map +1 -0
  7. package/dist/benchmark/fanout-tree-parent-upgrade-eval.js +712 -0
  8. package/dist/benchmark/fanout-tree-parent-upgrade-eval.js.map +1 -0
  9. package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.d.ts +2 -0
  10. package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.d.ts.map +1 -0
  11. package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.js +1591 -0
  12. package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.js.map +1 -0
  13. package/dist/benchmark/fanout-tree-parent-upgrade-prepush.d.ts +2 -0
  14. package/dist/benchmark/fanout-tree-parent-upgrade-prepush.d.ts.map +1 -0
  15. package/dist/benchmark/fanout-tree-parent-upgrade-prepush.js +297 -0
  16. package/dist/benchmark/fanout-tree-parent-upgrade-prepush.js.map +1 -0
  17. package/dist/benchmark/fanout-tree-parent-upgrade-preset.d.ts +73 -0
  18. package/dist/benchmark/fanout-tree-parent-upgrade-preset.d.ts.map +1 -0
  19. package/dist/benchmark/fanout-tree-parent-upgrade-preset.js +183 -0
  20. package/dist/benchmark/fanout-tree-parent-upgrade-preset.js.map +1 -0
  21. package/dist/benchmark/fanout-tree-sim-lib.d.ts +101 -0
  22. package/dist/benchmark/fanout-tree-sim-lib.d.ts.map +1 -1
  23. package/dist/benchmark/fanout-tree-sim-lib.js +540 -38
  24. package/dist/benchmark/fanout-tree-sim-lib.js.map +1 -1
  25. package/dist/benchmark/fanout-tree-sim.d.ts +0 -9
  26. package/dist/benchmark/fanout-tree-sim.d.ts.map +1 -1
  27. package/dist/benchmark/fanout-tree-sim.js +369 -28
  28. package/dist/benchmark/fanout-tree-sim.js.map +1 -1
  29. package/dist/benchmark/index.js +20 -0
  30. package/dist/benchmark/index.js.map +1 -1
  31. package/dist/benchmark/pubsub-topic-sim-lib.d.ts +4 -0
  32. package/dist/benchmark/pubsub-topic-sim-lib.d.ts.map +1 -1
  33. package/dist/benchmark/pubsub-topic-sim-lib.js +58 -3
  34. package/dist/benchmark/pubsub-topic-sim-lib.js.map +1 -1
  35. package/dist/benchmark/pubsub-topic-sim.js +18 -0
  36. package/dist/benchmark/pubsub-topic-sim.js.map +1 -1
  37. package/dist/src/debounced-set.d.ts +1 -1
  38. package/dist/src/debounced-set.d.ts.map +1 -1
  39. package/dist/src/debounced-set.js +5 -1
  40. package/dist/src/debounced-set.js.map +1 -1
  41. package/dist/src/fanout-tree-codec.d.ts +330 -0
  42. package/dist/src/fanout-tree-codec.d.ts.map +1 -0
  43. package/dist/src/fanout-tree-codec.js +1209 -0
  44. package/dist/src/fanout-tree-codec.js.map +1 -0
  45. package/dist/src/fanout-tree-parent-upgrade.d.ts +117 -0
  46. package/dist/src/fanout-tree-parent-upgrade.d.ts.map +1 -0
  47. package/dist/src/fanout-tree-parent-upgrade.js +135 -0
  48. package/dist/src/fanout-tree-parent-upgrade.js.map +1 -0
  49. package/dist/src/fanout-tree.d.ts +241 -73
  50. package/dist/src/fanout-tree.d.ts.map +1 -1
  51. package/dist/src/fanout-tree.js +2224 -1050
  52. package/dist/src/fanout-tree.js.map +1 -1
  53. package/dist/src/index.d.ts +57 -0
  54. package/dist/src/index.d.ts.map +1 -1
  55. package/dist/src/index.js +293 -41
  56. package/dist/src/index.js.map +1 -1
  57. package/dist/src/topic-root-control-plane.d.ts +13 -0
  58. package/dist/src/topic-root-control-plane.d.ts.map +1 -1
  59. package/dist/src/topic-root-control-plane.js +45 -0
  60. package/dist/src/topic-root-control-plane.js.map +1 -1
  61. package/package.json +6 -6
  62. package/src/debounced-set.ts +5 -0
  63. package/src/fanout-tree-codec.ts +1580 -0
  64. package/src/fanout-tree-parent-upgrade.ts +353 -0
  65. package/src/fanout-tree.ts +4467 -2204
  66. package/src/index.ts +392 -56
  67. package/src/topic-root-control-plane.ts +48 -0
@@ -6,6 +6,8 @@ import { AcknowledgeDelivery, DataMessage, type ExpiresAtOptions, type IdOptions
6
6
  import type { FanoutTree, FanoutTreeChannelOptions, FanoutTreeJoinOptions } from "./fanout-tree.js";
7
7
  import { TopicRootControlPlane } from "./topic-root-control-plane.js";
8
8
  export * from "./fanout-tree.js";
9
+ export * as fanoutWire from "./fanout-tree-codec.js";
10
+ export * as fanoutParentUpgrade from "./fanout-tree-parent-upgrade.js";
9
11
  export * from "./fanout-channel.js";
10
12
  export * from "./topic-root-control-plane.js";
11
13
  export { toUint8Array } from "./bytes.js";
@@ -100,6 +102,14 @@ export declare class TopicControlPlane extends DirectStream<PubSubEvents> implem
100
102
  counter: number;
101
103
  }>;
102
104
  private pendingSubscriptions;
105
+ /**
106
+ * Per-shard batching of Subscribe{requestSubscribers} responses. When many
107
+ * peers announce subscriptions in a burst (group subscribe), answering each
108
+ * announcer with a routed unicast is quadratic in subscriber count and
109
+ * melts the fanout route-resolution machinery (#983); a short window lets
110
+ * one broadcast answer all concurrent announcers instead.
111
+ */
112
+ private pendingSubscriberResponses;
103
113
  lastSubscriptionMessages: Map<string, Map<string, {
104
114
  session: bigint;
105
115
  timestamp: bigint;
@@ -108,6 +118,7 @@ export declare class TopicControlPlane extends DirectStream<PubSubEvents> implem
108
118
  readonly topicRootControlPlane: TopicRootControlPlane;
109
119
  readonly subscriberCacheMaxEntries: number;
110
120
  readonly fanout: FanoutTree;
121
+ private readonly nativeTopicControl?;
111
122
  private debounceSubscribeAggregator;
112
123
  private debounceUnsubscribeAggregator;
113
124
  private readonly shardCount;
@@ -172,6 +183,17 @@ export declare class TopicControlPlane extends DirectStream<PubSubEvents> implem
172
183
  private touchFanoutChannel;
173
184
  private evictEphemeralFanoutChannels;
174
185
  private getShardTopicForUserTopic;
186
+ /**
187
+ * Serialize a control-plane message (borsh `PubSubMessage`); rust-core
188
+ * mode encodes natively (byte-identical, covered by the parity suite).
189
+ */
190
+ private encodePubSubMessage;
191
+ /**
192
+ * Parse a control-plane payload; rust-core mode decodes natively into the
193
+ * same message classes (decode failures throw either way, so callers keep
194
+ * their fallback behavior).
195
+ */
196
+ private decodePubSubMessage;
175
197
  private resolveTopicRootState;
176
198
  resolveTopicRoot(topic: string): Promise<string | undefined>;
177
199
  private resolveShardRoot;
@@ -212,13 +234,48 @@ export declare class TopicControlPlane extends DirectStream<PubSubEvents> implem
212
234
  } & PriorityOptions & ResponsePriorityOptions & ExpiresAtOptions & IdOptions & WithExtraSigners & {
213
235
  signal?: AbortSignal;
214
236
  }): Promise<Uint8Array | undefined>;
237
+ /**
238
+ * Publish a pre-encoded `PubSubData` payload to explicit recipients.
239
+ *
240
+ * `payload` must be the exact borsh serialization of
241
+ * `new PubSubData({ topics, data, strict: true })` — callers that encode
242
+ * the payload elsewhere (e.g. the shared-log fused send path serializing
243
+ * inside wasm) hand the finished bytes here so the wrapping `PubSubData`
244
+ * copy of the regular `publish` path is skipped. Everything else matches
245
+ * the explicit-recipient branch of {@link publish}: the payload becomes a
246
+ * signed `DataMessage` delivered over DirectStream.
247
+ */
248
+ publishPreEncodedData(payload: Uint8Array, properties: {
249
+ topics: string[];
250
+ }, options: {
251
+ mode: SilentDelivery | AcknowledgeDelivery;
252
+ } & {
253
+ client?: string;
254
+ } & PriorityOptions & ResponsePriorityOptions & ExpiresAtOptions & IdOptions & WithExtraSigners & {
255
+ signal?: AbortSignal;
256
+ }): Promise<Uint8Array | undefined>;
215
257
  onPeerSession(key: PublicSignKey, _session: number): void;
216
258
  onPeerUnreachable(publicKeyHash: string): void;
217
259
  private collectSubscriptionState;
218
260
  private removeSubscriptionsBeforeSession;
219
261
  private removeSubscriptions;
220
262
  private subscriptionStateIsLatest;
263
+ /**
264
+ * `Subscribe` replaces tracked subscription data for new subscribers or
265
+ * strictly newer sessions; equal/older sessions only refresh cache order.
266
+ */
267
+ private subscribeShouldReplace;
221
268
  private subscriptionMessageIsLatest;
269
+ /**
270
+ * Answer Subscribe{requestSubscribers} announces without letting bursts go
271
+ * quadratic (#983). Leading edge: the first announcer is answered
272
+ * immediately with a targeted unicast — a lone joiner sees no added
273
+ * latency. Announcers arriving within the trailing window that this opens
274
+ * are batched and answered with ONE broadcast, instead of S(S-1) routed
275
+ * unicasts whose cold route resolutions flood the shard tree.
276
+ */
277
+ private queueSubscriberResponse;
278
+ private flushSubscriberResponses;
222
279
  private sendFanoutUnicastOrBroadcast;
223
280
  private processDirectPubSubMessage;
224
281
  private processShardPubSubMessage;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
package/dist/src/index.js CHANGED
@@ -11,11 +11,28 @@ import { debouncedAccumulatorSetCounter, } from "./debounced-set.js";
11
11
  import { FanoutChannel } from "./fanout-channel.js";
12
12
  import { TopicRootControlPlane } from "./topic-root-control-plane.js";
13
13
  export * from "./fanout-tree.js";
14
+ // The complete /peerbit/fanout-tree/0.5.0 wire codec and the
15
+ // parent-upgrade decision module (the TS references the native rust-core
16
+ // implementations are parity-tested against).
17
+ export * as fanoutWire from "./fanout-tree-codec.js";
18
+ export * as fanoutParentUpgrade from "./fanout-tree-parent-upgrade.js";
14
19
  export * from "./fanout-channel.js";
15
20
  export * from "./topic-root-control-plane.js";
16
21
  export { toUint8Array } from "./bytes.js";
17
22
  export const logger = loggerFn("peerbit:transport:topic-control-plane");
18
23
  const warn = logger.newScope("warn");
24
+ const utf8Encoder = new TextEncoder();
25
+ /**
26
+ * Offset of the `data` bytes inside a borsh-serialized `PubSubData`
27
+ * (variant byte + topics vec + strict bool + data length prefix).
28
+ */
29
+ const preEncodedPubSubDataOffset = (topics) => {
30
+ let offset = 1 + 4; // variant + topics vec length
31
+ for (const topic of topics) {
32
+ offset += 4 + utf8Encoder.encode(topic).byteLength;
33
+ }
34
+ return offset + 1 + 4; // strict + data length prefix
35
+ };
19
36
  const logError = (e) => {
20
37
  logger.error(e?.message);
21
38
  };
@@ -89,11 +106,26 @@ export class TopicControlPlane extends DirectStream {
89
106
  subscriptions;
90
107
  // Local topics requested via debounced subscribe, not yet applied in `subscriptions`.
91
108
  pendingSubscriptions;
109
+ /**
110
+ * Per-shard batching of Subscribe{requestSubscribers} responses. When many
111
+ * peers announce subscriptions in a burst (group subscribe), answering each
112
+ * announcer with a routed unicast is quadratic in subscriber count and
113
+ * melts the fanout route-resolution machinery (#983); a short window lets
114
+ * one broadcast answer all concurrent announcers instead.
115
+ */
116
+ pendingSubscriberResponses = new Map();
92
117
  lastSubscriptionMessages = new Map();
93
118
  dispatchEventOnSelfPublish;
94
119
  topicRootControlPlane;
95
120
  subscriberCacheMaxEntries;
96
121
  fanout;
122
+ // Native topic-control components of the rust-core DirectStream engine
123
+ // (`@peerbit/network-rust`). When set, the PubSubMessage codec, the topic
124
+ // hashing behind shard mapping and root selection, the root-directory
125
+ // state and the subscribe-state convergence rules run natively; the
126
+ // observable subscription maps and all events stay unchanged. Unset (the
127
+ // default) leaves every code path as-is.
128
+ nativeTopicControl;
97
129
  debounceSubscribeAggregator;
98
130
  debounceUnsubscribeAggregator;
99
131
  shardCount;
@@ -132,6 +164,10 @@ export class TopicControlPlane extends DirectStream {
132
164
  this.peerToTopic = new Map();
133
165
  this.topicRootControlPlane =
134
166
  props?.topicRootControlPlane || new TopicRootControlPlane();
167
+ this.nativeTopicControl = this.rustCore?.topicControl;
168
+ if (this.nativeTopicControl) {
169
+ this.topicRootControlPlane.adoptNativeDirectoryState(this.nativeTopicControl.createRootDirectoryState());
170
+ }
135
171
  this.dispatchEventOnSelfPublish =
136
172
  props?.dispatchEventOnSelfPublish || false;
137
173
  if (!props?.fanout) {
@@ -173,10 +209,10 @@ export class TopicControlPlane extends DirectStream {
173
209
  this.fanoutPublishMaxEphemeralChannels = Math.max(0, Math.floor(requestedMaxEphemeral));
174
210
  const requestedSubscriberCacheMaxEntries = props?.subscriberCacheMaxEntries ?? SUBSCRIBER_CACHE_DEFAULT_MAX_ENTRIES;
175
211
  this.subscriberCacheMaxEntries = Math.min(SUBSCRIBER_CACHE_MAX_ENTRIES_HARD_CAP, Math.max(1, Math.floor(requestedSubscriberCacheMaxEntries)));
176
- this.debounceSubscribeAggregator = debouncedAccumulatorSetCounter((set) => this._subscribe([...set.values()]), props?.subscriptionDebounceDelay ?? 50);
212
+ this.debounceSubscribeAggregator = debouncedAccumulatorSetCounter((set) => this._subscribe([...set.values()]), props?.subscriptionDebounceDelay ?? 50, (error) => logger.error(`Debounced subscribe failed: ${error?.message ?? error}`));
177
213
  // NOTE: Unsubscribe should update local state immediately and batch only the
178
214
  // best-effort network announcements to avoid teardown stalls (program close).
179
- this.debounceUnsubscribeAggregator = debouncedAccumulatorSetCounter((set) => this._announceUnsubscribe([...set.values()]), props?.subscriptionDebounceDelay ?? 50);
215
+ this.debounceUnsubscribeAggregator = debouncedAccumulatorSetCounter((set) => this._announceUnsubscribe([...set.values()]), props?.subscriptionDebounceDelay ?? 50, (error) => logger.error(`Debounced unsubscribe announce failed: ${error?.message ?? error}`));
180
216
  }
181
217
  /**
182
218
  * Configure deterministic topic-root candidates and disable the pubsub "auto"
@@ -268,6 +304,10 @@ export class TopicControlPlane extends DirectStream {
268
304
  this.hostOwnedShardRootsDirty = false;
269
305
  this.subscriptions.clear();
270
306
  this.pendingSubscriptions.clear();
307
+ for (const entry of this.pendingSubscriberResponses.values()) {
308
+ clearTimeout(entry.timer);
309
+ }
310
+ this.pendingSubscriberResponses.clear();
271
311
  this.topics.clear();
272
312
  this.peerToTopic.clear();
273
313
  this.lastSubscriptionMessages.clear();
@@ -362,6 +402,9 @@ export class TopicControlPlane extends DirectStream {
362
402
  this.scheduleAutoTopicRootCandidatesBroadcast();
363
403
  }
364
404
  normalizeAutoTopicRootCandidates(candidates) {
405
+ if (this.nativeTopicControl) {
406
+ return this.nativeTopicControl.normalizeAutoCandidates(candidates, this.publicKeyHash);
407
+ }
365
408
  const unique = new Set();
366
409
  for (const c of candidates) {
367
410
  if (!c)
@@ -430,7 +473,7 @@ export class TopicControlPlane extends DirectStream {
430
473
  if (candidates.length === 0)
431
474
  return;
432
475
  const msg = new TopicRootCandidates({ candidates });
433
- const embedded = await this.createMessage(toUint8Array(msg.bytes()), {
476
+ const embedded = await this.createMessage(this.encodePubSubMessage(msg), {
434
477
  mode: new AnyWhere(),
435
478
  priority: 1,
436
479
  skipRecipientValidation: true,
@@ -527,7 +570,7 @@ export class TopicControlPlane extends DirectStream {
527
570
  topics: userTopics,
528
571
  requestSubscribers: true,
529
572
  });
530
- const embedded = await this.createMessage(toUint8Array(msg.bytes()), {
573
+ const embedded = await this.createMessage(this.encodePubSubMessage(msg), {
531
574
  mode: new AnyWhere(),
532
575
  priority: 1,
533
576
  skipRecipientValidation: true,
@@ -671,11 +714,95 @@ export class TopicControlPlane extends DirectStream {
671
714
  const cached = this.shardTopicCache.get(t);
672
715
  if (cached)
673
716
  return cached;
674
- const index = topicHash32(t) % this.shardCount;
675
- const shardTopic = `${this.shardTopicPrefix}${index}`;
717
+ const shardTopic = this.nativeTopicControl
718
+ ? this.nativeTopicControl.shardTopic(t, this.shardCount, this.shardTopicPrefix)
719
+ : `${this.shardTopicPrefix}${topicHash32(t) % this.shardCount}`;
676
720
  this.shardTopicCache.set(t, shardTopic);
677
721
  return shardTopic;
678
722
  }
723
+ /**
724
+ * Serialize a control-plane message (borsh `PubSubMessage`); rust-core
725
+ * mode encodes natively (byte-identical, covered by the parity suite).
726
+ */
727
+ encodePubSubMessage(message) {
728
+ const native = this.nativeTopicControl;
729
+ if (native) {
730
+ if (message instanceof PubSubData) {
731
+ return native.encodePubSubData(message.topics, message.strict, message.data);
732
+ }
733
+ if (message instanceof Subscribe) {
734
+ return native.encodeSubscribe(message.topics, message.requestSubscribers);
735
+ }
736
+ if (message instanceof Unsubscribe) {
737
+ return native.encodeUnsubscribe(message.topics);
738
+ }
739
+ if (message instanceof GetSubscribers) {
740
+ return native.encodeGetSubscribers(message.topics);
741
+ }
742
+ if (message instanceof TopicRootCandidates) {
743
+ return native.encodeTopicRootCandidates(message.candidates);
744
+ }
745
+ if (message instanceof PeerUnavailable) {
746
+ return native.encodePeerUnavailable(message.publicKeyHash, message.session, message.timestamp, message.topics);
747
+ }
748
+ if (message instanceof TopicRootQuery) {
749
+ return native.encodeTopicRootQuery(message.requestId, message.topic);
750
+ }
751
+ if (message instanceof TopicRootQueryResponse) {
752
+ return native.encodeTopicRootQueryResponse(message.requestId, message.topic, message.root);
753
+ }
754
+ }
755
+ return toUint8Array(message.bytes());
756
+ }
757
+ /**
758
+ * Parse a control-plane payload; rust-core mode decodes natively into the
759
+ * same message classes (decode failures throw either way, so callers keep
760
+ * their fallback behavior).
761
+ */
762
+ decodePubSubMessage(bytes) {
763
+ const native = this.nativeTopicControl;
764
+ if (native) {
765
+ const decoded = native.decodePubSubMessage(bytes);
766
+ switch (decoded.type) {
767
+ case "data":
768
+ return new PubSubData({
769
+ topics: decoded.topics,
770
+ data: decoded.data,
771
+ strict: decoded.strict,
772
+ });
773
+ case "subscribe":
774
+ return new Subscribe({
775
+ topics: decoded.topics,
776
+ requestSubscribers: decoded.requestSubscribers,
777
+ });
778
+ case "unsubscribe":
779
+ return new Unsubscribe({ topics: decoded.topics });
780
+ case "get-subscribers":
781
+ return new GetSubscribers({ topics: decoded.topics });
782
+ case "topic-root-candidates":
783
+ return new TopicRootCandidates({ candidates: decoded.candidates });
784
+ case "peer-unavailable":
785
+ return new PeerUnavailable({
786
+ publicKeyHash: decoded.publicKeyHash,
787
+ session: decoded.session,
788
+ timestamp: decoded.timestamp,
789
+ topics: decoded.topics,
790
+ });
791
+ case "topic-root-query":
792
+ return new TopicRootQuery({
793
+ requestId: decoded.requestId,
794
+ topic: decoded.topic,
795
+ });
796
+ case "topic-root-query-response":
797
+ return new TopicRootQueryResponse({
798
+ requestId: decoded.requestId,
799
+ topic: decoded.topic,
800
+ root: decoded.root,
801
+ });
802
+ }
803
+ }
804
+ return PubSubMessage.from(bytes);
805
+ }
679
806
  async resolveTopicRootState(topic) {
680
807
  const tracked = await this.topicRootControlPlane.resolveTrackedTopicRoot(topic);
681
808
  if (tracked) {
@@ -747,7 +874,7 @@ export class TopicControlPlane extends DirectStream {
747
874
  } while (true);
748
875
  }
749
876
  async sendDirectControlMessage(peer, pubsubMessage) {
750
- const embedded = await this.createMessage(toUint8Array(pubsubMessage.bytes()), {
877
+ const embedded = await this.createMessage(this.encodePubSubMessage(pubsubMessage), {
751
878
  mode: new SilentDelivery({
752
879
  to: [peer.publicKey.hashcode()],
753
880
  redundancy: 1,
@@ -828,6 +955,8 @@ export class TopicControlPlane extends DirectStream {
828
955
  return undefined;
829
956
  }
830
957
  async ensureFanoutChannel(shardTopic, options) {
958
+ if (!this.started)
959
+ throw new NotStartedError();
831
960
  const t = shardTopic.toString();
832
961
  const pin = options?.pin === true;
833
962
  const wantEphemeral = options?.ephemeral === true;
@@ -874,7 +1003,7 @@ export class TopicControlPlane extends DirectStream {
874
1003
  return;
875
1004
  let pubsubMessage;
876
1005
  try {
877
- pubsubMessage = PubSubMessage.from(dm.data);
1006
+ pubsubMessage = this.decodePubSubMessage(dm.data);
878
1007
  }
879
1008
  catch {
880
1009
  return;
@@ -915,6 +1044,9 @@ export class TopicControlPlane extends DirectStream {
915
1044
  this.seenCache.add(msgId, seen ? seen + 1 : 1);
916
1045
  if (seen)
917
1046
  return;
1047
+ // fanout-delivered frames bypass the inbound stream decode, so
1048
+ // the nested envelope is verified natively here when possible
1049
+ this.seedNativeWireVerification(dm, payload);
918
1050
  if ((await this.verifyAndProcess(dm)) === false) {
919
1051
  return;
920
1052
  }
@@ -1106,7 +1238,7 @@ export class TopicControlPlane extends DirectStream {
1106
1238
  topics: userTopics,
1107
1239
  requestSubscribers: true,
1108
1240
  });
1109
- const embedded = await this.createMessage(toUint8Array(msg.bytes()), {
1241
+ const embedded = await this.createMessage(this.encodePubSubMessage(msg), {
1110
1242
  mode: new AnyWhere(),
1111
1243
  priority: 1,
1112
1244
  skipRecipientValidation: true,
@@ -1173,7 +1305,7 @@ export class TopicControlPlane extends DirectStream {
1173
1305
  // Announce first.
1174
1306
  try {
1175
1307
  const msg = new Unsubscribe({ topics: userTopics });
1176
- const embedded = await this.createMessage(toUint8Array(msg.bytes()), {
1308
+ const embedded = await this.createMessage(this.encodePubSubMessage(msg), {
1177
1309
  mode: new AnyWhere(),
1178
1310
  priority: 1,
1179
1311
  skipRecipientValidation: true,
@@ -1238,7 +1370,7 @@ export class TopicControlPlane extends DirectStream {
1238
1370
  timestamp: batch.timestamp,
1239
1371
  topics: batch.topics,
1240
1372
  });
1241
- const embedded = await this.createMessage(toUint8Array(msg.bytes()), {
1373
+ const embedded = await this.createMessage(this.encodePubSubMessage(msg), {
1242
1374
  mode: new AnyWhere(),
1243
1375
  priority: 1,
1244
1376
  skipRecipientValidation: true,
@@ -1265,7 +1397,7 @@ export class TopicControlPlane extends DirectStream {
1265
1397
  timestamp: 0n,
1266
1398
  topics: [],
1267
1399
  });
1268
- const embedded = await this.createMessage(toUint8Array(msg.bytes()), {
1400
+ const embedded = await this.createMessage(this.encodePubSubMessage(msg), {
1269
1401
  mode: new AnyWhere(),
1270
1402
  priority: 1,
1271
1403
  skipRecipientValidation: true,
@@ -1349,7 +1481,7 @@ export class TopicControlPlane extends DirectStream {
1349
1481
  }
1350
1482
  const persistent = (this.shardRefCounts.get(shardTopic) ?? 0) > 0;
1351
1483
  await this.ensureFanoutChannel(shardTopic, { ephemeral: !persistent });
1352
- const embedded = await this.createMessage(toUint8Array(msg.bytes()), {
1484
+ const embedded = await this.createMessage(this.encodePubSubMessage(msg), {
1353
1485
  mode: new AnyWhere(),
1354
1486
  priority: 1,
1355
1487
  skipRecipientValidation: true,
@@ -1384,7 +1516,7 @@ export class TopicControlPlane extends DirectStream {
1384
1516
  const msg = data
1385
1517
  ? new PubSubData({ topics: topicsAll, data, strict: true })
1386
1518
  : undefined;
1387
- const message = await this.createMessage(msg?.bytes(), {
1519
+ const message = await this.createMessage(msg ? this.encodePubSubMessage(msg) : undefined, {
1388
1520
  ...options,
1389
1521
  skipRecipientValidation: this.dispatchEventOnSelfPublish,
1390
1522
  });
@@ -1417,7 +1549,7 @@ export class TopicControlPlane extends DirectStream {
1417
1549
  }
1418
1550
  }
1419
1551
  const msg = new PubSubData({ topics: topicsAll, data, strict: false });
1420
- const embedded = await this.createMessage(toUint8Array(msg.bytes()), {
1552
+ const embedded = await this.createMessage(this.encodePubSubMessage(msg), {
1421
1553
  mode: new AnyWhere(),
1422
1554
  priority: options?.priority,
1423
1555
  responsePriority: options?.responsePriority,
@@ -1467,6 +1599,50 @@ export class TopicControlPlane extends DirectStream {
1467
1599
  }
1468
1600
  return embedded.id;
1469
1601
  }
1602
+ /**
1603
+ * Publish a pre-encoded `PubSubData` payload to explicit recipients.
1604
+ *
1605
+ * `payload` must be the exact borsh serialization of
1606
+ * `new PubSubData({ topics, data, strict: true })` — callers that encode
1607
+ * the payload elsewhere (e.g. the shared-log fused send path serializing
1608
+ * inside wasm) hand the finished bytes here so the wrapping `PubSubData`
1609
+ * copy of the regular `publish` path is skipped. Everything else matches
1610
+ * the explicit-recipient branch of {@link publish}: the payload becomes a
1611
+ * signed `DataMessage` delivered over DirectStream.
1612
+ */
1613
+ async publishPreEncodedData(payload, properties, options) {
1614
+ if (!this.started)
1615
+ throw new NotStartedError();
1616
+ if (!options?.mode || !deliveryModeHasReceiver(options.mode)) {
1617
+ throw new Error("publishPreEncodedData requires a delivery mode with explicit recipients");
1618
+ }
1619
+ const message = await this.createMessage(payload, {
1620
+ ...options,
1621
+ skipRecipientValidation: this.dispatchEventOnSelfPublish,
1622
+ });
1623
+ this.dispatchEvent(new CustomEvent("publish", {
1624
+ detail: new PublishEvent({
1625
+ client: options?.client,
1626
+ data: new PubSubData({
1627
+ topics: properties.topics,
1628
+ data: payload.subarray(preEncodedPubSubDataOffset(properties.topics)),
1629
+ strict: true,
1630
+ }),
1631
+ message,
1632
+ }),
1633
+ }));
1634
+ const silentDelivery = options.mode instanceof SilentDelivery;
1635
+ try {
1636
+ await this.publishMessage(this.publicKey, message, undefined, undefined, options?.signal);
1637
+ }
1638
+ catch (error) {
1639
+ if (error instanceof DeliveryError && silentDelivery !== false) {
1640
+ return message.id;
1641
+ }
1642
+ throw error;
1643
+ }
1644
+ return message.id;
1645
+ }
1470
1646
  onPeerSession(key, _session) {
1471
1647
  if (!Number.isFinite(_session) || _session < 0) {
1472
1648
  return;
@@ -1575,20 +1751,36 @@ export class TopicControlPlane extends DirectStream {
1575
1751
  return changed;
1576
1752
  }
1577
1753
  subscriptionStateIsLatest(subscriberKey, session, timestamp, relevantTopics) {
1578
- for (const topic of relevantTopics) {
1579
- const last = this.lastSubscriptionMessages
1580
- .get(subscriberKey)
1581
- ?.get(topic);
1582
- if (!last) {
1583
- continue;
1754
+ if (this.nativeTopicControl) {
1755
+ const lasts = [];
1756
+ for (const topic of relevantTopics) {
1757
+ const last = this.lastSubscriptionMessages
1758
+ .get(subscriberKey)
1759
+ ?.get(topic);
1760
+ if (last) {
1761
+ lasts.push(last.session, last.timestamp);
1762
+ }
1584
1763
  }
1585
- if (last.session > session) {
1764
+ if (!this.nativeTopicControl.subscriptionIsLatest(BigUint64Array.from(lasts), session, timestamp)) {
1586
1765
  return false;
1587
1766
  }
1588
- if (timestamp !== 0n &&
1589
- last.session === session &&
1590
- last.timestamp > timestamp) {
1591
- return false;
1767
+ }
1768
+ else {
1769
+ for (const topic of relevantTopics) {
1770
+ const last = this.lastSubscriptionMessages
1771
+ .get(subscriberKey)
1772
+ ?.get(topic);
1773
+ if (!last) {
1774
+ continue;
1775
+ }
1776
+ if (last.session > session) {
1777
+ return false;
1778
+ }
1779
+ if (timestamp !== 0n &&
1780
+ last.session === session &&
1781
+ last.timestamp > timestamp) {
1782
+ return false;
1783
+ }
1592
1784
  }
1593
1785
  }
1594
1786
  for (const topic of relevantTopics) {
@@ -1604,10 +1796,80 @@ export class TopicControlPlane extends DirectStream {
1604
1796
  }
1605
1797
  return true;
1606
1798
  }
1799
+ /**
1800
+ * `Subscribe` replaces tracked subscription data for new subscribers or
1801
+ * strictly newer sessions; equal/older sessions only refresh cache order.
1802
+ */
1803
+ subscribeShouldReplace(existing, session) {
1804
+ if (this.nativeTopicControl) {
1805
+ return this.nativeTopicControl.subscribeShouldReplace(existing?.session, session);
1806
+ }
1807
+ return !existing || existing.session < session;
1808
+ }
1607
1809
  subscriptionMessageIsLatest(message, _pubsubMessage, relevantTopics) {
1608
1810
  const subscriber = message.header.signatures.signatures[0].publicKey;
1609
1811
  return this.subscriptionStateIsLatest(subscriber.hashcode(), message.header.session, message.header.timestamp, relevantTopics);
1610
1812
  }
1813
+ /**
1814
+ * Answer Subscribe{requestSubscribers} announces without letting bursts go
1815
+ * quadratic (#983). Leading edge: the first announcer is answered
1816
+ * immediately with a targeted unicast — a lone joiner sees no added
1817
+ * latency. Announcers arriving within the trailing window that this opens
1818
+ * are batched and answered with ONE broadcast, instead of S(S-1) routed
1819
+ * unicasts whose cold route resolutions flood the shard tree.
1820
+ */
1821
+ queueSubscriberResponse(shardTopic, targetHash, topics) {
1822
+ let entry = this.pendingSubscriberResponses.get(shardTopic);
1823
+ if (!entry) {
1824
+ const created = {
1825
+ targets: new Set(),
1826
+ topics: new Set(),
1827
+ timer: setTimeout(() => {
1828
+ this.pendingSubscriberResponses.delete(shardTopic);
1829
+ if (created.targets.size === 0)
1830
+ return;
1831
+ void this.flushSubscriberResponses(shardTopic, created).catch(logErrorIfStarted);
1832
+ }, 150 + Math.floor(Math.random() * 150)),
1833
+ };
1834
+ this.pendingSubscriberResponses.set(shardTopic, created);
1835
+ // Leading edge: respond to the burst opener right away.
1836
+ void this.flushSubscriberResponses(shardTopic, {
1837
+ targets: new Set([targetHash]),
1838
+ topics: new Set(topics),
1839
+ }).catch(logErrorIfStarted);
1840
+ return;
1841
+ }
1842
+ entry.targets.add(targetHash);
1843
+ for (const t of topics)
1844
+ entry.topics.add(t);
1845
+ }
1846
+ async flushSubscriberResponses(shardTopic, entry) {
1847
+ if (!this.started)
1848
+ return;
1849
+ // Re-filter: our subscriptions may have changed within the window.
1850
+ const topics = this.getSubscriptionOverlap([...entry.topics]);
1851
+ if (topics.length === 0)
1852
+ return;
1853
+ const response = new Subscribe({
1854
+ topics,
1855
+ requestSubscribers: false,
1856
+ });
1857
+ const embedded = await this.createMessage(this.encodePubSubMessage(response), {
1858
+ mode: new AnyWhere(),
1859
+ priority: 1,
1860
+ skipRecipientValidation: true,
1861
+ });
1862
+ const payload = toUint8Array(embedded.bytes());
1863
+ const [firstTarget] = entry.targets;
1864
+ if (entry.targets.size === 1 && firstTarget) {
1865
+ await this.sendFanoutUnicastOrBroadcast(shardTopic, firstTarget, payload);
1866
+ return;
1867
+ }
1868
+ const st = this.fanoutChannels.get(shardTopic);
1869
+ if (!st)
1870
+ return;
1871
+ await st.channel.publishMaybe(payload);
1872
+ }
1611
1873
  async sendFanoutUnicastOrBroadcast(shardTopic, targetHash, payload) {
1612
1874
  const st = this.fanoutChannels.get(shardTopic);
1613
1875
  if (!st)
@@ -1667,7 +1929,7 @@ export class TopicControlPlane extends DirectStream {
1667
1929
  continue;
1668
1930
  this.initializePeer(sender);
1669
1931
  const existing = peers.get(senderKey);
1670
- if (!existing || existing.session < message.header.session) {
1932
+ if (this.subscribeShouldReplace(existing, message.header.session)) {
1671
1933
  peers.delete(senderKey);
1672
1934
  peers.set(senderKey, new SubscriptionData({
1673
1935
  session: message.header.session,
@@ -1746,7 +2008,7 @@ export class TopicControlPlane extends DirectStream {
1746
2008
  continue;
1747
2009
  this.initializePeer(sender);
1748
2010
  const existing = peers.get(senderKey);
1749
- if (!existing || existing.session < message.header.session) {
2011
+ if (this.subscribeShouldReplace(existing, message.header.session)) {
1750
2012
  peers.delete(senderKey);
1751
2013
  peers.set(senderKey, new SubscriptionData({
1752
2014
  session: message.header.session,
@@ -1773,17 +2035,7 @@ export class TopicControlPlane extends DirectStream {
1773
2035
  if (pubsubMessage.requestSubscribers) {
1774
2036
  const overlap = this.getSubscriptionOverlap(pubsubMessage.topics);
1775
2037
  if (overlap.length > 0) {
1776
- const response = new Subscribe({
1777
- topics: overlap,
1778
- requestSubscribers: false,
1779
- });
1780
- const embedded = await this.createMessage(toUint8Array(response.bytes()), {
1781
- mode: new AnyWhere(),
1782
- priority: 1,
1783
- skipRecipientValidation: true,
1784
- });
1785
- const payload = toUint8Array(embedded.bytes());
1786
- await this.sendFanoutUnicastOrBroadcast(shardTopic, senderKey, payload);
2038
+ this.queueSubscriberResponse(shardTopic, senderKey, overlap);
1787
2039
  }
1788
2040
  }
1789
2041
  return;
@@ -1873,7 +2125,7 @@ export class TopicControlPlane extends DirectStream {
1873
2125
  topics: overlap,
1874
2126
  requestSubscribers: false,
1875
2127
  });
1876
- const embedded = await this.createMessage(toUint8Array(response.bytes()), {
2128
+ const embedded = await this.createMessage(this.encodePubSubMessage(response), {
1877
2129
  mode: new AnyWhere(),
1878
2130
  priority: 1,
1879
2131
  skipRecipientValidation: true,
@@ -1891,7 +2143,7 @@ export class TopicControlPlane extends DirectStream {
1891
2143
  return false;
1892
2144
  let pubsubMessage;
1893
2145
  try {
1894
- pubsubMessage = PubSubMessage.from(message.data);
2146
+ pubsubMessage = this.decodePubSubMessage(message.data);
1895
2147
  }
1896
2148
  catch {
1897
2149
  return super.onDataMessage(from, stream, message, seenBefore);