added gossipsub in parallel with zeromq. converted project to ESM for compatibility with gossipsub
This commit is contained in:
parent
940c3212be
commit
44e6cfa13c
|
@ -1,5 +1,5 @@
|
|||
import {Delta, DeltaFilter} from '../src/delta';
|
||||
import {Lossless} from '../src/lossless';
|
||||
import {Delta, DeltaFilter} from '../src/delta.js';
|
||||
import {Lossless} from '../src/lossless.js';
|
||||
|
||||
describe('Lossless', () => {
|
||||
it('creates a lossless view of keanu as neo in the matrix', () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {Delta, PointerTarget} from "../src/delta";
|
||||
import {Lossless, LosslessViewMany} from "../src/lossless";
|
||||
import {Lossy, lastValueFromLosslessViewOne, valueFromCollapsedDelta, ResolvedViewMany} from "../src/lossy";
|
||||
import {Delta, PointerTarget} from "../src/delta.js";
|
||||
import {Lossless, LosslessViewMany} from "../src/lossless.js";
|
||||
import {Lossy, lastValueFromLosslessViewOne, valueFromCollapsedDelta } from "../src/lossy.js";
|
||||
|
||||
describe('Lossy', () => {
|
||||
describe('se a provided function to resolve entity views', () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {PeerAddress} from '../src/types';
|
||||
import {PeerAddress} from '../src/types.js';
|
||||
|
||||
describe('PeerAddress', () => {
|
||||
it('toString()', () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {App} from "../../util/app";
|
||||
import {App} from "../../util/app.js";
|
||||
|
||||
describe('Run', () => {
|
||||
let app: App;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Debug from 'debug';
|
||||
import {App} from '../../util/app';
|
||||
import {App} from '../../util/app.js';
|
||||
const debug = Debug('test:two');
|
||||
|
||||
describe('Run', () => {
|
||||
|
@ -13,6 +13,8 @@ describe('Run', () => {
|
|||
apps[1] = new App({
|
||||
httpEnable: true,
|
||||
peerId: 'app1',
|
||||
// Make the apps use the same pubsub topic so they can talk to each other
|
||||
pubSubTopic: apps[0].config.pubSubTopic,
|
||||
});
|
||||
apps[0].config.seedPeers.push(apps[1].myRequestAddr);
|
||||
apps[1].config.seedPeers.push(apps[0].myRequestAddr);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Debug from 'debug';
|
||||
import {Collection} from "../src/collection";
|
||||
import {Entity} from "../src/entity";
|
||||
import {RhizomeNode} from "../src/node";
|
||||
import {Collection} from "../src/collection.js";
|
||||
import {Entity} from "../src/entity.js";
|
||||
import {RhizomeNode} from "../src/node.js";
|
||||
const debug = Debug('example-app');
|
||||
|
||||
// As an app we want to be able to write and read data.
|
||||
|
|
File diff suppressed because it is too large
Load Diff
37
package.json
37
package.json
|
@ -2,11 +2,12 @@
|
|||
"name": "rhizome-node",
|
||||
"version": "0.1.0",
|
||||
"description": "Rhizomatic database engine node",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build:watch": "tsc --watch",
|
||||
"lint": "eslint",
|
||||
"test": "jest",
|
||||
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
||||
"coverage": "./scripts/coverage.sh",
|
||||
"example-app": "node dist/examples/app.js"
|
||||
},
|
||||
|
@ -18,15 +19,38 @@
|
|||
],
|
||||
"testMatch": [
|
||||
"**/__tests__/**/*"
|
||||
]
|
||||
],
|
||||
"verbose": true,
|
||||
"transform": {
|
||||
"^.+\\.ts?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
"useESM": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"extensionsToTreatAsEsm": [
|
||||
".ts"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^(\\.{1,2}/.*)\\.js$": "$1"
|
||||
}
|
||||
},
|
||||
"author": "Taliesin (Ladd) <ladd@dgov.io>",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"@chainsafe/libp2p-gossipsub": "^14.1.0",
|
||||
"@chainsafe/libp2p-noise": "^16.0.0",
|
||||
"@chainsafe/libp2p-yamux": "^7.0.1",
|
||||
"@libp2p/identify": "^3.0.14",
|
||||
"@libp2p/mdns": "^11.0.16",
|
||||
"@libp2p/ping": "^2.0.14",
|
||||
"@libp2p/tcp": "^10.0.14",
|
||||
"debug": "^4.4.0",
|
||||
"express": "^4.21.2",
|
||||
"json-logic-js": "^2.0.5",
|
||||
"level": "^9.0.0",
|
||||
"libp2p": "^2.4.2",
|
||||
"microtime": "^3.1.1",
|
||||
"object-hash": "^3.0.0",
|
||||
"showdown": "^2.1.0",
|
||||
|
@ -34,20 +58,21 @@
|
|||
"zeromq": "^6.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@types/bluebird": "^3.5.42",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/json-logic-js": "^2.0.8",
|
||||
"@types/microtime": "^2.1.2",
|
||||
"@types/object-hash": "^3.0.6",
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/json-logic-js": "^2.0.8",
|
||||
"@types/microtime": "^2.1.2",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/object-hash": "^3.0.6",
|
||||
"@types/showdown": "^2.0.6",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-airbnb-base-typescript": "^1.1.0",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"tsc-alias": "^1.8.10",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.18.0"
|
||||
}
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
import Debug from 'debug';
|
||||
import {randomUUID} from "node:crypto";
|
||||
import EventEmitter from "node:events";
|
||||
import {Delta, DeltaFilter} from "./delta";
|
||||
import {Entity, EntityProperties} from "./entity";
|
||||
import {Lossy, ResolvedViewOne, Resolver} from "./lossy";
|
||||
import {RhizomeNode} from "./node";
|
||||
import {DomainEntityID} from "./types";
|
||||
import {Delta, DeltaFilter} from "./delta.js";
|
||||
import {Entity, EntityProperties} from "./entity.js";
|
||||
import {Lossy, ResolvedViewOne, Resolver} from "./lossy.js";
|
||||
import {RhizomeNode} from "./node.js";
|
||||
import {DomainEntityID} from "./types.js";
|
||||
const debug = Debug('collection');
|
||||
|
||||
export class Collection {
|
||||
|
@ -214,9 +214,10 @@ export class Collection {
|
|||
resolver?: Resolver,
|
||||
deltaFilter?: DeltaFilter
|
||||
): T | undefined {
|
||||
if (!this.rhizomeNode) return undefined;
|
||||
if (!this.rhizomeNode) throw new Error('collection not connected to rhizome');
|
||||
if (!this.lossy) throw new Error('lossy view not initialized');
|
||||
|
||||
const res = this.lossy?.resolve(resolver, [id], deltaFilter) || {};
|
||||
const res = this.lossy.resolve(resolver, [id], deltaFilter) || {};
|
||||
|
||||
return res[id] as T;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {randomUUID} from "crypto";
|
||||
import {PeerAddress} from "./types";
|
||||
import {PeerAddress} from "./types.js";
|
||||
|
||||
// _HOST refers to the address from an external perspective
|
||||
// _ADDR refers to the interface address from the service's perspective
|
||||
|
@ -20,3 +20,5 @@ export const HTTP_API_ENABLE = process.env.RHIZOME_HTTP_API_ENABLE === 'true';
|
|||
export const SEED_PEERS: PeerAddress[] = (process.env.RHIZOME_SEED_PEERS || '').split(',')
|
||||
.filter(x => !!x)
|
||||
.map((peer: string) => PeerAddress.fromString(peer));
|
||||
|
||||
export const PUB_SUB_TOPIC = process.env.RHIZOME_PUB_SUB_TOPIC || `deltas-${randomUUID()}`;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {randomUUID} from "crypto";
|
||||
import microtime from 'microtime';
|
||||
import {CreatorID, HostID, PeerAddress, Timestamp, TransactionID} from "./types";
|
||||
import {CreatorID, HostID, PeerAddress, Timestamp, TransactionID} from "./types.js";
|
||||
|
||||
export type DeltaID = string;
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import Debug from 'debug';
|
||||
import EventEmitter from 'node:events';
|
||||
import objectHash from 'object-hash';
|
||||
import {Delta, DeltaNetworkImage} from './delta';
|
||||
import {RhizomeNode} from './node';
|
||||
import {Delta, DeltaNetworkImage} from './delta.js';
|
||||
import {RhizomeNode} from './node.js';
|
||||
const debug = Debug('deltas');
|
||||
|
||||
enum Decision {
|
||||
|
@ -87,7 +87,10 @@ export class DeltaStream {
|
|||
|
||||
async publishDelta(delta: Delta) {
|
||||
debug(`Publishing delta: ${JSON.stringify(delta)}`);
|
||||
await this.rhizomeNode.pubSub.publish("deltas", this.serializeDelta(delta));
|
||||
await this.rhizomeNode.pubSub.publish(
|
||||
this.rhizomeNode.config.pubSubTopic,
|
||||
this.serializeDelta(delta)
|
||||
);
|
||||
}
|
||||
|
||||
serializeDelta(delta: Delta): string {
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
// - As typescript interfaces?
|
||||
// - As typescript classes?
|
||||
|
||||
import {Collection} from "./collection";
|
||||
import {PropertyTypes} from "./types";
|
||||
import {Collection} from "./collection.js";
|
||||
import {PropertyTypes} from "./types.js";
|
||||
|
||||
export type EntityProperties = {
|
||||
[key: string]: PropertyTypes;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { add_operation, apply } from 'json-logic-js';
|
||||
import { Delta } from '../delta';
|
||||
import { Delta } from '../delta.js';
|
||||
|
||||
type DeltaContext = Delta & {
|
||||
creatorAddress: string;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { FilterExpr } from "../types";
|
||||
import { FilterExpr } from "../types.js";
|
||||
// import { map } from 'radash';
|
||||
|
||||
// A creator as seen by a host
|
||||
|
@ -30,4 +30,4 @@ export function generateFilter(): FilterExpr {
|
|||
return {
|
||||
'in': ['$creatorAddress', addresses]
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -30,24 +30,25 @@ export class HttpApi {
|
|||
|
||||
// Get the list of peers seen by this node (including itself)
|
||||
this.router.get("/peers", (_req: express.Request, res: express.Response) => {
|
||||
res.json(this.rhizomeNode.peers.peers.map(({reqAddr, publishAddr, isSelf, isSeedPeer}) => {
|
||||
const deltasAcceptedCount = this.rhizomeNode.deltaStream.deltasAccepted
|
||||
.filter((delta: Delta) => {
|
||||
return delta.receivedFrom?.addr == reqAddr.addr &&
|
||||
delta.receivedFrom?.port == reqAddr.port;
|
||||
})
|
||||
.length;
|
||||
const peerInfo = {
|
||||
reqAddr: reqAddr.toAddrString(),
|
||||
publishAddr: publishAddr?.toAddrString(),
|
||||
isSelf,
|
||||
isSeedPeer,
|
||||
deltaCount: {
|
||||
accepted: deltasAcceptedCount
|
||||
}
|
||||
};
|
||||
return peerInfo;
|
||||
}));
|
||||
res.json(this.rhizomeNode.peers.peers.map(
|
||||
({reqAddr, publishAddr, isSelf, isSeedPeer}) => {
|
||||
const deltasAcceptedCount = this.rhizomeNode.deltaStream.deltasAccepted
|
||||
.filter((delta: Delta) => {
|
||||
return delta.receivedFrom?.addr == reqAddr.addr &&
|
||||
delta.receivedFrom?.port == reqAddr.port;
|
||||
})
|
||||
.length;
|
||||
const peerInfo = {
|
||||
reqAddr: reqAddr.toAddrString(),
|
||||
publishAddr: publishAddr?.toAddrString(),
|
||||
isSelf,
|
||||
isSeedPeer,
|
||||
deltaCount: {
|
||||
accepted: deltasAcceptedCount
|
||||
}
|
||||
};
|
||||
return peerInfo;
|
||||
}));
|
||||
});
|
||||
|
||||
// Get the number of peers seen by this node (including itself)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import express, {Router} from "express";
|
||||
import {htmlDocFromMarkdown, MDFiles} from "../util/md-files";
|
||||
import {htmlDocFromMarkdown, MDFiles} from "../util/md-files.js";
|
||||
|
||||
export class HttpHtml {
|
||||
router = Router();
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import Debug from "debug";
|
||||
import express from "express";
|
||||
import {Server} from "http";
|
||||
import {RhizomeNode} from "../node";
|
||||
import {HttpApi} from "./api";
|
||||
import {HttpHtml} from "./html";
|
||||
import {RhizomeNode} from "../node.js";
|
||||
import {HttpApi} from "./api.js";
|
||||
import {HttpHtml} from "./html.js";
|
||||
const debug = Debug('http-api');
|
||||
|
||||
export class HttpServer {
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
|
||||
import Debug from 'debug';
|
||||
import EventEmitter from 'events';
|
||||
import {Delta, DeltaFilter, DeltaNetworkImage} from './delta';
|
||||
import {Transactions} from './transactions';
|
||||
import {DomainEntityID, PropertyID, PropertyTypes, TransactionID, ViewMany} from "./types";
|
||||
import {Delta, DeltaFilter, DeltaNetworkImage} from './delta.js';
|
||||
import {Transactions} from './transactions.js';
|
||||
import {DomainEntityID, PropertyID, PropertyTypes, TransactionID, ViewMany} from "./types.js";
|
||||
const debug = Debug('lossless');
|
||||
|
||||
export type CollapsedPointer = {[key: PropertyID]: PropertyTypes};
|
||||
|
@ -141,6 +141,7 @@ export class Lossless {
|
|||
if (delta.transactionId) {
|
||||
if (!this.transactions.isComplete(delta.transactionId)) {
|
||||
// TODO: Test this condition
|
||||
debug(`excluding delta ${delta.id} because transaction ${delta.transactionId} is not completed`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
10
src/lossy.ts
10
src/lossy.ts
|
@ -6,9 +6,9 @@
|
|||
// Fields in the output can be described as transformations
|
||||
|
||||
import Debug from 'debug';
|
||||
import {DeltaFilter} from "./delta";
|
||||
import {CollapsedDelta, Lossless, LosslessViewMany, LosslessViewOne} from "./lossless";
|
||||
import {DomainEntityID, PropertyID, PropertyTypes, Timestamp, ViewMany} from "./types";
|
||||
import {DeltaFilter} from "./delta.js";
|
||||
import {CollapsedDelta, Lossless, LosslessViewMany, LosslessViewOne} from "./lossless.js";
|
||||
import {DomainEntityID, PropertyID, PropertyTypes, Timestamp, ViewMany} from "./types.js";
|
||||
const debug = Debug('lossy');
|
||||
|
||||
type TimestampedProperty = {
|
||||
|
@ -77,14 +77,14 @@ export function lastValueFromLosslessViewOne(
|
|||
function defaultResolver(losslessView: LosslessViewMany): ResolvedViewMany {
|
||||
const resolved: ResolvedViewMany = {};
|
||||
|
||||
// debug('default resolver, lossless view', JSON.stringify(losslessView));
|
||||
debug('default resolver, lossless view', JSON.stringify(losslessView));
|
||||
for (const [id, ent] of Object.entries(losslessView)) {
|
||||
resolved[id] = {id, properties: {}};
|
||||
|
||||
for (const key of Object.keys(ent.properties)) {
|
||||
const {value} = lastValueFromLosslessViewOne(ent, key) || {};
|
||||
|
||||
// debug(`[ ${key} ] = ${value}`);
|
||||
debug(`[ ${key} ] = ${value}`);
|
||||
resolved[id].properties[key] = value;
|
||||
}
|
||||
}
|
||||
|
|
23
src/node.ts
23
src/node.ts
|
@ -1,12 +1,12 @@
|
|||
import Debug from 'debug';
|
||||
import {CREATOR, HTTP_API_ADDR, HTTP_API_ENABLE, HTTP_API_PORT, PEER_ID, PUBLISH_BIND_ADDR, PUBLISH_BIND_HOST, PUBLISH_BIND_PORT, REQUEST_BIND_ADDR, REQUEST_BIND_HOST, REQUEST_BIND_PORT, SEED_PEERS} from './config';
|
||||
import {DeltaStream} from './deltas';
|
||||
import {HttpServer} from './http';
|
||||
import {Lossless} from './lossless';
|
||||
import {Peers} from './peers';
|
||||
import {PubSub} from './pub-sub';
|
||||
import {RequestReply} from './request-reply';
|
||||
import {PeerAddress} from './types';
|
||||
import {CREATOR, HTTP_API_ADDR, HTTP_API_ENABLE, HTTP_API_PORT, PEER_ID, PUB_SUB_TOPIC, PUBLISH_BIND_ADDR, PUBLISH_BIND_HOST, PUBLISH_BIND_PORT, REQUEST_BIND_ADDR, REQUEST_BIND_HOST, REQUEST_BIND_PORT, SEED_PEERS} from './config.js';
|
||||
import {DeltaStream} from './deltas.js';
|
||||
import {HttpServer} from './http/index.js';
|
||||
import {Lossless} from './lossless.js';
|
||||
import {Peers} from './peers.js';
|
||||
import {PubSub} from './pub-sub.js';
|
||||
import {RequestReply} from './request-reply.js';
|
||||
import {PeerAddress} from './types.js';
|
||||
const debug = Debug('rhizome-node');
|
||||
|
||||
export type RhizomeNodeConfig = {
|
||||
|
@ -22,6 +22,7 @@ export type RhizomeNodeConfig = {
|
|||
seedPeers: PeerAddress[];
|
||||
peerId: string;
|
||||
creator: string; // TODO each host should be able to support multiple users
|
||||
pubSubTopic: string;
|
||||
};
|
||||
|
||||
// So that we can run more than one instance in the same process (for testing)
|
||||
|
@ -50,6 +51,7 @@ export class RhizomeNode {
|
|||
seedPeers: SEED_PEERS,
|
||||
peerId: PEER_ID,
|
||||
creator: CREATOR,
|
||||
pubSubTopic: PUB_SUB_TOPIC,
|
||||
...config
|
||||
};
|
||||
debug('config', this.config);
|
||||
|
@ -74,7 +76,7 @@ export class RhizomeNode {
|
|||
this.deltaStream.subscribeDeltas((delta) => this.lossless.ingestDelta(delta));
|
||||
|
||||
// Start ZeroMQ publish and reply sockets
|
||||
this.pubSub.start();
|
||||
await this.pubSub.start();
|
||||
this.requestReply.start();
|
||||
|
||||
// Start HTTP server
|
||||
|
@ -82,6 +84,9 @@ export class RhizomeNode {
|
|||
this.httpServer.start();
|
||||
}
|
||||
|
||||
// Start libp2p subscription
|
||||
this.peers.start();
|
||||
|
||||
// Wait a short time for sockets to initialize
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
|
|
27
src/peers.ts
27
src/peers.ts
|
@ -1,11 +1,11 @@
|
|||
import Debug from 'debug';
|
||||
import {Message} from 'zeromq';
|
||||
import {SEED_PEERS} from "./config";
|
||||
import {Delta} from "./delta";
|
||||
import {RhizomeNode} from "./node";
|
||||
import {Subscription} from './pub-sub';
|
||||
import {PeerRequest, RequestSocket, ResponseSocket} from "./request-reply";
|
||||
import {PeerAddress} from "./types";
|
||||
import {SEED_PEERS} from "./config.js";
|
||||
import {Delta} from "./delta.js";
|
||||
import {RhizomeNode} from "./node.js";
|
||||
import {Subscription} from './pub-sub.js';
|
||||
import {PeerRequest, RequestSocket, ResponseSocket} from "./request-reply.js";
|
||||
import {PeerAddress} from "./types.js";
|
||||
const debug = Debug('peers');
|
||||
|
||||
export enum RequestMethods {
|
||||
|
@ -46,9 +46,9 @@ class Peer {
|
|||
|
||||
this.subscription = this.rhizomeNode.pubSub.subscribe(
|
||||
this.publishAddr,
|
||||
"deltas",
|
||||
this.rhizomeNode.config.pubSubTopic,
|
||||
(sender, msg) => {
|
||||
const delta = this.rhizomeNode.deltaStream.deserializeDelta(msg.toString());
|
||||
const delta = this.rhizomeNode.deltaStream.deserializeDelta(msg);
|
||||
delta.receivedFrom = sender;
|
||||
debug(`Received delta: ${JSON.stringify(delta)}`);
|
||||
this.rhizomeNode.deltaStream.ingestDelta(delta);
|
||||
|
@ -99,7 +99,18 @@ export class Peers {
|
|||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
start() {
|
||||
this.rhizomeNode.pubSub.subscribeTopic(
|
||||
this.rhizomeNode.config.pubSubTopic,
|
||||
(sender, msg) => {
|
||||
const delta = this.rhizomeNode.deltaStream.deserializeDelta(msg);
|
||||
delta.receivedFrom = sender;
|
||||
debug(`Received delta: ${JSON.stringify(delta)}`);
|
||||
this.rhizomeNode.deltaStream.ingestDelta(delta);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
addPeer(addr: PeerAddress): Peer {
|
||||
|
|
123
src/pub-sub.ts
123
src/pub-sub.ts
|
@ -1,10 +1,18 @@
|
|||
import {GossipSub, gossipsub} from '@chainsafe/libp2p-gossipsub';
|
||||
import {noise} from '@chainsafe/libp2p-noise';
|
||||
import {yamux} from '@chainsafe/libp2p-yamux';
|
||||
import {identify} from '@libp2p/identify';
|
||||
import {mdns} from '@libp2p/mdns';
|
||||
import {ping} from '@libp2p/ping';
|
||||
import {tcp} from '@libp2p/tcp';
|
||||
import Debug from 'debug';
|
||||
import {Message, Publisher, Subscriber} from 'zeromq';
|
||||
import {RhizomeNode} from './node';
|
||||
import {PeerAddress} from './types';
|
||||
import {Libp2p, createLibp2p} from 'libp2p';
|
||||
import {Publisher, Subscriber} from 'zeromq';
|
||||
import {RhizomeNode} from './node.js';
|
||||
import {PeerAddress} from './types.js';
|
||||
const debug = Debug('pub-sub');
|
||||
|
||||
export type SubscribedMessageHandler = (sender: PeerAddress, msg: Message) => void;
|
||||
export type SubscribedMessageHandler = (sender: PeerAddress, msg: string) => void;
|
||||
|
||||
// TODO: Allow subscribing to multiple topics on one socket
|
||||
export class Subscription {
|
||||
|
@ -13,22 +21,33 @@ export class Subscription {
|
|||
publishAddr: PeerAddress;
|
||||
publishAddrStr: string;
|
||||
cb: SubscribedMessageHandler;
|
||||
libp2p?: Libp2p;
|
||||
|
||||
constructor(publishAddr: PeerAddress, topic: string, cb: SubscribedMessageHandler) {
|
||||
constructor(
|
||||
publishAddr: PeerAddress,
|
||||
topic: string,
|
||||
cb: SubscribedMessageHandler,
|
||||
libp2p?: Libp2p
|
||||
) {
|
||||
this.cb = cb;
|
||||
this.topic = topic;
|
||||
this.publishAddr = publishAddr;
|
||||
this.publishAddrStr = `tcp://${this.publishAddr.toAddrString()}`;
|
||||
this.libp2p = libp2p;
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.sock.connect(this.publishAddrStr);
|
||||
this.sock.subscribe(this.topic);
|
||||
debug(`Subscribing to ${this.topic} topic on ${this.publishAddrStr}`);
|
||||
debug(`Subscribing to ${this.topic} topic on ZeroMQ ${this.publishAddrStr}`);
|
||||
|
||||
// Wait for ZeroMQ messages.
|
||||
// This will block indefinitely.
|
||||
for await (const [, sender, msg] of this.sock) {
|
||||
const senderAddr = PeerAddress.fromString(sender.toString());
|
||||
this.cb(senderAddr, msg);
|
||||
const senderStr = PeerAddress.fromString(sender.toString());
|
||||
const msgStr = msg.toString();
|
||||
debug(`ZeroMQ subscribtion received msg: ${msgStr}`);
|
||||
this.cb(senderStr, msgStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +57,7 @@ export class PubSub {
|
|||
publishSock: Publisher;
|
||||
publishAddrStr: string;
|
||||
subscriptions: Subscription[] = [];
|
||||
libp2p?: Libp2p;
|
||||
|
||||
constructor(rhizomeNode: RhizomeNode) {
|
||||
this.rhizomeNode = rhizomeNode;
|
||||
|
@ -49,19 +69,82 @@ export class PubSub {
|
|||
|
||||
async start() {
|
||||
await this.publishSock.bind(this.publishAddrStr);
|
||||
debug(`Publishing socket bound to ${this.publishAddrStr}`);
|
||||
debug(`ZeroMQ publishing socket bound to ${this.publishAddrStr}`);
|
||||
|
||||
this.libp2p = await createLibp2p({
|
||||
addresses: {
|
||||
// TODO: Config
|
||||
listen: ['/ip4/127.0.0.1/tcp/0']
|
||||
},
|
||||
transports: [tcp()],
|
||||
connectionEncrypters: [noise()],
|
||||
streamMuxers: [yamux()],
|
||||
peerDiscovery: [mdns()],
|
||||
services: {
|
||||
pubsub: gossipsub(),
|
||||
identify: identify(),
|
||||
ping: ping(),
|
||||
}
|
||||
});
|
||||
|
||||
this.libp2p.addEventListener("peer:discovery", (event) => {
|
||||
debug(`found peer: ${JSON.stringify(event.detail, null, 2)}`);
|
||||
this.libp2p?.dial(event.detail.multiaddrs);
|
||||
});
|
||||
|
||||
this.libp2p.addEventListener("peer:connect", (event) => {
|
||||
debug(`connected to peer: ${JSON.stringify(event.detail, null, 2)}`);
|
||||
// TODO: Subscribe
|
||||
});
|
||||
}
|
||||
|
||||
async publish(topic: string, msg: string) {
|
||||
debug(`publishing to ZeroMQ, msg: ${msg}`);
|
||||
await this.publishSock.send([
|
||||
topic,
|
||||
this.rhizomeNode.myRequestAddr.toAddrString(),
|
||||
msg
|
||||
]);
|
||||
|
||||
if (this.libp2p) {
|
||||
const pubsub = this.libp2p.services.pubsub as GossipSub;
|
||||
debug(`publishing to Libp2p, msg: ${msg}`);
|
||||
try {
|
||||
await pubsub.publish(topic, Buffer.from(msg));
|
||||
} catch (e: unknown) {
|
||||
debug('Libp2p publish:', (e as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(publishAddr: PeerAddress, topic: string, cb: SubscribedMessageHandler): Subscription {
|
||||
const subscription = new Subscription(publishAddr, topic, cb);
|
||||
subscribedTopics = new Set<string>();
|
||||
|
||||
subscribeTopic(topic: string, cb: SubscribedMessageHandler) {
|
||||
if (!this.libp2p) throw new Error('libp2p not initialized');
|
||||
const pubsub = this.libp2p.services.pubsub as GossipSub;
|
||||
|
||||
// TODO: If we subscribe to multiple topics this callback will be duplicated
|
||||
pubsub.addEventListener("message", (event) => {
|
||||
const msg = Buffer.from(event.detail.data).toString();
|
||||
debug(`Libp2p subscribtion received msg: ${msg}`);
|
||||
cb(new PeerAddress('libp2p', 0), msg);
|
||||
});
|
||||
|
||||
// Add to our list of subscribed topics so we can unsubscribe later.
|
||||
// Also has the effect of not calling subscribe more than once per topic.
|
||||
if (!this.subscribedTopics.has(topic)) {
|
||||
pubsub.subscribe(topic);
|
||||
this.subscribedTopics.add(topic);
|
||||
debug('subscribed topics:', Array.from(this.subscribedTopics.keys()));
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(
|
||||
publishAddr: PeerAddress,
|
||||
topic: string,
|
||||
cb: SubscribedMessageHandler
|
||||
): Subscription {
|
||||
const subscription = new Subscription(publishAddr, topic, cb, this.libp2p);
|
||||
this.subscriptions.push(subscription);
|
||||
return subscription;
|
||||
}
|
||||
|
@ -73,7 +156,23 @@ export class PubSub {
|
|||
|
||||
for (const subscription of this.subscriptions) {
|
||||
subscription.sock.close();
|
||||
debug('subscription socket is closed?', subscription.sock.closed);
|
||||
}
|
||||
|
||||
if (this.libp2p) {
|
||||
const pubsub = this.libp2p.services.pubsub as GossipSub;
|
||||
|
||||
pubsub.removeEventListener("message");
|
||||
|
||||
for (const topic of this.subscribedTopics) {
|
||||
debug(`unsubscribing Libp2p topic ${topic}`);
|
||||
pubsub.unsubscribe(topic)
|
||||
}
|
||||
|
||||
debug('stopping gossipsub');
|
||||
|
||||
await pubsub.stop();
|
||||
|
||||
await this.libp2p.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import {Request, Reply, Message} from 'zeromq';
|
||||
import {EventEmitter} from 'node:events';
|
||||
import {RequestMethods} from './peers';
|
||||
import Debug from 'debug';
|
||||
import {RhizomeNode} from './node';
|
||||
import {PeerAddress} from './types';
|
||||
import {EventEmitter} from 'node:events';
|
||||
import {Message, Reply, Request} from 'zeromq';
|
||||
import {RhizomeNode} from './node.js';
|
||||
import {RequestMethods} from './peers.js';
|
||||
import {PeerAddress} from './types.js';
|
||||
const debug = Debug('request-reply');
|
||||
|
||||
export type PeerRequest = {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Level } from 'level';
|
||||
import { LEVEL_DB_DIR } from './config';
|
||||
import { LEVEL_DB_DIR } from './config.js';
|
||||
import path from 'path';
|
||||
|
||||
function newStore(name: string): Level {
|
||||
|
@ -8,4 +8,4 @@ function newStore(name: string): Level {
|
|||
|
||||
export const queryResultStore = newStore('query-results');
|
||||
|
||||
export const deltasAcceptedStore = newStore('deltas-accepted');
|
||||
export const deltasAcceptedStore = newStore('deltas-accepted');
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Debug from "debug";
|
||||
import EventEmitter from "events";
|
||||
import {Delta, DeltaID} from "./delta";
|
||||
import {DomainEntityID, TransactionID} from "./types";
|
||||
import {Delta, DeltaID} from "./delta.js";
|
||||
import {DomainEntityID, TransactionID} from "./types.js";
|
||||
const debug = Debug("transactions");
|
||||
|
||||
function getDeltaTransactionId(delta: Delta): TransactionID | undefined {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import Debug from "debug";
|
||||
import {FSWatcher, readdirSync, readFileSync, watch} from "fs";
|
||||
import path, {join} from "path";
|
||||
import {Converter} from "showdown";
|
||||
import showdown from "showdown";
|
||||
const {Converter} = showdown;
|
||||
const debug = Debug('md-files');
|
||||
|
||||
const docConverter = new Converter({
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"module": "CommonJS",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "Node",
|
||||
"moduleResolution": "bundler",
|
||||
"sourceMap": false,
|
||||
"isolatedModules": true,
|
||||
/* "allowImportingTsExtensions": true, */
|
||||
/* "noEmit": true, */
|
||||
"baseUrl": ".",
|
||||
"outDir": "dist",
|
||||
"lib": ["ESNext"],
|
||||
"types": ["node", "jest"],
|
||||
"importsNotUsedAsValues": "remove",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
|
@ -21,5 +26,8 @@
|
|||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
],
|
||||
"tsc-alias": {
|
||||
"resolveFullPaths": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {RhizomeNode, RhizomeNodeConfig} from "../src/node";
|
||||
import {Collection} from "../src/collection";
|
||||
import {RhizomeNode, RhizomeNodeConfig} from "../src/node.js";
|
||||
import {Collection} from "../src/collection.js";
|
||||
import {randomUUID} from "crypto";
|
||||
|
||||
const start = 5000;
|
||||
const range = 5000;
|
||||
|
@ -14,6 +15,7 @@ export class App extends RhizomeNode {
|
|||
publishBindPort: getRandomPort(),
|
||||
requestBindPort: getRandomPort(),
|
||||
httpPort: getRandomPort(),
|
||||
pubSubTopic: config?.pubSubTopic || `deltas-${randomUUID()}`,
|
||||
...config,
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue