@dao-xyz/peerbit
Version:
Distributed p2p database on IPFS
802 lines (798 loc) • 36.3 kB
JavaScript
import LazyLevel from "@dao-xyz/lazy-level";
import { Level } from "level";
import { MemoryLevel } from "memory-level";
import { multiaddr } from "@multiformats/multiaddr";
import { createExchangeHeadsMessage, ExchangeHeadsMessage, AbsolutMinReplicas, } from "./exchange-heads.js";
import { Entry, Log } from "@dao-xyz/peerbit-log";
import { serialize, deserialize, BorshError, BinaryReader, BinaryWriter, } from "@dao-xyz/borsh";
import { TransportMessage } from "./message.js";
import { AccessError, Ed25519Keypair, MaybeEncrypted, Ed25519PublicKey, sha256, } from "@dao-xyz/peerbit-crypto";
import { FastKeychain } from "./encryption.js";
import { MaybeSigned } from "@dao-xyz/peerbit-crypto";
import { Program, Address } from "@dao-xyz/peerbit-program";
import PQueue from "p-queue";
import { logger as loggerFn } from "@dao-xyz/peerbit-logger";
import { DirectSub, waitForSubscribers, } from "@dao-xyz/libp2p-direct-sub";
import sodium from "libsodium-wrappers";
import path from "path-browserify";
import { TimeoutError, waitFor } from "@dao-xyz/peerbit-time";
import "@libp2p/peer-id";
import { createLibp2pExtended } from "@dao-xyz/peerbit-libp2p";
import { OBSERVER_TYPE_VARIANT, Replicator, REPLICATOR_TYPE_VARIANT, SubscriptionType, } from "@dao-xyz/peerbit-program";
import { startsWith } from "@dao-xyz/uint8arrays";
import { DirectBlock } from "@dao-xyz/libp2p-direct-block";
import { LevelDatastore } from "datastore-level";
export const logger = loggerFn({ module: "peer" });
const MIN_REPLICAS = 2;
const isLibp2pInstance = (libp2p) => !!libp2p.getMultiaddrs;
const groupByGid = async (entries) => {
const groupByGid = new Map();
for (const head of entries) {
const gid = await (head instanceof Entry
? head.getGid()
: head.entry.getGid());
let value = groupByGid.get(gid);
if (!value) {
value = [];
groupByGid.set(gid, value);
}
value.push(head);
}
return groupByGid;
};
const createLevel = (path) => {
return path
? new Level(path, { valueEncoding: "view" })
: new MemoryLevel({ valueEncoding: "view" });
};
const createCache = async (directory, options) => {
const cache = await new LazyLevel(createLevel(directory));
// "Wake up" the caches if they need it
if (cache)
await cache.open();
if (options?.reset) {
await cache._store.clear();
}
return cache;
};
const createSubCache = async (from, name, options) => {
const cache = await new LazyLevel(from._store.sublevel(name));
// "Wake up" the caches if they need it
if (cache)
await cache.open();
if (options?.reset) {
await cache._store.clear();
}
return cache;
};
export class Peerbit {
_libp2p;
directory;
_minReplicas;
/// program address => Program metadata
programs;
limitSigning;
logs = new Map();
_sortedPeersCache = new Map();
_gidPeersHistory = new Map();
_openProgramQueue;
_disconnected = false;
_disconnecting = false;
_refreshInterval;
_lastSubscriptionMessageId = 0;
_cache;
_libp2pExternal = false;
// Libp2p peerid in Identity form
_identityHash;
_identity;
_keychain; // Keychain + Caching + X25519 keys
constructor(libp2p, options) {
if (libp2p == null) {
throw new Error("Libp2p required");
}
this._libp2p = libp2p;
if (this.libp2p.peerId.type !== "Ed25519") {
throw new Error("Unsupported id type, expecting Ed25519 but got " +
this.libp2p.peerId.type);
}
if (this.libp2p.peerId.type !== "Ed25519") {
throw new Error("Only Ed25519 peerIds are supported");
}
this._identity = options.identity;
this._keychain = options.keychain;
this._identityHash = this._identity.publicKey.hashcode();
this.directory = options.directory;
this.programs = new Map();
this._minReplicas = options.minReplicas || MIN_REPLICAS;
this.limitSigning = options.limitSigning || false;
this._cache = options.cache;
this._libp2pExternal = options.libp2pExternal;
this._openProgramQueue = new PQueue({ concurrency: 1 });
this.libp2p.services.pubsub.addEventListener("data", this._onMessage.bind(this));
this.libp2p.services.pubsub.addEventListener("subscribe", this._onSubscription.bind(this));
this.libp2p.services.pubsub.addEventListener("unsubscribe", this._onUnsubscription.bind(this));
}
static async create(options = {}) {
await sodium.ready; // Some of the modules depends on sodium to be readyy
let libp2pExtended = options.libp2p;
const blocksDirectory = options.directory != null
? path.join(options.directory, "/blocks").toString()
: undefined;
let libp2pExternal = false;
const datastore = options.directory != null
? new LevelDatastore(path.join(options.directory, "/libp2p").toString())
: undefined;
if (datastore) {
await datastore.open();
}
if (!libp2pExtended) {
libp2pExtended = await createLibp2pExtended({
services: {
blocks: (c) => new DirectBlock(c, { directory: blocksDirectory }),
pubsub: (c) => new DirectSub(c),
},
// If directory is passed, we store keys within that directory, else we will use memory datastore (which is the default behaviour)
datastore,
});
}
else {
if (isLibp2pInstance(libp2pExtended)) {
libp2pExternal = true; // libp2p was created outside
}
else {
const extendedOptions = libp2pExtended;
libp2pExtended = await createLibp2pExtended({
...extendedOptions,
services: {
blocks: (c) => new DirectBlock(c, { directory: blocksDirectory }),
pubsub: (c) => new DirectSub(c),
...extendedOptions?.services,
},
datastore,
});
}
}
if (datastore) {
const stopFn = libp2pExtended.stop.bind(libp2pExtended);
libp2pExtended.stop = async () => {
await stopFn();
await datastore?.close();
};
}
if (!libp2pExtended.isStarted()) {
await libp2pExtended.start();
}
if (libp2pExtended.peerId.type !== "Ed25519") {
throw new Error("Unsupported id type, expecting Ed25519 but got " +
libp2pExtended.peerId.type);
}
const directory = options.directory;
const cache = options.cache ||
(await createCache(directory ? path.join(directory, "/cache") : undefined));
const identity = Ed25519Keypair.fromPeerId(libp2pExtended.peerId);
const peer = new Peerbit(libp2pExtended, {
directory,
cache,
libp2pExternal,
limitSigning: options.limitSigning,
minReplicas: options.minReplicas,
refreshIntreval: options.refreshIntreval,
identity,
keychain: await FastKeychain.create(identity, libp2pExtended.keychain),
});
return peer;
}
get libp2p() {
return this._libp2p;
}
get cache() {
return this._cache;
}
get encryption() {
return this._keychain;
}
get disconnected() {
return this._disconnected;
}
get disconnecting() {
return this._disconnecting;
}
get identityHash() {
return this._identityHash;
}
get identity() {
return this._identity;
}
async importKeypair(keypair) {
return this._keychain.importKeypair(keypair);
}
async exportKeypair(publicKey) {
return this._keychain.exportKeypair(publicKey);
}
/**
* Dial a peer with an Ed25519 peerId
*/
async dial(address) {
const maddress = typeof address == "string"
? multiaddr(address)
: address instanceof Peerbit
? address.libp2p.getMultiaddrs()
: address;
const connection = await this.libp2p.dial(maddress);
const publicKey = Ed25519PublicKey.fromPeerId(connection.remotePeer);
// TODO, do this as a promise instead using the onPeerConnected vents in pubsub and blocks
return waitFor(() => this.libp2p.services.pubsub.peers.has(publicKey.hashcode()) &&
this.libp2p.services.blocks.peers.has(publicKey.hashcode()));
}
async stop() {
this._disconnecting = true;
// Close a direct connection and remove it from internal state
this._refreshInterval && clearInterval(this._refreshInterval);
// Close all open databases
await Promise.all([...this.programs.values()].map((program) => program.program.close()));
await this._cache.close();
// Close libp2p (after above)
if (!this._libp2pExternal) {
// only close it if we created it
await this.libp2p.stop();
}
// Remove all databases from the state
this.programs = new Map();
this._disconnecting = false;
this._disconnected = true;
}
// Callback for local writes to the database. We the update to pubsub.
onWrite(_program, log, entry) {
// TODO Should we also do gidHashhistory update here?
createExchangeHeadsMessage(log, [entry], true, this.limitSigning ? undefined : this.identity).then((bytes) => {
this.libp2p.services.pubsub.publish(bytes, { topics: [log.idString] });
});
}
_maybeOpenStorePromise;
// Callback for receiving a message from the network
async _onMessage(evt) {
const message = evt.detail;
/* logger.debug(
`${this.id}: Recieved message on topics: ${
message.topics.length > 1
? "#" + message.topics.length
: message.topics[0]
} ${message.data.length}`
); */
if (message.topics.find((x) => this.logs.has(x)) == null) {
return; // not for me
}
if (this._disconnecting) {
logger.warn("Got message while disconnecting");
return;
}
if (this._disconnected) {
if (!areWeTestingWithJest())
throw new Error("Got message while disconnected");
return; // because these could just be testing sideffects
}
try {
/* const peer =
message.type === "signed"
? (message as SignedPubSubMessage).from
: undefined; */
const maybeEncryptedMessage = deserialize(message.data, MaybeEncrypted);
const decrypted = await maybeEncryptedMessage.decrypt(this.encryption.getAnyKeypair);
const signedMessage = decrypted.getValue(MaybeSigned);
await signedMessage.verify();
const msg = signedMessage.getValue(TransportMessage);
if (msg instanceof ExchangeHeadsMessage) {
/**
* I have recieved heads from someone else.
* I can use them to load associated logs and join/sync them with the data stores I own
*/
const { logId } = msg;
const { heads } = msg;
// replication topic === trustedNetwork address
const idString = Log.createIdString(logId);
logger.debug(`${this.identity.publicKey.hashcode()}: Recieved heads: ${heads.length === 1 ? heads[0].entry.hash : "#" + heads.length}, logId: ${idString}`);
if (heads) {
const logInfo = this.logs.get(idString);
if (!logInfo) {
logger.error("Missing log info, which was expected to exist for " + idString);
return;
}
const filteredHeads = [];
for (const head of heads) {
if (!logInfo.log.has(head.entry.hash)) {
head.entry.init({
// we need to init because we perhaps need to decrypt gid
encryption: logInfo.log.encryption,
encoding: logInfo.log.encoding,
});
filteredHeads.push(head);
}
}
let toMerge;
if (!logInfo.sync) {
toMerge = [];
for (const [gid, value] of await groupByGid(filteredHeads)) {
if (!(await this.isLeader(logInfo.log, gid, logInfo.minReplicas.value))) {
logger.debug(`${this.identity.publicKey.hashcode()}: Dropping heads with gid: ${gid}. Because not leader`);
continue;
}
for (const head of value) {
toMerge.push(head);
}
}
}
else {
toMerge = await Promise.all(filteredHeads.map((x) => logInfo.sync(x.entry))).then((filter) => filteredHeads.filter((v, ix) => filter[ix]));
}
if (toMerge.length > 0) {
await logInfo.log.join(toMerge);
/* TODO does this debug affect performance?
logger.debug(
`${this.id}: Synced ${toMerge.length} heads for '${programAddressObject}/${storeIndex}':\n`,
JSON.stringify(
toMerge.map((e) => e.entry.hash),
null,
2
)
); */
}
}
}
else {
throw new Error("Unexpected message");
}
}
catch (e) {
if (e instanceof BorshError) {
logger.trace(`${this.identity.publicKey.hashcode()}: Failed to handle message on topic: ${JSON.stringify(message.topics)} ${message.data.length}: Got message for a different namespace`);
return;
}
if (e instanceof AccessError) {
logger.trace(`${this.identity.publicKey.hashcode()}: Failed to handle message on topic: ${JSON.stringify(message.topics)} ${message.data.length}: Got message I could not decrypt`);
return;
}
logger.error(e);
}
}
async _onUnsubscription(evt) {
logger.debug(`Peer disconnected '${evt.detail.from.hashcode()}' from '${JSON.stringify(evt.detail.unsubscriptions.map((x) => x.topic))}'`);
return this.handleSubscriptionChange(evt.detail.from.hashcode(), evt.detail.unsubscriptions, false);
}
async _onSubscription(evt) {
logger.debug(`New peer '${evt.detail.from.hashcode()}' connected to '${JSON.stringify(evt.detail.subscriptions.map((x) => x.topic))}'`);
return this.handleSubscriptionChange(evt.detail.from.hashcode(), evt.detail.subscriptions, true);
}
modifySortedSubscriptionCache(topic, subscribed, fromHash) {
const sortedPeer = this._sortedPeersCache.get(topic);
if (sortedPeer) {
const code = fromHash;
if (subscribed) {
// TODO use Set + list for fast lookup
if (!sortedPeer.find((x) => x === code)) {
sortedPeer.push(code);
sortedPeer.sort((a, b) => a.localeCompare(b));
}
}
else {
const deleteIndex = sortedPeer.findIndex((x) => x === code);
sortedPeer.splice(deleteIndex, 1);
}
}
else if (subscribed) {
this._sortedPeersCache.set(topic, [fromHash]);
}
}
async handleSubscriptionChange(fromHash, changes, subscribed) {
for (const c of changes) {
if (!c.data || !startsWith(c.data, REPLICATOR_TYPE_VARIANT)) {
return;
}
this._lastSubscriptionMessageId += 1;
this.modifySortedSubscriptionCache(c.topic, subscribed, fromHash);
}
for (const subscription of changes) {
if (subscription.data) {
try {
const type = deserialize(subscription.data, SubscriptionType);
if (type instanceof Replicator) {
const p = this.logs.get(subscription.topic);
if (p) {
await this.replicationReorganization([p.log.idString]);
}
}
}
catch (error) {
logger.warn("Recieved subscription with invalid data on topic: " +
subscription.topic +
". Error: " +
error?.message);
}
}
}
}
/**
* When a peers join the networkk and want to participate the leaders for particular log subgraphs might change, hence some might start replicating, might some stop
* This method will go through my owned entries, and see whether I should share them with a new leader, and/or I should stop care about specific entries
* @param channel
*/
async replicationReorganization(changedLogs) {
let changed = false;
for (const logId of changedLogs) {
const logInfo = this.logs.get(logId);
if (!logInfo || logInfo.log.closed) {
continue;
}
const heads = await logInfo.log.getHeads();
const groupedByGid = await groupByGid(heads);
let storeChanged = false;
for (const [gid, entries] of groupedByGid) {
const toSend = new Map();
const newPeers = [];
if (entries.length === 0) {
continue; // TODO maybe close store?
}
const oldPeersSet = this._gidPeersHistory.get(gid);
const currentPeers = await this.findLeaders(logInfo.log, gid, logInfo.minReplicas.value);
for (const currentPeer of currentPeers) {
if (!oldPeersSet?.has(currentPeer) &&
currentPeer !== this.identityHash) {
storeChanged = true;
// second condition means that if the new peer is us, we should not do anything, since we are expecting to recieve heads, not send
newPeers.push(currentPeer);
// send heads to the new peer
// console.log('new gid for peer', newPeers.length, this.id.toString(), newPeer, gid, entries.length, newPeers)
try {
logger.debug(`${this.identity.publicKey.hashcode()}: Exchange heads ${entries.length === 1 ? entries[0].hash : "#" + entries.length} on rebalance`);
for (const entry of entries) {
toSend.set(entry.hash, entry);
}
}
catch (error) {
if (error instanceof TimeoutError) {
logger.error("Missing channel when reorg to peer: " +
currentPeer.toString());
continue;
}
throw error;
}
}
}
// We don't need this clause anymore because we got the trim option!
if (!currentPeers.find((x) => x === this.identityHash)) {
let entriesToDelete = entries.filter((e) => !e.createdLocally);
if (logInfo.sync) {
// dont delete entries which we wish to keep
entriesToDelete = await Promise.all(entriesToDelete.map((x) => logInfo.sync(x))).then((filter) => entriesToDelete.filter((v, ix) => !filter[ix]));
}
// delete entries since we are not suppose to replicate this anymore
// TODO add delay? freeze time? (to ensure resiliance for bad io)
if (entriesToDelete.length > 0) {
await logInfo.log.remove(entriesToDelete, {
recursively: true,
});
}
// TODO if length === 0 maybe close store?
}
this._gidPeersHistory.set(gid, new Set(currentPeers));
if (toSend.size === 0) {
continue;
}
const bytes = await createExchangeHeadsMessage(logInfo.log, [...toSend.values()], // TODO send to peers directly
true, this.limitSigning ? undefined : this.identity);
// TODO perhaps send less messages to more recievers for performance reasons?
await this._libp2p.services.pubsub.publish(bytes, {
to: newPeers,
strict: true,
topics: [logInfo.log.idString],
});
}
if (storeChanged) {
await logInfo.log.trim(); // because for entries createdLocally,we can have trim options that still allow us to delete them
}
changed = storeChanged || changed;
}
return changed;
}
/* TODO put this on the program level
getCanTrust(address: Address): CanTrust | undefined {
const p = this.programs.get(address.toString())?.program;
if (p) {
const ct = this.programs.get(address.toString())
?.program as any as CanTrust;
if (ct.isTrusted !== undefined) {
return ct;
}
}
return;
} */
// Callback when a store was closed
async _onClose(program, log) {
// TODO Can we really close a this.programs, either we close all stores in the replication topic or none
const programAddress = program.address?.toString();
logger.debug(`Close ${programAddress}/${log.idString}`);
const logid = log.idString;
const lookup = this.logs.get(logid);
if (lookup) {
lookup.open -= 1;
if (lookup.open === 0) {
this.logs.delete(logid);
await this.unsubscribeToProgram(log); // TODO unsubscribe with 1 role but maybe have another role left?
}
}
}
async _onProgamClose(program, programCache) {
await programCache.close();
const programAddress = program.address?.toString();
if (programAddress) {
this.programs.delete(programAddress);
}
}
addProgram(program) {
const programAddress = program.address?.toString();
if (!programAddress) {
throw new Error("Missing program address");
}
const existingProgramAndStores = this.programs.get(programAddress);
if (!!existingProgramAndStores &&
existingProgramAndStores.program !== program) {
// second condition only makes this throw error if we are to add a new instance with the same address
throw new Error(`Program at ${programAddress} is already created`);
}
const p = {
program,
openCounter: 1,
replicators: new Set(),
};
this.programs.set(programAddress, p);
return p;
}
/* getReplicators(log: Log<any>): string[] | undefined {
let replicators = this.libp2p.services.pubsub.getSubscribersWithData(
log.idString,
REPLICATOR_TYPE_VARIANT,
{ prefix: true }
);
const iAmReplicating = this._logsById.get(log.idString)?.log.replication.replicating; // TODO add conditional whether this represents a network (I am not replicating if I am not trusted (pointless))
if (iAmReplicating) {
replicators = replicators || [];
replicators.push(this.idKeyHash.toString());
}
return replicators;
} */
getReplicatorsSorted(log) {
return this._sortedPeersCache.get(log.idString);
}
getObservers(address) {
return this.libp2p.services.pubsub.getSubscribersWithData(address.toString(), OBSERVER_TYPE_VARIANT, { prefix: true });
}
async isLeader(log, slot, numberOfLeaders) {
const isLeader = (await this.findLeaders(log, slot, numberOfLeaders)).find((l) => l === this.identityHash);
return !!isLeader;
}
async findLeaders(log, subject, numberOfLeaders) {
// For a fixed set or members, the choosen leaders will always be the same (address invariant)
// This allows for that same content is always chosen to be distributed to same peers, to remove unecessary copies
const peers = this.getReplicatorsSorted(log) || [];
// Assumption: Network specification is accurate
// Replication topic is not an address we assume that the network allows all participants
/* TODO put this on the program level
const network = this.getCanTrust(address);
let peers: string[];
if (network) {
const isTrusted = (peer: string) =>
network
? network.isTrusted(
peer // TODO improve perf, caching etc?
)
: true;
peers = await Promise.all(peersPreFilter.map(isTrusted)).then((results) =>
peersPreFilter.filter((_v, index) => results[index])
);
} else {
peers = peersPreFilter;
} */
if (peers.length === 0) {
return [];
}
numberOfLeaders = Math.min(numberOfLeaders, peers.length);
// Convert this thing we wan't to distribute to 8 bytes so we get can convert it into a u64
// modulus into an index
const utf8writer = new BinaryWriter();
utf8writer.string(subject.toString());
const seed = await sha256(utf8writer.finalize());
// convert hash of slot to a number
const seedNumber = new BinaryReader(seed.subarray(seed.length - 8, seed.length)).u64();
const startIndex = Number(seedNumber % BigInt(peers.length));
// we only step forward 1 step (ignoring that step backward 1 could be 'closer')
// This does not matter, we only have to make sure all nodes running the code comes to somewhat the
// same conclusion (are running the same leader selection algorithm)
const leaders = new Array(numberOfLeaders);
for (let i = 0; i < numberOfLeaders; i++) {
leaders[i] = peers[(i + startIndex) % peers.length];
}
return leaders;
}
async subscribeToProgram(log, role) {
if (this._disconnected || this._disconnecting) {
throw new Error("Disconnected");
}
if (role instanceof Replicator) {
this.modifySortedSubscriptionCache(log.idString, true, this.identityHash);
}
this.libp2p.services.pubsub.subscribe(log.idString, {
data: serialize(role),
});
return this.libp2p.services.pubsub.requestSubscribers(log.idString); // get up to date with who are subscribing to this topic
}
async unsubscribeToProgram(id) {
if (this._disconnected) {
throw new Error("Disconnected");
}
this._sortedPeersCache.delete(id.idString);
await this.libp2p.services.pubsub.unsubscribe(id.idString);
}
hasSubscribedToTopic(topic) {
return this.programs.has(topic);
}
/**
* Default behaviour of a store is only to accept heads that are forks (new roots) with some probability
* and to replicate heads (and updates) which is requested by another peer
* @param store
* @param options
* @returns
*/
async open(storeOrAddress, options = {}) {
if (this._disconnected || this._disconnecting) {
throw new Error("Can not open a store while disconnected");
}
const fn = async () => {
// TODO add locks for store lifecycle, e.g. what happens if we try to open and close a store at the same time?
if (typeof storeOrAddress === "string" ||
storeOrAddress instanceof Address) {
storeOrAddress =
storeOrAddress instanceof Address
? storeOrAddress
: Address.parse(storeOrAddress);
}
let program = storeOrAddress;
let existing = false;
if (storeOrAddress instanceof Address ||
typeof storeOrAddress === "string") {
try {
const fromExisting = this.programs?.get(storeOrAddress.toString())
?.program;
if (fromExisting) {
program = fromExisting;
existing = true;
}
else {
program = (await Program.load(this._libp2p.services.blocks, storeOrAddress, options)); // TODO fix typings
if (program instanceof Program === false) {
throw new Error(`Failed to open program because program is of type ${program?.constructor.name} and not ${Program.name}`);
}
}
}
catch (error) {
logger.error("Failed to load store with address: " + storeOrAddress.toString());
throw error;
}
}
if (!program.address && !existing) {
await program.save(this._libp2p.services.blocks);
}
const programAddress = program.address.toString();
if (programAddress) {
const existingProgram = this.programs?.get(programAddress);
if (existingProgram) {
existingProgram.openCounter += 1;
return existingProgram;
}
}
logger.debug(`Open database '${program.constructor.name}`);
const role = options.role || new Replicator();
const minReplicas = options.minReplicas != null
? typeof options.minReplicas === "number"
? new AbsolutMinReplicas(options.minReplicas)
: options.minReplicas
: new AbsolutMinReplicas(this._minReplicas);
let programCache = undefined;
const resolveMinReplicas = (log) => this.logs.get(log.idString).minReplicas.value;
await program.init(this.libp2p, {
onClose: async () => {
return this._onProgamClose(program, programCache);
},
onDrop: () => this._onProgamClose(program, programCache),
role,
// If the program opens more programs
open: (program) => this.open(program, options),
onSave: async (address) => {
programCache = await createSubCache(this._cache, address.toString(), {
reset: options.reset,
});
},
encryption: this.encryption,
waitFor: async (other) => {
await Promise.all(program.logs.map((x) => waitForSubscribers(this.libp2p, other, x.idString)));
},
log: (log) => {
const cfg = {
encryption: this.encryption,
trim: options.trim && {
...options.trim,
filter: {
canTrim: async (gid) => !(await this.isLeader(log, gid, resolveMinReplicas(log))),
cacheId: () => this._lastSubscriptionMessageId,
},
},
cache: async (name) => {
return createSubCache(programCache, // TODO types
path.join("log", name));
},
onClose: async () => {
await this._onClose(program, log);
return options.log?.onClose?.(log);
},
onDrop: async () => {
await this._onClose(program, log);
return options.log?.onClose?.(log);
},
replication: {
replicators: () => {
// TODO Optimize this so we don't have to recreate the array all the time!
const minReplicas = resolveMinReplicas(log);
const replicators = this.getReplicatorsSorted(log);
if (!replicators) {
return []; // No subscribers and we are not replicating
}
const numberOfGroups = Math.min(Math.ceil(replicators.length / minReplicas));
const groups = new Array(numberOfGroups);
for (let i = 0; i < groups.length; i++) {
groups[i] = [];
}
for (let i = 0; i < replicators.length; i++) {
groups[i % numberOfGroups].push(replicators[i]);
}
return groups;
},
replicator: (gid) => this.isLeader(log, gid, resolveMinReplicas(log)),
},
onOpen: async () => {
const logid = log.idString;
const lookup = this.logs.get(logid);
if (lookup) {
lookup.open += 1;
}
else {
await this.subscribeToProgram(log, role);
this.logs.set(logid, {
log,
open: 1,
sync: options.sync,
minReplicas,
});
}
},
onWrite: async (entry) => {
await this.onWrite(program, log, entry);
return options.log?.onWrite?.(log, entry);
},
onChange: async (change) => {
return options?.log?.onChange?.(log, change);
},
};
return cfg;
},
});
return this.addProgram(program);
};
const openStore = await this._openProgramQueue.add(fn);
if (!openStore?.program.address) {
throw new Error("Unexpected");
}
return openStore.program;
}
}
const areWeTestingWithJest = () => {
return process.env.JEST_WORKER_ID !== undefined;
};
//# sourceMappingURL=peer.js.map