cojson 0.0.1 → 0.0.2

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/src/sync.ts ADDED
@@ -0,0 +1,512 @@
1
+ import { Hash, Signature } from "./crypto";
2
+ import { CoValueHeader, RawCoValueID, SessionID, Transaction } from "./coValue";
3
+ import { CoValue } from "./coValue";
4
+ import { LocalNode } from "./node";
5
+ import { newLoadingState } from "./node";
6
+ import { ReadableStream, WritableStream, WritableStreamDefaultWriter } from "isomorphic-streams";
7
+
8
+ export type CoValueKnownState = {
9
+ coValueID: RawCoValueID;
10
+ header: boolean;
11
+ sessions: { [sessionID: SessionID]: number };
12
+ };
13
+
14
+ export function emptyKnownState(coValueID: RawCoValueID): CoValueKnownState {
15
+ return {
16
+ coValueID,
17
+ header: false,
18
+ sessions: {},
19
+ };
20
+ }
21
+
22
+ export type SyncMessage =
23
+ | SubscribeMessage
24
+ | TellKnownStateMessage
25
+ | NewContentMessage
26
+ | WrongAssumedKnownStateMessage
27
+ | UnsubscribeMessage;
28
+
29
+ export type SubscribeMessage = {
30
+ action: "subscribe";
31
+ } & CoValueKnownState;
32
+
33
+ export type TellKnownStateMessage = {
34
+ action: "tellKnownState";
35
+ asDependencyOf?: RawCoValueID;
36
+ } & CoValueKnownState;
37
+
38
+ export type NewContentMessage = {
39
+ action: "newContent";
40
+ coValueID: RawCoValueID;
41
+ header?: CoValueHeader;
42
+ newContent: {
43
+ [sessionID: SessionID]: SessionNewContent;
44
+ };
45
+ };
46
+
47
+ export type SessionNewContent = {
48
+ after: number;
49
+ newTransactions: Transaction[];
50
+ // TODO: is lastHash needed here?
51
+ lastHash: Hash;
52
+ lastSignature: Signature;
53
+ };
54
+
55
+ export type WrongAssumedKnownStateMessage = {
56
+ action: "wrongAssumedKnownState";
57
+ } & CoValueKnownState;
58
+
59
+ export type UnsubscribeMessage = {
60
+ action: "unsubscribe";
61
+ coValueID: RawCoValueID;
62
+ };
63
+
64
+ export type PeerID = string;
65
+
66
+ export interface Peer {
67
+ id: PeerID;
68
+ incoming: ReadableStream<SyncMessage>;
69
+ outgoing: WritableStream<SyncMessage>;
70
+ role: "peer" | "server" | "client";
71
+ }
72
+
73
+ export interface PeerState {
74
+ id: PeerID;
75
+ optimisticKnownStates: { [coValueID: RawCoValueID]: CoValueKnownState };
76
+ toldKnownState: Set<RawCoValueID>;
77
+ incoming: ReadableStream<SyncMessage>;
78
+ outgoing: WritableStreamDefaultWriter<SyncMessage>;
79
+ role: "peer" | "server" | "client";
80
+ }
81
+
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
+ export function combinedKnownStates(
108
+ stateA: CoValueKnownState,
109
+ stateB: CoValueKnownState
110
+ ): CoValueKnownState {
111
+ const sessionStates: CoValueKnownState["sessions"] = {};
112
+
113
+ const allSessions = new Set([
114
+ ...Object.keys(stateA.sessions),
115
+ ...Object.keys(stateB.sessions),
116
+ ] as SessionID[]);
117
+
118
+ for (const sessionID of allSessions) {
119
+ const stateAValue = stateA.sessions[sessionID];
120
+ const stateBValue = stateB.sessions[sessionID];
121
+
122
+ sessionStates[sessionID] = Math.max(stateAValue || 0, stateBValue || 0);
123
+ }
124
+
125
+ return {
126
+ coValueID: stateA.coValueID,
127
+ header: stateA.header || stateB.header,
128
+ sessions: sessionStates,
129
+ };
130
+ }
131
+
132
+ export class SyncManager {
133
+ peers: { [key: PeerID]: PeerState } = {};
134
+ local: LocalNode;
135
+
136
+ constructor(local: LocalNode) {
137
+ this.local = local;
138
+ }
139
+
140
+ loadFromPeers(id: RawCoValueID) {
141
+ for (const peer of Object.values(this.peers)) {
142
+ peer.outgoing
143
+ .write({
144
+ action: "subscribe",
145
+ coValueID: id,
146
+ header: false,
147
+ sessions: {},
148
+ })
149
+ .catch((e) => {
150
+ console.error("Error writing to peer", e);
151
+ });
152
+ }
153
+ }
154
+
155
+ async handleSyncMessage(msg: SyncMessage, peer: PeerState) {
156
+ // TODO: validate
157
+ switch (msg.action) {
158
+ case "subscribe":
159
+ return await this.handleSubscribe(msg, peer);
160
+ case "tellKnownState":
161
+ return await this.handleTellKnownState(msg, peer);
162
+ case "newContent":
163
+ return await this.handleNewContent(msg, peer);
164
+ case "wrongAssumedKnownState":
165
+ return await this.handleWrongAssumedKnownState(msg, peer);
166
+ case "unsubscribe":
167
+ return await this.handleUnsubscribe(msg);
168
+ default:
169
+ throw new Error(
170
+ `Unknown message type ${
171
+ (msg as { action: "string" }).action
172
+ }`
173
+ );
174
+ }
175
+ }
176
+
177
+ async subscribeToIncludingDependencies(
178
+ coValueID: RawCoValueID,
179
+ peer: PeerState
180
+ ) {
181
+ const coValue = this.local.expectCoValueLoaded(coValueID);
182
+
183
+ for (const coValueID of coValue.getDependedOnCoValues()) {
184
+ await this.subscribeToIncludingDependencies(coValueID, peer);
185
+ }
186
+
187
+ if (!peer.toldKnownState.has(coValueID)) {
188
+ peer.toldKnownState.add(coValueID);
189
+ await peer.outgoing.write({
190
+ action: "subscribe",
191
+ ...coValue.knownState(),
192
+ });
193
+ }
194
+ }
195
+
196
+ async tellUntoldKnownStateIncludingDependencies(
197
+ coValueID: RawCoValueID,
198
+ peer: PeerState,
199
+ asDependencyOf?: RawCoValueID
200
+ ) {
201
+ const coValue = this.local.expectCoValueLoaded(coValueID);
202
+
203
+ for (const dependentCoValueID of coValue.getDependedOnCoValues()) {
204
+ await this.tellUntoldKnownStateIncludingDependencies(
205
+ dependentCoValueID,
206
+ peer,
207
+ asDependencyOf || coValueID
208
+ );
209
+ }
210
+
211
+ if (!peer.toldKnownState.has(coValueID)) {
212
+ await peer.outgoing.write({
213
+ action: "tellKnownState",
214
+ asDependencyOf,
215
+ ...coValue.knownState(),
216
+ });
217
+
218
+ peer.toldKnownState.add(coValueID);
219
+ }
220
+ }
221
+
222
+ async sendNewContentIncludingDependencies(
223
+ coValueID: RawCoValueID,
224
+ peer: PeerState
225
+ ) {
226
+ const coValue = this.local.expectCoValueLoaded(coValueID);
227
+
228
+ for (const coValueID of coValue.getDependedOnCoValues()) {
229
+ await this.sendNewContentIncludingDependencies(coValueID, peer);
230
+ }
231
+
232
+ const newContent = coValue.newContentSince(
233
+ peer.optimisticKnownStates[coValueID]
234
+ );
235
+
236
+ if (newContent) {
237
+ await peer.outgoing.write(newContent);
238
+ peer.optimisticKnownStates[coValueID] = combinedKnownStates(
239
+ peer.optimisticKnownStates[coValueID] ||
240
+ emptyKnownState(coValueID),
241
+ coValue.knownState()
242
+ );
243
+ }
244
+ }
245
+
246
+ addPeer(peer: Peer) {
247
+ const peerState: PeerState = {
248
+ id: peer.id,
249
+ optimisticKnownStates: {},
250
+ incoming: peer.incoming,
251
+ outgoing: peer.outgoing.getWriter(),
252
+ toldKnownState: new Set(),
253
+ role: peer.role,
254
+ };
255
+ this.peers[peer.id] = peerState;
256
+
257
+ if (peer.role === "server") {
258
+ const initialSync = async () => {
259
+ for (const entry of Object.values(this.local.coValues)) {
260
+ if (entry.state === "loading") {
261
+ continue;
262
+ }
263
+
264
+ await this.subscribeToIncludingDependencies(
265
+ entry.coValue.id,
266
+ peerState
267
+ );
268
+
269
+ peerState.optimisticKnownStates[entry.coValue.id] = {
270
+ coValueID: entry.coValue.id,
271
+ header: false,
272
+ sessions: {},
273
+ };
274
+ }
275
+ };
276
+ void initialSync();
277
+ }
278
+
279
+ const readIncoming = async () => {
280
+ for await (const msg of peerState.incoming) {
281
+ try {
282
+ await this.handleSyncMessage(msg, peerState);
283
+ } catch (e) {
284
+ console.error(
285
+ `Error reading from peer ${peer.id}`,
286
+ JSON.stringify(msg),
287
+ e
288
+ );
289
+ }
290
+ }
291
+ };
292
+
293
+ void readIncoming();
294
+ }
295
+
296
+ async handleSubscribe(msg: SubscribeMessage, peer: PeerState) {
297
+ const entry = this.local.coValues[msg.coValueID];
298
+
299
+ if (!entry || entry.state === "loading") {
300
+ if (!entry) {
301
+ this.local.coValues[msg.coValueID] = newLoadingState();
302
+ }
303
+
304
+ peer.optimisticKnownStates[msg.coValueID] = knownStateIn(msg);
305
+ peer.toldKnownState.add(msg.coValueID);
306
+
307
+ await peer.outgoing.write({
308
+ action: "tellKnownState",
309
+ coValueID: msg.coValueID,
310
+ header: false,
311
+ sessions: {},
312
+ });
313
+
314
+ return;
315
+ }
316
+
317
+ peer.optimisticKnownStates[msg.coValueID] = knownStateIn(msg);
318
+
319
+ await this.tellUntoldKnownStateIncludingDependencies(
320
+ msg.coValueID,
321
+ peer
322
+ );
323
+
324
+ await this.sendNewContentIncludingDependencies(msg.coValueID, peer);
325
+ }
326
+
327
+ async handleTellKnownState(msg: TellKnownStateMessage, peer: PeerState) {
328
+ let entry = this.local.coValues[msg.coValueID];
329
+
330
+ peer.optimisticKnownStates[msg.coValueID] = combinedKnownStates(
331
+ peer.optimisticKnownStates[msg.coValueID] || emptyKnownState(msg.coValueID),
332
+ knownStateIn(msg)
333
+ );
334
+
335
+ if (!entry) {
336
+ if (msg.asDependencyOf) {
337
+ if (this.local.coValues[msg.asDependencyOf]) {
338
+ entry = newLoadingState();
339
+
340
+ this.local.coValues[msg.coValueID] = entry;
341
+ } else {
342
+ throw new Error(
343
+ "Expected coValue dependency entry to be created, missing subscribe?"
344
+ );
345
+ }
346
+ } else {
347
+ throw new Error(
348
+ "Expected coValue entry to be created, missing subscribe?"
349
+ );
350
+ }
351
+ }
352
+
353
+ if (entry.state === "loading") {
354
+ return [];
355
+ }
356
+
357
+ await this.tellUntoldKnownStateIncludingDependencies(
358
+ msg.coValueID,
359
+ peer
360
+ );
361
+ await this.sendNewContentIncludingDependencies(msg.coValueID, peer);
362
+ }
363
+
364
+ async handleNewContent(msg: NewContentMessage, peer: PeerState) {
365
+ let entry = this.local.coValues[msg.coValueID];
366
+
367
+ if (!entry) {
368
+ throw new Error(
369
+ "Expected coValue entry to be created, missing subscribe?"
370
+ );
371
+ }
372
+
373
+ let resolveAfterDone: ((coValue: CoValue) => void) | undefined;
374
+
375
+ const peerOptimisticKnownState =
376
+ peer.optimisticKnownStates[msg.coValueID];
377
+
378
+ if (!peerOptimisticKnownState) {
379
+ throw new Error(
380
+ "Expected optimisticKnownState to be set for coValue we receive new content for"
381
+ );
382
+ }
383
+
384
+ if (entry.state === "loading") {
385
+ if (!msg.header) {
386
+ throw new Error("Expected header to be sent in first message");
387
+ }
388
+
389
+ peerOptimisticKnownState.header = true;
390
+
391
+ const coValue = new CoValue(msg.header, this.local);
392
+
393
+ resolveAfterDone = entry.resolve;
394
+
395
+ entry = {
396
+ state: "loaded",
397
+ coValue: coValue,
398
+ };
399
+
400
+ this.local.coValues[msg.coValueID] = entry;
401
+ }
402
+
403
+ const coValue = entry.coValue;
404
+
405
+ let invalidStateAssumed = false;
406
+
407
+ for (const [sessionID, newContentForSession] of Object.entries(
408
+ msg.newContent
409
+ ) as [SessionID, SessionNewContent][]) {
410
+ const ourKnownTxIdx =
411
+ coValue.sessions[sessionID]?.transactions.length;
412
+ const theirFirstNewTxIdx = newContentForSession.after;
413
+
414
+ if ((ourKnownTxIdx || 0) < theirFirstNewTxIdx) {
415
+ invalidStateAssumed = true;
416
+ continue;
417
+ }
418
+
419
+ const alreadyKnownOffset = ourKnownTxIdx
420
+ ? ourKnownTxIdx - theirFirstNewTxIdx
421
+ : 0;
422
+
423
+ const newTransactions =
424
+ newContentForSession.newTransactions.slice(alreadyKnownOffset);
425
+
426
+ const success = coValue.tryAddTransactions(
427
+ sessionID,
428
+ newTransactions,
429
+ newContentForSession.lastHash,
430
+ newContentForSession.lastSignature
431
+ );
432
+
433
+ if (!success) {
434
+ console.error("Failed to add transactions", newTransactions);
435
+ continue;
436
+ }
437
+
438
+ peerOptimisticKnownState.sessions[sessionID] =
439
+ newContentForSession.after +
440
+ newContentForSession.newTransactions.length;
441
+ }
442
+
443
+ if (resolveAfterDone) {
444
+ resolveAfterDone(coValue);
445
+ }
446
+
447
+ await this.syncCoValue(coValue);
448
+
449
+ if (invalidStateAssumed) {
450
+ await peer.outgoing.write({
451
+ action: "wrongAssumedKnownState",
452
+ ...coValue.knownState(),
453
+ });
454
+ }
455
+ }
456
+
457
+ async handleWrongAssumedKnownState(
458
+ msg: WrongAssumedKnownStateMessage,
459
+ peer: PeerState
460
+ ) {
461
+ const coValue = this.local.expectCoValueLoaded(msg.coValueID);
462
+
463
+ peer.optimisticKnownStates[msg.coValueID] = combinedKnownStates(
464
+ msg,
465
+ coValue.knownState()
466
+ );
467
+
468
+ const newContent = coValue.newContentSince(msg);
469
+
470
+ if (newContent) {
471
+ await peer.outgoing.write(newContent);
472
+ }
473
+ }
474
+
475
+ handleUnsubscribe(_msg: UnsubscribeMessage) {
476
+ throw new Error("Method not implemented.");
477
+ }
478
+
479
+ async syncCoValue(coValue: CoValue) {
480
+ for (const peer of Object.values(this.peers)) {
481
+ const optimisticKnownState = peer.optimisticKnownStates[coValue.id];
482
+
483
+ const shouldSync =
484
+ optimisticKnownState ||
485
+ peer.role === "server";
486
+
487
+ if (shouldSync) {
488
+ await this.tellUntoldKnownStateIncludingDependencies(
489
+ coValue.id,
490
+ peer
491
+ );
492
+ await this.sendNewContentIncludingDependencies(
493
+ coValue.id,
494
+ peer
495
+ );
496
+ }
497
+ }
498
+ }
499
+ }
500
+
501
+ function knownStateIn(
502
+ msg:
503
+ | SubscribeMessage
504
+ | TellKnownStateMessage
505
+ | WrongAssumedKnownStateMessage
506
+ ) {
507
+ return {
508
+ coValueID: msg.coValueID,
509
+ header: msg.header,
510
+ sessions: msg.sessions,
511
+ };
512
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["ESNext"],
4
+ "module": "esnext",
5
+ "target": "esnext",
6
+ "moduleResolution": "bundler",
7
+ "moduleDetection": "force",
8
+ "allowImportingTsExtensions": true,
9
+ "strict": true,
10
+ "downlevelIteration": true,
11
+ "skipLibCheck": true,
12
+ "jsx": "preserve",
13
+ "allowSyntheticDefaultImports": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "allowJs": true,
16
+ "noEmit": true,
17
+ "noUncheckedIndexedAccess": true,
18
+ "esModuleInterop": true,
19
+ },
20
+ "include": ["./src/**/*"],
21
+ }