Lots of improvements

This commit is contained in:
Anselm
2023-08-09 11:47:41 +01:00
parent 8cff72041c
commit 209839d889
17 changed files with 499 additions and 295 deletions

View File

@@ -1,8 +1,10 @@
{
"name": "cojson",
"module": "src/index.ts",
"types": "src/index.ts",
"type": "module",
"license": "MIT",
"version": "0.0.3",
"devDependencies": {
"@types/jest": "^29.5.3",
"@typescript-eslint/eslint-plugin": "^6.2.1",

View File

@@ -7,7 +7,7 @@ import {
newRandomSessionID,
} from "./coValue";
import { LocalNode } from "./node";
import { sign } from "./crypto";
import { createdNowUnique, sign, uniquenessForHeader } from "./crypto";
test("Can create coValue with new agent credentials and add transaction to it", () => {
const agentCredential = newRandomAgentCredential("agent1");
@@ -20,6 +20,7 @@ test("Can create coValue with new agent credentials and add transaction to it",
type: "costream",
ruleset: { type: "unsafeAllowAll" },
meta: null,
...createdNowUnique()
});
const transaction: Transaction = {
@@ -59,6 +60,7 @@ test("transactions with wrong signature are rejected", () => {
type: "costream",
ruleset: { type: "unsafeAllowAll" },
meta: null,
...createdNowUnique()
});
const transaction: Transaction = {
@@ -97,6 +99,7 @@ test("transactions with correctly signed, but wrong hash are rejected", () => {
type: "costream",
ruleset: { type: "unsafeAllowAll" },
meta: null,
...createdNowUnique()
});
const transaction: Transaction = {

View File

@@ -1,5 +1,8 @@
import { randomBytes } from "@noble/hashes/utils";
import { CoList, CoMap, ContentType, Static, CoStream } from "./contentType";
import { ContentType } from "./contentType";
import { Static } from "./contentTypes/static";
import { CoStream } from "./contentTypes/coStream";
import { CoMap } from "./contentTypes/coMap";
import {
Encrypted,
Hash,
@@ -22,23 +25,30 @@ import {
decryptForTransaction,
KeyID,
unsealKeySecret,
signatorySecretToBytes,
recipientSecretToBytes,
signatorySecretFromBytes,
recipientSecretFromBytes,
} from "./crypto";
import { JsonValue } from "./jsonValue";
import { base58 } from "@scure/base";
import {
PermissionsDef as RulesetDef,
Team,
determineValidTransactions,
expectTeamContent,
} from "./permissions";
import { LocalNode } from "./node";
import { CoValueKnownState, NewContentMessage } from "./sync";
export type RawCoValueID = `co_z${string}` | `co_${string}_z${string}`;
import { AgentID, RawCoValueID, SessionID, TransactionID } from "./ids";
import { CoList } from "./contentTypes/coList";
export type CoValueHeader = {
type: ContentType["type"];
ruleset: RulesetDef;
meta: JsonValue;
createdAt: `2${string}` | null;
uniqueness: `z${string}` | null;
publicNickname?: string;
};
@@ -53,8 +63,6 @@ function coValueIDforHeader(header: CoValueHeader): RawCoValueID {
}
}
export type SessionID = `${AgentID}_session_z${string}`;
export function agentIDfromSessionID(sessionID: SessionID): AgentID {
return sessionID.split("_session")[0] as AgentID;
}
@@ -94,14 +102,13 @@ export type DecryptedTransaction = {
madeAt: number;
};
export type TransactionID = { sessionID: SessionID; txIndex: number };
export class CoValue {
id: RawCoValueID;
node: LocalNode;
header: CoValueHeader;
sessions: { [key: SessionID]: SessionLog };
content?: ContentType;
listeners: Set<(content?: ContentType) => void> = new Set();
constructor(header: CoValueHeader, node: LocalNode) {
this.id = coValueIDforHeader(header);
@@ -185,6 +192,8 @@ export class CoValue {
const transactions = this.sessions[sessionID]?.transactions ?? [];
console.log("transactions before", this.id, transactions.length, this.getValidSortedTransactions().length);
transactions.push(...newTransactions);
this.sessions[sessionID] = {
@@ -196,11 +205,28 @@ export class CoValue {
this.content = undefined;
const _ = this.getCurrentContent();
console.log("transactions after", this.id, transactions.length, this.getValidSortedTransactions().length);
const content = this.getCurrentContent();
for (const listener of this.listeners) {
console.log("Calling listener (update)", this.id, content.toJSON());
listener(content);
}
return true;
}
subscribe(listener: (content?: ContentType) => void): () => void {
this.listeners.add(listener);
console.log("Calling listener (initial)", this.id, this.getCurrentContent().toJSON());
listener(this.getCurrentContent());
return () => {
this.listeners.delete(listener);
};
}
expectedNewHashAfter(
sessionID: SessionID,
newTransactions: Transaction[]
@@ -232,7 +258,9 @@ export class CoValue {
const { secret: keySecret, id: keyID } = this.getCurrentReadKey();
if (!keySecret) {
throw new Error("Can't make transaction without read key secret");
throw new Error(
"Can't make transaction without read key secret"
);
}
transaction = {
@@ -300,8 +328,8 @@ export class CoValue {
getValidSortedTransactions(): DecryptedTransaction[] {
const validTransactions = determineValidTransactions(this);
const allTransactions: DecryptedTransaction[] = validTransactions.map(
({ txID, tx }) => {
const allTransactions: DecryptedTransaction[] = validTransactions
.map(({ txID, tx }) => {
if (tx.privacy === "trusting") {
return {
txID,
@@ -324,7 +352,9 @@ export class CoValue {
);
if (!decrytedChanges) {
console.error("Failed to decrypt transaction despite having key");
console.error(
"Failed to decrypt transaction despite having key"
);
return undefined;
}
return {
@@ -334,8 +364,8 @@ export class CoValue {
};
}
}
}
).filter((x): x is Exclude<typeof x, undefined> => !!x);
})
.filter((x): x is Exclude<typeof x, undefined> => !!x);
allTransactions.sort(
(a, b) =>
a.madeAt - b.madeAt ||
@@ -446,6 +476,21 @@ export class CoValue {
}
}
getTeam(): Team {
if (this.header.ruleset.type !== "ownedByTeam") {
throw new Error("Only values owned by teams have teams");
}
return new Team(
expectTeamContent(
this.node
.expectCoValueLoaded(this.header.ruleset.team)
.getCurrentContent()
),
this.node
);
}
getTx(txID: TransactionID): Transaction | undefined {
return this.sessions[txID.sessionID]?.transactions[txID.txIndex];
}
@@ -510,8 +555,6 @@ export class CoValue {
}
}
export type AgentID = `co_agent${string}_z${string}`;
export type Agent = {
signatoryID: SignatoryID;
recipientID: RecipientID;
@@ -535,6 +578,8 @@ export function getAgentCoValueHeader(agent: Agent): CoValueHeader {
initialRecipientID: agent.recipientID,
},
meta: null,
createdAt: null,
uniqueness: null,
publicNickname:
"agent" + (agent.publicNickname ? `-${agent.publicNickname}` : ""),
};
@@ -551,13 +596,45 @@ export type AgentCredential = {
};
export function newRandomAgentCredential(
publicNickname: string
publicNickname?: string
): AgentCredential {
const signatorySecret = newRandomSignatory();
const recipientSecret = newRandomRecipient();
return { signatorySecret, recipientSecret, publicNickname };
}
export function agentCredentialToBytes(cred: AgentCredential): Uint8Array {
if (cred.publicNickname) {
throw new Error("Can't convert agent credential with publicNickname");
}
const bytes = new Uint8Array(64);
const signatorySecretBytes = signatorySecretToBytes(cred.signatorySecret);
if (signatorySecretBytes.length !== 32) {
throw new Error("Invalid signatorySecret length");
}
bytes.set(signatorySecretBytes);
const recipientSecretBytes = recipientSecretToBytes(cred.recipientSecret);
if (recipientSecretBytes.length !== 32) {
throw new Error("Invalid recipientSecret length");
}
bytes.set(recipientSecretBytes, 32);
return bytes;
}
export function agentCredentialFromBytes(
bytes: Uint8Array
): AgentCredential | undefined {
if (bytes.length !== 64) {
return undefined;
}
const signatorySecret = signatorySecretFromBytes(bytes.slice(0, 32));
const recipientSecret = recipientSecretFromBytes(bytes.slice(32));
return { signatorySecret, recipientSecret };
}
// type Role = "admin" | "writer" | "reader";
// type PermissionsDef = CJMap<AgentID, Role, {[agent: AgentID]: Role}>;

View File

@@ -5,6 +5,7 @@ import {
newRandomAgentCredential,
newRandomSessionID,
} from "./coValue";
import { createdNowUnique } from "./crypto";
import { LocalNode } from "./node";
test("Empty COJSON Map works", () => {
@@ -18,6 +19,7 @@ test("Empty COJSON Map works", () => {
type: "comap",
ruleset: { type: "unsafeAllowAll" },
meta: null,
...createdNowUnique()
});
const content = coValue.getCurrentContent();
@@ -42,6 +44,7 @@ test("Can insert and delete Map entries in edit()", () => {
type: "comap",
ruleset: { type: "unsafeAllowAll" },
meta: null,
...createdNowUnique()
});
const content = coValue.getCurrentContent();
@@ -74,6 +77,7 @@ test("Can get map entry values at different points in time", () => {
type: "comap",
ruleset: { type: "unsafeAllowAll" },
meta: null,
...createdNowUnique()
});
const content = coValue.getCurrentContent();
@@ -113,6 +117,7 @@ test("Can get all historic values of key", () => {
type: "comap",
ruleset: { type: "unsafeAllowAll" },
meta: null,
...createdNowUnique()
});
const content = coValue.getCurrentContent();
@@ -170,6 +175,7 @@ test("Can get last tx ID for a key", () => {
type: "comap",
ruleset: { type: "unsafeAllowAll" },
meta: null,
...createdNowUnique()
});
const content = coValue.getCurrentContent();

View File

@@ -1,5 +1,9 @@
import { JsonAtom, JsonObject, JsonValue } from "./jsonValue";
import { CoValue, RawCoValueID, TransactionID } from "./coValue";
import { JsonValue } from "./jsonValue";
import { RawCoValueID } from "./ids";
import { CoMap } from "./contentTypes/coMap";
import { CoStream } from "./contentTypes/coStream";
import { Static } from "./contentTypes/static";
import { CoList } from "./contentTypes/coList";
export type CoValueID<T extends ContentType> = RawCoValueID & {
readonly __type: T;
@@ -11,225 +15,6 @@ export type ContentType =
| CoStream<JsonValue, JsonValue>
| Static<JsonValue>;
type MapOp<K extends string, V extends JsonValue> = {
txID: TransactionID;
madeAt: number;
changeIdx: number;
} & MapOpPayload<K, V>;
// TODO: add after TransactionID[] for conflicts/ordering
export type MapOpPayload<K extends string, V extends JsonValue> =
| {
op: "insert";
key: K;
value: V;
}
| {
op: "delete";
key: K;
};
export class CoMap<
M extends {[key: string]: JsonValue},
Meta extends JsonValue,
K extends string = keyof M & string,
V extends JsonValue = M[K],
MM extends {[key: string]: JsonValue} = {[KK in K]: M[KK]}
> {
id: CoValueID<CoMap<MM, Meta>>;
coValue: CoValue;
type: "comap" = "comap";
ops: {[KK in K]?: MapOp<K, M[KK]>[]};
constructor(coValue: CoValue) {
this.id = coValue.id as CoValueID<CoMap<MM, Meta>>;
this.coValue = coValue;
this.ops = {};
this.fillOpsFromCoValue();
}
protected fillOpsFromCoValue() {
this.ops = {};
for (const { txID, changes, madeAt } of this.coValue.getValidSortedTransactions()) {
for (const [changeIdx, changeUntyped] of (
changes
).entries()) {
const change = changeUntyped as MapOpPayload<K, V>
let entries = this.ops[change.key];
if (!entries) {
entries = [];
this.ops[change.key] = entries;
}
entries.push({
txID,
madeAt,
changeIdx,
...(change as any),
});
}
}
}
keys(): K[] {
return Object.keys(this.ops) as K[];
}
get<KK extends K>(key: KK): M[KK] | undefined {
const ops = this.ops[key];
if (!ops) {
return undefined;
}
const lastEntry = ops[ops.length - 1]!;
if (lastEntry.op === "delete") {
return undefined;
} else {
return lastEntry.value;
}
}
getAtTime<KK extends K>(key: KK, time: number): M[KK] | undefined {
const ops = this.ops[key];
if (!ops) {
return undefined;
}
const lastOpBeforeOrAtTime = ops.findLast((op) => op.madeAt <= time);
if (!lastOpBeforeOrAtTime) {
return undefined;
}
if (lastOpBeforeOrAtTime.op === "delete") {
return undefined;
} else {
return lastOpBeforeOrAtTime.value;
}
}
getLastTxID<KK extends K>(key: KK): TransactionID | undefined {
const ops = this.ops[key];
if (!ops) {
return undefined;
}
const lastEntry = ops[ops.length - 1]!;
return lastEntry.txID;
}
getHistory<KK extends K>(key: KK): {at: number, txID: TransactionID, value: M[KK] | undefined}[] {
const ops = this.ops[key];
if (!ops) {
return [];
}
const history: {at: number, txID: TransactionID, value: M[KK] | undefined}[] = [];
for (const op of ops) {
if (op.op === "delete") {
history.push({at: op.madeAt, txID: op.txID, value: undefined});
} else {
history.push({at: op.madeAt, txID: op.txID, value: op.value});
}
}
return history;
}
toJSON(): JsonObject {
const json: JsonObject = {};
for (const key of this.keys()) {
const value = this.get(key);
if (value !== undefined) {
json[key] = value;
}
}
return json;
}
edit(changer: (editable: WriteableCoMap<M, Meta>) => void): CoMap<M, Meta> {
const editable = new WriteableCoMap<M, Meta>(this.coValue);
changer(editable);
return new CoMap(this.coValue);
}
}
export class WriteableCoMap<
M extends {[key: string]: JsonValue},
Meta extends JsonValue,
K extends string = keyof M & string,
V extends JsonValue = M[K],
MM extends {[key: string]: JsonValue} = {[KK in K]: M[KK]}
> extends CoMap<M, Meta, K, V, MM> {
set<KK extends K>(key: KK, value: M[KK], privacy: "private" | "trusting" = "private"): void {
this.coValue.makeTransaction([
{
op: "insert",
key,
value,
},
], privacy);
this.fillOpsFromCoValue();
}
delete(key: K, privacy: "private" | "trusting" = "private"): void {
this.coValue.makeTransaction([
{
op: "delete",
key,
},
], privacy);
this.fillOpsFromCoValue();
}
}
export class CoList<T extends JsonValue, Meta extends JsonValue> {
id: CoValueID<CoList<T, Meta>>;
type: "colist" = "colist";
constructor(coValue: CoValue) {
this.id = coValue.id as CoValueID<CoList<T, Meta>>;
}
toJSON(): JsonObject {
throw new Error("Method not implemented.");
}
}
export class CoStream<T extends JsonValue, Meta extends JsonValue> {
id: CoValueID<CoStream<T, Meta>>;
type: "costream" = "costream";
constructor(coValue: CoValue) {
this.id = coValue.id as CoValueID<CoStream<T, Meta>>;
}
toJSON(): JsonObject {
throw new Error("Method not implemented.");
}
}
export class Static<T extends JsonValue> {
id: CoValueID<Static<T>>;
type: "static" = "static";
constructor(coValue: CoValue) {
this.id = coValue.id as CoValueID<Static<T>>;
}
toJSON(): JsonObject {
throw new Error("Method not implemented.");
}
}
export function expectMap(content: ContentType): CoMap<{ [key: string]: string }, {}> {
if (content.type !== "comap") {
throw new Error("Expected map");

View File

@@ -0,0 +1,24 @@
import { JsonObject, JsonValue } from "../jsonValue";
import { CoValueID } from "../contentType";
import { CoValue } from "../coValue";
export class CoList<T extends JsonValue, Meta extends JsonValue> {
id: CoValueID<CoList<T, Meta>>;
type: "colist" = "colist";
coValue: CoValue;
constructor(coValue: CoValue) {
this.id = coValue.id as CoValueID<CoList<T, Meta>>;
this.coValue = coValue;
}
toJSON(): JsonObject {
throw new Error("Method not implemented.");
}
subscribe(listener: (coMap: CoList<T, Meta>) => void): () => void {
return this.coValue.subscribe((content) => {
listener(content as CoList<T, Meta>);
});
}
}

195
src/contentTypes/coMap.ts Normal file
View File

@@ -0,0 +1,195 @@
import { JsonObject, JsonValue } from "../jsonValue";
import { TransactionID } from "../ids";
import { CoValueID } from "../contentType";
import { CoValue } from "../coValue";
type MapOp<K extends string, V extends JsonValue> = {
txID: TransactionID;
madeAt: number;
changeIdx: number;
} & MapOpPayload<K, V>;
// TODO: add after TransactionID[] for conflicts/ordering
export type MapOpPayload<K extends string, V extends JsonValue> = {
op: "insert";
key: K;
value: V;
} |
{
op: "delete";
key: K;
};
export class CoMap<
M extends { [key: string]: JsonValue; },
Meta extends JsonValue,
K extends string = keyof M & string,
V extends JsonValue = M[K],
MM extends { [key: string]: JsonValue; } = {
[KK in K]: M[KK];
}
> {
id: CoValueID<CoMap<MM, Meta>>;
coValue: CoValue;
type: "comap" = "comap";
ops: {
[KK in K]?: MapOp<K, M[KK]>[];
};
constructor(coValue: CoValue) {
this.id = coValue.id as CoValueID<CoMap<MM, Meta>>;
this.coValue = coValue;
this.ops = {};
this.fillOpsFromCoValue();
}
protected fillOpsFromCoValue() {
this.ops = {};
for (const { txID, changes, madeAt } of this.coValue.getValidSortedTransactions()) {
for (const [changeIdx, changeUntyped] of (
changes
).entries()) {
const change = changeUntyped as MapOpPayload<K, V>;
let entries = this.ops[change.key];
if (!entries) {
entries = [];
this.ops[change.key] = entries;
}
entries.push({
txID,
madeAt,
changeIdx,
...(change as any),
});
}
}
}
keys(): K[] {
return Object.keys(this.ops) as K[];
}
get<KK extends K>(key: KK): M[KK] | undefined {
const ops = this.ops[key];
if (!ops) {
return undefined;
}
const lastEntry = ops[ops.length - 1]!;
if (lastEntry.op === "delete") {
return undefined;
} else {
return lastEntry.value;
}
}
getAtTime<KK extends K>(key: KK, time: number): M[KK] | undefined {
const ops = this.ops[key];
if (!ops) {
return undefined;
}
const lastOpBeforeOrAtTime = ops.findLast((op) => op.madeAt <= time);
if (!lastOpBeforeOrAtTime) {
return undefined;
}
if (lastOpBeforeOrAtTime.op === "delete") {
return undefined;
} else {
return lastOpBeforeOrAtTime.value;
}
}
getLastTxID<KK extends K>(key: KK): TransactionID | undefined {
const ops = this.ops[key];
if (!ops) {
return undefined;
}
const lastEntry = ops[ops.length - 1]!;
return lastEntry.txID;
}
getHistory<KK extends K>(key: KK): { at: number; txID: TransactionID; value: M[KK] | undefined; }[] {
const ops = this.ops[key];
if (!ops) {
return [];
}
const history: { at: number; txID: TransactionID; value: M[KK] | undefined; }[] = [];
for (const op of ops) {
if (op.op === "delete") {
history.push({ at: op.madeAt, txID: op.txID, value: undefined });
} else {
history.push({ at: op.madeAt, txID: op.txID, value: op.value });
}
}
return history;
}
toJSON(): JsonObject {
const json: JsonObject = {};
for (const key of this.keys()) {
const value = this.get(key);
if (value !== undefined) {
json[key] = value;
}
}
return json;
}
edit(changer: (editable: WriteableCoMap<M, Meta>) => void): CoMap<M, Meta> {
const editable = new WriteableCoMap<M, Meta>(this.coValue);
changer(editable);
return new CoMap(this.coValue);
}
subscribe(listener: (coMap: CoMap<M, Meta>) => void): () => void {
return this.coValue.subscribe((content) => {
listener(content as CoMap<M, Meta>);
});
}
}
export class WriteableCoMap<
M extends { [key: string]: JsonValue; },
Meta extends JsonValue,
K extends string = keyof M & string,
V extends JsonValue = M[K],
MM extends { [key: string]: JsonValue; } = {
[KK in K]: M[KK];
}
> extends CoMap<M, Meta, K, V, MM> {
set<KK extends K>(key: KK, value: M[KK], privacy: "private" | "trusting" = "private"): void {
this.coValue.makeTransaction([
{
op: "insert",
key,
value,
},
], privacy);
this.fillOpsFromCoValue();
}
delete(key: K, privacy: "private" | "trusting" = "private"): void {
this.coValue.makeTransaction([
{
op: "delete",
key,
},
], privacy);
this.fillOpsFromCoValue();
}
}

View File

@@ -0,0 +1,24 @@
import { JsonObject, JsonValue } from "../jsonValue";
import { CoValueID } from "../contentType";
import { CoValue } from "../coValue";
export class CoStream<T extends JsonValue, Meta extends JsonValue> {
id: CoValueID<CoStream<T, Meta>>;
type: "costream" = "costream";
coValue: CoValue;
constructor(coValue: CoValue) {
this.id = coValue.id as CoValueID<CoStream<T, Meta>>;
this.coValue = coValue;
}
toJSON(): JsonObject {
throw new Error("Method not implemented.");
}
subscribe(listener: (coMap: CoStream<T, Meta>) => void): () => void {
return this.coValue.subscribe((content) => {
listener(content as CoStream<T, Meta>);
});
}
}

View File

@@ -0,0 +1,22 @@
import { JsonObject, JsonValue } from "../jsonValue";
import { CoValueID } from "../contentType";
import { CoValue } from "../coValue";
export class Static<T extends JsonValue> {
id: CoValueID<Static<T>>;
type: "static" = "static";
coValue: CoValue;
constructor(coValue: CoValue) {
this.id = coValue.id as CoValueID<Static<T>>;
this.coValue = coValue;
}
toJSON(): JsonObject {
throw new Error("Method not implemented.");
}
subscribe(listener: (coMap: Static<T>) => void): () => void {
throw new Error("Method not implemented.");
}
}

View File

@@ -5,7 +5,7 @@ import { base58, base64url } from "@scure/base";
import { default as stableStringify } from "fast-json-stable-stringify";
import { blake3 } from "@noble/hashes/blake3";
import { randomBytes } from "@noble/ciphers/webcrypto/utils";
import { RawCoValueID, SessionID, TransactionID } from "./coValue";
import { RawCoValueID, TransactionID } from "./ids";
export type SignatorySecret = `signatorySecret_z${string}`;
export type SignatoryID = `signatory_z${string}`;
@@ -24,6 +24,14 @@ export function newRandomSignatory(): SignatorySecret {
)}`;
}
export function signatorySecretToBytes(secret: SignatorySecret): Uint8Array {
return base58.decode(secret.substring("signatorySecret_z".length));
}
export function signatorySecretFromBytes(bytes: Uint8Array): SignatorySecret {
return `signatorySecret_z${base58.encode(bytes)}`;
}
export function getSignatoryID(secret: SignatorySecret): SignatoryID {
return `signatory_z${base58.encode(
ed25519.getPublicKey(
@@ -56,6 +64,14 @@ export function newRandomRecipient(): RecipientSecret {
return `recipientSecret_z${base58.encode(x25519.utils.randomPrivateKey())}`;
}
export function recipientSecretToBytes(secret: RecipientSecret): Uint8Array {
return base58.decode(secret.substring("recipientSecret_z".length));
}
export function recipientSecretFromBytes(bytes: Uint8Array): RecipientSecret {
return `recipientSecret_z${base58.encode(bytes)}`;
}
export function getRecipientID(secret: RecipientSecret): RecipientID {
return `recipient_z${base58.encode(
x25519.getPublicKey(
@@ -295,3 +311,15 @@ export function unsealKeySecret(
return decrypt(sealedInfo.encrypted, sealingSecret, nOnceMaterial);
}
export function uniquenessForHeader(): `z${string}` {
return `z${base58.encode(randomBytes(12))}`;
}
export function createdNowUnique(): {createdAt: `2${string}`, uniqueness: `z${string}`} {
const createdAt = (new Date()).toISOString() as `2${string}`;
return {
createdAt,
uniqueness: uniquenessForHeader(),
}
}

7
src/ids.ts Normal file
View File

@@ -0,0 +1,7 @@
export type RawCoValueID = `co_z${string}` | `co_${string}_z${string}`;
export type TransactionID = { sessionID: SessionID; txIndex: number };
export type AgentID = `co_agent${string}_z${string}`;
export type SessionID = `${AgentID}_session_z${string}`;

View File

@@ -1,14 +1,28 @@
import { ContentType } from "./contentType";
import { JsonValue } from "./jsonValue";
import { CoValue } from "./coValue";
import type { CoValueID, ContentType } from "./contentType";
import type { JsonValue } from "./jsonValue";
import {
CoValue,
agentCredentialFromBytes,
agentCredentialToBytes,
getAgent,
getAgentID,
newRandomAgentCredential,
newRandomSessionID,
} from "./coValue";
import { LocalNode } from "./node";
import { CoMap } from "./contentTypes/coMap";
type Value = JsonValue | ContentType;
export {
JsonValue,
ContentType,
Value,
LocalNode,
CoValue
}
const internals = {
agentCredentialToBytes,
agentCredentialFromBytes,
getAgent,
getAgentID,
newRandomAgentCredential,
newRandomSessionID,
};
export { LocalNode, CoValue, CoMap, internals };
export type { Value, JsonValue, ContentType, CoValueID };

View File

@@ -1,10 +1,7 @@
import { newRandomKeySecret, seal } from "./crypto";
import { createdNowUnique, newRandomKeySecret, seal } from "./crypto";
import {
RawCoValueID,
CoValue,
AgentCredential,
AgentID,
SessionID,
Agent,
getAgent,
getAgentID,
@@ -15,6 +12,8 @@ import {
} from "./coValue";
import { Team, expectTeamContent } from "./permissions";
import { SyncManager } from "./sync";
import { AgentID, RawCoValueID, SessionID } from "./ids";
import { CoValueID, ContentType } from ".";
export class LocalNode {
coValues: { [key: RawCoValueID]: CoValueState } = {};
@@ -61,6 +60,10 @@ export class LocalNode {
return entry.done;
}
async load<T extends ContentType>(id: CoValueID<T>): Promise<T> {
return (await this.loadCoValue(id)).getCurrentContent() as T;
}
expectCoValueLoaded(id: RawCoValueID, expectation?: string): CoValue {
const entry = this.coValues[id];
if (!entry) {
@@ -112,6 +115,7 @@ export class LocalNode {
type: "comap",
ruleset: { type: "team", initialAdmin: this.agentID },
meta: null,
...createdNowUnique(),
publicNickname: "team",
});

View File

@@ -8,6 +8,7 @@ import { LocalNode } from "./node";
import { expectMap } from "./contentType";
import { expectTeamContent } from "./permissions";
import {
createdNowUnique,
getRecipientID,
newRandomKeySecret,
seal,
@@ -47,6 +48,7 @@ function newTeam() {
type: "comap",
ruleset: { type: "team", initialAdmin: adminID },
meta: null,
...createdNowUnique(),
publicNickname: "team",
});
@@ -343,6 +345,7 @@ test("Admins can write to an object that is owned by their team", () => {
type: "comap",
ruleset: { type: "ownedByTeam", team: team.id },
meta: null,
...createdNowUnique(),
publicNickname: "childObject",
});
@@ -386,6 +389,7 @@ test("Writers can write to an object that is owned by their team", () => {
type: "comap",
ruleset: { type: "ownedByTeam", team: team.id },
meta: null,
...createdNowUnique(),
publicNickname: "childObject",
});
@@ -447,6 +451,7 @@ test("Readers can not write to an object that is owned by their team", () => {
type: "comap",
ruleset: { type: "ownedByTeam", team: team.id },
meta: null,
...createdNowUnique(),
publicNickname: "childObject",
});
@@ -521,6 +526,7 @@ test("Admins can set team read key and then use it to create and read private tr
type: "comap",
ruleset: { type: "ownedByTeam", team: team.id },
meta: null,
...createdNowUnique(),
publicNickname: "childObject",
});
@@ -580,6 +586,7 @@ test("Admins can set team read key and then writers can use it to create and rea
type: "comap",
ruleset: { type: "ownedByTeam", team: team.id },
meta: null,
...createdNowUnique(),
publicNickname: "childObject",
});
@@ -660,6 +667,7 @@ test("Admins can set team read key and then use it to create private transaction
type: "comap",
ruleset: { type: "ownedByTeam", team: team.id },
meta: null,
...createdNowUnique(),
publicNickname: "childObject",
});
@@ -759,6 +767,7 @@ test("Admins can set team read key and then use it to create private transaction
type: "comap",
ruleset: { type: "ownedByTeam", team: team.id },
meta: null,
...createdNowUnique(),
publicNickname: "childObject",
});
@@ -864,6 +873,7 @@ test("Admins can set team read key, make a private transaction in an owned objec
type: "comap",
ruleset: { type: "ownedByTeam", team: team.id },
meta: null,
...createdNowUnique(),
publicNickname: "childObject",
});
@@ -944,6 +954,7 @@ test("Admins can set team read key, make a private transaction in an owned objec
type: "comap",
ruleset: { type: "ownedByTeam", team: team.id },
meta: null,
...createdNowUnique(),
publicNickname: "childObject",
});
@@ -1085,6 +1096,7 @@ test("Admins can set team read rey, make a private transaction in an owned objec
type: "comap",
ruleset: { type: "ownedByTeam", team: team.id },
meta: null,
...createdNowUnique(),
publicNickname: "childObject",
});
@@ -1267,3 +1279,23 @@ test("Admins can set team read rey, make a private transaction in an owned objec
).get("foo3")
).toBeUndefined();
});
test("Can create two owned objects in the same team and they will have different ids", () => {
const { node, team, admin, adminID } = newTeam();
const childObject1 = node.createCoValue({
type: "comap",
ruleset: { type: "ownedByTeam", team: team.id },
meta: null,
...createdNowUnique()
});
const childObject2 = node.createCoValue({
type: "comap",
ruleset: { type: "ownedByTeam", team: team.id },
meta: null,
...createdNowUnique()
});
expect(childObject1.id).not.toEqual(childObject2.id);
});

View File

@@ -1,4 +1,5 @@
import { CoMap, ContentType, MapOpPayload } from "./contentType";
import { ContentType } from "./contentType";
import { CoMap, MapOpPayload } from "./contentTypes/coMap";
import { JsonValue } from "./jsonValue";
import {
Encrypted,
@@ -7,23 +8,20 @@ import {
RecipientID,
SealedSet,
SignatoryID,
encryptForTransaction,
createdNowUnique,
newRandomKeySecret,
seal,
sealKeySecret,
} from "./crypto";
import {
AgentCredential,
AgentID,
CoValue,
RawCoValueID,
SessionID,
Transaction,
TransactionID,
TrustingTransaction,
agentIDfromSessionID,
} from "./coValue";
import { LocalNode } from ".";
import { AgentID, RawCoValueID, SessionID, TransactionID } from "./ids";
export type PermissionsDef =
| { type: "team"; initialAdmin: AgentID; parentTeams?: RawCoValueID[] }
@@ -355,6 +353,7 @@ export class Team {
team: this.teamMap.id,
},
meta: meta || null,
...createdNowUnique(),
publicNickname: "map",
})
.getCurrentContent() as CoMap<M, Meta>;

View File

@@ -1,5 +1,4 @@
import {
AgentID,
getAgent,
getAgentID,
newRandomAgentCredential,
@@ -7,13 +6,15 @@ import {
} from "./coValue";
import { LocalNode } from "./node";
import { Peer, PeerID, SyncMessage } from "./sync";
import { MapOpPayload, expectMap } from "./contentType";
import { expectMap } from "./contentType";
import { MapOpPayload } from "./contentTypes/coMap";
import { Team } from "./permissions";
import {
ReadableStream,
WritableStream,
TransformStream,
} from "isomorphic-streams";
import { AgentID } from "./ids";
test(
"Node replies with initial tx and header to empty subscribe",
@@ -73,6 +74,8 @@ test(
type: "comap",
ruleset: { type: "ownedByTeam", team: team.id },
meta: null,
createdAt: map.coValue.header.createdAt,
uniqueness: map.coValue.header.uniqueness,
publicNickname: "map",
},
newContent: {
@@ -609,8 +612,6 @@ test("If we add a server peer, newly created coValues are auto-subscribed to", a
const team = node.createTeam();
team.createMap();
const [inRx, _inTx] = newStreamPair<SyncMessage>();
const [outRx, outTx] = newStreamPair<SyncMessage>();

View File

@@ -1,9 +1,10 @@
import { Hash, Signature } from "./crypto";
import { CoValueHeader, RawCoValueID, SessionID, Transaction } from "./coValue";
import { CoValueHeader, Transaction } from "./coValue";
import { CoValue } from "./coValue";
import { LocalNode } from "./node";
import { newLoadingState } from "./node";
import { ReadableStream, WritableStream, WritableStreamDefaultWriter } from "isomorphic-streams";
import { RawCoValueID, SessionID } from "./ids";
export type CoValueKnownState = {
coValueID: RawCoValueID;
@@ -79,31 +80,6 @@ export interface PeerState {
role: "peer" | "server" | "client";
}
export function weAreStrictlyAhead(
ourKnownState: CoValueKnownState,
theirKnownState: CoValueKnownState
): boolean {
if (theirKnownState.header && !ourKnownState.header) {
return false;
}
const allSessions = new Set([
...(Object.keys(ourKnownState.sessions) as SessionID[]),
...(Object.keys(theirKnownState.sessions) as SessionID[]),
]);
for (const sessionID of allSessions) {
const ourSession = ourKnownState.sessions[sessionID];
const theirSession = theirKnownState.sessions[sessionID];
if ((ourSession || 0) < (theirSession || 0)) {
return false;
}
}
return true;
}
export function combinedKnownStates(
stateA: CoValueKnownState,
stateB: CoValueKnownState
@@ -480,11 +456,7 @@ export class SyncManager {
for (const peer of Object.values(this.peers)) {
const optimisticKnownState = peer.optimisticKnownStates[coValue.id];
const shouldSync =
optimisticKnownState ||
peer.role === "server";
if (shouldSync) {
if (optimisticKnownState) {
await this.tellUntoldKnownStateIncludingDependencies(
coValue.id,
peer
@@ -493,6 +465,15 @@ export class SyncManager {
coValue.id,
peer
);
} else if (peer.role === "server") {
await this.subscribeToIncludingDependencies(
coValue.id,
peer
);
await this.sendNewContentIncludingDependencies(
coValue.id,
peer
);
}
}
}