cojson 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "cojson",
3
3
  "module": "src/index.ts",
4
+ "types": "src/index.ts",
4
5
  "type": "module",
5
6
  "license": "MIT",
6
- "version": "0.0.2",
7
+ "version": "0.0.4",
7
8
  "devDependencies": {
8
9
  "@types/jest": "^29.5.3",
9
10
  "@typescript-eslint/eslint-plugin": "^6.2.1",
@@ -7,7 +7,7 @@ import {
7
7
  newRandomSessionID,
8
8
  } from "./coValue";
9
9
  import { LocalNode } from "./node";
10
- import { sign } from "./crypto";
10
+ import { createdNowUnique, sign, uniquenessForHeader } from "./crypto";
11
11
 
12
12
  test("Can create coValue with new agent credentials and add transaction to it", () => {
13
13
  const agentCredential = newRandomAgentCredential("agent1");
@@ -20,6 +20,7 @@ test("Can create coValue with new agent credentials and add transaction to it",
20
20
  type: "costream",
21
21
  ruleset: { type: "unsafeAllowAll" },
22
22
  meta: null,
23
+ ...createdNowUnique()
23
24
  });
24
25
 
25
26
  const transaction: Transaction = {
@@ -59,6 +60,7 @@ test("transactions with wrong signature are rejected", () => {
59
60
  type: "costream",
60
61
  ruleset: { type: "unsafeAllowAll" },
61
62
  meta: null,
63
+ ...createdNowUnique()
62
64
  });
63
65
 
64
66
  const transaction: Transaction = {
@@ -97,6 +99,7 @@ test("transactions with correctly signed, but wrong hash are rejected", () => {
97
99
  type: "costream",
98
100
  ruleset: { type: "unsafeAllowAll" },
99
101
  meta: null,
102
+ ...createdNowUnique()
100
103
  });
101
104
 
102
105
  const transaction: Transaction = {
package/src/coValue.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { randomBytes } from "@noble/hashes/utils";
2
- import { CoList, CoMap, ContentType, Static, CoStream } from "./contentType";
2
+ import { ContentType } from "./contentType";
3
+ import { Static } from "./contentTypes/static";
4
+ import { CoStream } from "./contentTypes/coStream";
5
+ import { CoMap } from "./contentTypes/coMap";
3
6
  import {
4
7
  Encrypted,
5
8
  Hash,
@@ -22,23 +25,30 @@ import {
22
25
  decryptForTransaction,
23
26
  KeyID,
24
27
  unsealKeySecret,
28
+ signatorySecretToBytes,
29
+ recipientSecretToBytes,
30
+ signatorySecretFromBytes,
31
+ recipientSecretFromBytes,
25
32
  } from "./crypto";
26
33
  import { JsonValue } from "./jsonValue";
27
34
  import { base58 } from "@scure/base";
28
35
  import {
29
36
  PermissionsDef as RulesetDef,
37
+ Team,
30
38
  determineValidTransactions,
31
39
  expectTeamContent,
32
40
  } from "./permissions";
33
41
  import { LocalNode } from "./node";
34
42
  import { CoValueKnownState, NewContentMessage } from "./sync";
35
-
36
- export type RawCoValueID = `co_z${string}` | `co_${string}_z${string}`;
43
+ import { AgentID, RawCoValueID, SessionID, TransactionID } from "./ids";
44
+ import { CoList } from "./contentTypes/coList";
37
45
 
38
46
  export type CoValueHeader = {
39
47
  type: ContentType["type"];
40
48
  ruleset: RulesetDef;
41
49
  meta: JsonValue;
50
+ createdAt: `2${string}` | null;
51
+ uniqueness: `z${string}` | null;
42
52
  publicNickname?: string;
43
53
  };
44
54
 
@@ -53,8 +63,6 @@ function coValueIDforHeader(header: CoValueHeader): RawCoValueID {
53
63
  }
54
64
  }
55
65
 
56
- export type SessionID = `${AgentID}_session_z${string}`;
57
-
58
66
  export function agentIDfromSessionID(sessionID: SessionID): AgentID {
59
67
  return sessionID.split("_session")[0] as AgentID;
60
68
  }
@@ -94,14 +102,13 @@ export type DecryptedTransaction = {
94
102
  madeAt: number;
95
103
  };
96
104
 
97
- export type TransactionID = { sessionID: SessionID; txIndex: number };
98
-
99
105
  export class CoValue {
100
106
  id: RawCoValueID;
101
107
  node: LocalNode;
102
108
  header: CoValueHeader;
103
109
  sessions: { [key: SessionID]: SessionLog };
104
110
  content?: ContentType;
111
+ listeners: Set<(content?: ContentType) => void> = new Set();
105
112
 
106
113
  constructor(header: CoValueHeader, node: LocalNode) {
107
114
  this.id = coValueIDforHeader(header);
@@ -185,6 +192,8 @@ export class CoValue {
185
192
 
186
193
  const transactions = this.sessions[sessionID]?.transactions ?? [];
187
194
 
195
+ console.log("transactions before", this.id, transactions.length, this.getValidSortedTransactions().length);
196
+
188
197
  transactions.push(...newTransactions);
189
198
 
190
199
  this.sessions[sessionID] = {
@@ -196,11 +205,28 @@ export class CoValue {
196
205
 
197
206
  this.content = undefined;
198
207
 
199
- const _ = this.getCurrentContent();
208
+ console.log("transactions after", this.id, transactions.length, this.getValidSortedTransactions().length);
209
+
210
+ const content = this.getCurrentContent();
211
+
212
+ for (const listener of this.listeners) {
213
+ console.log("Calling listener (update)", this.id, content.toJSON());
214
+ listener(content);
215
+ }
200
216
 
201
217
  return true;
202
218
  }
203
219
 
220
+ subscribe(listener: (content?: ContentType) => void): () => void {
221
+ this.listeners.add(listener);
222
+ console.log("Calling listener (initial)", this.id, this.getCurrentContent().toJSON());
223
+ listener(this.getCurrentContent());
224
+
225
+ return () => {
226
+ this.listeners.delete(listener);
227
+ };
228
+ }
229
+
204
230
  expectedNewHashAfter(
205
231
  sessionID: SessionID,
206
232
  newTransactions: Transaction[]
@@ -232,7 +258,9 @@ export class CoValue {
232
258
  const { secret: keySecret, id: keyID } = this.getCurrentReadKey();
233
259
 
234
260
  if (!keySecret) {
235
- throw new Error("Can't make transaction without read key secret");
261
+ throw new Error(
262
+ "Can't make transaction without read key secret"
263
+ );
236
264
  }
237
265
 
238
266
  transaction = {
@@ -300,8 +328,8 @@ export class CoValue {
300
328
  getValidSortedTransactions(): DecryptedTransaction[] {
301
329
  const validTransactions = determineValidTransactions(this);
302
330
 
303
- const allTransactions: DecryptedTransaction[] = validTransactions.map(
304
- ({ txID, tx }) => {
331
+ const allTransactions: DecryptedTransaction[] = validTransactions
332
+ .map(({ txID, tx }) => {
305
333
  if (tx.privacy === "trusting") {
306
334
  return {
307
335
  txID,
@@ -324,7 +352,9 @@ export class CoValue {
324
352
  );
325
353
 
326
354
  if (!decrytedChanges) {
327
- console.error("Failed to decrypt transaction despite having key");
355
+ console.error(
356
+ "Failed to decrypt transaction despite having key"
357
+ );
328
358
  return undefined;
329
359
  }
330
360
  return {
@@ -334,8 +364,8 @@ export class CoValue {
334
364
  };
335
365
  }
336
366
  }
337
- }
338
- ).filter((x): x is Exclude<typeof x, undefined> => !!x);
367
+ })
368
+ .filter((x): x is Exclude<typeof x, undefined> => !!x);
339
369
  allTransactions.sort(
340
370
  (a, b) =>
341
371
  a.madeAt - b.madeAt ||
@@ -446,6 +476,21 @@ export class CoValue {
446
476
  }
447
477
  }
448
478
 
479
+ getTeam(): Team {
480
+ if (this.header.ruleset.type !== "ownedByTeam") {
481
+ throw new Error("Only values owned by teams have teams");
482
+ }
483
+
484
+ return new Team(
485
+ expectTeamContent(
486
+ this.node
487
+ .expectCoValueLoaded(this.header.ruleset.team)
488
+ .getCurrentContent()
489
+ ),
490
+ this.node
491
+ );
492
+ }
493
+
449
494
  getTx(txID: TransactionID): Transaction | undefined {
450
495
  return this.sessions[txID.sessionID]?.transactions[txID.txIndex];
451
496
  }
@@ -510,8 +555,6 @@ export class CoValue {
510
555
  }
511
556
  }
512
557
 
513
- export type AgentID = `co_agent${string}_z${string}`;
514
-
515
558
  export type Agent = {
516
559
  signatoryID: SignatoryID;
517
560
  recipientID: RecipientID;
@@ -535,6 +578,8 @@ export function getAgentCoValueHeader(agent: Agent): CoValueHeader {
535
578
  initialRecipientID: agent.recipientID,
536
579
  },
537
580
  meta: null,
581
+ createdAt: null,
582
+ uniqueness: null,
538
583
  publicNickname:
539
584
  "agent" + (agent.publicNickname ? `-${agent.publicNickname}` : ""),
540
585
  };
@@ -551,13 +596,45 @@ export type AgentCredential = {
551
596
  };
552
597
 
553
598
  export function newRandomAgentCredential(
554
- publicNickname: string
599
+ publicNickname?: string
555
600
  ): AgentCredential {
556
601
  const signatorySecret = newRandomSignatory();
557
602
  const recipientSecret = newRandomRecipient();
558
603
  return { signatorySecret, recipientSecret, publicNickname };
559
604
  }
560
605
 
606
+ export function agentCredentialToBytes(cred: AgentCredential): Uint8Array {
607
+ if (cred.publicNickname) {
608
+ throw new Error("Can't convert agent credential with publicNickname");
609
+ }
610
+ const bytes = new Uint8Array(64);
611
+ const signatorySecretBytes = signatorySecretToBytes(cred.signatorySecret);
612
+ if (signatorySecretBytes.length !== 32) {
613
+ throw new Error("Invalid signatorySecret length");
614
+ }
615
+ bytes.set(signatorySecretBytes);
616
+ const recipientSecretBytes = recipientSecretToBytes(cred.recipientSecret);
617
+ if (recipientSecretBytes.length !== 32) {
618
+ throw new Error("Invalid recipientSecret length");
619
+ }
620
+ bytes.set(recipientSecretBytes, 32);
621
+
622
+ return bytes;
623
+ }
624
+
625
+ export function agentCredentialFromBytes(
626
+ bytes: Uint8Array
627
+ ): AgentCredential | undefined {
628
+ if (bytes.length !== 64) {
629
+ return undefined;
630
+ }
631
+
632
+ const signatorySecret = signatorySecretFromBytes(bytes.slice(0, 32));
633
+ const recipientSecret = recipientSecretFromBytes(bytes.slice(32));
634
+
635
+ return { signatorySecret, recipientSecret };
636
+ }
637
+
561
638
  // type Role = "admin" | "writer" | "reader";
562
639
 
563
640
  // type PermissionsDef = CJMap<AgentID, Role, {[agent: AgentID]: Role}>;
@@ -5,6 +5,7 @@ import {
5
5
  newRandomAgentCredential,
6
6
  newRandomSessionID,
7
7
  } from "./coValue";
8
+ import { createdNowUnique } from "./crypto";
8
9
  import { LocalNode } from "./node";
9
10
 
10
11
  test("Empty COJSON Map works", () => {
@@ -18,6 +19,7 @@ test("Empty COJSON Map works", () => {
18
19
  type: "comap",
19
20
  ruleset: { type: "unsafeAllowAll" },
20
21
  meta: null,
22
+ ...createdNowUnique()
21
23
  });
22
24
 
23
25
  const content = coValue.getCurrentContent();
@@ -42,6 +44,7 @@ test("Can insert and delete Map entries in edit()", () => {
42
44
  type: "comap",
43
45
  ruleset: { type: "unsafeAllowAll" },
44
46
  meta: null,
47
+ ...createdNowUnique()
45
48
  });
46
49
 
47
50
  const content = coValue.getCurrentContent();
@@ -74,6 +77,7 @@ test("Can get map entry values at different points in time", () => {
74
77
  type: "comap",
75
78
  ruleset: { type: "unsafeAllowAll" },
76
79
  meta: null,
80
+ ...createdNowUnique()
77
81
  });
78
82
 
79
83
  const content = coValue.getCurrentContent();
@@ -113,6 +117,7 @@ test("Can get all historic values of key", () => {
113
117
  type: "comap",
114
118
  ruleset: { type: "unsafeAllowAll" },
115
119
  meta: null,
120
+ ...createdNowUnique()
116
121
  });
117
122
 
118
123
  const content = coValue.getCurrentContent();
@@ -170,6 +175,7 @@ test("Can get last tx ID for a key", () => {
170
175
  type: "comap",
171
176
  ruleset: { type: "unsafeAllowAll" },
172
177
  meta: null,
178
+ ...createdNowUnique()
173
179
  });
174
180
 
175
181
  const content = coValue.getCurrentContent();
@@ -1,5 +1,9 @@
1
- import { JsonAtom, JsonObject, JsonValue } from "./jsonValue";
2
- import { CoValue, RawCoValueID, TransactionID } from "./coValue";
1
+ import { JsonValue } from "./jsonValue";
2
+ import { RawCoValueID } from "./ids";
3
+ import { CoMap } from "./contentTypes/coMap";
4
+ import { CoStream } from "./contentTypes/coStream";
5
+ import { Static } from "./contentTypes/static";
6
+ import { CoList } from "./contentTypes/coList";
3
7
 
4
8
  export type CoValueID<T extends ContentType> = RawCoValueID & {
5
9
  readonly __type: T;
@@ -11,225 +15,6 @@ export type ContentType =
11
15
  | CoStream<JsonValue, JsonValue>
12
16
  | Static<JsonValue>;
13
17
 
14
- type MapOp<K extends string, V extends JsonValue> = {
15
- txID: TransactionID;
16
- madeAt: number;
17
- changeIdx: number;
18
- } & MapOpPayload<K, V>;
19
-
20
- // TODO: add after TransactionID[] for conflicts/ordering
21
- export type MapOpPayload<K extends string, V extends JsonValue> =
22
- | {
23
- op: "insert";
24
- key: K;
25
- value: V;
26
- }
27
- | {
28
- op: "delete";
29
- key: K;
30
- };
31
-
32
- export class CoMap<
33
- M extends {[key: string]: JsonValue},
34
- Meta extends JsonValue,
35
- K extends string = keyof M & string,
36
- V extends JsonValue = M[K],
37
- MM extends {[key: string]: JsonValue} = {[KK in K]: M[KK]}
38
- > {
39
- id: CoValueID<CoMap<MM, Meta>>;
40
- coValue: CoValue;
41
- type: "comap" = "comap";
42
- ops: {[KK in K]?: MapOp<K, M[KK]>[]};
43
-
44
- constructor(coValue: CoValue) {
45
- this.id = coValue.id as CoValueID<CoMap<MM, Meta>>;
46
- this.coValue = coValue;
47
- this.ops = {};
48
-
49
- this.fillOpsFromCoValue();
50
- }
51
-
52
- protected fillOpsFromCoValue() {
53
- this.ops = {};
54
-
55
- for (const { txID, changes, madeAt } of this.coValue.getValidSortedTransactions()) {
56
- for (const [changeIdx, changeUntyped] of (
57
- changes
58
- ).entries()) {
59
- const change = changeUntyped as MapOpPayload<K, V>
60
- let entries = this.ops[change.key];
61
- if (!entries) {
62
- entries = [];
63
- this.ops[change.key] = entries;
64
- }
65
- entries.push({
66
- txID,
67
- madeAt,
68
- changeIdx,
69
- ...(change as any),
70
- });
71
- }
72
- }
73
- }
74
-
75
- keys(): K[] {
76
- return Object.keys(this.ops) as K[];
77
- }
78
-
79
- get<KK extends K>(key: KK): M[KK] | undefined {
80
- const ops = this.ops[key];
81
- if (!ops) {
82
- return undefined;
83
- }
84
-
85
- const lastEntry = ops[ops.length - 1]!;
86
-
87
- if (lastEntry.op === "delete") {
88
- return undefined;
89
- } else {
90
- return lastEntry.value;
91
- }
92
- }
93
-
94
- getAtTime<KK extends K>(key: KK, time: number): M[KK] | undefined {
95
- const ops = this.ops[key];
96
- if (!ops) {
97
- return undefined;
98
- }
99
-
100
- const lastOpBeforeOrAtTime = ops.findLast((op) => op.madeAt <= time);
101
-
102
- if (!lastOpBeforeOrAtTime) {
103
- return undefined;
104
- }
105
-
106
- if (lastOpBeforeOrAtTime.op === "delete") {
107
- return undefined;
108
- } else {
109
- return lastOpBeforeOrAtTime.value;
110
- }
111
- }
112
-
113
- getLastTxID<KK extends K>(key: KK): TransactionID | undefined {
114
- const ops = this.ops[key];
115
- if (!ops) {
116
- return undefined;
117
- }
118
-
119
- const lastEntry = ops[ops.length - 1]!;
120
-
121
- return lastEntry.txID;
122
- }
123
-
124
- getHistory<KK extends K>(key: KK): {at: number, txID: TransactionID, value: M[KK] | undefined}[] {
125
- const ops = this.ops[key];
126
- if (!ops) {
127
- return [];
128
- }
129
-
130
- const history: {at: number, txID: TransactionID, value: M[KK] | undefined}[] = [];
131
-
132
- for (const op of ops) {
133
- if (op.op === "delete") {
134
- history.push({at: op.madeAt, txID: op.txID, value: undefined});
135
- } else {
136
- history.push({at: op.madeAt, txID: op.txID, value: op.value});
137
- }
138
- }
139
-
140
- return history;
141
- }
142
-
143
- toJSON(): JsonObject {
144
- const json: JsonObject = {};
145
-
146
- for (const key of this.keys()) {
147
- const value = this.get(key);
148
- if (value !== undefined) {
149
- json[key] = value;
150
- }
151
- }
152
-
153
- return json;
154
- }
155
-
156
- edit(changer: (editable: WriteableCoMap<M, Meta>) => void): CoMap<M, Meta> {
157
- const editable = new WriteableCoMap<M, Meta>(this.coValue);
158
- changer(editable);
159
- return new CoMap(this.coValue);
160
- }
161
- }
162
-
163
- export class WriteableCoMap<
164
- M extends {[key: string]: JsonValue},
165
- Meta extends JsonValue,
166
- K extends string = keyof M & string,
167
- V extends JsonValue = M[K],
168
- MM extends {[key: string]: JsonValue} = {[KK in K]: M[KK]}
169
- > extends CoMap<M, Meta, K, V, MM> {
170
- set<KK extends K>(key: KK, value: M[KK], privacy: "private" | "trusting" = "private"): void {
171
- this.coValue.makeTransaction([
172
- {
173
- op: "insert",
174
- key,
175
- value,
176
- },
177
- ], privacy);
178
-
179
- this.fillOpsFromCoValue();
180
- }
181
-
182
- delete(key: K, privacy: "private" | "trusting" = "private"): void {
183
- this.coValue.makeTransaction([
184
- {
185
- op: "delete",
186
- key,
187
- },
188
- ], privacy);
189
-
190
- this.fillOpsFromCoValue();
191
- }
192
- }
193
-
194
- export class CoList<T extends JsonValue, Meta extends JsonValue> {
195
- id: CoValueID<CoList<T, Meta>>;
196
- type: "colist" = "colist";
197
-
198
- constructor(coValue: CoValue) {
199
- this.id = coValue.id as CoValueID<CoList<T, Meta>>;
200
- }
201
-
202
- toJSON(): JsonObject {
203
- throw new Error("Method not implemented.");
204
- }
205
- }
206
-
207
- export class CoStream<T extends JsonValue, Meta extends JsonValue> {
208
- id: CoValueID<CoStream<T, Meta>>;
209
- type: "costream" = "costream";
210
-
211
- constructor(coValue: CoValue) {
212
- this.id = coValue.id as CoValueID<CoStream<T, Meta>>;
213
- }
214
-
215
- toJSON(): JsonObject {
216
- throw new Error("Method not implemented.");
217
- }
218
- }
219
-
220
- export class Static<T extends JsonValue> {
221
- id: CoValueID<Static<T>>;
222
- type: "static" = "static";
223
-
224
- constructor(coValue: CoValue) {
225
- this.id = coValue.id as CoValueID<Static<T>>;
226
- }
227
-
228
- toJSON(): JsonObject {
229
- throw new Error("Method not implemented.");
230
- }
231
- }
232
-
233
18
  export function expectMap(content: ContentType): CoMap<{ [key: string]: string }, {}> {
234
19
  if (content.type !== "comap") {
235
20
  throw new Error("Expected map");
@@ -0,0 +1,24 @@
1
+ import { JsonObject, JsonValue } from "../jsonValue";
2
+ import { CoValueID } from "../contentType";
3
+ import { CoValue } from "../coValue";
4
+
5
+ export class CoList<T extends JsonValue, Meta extends JsonValue> {
6
+ id: CoValueID<CoList<T, Meta>>;
7
+ type: "colist" = "colist";
8
+ coValue: CoValue;
9
+
10
+ constructor(coValue: CoValue) {
11
+ this.id = coValue.id as CoValueID<CoList<T, Meta>>;
12
+ this.coValue = coValue;
13
+ }
14
+
15
+ toJSON(): JsonObject {
16
+ throw new Error("Method not implemented.");
17
+ }
18
+
19
+ subscribe(listener: (coMap: CoList<T, Meta>) => void): () => void {
20
+ return this.coValue.subscribe((content) => {
21
+ listener(content as CoList<T, Meta>);
22
+ });
23
+ }
24
+ }
@@ -0,0 +1,195 @@
1
+ import { JsonObject, JsonValue } from "../jsonValue";
2
+ import { TransactionID } from "../ids";
3
+ import { CoValueID } from "../contentType";
4
+ import { CoValue } from "../coValue";
5
+
6
+ type MapOp<K extends string, V extends JsonValue> = {
7
+ txID: TransactionID;
8
+ madeAt: number;
9
+ changeIdx: number;
10
+ } & MapOpPayload<K, V>;
11
+ // TODO: add after TransactionID[] for conflicts/ordering
12
+
13
+ export type MapOpPayload<K extends string, V extends JsonValue> = {
14
+ op: "insert";
15
+ key: K;
16
+ value: V;
17
+ } |
18
+ {
19
+ op: "delete";
20
+ key: K;
21
+ };
22
+
23
+ export class CoMap<
24
+ M extends { [key: string]: JsonValue; },
25
+ Meta extends JsonValue,
26
+ K extends string = keyof M & string,
27
+ V extends JsonValue = M[K],
28
+ MM extends { [key: string]: JsonValue; } = {
29
+ [KK in K]: M[KK];
30
+ }
31
+ > {
32
+ id: CoValueID<CoMap<MM, Meta>>;
33
+ coValue: CoValue;
34
+ type: "comap" = "comap";
35
+ ops: {
36
+ [KK in K]?: MapOp<K, M[KK]>[];
37
+ };
38
+
39
+ constructor(coValue: CoValue) {
40
+ this.id = coValue.id as CoValueID<CoMap<MM, Meta>>;
41
+ this.coValue = coValue;
42
+ this.ops = {};
43
+
44
+ this.fillOpsFromCoValue();
45
+ }
46
+
47
+ protected fillOpsFromCoValue() {
48
+ this.ops = {};
49
+
50
+ for (const { txID, changes, madeAt } of this.coValue.getValidSortedTransactions()) {
51
+ for (const [changeIdx, changeUntyped] of (
52
+ changes
53
+ ).entries()) {
54
+ const change = changeUntyped as MapOpPayload<K, V>;
55
+ let entries = this.ops[change.key];
56
+ if (!entries) {
57
+ entries = [];
58
+ this.ops[change.key] = entries;
59
+ }
60
+ entries.push({
61
+ txID,
62
+ madeAt,
63
+ changeIdx,
64
+ ...(change as any),
65
+ });
66
+ }
67
+ }
68
+ }
69
+
70
+ keys(): K[] {
71
+ return Object.keys(this.ops) as K[];
72
+ }
73
+
74
+ get<KK extends K>(key: KK): M[KK] | undefined {
75
+ const ops = this.ops[key];
76
+ if (!ops) {
77
+ return undefined;
78
+ }
79
+
80
+ const lastEntry = ops[ops.length - 1]!;
81
+
82
+ if (lastEntry.op === "delete") {
83
+ return undefined;
84
+ } else {
85
+ return lastEntry.value;
86
+ }
87
+ }
88
+
89
+ getAtTime<KK extends K>(key: KK, time: number): M[KK] | undefined {
90
+ const ops = this.ops[key];
91
+ if (!ops) {
92
+ return undefined;
93
+ }
94
+
95
+ const lastOpBeforeOrAtTime = ops.findLast((op) => op.madeAt <= time);
96
+
97
+ if (!lastOpBeforeOrAtTime) {
98
+ return undefined;
99
+ }
100
+
101
+ if (lastOpBeforeOrAtTime.op === "delete") {
102
+ return undefined;
103
+ } else {
104
+ return lastOpBeforeOrAtTime.value;
105
+ }
106
+ }
107
+
108
+ getLastTxID<KK extends K>(key: KK): TransactionID | undefined {
109
+ const ops = this.ops[key];
110
+ if (!ops) {
111
+ return undefined;
112
+ }
113
+
114
+ const lastEntry = ops[ops.length - 1]!;
115
+
116
+ return lastEntry.txID;
117
+ }
118
+
119
+ getHistory<KK extends K>(key: KK): { at: number; txID: TransactionID; value: M[KK] | undefined; }[] {
120
+ const ops = this.ops[key];
121
+ if (!ops) {
122
+ return [];
123
+ }
124
+
125
+ const history: { at: number; txID: TransactionID; value: M[KK] | undefined; }[] = [];
126
+
127
+ for (const op of ops) {
128
+ if (op.op === "delete") {
129
+ history.push({ at: op.madeAt, txID: op.txID, value: undefined });
130
+ } else {
131
+ history.push({ at: op.madeAt, txID: op.txID, value: op.value });
132
+ }
133
+ }
134
+
135
+ return history;
136
+ }
137
+
138
+ toJSON(): JsonObject {
139
+ const json: JsonObject = {};
140
+
141
+ for (const key of this.keys()) {
142
+ const value = this.get(key);
143
+ if (value !== undefined) {
144
+ json[key] = value;
145
+ }
146
+ }
147
+
148
+ return json;
149
+ }
150
+
151
+ edit(changer: (editable: WriteableCoMap<M, Meta>) => void): CoMap<M, Meta> {
152
+ const editable = new WriteableCoMap<M, Meta>(this.coValue);
153
+ changer(editable);
154
+ return new CoMap(this.coValue);
155
+ }
156
+
157
+ subscribe(listener: (coMap: CoMap<M, Meta>) => void): () => void {
158
+ return this.coValue.subscribe((content) => {
159
+ listener(content as CoMap<M, Meta>);
160
+ });
161
+ }
162
+ }
163
+
164
+ export class WriteableCoMap<
165
+ M extends { [key: string]: JsonValue; },
166
+ Meta extends JsonValue,
167
+ K extends string = keyof M & string,
168
+ V extends JsonValue = M[K],
169
+ MM extends { [key: string]: JsonValue; } = {
170
+ [KK in K]: M[KK];
171
+ }
172
+ > extends CoMap<M, Meta, K, V, MM> {
173
+ set<KK extends K>(key: KK, value: M[KK], privacy: "private" | "trusting" = "private"): void {
174
+ this.coValue.makeTransaction([
175
+ {
176
+ op: "insert",
177
+ key,
178
+ value,
179
+ },
180
+ ], privacy);
181
+
182
+ this.fillOpsFromCoValue();
183
+ }
184
+
185
+ delete(key: K, privacy: "private" | "trusting" = "private"): void {
186
+ this.coValue.makeTransaction([
187
+ {
188
+ op: "delete",
189
+ key,
190
+ },
191
+ ], privacy);
192
+
193
+ this.fillOpsFromCoValue();
194
+ }
195
+ }
@@ -0,0 +1,24 @@
1
+ import { JsonObject, JsonValue } from "../jsonValue";
2
+ import { CoValueID } from "../contentType";
3
+ import { CoValue } from "../coValue";
4
+
5
+ export class CoStream<T extends JsonValue, Meta extends JsonValue> {
6
+ id: CoValueID<CoStream<T, Meta>>;
7
+ type: "costream" = "costream";
8
+ coValue: CoValue;
9
+
10
+ constructor(coValue: CoValue) {
11
+ this.id = coValue.id as CoValueID<CoStream<T, Meta>>;
12
+ this.coValue = coValue;
13
+ }
14
+
15
+ toJSON(): JsonObject {
16
+ throw new Error("Method not implemented.");
17
+ }
18
+
19
+ subscribe(listener: (coMap: CoStream<T, Meta>) => void): () => void {
20
+ return this.coValue.subscribe((content) => {
21
+ listener(content as CoStream<T, Meta>);
22
+ });
23
+ }
24
+ }
@@ -0,0 +1,22 @@
1
+ import { JsonObject, JsonValue } from "../jsonValue";
2
+ import { CoValueID } from "../contentType";
3
+ import { CoValue } from "../coValue";
4
+
5
+ export class Static<T extends JsonValue> {
6
+ id: CoValueID<Static<T>>;
7
+ type: "static" = "static";
8
+ coValue: CoValue;
9
+
10
+ constructor(coValue: CoValue) {
11
+ this.id = coValue.id as CoValueID<Static<T>>;
12
+ this.coValue = coValue;
13
+ }
14
+
15
+ toJSON(): JsonObject {
16
+ throw new Error("Method not implemented.");
17
+ }
18
+
19
+ subscribe(listener: (coMap: Static<T>) => void): () => void {
20
+ throw new Error("Method not implemented.");
21
+ }
22
+ }
package/src/crypto.ts CHANGED
@@ -5,7 +5,7 @@ import { base58, base64url } from "@scure/base";
5
5
  import { default as stableStringify } from "fast-json-stable-stringify";
6
6
  import { blake3 } from "@noble/hashes/blake3";
7
7
  import { randomBytes } from "@noble/ciphers/webcrypto/utils";
8
- import { RawCoValueID, SessionID, TransactionID } from "./coValue";
8
+ import { RawCoValueID, TransactionID } from "./ids";
9
9
 
10
10
  export type SignatorySecret = `signatorySecret_z${string}`;
11
11
  export type SignatoryID = `signatory_z${string}`;
@@ -24,6 +24,14 @@ export function newRandomSignatory(): SignatorySecret {
24
24
  )}`;
25
25
  }
26
26
 
27
+ export function signatorySecretToBytes(secret: SignatorySecret): Uint8Array {
28
+ return base58.decode(secret.substring("signatorySecret_z".length));
29
+ }
30
+
31
+ export function signatorySecretFromBytes(bytes: Uint8Array): SignatorySecret {
32
+ return `signatorySecret_z${base58.encode(bytes)}`;
33
+ }
34
+
27
35
  export function getSignatoryID(secret: SignatorySecret): SignatoryID {
28
36
  return `signatory_z${base58.encode(
29
37
  ed25519.getPublicKey(
@@ -56,6 +64,14 @@ export function newRandomRecipient(): RecipientSecret {
56
64
  return `recipientSecret_z${base58.encode(x25519.utils.randomPrivateKey())}`;
57
65
  }
58
66
 
67
+ export function recipientSecretToBytes(secret: RecipientSecret): Uint8Array {
68
+ return base58.decode(secret.substring("recipientSecret_z".length));
69
+ }
70
+
71
+ export function recipientSecretFromBytes(bytes: Uint8Array): RecipientSecret {
72
+ return `recipientSecret_z${base58.encode(bytes)}`;
73
+ }
74
+
59
75
  export function getRecipientID(secret: RecipientSecret): RecipientID {
60
76
  return `recipient_z${base58.encode(
61
77
  x25519.getPublicKey(
@@ -295,3 +311,15 @@ export function unsealKeySecret(
295
311
 
296
312
  return decrypt(sealedInfo.encrypted, sealingSecret, nOnceMaterial);
297
313
  }
314
+
315
+ export function uniquenessForHeader(): `z${string}` {
316
+ return `z${base58.encode(randomBytes(12))}`;
317
+ }
318
+
319
+ export function createdNowUnique(): {createdAt: `2${string}`, uniqueness: `z${string}`} {
320
+ const createdAt = (new Date()).toISOString() as `2${string}`;
321
+ return {
322
+ createdAt,
323
+ uniqueness: uniquenessForHeader(),
324
+ }
325
+ }
package/src/ids.ts ADDED
@@ -0,0 +1,7 @@
1
+ export type RawCoValueID = `co_z${string}` | `co_${string}_z${string}`;
2
+
3
+ export type TransactionID = { sessionID: SessionID; txIndex: number };
4
+
5
+ export type AgentID = `co_agent${string}_z${string}`;
6
+
7
+ export type SessionID = `${AgentID}_session_z${string}`;
package/src/index.ts CHANGED
@@ -1,14 +1,39 @@
1
- import { ContentType } from "./contentType";
2
- import { JsonValue } from "./jsonValue";
3
- import { CoValue } from "./coValue";
1
+ import {
2
+ CoValue,
3
+ agentCredentialFromBytes,
4
+ agentCredentialToBytes,
5
+ getAgent,
6
+ getAgentID,
7
+ newRandomAgentCredential,
8
+ newRandomSessionID,
9
+ } from "./coValue";
4
10
  import { LocalNode } from "./node";
11
+ import { CoMap } from "./contentTypes/coMap";
12
+
13
+ import type { AgentCredential } from "./coValue";
14
+ import type { AgentID, SessionID } from "./ids";
15
+ import type { CoValueID, ContentType } from "./contentType";
16
+ import type { JsonValue } from "./jsonValue";
5
17
 
6
18
  type Value = JsonValue | ContentType;
7
19
 
8
- export {
20
+ const internals = {
21
+ agentCredentialToBytes,
22
+ agentCredentialFromBytes,
23
+ getAgent,
24
+ getAgentID,
25
+ newRandomAgentCredential,
26
+ newRandomSessionID,
27
+ };
28
+
29
+ export { LocalNode, CoValue, CoMap, internals };
30
+
31
+ export type {
32
+ Value,
9
33
  JsonValue,
10
34
  ContentType,
11
- Value,
12
- LocalNode,
13
- CoValue
14
- }
35
+ CoValueID,
36
+ AgentCredential,
37
+ SessionID,
38
+ AgentID,
39
+ };
package/src/node.ts CHANGED
@@ -1,10 +1,7 @@
1
- import { newRandomKeySecret, seal } from "./crypto";
1
+ import { createdNowUnique, newRandomKeySecret, seal } from "./crypto";
2
2
  import {
3
- RawCoValueID,
4
3
  CoValue,
5
4
  AgentCredential,
6
- AgentID,
7
- SessionID,
8
5
  Agent,
9
6
  getAgent,
10
7
  getAgentID,
@@ -15,6 +12,8 @@ import {
15
12
  } from "./coValue";
16
13
  import { Team, expectTeamContent } from "./permissions";
17
14
  import { SyncManager } from "./sync";
15
+ import { AgentID, RawCoValueID, SessionID } from "./ids";
16
+ import { CoValueID, ContentType } from ".";
18
17
 
19
18
  export class LocalNode {
20
19
  coValues: { [key: RawCoValueID]: CoValueState } = {};
@@ -61,6 +60,10 @@ export class LocalNode {
61
60
  return entry.done;
62
61
  }
63
62
 
63
+ async load<T extends ContentType>(id: CoValueID<T>): Promise<T> {
64
+ return (await this.loadCoValue(id)).getCurrentContent() as T;
65
+ }
66
+
64
67
  expectCoValueLoaded(id: RawCoValueID, expectation?: string): CoValue {
65
68
  const entry = this.coValues[id];
66
69
  if (!entry) {
@@ -112,6 +115,7 @@ export class LocalNode {
112
115
  type: "comap",
113
116
  ruleset: { type: "team", initialAdmin: this.agentID },
114
117
  meta: null,
118
+ ...createdNowUnique(),
115
119
  publicNickname: "team",
116
120
  });
117
121
 
@@ -8,6 +8,7 @@ import { LocalNode } from "./node";
8
8
  import { expectMap } from "./contentType";
9
9
  import { expectTeamContent } from "./permissions";
10
10
  import {
11
+ createdNowUnique,
11
12
  getRecipientID,
12
13
  newRandomKeySecret,
13
14
  seal,
@@ -47,6 +48,7 @@ function newTeam() {
47
48
  type: "comap",
48
49
  ruleset: { type: "team", initialAdmin: adminID },
49
50
  meta: null,
51
+ ...createdNowUnique(),
50
52
  publicNickname: "team",
51
53
  });
52
54
 
@@ -343,6 +345,7 @@ test("Admins can write to an object that is owned by their team", () => {
343
345
  type: "comap",
344
346
  ruleset: { type: "ownedByTeam", team: team.id },
345
347
  meta: null,
348
+ ...createdNowUnique(),
346
349
  publicNickname: "childObject",
347
350
  });
348
351
 
@@ -386,6 +389,7 @@ test("Writers can write to an object that is owned by their team", () => {
386
389
  type: "comap",
387
390
  ruleset: { type: "ownedByTeam", team: team.id },
388
391
  meta: null,
392
+ ...createdNowUnique(),
389
393
  publicNickname: "childObject",
390
394
  });
391
395
 
@@ -447,6 +451,7 @@ test("Readers can not write to an object that is owned by their team", () => {
447
451
  type: "comap",
448
452
  ruleset: { type: "ownedByTeam", team: team.id },
449
453
  meta: null,
454
+ ...createdNowUnique(),
450
455
  publicNickname: "childObject",
451
456
  });
452
457
 
@@ -521,6 +526,7 @@ test("Admins can set team read key and then use it to create and read private tr
521
526
  type: "comap",
522
527
  ruleset: { type: "ownedByTeam", team: team.id },
523
528
  meta: null,
529
+ ...createdNowUnique(),
524
530
  publicNickname: "childObject",
525
531
  });
526
532
 
@@ -580,6 +586,7 @@ test("Admins can set team read key and then writers can use it to create and rea
580
586
  type: "comap",
581
587
  ruleset: { type: "ownedByTeam", team: team.id },
582
588
  meta: null,
589
+ ...createdNowUnique(),
583
590
  publicNickname: "childObject",
584
591
  });
585
592
 
@@ -660,6 +667,7 @@ test("Admins can set team read key and then use it to create private transaction
660
667
  type: "comap",
661
668
  ruleset: { type: "ownedByTeam", team: team.id },
662
669
  meta: null,
670
+ ...createdNowUnique(),
663
671
  publicNickname: "childObject",
664
672
  });
665
673
 
@@ -759,6 +767,7 @@ test("Admins can set team read key and then use it to create private transaction
759
767
  type: "comap",
760
768
  ruleset: { type: "ownedByTeam", team: team.id },
761
769
  meta: null,
770
+ ...createdNowUnique(),
762
771
  publicNickname: "childObject",
763
772
  });
764
773
 
@@ -864,6 +873,7 @@ test("Admins can set team read key, make a private transaction in an owned objec
864
873
  type: "comap",
865
874
  ruleset: { type: "ownedByTeam", team: team.id },
866
875
  meta: null,
876
+ ...createdNowUnique(),
867
877
  publicNickname: "childObject",
868
878
  });
869
879
 
@@ -944,6 +954,7 @@ test("Admins can set team read key, make a private transaction in an owned objec
944
954
  type: "comap",
945
955
  ruleset: { type: "ownedByTeam", team: team.id },
946
956
  meta: null,
957
+ ...createdNowUnique(),
947
958
  publicNickname: "childObject",
948
959
  });
949
960
 
@@ -1085,6 +1096,7 @@ test("Admins can set team read rey, make a private transaction in an owned objec
1085
1096
  type: "comap",
1086
1097
  ruleset: { type: "ownedByTeam", team: team.id },
1087
1098
  meta: null,
1099
+ ...createdNowUnique(),
1088
1100
  publicNickname: "childObject",
1089
1101
  });
1090
1102
 
@@ -1267,3 +1279,23 @@ test("Admins can set team read rey, make a private transaction in an owned objec
1267
1279
  ).get("foo3")
1268
1280
  ).toBeUndefined();
1269
1281
  });
1282
+
1283
+ test("Can create two owned objects in the same team and they will have different ids", () => {
1284
+ const { node, team, admin, adminID } = newTeam();
1285
+
1286
+ const childObject1 = node.createCoValue({
1287
+ type: "comap",
1288
+ ruleset: { type: "ownedByTeam", team: team.id },
1289
+ meta: null,
1290
+ ...createdNowUnique()
1291
+ });
1292
+
1293
+ const childObject2 = node.createCoValue({
1294
+ type: "comap",
1295
+ ruleset: { type: "ownedByTeam", team: team.id },
1296
+ meta: null,
1297
+ ...createdNowUnique()
1298
+ });
1299
+
1300
+ expect(childObject1.id).not.toEqual(childObject2.id);
1301
+ });
@@ -1,4 +1,5 @@
1
- import { CoMap, ContentType, MapOpPayload } from "./contentType";
1
+ import { ContentType } from "./contentType";
2
+ import { CoMap, MapOpPayload } from "./contentTypes/coMap";
2
3
  import { JsonValue } from "./jsonValue";
3
4
  import {
4
5
  Encrypted,
@@ -7,23 +8,20 @@ import {
7
8
  RecipientID,
8
9
  SealedSet,
9
10
  SignatoryID,
10
- encryptForTransaction,
11
+ createdNowUnique,
11
12
  newRandomKeySecret,
12
13
  seal,
13
14
  sealKeySecret,
14
15
  } from "./crypto";
15
16
  import {
16
17
  AgentCredential,
17
- AgentID,
18
18
  CoValue,
19
- RawCoValueID,
20
- SessionID,
21
19
  Transaction,
22
- TransactionID,
23
20
  TrustingTransaction,
24
21
  agentIDfromSessionID,
25
22
  } from "./coValue";
26
23
  import { LocalNode } from ".";
24
+ import { AgentID, RawCoValueID, SessionID, TransactionID } from "./ids";
27
25
 
28
26
  export type PermissionsDef =
29
27
  | { type: "team"; initialAdmin: AgentID; parentTeams?: RawCoValueID[] }
@@ -355,6 +353,7 @@ export class Team {
355
353
  team: this.teamMap.id,
356
354
  },
357
355
  meta: meta || null,
356
+ ...createdNowUnique(),
358
357
  publicNickname: "map",
359
358
  })
360
359
  .getCurrentContent() as CoMap<M, Meta>;
package/src/sync.test.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import {
2
- AgentID,
3
2
  getAgent,
4
3
  getAgentID,
5
4
  newRandomAgentCredential,
@@ -7,13 +6,15 @@ import {
7
6
  } from "./coValue";
8
7
  import { LocalNode } from "./node";
9
8
  import { Peer, PeerID, SyncMessage } from "./sync";
10
- import { MapOpPayload, expectMap } from "./contentType";
9
+ import { expectMap } from "./contentType";
10
+ import { MapOpPayload } from "./contentTypes/coMap";
11
11
  import { Team } from "./permissions";
12
12
  import {
13
13
  ReadableStream,
14
14
  WritableStream,
15
15
  TransformStream,
16
16
  } from "isomorphic-streams";
17
+ import { AgentID } from "./ids";
17
18
 
18
19
  test(
19
20
  "Node replies with initial tx and header to empty subscribe",
@@ -73,6 +74,8 @@ test(
73
74
  type: "comap",
74
75
  ruleset: { type: "ownedByTeam", team: team.id },
75
76
  meta: null,
77
+ createdAt: map.coValue.header.createdAt,
78
+ uniqueness: map.coValue.header.uniqueness,
76
79
  publicNickname: "map",
77
80
  },
78
81
  newContent: {
@@ -609,8 +612,6 @@ test("If we add a server peer, newly created coValues are auto-subscribed to", a
609
612
 
610
613
  const team = node.createTeam();
611
614
 
612
- team.createMap();
613
-
614
615
  const [inRx, _inTx] = newStreamPair<SyncMessage>();
615
616
  const [outRx, outTx] = newStreamPair<SyncMessage>();
616
617
 
package/src/sync.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { Hash, Signature } from "./crypto";
2
- import { CoValueHeader, RawCoValueID, SessionID, Transaction } from "./coValue";
2
+ import { CoValueHeader, Transaction } from "./coValue";
3
3
  import { CoValue } from "./coValue";
4
4
  import { LocalNode } from "./node";
5
5
  import { newLoadingState } from "./node";
6
6
  import { ReadableStream, WritableStream, WritableStreamDefaultWriter } from "isomorphic-streams";
7
+ import { RawCoValueID, SessionID } from "./ids";
7
8
 
8
9
  export type CoValueKnownState = {
9
10
  coValueID: RawCoValueID;
@@ -79,31 +80,6 @@ export interface PeerState {
79
80
  role: "peer" | "server" | "client";
80
81
  }
81
82
 
82
- export function weAreStrictlyAhead(
83
- ourKnownState: CoValueKnownState,
84
- theirKnownState: CoValueKnownState
85
- ): boolean {
86
- if (theirKnownState.header && !ourKnownState.header) {
87
- return false;
88
- }
89
-
90
- const allSessions = new Set([
91
- ...(Object.keys(ourKnownState.sessions) as SessionID[]),
92
- ...(Object.keys(theirKnownState.sessions) as SessionID[]),
93
- ]);
94
-
95
- for (const sessionID of allSessions) {
96
- const ourSession = ourKnownState.sessions[sessionID];
97
- const theirSession = theirKnownState.sessions[sessionID];
98
-
99
- if ((ourSession || 0) < (theirSession || 0)) {
100
- return false;
101
- }
102
- }
103
-
104
- return true;
105
- }
106
-
107
83
  export function combinedKnownStates(
108
84
  stateA: CoValueKnownState,
109
85
  stateB: CoValueKnownState
@@ -480,11 +456,7 @@ export class SyncManager {
480
456
  for (const peer of Object.values(this.peers)) {
481
457
  const optimisticKnownState = peer.optimisticKnownStates[coValue.id];
482
458
 
483
- const shouldSync =
484
- optimisticKnownState ||
485
- peer.role === "server";
486
-
487
- if (shouldSync) {
459
+ if (optimisticKnownState) {
488
460
  await this.tellUntoldKnownStateIncludingDependencies(
489
461
  coValue.id,
490
462
  peer
@@ -493,6 +465,15 @@ export class SyncManager {
493
465
  coValue.id,
494
466
  peer
495
467
  );
468
+ } else if (peer.role === "server") {
469
+ await this.subscribeToIncludingDependencies(
470
+ coValue.id,
471
+ peer
472
+ );
473
+ await this.sendNewContentIncludingDependencies(
474
+ coValue.id,
475
+ peer
476
+ );
496
477
  }
497
478
  }
498
479
  }