Lots of improvements
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
111
src/coValue.ts
111
src/coValue.ts
@@ -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}>;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
24
src/contentTypes/coList.ts
Normal file
24
src/contentTypes/coList.ts
Normal 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
195
src/contentTypes/coMap.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
24
src/contentTypes/coStream.ts
Normal file
24
src/contentTypes/coStream.ts
Normal 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>);
|
||||
});
|
||||
}
|
||||
}
|
||||
22
src/contentTypes/static.ts
Normal file
22
src/contentTypes/static.ts
Normal 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.");
|
||||
}
|
||||
}
|
||||
@@ -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
7
src/ids.ts
Normal 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}`;
|
||||
34
src/index.ts
34
src/index.ts
@@ -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 };
|
||||
|
||||
12
src/node.ts
12
src/node.ts
@@ -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",
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
43
src/sync.ts
43
src/sync.ts
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user