peerbit 5.3.6 → 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/src/peer.ts CHANGED
@@ -42,6 +42,12 @@ import sodium from "libsodium-wrappers";
42
42
  import path from "path-browserify";
43
43
  import { concat } from "uint8arrays";
44
44
  import { getBootstrapPeerId, resolveBootstrapAddresses } from "./bootstrap.js";
45
+ import {
46
+ BootstrapRecoveryController,
47
+ type BootstrapRecoveryEventTarget,
48
+ type BootstrapRecoveryOptions,
49
+ validateBootstrapRecoveryOptions,
50
+ } from "./bootstrap-recovery.js";
45
51
  import {
46
52
  type Libp2pCreateOptions as ClientCreateOptions,
47
53
  type Libp2pExtended,
@@ -169,6 +175,12 @@ export type CreateInstanceOptions = (SimpleLibp2pOptions | Libp2pOptions) & {
169
175
  indexer?: (directory?: string) => Promise<Indices> | Indices;
170
176
  storage?: StorageCreateOptions;
171
177
  network?: NativeNetworkCreateOptions;
178
+ /**
179
+ * Opt-in automatic bootstrap recovery. `true` uses bounded defaults; an
180
+ * options object customizes retry policy or fixed targets. It is disabled by
181
+ * default to avoid unexpected network access from `Peerbit.create()`.
182
+ */
183
+ bootstrapRecovery?: boolean | BootstrapRecoveryOptions;
172
184
  } & OptionalCreateOptions;
173
185
 
174
186
  export type DialReadiness =
@@ -193,6 +205,11 @@ export type BootstrapResult = {
193
205
  failures: BootstrapFailure[];
194
206
  };
195
207
 
208
+ export type BootstrapDialOptions = {
209
+ dialTimeoutMs?: number;
210
+ signal?: AbortSignal;
211
+ };
212
+
196
213
  const isLibp2pInstance = (libp2p: Libp2pExtended | ClientCreateOptions) =>
197
214
  !!(libp2p as Libp2p).getMultiaddrs;
198
215
 
@@ -210,6 +227,17 @@ const createCache = async (
210
227
  return cache;
211
228
  };
212
229
 
230
+ const getOnlineEventTarget = (): BootstrapRecoveryEventTarget | undefined => {
231
+ const target = globalThis as unknown as Partial<BootstrapRecoveryEventTarget>;
232
+ return typeof target.addEventListener === "function" &&
233
+ typeof target.removeEventListener === "function"
234
+ ? (target as BootstrapRecoveryEventTarget)
235
+ : undefined;
236
+ };
237
+
238
+ const isEnvironmentOnline = (): boolean =>
239
+ typeof navigator === "undefined" || navigator.onLine !== false;
240
+
213
241
  const SELF_IDENTITY_KEY_ID = new Uint8Array([
214
242
  95, 95, 115, 101, 108, 102, 95, 95,
215
243
  ]); // new TextEncoder().encode("__self__");
@@ -223,6 +251,11 @@ export class Peerbit implements ProgramClient {
223
251
  private _indexer: Indices;
224
252
  private _libp2pExternal?: boolean = false;
225
253
  private _nativeNetwork?: NativeNetworkRuntime;
254
+ private _bootstrapRecoveryOptions?: BootstrapRecoveryOptions;
255
+ private _bootstrapRecovery?: BootstrapRecoveryController;
256
+ private _bootstrapRecoveryGeneration = 0;
257
+ private _bootstrapRecoveryTransition: Promise<void> = Promise.resolve();
258
+ private _bootstrapRecoveryPaused = false;
226
259
 
227
260
  /**
228
261
  * Native shared-log defaults advertised to programs opened on this
@@ -266,6 +299,14 @@ export class Peerbit implements ProgramClient {
266
299
  }
267
300
 
268
301
  static async create(options: CreateInstanceOptions = {}): Promise<Peerbit> {
302
+ const bootstrapRecoveryOptions = options.bootstrapRecovery;
303
+ if (
304
+ bootstrapRecoveryOptions &&
305
+ bootstrapRecoveryOptions !== true &&
306
+ bootstrapRecoveryOptions.enabled !== false
307
+ ) {
308
+ validateBootstrapRecoveryOptions(bootstrapRecoveryOptions);
309
+ }
269
310
  await sodium.ready; // Some of the modules depends on sodium to be readyy
270
311
 
271
312
  let libp2pExtended: Libp2pExtended | undefined = (options as Libp2pOptions)
@@ -594,6 +635,14 @@ export class Peerbit implements ProgramClient {
594
635
  nativeNetwork,
595
636
  sharedLogNativeDefaults,
596
637
  });
638
+ if (options.bootstrapRecovery === true) {
639
+ peer.enableBootstrapRecovery();
640
+ } else if (
641
+ options.bootstrapRecovery &&
642
+ options.bootstrapRecovery.enabled !== false
643
+ ) {
644
+ peer.enableBootstrapRecovery(options.bootstrapRecovery);
645
+ }
597
646
  return peer;
598
647
  }
599
648
  get libp2p(): Libp2pExtended {
@@ -734,16 +783,100 @@ export class Peerbit implements ProgramClient {
734
783
  // TODO wait for pubsub and blocks to disconnect?
735
784
  }
736
785
 
786
+ get bootstrapRecoveryEnabled(): boolean {
787
+ return this._bootstrapRecoveryOptions != null;
788
+ }
789
+
790
+ /** Enable automatic recovery and schedule an immediate attempt if disconnected. */
791
+ enableBootstrapRecovery(options: BootstrapRecoveryOptions = {}): void {
792
+ if (options.enabled === false) {
793
+ this.disableBootstrapRecovery();
794
+ return;
795
+ }
796
+ validateBootstrapRecoveryOptions(options);
797
+ this._bootstrapRecoveryOptions = {
798
+ ...options,
799
+ enabled: true,
800
+ addresses: options.addresses ? [...options.addresses] : undefined,
801
+ };
802
+ this.transitionBootstrapRecovery(true);
803
+ }
804
+
805
+ /** Disable recovery, remove environment listeners, and abort its active dial. */
806
+ disableBootstrapRecovery(): void {
807
+ this._bootstrapRecoveryOptions = undefined;
808
+ this.transitionBootstrapRecovery(false);
809
+ }
810
+
811
+ private transitionBootstrapRecovery(startAfterDrain: boolean): Promise<void> {
812
+ const generation = ++this._bootstrapRecoveryGeneration;
813
+ const previous = this._bootstrapRecovery;
814
+ this._bootstrapRecovery = undefined;
815
+ const drain = previous?.stop() ?? Promise.resolve();
816
+ const transition = Promise.all([
817
+ this._bootstrapRecoveryTransition,
818
+ drain,
819
+ ]).then(() => {
820
+ if (
821
+ startAfterDrain &&
822
+ generation === this._bootstrapRecoveryGeneration
823
+ ) {
824
+ this.startBootstrapRecovery();
825
+ }
826
+ });
827
+ this._bootstrapRecoveryTransition = transition.catch((error) => {
828
+ logger.error(`Bootstrap recovery lifecycle transition failed: ${error}`);
829
+ });
830
+ return this._bootstrapRecoveryTransition;
831
+ }
832
+
833
+ private startBootstrapRecovery(): void {
834
+ const options = this._bootstrapRecoveryOptions;
835
+ if (
836
+ !options ||
837
+ this._bootstrapRecovery ||
838
+ this._bootstrapRecoveryPaused ||
839
+ this.libp2p.status !== "started"
840
+ ) {
841
+ return;
842
+ }
843
+ const controller = new BootstrapRecoveryController(
844
+ {
845
+ bootstrap: (signal) => {
846
+ const addresses = this._bootstrapRecoveryOptions?.addresses;
847
+ return this.bootstrap(addresses ? [...addresses] : undefined, {
848
+ signal,
849
+ });
850
+ },
851
+ connectionEvents: this
852
+ .libp2p as unknown as BootstrapRecoveryEventTarget,
853
+ onlineEvents: getOnlineEventTarget(),
854
+ isConnected: () => this.libp2p.getConnections().length > 0,
855
+ isOnline: isEnvironmentOnline,
856
+ },
857
+ options,
858
+ );
859
+ this._bootstrapRecovery = controller;
860
+ controller.start();
861
+ }
862
+
737
863
  async start() {
864
+ this._handler?.assertCanStart();
738
865
  await this._storage.open();
739
866
  await this.indexer.start();
740
867
 
741
868
  if (this.libp2p.status === "stopped" || this.libp2p.status === "stopping") {
742
869
  this._libp2pExternal = false; // this means we will also close libp2p client on close
743
- return this.libp2p.start();
870
+ await this.libp2p.start();
744
871
  }
872
+ this._bootstrapRecoveryPaused = false;
873
+ await this._bootstrapRecoveryTransition;
874
+ this._handler?.start();
875
+ this.startBootstrapRecovery();
745
876
  }
746
877
  async stop() {
878
+ this._bootstrapRecoveryPaused = true;
879
+ await this.transitionBootstrapRecovery(false);
747
880
  await this._handler?.stop();
748
881
  await this._storage.close();
749
882
  await this.indexer.stop();
@@ -755,8 +888,16 @@ export class Peerbit implements ProgramClient {
755
888
  }
756
889
  }
757
890
 
758
- async bootstrap(addresses?: string[] | Multiaddr[]) {
759
- const _addresses = addresses ?? (await resolveBootstrapAddresses());
891
+ async bootstrap(
892
+ addresses?: Array<string | Multiaddr>,
893
+ options: BootstrapDialOptions = {},
894
+ ) {
895
+ if (options.signal?.aborted) {
896
+ throw options.signal.reason ?? new Error("Bootstrap aborted");
897
+ }
898
+ const _addresses =
899
+ addresses ??
900
+ (await resolveBootstrapAddresses("5", { signal: options.signal }));
760
901
  if (_addresses.length === 0) {
761
902
  throw new Error("Failed to find any addresses to dial");
762
903
  }
@@ -785,7 +926,7 @@ export class Peerbit implements ProgramClient {
785
926
  byPeerId.set(pid, list);
786
927
  }
787
928
 
788
- const dialTimeoutMs = 30_000;
929
+ const dialTimeoutMs = options.dialTimeoutMs ?? 30_000;
789
930
  const scoreBootstrapAddr = (a: Multiaddr) => {
790
931
  const s = a.toString();
791
932
  const isCircuit = s.includes("p2p-circuit");
@@ -803,7 +944,11 @@ export class Peerbit implements ProgramClient {
803
944
  // Bootstrap nodes are rendezvous points; they may not immediately satisfy
804
945
  // "services" readiness (pubsub/blocks/fanout neighbor checks), especially in
805
946
  // browser runtimes. A successful transport-level dial is enough here.
806
- await this.dial(ma, { dialTimeoutMs, readiness: "connection" });
947
+ await this.dial(ma, {
948
+ dialTimeoutMs,
949
+ readiness: "connection",
950
+ signal: options.signal,
951
+ });
807
952
  return true;
808
953
  } catch (e) {
809
954
  lastError = e;
@@ -832,11 +977,15 @@ export class Peerbit implements ProgramClient {
832
977
  promise: this.dial(typeof a === "string" ? multiaddr(a) : a, {
833
978
  dialTimeoutMs,
834
979
  readiness: "connection",
980
+ signal: options.signal,
835
981
  }),
836
982
  });
837
983
  }
838
984
 
839
985
  const settled = await Promise.allSettled(dialTasks.map((t) => t.promise));
986
+ if (options.signal?.aborted) {
987
+ throw options.signal.reason ?? new Error("Bootstrap aborted");
988
+ }
840
989
  let once = false;
841
990
  const connectedPeerIds = new Set<string>();
842
991
  const failures: BootstrapFailure[] = [];