cojson 0.1.4 → 0.1.6

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.
@@ -1,19 +1,262 @@
1
- import { JsonObject, JsonValue } from '../jsonValue.js';
2
- import { CoID } from '../contentType.js';
3
- import { CoValue } from '../coValue.js';
1
+ import { JsonObject, JsonValue } from "../jsonValue.js";
2
+ import { CoID } from "../contentType.js";
3
+ import { CoValue, accountOrAgentIDfromSessionID } from "../coValue.js";
4
+ import { SessionID, TransactionID } from "../ids.js";
5
+ import { AccountID } from "../index.js";
6
+ import { isAccountID } from "../account.js";
4
7
 
5
- export class CoList<T extends JsonValue, Meta extends JsonObject | null = null> {
8
+ type OpID = TransactionID & { changeIdx: number };
9
+
10
+ type InsertionOpPayload<T extends JsonValue> =
11
+ | {
12
+ op: "pre";
13
+ value: T;
14
+ before: OpID | "end";
15
+ }
16
+ | {
17
+ op: "app";
18
+ value: T;
19
+ after: OpID | "start";
20
+ };
21
+
22
+ type DeletionOpPayload = {
23
+ op: "del";
24
+ insertion: OpID;
25
+ };
26
+
27
+ export type ListOpPayload<T extends JsonValue> =
28
+ | InsertionOpPayload<T>
29
+ | DeletionOpPayload;
30
+
31
+ type InsertionEntry<T extends JsonValue> = {
32
+ madeAt: number;
33
+ predecessors: OpID[];
34
+ successors: OpID[];
35
+ } & InsertionOpPayload<T>;
36
+
37
+ type DeletionEntry = {
38
+ madeAt: number;
39
+ deletionID: OpID;
40
+ } & DeletionOpPayload;
41
+
42
+ export class CoList<
43
+ T extends JsonValue,
44
+ Meta extends JsonObject | null = null
45
+ > {
6
46
  id: CoID<CoList<T, Meta>>;
7
47
  type = "colist" as const;
8
48
  coValue: CoValue;
49
+ afterStart: OpID[];
50
+ beforeEnd: OpID[];
51
+ insertions: {
52
+ [sessionID: SessionID]: {
53
+ [txIdx: number]: {
54
+ [changeIdx: number]: InsertionEntry<T>;
55
+ };
56
+ };
57
+ };
58
+ deletionsByInsertion: {
59
+ [deletedSessionID: SessionID]: {
60
+ [deletedTxIdx: number]: {
61
+ [deletedChangeIdx: number]: DeletionEntry[];
62
+ };
63
+ };
64
+ };
9
65
 
10
66
  constructor(coValue: CoValue) {
11
67
  this.id = coValue.id as CoID<CoList<T, Meta>>;
12
68
  this.coValue = coValue;
69
+ this.afterStart = [];
70
+ this.beforeEnd = [];
71
+ this.insertions = {};
72
+ this.deletionsByInsertion = {};
73
+
74
+ this.fillOpsFromCoValue();
75
+ }
76
+
77
+
78
+ get meta(): Meta {
79
+ return this.coValue.header.meta as Meta;
80
+ }
81
+
82
+ protected fillOpsFromCoValue() {
83
+ this.insertions = {};
84
+ this.deletionsByInsertion = {};
85
+ this.afterStart = [];
86
+ this.beforeEnd = [];
87
+
88
+ for (const {
89
+ txID,
90
+ changes,
91
+ madeAt,
92
+ } of this.coValue.getValidSortedTransactions()) {
93
+ for (const [changeIdx, changeUntyped] of changes.entries()) {
94
+ const change = changeUntyped as ListOpPayload<T>;
95
+
96
+ if (change.op === "pre" || change.op === "app") {
97
+ let sessionEntry = this.insertions[txID.sessionID];
98
+ if (!sessionEntry) {
99
+ sessionEntry = {};
100
+ this.insertions[txID.sessionID] = sessionEntry;
101
+ }
102
+ let txEntry = sessionEntry[txID.txIndex];
103
+ if (!txEntry) {
104
+ txEntry = {};
105
+ sessionEntry[txID.txIndex] = txEntry;
106
+ }
107
+ txEntry[changeIdx] = {
108
+ madeAt,
109
+ predecessors: [],
110
+ successors: [],
111
+ ...change,
112
+ };
113
+ if (change.op === "pre") {
114
+ if (change.before === "end") {
115
+ this.beforeEnd.push({
116
+ ...txID,
117
+ changeIdx,
118
+ });
119
+ } else {
120
+ const beforeEntry =
121
+ this.insertions[change.before.sessionID]?.[
122
+ change.before.txIndex
123
+ ]?.[change.before.changeIdx];
124
+ if (!beforeEntry) {
125
+ throw new Error(
126
+ "Not yet implemented: insertion before missing op " +
127
+ change.before
128
+ );
129
+ }
130
+ beforeEntry.predecessors.splice(0, 0, {
131
+ ...txID,
132
+ changeIdx,
133
+ });
134
+ }
135
+ } else {
136
+ if (change.after === "start") {
137
+ this.afterStart.push({
138
+ ...txID,
139
+ changeIdx,
140
+ });
141
+ } else {
142
+ const afterEntry =
143
+ this.insertions[change.after.sessionID]?.[
144
+ change.after.txIndex
145
+ ]?.[change.after.changeIdx];
146
+ if (!afterEntry) {
147
+ throw new Error(
148
+ "Not yet implemented: insertion after missing op " +
149
+ change.after
150
+ );
151
+ }
152
+ afterEntry.successors.push({
153
+ ...txID,
154
+ changeIdx,
155
+ });
156
+ }
157
+ }
158
+ } else if (change.op === "del") {
159
+ let sessionEntry =
160
+ this.deletionsByInsertion[change.insertion.sessionID];
161
+ if (!sessionEntry) {
162
+ sessionEntry = {};
163
+ this.deletionsByInsertion[change.insertion.sessionID] =
164
+ sessionEntry;
165
+ }
166
+ let txEntry = sessionEntry[change.insertion.txIndex];
167
+ if (!txEntry) {
168
+ txEntry = {};
169
+ sessionEntry[change.insertion.txIndex] = txEntry;
170
+ }
171
+ let changeEntry = txEntry[change.insertion.changeIdx];
172
+ if (!changeEntry) {
173
+ changeEntry = [];
174
+ txEntry[change.insertion.changeIdx] = changeEntry;
175
+ }
176
+ changeEntry.push({
177
+ madeAt,
178
+ deletionID: {
179
+ ...txID,
180
+ changeIdx,
181
+ },
182
+ ...change,
183
+ });
184
+ } else {
185
+ throw new Error(
186
+ "Unknown list operation " +
187
+ (change as { op: unknown }).op
188
+ );
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ entries(): { value: T; madeAt: number; opID: OpID }[] {
195
+ const arr: { value: T; madeAt: number; opID: OpID }[] = [];
196
+ for (const opID of this.afterStart) {
197
+ this.fillArrayFromOpID(opID, arr);
198
+ }
199
+ for (const opID of this.beforeEnd) {
200
+ this.fillArrayFromOpID(opID, arr);
201
+ }
202
+ return arr;
203
+ }
204
+
205
+ private fillArrayFromOpID(
206
+ opID: OpID,
207
+ arr: { value: T; madeAt: number; opID: OpID }[]
208
+ ) {
209
+ const entry =
210
+ this.insertions[opID.sessionID]?.[opID.txIndex]?.[opID.changeIdx];
211
+ if (!entry) {
212
+ throw new Error("Missing op " + opID);
213
+ }
214
+ for (const predecessor of entry.predecessors) {
215
+ this.fillArrayFromOpID(predecessor, arr);
216
+ }
217
+ const deleted =
218
+ (this.deletionsByInsertion[opID.sessionID]?.[opID.txIndex]?.[
219
+ opID.changeIdx
220
+ ]?.length || 0) > 0;
221
+ if (!deleted) {
222
+ arr.push({
223
+ value: entry.value,
224
+ madeAt: entry.madeAt,
225
+ opID,
226
+ });
227
+ }
228
+ for (const successor of entry.successors) {
229
+ this.fillArrayFromOpID(successor, arr);
230
+ }
13
231
  }
14
232
 
15
- toJSON(): JsonObject {
16
- throw new Error("Method not implemented.");
233
+ getLastEditor(idx: number): AccountID | undefined {
234
+ const entry = this.entries()[idx];
235
+ if (!entry) {
236
+ return undefined;
237
+ }
238
+ const accountID = accountOrAgentIDfromSessionID(entry.opID.sessionID);
239
+ if (isAccountID(accountID)) {
240
+ return accountID;
241
+ } else {
242
+ return undefined;
243
+ }
244
+ }
245
+
246
+ toJSON(): T[] {
247
+ return this.asArray();
248
+ }
249
+
250
+ asArray(): T[] {
251
+ return this.entries().map((entry) => entry.value);
252
+ }
253
+
254
+ edit(
255
+ changer: (editable: WriteableCoList<T, Meta>) => void
256
+ ): CoList<T, Meta> {
257
+ const editable = new WriteableCoList<T, Meta>(this.coValue);
258
+ changer(editable);
259
+ return new CoList(this.coValue);
17
260
  }
18
261
 
19
262
  subscribe(listener: (coMap: CoList<T, Meta>) => void): () => void {
@@ -22,3 +265,106 @@ export class CoList<T extends JsonValue, Meta extends JsonObject | null = null>
22
265
  });
23
266
  }
24
267
  }
268
+
269
+ export class WriteableCoList<
270
+ T extends JsonValue,
271
+ Meta extends JsonObject | null = null
272
+ > extends CoList<T, Meta> {
273
+ append(
274
+ after: number,
275
+ value: T,
276
+ privacy: "private" | "trusting" = "private"
277
+ ): void {
278
+ const entries = this.entries();
279
+ let opIDBefore;
280
+ if (entries.length > 0) {
281
+ const entryBefore = entries[after];
282
+ if (!entryBefore) {
283
+ throw new Error("Invalid index " + after);
284
+ }
285
+ opIDBefore = entryBefore.opID;
286
+ } else {
287
+ if (after !== 0) {
288
+ throw new Error("Invalid index " + after);
289
+ }
290
+ opIDBefore = "start";
291
+ }
292
+ this.coValue.makeTransaction(
293
+ [
294
+ {
295
+ op: "app",
296
+ value,
297
+ after: opIDBefore,
298
+ },
299
+ ],
300
+ privacy
301
+ );
302
+
303
+ this.fillOpsFromCoValue();
304
+ }
305
+
306
+ push(value: T, privacy: "private" | "trusting" = "private"): void {
307
+ // TODO: optimize
308
+ const entries = this.entries();
309
+ this.append(entries.length > 0 ? entries.length - 1 : 0, value, privacy);
310
+ }
311
+
312
+ prepend(
313
+ before: number,
314
+ value: T,
315
+ privacy: "private" | "trusting" = "private"
316
+ ): void {
317
+ const entries = this.entries();
318
+ let opIDAfter;
319
+ if (entries.length > 0) {
320
+ const entryAfter = entries[before];
321
+ if (entryAfter) {
322
+ opIDAfter = entryAfter.opID;
323
+ } else {
324
+ if (before !== entries.length) {
325
+ throw new Error("Invalid index " + before);
326
+ }
327
+ opIDAfter = "end";
328
+ }
329
+ } else {
330
+ if (before !== 0) {
331
+ throw new Error("Invalid index " + before);
332
+ }
333
+ opIDAfter = "end";
334
+ }
335
+ this.coValue.makeTransaction(
336
+ [
337
+ {
338
+ op: "pre",
339
+ value,
340
+ before: opIDAfter,
341
+ },
342
+ ],
343
+ privacy
344
+ );
345
+
346
+ this.fillOpsFromCoValue();
347
+ }
348
+
349
+ delete(
350
+ at: number,
351
+ privacy: "private" | "trusting" = "private"
352
+ ): void {
353
+ const entries = this.entries();
354
+ const entry = entries[at];
355
+ if (!entry) {
356
+ throw new Error("Invalid index " + at);
357
+ }
358
+ this.coValue.makeTransaction(
359
+ [
360
+ {
361
+ op: "del",
362
+ insertion: entry.opID,
363
+ },
364
+ ],
365
+ privacy
366
+ );
367
+
368
+ this.fillOpsFromCoValue();
369
+ }
370
+ }
@@ -46,6 +46,10 @@ export class CoMap<
46
46
  this.fillOpsFromCoValue();
47
47
  }
48
48
 
49
+ get meta(): Meta {
50
+ return this.coValue.header.meta as Meta;
51
+ }
52
+
49
53
  protected fillOpsFromCoValue() {
50
54
  this.ops = {};
51
55
 
package/src/group.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  } from "./account.js";
25
25
  import { Role } from "./permissions.js";
26
26
  import { base58 } from "@scure/base";
27
+ import { CoList } from "./contentTypes/coList.js";
27
28
 
28
29
  export type GroupContent = {
29
30
  profile: CoID<Profile> | null;
@@ -186,10 +187,9 @@ export class Group {
186
187
  this.rotateReadKey();
187
188
  }
188
189
 
189
- createMap<
190
- M extends { [key: string]: JsonValue },
191
- Meta extends JsonObject | null = null
192
- >(meta?: Meta): CoMap<M, Meta> {
190
+ createMap<M extends CoMap<{ [key: string]: JsonValue }, JsonObject | null>>(
191
+ meta?: M["meta"]
192
+ ): M {
193
193
  return this.node
194
194
  .createCoValue({
195
195
  type: "comap",
@@ -200,7 +200,23 @@ export class Group {
200
200
  meta: meta || null,
201
201
  ...createdNowUnique(),
202
202
  })
203
- .getCurrentContent() as CoMap<M, Meta>;
203
+ .getCurrentContent() as M;
204
+ }
205
+
206
+ createList<L extends CoList<JsonValue, JsonObject | null>>(
207
+ meta?: L["meta"]
208
+ ): L {
209
+ return this.node
210
+ .createCoValue({
211
+ type: "colist",
212
+ ruleset: {
213
+ type: "ownedByGroup",
214
+ group: this.groupMap.id,
215
+ },
216
+ meta: meta || null,
217
+ ...createdNowUnique(),
218
+ })
219
+ .getCurrentContent() as L;
204
220
  }
205
221
 
206
222
  testWithDifferentAccount(
@@ -230,4 +246,4 @@ export function secretSeedFromInviteSecret(inviteSecret: InviteSecret) {
230
246
  }
231
247
 
232
248
  return base58.decode(inviteSecret.slice("inviteSecret_z".length));
233
- }
249
+ }
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ import type {
25
25
  AccountID,
26
26
  AccountContent,
27
27
  ProfileContent,
28
+ ProfileMeta,
28
29
  Profile,
29
30
  } from "./account.js";
30
31
  import type { InviteSecret } from "./group.js";
@@ -70,6 +71,7 @@ export type {
70
71
  AccountContent,
71
72
  Profile,
72
73
  ProfileContent,
74
+ ProfileMeta,
73
75
  InviteSecret
74
76
  };
75
77
 
package/src/node.ts CHANGED
@@ -31,8 +31,7 @@ import {
31
31
  AccountID,
32
32
  Profile,
33
33
  AccountContent,
34
- ProfileContent,
35
- ProfileMeta,
34
+ AccountMap,
36
35
  } from "./account.js";
37
36
  import { CoMap } from "./index.js";
38
37
 
@@ -139,7 +138,7 @@ export class LocalNode {
139
138
  }
140
139
 
141
140
  async loadProfile(id: AccountID): Promise<Profile> {
142
- const account = await this.load<CoMap<AccountContent>>(id);
141
+ const account = await this.load<AccountMap>(id);
143
142
  const profileID = account.get("profile");
144
143
 
145
144
  if (!profileID) {
@@ -307,7 +306,7 @@ export class LocalNode {
307
306
  account.node
308
307
  );
309
308
 
310
- const profile = accountAsGroup.createMap<ProfileContent, ProfileMeta>({
309
+ const profile = accountAsGroup.createMap<Profile>({
311
310
  type: "profile",
312
311
  });
313
312
 
@@ -1,12 +1,17 @@
1
- import { ReadableStream, TransformStream, WritableStream } from "isomorphic-streams";
1
+ import {
2
+ ReadableStream,
3
+ TransformStream,
4
+ WritableStream,
5
+ } from "isomorphic-streams";
2
6
  import { Peer, PeerID, SyncMessage } from "./sync.js";
3
7
 
4
-
5
8
  export function connectedPeers(
6
9
  peer1id: PeerID,
7
10
  peer2id: PeerID,
8
11
  {
9
- trace = false, peer1role = "peer", peer2role = "peer",
12
+ trace = false,
13
+ peer1role = "peer",
14
+ peer2role = "peer",
10
15
  }: {
11
16
  trace?: boolean;
12
17
  peer1role?: Peer["role"];
@@ -24,9 +29,13 @@ export function connectedPeers(
24
29
  new TransformStream({
25
30
  transform(
26
31
  chunk: SyncMessage,
27
- controller: { enqueue: (msg: SyncMessage) => void; }
32
+ controller: { enqueue: (msg: SyncMessage) => void }
28
33
  ) {
29
- trace && console.debug(`${peer2id} -> ${peer1id}`, JSON.stringify(chunk, null, 2));
34
+ trace &&
35
+ console.debug(
36
+ `${peer2id} -> ${peer1id}`,
37
+ JSON.stringify(chunk, null, 2)
38
+ );
30
39
  controller.enqueue(chunk);
31
40
  },
32
41
  })
@@ -38,9 +47,13 @@ export function connectedPeers(
38
47
  new TransformStream({
39
48
  transform(
40
49
  chunk: SyncMessage,
41
- controller: { enqueue: (msg: SyncMessage) => void; }
50
+ controller: { enqueue: (msg: SyncMessage) => void }
42
51
  ) {
43
- trace && console.debug(`${peer1id} -> ${peer2id}`, JSON.stringify(chunk, null, 2));
52
+ trace &&
53
+ console.debug(
54
+ `${peer1id} -> ${peer2id}`,
55
+ JSON.stringify(chunk, null, 2)
56
+ );
44
57
  controller.enqueue(chunk);
45
58
  },
46
59
  })
@@ -65,39 +78,22 @@ export function connectedPeers(
65
78
  }
66
79
 
67
80
  export function newStreamPair<T>(): [ReadableStream<T>, WritableStream<T>] {
68
- const queue: T[] = [];
69
- let resolveNextItemReady: () => void = () => { };
70
- let nextItemReady: Promise<void> = new Promise((resolve) => {
71
- resolveNextItemReady = resolve;
81
+ let readerClosed = false;
82
+
83
+ let resolveEnqueue: (enqueue: (item: T) => void) => void;
84
+ const enqueuePromise = new Promise<(item: T) => void>((resolve) => {
85
+ resolveEnqueue = resolve;
72
86
  });
73
87
 
74
- let writerClosed = false;
75
- let readerClosed = false;
88
+ let resolveClose: (close: () => void) => void;
89
+ const closePromise = new Promise<() => void>((resolve) => {
90
+ resolveClose = resolve;
91
+ });
76
92
 
77
93
  const readable = new ReadableStream<T>({
78
- async pull(controller) {
79
- let retriesLeft = 3;
80
- while (retriesLeft > 0) {
81
- if (writerClosed) {
82
- controller.close();
83
- return;
84
- }
85
- retriesLeft--;
86
- if (queue.length > 0) {
87
- controller.enqueue(queue.shift()!);
88
- if (queue.length === 0) {
89
- nextItemReady = new Promise((resolve) => {
90
- resolveNextItemReady = resolve;
91
- });
92
- }
93
- return;
94
- } else {
95
- await nextItemReady;
96
- }
97
- }
98
- throw new Error(
99
- "Should only use one retry to get next item in queue."
100
- );
94
+ async start(controller) {
95
+ resolveEnqueue(controller.enqueue.bind(controller));
96
+ resolveClose(controller.close.bind(controller));
101
97
  },
102
98
 
103
99
  cancel(_reason) {
@@ -107,22 +103,21 @@ export function newStreamPair<T>(): [ReadableStream<T>, WritableStream<T>] {
107
103
  });
108
104
 
109
105
  const writable = new WritableStream<T>({
110
- write(chunk) {
106
+ async write(chunk) {
107
+ const enqueue = await enqueuePromise;
111
108
  if (readerClosed) {
112
- console.log("Reader closed, not writing chunk", chunk);
113
- throw new Error("Reader closed, not writing chunk");
114
- }
115
- queue.push(chunk);
116
- if (queue.length === 1) {
117
- // make sure that await write resolves before corresponding read
118
- setTimeout(() => resolveNextItemReady());
109
+ throw new Error("Reader closed");
110
+ } else {
111
+ // make sure write resolves before corresponding read
112
+ setTimeout(() => {
113
+ enqueue(chunk);
114
+ })
119
115
  }
120
116
  },
121
- abort(_reason) {
122
- console.log("Manually closing writer");
123
- writerClosed = true;
124
- resolveNextItemReady();
125
- return Promise.resolve();
117
+ async abort(reason) {
118
+ console.debug("Manually closing writer", reason);
119
+ const close = await closePromise;
120
+ close();
126
121
  },
127
122
  });
128
123
 
package/src/sync.ts CHANGED
@@ -268,6 +268,7 @@ export class SyncManager {
268
268
  );
269
269
  }
270
270
  }
271
+ console.log("DONE!!!");
271
272
  } catch (e) {
272
273
  console.error(`Error reading from peer ${peer.id}`, e);
273
274
  }
@@ -280,13 +281,32 @@ export class SyncManager {
280
281
  }
281
282
 
282
283
  trySendToPeer(peer: PeerState, msg: SyncMessage) {
283
- return peer.outgoing.write(msg).catch((e) => {
284
- console.error(
285
- new Error(`Error writing to peer ${peer.id}, disconnecting`, {
286
- cause: e,
284
+ return new Promise<void>((resolve) => {
285
+ const timeout = setTimeout(() => {
286
+ console.error(
287
+ new Error(
288
+ `Writing to peer ${peer.id} took >1s - this should never happen as write should resolve quickly or error`
289
+ )
290
+ );
291
+ resolve();
292
+ }, 1000);
293
+ peer.outgoing
294
+ .write(msg)
295
+ .then(() => {
296
+ clearTimeout(timeout);
297
+ resolve();
287
298
  })
288
- );
289
- delete this.peers[peer.id];
299
+ .catch((e) => {
300
+ console.error(
301
+ new Error(
302
+ `Error writing to peer ${peer.id}, disconnecting`,
303
+ {
304
+ cause: e,
305
+ }
306
+ )
307
+ );
308
+ delete this.peers[peer.id];
309
+ });
290
310
  });
291
311
  }
292
312