Compare commits
2 Commits
docs/loadi
...
cojson-cle
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d1a30f4eb | ||
|
|
e32ee18608 |
840
packages/cojson/src/node.test.ts
Normal file
840
packages/cojson/src/node.test.ts
Normal file
@@ -0,0 +1,840 @@
|
|||||||
|
import { describe, test, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
CoValueState,
|
||||||
|
NodeState,
|
||||||
|
addPeer,
|
||||||
|
markCoValueDone,
|
||||||
|
removePeer,
|
||||||
|
unsafeAddVerifiedHeader,
|
||||||
|
unsafeAddVerifiedTransactions,
|
||||||
|
} from "./node";
|
||||||
|
import { RawCoID, SessionID } from "./ids";
|
||||||
|
import { CoValueHeader, Transaction } from "./coValueCore";
|
||||||
|
import { Hash, Signature, StreamingHash } from "./crypto";
|
||||||
|
|
||||||
|
describe("NodeState", () => {
|
||||||
|
describe("unsafeAddVerifiedHeader", () => {
|
||||||
|
test("Adding a new header locally to an empty state", () => {
|
||||||
|
const state: NodeState = {
|
||||||
|
coValues: new Map(),
|
||||||
|
peers: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const id = "id1" as RawCoID;
|
||||||
|
const header = {};
|
||||||
|
|
||||||
|
const result = unsafeAddVerifiedHeader(
|
||||||
|
state,
|
||||||
|
id,
|
||||||
|
header as CoValueHeader,
|
||||||
|
"local"
|
||||||
|
);
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
result: "success",
|
||||||
|
events: [{ type: "addedNewHeader", id }],
|
||||||
|
});
|
||||||
|
expect(state.coValues.get(id)).toEqual({
|
||||||
|
id,
|
||||||
|
header,
|
||||||
|
done: false,
|
||||||
|
sessions: new Map(),
|
||||||
|
sync: new Map(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Adding a new header locally with peers", () => {
|
||||||
|
const state: NodeState = {
|
||||||
|
coValues: new Map(),
|
||||||
|
peers: threeDifferentPeers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const id = "id1" as RawCoID;
|
||||||
|
const header = {};
|
||||||
|
|
||||||
|
const result = unsafeAddVerifiedHeader(
|
||||||
|
state,
|
||||||
|
id,
|
||||||
|
header as CoValueHeader,
|
||||||
|
"local"
|
||||||
|
);
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
result: "success",
|
||||||
|
events: [{ type: "addedNewHeader", id }],
|
||||||
|
});
|
||||||
|
expect(state.coValues.get(id)).toEqual({
|
||||||
|
id,
|
||||||
|
header,
|
||||||
|
done: false,
|
||||||
|
sessions: new Map(),
|
||||||
|
sync: new Map([
|
||||||
|
[
|
||||||
|
"peer1",
|
||||||
|
{
|
||||||
|
assumedState: "unknown",
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: toldKnownStateWithJustHeader(id),
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"peer2",
|
||||||
|
{
|
||||||
|
assumedState: "unknown",
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: toldKnownStateWithJustHeader(id),
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"peer3",
|
||||||
|
{
|
||||||
|
assumedState: "unknown",
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: toldKnownStateWithJustHeader(id),
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Adding a new header from a peer", () => {
|
||||||
|
const state: NodeState = {
|
||||||
|
coValues: new Map(),
|
||||||
|
peers: threeDifferentPeers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const id = "id1" as RawCoID;
|
||||||
|
const header = {} as CoValueHeader;
|
||||||
|
|
||||||
|
const result = unsafeAddVerifiedHeader(state, id, header, "peer2");
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
result: "success",
|
||||||
|
events: [{ type: "addedNewHeader", id }],
|
||||||
|
});
|
||||||
|
expect(state.coValues.get(id)).toEqual({
|
||||||
|
id,
|
||||||
|
header,
|
||||||
|
done: false,
|
||||||
|
sessions: new Map(),
|
||||||
|
sync: new Map([
|
||||||
|
[
|
||||||
|
"peer1",
|
||||||
|
{
|
||||||
|
assumedState: "unknown",
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: toldKnownStateWithJustHeader(id),
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"peer2",
|
||||||
|
{
|
||||||
|
assumedState: toldKnownStateWithJustHeader(id),
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: toldKnownStateWithJustHeader(id),
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"peer3",
|
||||||
|
{
|
||||||
|
assumedState: "unknown",
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: toldKnownStateWithJustHeader(id),
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("unsafeAddVerifiedTransactions", () => {
|
||||||
|
test("Adding transactions locally to an empty CoValue", () => {
|
||||||
|
const id = "id1" as RawCoID;
|
||||||
|
const header = {} as CoValueHeader;
|
||||||
|
|
||||||
|
const state: NodeState = {
|
||||||
|
coValues: new Map(),
|
||||||
|
peers: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafeAddVerifiedHeader(state, id, header, "local");
|
||||||
|
|
||||||
|
const sessionID = "session1" as SessionID;
|
||||||
|
const transactions = [
|
||||||
|
{ _tx: 0 },
|
||||||
|
{ _tx: 1 },
|
||||||
|
] as unknown as Transaction[];
|
||||||
|
const signature = "signatureAfterTx1" as Signature;
|
||||||
|
const newHash = "hashAfterTx1" as Hash;
|
||||||
|
const streamingHash = {
|
||||||
|
_streamingHashAfterTx: 1,
|
||||||
|
} as unknown as StreamingHash;
|
||||||
|
|
||||||
|
const result = unsafeAddVerifiedTransactions(
|
||||||
|
state,
|
||||||
|
id,
|
||||||
|
sessionID,
|
||||||
|
0,
|
||||||
|
transactions,
|
||||||
|
signature,
|
||||||
|
newHash,
|
||||||
|
streamingHash,
|
||||||
|
"local"
|
||||||
|
);
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
result: "success",
|
||||||
|
events: [{ type: "addedNewTransactions", id }],
|
||||||
|
});
|
||||||
|
expect(state.coValues.get(id)).toEqual({
|
||||||
|
id,
|
||||||
|
header,
|
||||||
|
done: false,
|
||||||
|
sessions: new Map([
|
||||||
|
[
|
||||||
|
sessionID,
|
||||||
|
{
|
||||||
|
transactions,
|
||||||
|
lastSignature: signature,
|
||||||
|
signatureAfter: { 1: signature },
|
||||||
|
lastHash: newHash,
|
||||||
|
streamingHash,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
sync: new Map(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Adding transactions locally to an empty CoValue with peers", () => {
|
||||||
|
const id = "id1" as RawCoID;
|
||||||
|
const header = {} as CoValueHeader;
|
||||||
|
|
||||||
|
const state: NodeState = {
|
||||||
|
coValues: new Map(),
|
||||||
|
peers: threeDifferentPeers,
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafeAddVerifiedHeader(state, id, header, "local");
|
||||||
|
|
||||||
|
const sessionID = "session1" as SessionID;
|
||||||
|
const transactions = [
|
||||||
|
{ _tx: 0 },
|
||||||
|
{ _tx: 1 },
|
||||||
|
] as unknown as Transaction[];
|
||||||
|
const signature = "signatureAfterTx1" as Signature;
|
||||||
|
const newHash = "hashAfterTx1" as Hash;
|
||||||
|
const streamingHash = {
|
||||||
|
_streamingHashAfterTx: 1,
|
||||||
|
} as unknown as StreamingHash;
|
||||||
|
|
||||||
|
const result = unsafeAddVerifiedTransactions(
|
||||||
|
state,
|
||||||
|
id,
|
||||||
|
sessionID,
|
||||||
|
0,
|
||||||
|
transactions,
|
||||||
|
signature,
|
||||||
|
newHash,
|
||||||
|
streamingHash,
|
||||||
|
"local"
|
||||||
|
);
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
result: "success",
|
||||||
|
events: [{ type: "addedNewTransactions", id }],
|
||||||
|
});
|
||||||
|
expect(state.coValues.get(id)).toEqual({
|
||||||
|
id,
|
||||||
|
header,
|
||||||
|
done: false,
|
||||||
|
sessions: new Map([
|
||||||
|
[
|
||||||
|
sessionID,
|
||||||
|
{
|
||||||
|
transactions,
|
||||||
|
lastSignature: signature,
|
||||||
|
signatureAfter: { 1: signature },
|
||||||
|
lastHash: newHash,
|
||||||
|
streamingHash,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
sync: new Map([
|
||||||
|
[
|
||||||
|
"peer1",
|
||||||
|
{
|
||||||
|
assumedState: "unknown",
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: toldKnownStateWithJustHeader(id),
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"peer2",
|
||||||
|
{
|
||||||
|
assumedState: "unknown",
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: toldKnownStateWithJustHeader(id),
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"peer3",
|
||||||
|
{
|
||||||
|
assumedState: "unknown",
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: toldKnownStateWithJustHeader(id),
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Adding transactions to an empty CoValue from peer", () => {
|
||||||
|
const id = "id1" as RawCoID;
|
||||||
|
const header = {} as CoValueHeader;
|
||||||
|
|
||||||
|
const state: NodeState = {
|
||||||
|
coValues: new Map(),
|
||||||
|
peers: threeDifferentPeers,
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafeAddVerifiedHeader(state, id, header, "peer2");
|
||||||
|
|
||||||
|
const sessionID = "session1" as SessionID;
|
||||||
|
const transactions = [
|
||||||
|
{ _tx: 0 },
|
||||||
|
{ _tx: 1 },
|
||||||
|
] as unknown as Transaction[];
|
||||||
|
const signature = "signatureAfterTx1" as Signature;
|
||||||
|
const newHash = "hashAfterTx1" as Hash;
|
||||||
|
const streamingHash = {
|
||||||
|
_streamingHashAfterTx: 1,
|
||||||
|
} as unknown as StreamingHash;
|
||||||
|
|
||||||
|
const result = unsafeAddVerifiedTransactions(
|
||||||
|
state,
|
||||||
|
id,
|
||||||
|
sessionID,
|
||||||
|
0,
|
||||||
|
transactions,
|
||||||
|
signature,
|
||||||
|
newHash,
|
||||||
|
streamingHash,
|
||||||
|
"peer2"
|
||||||
|
);
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
result: "success",
|
||||||
|
events: [{ type: "addedNewTransactions", id }],
|
||||||
|
});
|
||||||
|
expect(state.coValues.get(id)).toEqual({
|
||||||
|
id,
|
||||||
|
header,
|
||||||
|
done: false,
|
||||||
|
sessions: new Map([
|
||||||
|
[
|
||||||
|
sessionID,
|
||||||
|
{
|
||||||
|
transactions,
|
||||||
|
lastSignature: signature,
|
||||||
|
signatureAfter: { 1: signature },
|
||||||
|
lastHash: newHash,
|
||||||
|
streamingHash,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
sync: new Map([
|
||||||
|
[
|
||||||
|
"peer1",
|
||||||
|
{
|
||||||
|
assumedState: "unknown",
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: toldKnownStateWithJustHeader(id),
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"peer2",
|
||||||
|
{
|
||||||
|
assumedState: {
|
||||||
|
id,
|
||||||
|
header: true,
|
||||||
|
sessions: {
|
||||||
|
session1: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: toldKnownStateWithJustHeader(id),
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"peer3",
|
||||||
|
{
|
||||||
|
assumedState: "unknown",
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: toldKnownStateWithJustHeader(id),
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Adding transactions locally to a non-empty CoValue", () => {
|
||||||
|
const id = "id1" as RawCoID;
|
||||||
|
const header = {} as CoValueHeader;
|
||||||
|
|
||||||
|
const state: NodeState = {
|
||||||
|
coValues: new Map(),
|
||||||
|
peers: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result1 = unsafeAddVerifiedHeader(state, id, header, "local");
|
||||||
|
expect(result1.result).toBe("success");
|
||||||
|
|
||||||
|
const sessionID = "session1" as SessionID;
|
||||||
|
const transactions = [
|
||||||
|
{ _tx: 0 },
|
||||||
|
{ _tx: 1 },
|
||||||
|
] as unknown as Transaction[];
|
||||||
|
const signature = "signatureAfterTx1" as Signature;
|
||||||
|
const newHash = "hashAfterTx1" as Hash;
|
||||||
|
const streamingHash = {
|
||||||
|
_streamingHashAfterTx: 1,
|
||||||
|
} as unknown as StreamingHash;
|
||||||
|
|
||||||
|
const result2 = unsafeAddVerifiedTransactions(
|
||||||
|
state,
|
||||||
|
id,
|
||||||
|
sessionID,
|
||||||
|
0,
|
||||||
|
transactions,
|
||||||
|
signature,
|
||||||
|
newHash,
|
||||||
|
streamingHash,
|
||||||
|
"local"
|
||||||
|
);
|
||||||
|
expect(result2.result).toBe("success");
|
||||||
|
|
||||||
|
const transactions2 = [
|
||||||
|
{ _tx: 2 },
|
||||||
|
{ _tx: 3 },
|
||||||
|
] as unknown as Transaction[];
|
||||||
|
const signature2 = "signatureAfterTx3" as Signature;
|
||||||
|
const newHash2 = "hashAfterTx3" as Hash;
|
||||||
|
const streamingHash2 = {
|
||||||
|
_streamingHashAfterTx: 3,
|
||||||
|
} as unknown as StreamingHash;
|
||||||
|
|
||||||
|
const result3 = unsafeAddVerifiedTransactions(
|
||||||
|
state,
|
||||||
|
id,
|
||||||
|
sessionID,
|
||||||
|
2,
|
||||||
|
transactions2,
|
||||||
|
signature2,
|
||||||
|
newHash2,
|
||||||
|
streamingHash2,
|
||||||
|
"local"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result3).toMatchObject({
|
||||||
|
result: "success",
|
||||||
|
events: [{ type: "addedNewTransactions", id }],
|
||||||
|
});
|
||||||
|
expect(state.coValues.get(id)).toEqual({
|
||||||
|
id,
|
||||||
|
header,
|
||||||
|
done: false,
|
||||||
|
sessions: new Map([
|
||||||
|
[
|
||||||
|
sessionID,
|
||||||
|
{
|
||||||
|
transactions: [...transactions, ...transactions2],
|
||||||
|
lastSignature: signature2,
|
||||||
|
signatureAfter: { 1: signature, 3: signature2 },
|
||||||
|
lastHash: newHash2,
|
||||||
|
streamingHash: streamingHash2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
sync: new Map()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Adding transactions locally to a non-empty CoValue with Peers", () => {
|
||||||
|
const id = "id1" as RawCoID;
|
||||||
|
const header = {} as CoValueHeader;
|
||||||
|
|
||||||
|
const state: NodeState = {
|
||||||
|
coValues: new Map(),
|
||||||
|
peers: threeDifferentPeers,
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafeAddVerifiedHeader(state, id, header, "local");
|
||||||
|
|
||||||
|
const sessionID = "session1" as SessionID;
|
||||||
|
const transactions = [
|
||||||
|
{ _tx: 0 },
|
||||||
|
{ _tx: 1 },
|
||||||
|
] as unknown as Transaction[];
|
||||||
|
const signature = "signatureAfterTx1" as Signature;
|
||||||
|
const newHash = "hashAfterTx1" as Hash;
|
||||||
|
const streamingHash = {
|
||||||
|
_streamingHashAfterTx: 1,
|
||||||
|
} as unknown as StreamingHash;
|
||||||
|
|
||||||
|
const _result = unsafeAddVerifiedTransactions(
|
||||||
|
state,
|
||||||
|
id,
|
||||||
|
sessionID,
|
||||||
|
0,
|
||||||
|
transactions,
|
||||||
|
signature,
|
||||||
|
newHash,
|
||||||
|
streamingHash,
|
||||||
|
"local"
|
||||||
|
);
|
||||||
|
|
||||||
|
const transactions2 = [
|
||||||
|
{ _tx: 2 },
|
||||||
|
{ _tx: 3 },
|
||||||
|
] as unknown as Transaction[];
|
||||||
|
const signature2 = "signatureAfterTx3" as Signature;
|
||||||
|
const newHash2 = "hashAfterTx3" as Hash;
|
||||||
|
const streamingHash2 = {
|
||||||
|
_streamingHashAfterTx: 3,
|
||||||
|
} as unknown as StreamingHash;
|
||||||
|
|
||||||
|
const result2 = unsafeAddVerifiedTransactions(
|
||||||
|
state,
|
||||||
|
id,
|
||||||
|
sessionID,
|
||||||
|
2,
|
||||||
|
transactions2,
|
||||||
|
signature2,
|
||||||
|
newHash2,
|
||||||
|
streamingHash2,
|
||||||
|
"local"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result2).toMatchObject({
|
||||||
|
result: "success",
|
||||||
|
events: [{ type: "addedNewTransactions", id }],
|
||||||
|
});
|
||||||
|
expect(state.coValues.get(id)).toEqual({
|
||||||
|
id,
|
||||||
|
header,
|
||||||
|
done: false,
|
||||||
|
sessions: new Map([
|
||||||
|
[
|
||||||
|
sessionID,
|
||||||
|
{
|
||||||
|
transactions: [...transactions, ...transactions2],
|
||||||
|
lastSignature: signature2,
|
||||||
|
signatureAfter: { 1: signature, 3: signature2 },
|
||||||
|
lastHash: newHash2,
|
||||||
|
streamingHash: streamingHash2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
sync: new Map([
|
||||||
|
[
|
||||||
|
"peer1",
|
||||||
|
{
|
||||||
|
assumedState: "unknown",
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: toldKnownStateWithJustHeader(id),
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"peer2",
|
||||||
|
{
|
||||||
|
assumedState: "unknown",
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: toldKnownStateWithJustHeader(id),
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"peer3",
|
||||||
|
{
|
||||||
|
assumedState: "unknown",
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: toldKnownStateWithJustHeader(id),
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Adding transactions from a peer to a non-empty CoValue", () => {
|
||||||
|
const id = "id1" as RawCoID;
|
||||||
|
const header = {} as CoValueHeader;
|
||||||
|
|
||||||
|
const state: NodeState = {
|
||||||
|
coValues: new Map(),
|
||||||
|
peers: threeDifferentPeers,
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafeAddVerifiedHeader(state, id, header, "peer2");
|
||||||
|
|
||||||
|
const sessionID = "session1" as SessionID;
|
||||||
|
const transactions = [
|
||||||
|
{ _tx: 0 },
|
||||||
|
{ _tx: 1 },
|
||||||
|
] as unknown as Transaction[];
|
||||||
|
const signature = "signatureAfterTx1" as Signature;
|
||||||
|
const newHash = "hashAfterTx1" as Hash;
|
||||||
|
const streamingHash = {
|
||||||
|
_streamingHashAfterTx: 1,
|
||||||
|
} as unknown as StreamingHash;
|
||||||
|
|
||||||
|
const _result = unsafeAddVerifiedTransactions(
|
||||||
|
state,
|
||||||
|
id,
|
||||||
|
sessionID,
|
||||||
|
0,
|
||||||
|
transactions,
|
||||||
|
signature,
|
||||||
|
newHash,
|
||||||
|
streamingHash,
|
||||||
|
"peer2"
|
||||||
|
);
|
||||||
|
|
||||||
|
const transactions2 = [
|
||||||
|
{ _tx: 2 },
|
||||||
|
{ _tx: 3 },
|
||||||
|
] as unknown as Transaction[];
|
||||||
|
const signature2 = "signatureAfterTx3" as Signature;
|
||||||
|
const newHash2 = "hashAfterTx3" as Hash;
|
||||||
|
const streamingHash2 = {
|
||||||
|
_streamingHashAfterTx: 3,
|
||||||
|
} as unknown as StreamingHash;
|
||||||
|
|
||||||
|
const result2 = unsafeAddVerifiedTransactions(
|
||||||
|
state,
|
||||||
|
id,
|
||||||
|
sessionID,
|
||||||
|
2,
|
||||||
|
transactions2,
|
||||||
|
signature2,
|
||||||
|
newHash2,
|
||||||
|
streamingHash2,
|
||||||
|
"peer2"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result2).toMatchObject({
|
||||||
|
result: "success",
|
||||||
|
events: [{ type: "addedNewTransactions", id }],
|
||||||
|
});
|
||||||
|
expect(state.coValues.get(id)).toEqual({
|
||||||
|
id,
|
||||||
|
header,
|
||||||
|
done: false,
|
||||||
|
sessions: new Map([
|
||||||
|
[
|
||||||
|
sessionID,
|
||||||
|
{
|
||||||
|
transactions: [...transactions, ...transactions2],
|
||||||
|
lastSignature: signature2,
|
||||||
|
signatureAfter: { 1: signature, 3: signature2 },
|
||||||
|
lastHash: newHash2,
|
||||||
|
streamingHash: streamingHash2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
sync: new Map([
|
||||||
|
[
|
||||||
|
"peer1",
|
||||||
|
{
|
||||||
|
assumedState: "unknown",
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: toldKnownStateWithJustHeader(id),
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"peer2",
|
||||||
|
{
|
||||||
|
assumedState: {
|
||||||
|
id,
|
||||||
|
header: true,
|
||||||
|
sessions: {
|
||||||
|
session1: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: toldKnownStateWithJustHeader(id),
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"peer3",
|
||||||
|
{
|
||||||
|
assumedState: "unknown",
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: toldKnownStateWithJustHeader(id),
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("markCoValueDone", () => {
|
||||||
|
test("Marking a CoValue as done", () => {
|
||||||
|
const id = "id1" as RawCoID;
|
||||||
|
const header = {} as CoValueHeader;
|
||||||
|
|
||||||
|
const state: NodeState = {
|
||||||
|
coValues: new Map(),
|
||||||
|
peers: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result1 = unsafeAddVerifiedHeader(state, id, header, "local");
|
||||||
|
expect(result1.result).toBe("success");
|
||||||
|
|
||||||
|
const result2 = markCoValueDone(state, id);
|
||||||
|
expect(result2).toMatchObject({
|
||||||
|
result: "success",
|
||||||
|
events: [{ type: "coValueDone", id }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(state.coValues.get(id)).toEqual({
|
||||||
|
id,
|
||||||
|
header,
|
||||||
|
done: true,
|
||||||
|
sessions: new Map(),
|
||||||
|
sync: new Map(),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Add and removing peers", () => {
|
||||||
|
test("Adding a peer to an empty state", () => {
|
||||||
|
const state: NodeState = {
|
||||||
|
coValues: new Map(),
|
||||||
|
peers: new Map(),
|
||||||
|
};
|
||||||
|
const id = "id1" as RawCoID;
|
||||||
|
const header = {} as CoValueHeader;
|
||||||
|
|
||||||
|
const result1 = unsafeAddVerifiedHeader(state, id, header, "local");
|
||||||
|
expect(result1.result).toBe("success");
|
||||||
|
|
||||||
|
const result2 = addPeer(state, "peer1", "syncAllOurs", 10);
|
||||||
|
expect(result2).toMatchObject({
|
||||||
|
result: "success",
|
||||||
|
events: [{ type: "addedPeer", peer: "peer1" }],
|
||||||
|
});
|
||||||
|
expect(state.peers.get("peer1")).toEqual({
|
||||||
|
name: "peer1",
|
||||||
|
relationship: "syncAllOurs",
|
||||||
|
priority: 10,
|
||||||
|
});
|
||||||
|
expect(state.coValues.get(id)).toMatchObject({
|
||||||
|
sync: new Map([
|
||||||
|
[
|
||||||
|
"peer1",
|
||||||
|
{
|
||||||
|
assumedState: "unknown",
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: undefined,
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Removing a peer from a state", () => {
|
||||||
|
const state: NodeState = {
|
||||||
|
coValues: new Map(),
|
||||||
|
peers: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const id = "id1" as RawCoID;
|
||||||
|
const header = {} as CoValueHeader;
|
||||||
|
|
||||||
|
const result1 = unsafeAddVerifiedHeader(state, id, header, "local");
|
||||||
|
expect(result1.result).toBe("success");
|
||||||
|
|
||||||
|
const result2 = addPeer(state, "peer1", "syncAllOurs", 10);
|
||||||
|
expect(result2).toMatchObject({
|
||||||
|
result: "success",
|
||||||
|
events: [{ type: "addedPeer", peer: "peer1" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result3 = removePeer(state, "peer1");
|
||||||
|
expect(result3).toMatchObject({
|
||||||
|
result: "success",
|
||||||
|
events: [{ type: "removedPeer", peer: "peer1" }],
|
||||||
|
});
|
||||||
|
expect(state.peers.get("peer1")).toBeUndefined();
|
||||||
|
expect(state.coValues.get(id)?.sync.get("peer1")).toBeUndefined();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Updating peer state", () => {
|
||||||
|
test.todo("Confirming a peer's state updates confirmed state");
|
||||||
|
test.todo("Confirming a peer's state older than the assumed state leaves the assumed state intact");
|
||||||
|
test.todo("Confirming a peer's state newer than the assumed state updates the assumed state");
|
||||||
|
|
||||||
|
test.todo("Correcting a peer's assumed state updates the assumed state");
|
||||||
|
test.todo("Correcting a peer's assumed state older than the confirmed state updates the confirmed state as well");
|
||||||
|
test.todo(
|
||||||
|
"Correcting a peer's assumed state newer than the confirmed state updates both assumed and confirmed state"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Syncing a CoValue", () => {
|
||||||
|
test.todo("Syncing a CoValue with unknown assumed state and no confirmed state sends a load request to syncAllOurs peers");
|
||||||
|
test.todo(
|
||||||
|
"Syncing a CoValue with unknown assumed state and no confirmed state sends a known request to syncTheirs peers"
|
||||||
|
);
|
||||||
|
test.todo("Syncing a CoValue with assumed state behind our state sends a newContent message to syncAllOurs peers");
|
||||||
|
test.todo("Syncing a CoValue with assumed state behind our state sends a newContent message to syncTheirs peers");
|
||||||
|
test.todo("Syncing a CoValue only creates messages for the highest priority peer that has assumed state behind ours")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const toldKnownStateWithJustHeader = (id: RawCoID) => ({
|
||||||
|
id,
|
||||||
|
header: true,
|
||||||
|
sessions: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const threeDifferentPeers = new Map([
|
||||||
|
[
|
||||||
|
"peer1",
|
||||||
|
{
|
||||||
|
name: "peer1",
|
||||||
|
relationship: "syncAllOurs",
|
||||||
|
priority: 0,
|
||||||
|
},
|
||||||
|
] as const,
|
||||||
|
[
|
||||||
|
"peer2",
|
||||||
|
{
|
||||||
|
name: "peer2",
|
||||||
|
relationship: "syncTheirs",
|
||||||
|
priority: 0,
|
||||||
|
},
|
||||||
|
] as const,
|
||||||
|
[
|
||||||
|
"peer3",
|
||||||
|
{
|
||||||
|
name: "peer3",
|
||||||
|
relationship: "syncCommon",
|
||||||
|
priority: 0,
|
||||||
|
},
|
||||||
|
] as const,
|
||||||
|
]);
|
||||||
318
packages/cojson/src/node.ts
Normal file
318
packages/cojson/src/node.ts
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
import { CoValueHeader, Transaction } from "./coValueCore";
|
||||||
|
import { Hash, Signature, StreamingHash } from "./crypto";
|
||||||
|
import { RawCoID, SessionID } from "./ids";
|
||||||
|
import {
|
||||||
|
CoValueKnownState,
|
||||||
|
KnownStateMessage,
|
||||||
|
SyncMessage,
|
||||||
|
combinedKnownStates,
|
||||||
|
} from "./sync";
|
||||||
|
|
||||||
|
type SessionLog = {
|
||||||
|
transactions: Transaction[];
|
||||||
|
lastHash: Hash;
|
||||||
|
streamingHash: StreamingHash;
|
||||||
|
signatureAfter: { [txIdx: number]: Signature | undefined };
|
||||||
|
lastSignature: Signature;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PeerName = Exclude<string, "local">;
|
||||||
|
type PeerRelationship = "syncAllOurs" | "syncTheirs" | "syncCommon";
|
||||||
|
|
||||||
|
export type CoValueState = {
|
||||||
|
id: RawCoID;
|
||||||
|
header?: CoValueHeader;
|
||||||
|
sessions: Map<SessionID, SessionLog>;
|
||||||
|
done: boolean;
|
||||||
|
sync: Map<
|
||||||
|
PeerName,
|
||||||
|
{
|
||||||
|
toldKnownState: CoValueKnownState | undefined;
|
||||||
|
assumedState: CoValueKnownState | "unknown" | "unavailable";
|
||||||
|
confirmedState: CoValueKnownState | undefined;
|
||||||
|
error: string | undefined;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NodeState = {
|
||||||
|
peers: Map<
|
||||||
|
PeerName,
|
||||||
|
{
|
||||||
|
name: PeerName;
|
||||||
|
relationship: PeerRelationship;
|
||||||
|
priority: number;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
coValues: Map<RawCoID, CoValueState>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ATOMIC UPDATES
|
||||||
|
|
||||||
|
export function unsafeAddVerifiedHeader(
|
||||||
|
state: NodeState,
|
||||||
|
id: RawCoID,
|
||||||
|
header: CoValueHeader,
|
||||||
|
from: PeerName | "local"
|
||||||
|
):
|
||||||
|
| {
|
||||||
|
result: "success";
|
||||||
|
events: [{ type: "addedNewHeader"; id: RawCoID }];
|
||||||
|
}
|
||||||
|
| { result: "error"; reason: "coValueAlreadyExists" | "unknownPeer" } {
|
||||||
|
if (state.coValues.has(id)) {
|
||||||
|
return { result: "error", reason: "coValueAlreadyExists" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from !== "local" && !state.peers.has(from)) {
|
||||||
|
return { result: "error", reason: "unknownPeer" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry: CoValueState = {
|
||||||
|
id,
|
||||||
|
header,
|
||||||
|
done: false,
|
||||||
|
sessions: new Map(),
|
||||||
|
sync: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
state.coValues.set(id, entry);
|
||||||
|
|
||||||
|
for (const peerName of state.peers.keys()) {
|
||||||
|
entry.sync.set(peerName, {
|
||||||
|
assumedState:
|
||||||
|
peerName === from
|
||||||
|
? {
|
||||||
|
id,
|
||||||
|
header: true,
|
||||||
|
sessions: {},
|
||||||
|
}
|
||||||
|
: "unknown",
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: {
|
||||||
|
id,
|
||||||
|
header: true,
|
||||||
|
sessions: {},
|
||||||
|
},
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
result: "success",
|
||||||
|
events: [{ type: "addedNewHeader", id }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unsafeAddVerifiedTransactions(
|
||||||
|
state: NodeState,
|
||||||
|
id: RawCoID,
|
||||||
|
sessionID: SessionID,
|
||||||
|
after: number,
|
||||||
|
transactions: Transaction[],
|
||||||
|
signatureAfter: Signature,
|
||||||
|
hash: Hash,
|
||||||
|
streamingHash: StreamingHash,
|
||||||
|
from: PeerName | "local"
|
||||||
|
):
|
||||||
|
| {
|
||||||
|
result: "success";
|
||||||
|
events: [{ type: "addedNewTransactions"; id: RawCoID }];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
result: "error";
|
||||||
|
reason: "coValueNotFound" | "invalidAfter" | "unknownPeer";
|
||||||
|
} {
|
||||||
|
const coValue = state.coValues.get(id);
|
||||||
|
if (coValue === undefined) {
|
||||||
|
return { result: "error", reason: "coValueNotFound" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from !== "local" && !state.peers.has(from)) {
|
||||||
|
return { result: "error", reason: "unknownPeer" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = coValue.sessions.get(sessionID);
|
||||||
|
if (session === undefined) {
|
||||||
|
if (after !== 0) {
|
||||||
|
return { result: "error", reason: "invalidAfter" };
|
||||||
|
}
|
||||||
|
|
||||||
|
coValue.sessions.set(sessionID, {
|
||||||
|
transactions: [...transactions],
|
||||||
|
lastHash: hash,
|
||||||
|
streamingHash,
|
||||||
|
signatureAfter: {
|
||||||
|
[after + transactions.length - 1]: signatureAfter,
|
||||||
|
},
|
||||||
|
lastSignature: signatureAfter,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (after !== session.transactions.length) {
|
||||||
|
return { result: "error", reason: "invalidAfter" };
|
||||||
|
}
|
||||||
|
|
||||||
|
session.transactions.push(...transactions);
|
||||||
|
session.lastHash = hash;
|
||||||
|
session.streamingHash = streamingHash;
|
||||||
|
session.signatureAfter[after + transactions.length - 1] =
|
||||||
|
signatureAfter;
|
||||||
|
session.lastSignature = signatureAfter;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from !== "local") {
|
||||||
|
const fromState = coValue.sync.get(from)!;
|
||||||
|
const knownStateForThisSession = {
|
||||||
|
id,
|
||||||
|
header: true,
|
||||||
|
sessions: {
|
||||||
|
[sessionID]:
|
||||||
|
coValue.sessions.get(sessionID)!.transactions.length,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
fromState.assumedState === "unknown" ||
|
||||||
|
fromState.assumedState === "unavailable"
|
||||||
|
) {
|
||||||
|
fromState.assumedState = knownStateForThisSession;
|
||||||
|
} else {
|
||||||
|
fromState.assumedState = combinedKnownStates(
|
||||||
|
fromState.assumedState,
|
||||||
|
knownStateForThisSession
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
result: "success",
|
||||||
|
events: [{ type: "addedNewTransactions", id }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markCoValueDone(
|
||||||
|
state: NodeState,
|
||||||
|
id: RawCoID
|
||||||
|
):
|
||||||
|
| {
|
||||||
|
result: "success";
|
||||||
|
events: [{ type: "coValueDone"; id: RawCoID }];
|
||||||
|
}
|
||||||
|
| { result: "error"; reason: "coValueNotFound" } {
|
||||||
|
const coValue = state.coValues.get(id);
|
||||||
|
if (coValue === undefined) {
|
||||||
|
return { result: "error", reason: "coValueNotFound" };
|
||||||
|
}
|
||||||
|
coValue.done = true;
|
||||||
|
return {
|
||||||
|
result: "success",
|
||||||
|
events: [{ type: "coValueDone", id }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addPeer(
|
||||||
|
state: NodeState,
|
||||||
|
peer: PeerName,
|
||||||
|
relationship: PeerRelationship,
|
||||||
|
priority: number
|
||||||
|
):
|
||||||
|
| {
|
||||||
|
result: "success";
|
||||||
|
events: [{ type: "addedPeer"; peer: PeerName }];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
result: "error";
|
||||||
|
reason: "peerAlreadyExists";
|
||||||
|
} {
|
||||||
|
if (state.peers.has(peer)) {
|
||||||
|
return { result: "error", reason: "peerAlreadyExists" };
|
||||||
|
}
|
||||||
|
state.peers.set(peer, { name: peer, relationship, priority });
|
||||||
|
|
||||||
|
for (const coValue of state.coValues.values()) {
|
||||||
|
coValue.sync.set(peer, {
|
||||||
|
assumedState: "unknown",
|
||||||
|
confirmedState: undefined,
|
||||||
|
toldKnownState: undefined,
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
result: "success",
|
||||||
|
events: [{ type: "addedPeer", peer }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removePeer(
|
||||||
|
state: NodeState,
|
||||||
|
peer: PeerName
|
||||||
|
):
|
||||||
|
| {
|
||||||
|
result: "success";
|
||||||
|
events: [{ type: "removedPeer"; peer: PeerName }];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
result: "error";
|
||||||
|
reason: "unknownPeer";
|
||||||
|
} {
|
||||||
|
if (!state.peers.has(peer)) {
|
||||||
|
return { result: "error", reason: "unknownPeer" };
|
||||||
|
}
|
||||||
|
state.peers.delete(peer);
|
||||||
|
|
||||||
|
for (const coValue of state.coValues.values()) {
|
||||||
|
coValue.sync.delete(peer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
result: "success",
|
||||||
|
events: [{ type: "removedPeer", peer }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function confirmPeerState(
|
||||||
|
state: NodeState,
|
||||||
|
peer: PeerName,
|
||||||
|
knownState: CoValueKnownState
|
||||||
|
) {}
|
||||||
|
|
||||||
|
|
||||||
|
export function correctPeerAssumedState(
|
||||||
|
state: NodeState,
|
||||||
|
peer: PeerName,
|
||||||
|
knownState: CoValueKnownState
|
||||||
|
):
|
||||||
|
| {
|
||||||
|
result: "success";
|
||||||
|
events: [{ type: "syncStateUpdated"; peer: PeerName; id: RawCoID }];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
result: "error";
|
||||||
|
reason: "unknownPeer";
|
||||||
|
} {}
|
||||||
|
|
||||||
|
function syncToPeer(
|
||||||
|
state: NodeState,
|
||||||
|
id: RawCoID
|
||||||
|
):
|
||||||
|
| {
|
||||||
|
result: "success";
|
||||||
|
send: { to: PeerName; messages: SyncMessage[] } | undefined;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
result: "error";
|
||||||
|
reason: "unknownPeer";
|
||||||
|
} {}
|
||||||
|
|
||||||
|
function loop() {
|
||||||
|
const state: NodeState = {
|
||||||
|
coValues: new Map(),
|
||||||
|
peers: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// trigger on local changes or incoming messages
|
||||||
|
while (true) {
|
||||||
|
// apply incoming or local changes
|
||||||
|
// for each covalue, sync with the highest priority peer that has changes to sync
|
||||||
|
// send resulting messages
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user