@peerbit/pubsub 5.3.5 → 5.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.d.ts +9 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +291 -72
- package/dist/src/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +372 -84
package/src/index.ts
CHANGED
|
@@ -230,6 +230,14 @@ export type TopicControlPlaneOptions = DirectStreamOptions & {
|
|
|
230
230
|
subscriberCacheMaxEntries?: number;
|
|
231
231
|
};
|
|
232
232
|
|
|
233
|
+
type EnsureFanoutChannelOptions = {
|
|
234
|
+
ephemeral?: boolean;
|
|
235
|
+
pin?: boolean;
|
|
236
|
+
root?: string;
|
|
237
|
+
rootCandidateGeneration?: string;
|
|
238
|
+
signal?: AbortSignal;
|
|
239
|
+
};
|
|
240
|
+
|
|
233
241
|
export type TopicControlPlaneComponents = DirectStreamComponents;
|
|
234
242
|
|
|
235
243
|
export type PeerId = Libp2pPeerId | PublicSignKey;
|
|
@@ -333,6 +341,9 @@ export class TopicControlPlane
|
|
|
333
341
|
private autoTopicRootCandidates = false;
|
|
334
342
|
private autoTopicRootCandidateSet?: Set<string>;
|
|
335
343
|
private reconcileShardOverlaysInFlight?: Promise<void>;
|
|
344
|
+
private reconcileShardOverlaysDirty = false;
|
|
345
|
+
private topicControlPlaneStopping = false;
|
|
346
|
+
private topicControlPlaneLifecycleRevision = 0;
|
|
336
347
|
private hostOwnedShardRootsInFlight?: Promise<void>;
|
|
337
348
|
private hostOwnedShardRootsDirty = false;
|
|
338
349
|
private autoCandidatesBroadcastTimers: Array<ReturnType<typeof setTimeout>> =
|
|
@@ -356,9 +367,18 @@ export class TopicControlPlane
|
|
|
356
367
|
idleCloseTimeout?: ReturnType<typeof setTimeout>;
|
|
357
368
|
}
|
|
358
369
|
>();
|
|
370
|
+
private ensureFanoutChannelInFlight = new Map<
|
|
371
|
+
string,
|
|
372
|
+
{
|
|
373
|
+
candidateGeneration: string;
|
|
374
|
+
lifecycleRevision: number;
|
|
375
|
+
opening: Promise<void>;
|
|
376
|
+
}
|
|
377
|
+
>();
|
|
359
378
|
private pendingTopicRootQueries = new Map<
|
|
360
379
|
number,
|
|
361
380
|
{
|
|
381
|
+
expectedPeerHash: string;
|
|
362
382
|
topic: string;
|
|
363
383
|
resolve: (root: string | undefined) => void;
|
|
364
384
|
timer: ReturnType<typeof setTimeout>;
|
|
@@ -524,6 +544,7 @@ export class TopicControlPlane
|
|
|
524
544
|
}
|
|
525
545
|
|
|
526
546
|
public override async start() {
|
|
547
|
+
this.topicControlPlaneStopping = false;
|
|
527
548
|
await this.fanout.start();
|
|
528
549
|
this._onFanoutPeerUnreachable =
|
|
529
550
|
this._onFanoutPeerUnreachable ||
|
|
@@ -540,9 +561,16 @@ export class TopicControlPlane
|
|
|
540
561
|
if (this.hostOwnedShardRootsDirty) {
|
|
541
562
|
this.scheduleHostOwnedShardRoots();
|
|
542
563
|
}
|
|
564
|
+
if (this.reconcileShardOverlaysDirty) {
|
|
565
|
+
this.scheduleReconcileShardOverlays();
|
|
566
|
+
}
|
|
543
567
|
}
|
|
544
568
|
|
|
545
569
|
public override async stop() {
|
|
570
|
+
this.topicControlPlaneStopping = true;
|
|
571
|
+
this.topicControlPlaneLifecycleRevision += 1;
|
|
572
|
+
this.reconcileShardOverlaysDirty = false;
|
|
573
|
+
this.ensureFanoutChannelInFlight.clear();
|
|
546
574
|
if (this._onFanoutPeerUnreachable) {
|
|
547
575
|
this.fanout.removeEventListener(
|
|
548
576
|
"fanout:peer-unreachable",
|
|
@@ -603,7 +631,8 @@ export class TopicControlPlane
|
|
|
603
631
|
|
|
604
632
|
this.debounceSubscribeAggregator.close();
|
|
605
633
|
this.debounceUnsubscribeAggregator.close();
|
|
606
|
-
|
|
634
|
+
await super.stop();
|
|
635
|
+
this.reconcileShardOverlaysDirty = false;
|
|
607
636
|
}
|
|
608
637
|
|
|
609
638
|
public override async onPeerConnected(
|
|
@@ -840,21 +869,45 @@ export class TopicControlPlane
|
|
|
840
869
|
}
|
|
841
870
|
|
|
842
871
|
private scheduleReconcileShardOverlays() {
|
|
872
|
+
this.reconcileShardOverlaysDirty = true;
|
|
873
|
+
if (!this.started || this.stopping || this.topicControlPlaneStopping)
|
|
874
|
+
return;
|
|
843
875
|
if (this.reconcileShardOverlaysInFlight) return;
|
|
844
|
-
this.reconcileShardOverlaysInFlight =
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
876
|
+
this.reconcileShardOverlaysInFlight = (async () => {
|
|
877
|
+
for (;;) {
|
|
878
|
+
if (!this.started || this.stopping || this.topicControlPlaneStopping)
|
|
879
|
+
return;
|
|
880
|
+
if (!this.reconcileShardOverlaysDirty) return;
|
|
881
|
+
this.reconcileShardOverlaysDirty = false;
|
|
882
|
+
try {
|
|
883
|
+
await this.reconcileShardOverlays();
|
|
884
|
+
} catch {
|
|
885
|
+
// Fanout streams or roots might not be ready yet. Preserve both
|
|
886
|
+
// failures and root changes that arrived during the active pass.
|
|
887
|
+
if (!this.started || this.stopping || this.topicControlPlaneStopping)
|
|
888
|
+
return;
|
|
889
|
+
this.reconcileShardOverlaysDirty = true;
|
|
890
|
+
await new Promise<void>((resolve) => {
|
|
891
|
+
const timer = setTimeout(resolve, 250);
|
|
892
|
+
timer.unref?.();
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
})().finally(() => {
|
|
897
|
+
this.reconcileShardOverlaysInFlight = undefined;
|
|
898
|
+
if (
|
|
899
|
+
this.reconcileShardOverlaysDirty &&
|
|
900
|
+
this.started &&
|
|
901
|
+
!this.stopping &&
|
|
902
|
+
!this.topicControlPlaneStopping
|
|
903
|
+
) {
|
|
904
|
+
this.scheduleReconcileShardOverlays();
|
|
905
|
+
}
|
|
906
|
+
});
|
|
854
907
|
}
|
|
855
908
|
|
|
856
909
|
private async reconcileShardOverlays() {
|
|
857
|
-
if (!this.started) return;
|
|
910
|
+
if (!this.started || this.topicControlPlaneStopping) return;
|
|
858
911
|
|
|
859
912
|
const byShard = new Map<string, string[]>();
|
|
860
913
|
for (const topic of this.subscriptions.keys()) {
|
|
@@ -1134,6 +1187,41 @@ export class TopicControlPlane
|
|
|
1134
1187
|
return PubSubMessage.from(bytes);
|
|
1135
1188
|
}
|
|
1136
1189
|
|
|
1190
|
+
private getTopicRootCandidateGeneration(): string {
|
|
1191
|
+
return JSON.stringify([
|
|
1192
|
+
this.autoTopicRootCandidates,
|
|
1193
|
+
this.topicRootControlPlane.getTopicRootCandidates(),
|
|
1194
|
+
]);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
private assertTopicControlPlaneActive(lifecycleRevision: number): void {
|
|
1198
|
+
if (
|
|
1199
|
+
lifecycleRevision !== this.topicControlPlaneLifecycleRevision ||
|
|
1200
|
+
!this.started ||
|
|
1201
|
+
this.stopping ||
|
|
1202
|
+
this.topicControlPlaneStopping
|
|
1203
|
+
) {
|
|
1204
|
+
throw new NotStartedError();
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
private normalizePeerTopicRootState(
|
|
1209
|
+
topic: string,
|
|
1210
|
+
root: string,
|
|
1211
|
+
): { root: string; authoritative: boolean } {
|
|
1212
|
+
const deterministic =
|
|
1213
|
+
this.topicRootControlPlane.resolveDeterministicTopicRoot(topic);
|
|
1214
|
+
if (
|
|
1215
|
+
this.autoTopicRootCandidates &&
|
|
1216
|
+
topic.startsWith(this.shardTopicPrefix) &&
|
|
1217
|
+
deterministic &&
|
|
1218
|
+
root !== deterministic
|
|
1219
|
+
) {
|
|
1220
|
+
return { root: deterministic, authoritative: false };
|
|
1221
|
+
}
|
|
1222
|
+
return { root, authoritative: true };
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1137
1225
|
private async resolveTopicRootState(
|
|
1138
1226
|
topic: string,
|
|
1139
1227
|
): Promise<{ root?: string; authoritative: boolean }> {
|
|
@@ -1144,7 +1232,12 @@ export class TopicControlPlane
|
|
|
1144
1232
|
|
|
1145
1233
|
const resolvedThroughPeers = await this.resolveTopicRootThroughPeers(topic);
|
|
1146
1234
|
if (resolvedThroughPeers) {
|
|
1147
|
-
|
|
1235
|
+
// Unconfigured peer-query replies cannot override the locally
|
|
1236
|
+
// deterministic root for internal shards in auto mode. Roots, resolvers,
|
|
1237
|
+
// and trackers explicitly configured on this control plane are resolved
|
|
1238
|
+
// above and remain authoritative. A leaf relying on a queried gateway for
|
|
1239
|
+
// shard roots must first disable auto mode with setTopicRootCandidates([]).
|
|
1240
|
+
return this.normalizePeerTopicRootState(topic, resolvedThroughPeers);
|
|
1148
1241
|
}
|
|
1149
1242
|
|
|
1150
1243
|
const deterministic = this.topicRootControlPlane.resolveDeterministicTopicRoot(topic);
|
|
@@ -1157,46 +1250,65 @@ export class TopicControlPlane
|
|
|
1157
1250
|
await delay(150 * (attempt < 4 ? 1 : 2));
|
|
1158
1251
|
const retried = await this.resolveTopicRootThroughPeers(topic);
|
|
1159
1252
|
if (retried) {
|
|
1160
|
-
return
|
|
1253
|
+
return this.normalizePeerTopicRootState(topic, retried);
|
|
1161
1254
|
}
|
|
1162
1255
|
}
|
|
1163
1256
|
}
|
|
1164
1257
|
|
|
1165
|
-
return {
|
|
1258
|
+
return {
|
|
1259
|
+
root: this.topicRootControlPlane.resolveDeterministicTopicRoot(topic),
|
|
1260
|
+
authoritative: false,
|
|
1261
|
+
};
|
|
1166
1262
|
}
|
|
1167
1263
|
|
|
1168
1264
|
public async resolveTopicRoot(topic: string): Promise<string | undefined> {
|
|
1169
1265
|
return (await this.resolveTopicRootState(topic)).root;
|
|
1170
1266
|
}
|
|
1171
1267
|
|
|
1172
|
-
private async
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
this.
|
|
1178
|
-
|
|
1268
|
+
private async resolveShardRootState(
|
|
1269
|
+
shardTopic: string,
|
|
1270
|
+
): Promise<{ root: string; candidateGeneration: string }> {
|
|
1271
|
+
const lifecycleRevision = this.topicControlPlaneLifecycleRevision;
|
|
1272
|
+
for (;;) {
|
|
1273
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1274
|
+
// If someone configured topic-root candidates externally (e.g.
|
|
1275
|
+
// TestSession router selection or Peerbit.bootstrap) after this peer
|
|
1276
|
+
// entered auto mode, disable auto mode before caching a root.
|
|
1277
|
+
if (this.autoTopicRootCandidates) {
|
|
1278
|
+
this.maybeDisableAutoTopicRootCandidatesIfExternallyConfigured();
|
|
1279
|
+
}
|
|
1179
1280
|
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
);
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1281
|
+
const candidateGeneration = this.getTopicRootCandidateGeneration();
|
|
1282
|
+
const hasConnectedTrackers =
|
|
1283
|
+
this.getConnectedTopicRootTrackers().length > 0;
|
|
1284
|
+
const cached = this.shardRootCache.get(shardTopic);
|
|
1285
|
+
if (cached && (cached.authoritative || !hasConnectedTrackers)) {
|
|
1286
|
+
return { root: cached.root, candidateGeneration };
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
const resolved = await this.resolveTopicRootState(shardTopic);
|
|
1290
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1291
|
+
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
if (!resolved.root) {
|
|
1295
|
+
throw new Error(
|
|
1296
|
+
`No root resolved for shard topic ${shardTopic}. Configure TopicRootControlPlane candidates/resolver/trackers, or dial/bootstrap a peer that can resolve shard roots.`,
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
if (resolved.authoritative || !hasConnectedTrackers) {
|
|
1300
|
+
this.shardRootCache.set(
|
|
1301
|
+
shardTopic,
|
|
1302
|
+
resolved as {
|
|
1303
|
+
root: string;
|
|
1304
|
+
authoritative: boolean;
|
|
1305
|
+
},
|
|
1306
|
+
);
|
|
1307
|
+
} else {
|
|
1308
|
+
this.shardRootCache.delete(shardTopic);
|
|
1309
|
+
}
|
|
1310
|
+
return { root: resolved.root, candidateGeneration };
|
|
1198
1311
|
}
|
|
1199
|
-
return resolved.root;
|
|
1200
1312
|
}
|
|
1201
1313
|
|
|
1202
1314
|
private getConnectedTopicRootTrackers(): PeerStreams[] {
|
|
@@ -1240,9 +1352,14 @@ export class TopicControlPlane
|
|
|
1240
1352
|
|
|
1241
1353
|
private resolvePendingTopicRootQuery(
|
|
1242
1354
|
message: TopicRootQueryResponse,
|
|
1355
|
+
responderPeerHash: string,
|
|
1243
1356
|
): boolean {
|
|
1244
1357
|
const pending = this.pendingTopicRootQueries.get(message.requestId);
|
|
1245
|
-
if (
|
|
1358
|
+
if (
|
|
1359
|
+
!pending ||
|
|
1360
|
+
pending.topic !== message.topic ||
|
|
1361
|
+
pending.expectedPeerHash !== responderPeerHash
|
|
1362
|
+
) {
|
|
1246
1363
|
return false;
|
|
1247
1364
|
}
|
|
1248
1365
|
this.pendingTopicRootQueries.delete(message.requestId);
|
|
@@ -1258,6 +1375,7 @@ export class TopicControlPlane
|
|
|
1258
1375
|
): Promise<string | undefined> {
|
|
1259
1376
|
if (!this.started || this.stopping) return undefined;
|
|
1260
1377
|
|
|
1378
|
+
const expectedPeerHash = peer.publicKey.hashcode();
|
|
1261
1379
|
const requestId = this.nextTopicRootRequestIdValue();
|
|
1262
1380
|
const responsePromise = new Promise<string | undefined>((resolve) => {
|
|
1263
1381
|
const timer = setTimeout(() => {
|
|
@@ -1265,7 +1383,12 @@ export class TopicControlPlane
|
|
|
1265
1383
|
resolve(undefined);
|
|
1266
1384
|
}, Math.max(1, Math.floor(timeoutMs)));
|
|
1267
1385
|
timer.unref?.();
|
|
1268
|
-
this.pendingTopicRootQueries.set(requestId, {
|
|
1386
|
+
this.pendingTopicRootQueries.set(requestId, {
|
|
1387
|
+
expectedPeerHash,
|
|
1388
|
+
topic,
|
|
1389
|
+
resolve,
|
|
1390
|
+
timer,
|
|
1391
|
+
});
|
|
1269
1392
|
});
|
|
1270
1393
|
|
|
1271
1394
|
try {
|
|
@@ -1288,8 +1411,11 @@ export class TopicControlPlane
|
|
|
1288
1411
|
private async confirmDirectShardRoot(
|
|
1289
1412
|
shardTopic: string,
|
|
1290
1413
|
root: string,
|
|
1414
|
+
candidateGeneration: string,
|
|
1415
|
+
lifecycleRevision: number,
|
|
1291
1416
|
signal?: AbortSignal,
|
|
1292
1417
|
): Promise<string> {
|
|
1418
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1293
1419
|
if (root === this.publicKeyHash) return root;
|
|
1294
1420
|
const rootPeer = this.peers.get(root);
|
|
1295
1421
|
if (!rootPeer) return root;
|
|
@@ -1302,37 +1428,64 @@ export class TopicControlPlane
|
|
|
1302
1428
|
),
|
|
1303
1429
|
signal,
|
|
1304
1430
|
);
|
|
1431
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1432
|
+
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1433
|
+
return root;
|
|
1434
|
+
}
|
|
1305
1435
|
if (!confirmation) return root;
|
|
1306
|
-
|
|
1436
|
+
const resolved = this.normalizePeerTopicRootState(shardTopic, confirmation);
|
|
1437
|
+
if (!resolved.authoritative) {
|
|
1438
|
+
this.shardRootCache.delete(shardTopic);
|
|
1439
|
+
} else if (resolved.root !== root) {
|
|
1307
1440
|
this.shardRootCache.set(shardTopic, {
|
|
1308
|
-
root:
|
|
1441
|
+
root: resolved.root,
|
|
1309
1442
|
authoritative: true,
|
|
1310
1443
|
});
|
|
1311
1444
|
}
|
|
1312
|
-
return
|
|
1445
|
+
return resolved.root;
|
|
1313
1446
|
}
|
|
1314
1447
|
|
|
1315
1448
|
private async resolveQueryableTopicRoot(
|
|
1316
1449
|
topic: string,
|
|
1317
1450
|
): Promise<string | undefined> {
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1451
|
+
const lifecycleRevision = this.topicControlPlaneLifecycleRevision;
|
|
1452
|
+
for (;;) {
|
|
1453
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1454
|
+
const rootCandidateGeneration = this.getTopicRootCandidateGeneration();
|
|
1455
|
+
const root =
|
|
1456
|
+
await this.topicRootControlPlane.resolveCanonicalTopicRoot(topic);
|
|
1457
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1458
|
+
if (rootCandidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
if (root !== this.publicKeyHash) {
|
|
1462
|
+
return root;
|
|
1463
|
+
}
|
|
1464
|
+
if (!topic.startsWith(this.shardTopicPrefix)) {
|
|
1465
|
+
return root;
|
|
1466
|
+
}
|
|
1325
1467
|
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1468
|
+
try {
|
|
1469
|
+
await this.ensureFanoutChannel(topic, {
|
|
1470
|
+
pin: true,
|
|
1471
|
+
root,
|
|
1472
|
+
rootCandidateGeneration,
|
|
1473
|
+
});
|
|
1474
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1475
|
+
if (
|
|
1476
|
+
rootCandidateGeneration !== this.getTopicRootCandidateGeneration()
|
|
1477
|
+
) {
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
return root;
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
warn(
|
|
1483
|
+
`Failed to host shard root ${topic} before answering root query: ${
|
|
1484
|
+
error instanceof Error ? error.message : String(error)
|
|
1485
|
+
}`,
|
|
1486
|
+
);
|
|
1487
|
+
return undefined;
|
|
1488
|
+
}
|
|
1336
1489
|
}
|
|
1337
1490
|
}
|
|
1338
1491
|
|
|
@@ -1361,14 +1514,69 @@ export class TopicControlPlane
|
|
|
1361
1514
|
|
|
1362
1515
|
private async ensureFanoutChannel(
|
|
1363
1516
|
shardTopic: string,
|
|
1364
|
-
options?:
|
|
1365
|
-
ephemeral?: boolean;
|
|
1366
|
-
pin?: boolean;
|
|
1367
|
-
root?: string;
|
|
1368
|
-
signal?: AbortSignal;
|
|
1369
|
-
},
|
|
1517
|
+
options?: EnsureFanoutChannelOptions,
|
|
1370
1518
|
): Promise<void> {
|
|
1371
|
-
|
|
1519
|
+
const topic = shardTopic.toString();
|
|
1520
|
+
const lifecycleRevision = this.topicControlPlaneLifecycleRevision;
|
|
1521
|
+
for (;;) {
|
|
1522
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1523
|
+
const candidateGeneration = this.getTopicRootCandidateGeneration();
|
|
1524
|
+
const active = this.ensureFanoutChannelInFlight.get(topic);
|
|
1525
|
+
if (
|
|
1526
|
+
active?.candidateGeneration === candidateGeneration &&
|
|
1527
|
+
active.lifecycleRevision === lifecycleRevision
|
|
1528
|
+
) {
|
|
1529
|
+
try {
|
|
1530
|
+
await withAbort(active.opening, options?.signal);
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
if (options?.signal?.aborted) throw error;
|
|
1533
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1534
|
+
}
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
const opening = this.ensureFanoutChannelOnce(
|
|
1539
|
+
topic,
|
|
1540
|
+
options,
|
|
1541
|
+
lifecycleRevision,
|
|
1542
|
+
candidateGeneration,
|
|
1543
|
+
);
|
|
1544
|
+
const inFlight = {
|
|
1545
|
+
candidateGeneration,
|
|
1546
|
+
lifecycleRevision,
|
|
1547
|
+
opening,
|
|
1548
|
+
};
|
|
1549
|
+
this.ensureFanoutChannelInFlight.set(topic, inFlight);
|
|
1550
|
+
try {
|
|
1551
|
+
await opening;
|
|
1552
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1553
|
+
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
return;
|
|
1557
|
+
} catch (error) {
|
|
1558
|
+
if (options?.signal?.aborted) throw error;
|
|
1559
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1560
|
+
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
throw error;
|
|
1564
|
+
} finally {
|
|
1565
|
+
if (this.ensureFanoutChannelInFlight.get(topic) === inFlight) {
|
|
1566
|
+
this.ensureFanoutChannelInFlight.delete(topic);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
private async ensureFanoutChannelOnce(
|
|
1573
|
+
shardTopic: string,
|
|
1574
|
+
options: EnsureFanoutChannelOptions | undefined,
|
|
1575
|
+
lifecycleRevision: number,
|
|
1576
|
+
candidateGeneration: string,
|
|
1577
|
+
): Promise<void> {
|
|
1578
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1579
|
+
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) return;
|
|
1372
1580
|
const t = shardTopic.toString();
|
|
1373
1581
|
const pin = options?.pin === true;
|
|
1374
1582
|
const wantEphemeral = options?.ephemeral === true;
|
|
@@ -1376,10 +1584,31 @@ export class TopicControlPlane
|
|
|
1376
1584
|
// Allow callers that already resolved the shard root (e.g. hostShardRootsNow)
|
|
1377
1585
|
// to pass it through to avoid a race where the candidate set changes between
|
|
1378
1586
|
// two resolve calls, causing an unnecessary (and potentially slow) join.
|
|
1379
|
-
|
|
1587
|
+
const suppliedRoot = options?.root;
|
|
1588
|
+
const suppliedRootGeneration = options?.rootCandidateGeneration;
|
|
1589
|
+
if (
|
|
1590
|
+
suppliedRoot !== undefined &&
|
|
1591
|
+
suppliedRootGeneration !== candidateGeneration
|
|
1592
|
+
) {
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1596
|
+
let resolvedGeneration = candidateGeneration;
|
|
1597
|
+
let root = suppliedRoot;
|
|
1380
1598
|
const existing = this.fanoutChannels.get(t);
|
|
1381
1599
|
if (existing) {
|
|
1382
|
-
|
|
1600
|
+
if (!root) {
|
|
1601
|
+
const resolved = await this.resolveShardRootState(t);
|
|
1602
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1603
|
+
root = resolved.root;
|
|
1604
|
+
resolvedGeneration = resolved.candidateGeneration;
|
|
1605
|
+
}
|
|
1606
|
+
if (
|
|
1607
|
+
resolvedGeneration !== candidateGeneration ||
|
|
1608
|
+
candidateGeneration !== this.getTopicRootCandidateGeneration()
|
|
1609
|
+
) {
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1383
1612
|
if (root === existing.root) {
|
|
1384
1613
|
existing.lastUsedAt = Date.now();
|
|
1385
1614
|
if (existing.ephemeral && !wantEphemeral) {
|
|
@@ -1390,15 +1619,48 @@ export class TopicControlPlane
|
|
|
1390
1619
|
}
|
|
1391
1620
|
if (pin) this.pinnedShards.add(t);
|
|
1392
1621
|
await withAbort(existing.join, options?.signal);
|
|
1622
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1623
|
+
if (
|
|
1624
|
+
resolvedGeneration !== candidateGeneration ||
|
|
1625
|
+
candidateGeneration !== this.getTopicRootCandidateGeneration()
|
|
1626
|
+
) {
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1393
1629
|
return;
|
|
1394
1630
|
}
|
|
1395
1631
|
|
|
1396
|
-
// Root mapping changed (candidate set updated): migrate to the new
|
|
1397
|
-
|
|
1632
|
+
// Root mapping changed (candidate set updated): migrate to the new
|
|
1633
|
+
// overlay.
|
|
1634
|
+
await withAbort(
|
|
1635
|
+
this.closeFanoutChannel(t, { force: true }),
|
|
1636
|
+
options?.signal,
|
|
1637
|
+
);
|
|
1638
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1639
|
+
if (candidateGeneration !== this.getTopicRootCandidateGeneration()) {
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1398
1642
|
}
|
|
1399
1643
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1644
|
+
if (!root) {
|
|
1645
|
+
const resolved = await this.resolveShardRootState(t);
|
|
1646
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1647
|
+
root = resolved.root;
|
|
1648
|
+
resolvedGeneration = resolved.candidateGeneration;
|
|
1649
|
+
}
|
|
1650
|
+
root = await this.confirmDirectShardRoot(
|
|
1651
|
+
t,
|
|
1652
|
+
root,
|
|
1653
|
+
resolvedGeneration,
|
|
1654
|
+
lifecycleRevision,
|
|
1655
|
+
options?.signal,
|
|
1656
|
+
);
|
|
1657
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1658
|
+
if (
|
|
1659
|
+
resolvedGeneration !== candidateGeneration ||
|
|
1660
|
+
candidateGeneration !== this.getTopicRootCandidateGeneration()
|
|
1661
|
+
) {
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1402
1664
|
const channel = new FanoutChannel(this.fanout, { topic: t, root });
|
|
1403
1665
|
|
|
1404
1666
|
const onPayload = (payload: Uint8Array) => {
|
|
@@ -1541,7 +1803,7 @@ export class TopicControlPlane
|
|
|
1541
1803
|
|
|
1542
1804
|
const lastUsedAt = Date.now();
|
|
1543
1805
|
if (pin) this.pinnedShards.add(t);
|
|
1544
|
-
|
|
1806
|
+
const channelState = {
|
|
1545
1807
|
root,
|
|
1546
1808
|
channel,
|
|
1547
1809
|
join,
|
|
@@ -1549,14 +1811,19 @@ export class TopicControlPlane
|
|
|
1549
1811
|
onUnicast,
|
|
1550
1812
|
ephemeral: wantEphemeral,
|
|
1551
1813
|
lastUsedAt,
|
|
1552
|
-
}
|
|
1814
|
+
};
|
|
1815
|
+
this.fanoutChannels.set(t, channelState);
|
|
1553
1816
|
join.catch(() => {
|
|
1554
|
-
this.fanoutChannels.
|
|
1817
|
+
if (this.fanoutChannels.get(t) === channelState) {
|
|
1818
|
+
this.fanoutChannels.delete(t);
|
|
1819
|
+
}
|
|
1555
1820
|
});
|
|
1556
1821
|
join
|
|
1557
1822
|
.then(() => {
|
|
1558
1823
|
const st = this.fanoutChannels.get(t);
|
|
1559
|
-
if (st
|
|
1824
|
+
if (st === channelState && st.ephemeral) {
|
|
1825
|
+
this.scheduleFanoutIdleClose(t);
|
|
1826
|
+
}
|
|
1560
1827
|
})
|
|
1561
1828
|
.catch(() => {
|
|
1562
1829
|
// ignore
|
|
@@ -1565,6 +1832,7 @@ export class TopicControlPlane
|
|
|
1565
1832
|
this.evictEphemeralFanoutChannels(t);
|
|
1566
1833
|
}
|
|
1567
1834
|
await withAbort(join, options?.signal);
|
|
1835
|
+
this.assertTopicControlPlaneActive(lifecycleRevision);
|
|
1568
1836
|
}
|
|
1569
1837
|
|
|
1570
1838
|
private async closeFanoutChannel(
|
|
@@ -1604,9 +1872,15 @@ export class TopicControlPlane
|
|
|
1604
1872
|
const joins: Promise<void>[] = [];
|
|
1605
1873
|
for (let i = 0; i < this.shardCount; i++) {
|
|
1606
1874
|
const shardTopic = `${this.shardTopicPrefix}${i}`;
|
|
1607
|
-
const
|
|
1608
|
-
if (root !== this.publicKeyHash) continue;
|
|
1609
|
-
joins.push(
|
|
1875
|
+
const resolved = await this.resolveShardRootState(shardTopic);
|
|
1876
|
+
if (resolved.root !== this.publicKeyHash) continue;
|
|
1877
|
+
joins.push(
|
|
1878
|
+
this.ensureFanoutChannel(shardTopic, {
|
|
1879
|
+
pin: true,
|
|
1880
|
+
root: resolved.root,
|
|
1881
|
+
rootCandidateGeneration: resolved.candidateGeneration,
|
|
1882
|
+
}),
|
|
1883
|
+
);
|
|
1610
1884
|
}
|
|
1611
1885
|
await Promise.all(joins);
|
|
1612
1886
|
}
|
|
@@ -2503,6 +2777,19 @@ export class TopicControlPlane
|
|
|
2503
2777
|
stream: PeerStreams;
|
|
2504
2778
|
}): Promise<void> {
|
|
2505
2779
|
const { pubsubMessage, message, from, stream } = input;
|
|
2780
|
+
const isDirectRootControl =
|
|
2781
|
+
pubsubMessage instanceof TopicRootCandidates ||
|
|
2782
|
+
pubsubMessage instanceof TopicRootQuery ||
|
|
2783
|
+
pubsubMessage instanceof TopicRootQueryResponse;
|
|
2784
|
+
const directRootSigner = isDirectRootControl
|
|
2785
|
+
? message.header.signatures?.publicKeys[0]
|
|
2786
|
+
: undefined;
|
|
2787
|
+
if (
|
|
2788
|
+
isDirectRootControl &&
|
|
2789
|
+
(!directRootSigner || !directRootSigner.equals(stream.publicKey))
|
|
2790
|
+
) {
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2506
2793
|
|
|
2507
2794
|
if (pubsubMessage instanceof TopicRootCandidates) {
|
|
2508
2795
|
// Used only to converge deterministic shard-root candidates in auto mode.
|
|
@@ -2524,7 +2811,8 @@ export class TopicControlPlane
|
|
|
2524
2811
|
}
|
|
2525
2812
|
|
|
2526
2813
|
if (pubsubMessage instanceof TopicRootQueryResponse) {
|
|
2527
|
-
|
|
2814
|
+
const senderHash = directRootSigner!.hashcode();
|
|
2815
|
+
this.resolvePendingTopicRootQuery(pubsubMessage, senderHash);
|
|
2528
2816
|
return;
|
|
2529
2817
|
}
|
|
2530
2818
|
|