@peerbit/pubsub 5.3.5 → 5.3.7
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 +9 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +300 -81
- package/dist/src/index.js.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +385 -92
package/src/index.ts
CHANGED
|
@@ -47,6 +47,10 @@ import {
|
|
|
47
47
|
type WithExtraSigners,
|
|
48
48
|
deliveryModeHasReceiver,
|
|
49
49
|
getMsgId,
|
|
50
|
+
isAcknowledgeAnyWhereDeliveryMode,
|
|
51
|
+
isAcknowledgeDeliveryMode,
|
|
52
|
+
isAnyWhereDeliveryMode,
|
|
53
|
+
isSilentDeliveryMode,
|
|
50
54
|
} from "@peerbit/stream-interface";
|
|
51
55
|
import { AbortError, TimeoutError, delay } from "@peerbit/time";
|
|
52
56
|
import { Uint8ArrayList } from "uint8arraylist";
|
|
@@ -230,6 +234,14 @@ export type TopicControlPlaneOptions = DirectStreamOptions & {
|
|
|
230
234
|
subscriberCacheMaxEntries?: number;
|
|
231
235
|
};
|
|
232
236
|
|
|
237
|
+
type EnsureFanoutChannelOptions = {
|
|
238
|
+
ephemeral?: boolean;
|
|
239
|
+
pin?: boolean;
|
|
240
|
+
root?: string;
|
|
241
|
+
rootCandidateGeneration?: string;
|
|
242
|
+
signal?: AbortSignal;
|
|
243
|
+
};
|
|
244
|
+
|
|
233
245
|
export type TopicControlPlaneComponents = DirectStreamComponents;
|
|
234
246
|
|
|
235
247
|
export type PeerId = Libp2pPeerId | PublicSignKey;
|
|
@@ -333,6 +345,9 @@ export class TopicControlPlane
|
|
|
333
345
|
private autoTopicRootCandidates = false;
|
|
334
346
|
private autoTopicRootCandidateSet?: Set<string>;
|
|
335
347
|
private reconcileShardOverlaysInFlight?: Promise<void>;
|
|
348
|
+
private reconcileShardOverlaysDirty = false;
|
|
349
|
+
private topicControlPlaneStopping = false;
|
|
350
|
+
private topicControlPlaneLifecycleRevision = 0;
|
|
336
351
|
private hostOwnedShardRootsInFlight?: Promise<void>;
|
|
337
352
|
private hostOwnedShardRootsDirty = false;
|
|
338
353
|
private autoCandidatesBroadcastTimers: Array<ReturnType<typeof setTimeout>> =
|
|
@@ -356,9 +371,18 @@ export class TopicControlPlane
|
|
|
356
371
|
idleCloseTimeout?: ReturnType<typeof setTimeout>;
|
|
357
372
|
}
|
|
358
373
|
>();
|
|
374
|
+
private ensureFanoutChannelInFlight = new Map<
|
|
375
|
+
string,
|
|
376
|
+
{
|
|
377
|
+
candidateGeneration: string;
|
|
378
|
+
lifecycleRevision: number;
|
|
379
|
+
opening: Promise<void>;
|
|
380
|
+
}
|
|
381
|
+
>();
|
|
359
382
|
private pendingTopicRootQueries = new Map<
|
|
360
383
|
number,
|
|
361
384
|
{
|
|
385
|
+
expectedPeerHash: string;
|
|
362
386
|
topic: string;
|
|
363
387
|
resolve: (root: string | undefined) => void;
|
|
364
388
|
timer: ReturnType<typeof setTimeout>;
|
|
@@ -524,6 +548,7 @@ export class TopicControlPlane
|
|
|
524
548
|
}
|
|
525
549
|
|
|
526
550
|
public override async start() {
|
|
551
|
+
this.topicControlPlaneStopping = false;
|
|
527
552
|
await this.fanout.start();
|
|
528
553
|
this._onFanoutPeerUnreachable =
|
|
529
554
|
this._onFanoutPeerUnreachable ||
|
|
@@ -540,9 +565,16 @@ export class TopicControlPlane
|
|
|
540
565
|
if (this.hostOwnedShardRootsDirty) {
|
|
541
566
|
this.scheduleHostOwnedShardRoots();
|
|
542
567
|
}
|
|
568
|
+
if (this.reconcileShardOverlaysDirty) {
|
|
569
|
+
this.scheduleReconcileShardOverlays();
|
|
570
|
+
}
|
|
543
571
|
}
|
|
544
572
|
|
|
545
573
|
public override async stop() {
|
|
574
|
+
this.topicControlPlaneStopping = true;
|
|
575
|
+
this.topicControlPlaneLifecycleRevision += 1;
|
|
576
|
+
this.reconcileShardOverlaysDirty = false;
|
|
577
|
+
this.ensureFanoutChannelInFlight.clear();
|
|
546
578
|
if (this._onFanoutPeerUnreachable) {
|
|
547
579
|
this.fanout.removeEventListener(
|
|
548
580
|
"fanout:peer-unreachable",
|
|
@@ -603,7 +635,8 @@ export class TopicControlPlane
|
|
|
603
635
|
|
|
604
636
|
this.debounceSubscribeAggregator.close();
|
|
605
637
|
this.debounceUnsubscribeAggregator.close();
|
|
606
|
-
|
|
638
|
+
await super.stop();
|
|
639
|
+
this.reconcileShardOverlaysDirty = false;
|
|
607
640
|
}
|
|
608
641
|
|
|
609
642
|
public override async onPeerConnected(
|
|
@@ -840,21 +873,45 @@ export class TopicControlPlane
|
|
|
840
873
|
}
|
|
841
874
|
|
|
842
875
|
private scheduleReconcileShardOverlays() {
|
|
876
|
+
this.reconcileShardOverlaysDirty = true;
|
|
877
|
+
if (!this.started || this.stopping || this.topicControlPlaneStopping)
|
|
878
|
+
return;
|
|
843
879
|
if (this.reconcileShardOverlaysInFlight) return;
|
|
844
|
-
this.reconcileShardOverlaysInFlight =
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
880
|
+
this.reconcileShardOverlaysInFlight = (async () => {
|
|
881
|
+
for (;;) {
|
|
882
|
+
if (!this.started || this.stopping || this.topicControlPlaneStopping)
|
|
883
|
+
return;
|
|
884
|
+
if (!this.reconcileShardOverlaysDirty) return;
|
|
885
|
+
this.reconcileShardOverlaysDirty = false;
|
|
886
|
+
try {
|
|
887
|
+
await this.reconcileShardOverlays();
|
|
888
|
+
} catch {
|
|
889
|
+
// Fanout streams or roots might not be ready yet. Preserve both
|
|
890
|
+
// failures and root changes that arrived during the active pass.
|
|
891
|
+
if (!this.started || this.stopping || this.topicControlPlaneStopping)
|
|
892
|
+
return;
|
|
893
|
+
this.reconcileShardOverlaysDirty = true;
|
|
894
|
+
await new Promise<void>((resolve) => {
|
|
895
|
+
const timer = setTimeout(resolve, 250);
|
|
896
|
+
timer.unref?.();
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
})().finally(() => {
|
|
901
|
+
this.reconcileShardOverlaysInFlight = undefined;
|
|
902
|
+
if (
|
|
903
|
+
this.reconcileShardOverlaysDirty &&
|
|
904
|
+
this.started &&
|
|
905
|
+
!this.stopping &&
|
|
906
|
+
!this.topicControlPlaneStopping
|
|
907
|
+
) {
|
|
908
|
+
this.scheduleReconcileShardOverlays();
|
|
909
|
+
}
|
|
910
|
+
});
|
|
854
911
|
}
|
|
855
912
|
|
|
856
913
|
private async reconcileShardOverlays() {
|
|
857
|
-
if (!this.started) return;
|
|
914
|
+
if (!this.started || this.topicControlPlaneStopping) return;
|
|
858
915
|
|
|
859
916
|
const byShard = new Map<string, string[]>();
|
|
860
917
|
for (const topic of this.subscriptions.keys()) {
|
|
@@ -1134,6 +1191,41 @@ export class TopicControlPlane
|
|
|
1134
1191
|
return PubSubMessage.from(bytes);
|
|
1135
1192
|
}
|
|
1136
1193
|
|
|
1194
|
+
private getTopicRootCandidateGeneration(): string {
|
|
1195
|
+
return JSON.stringify([
|
|
1196
|
+
this.autoTopicRootCandidates,
|
|
1197
|
+
this.topicRootControlPlane.getTopicRootCandidates(),
|
|
1198
|
+
]);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
private assertTopicControlPlaneActive(lifecycleRevision: number): void {
|
|
1202
|
+
if (
|
|
1203
|
+
lifecycleRevision !== this.topicControlPlaneLifecycleRevision ||
|
|
1204
|
+
!this.started ||
|
|
1205
|
+
this.stopping ||
|
|
1206
|
+
this.topicControlPlaneStopping
|
|
1207
|
+
) {
|
|
1208
|
+
throw new NotStartedError();
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
private normalizePeerTopicRootState(
|
|
1213
|
+
topic: string,
|
|
1214
|
+
root: string,
|
|
1215
|
+
): { root: string; authoritative: boolean } {
|
|
1216
|
+
const deterministic =
|
|
1217
|
+
this.topicRootControlPlane.resolveDeterministicTopicRoot(topic);
|
|
1218
|
+
if (
|
|
1219
|
+
this.autoTopicRootCandidates &&
|
|
1220
|
+
topic.startsWith(this.shardTopicPrefix) &&
|
|
1221
|
+
deterministic &&
|
|
1222
|
+
root !== deterministic
|
|
1223
|
+
) {
|
|
1224
|
+
return { root: deterministic, authoritative: false };
|
|
1225
|
+
}
|
|
1226
|
+
return { root, authoritative: true };
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1137
1229
|
private async resolveTopicRootState(
|
|
1138
1230
|
topic: string,
|
|
1139
1231
|
): Promise<{ root?: string; authoritative: boolean }> {
|
|
@@ -1144,7 +1236,12 @@ export class TopicControlPlane
|
|
|
1144
1236
|
|
|
1145
1237
|
const resolvedThroughPeers = await this.resolveTopicRootThroughPeers(topic);
|
|
1146
1238
|
if (resolvedThroughPeers) {
|
|
1147
|
-
|
|
1239
|
+
// Unconfigured peer-query replies cannot override the locally
|
|
1240
|
+
// deterministic root for internal shards in auto mode. Roots, resolvers,
|
|
1241
|
+
// and trackers explicitly configured on this control plane are resolved
|
|
1242
|
+
// above and remain authoritative. A leaf relying on a queried gateway for
|
|
1243
|
+
// shard roots must first disable auto mode with setTopicRootCandidates([]).
|
|
1244
|
+
return this.normalizePeerTopicRootState(topic, resolvedThroughPeers);
|
|
1148
1245
|
}
|
|
1149
1246
|
|
|
1150
1247
|
const deterministic = this.topicRootControlPlane.resolveDeterministicTopicRoot(topic);
|
|
@@ -1157,46 +1254,65 @@ export class TopicControlPlane
|
|
|
1157
1254
|
await delay(150 * (attempt < 4 ? 1 : 2));
|
|
1158
1255
|
const retried = await this.resolveTopicRootThroughPeers(topic);
|
|
1159
1256
|
if (retried) {
|
|
1160
|
-
return
|
|
1257
|
+
return this.normalizePeerTopicRootState(topic, retried);
|
|
1161
1258
|
}
|
|
1162
1259
|
}
|
|
1163
1260
|
}
|
|
1164
1261
|
|
|
1165
|
-
return {
|
|
1262
|
+
return {
|
|
1263
|
+
root: this.topicRootControlPlane.resolveDeterministicTopicRoot(topic),
|
|
1264
|
+
authoritative: false,
|
|
1265
|
+
};
|
|
1166
1266
|
}
|
|
1167
1267
|
|
|
1168
1268
|
public async resolveTopicRoot(topic: string): Promise<string | undefined> {
|
|
1169
1269
|
return (await this.resolveTopicRootState(topic)).root;
|
|
1170
1270
|
}
|
|
1171
1271
|
|
|
1172
|
-
private async
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
this.
|
|
1178
|
-
|
|
1272
|
+
private async resolveShardRootState(
|
|
1273
|
+
shardTopic: string,
|
|
1274
|
+
): Promise<{ root: string; candidateGeneration: string }> {
|
|
1275
|
+
const lifecycleRevision = this.topicControlPlaneLifecycleRevision;
|
|
1276
|
+
for (;;) {
|
|
1277
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1278
|
+
// If someone configured topic-root candidates externally (e.g.
|
|
1279
|
+
// TestSession router selection or Peerbit.bootstrap) after this peer
|
|
1280
|
+
// entered auto mode, disable auto mode before caching a root.
|
|
1281
|
+
if (this.autoTopicRootCandidates) {
|
|
1282
|
+
this.maybeDisableAutoTopicRootCandidatesIfExternallyConfigured();
|
|
1283
|
+
}
|
|
1179
1284
|
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
);
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1285
|
+
const candidateGeneration = this.getTopicRootCandidateGeneration();
|
|
1286
|
+
const hasConnectedTrackers =
|
|
1287
|
+
this.getConnectedTopicRootTrackers().length > 0;
|
|
1288
|
+
const cached = this.shardRootCache.get(shardTopic);
|
|
1289
|
+
if (cached && (cached.authoritative || !hasConnectedTrackers)) {
|
|
1290
|
+
return { root: cached.root, candidateGeneration };
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
const resolved = await this.resolveTopicRootState(shardTopic);
|
|
1294
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1295
|
+
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1298
|
+
if (!resolved.root) {
|
|
1299
|
+
throw new Error(
|
|
1300
|
+
`No root resolved for shard topic ${shardTopic}. Configure TopicRootControlPlane candidates/resolver/trackers, or dial/bootstrap a peer that can resolve shard roots.`,
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
if (resolved.authoritative || !hasConnectedTrackers) {
|
|
1304
|
+
this.shardRootCache.set(
|
|
1305
|
+
shardTopic,
|
|
1306
|
+
resolved as {
|
|
1307
|
+
root: string;
|
|
1308
|
+
authoritative: boolean;
|
|
1309
|
+
},
|
|
1310
|
+
);
|
|
1311
|
+
} else {
|
|
1312
|
+
this.shardRootCache.delete(shardTopic);
|
|
1313
|
+
}
|
|
1314
|
+
return { root: resolved.root, candidateGeneration };
|
|
1198
1315
|
}
|
|
1199
|
-
return resolved.root;
|
|
1200
1316
|
}
|
|
1201
1317
|
|
|
1202
1318
|
private getConnectedTopicRootTrackers(): PeerStreams[] {
|
|
@@ -1240,9 +1356,14 @@ export class TopicControlPlane
|
|
|
1240
1356
|
|
|
1241
1357
|
private resolvePendingTopicRootQuery(
|
|
1242
1358
|
message: TopicRootQueryResponse,
|
|
1359
|
+
responderPeerHash: string,
|
|
1243
1360
|
): boolean {
|
|
1244
1361
|
const pending = this.pendingTopicRootQueries.get(message.requestId);
|
|
1245
|
-
if (
|
|
1362
|
+
if (
|
|
1363
|
+
!pending ||
|
|
1364
|
+
pending.topic !== message.topic ||
|
|
1365
|
+
pending.expectedPeerHash !== responderPeerHash
|
|
1366
|
+
) {
|
|
1246
1367
|
return false;
|
|
1247
1368
|
}
|
|
1248
1369
|
this.pendingTopicRootQueries.delete(message.requestId);
|
|
@@ -1258,6 +1379,7 @@ export class TopicControlPlane
|
|
|
1258
1379
|
): Promise<string | undefined> {
|
|
1259
1380
|
if (!this.started || this.stopping) return undefined;
|
|
1260
1381
|
|
|
1382
|
+
const expectedPeerHash = peer.publicKey.hashcode();
|
|
1261
1383
|
const requestId = this.nextTopicRootRequestIdValue();
|
|
1262
1384
|
const responsePromise = new Promise<string | undefined>((resolve) => {
|
|
1263
1385
|
const timer = setTimeout(() => {
|
|
@@ -1265,7 +1387,12 @@ export class TopicControlPlane
|
|
|
1265
1387
|
resolve(undefined);
|
|
1266
1388
|
}, Math.max(1, Math.floor(timeoutMs)));
|
|
1267
1389
|
timer.unref?.();
|
|
1268
|
-
this.pendingTopicRootQueries.set(requestId, {
|
|
1390
|
+
this.pendingTopicRootQueries.set(requestId, {
|
|
1391
|
+
expectedPeerHash,
|
|
1392
|
+
topic,
|
|
1393
|
+
resolve,
|
|
1394
|
+
timer,
|
|
1395
|
+
});
|
|
1269
1396
|
});
|
|
1270
1397
|
|
|
1271
1398
|
try {
|
|
@@ -1288,8 +1415,11 @@ export class TopicControlPlane
|
|
|
1288
1415
|
private async confirmDirectShardRoot(
|
|
1289
1416
|
shardTopic: string,
|
|
1290
1417
|
root: string,
|
|
1418
|
+
candidateGeneration: string,
|
|
1419
|
+
lifecycleRevision: number,
|
|
1291
1420
|
signal?: AbortSignal,
|
|
1292
1421
|
): Promise<string> {
|
|
1422
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1293
1423
|
if (root === this.publicKeyHash) return root;
|
|
1294
1424
|
const rootPeer = this.peers.get(root);
|
|
1295
1425
|
if (!rootPeer) return root;
|
|
@@ -1302,37 +1432,64 @@ export class TopicControlPlane
|
|
|
1302
1432
|
),
|
|
1303
1433
|
signal,
|
|
1304
1434
|
);
|
|
1435
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1436
|
+
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1437
|
+
return root;
|
|
1438
|
+
}
|
|
1305
1439
|
if (!confirmation) return root;
|
|
1306
|
-
|
|
1440
|
+
const resolved = this.normalizePeerTopicRootState(shardTopic, confirmation);
|
|
1441
|
+
if (!resolved.authoritative) {
|
|
1442
|
+
this.shardRootCache.delete(shardTopic);
|
|
1443
|
+
} else if (resolved.root !== root) {
|
|
1307
1444
|
this.shardRootCache.set(shardTopic, {
|
|
1308
|
-
root:
|
|
1445
|
+
root: resolved.root,
|
|
1309
1446
|
authoritative: true,
|
|
1310
1447
|
});
|
|
1311
1448
|
}
|
|
1312
|
-
return
|
|
1449
|
+
return resolved.root;
|
|
1313
1450
|
}
|
|
1314
1451
|
|
|
1315
1452
|
private async resolveQueryableTopicRoot(
|
|
1316
1453
|
topic: string,
|
|
1317
1454
|
): Promise<string | undefined> {
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1455
|
+
const lifecycleRevision = this.topicControlPlaneLifecycleRevision;
|
|
1456
|
+
for (;;) {
|
|
1457
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1458
|
+
const rootCandidateGeneration = this.getTopicRootCandidateGeneration();
|
|
1459
|
+
const root =
|
|
1460
|
+
await this.topicRootControlPlane.resolveCanonicalTopicRoot(topic);
|
|
1461
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1462
|
+
if (rootCandidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
if (root !== this.publicKeyHash) {
|
|
1466
|
+
return root;
|
|
1467
|
+
}
|
|
1468
|
+
if (!topic.startsWith(this.shardTopicPrefix)) {
|
|
1469
|
+
return root;
|
|
1470
|
+
}
|
|
1325
1471
|
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1472
|
+
try {
|
|
1473
|
+
await this.ensureFanoutChannel(topic, {
|
|
1474
|
+
pin: true,
|
|
1475
|
+
root,
|
|
1476
|
+
rootCandidateGeneration,
|
|
1477
|
+
});
|
|
1478
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1479
|
+
if (
|
|
1480
|
+
rootCandidateGeneration !== this.getTopicRootCandidateGeneration()
|
|
1481
|
+
) {
|
|
1482
|
+
continue;
|
|
1483
|
+
}
|
|
1484
|
+
return root;
|
|
1485
|
+
} catch (error) {
|
|
1486
|
+
warn(
|
|
1487
|
+
`Failed to host shard root ${topic} before answering root query: ${
|
|
1488
|
+
error instanceof Error ? error.message : String(error)
|
|
1489
|
+
}`,
|
|
1490
|
+
);
|
|
1491
|
+
return undefined;
|
|
1492
|
+
}
|
|
1336
1493
|
}
|
|
1337
1494
|
}
|
|
1338
1495
|
|
|
@@ -1361,14 +1518,69 @@ export class TopicControlPlane
|
|
|
1361
1518
|
|
|
1362
1519
|
private async ensureFanoutChannel(
|
|
1363
1520
|
shardTopic: string,
|
|
1364
|
-
options?:
|
|
1365
|
-
ephemeral?: boolean;
|
|
1366
|
-
pin?: boolean;
|
|
1367
|
-
root?: string;
|
|
1368
|
-
signal?: AbortSignal;
|
|
1369
|
-
},
|
|
1521
|
+
options?: EnsureFanoutChannelOptions,
|
|
1370
1522
|
): Promise<void> {
|
|
1371
|
-
|
|
1523
|
+
const topic = shardTopic.toString();
|
|
1524
|
+
const lifecycleRevision = this.topicControlPlaneLifecycleRevision;
|
|
1525
|
+
for (;;) {
|
|
1526
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1527
|
+
const candidateGeneration = this.getTopicRootCandidateGeneration();
|
|
1528
|
+
const active = this.ensureFanoutChannelInFlight.get(topic);
|
|
1529
|
+
if (
|
|
1530
|
+
active?.candidateGeneration === candidateGeneration &&
|
|
1531
|
+
active.lifecycleRevision === lifecycleRevision
|
|
1532
|
+
) {
|
|
1533
|
+
try {
|
|
1534
|
+
await withAbort(active.opening, options?.signal);
|
|
1535
|
+
} catch (error) {
|
|
1536
|
+
if (options?.signal?.aborted) throw error;
|
|
1537
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1538
|
+
}
|
|
1539
|
+
continue;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
const opening = this.ensureFanoutChannelOnce(
|
|
1543
|
+
topic,
|
|
1544
|
+
options,
|
|
1545
|
+
lifecycleRevision,
|
|
1546
|
+
candidateGeneration,
|
|
1547
|
+
);
|
|
1548
|
+
const inFlight = {
|
|
1549
|
+
candidateGeneration,
|
|
1550
|
+
lifecycleRevision,
|
|
1551
|
+
opening,
|
|
1552
|
+
};
|
|
1553
|
+
this.ensureFanoutChannelInFlight.set(topic, inFlight);
|
|
1554
|
+
try {
|
|
1555
|
+
await opening;
|
|
1556
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1557
|
+
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1560
|
+
return;
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
if (options?.signal?.aborted) throw error;
|
|
1563
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1564
|
+
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
throw error;
|
|
1568
|
+
} finally {
|
|
1569
|
+
if (this.ensureFanoutChannelInFlight.get(topic) === inFlight) {
|
|
1570
|
+
this.ensureFanoutChannelInFlight.delete(topic);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
private async ensureFanoutChannelOnce(
|
|
1577
|
+
shardTopic: string,
|
|
1578
|
+
options: EnsureFanoutChannelOptions | undefined,
|
|
1579
|
+
lifecycleRevision: number,
|
|
1580
|
+
candidateGeneration: string,
|
|
1581
|
+
): Promise<void> {
|
|
1582
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1583
|
+
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) return;
|
|
1372
1584
|
const t = shardTopic.toString();
|
|
1373
1585
|
const pin = options?.pin === true;
|
|
1374
1586
|
const wantEphemeral = options?.ephemeral === true;
|
|
@@ -1376,10 +1588,31 @@ export class TopicControlPlane
|
|
|
1376
1588
|
// Allow callers that already resolved the shard root (e.g. hostShardRootsNow)
|
|
1377
1589
|
// to pass it through to avoid a race where the candidate set changes between
|
|
1378
1590
|
// two resolve calls, causing an unnecessary (and potentially slow) join.
|
|
1379
|
-
|
|
1591
|
+
const suppliedRoot = options?.root;
|
|
1592
|
+
const suppliedRootGeneration = options?.rootCandidateGeneration;
|
|
1593
|
+
if (
|
|
1594
|
+
suppliedRoot !== undefined &&
|
|
1595
|
+
suppliedRootGeneration !== candidateGeneration
|
|
1596
|
+
) {
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1600
|
+
let resolvedGeneration = candidateGeneration;
|
|
1601
|
+
let root = suppliedRoot;
|
|
1380
1602
|
const existing = this.fanoutChannels.get(t);
|
|
1381
1603
|
if (existing) {
|
|
1382
|
-
|
|
1604
|
+
if (!root) {
|
|
1605
|
+
const resolved = await this.resolveShardRootState(t);
|
|
1606
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1607
|
+
root = resolved.root;
|
|
1608
|
+
resolvedGeneration = resolved.candidateGeneration;
|
|
1609
|
+
}
|
|
1610
|
+
if (
|
|
1611
|
+
resolvedGeneration !== candidateGeneration ||
|
|
1612
|
+
candidateGeneration !== this.getTopicRootCandidateGeneration()
|
|
1613
|
+
) {
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1383
1616
|
if (root === existing.root) {
|
|
1384
1617
|
existing.lastUsedAt = Date.now();
|
|
1385
1618
|
if (existing.ephemeral && !wantEphemeral) {
|
|
@@ -1390,15 +1623,48 @@ export class TopicControlPlane
|
|
|
1390
1623
|
}
|
|
1391
1624
|
if (pin) this.pinnedShards.add(t);
|
|
1392
1625
|
await withAbort(existing.join, options?.signal);
|
|
1626
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1627
|
+
if (
|
|
1628
|
+
resolvedGeneration !== candidateGeneration ||
|
|
1629
|
+
candidateGeneration !== this.getTopicRootCandidateGeneration()
|
|
1630
|
+
) {
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1393
1633
|
return;
|
|
1394
1634
|
}
|
|
1395
1635
|
|
|
1396
|
-
// Root mapping changed (candidate set updated): migrate to the new
|
|
1397
|
-
|
|
1636
|
+
// Root mapping changed (candidate set updated): migrate to the new
|
|
1637
|
+
// overlay.
|
|
1638
|
+
await withAbort(
|
|
1639
|
+
this.closeFanoutChannel(t, { force: true }),
|
|
1640
|
+
options?.signal,
|
|
1641
|
+
);
|
|
1642
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1643
|
+
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1398
1646
|
}
|
|
1399
1647
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1648
|
+
if (!root) {
|
|
1649
|
+
const resolved = await this.resolveShardRootState(t);
|
|
1650
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1651
|
+
root = resolved.root;
|
|
1652
|
+
resolvedGeneration = resolved.candidateGeneration;
|
|
1653
|
+
}
|
|
1654
|
+
root = await this.confirmDirectShardRoot(
|
|
1655
|
+
t,
|
|
1656
|
+
root,
|
|
1657
|
+
resolvedGeneration,
|
|
1658
|
+
lifecycleRevision,
|
|
1659
|
+
options?.signal,
|
|
1660
|
+
);
|
|
1661
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1662
|
+
if (
|
|
1663
|
+
resolvedGeneration !== candidateGeneration ||
|
|
1664
|
+
candidateGeneration !== this.getTopicRootCandidateGeneration()
|
|
1665
|
+
) {
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1402
1668
|
const channel = new FanoutChannel(this.fanout, { topic: t, root });
|
|
1403
1669
|
|
|
1404
1670
|
const onPayload = (payload: Uint8Array) => {
|
|
@@ -1541,7 +1807,7 @@ export class TopicControlPlane
|
|
|
1541
1807
|
|
|
1542
1808
|
const lastUsedAt = Date.now();
|
|
1543
1809
|
if (pin) this.pinnedShards.add(t);
|
|
1544
|
-
|
|
1810
|
+
const channelState = {
|
|
1545
1811
|
root,
|
|
1546
1812
|
channel,
|
|
1547
1813
|
join,
|
|
@@ -1549,14 +1815,19 @@ export class TopicControlPlane
|
|
|
1549
1815
|
onUnicast,
|
|
1550
1816
|
ephemeral: wantEphemeral,
|
|
1551
1817
|
lastUsedAt,
|
|
1552
|
-
}
|
|
1818
|
+
};
|
|
1819
|
+
this.fanoutChannels.set(t, channelState);
|
|
1553
1820
|
join.catch(() => {
|
|
1554
|
-
this.fanoutChannels.
|
|
1821
|
+
if (this.fanoutChannels.get(t) === channelState) {
|
|
1822
|
+
this.fanoutChannels.delete(t);
|
|
1823
|
+
}
|
|
1555
1824
|
});
|
|
1556
1825
|
join
|
|
1557
1826
|
.then(() => {
|
|
1558
1827
|
const st = this.fanoutChannels.get(t);
|
|
1559
|
-
if (st
|
|
1828
|
+
if (st === channelState && st.ephemeral) {
|
|
1829
|
+
this.scheduleFanoutIdleClose(t);
|
|
1830
|
+
}
|
|
1560
1831
|
})
|
|
1561
1832
|
.catch(() => {
|
|
1562
1833
|
// ignore
|
|
@@ -1565,6 +1836,7 @@ export class TopicControlPlane
|
|
|
1565
1836
|
this.evictEphemeralFanoutChannels(t);
|
|
1566
1837
|
}
|
|
1567
1838
|
await withAbort(join, options?.signal);
|
|
1839
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1568
1840
|
}
|
|
1569
1841
|
|
|
1570
1842
|
private async closeFanoutChannel(
|
|
@@ -1604,9 +1876,15 @@ export class TopicControlPlane
|
|
|
1604
1876
|
const joins: Promise<void>[] = [];
|
|
1605
1877
|
for (let i = 0; i < this.shardCount; i++) {
|
|
1606
1878
|
const shardTopic = `${this.shardTopicPrefix}${i}`;
|
|
1607
|
-
const
|
|
1608
|
-
if (root !== this.publicKeyHash) continue;
|
|
1609
|
-
joins.push(
|
|
1879
|
+
const resolved = await this.resolveShardRootState(shardTopic);
|
|
1880
|
+
if (resolved.root !== this.publicKeyHash) continue;
|
|
1881
|
+
joins.push(
|
|
1882
|
+
this.ensureFanoutChannel(shardTopic, {
|
|
1883
|
+
pin: true,
|
|
1884
|
+
root: resolved.root,
|
|
1885
|
+
rootCandidateGeneration: resolved.candidateGeneration,
|
|
1886
|
+
}),
|
|
1887
|
+
);
|
|
1610
1888
|
}
|
|
1611
1889
|
await Promise.all(joins);
|
|
1612
1890
|
}
|
|
@@ -2010,7 +2288,8 @@ export class TopicControlPlane
|
|
|
2010
2288
|
);
|
|
2011
2289
|
}
|
|
2012
2290
|
|
|
2013
|
-
const silentDelivery =
|
|
2291
|
+
const silentDelivery =
|
|
2292
|
+
options?.mode != null && isSilentDeliveryMode(options.mode);
|
|
2014
2293
|
try {
|
|
2015
2294
|
await this.publishMessage(
|
|
2016
2295
|
this.publicKey,
|
|
@@ -2153,7 +2432,7 @@ export class TopicControlPlane
|
|
|
2153
2432
|
}),
|
|
2154
2433
|
);
|
|
2155
2434
|
|
|
2156
|
-
const silentDelivery = options.mode
|
|
2435
|
+
const silentDelivery = isSilentDeliveryMode(options.mode);
|
|
2157
2436
|
try {
|
|
2158
2437
|
await this.publishMessage(
|
|
2159
2438
|
this.publicKey,
|
|
@@ -2503,6 +2782,19 @@ export class TopicControlPlane
|
|
|
2503
2782
|
stream: PeerStreams;
|
|
2504
2783
|
}): Promise<void> {
|
|
2505
2784
|
const { pubsubMessage, message, from, stream } = input;
|
|
2785
|
+
const isDirectRootControl =
|
|
2786
|
+
pubsubMessage instanceof TopicRootCandidates ||
|
|
2787
|
+
pubsubMessage instanceof TopicRootQuery ||
|
|
2788
|
+
pubsubMessage instanceof TopicRootQueryResponse;
|
|
2789
|
+
const directRootSigner = isDirectRootControl
|
|
2790
|
+
? message.header.signatures?.publicKeys[0]
|
|
2791
|
+
: undefined;
|
|
2792
|
+
if (
|
|
2793
|
+
isDirectRootControl &&
|
|
2794
|
+
(!directRootSigner || !directRootSigner.equals(stream.publicKey))
|
|
2795
|
+
) {
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2506
2798
|
|
|
2507
2799
|
if (pubsubMessage instanceof TopicRootCandidates) {
|
|
2508
2800
|
// Used only to converge deterministic shard-root candidates in auto mode.
|
|
@@ -2524,7 +2816,8 @@ export class TopicControlPlane
|
|
|
2524
2816
|
}
|
|
2525
2817
|
|
|
2526
2818
|
if (pubsubMessage instanceof TopicRootQueryResponse) {
|
|
2527
|
-
|
|
2819
|
+
const senderHash = directRootSigner!.hashcode();
|
|
2820
|
+
this.resolvePendingTopicRootQuery(pubsubMessage, senderHash);
|
|
2528
2821
|
return;
|
|
2529
2822
|
}
|
|
2530
2823
|
|
|
@@ -2857,8 +3150,8 @@ export class TopicControlPlane
|
|
|
2857
3150
|
if (deliveryModeHasReceiver(message.header.mode)) {
|
|
2858
3151
|
isForMe = message.header.mode.to.includes(this.publicKeyHash);
|
|
2859
3152
|
} else if (
|
|
2860
|
-
message.header.mode
|
|
2861
|
-
message.header.mode
|
|
3153
|
+
isAnyWhereDeliveryMode(message.header.mode) ||
|
|
3154
|
+
isAcknowledgeAnyWhereDeliveryMode(message.header.mode)
|
|
2862
3155
|
) {
|
|
2863
3156
|
isForMe = true;
|
|
2864
3157
|
}
|
|
@@ -2889,8 +3182,8 @@ export class TopicControlPlane
|
|
|
2889
3182
|
}
|
|
2890
3183
|
|
|
2891
3184
|
if (
|
|
2892
|
-
message.header.mode
|
|
2893
|
-
message.header.mode
|
|
3185
|
+
isSilentDeliveryMode(message.header.mode) ||
|
|
3186
|
+
isAcknowledgeDeliveryMode(message.header.mode)
|
|
2894
3187
|
) {
|
|
2895
3188
|
if (
|
|
2896
3189
|
message.header.mode.to.length === 1 &&
|
|
@@ -2902,8 +3195,8 @@ export class TopicControlPlane
|
|
|
2902
3195
|
|
|
2903
3196
|
const shouldForward =
|
|
2904
3197
|
seenBefore === 0 ||
|
|
2905
|
-
((message.header.mode
|
|
2906
|
-
message.header.mode
|
|
3198
|
+
((isAcknowledgeDeliveryMode(message.header.mode) ||
|
|
3199
|
+
isAcknowledgeAnyWhereDeliveryMode(message.header.mode)) &&
|
|
2907
3200
|
seenBefore < message.header.mode.redundancy);
|
|
2908
3201
|
if (shouldForward) {
|
|
2909
3202
|
this.relayMessage(from, message).catch(logErrorIfStarted);
|