Files
jazz-tools/src/node.ts
2023-08-01 15:23:55 +01:00

230 lines
6.7 KiB
TypeScript

import { CoMap } from "./coValue";
import { newRandomKeySecret, seal } from "./crypto";
import {
MultiLogID,
MultiLog,
AgentCredential,
AgentID,
SessionID,
Agent,
getAgent,
getAgentID,
getAgentMultilogHeader,
MultiLogHeader,
agentIDfromSessionID,
} from "./multilog";
import { Team, expectTeamContent } from "./permissions";
import {
NewContentMessage,
Peer,
PeerID,
PeerState,
SessionNewContent,
SubscribeMessage,
SyncMessage,
UnsubscribeMessage,
WrongAssumedKnownStateMessage,
} from "./sync";
export class LocalNode {
multilogs: { [key: MultiLogID]: Promise<MultiLog> | MultiLog } = {};
peers: { [key: PeerID]: PeerState } = {};
agentCredential: AgentCredential;
agentID: AgentID;
ownSessionID: SessionID;
knownAgents: { [key: AgentID]: Agent } = {};
constructor(agentCredential: AgentCredential, ownSessionID: SessionID) {
this.agentCredential = agentCredential;
const agent = getAgent(agentCredential);
const agentID = getAgentID(agent);
this.agentID = agentID;
this.knownAgents[agentID] = agent;
this.ownSessionID = ownSessionID;
const agentMultilog = new MultiLog(getAgentMultilogHeader(agent), this);
this.multilogs[agentMultilog.id] = Promise.resolve(agentMultilog);
}
createMultiLog(header: MultiLogHeader): MultiLog {
const requiredMultiLogs =
header.ruleset.type === "ownedByTeam"
? {
[header.ruleset.team]: this.expectMultiLogLoaded(
header.ruleset.team
),
}
: {};
const multilog = new MultiLog(header, this);
this.multilogs[multilog.id] = multilog;
this.syncMultiLog(multilog);
return multilog;
}
expectMultiLogLoaded(id: MultiLogID): MultiLog {
const multilog = this.multilogs[id];
if (!multilog) {
throw new Error(`Unknown multilog ${id}`);
}
if (multilog instanceof Promise) {
throw new Error(`Multilog ${id} not yet loaded`);
}
return multilog;
}
addKnownAgent(agent: Agent) {
const agentID = getAgentID(agent);
this.knownAgents[agentID] = agent;
}
createTeam(): Team {
const teamMultilog = this.createMultiLog({
type: "comap",
ruleset: { type: "team", initialAdmin: this.agentID },
meta: null,
});
let teamContent = expectTeamContent(teamMultilog.getCurrentContent());
teamContent = teamContent.edit((editable) => {
editable.set(this.agentID, "admin", "trusting");
const readKey = newRandomKeySecret();
const revelation = seal(
readKey.secret,
this.agentCredential.recipientSecret,
new Set([getAgent(this.agentCredential).recipientID]),
{
in: teamMultilog.id,
tx: teamMultilog.nextTransactionID(),
}
);
editable.set(
"readKey",
{ keyID: readKey.id, revelation },
"trusting"
);
});
return new Team(teamContent, this);
}
async addPeer(peer: Peer) {
const peerState = {
id: peer.id,
optimisticKnownStates: {},
incoming: peer.incoming,
outgoing: peer.outgoing.getWriter(),
role: peer.role,
};
this.peers[peer.id] = peerState;
for await (const msg of peerState.incoming) {
const response = this.handleSyncMessage(msg, peerState);
if (response) {
await peerState.outgoing.write(response);
}
}
}
handleSyncMessage(
msg: SyncMessage,
peer: PeerState
): SyncMessage | undefined {
// TODO: validate
switch (msg.type) {
case "subscribe":
return this.handleSubscribe(msg, peer);
case "newContent":
return this.handleNewContent(msg);
case "wrongAssumedKnownState":
return this.handleWrongAssumedKnownState(msg, peer);
case "unsubscribe":
return this.handleUnsubscribe(msg);
}
}
handleSubscribe(
msg: SubscribeMessage,
peer: PeerState
): SyncMessage | undefined {
const multilog = this.expectMultiLogLoaded(msg.knownState.multilogID);
peer.optimisticKnownStates[multilog.id] = multilog.knownState();
return multilog.newContentSince(msg.knownState);
}
handleNewContent(msg: NewContentMessage): SyncMessage | undefined {
return undefined;
}
handleWrongAssumedKnownState(
msg: WrongAssumedKnownStateMessage,
peer: PeerState
): SyncMessage | undefined {
const multilog = this.expectMultiLogLoaded(msg.knownState.multilogID);
peer.optimisticKnownStates[msg.knownState.multilogID] = msg.knownState;
return multilog.newContentSince(msg.knownState);
}
handleUnsubscribe(msg: UnsubscribeMessage): SyncMessage | undefined {
return undefined;
}
async syncMultiLog(multilog: MultiLog) {
for (const peer of Object.values(this.peers)) {
const optimisticKnownState =
peer.optimisticKnownStates[multilog.id];
if (optimisticKnownState || peer.role === "server") {
const newContent =
multilog.newContentSince(optimisticKnownState);
peer.optimisticKnownStates[multilog.id] = multilog.knownState();
if (newContent) {
await peer.outgoing.write(newContent);
}
}
}
}
testWithDifferentCredentials(
agentCredential: AgentCredential,
ownSessionID: SessionID
): LocalNode {
const newNode = new LocalNode(agentCredential, ownSessionID);
newNode.multilogs = Object.fromEntries(
Object.entries(this.multilogs)
.map(([id, multilog]) => {
if (multilog instanceof Promise) {
return [id, undefined];
}
const newMultilog = new MultiLog(multilog.header, newNode);
newMultilog.sessions = multilog.sessions;
return [id, newMultilog];
})
.filter((x): x is Exclude<typeof x, undefined> => !!x)
);
newNode.knownAgents = {
...this.knownAgents,
[agentIDfromSessionID(ownSessionID)]: getAgent(agentCredential),
};
return newNode;
}
}