cojson 0.4.13 → 0.5.1
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/CHANGELOG.md +13 -0
- package/dist/coValueCore.js +109 -42
- package/dist/coValueCore.js.map +1 -1
- package/dist/coValues/coList.js +21 -4
- package/dist/coValues/coList.js.map +1 -1
- package/dist/crypto.js +57 -31
- package/dist/crypto.js.map +1 -1
- package/dist/localNode.js +69 -17
- package/dist/localNode.js.map +1 -1
- package/dist/permissions.js +5 -3
- package/dist/permissions.js.map +1 -1
- package/dist/streamUtils.js +34 -8
- package/dist/streamUtils.js.map +1 -1
- package/dist/sync.js +162 -40
- package/dist/sync.js.map +1 -1
- package/dist/typeUtils/accountOrAgentIDfromSessionID.js +2 -1
- package/dist/typeUtils/accountOrAgentIDfromSessionID.js.map +1 -1
- package/package.json +4 -3
- package/src/coValueCore.ts +140 -63
- package/src/coValues/coList.ts +33 -8
- package/src/crypto.ts +83 -41
- package/src/localNode.ts +145 -38
- package/src/permissions.ts +19 -24
- package/src/streamUtils.ts +41 -8
- package/src/sync.ts +206 -59
- package/src/tests/coValue.test.ts +3 -3
- package/src/tests/permissions.test.ts +124 -83
- package/src/tests/sync.test.ts +29 -20
- package/src/typeUtils/accountOrAgentIDfromSessionID.ts +2 -1
package/src/crypto.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
initBundledOnce,
|
|
3
|
+
Ed25519SigningKey,
|
|
4
|
+
Ed25519VerifyingKey,
|
|
5
|
+
X25519StaticSecret,
|
|
6
|
+
Memory,
|
|
7
|
+
Ed25519Signature,
|
|
8
|
+
X25519PublicKey,
|
|
9
|
+
} from "@hazae41/berith";
|
|
2
10
|
import { xsalsa20_poly1305, xsalsa20 } from "@noble/ciphers/salsa";
|
|
3
11
|
import { JsonValue } from "./jsonValue.js";
|
|
4
12
|
import { base58 } from "@scure/base";
|
|
@@ -10,7 +18,13 @@ import { createBLAKE3 } from "hash-wasm";
|
|
|
10
18
|
import { Stringified, parseJSON, stableStringify } from "./jsonStringify.js";
|
|
11
19
|
|
|
12
20
|
let blake3Instance: Awaited<ReturnType<typeof createBLAKE3>>;
|
|
13
|
-
let blake3HashOnce: (data: Uint8Array) => Uint8Array
|
|
21
|
+
let blake3HashOnce: (data: Uint8Array) => Uint8Array = () => {
|
|
22
|
+
throw new Error(
|
|
23
|
+
"cojson WASM dependencies not yet loaded; Make sure to import `cojsonReady` from `cojson` and await it before using any cojson functionality:\n\n" +
|
|
24
|
+
'import { cojsonReady } from "cojson";\n' +
|
|
25
|
+
"await cojsonReady;\n\n"
|
|
26
|
+
);
|
|
27
|
+
};
|
|
14
28
|
let blake3HashOnceWithContext: (
|
|
15
29
|
data: Uint8Array,
|
|
16
30
|
{ context }: { context: Uint8Array }
|
|
@@ -21,29 +35,36 @@ let blake3incrementalUpdateSLOW_WITH_DEVTOOLS: (
|
|
|
21
35
|
) => Uint8Array;
|
|
22
36
|
let blake3digestForState: (state: Uint8Array) => Uint8Array;
|
|
23
37
|
|
|
24
|
-
export const cryptoReady =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
export const cryptoReady = Promise.all([
|
|
39
|
+
new Promise<void>((resolve) => {
|
|
40
|
+
createBLAKE3()
|
|
41
|
+
.then((bl3) => {
|
|
42
|
+
blake3Instance = bl3;
|
|
43
|
+
blake3HashOnce = (data) => {
|
|
44
|
+
return bl3.init().update(data).digest("binary");
|
|
45
|
+
};
|
|
46
|
+
blake3HashOnceWithContext = (data, { context }) => {
|
|
47
|
+
return bl3
|
|
48
|
+
.init()
|
|
49
|
+
.update(context)
|
|
50
|
+
.update(data)
|
|
51
|
+
.digest("binary");
|
|
52
|
+
};
|
|
53
|
+
blake3incrementalUpdateSLOW_WITH_DEVTOOLS = (state, data) => {
|
|
54
|
+
bl3.load(state).update(data);
|
|
55
|
+
return bl3.save();
|
|
56
|
+
};
|
|
57
|
+
blake3digestForState = (state) => {
|
|
58
|
+
return bl3.load(state).digest("binary");
|
|
59
|
+
};
|
|
60
|
+
resolve();
|
|
61
|
+
})
|
|
62
|
+
.catch((e) =>
|
|
63
|
+
console.error("Failed to load cryptography dependencies", e)
|
|
64
|
+
);
|
|
65
|
+
}),
|
|
66
|
+
initBundledOnce(),
|
|
67
|
+
]);
|
|
47
68
|
|
|
48
69
|
export type SignerSecret = `signerSecret_z${string}`;
|
|
49
70
|
export type SignerID = `signer_z${string}`;
|
|
@@ -59,7 +80,9 @@ const textEncoder = new TextEncoder();
|
|
|
59
80
|
const textDecoder = new TextDecoder();
|
|
60
81
|
|
|
61
82
|
export function newRandomSigner(): SignerSecret {
|
|
62
|
-
return `signerSecret_z${base58.encode(
|
|
83
|
+
return `signerSecret_z${base58.encode(
|
|
84
|
+
new Ed25519SigningKey().to_bytes().copyAndDispose()
|
|
85
|
+
)}`;
|
|
63
86
|
}
|
|
64
87
|
|
|
65
88
|
export function signerSecretToBytes(secret: SignerSecret): Uint8Array {
|
|
@@ -72,17 +95,22 @@ export function signerSecretFromBytes(bytes: Uint8Array): SignerSecret {
|
|
|
72
95
|
|
|
73
96
|
export function getSignerID(secret: SignerSecret): SignerID {
|
|
74
97
|
return `signer_z${base58.encode(
|
|
75
|
-
|
|
76
|
-
base58.decode(secret.substring("signerSecret_z".length))
|
|
98
|
+
Ed25519SigningKey.from_bytes(
|
|
99
|
+
new Memory(base58.decode(secret.substring("signerSecret_z".length)))
|
|
77
100
|
)
|
|
101
|
+
.public()
|
|
102
|
+
.to_bytes()
|
|
103
|
+
.copyAndDispose()
|
|
78
104
|
)}`;
|
|
79
105
|
}
|
|
80
106
|
|
|
81
107
|
export function sign(secret: SignerSecret, message: JsonValue): Signature {
|
|
82
|
-
const signature =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
108
|
+
const signature = Ed25519SigningKey.from_bytes(
|
|
109
|
+
new Memory(base58.decode(secret.substring("signerSecret_z".length)))
|
|
110
|
+
)
|
|
111
|
+
.sign(new Memory(textEncoder.encode(stableStringify(message))))
|
|
112
|
+
.to_bytes()
|
|
113
|
+
.copyAndDispose();
|
|
86
114
|
return `signature_z${base58.encode(signature)}`;
|
|
87
115
|
}
|
|
88
116
|
|
|
@@ -91,15 +119,20 @@ export function verify(
|
|
|
91
119
|
message: JsonValue,
|
|
92
120
|
id: SignerID
|
|
93
121
|
): boolean {
|
|
94
|
-
return
|
|
95
|
-
base58.decode(
|
|
96
|
-
|
|
97
|
-
|
|
122
|
+
return new Ed25519VerifyingKey(
|
|
123
|
+
new Memory(base58.decode(id.substring("signer_z".length)))
|
|
124
|
+
).verify(
|
|
125
|
+
new Memory(textEncoder.encode(stableStringify(message))),
|
|
126
|
+
new Ed25519Signature(
|
|
127
|
+
new Memory(base58.decode(signature.substring("signature_z".length)))
|
|
128
|
+
)
|
|
98
129
|
);
|
|
99
130
|
}
|
|
100
131
|
|
|
101
132
|
export function newRandomSealer(): SealerSecret {
|
|
102
|
-
return `sealerSecret_z${base58.encode(
|
|
133
|
+
return `sealerSecret_z${base58.encode(
|
|
134
|
+
new X25519StaticSecret().to_bytes().copyAndDispose()
|
|
135
|
+
)}`;
|
|
103
136
|
}
|
|
104
137
|
|
|
105
138
|
export function sealerSecretToBytes(secret: SealerSecret): Uint8Array {
|
|
@@ -112,9 +145,12 @@ export function sealerSecretFromBytes(bytes: Uint8Array): SealerSecret {
|
|
|
112
145
|
|
|
113
146
|
export function getSealerID(secret: SealerSecret): SealerID {
|
|
114
147
|
return `sealer_z${base58.encode(
|
|
115
|
-
|
|
116
|
-
base58.decode(secret.substring("sealerSecret_z".length))
|
|
148
|
+
X25519StaticSecret.from_bytes(
|
|
149
|
+
new Memory(base58.decode(secret.substring("sealerSecret_z".length)))
|
|
117
150
|
)
|
|
151
|
+
.to_public()
|
|
152
|
+
.to_bytes()
|
|
153
|
+
.copyAndDispose()
|
|
118
154
|
)}`;
|
|
119
155
|
}
|
|
120
156
|
|
|
@@ -180,7 +216,10 @@ export function seal<T extends JsonValue>({
|
|
|
180
216
|
|
|
181
217
|
const plaintext = textEncoder.encode(stableStringify(message));
|
|
182
218
|
|
|
183
|
-
const sharedSecret =
|
|
219
|
+
const sharedSecret = X25519StaticSecret.from_bytes(new Memory(senderPriv))
|
|
220
|
+
.diffie_hellman(X25519PublicKey.from_bytes(new Memory(sealerPub)))
|
|
221
|
+
.to_bytes()
|
|
222
|
+
.copyAndDispose();
|
|
184
223
|
|
|
185
224
|
const sealedBytes = xsalsa20_poly1305(sharedSecret, nOnce).encrypt(
|
|
186
225
|
plaintext
|
|
@@ -205,7 +244,10 @@ export function unseal<T extends JsonValue>(
|
|
|
205
244
|
|
|
206
245
|
const sealedBytes = base64URLtoBytes(sealed.substring("sealed_U".length));
|
|
207
246
|
|
|
208
|
-
const sharedSecret =
|
|
247
|
+
const sharedSecret = X25519StaticSecret.from_bytes(new Memory(sealerPriv))
|
|
248
|
+
.diffie_hellman(X25519PublicKey.from_bytes(new Memory(senderPub)))
|
|
249
|
+
.to_bytes()
|
|
250
|
+
.copyAndDispose();
|
|
209
251
|
|
|
210
252
|
const plaintext = xsalsa20_poly1305(sharedSecret, nOnce).decrypt(
|
|
211
253
|
sealedBytes
|
package/src/localNode.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
Group,
|
|
20
20
|
secretSeedFromInviteSecret,
|
|
21
21
|
} from "./coValues/group.js";
|
|
22
|
-
import { Peer, SyncManager } from "./sync.js";
|
|
22
|
+
import { Peer, PeerID, SyncManager } from "./sync.js";
|
|
23
23
|
import { AgentID, RawCoID, SessionID, isAgentID } from "./ids.js";
|
|
24
24
|
import { CoID } from "./coValue.js";
|
|
25
25
|
import {
|
|
@@ -109,12 +109,26 @@ export class LocalNode {
|
|
|
109
109
|
|
|
110
110
|
if (migration) {
|
|
111
111
|
migration(accountOnNodeWithAccount, profile as P);
|
|
112
|
-
nodeWithAccount.account = new ControlledAccount(
|
|
113
|
-
accountOnNodeWithAccount.core,
|
|
114
|
-
accountOnNodeWithAccount.agentSecret
|
|
115
|
-
);
|
|
116
112
|
}
|
|
117
113
|
|
|
114
|
+
nodeWithAccount.account = new ControlledAccount(
|
|
115
|
+
accountOnNodeWithAccount.core,
|
|
116
|
+
accountOnNodeWithAccount.agentSecret
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// we shouldn't need this, but it fixes account data not syncing for new accounts
|
|
120
|
+
function syncAllCoValuesAfterCreateAccount() {
|
|
121
|
+
for (const coValueEntry of Object.values(nodeWithAccount.coValues)) {
|
|
122
|
+
if (coValueEntry.state === "loaded") {
|
|
123
|
+
void nodeWithAccount.syncManager.syncCoValue(coValueEntry.coValue);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
syncAllCoValuesAfterCreateAccount();
|
|
129
|
+
|
|
130
|
+
setTimeout(syncAllCoValuesAfterCreateAccount, 500);
|
|
131
|
+
|
|
118
132
|
return {
|
|
119
133
|
node: nodeWithAccount,
|
|
120
134
|
accountID: accountOnNodeWithAccount.id,
|
|
@@ -153,6 +167,11 @@ export class LocalNode {
|
|
|
153
167
|
}
|
|
154
168
|
|
|
155
169
|
const account = await accountPromise;
|
|
170
|
+
|
|
171
|
+
if (account === "unavailable") {
|
|
172
|
+
throw new Error("Account unavailable from all peers");
|
|
173
|
+
}
|
|
174
|
+
|
|
156
175
|
const controlledAccount = new ControlledAccount(
|
|
157
176
|
account.core,
|
|
158
177
|
accountSecret
|
|
@@ -199,14 +218,36 @@ export class LocalNode {
|
|
|
199
218
|
}
|
|
200
219
|
|
|
201
220
|
/** @internal */
|
|
202
|
-
|
|
221
|
+
async loadCoValueCore(
|
|
222
|
+
id: RawCoID,
|
|
223
|
+
options: {
|
|
224
|
+
dontLoadFrom?: PeerID;
|
|
225
|
+
dontWaitFor?: PeerID;
|
|
226
|
+
onProgress?: (progress: number) => void;
|
|
227
|
+
} = {}
|
|
228
|
+
): Promise<CoValueCore | "unavailable"> {
|
|
203
229
|
let entry = this.coValues[id];
|
|
204
230
|
if (!entry) {
|
|
205
|
-
|
|
231
|
+
const peersToWaitFor = new Set(
|
|
232
|
+
Object.values(this.syncManager.peers)
|
|
233
|
+
.filter((peer) => peer.role === "server")
|
|
234
|
+
.map((peer) => peer.id)
|
|
235
|
+
);
|
|
236
|
+
if (options.dontWaitFor) peersToWaitFor.delete(options.dontWaitFor);
|
|
237
|
+
entry = newLoadingState(peersToWaitFor, options.onProgress);
|
|
206
238
|
|
|
207
239
|
this.coValues[id] = entry;
|
|
208
240
|
|
|
209
|
-
this.syncManager
|
|
241
|
+
this.syncManager
|
|
242
|
+
.loadFromPeers(id, options.dontLoadFrom)
|
|
243
|
+
.catch((e) => {
|
|
244
|
+
console.error(
|
|
245
|
+
"Error loading from peers",
|
|
246
|
+
id,
|
|
247
|
+
|
|
248
|
+
e
|
|
249
|
+
);
|
|
250
|
+
});
|
|
210
251
|
}
|
|
211
252
|
if (entry.state === "loaded") {
|
|
212
253
|
return Promise.resolve(entry.coValue);
|
|
@@ -221,25 +262,38 @@ export class LocalNode {
|
|
|
221
262
|
*
|
|
222
263
|
* @category 3. Low-level
|
|
223
264
|
*/
|
|
224
|
-
async load<T extends CoValue>(
|
|
225
|
-
|
|
265
|
+
async load<T extends CoValue>(
|
|
266
|
+
id: CoID<T>,
|
|
267
|
+
onProgress?: (progress: number) => void
|
|
268
|
+
): Promise<T | "unavailable"> {
|
|
269
|
+
const core = await this.loadCoValueCore(id, { onProgress });
|
|
270
|
+
|
|
271
|
+
if (core === "unavailable") {
|
|
272
|
+
return "unavailable";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return core.getCurrentContent() as T;
|
|
226
276
|
}
|
|
227
277
|
|
|
228
278
|
/** @category 3. Low-level */
|
|
229
279
|
subscribe<T extends CoValue>(
|
|
230
280
|
id: CoID<T>,
|
|
231
|
-
callback: (update: T) => void
|
|
281
|
+
callback: (update: T | "unavailable") => void
|
|
232
282
|
): () => void {
|
|
233
283
|
let stopped = false;
|
|
234
284
|
let unsubscribe!: () => void;
|
|
235
285
|
|
|
236
|
-
console.log("Subscribing to " + id);
|
|
286
|
+
// console.log("Subscribing to " + id);
|
|
237
287
|
|
|
238
288
|
this.load(id)
|
|
239
289
|
.then((coValue) => {
|
|
240
290
|
if (stopped) {
|
|
241
291
|
return;
|
|
242
292
|
}
|
|
293
|
+
if (coValue === "unavailable") {
|
|
294
|
+
callback("unavailable");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
243
297
|
unsubscribe = coValue.subscribe(callback);
|
|
244
298
|
})
|
|
245
299
|
.catch((e) => {
|
|
@@ -260,6 +314,12 @@ export class LocalNode {
|
|
|
260
314
|
): Promise<void> {
|
|
261
315
|
const groupOrOwnedValue = await this.load(groupOrOwnedValueID);
|
|
262
316
|
|
|
317
|
+
if (groupOrOwnedValue === "unavailable") {
|
|
318
|
+
throw new Error(
|
|
319
|
+
"Trying to accept invite: Group/owned value unavailable from all peers"
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
263
323
|
if (groupOrOwnedValue.core.header.ruleset.type === "ownedByGroup") {
|
|
264
324
|
return this.acceptInvite(
|
|
265
325
|
groupOrOwnedValue.core.header.ruleset.group as CoID<Group>,
|
|
@@ -325,7 +385,7 @@ export class LocalNode {
|
|
|
325
385
|
: "reader"
|
|
326
386
|
);
|
|
327
387
|
|
|
328
|
-
group.core.
|
|
388
|
+
group.core._sessionLogs = groupAsInvite.core.sessionLogs;
|
|
329
389
|
group.core._cachedContent = undefined;
|
|
330
390
|
|
|
331
391
|
for (const groupListener of group.core.listeners) {
|
|
@@ -400,17 +460,6 @@ export class LocalNode {
|
|
|
400
460
|
},
|
|
401
461
|
});
|
|
402
462
|
|
|
403
|
-
console.log(
|
|
404
|
-
"Creating read key",
|
|
405
|
-
getAgentSealerSecret(agentSecret),
|
|
406
|
-
getAgentSealerID(accountAgentID),
|
|
407
|
-
account.id,
|
|
408
|
-
account.core.nextTransactionID(),
|
|
409
|
-
"in session",
|
|
410
|
-
account.core.node.currentSessionID,
|
|
411
|
-
"=",
|
|
412
|
-
sealed
|
|
413
|
-
);
|
|
414
463
|
editable.set(
|
|
415
464
|
`${readKey.id}_for_${accountAgentID}`,
|
|
416
465
|
sealed,
|
|
@@ -432,16 +481,13 @@ export class LocalNode {
|
|
|
432
481
|
|
|
433
482
|
const accountOnThisNode = this.expectCoValueLoaded(account.id);
|
|
434
483
|
|
|
435
|
-
accountOnThisNode.
|
|
436
|
-
|
|
437
|
-
};
|
|
484
|
+
accountOnThisNode._sessionLogs = new Map(account.core.sessionLogs);
|
|
485
|
+
|
|
438
486
|
accountOnThisNode._cachedContent = undefined;
|
|
439
487
|
|
|
440
488
|
const profileOnThisNode = this.createCoValue(profile.core.header);
|
|
441
489
|
|
|
442
|
-
profileOnThisNode.
|
|
443
|
-
...profile.core.sessions,
|
|
444
|
-
};
|
|
490
|
+
profileOnThisNode._sessionLogs = new Map(profile.core.sessionLogs);
|
|
445
491
|
profileOnThisNode._cachedContent = undefined;
|
|
446
492
|
|
|
447
493
|
return new ControlledAccount(accountOnThisNode, agentSecret);
|
|
@@ -475,6 +521,41 @@ export class LocalNode {
|
|
|
475
521
|
return new Account(coValue).getCurrentAgentID();
|
|
476
522
|
}
|
|
477
523
|
|
|
524
|
+
async resolveAccountAgentAsync(
|
|
525
|
+
id: AccountID | AgentID,
|
|
526
|
+
expectation?: string
|
|
527
|
+
): Promise<AgentID> {
|
|
528
|
+
if (isAgentID(id)) {
|
|
529
|
+
return id;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const coValue = await this.loadCoValueCore(id);
|
|
533
|
+
|
|
534
|
+
if (coValue === "unavailable") {
|
|
535
|
+
throw new Error(
|
|
536
|
+
`${
|
|
537
|
+
expectation ? expectation + ": " : ""
|
|
538
|
+
}Account ${id} is unavailable from all peers`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (
|
|
543
|
+
coValue.header.type !== "comap" ||
|
|
544
|
+
coValue.header.ruleset.type !== "group" ||
|
|
545
|
+
!coValue.header.meta ||
|
|
546
|
+
!("type" in coValue.header.meta) ||
|
|
547
|
+
coValue.header.meta.type !== "account"
|
|
548
|
+
) {
|
|
549
|
+
throw new Error(
|
|
550
|
+
`${
|
|
551
|
+
expectation ? expectation + ": " : ""
|
|
552
|
+
}CoValue ${id} is not an account`
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return new Account(coValue).getCurrentAgentID();
|
|
557
|
+
}
|
|
558
|
+
|
|
478
559
|
/**
|
|
479
560
|
* @deprecated use Account.createGroup() instead
|
|
480
561
|
*/
|
|
@@ -543,7 +624,7 @@ export class LocalNode {
|
|
|
543
624
|
const newCoValue = new CoValueCore(
|
|
544
625
|
entry.coValue.header,
|
|
545
626
|
newNode,
|
|
546
|
-
|
|
627
|
+
new Map(entry.coValue.sessionLogs)
|
|
547
628
|
);
|
|
548
629
|
|
|
549
630
|
newNode.coValues[coValueID as RawCoID] = {
|
|
@@ -575,17 +656,34 @@ export class LocalNode {
|
|
|
575
656
|
type CoValueState =
|
|
576
657
|
| {
|
|
577
658
|
state: "loading";
|
|
578
|
-
done: Promise<CoValueCore>;
|
|
579
|
-
resolve: (coValue: CoValueCore) => void;
|
|
659
|
+
done: Promise<CoValueCore | "unavailable">;
|
|
660
|
+
resolve: (coValue: CoValueCore | "unavailable") => void;
|
|
580
661
|
onProgress?: (progress: number) => void;
|
|
662
|
+
firstPeerState: {
|
|
663
|
+
[peerID: string]:
|
|
664
|
+
| {
|
|
665
|
+
type: "waiting";
|
|
666
|
+
done: Promise<void>;
|
|
667
|
+
resolve: () => void;
|
|
668
|
+
}
|
|
669
|
+
| { type: "available" }
|
|
670
|
+
| { type: "unavailable" };
|
|
671
|
+
};
|
|
581
672
|
}
|
|
582
|
-
| {
|
|
673
|
+
| {
|
|
674
|
+
state: "loaded";
|
|
675
|
+
coValue: CoValueCore;
|
|
676
|
+
onProgress?: (progress: number) => void;
|
|
677
|
+
};
|
|
583
678
|
|
|
584
679
|
/** @internal */
|
|
585
|
-
export function newLoadingState(
|
|
586
|
-
|
|
680
|
+
export function newLoadingState(
|
|
681
|
+
currentPeerIds: Set<PeerID>,
|
|
682
|
+
onProgress?: (progress: number) => void
|
|
683
|
+
): CoValueState {
|
|
684
|
+
let resolve: (coValue: CoValueCore | "unavailable") => void;
|
|
587
685
|
|
|
588
|
-
const promise = new Promise<CoValueCore>((r) => {
|
|
686
|
+
const promise = new Promise<CoValueCore | "unavailable">((r) => {
|
|
589
687
|
resolve = r;
|
|
590
688
|
});
|
|
591
689
|
|
|
@@ -593,6 +691,15 @@ export function newLoadingState(onProgress?: (progress: number) => void): CoValu
|
|
|
593
691
|
state: "loading",
|
|
594
692
|
done: promise,
|
|
595
693
|
resolve: resolve!,
|
|
596
|
-
onProgress
|
|
694
|
+
onProgress,
|
|
695
|
+
firstPeerState: Object.fromEntries(
|
|
696
|
+
[...currentPeerIds].map((id) => {
|
|
697
|
+
let resolve: () => void;
|
|
698
|
+
const done = new Promise<void>((r) => {
|
|
699
|
+
resolve = r;
|
|
700
|
+
});
|
|
701
|
+
return [id, { type: "waiting", done, resolve: resolve! }];
|
|
702
|
+
})
|
|
703
|
+
),
|
|
597
704
|
};
|
|
598
705
|
}
|
package/src/permissions.ts
CHANGED
|
@@ -2,10 +2,7 @@ import { CoID } from "./coValue.js";
|
|
|
2
2
|
import { MapOpPayload } from "./coValues/coMap.js";
|
|
3
3
|
import { JsonValue } from "./jsonValue.js";
|
|
4
4
|
import { KeyID } from "./crypto.js";
|
|
5
|
-
import {
|
|
6
|
-
CoValueCore,
|
|
7
|
-
Transaction,
|
|
8
|
-
} from "./coValueCore.js";
|
|
5
|
+
import { CoValueCore, Transaction } from "./coValueCore.js";
|
|
9
6
|
import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
|
|
10
7
|
import { AgentID, RawCoID, SessionID, TransactionID } from "./ids.js";
|
|
11
8
|
import { Account, AccountID, Profile } from "./coValues/account.js";
|
|
@@ -31,19 +28,19 @@ export function determineValidTransactions(
|
|
|
31
28
|
coValue: CoValueCore
|
|
32
29
|
): { txID: TransactionID; tx: Transaction }[] {
|
|
33
30
|
if (coValue.header.ruleset.type === "group") {
|
|
34
|
-
const allTransactionsSorted =
|
|
35
|
-
(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
);
|
|
31
|
+
const allTransactionsSorted = [
|
|
32
|
+
...coValue.sessionLogs.entries(),
|
|
33
|
+
].flatMap(([sessionID, sessionLog]) => {
|
|
34
|
+
return sessionLog.transactions.map((tx, txIndex) => ({
|
|
35
|
+
sessionID,
|
|
36
|
+
txIndex,
|
|
37
|
+
tx,
|
|
38
|
+
})) as {
|
|
39
|
+
sessionID: SessionID;
|
|
40
|
+
txIndex: number;
|
|
41
|
+
tx: Transaction;
|
|
42
|
+
}[];
|
|
43
|
+
});
|
|
47
44
|
|
|
48
45
|
allTransactionsSorted.sort((a, b) => {
|
|
49
46
|
return a.tx.madeAt - b.tx.madeAt;
|
|
@@ -242,11 +239,9 @@ export function determineValidTransactions(
|
|
|
242
239
|
throw new Error("Group must be a map");
|
|
243
240
|
}
|
|
244
241
|
|
|
245
|
-
return
|
|
242
|
+
return [...coValue.sessionLogs.entries()].flatMap(
|
|
246
243
|
([sessionID, sessionLog]) => {
|
|
247
|
-
const transactor = accountOrAgentIDfromSessionID(
|
|
248
|
-
sessionID as SessionID
|
|
249
|
-
);
|
|
244
|
+
const transactor = accountOrAgentIDfromSessionID(sessionID);
|
|
250
245
|
|
|
251
246
|
return sessionLog.transactions
|
|
252
247
|
.filter((tx) => {
|
|
@@ -266,16 +261,16 @@ export function determineValidTransactions(
|
|
|
266
261
|
);
|
|
267
262
|
})
|
|
268
263
|
.map((tx, txIndex) => ({
|
|
269
|
-
txID: { sessionID: sessionID
|
|
264
|
+
txID: { sessionID: sessionID, txIndex },
|
|
270
265
|
tx,
|
|
271
266
|
}));
|
|
272
267
|
}
|
|
273
268
|
);
|
|
274
269
|
} else if (coValue.header.ruleset.type === "unsafeAllowAll") {
|
|
275
|
-
return
|
|
270
|
+
return [...coValue.sessionLogs.entries()].flatMap(
|
|
276
271
|
([sessionID, sessionLog]) => {
|
|
277
272
|
return sessionLog.transactions.map((tx, txIndex) => ({
|
|
278
|
-
txID: { sessionID: sessionID
|
|
273
|
+
txID: { sessionID: sessionID, txIndex },
|
|
279
274
|
tx,
|
|
280
275
|
}));
|
|
281
276
|
}
|
package/src/streamUtils.ts
CHANGED
|
@@ -18,11 +18,11 @@ export function connectedPeers(
|
|
|
18
18
|
peer2role?: Peer["role"];
|
|
19
19
|
} = {}
|
|
20
20
|
): [Peer, Peer] {
|
|
21
|
-
const [inRx1, inTx1] = newStreamPair<SyncMessage>();
|
|
22
|
-
const [outRx1, outTx1] = newStreamPair<SyncMessage>();
|
|
21
|
+
const [inRx1, inTx1] = newStreamPair<SyncMessage>(peer1id + "_in");
|
|
22
|
+
const [outRx1, outTx1] = newStreamPair<SyncMessage>(peer1id + "_out");
|
|
23
23
|
|
|
24
|
-
const [inRx2, inTx2] = newStreamPair<SyncMessage>();
|
|
25
|
-
const [outRx2, outTx2] = newStreamPair<SyncMessage>();
|
|
24
|
+
const [inRx2, inTx2] = newStreamPair<SyncMessage>(peer2id + "_in");
|
|
25
|
+
const [outRx2, outTx2] = newStreamPair<SyncMessage>(peer2id + "_out");
|
|
26
26
|
|
|
27
27
|
void outRx2
|
|
28
28
|
.pipeThrough(
|
|
@@ -37,7 +37,7 @@ export function connectedPeers(
|
|
|
37
37
|
JSON.stringify(
|
|
38
38
|
chunk,
|
|
39
39
|
(k, v) =>
|
|
40
|
-
|
|
40
|
+
k === "changes" || k === "encryptedChanges"
|
|
41
41
|
? v.slice(0, 20) + "..."
|
|
42
42
|
: v,
|
|
43
43
|
2
|
|
@@ -62,7 +62,7 @@ export function connectedPeers(
|
|
|
62
62
|
JSON.stringify(
|
|
63
63
|
chunk,
|
|
64
64
|
(k, v) =>
|
|
65
|
-
|
|
65
|
+
k === "changes" || k === "encryptedChanges"
|
|
66
66
|
? v.slice(0, 20) + "..."
|
|
67
67
|
: v,
|
|
68
68
|
2
|
|
@@ -91,7 +91,10 @@ export function connectedPeers(
|
|
|
91
91
|
return [peer1AsPeer, peer2AsPeer];
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
export function newStreamPair<T>(
|
|
94
|
+
export function newStreamPair<T>(
|
|
95
|
+
pairName?: string
|
|
96
|
+
): [ReadableStream<T>, WritableStream<T>] {
|
|
97
|
+
let queueLength = 0;
|
|
95
98
|
let readerClosed = false;
|
|
96
99
|
|
|
97
100
|
let resolveEnqueue: (enqueue: (item: T) => void) => void;
|
|
@@ -104,6 +107,22 @@ export function newStreamPair<T>(): [ReadableStream<T>, WritableStream<T>] {
|
|
|
104
107
|
resolveClose = resolve;
|
|
105
108
|
});
|
|
106
109
|
|
|
110
|
+
let queueWasOverflowing = false;
|
|
111
|
+
|
|
112
|
+
function maybeReportQueueLength() {
|
|
113
|
+
if (queueLength >= 100) {
|
|
114
|
+
queueWasOverflowing = true;
|
|
115
|
+
if (queueLength % 100 === 0) {
|
|
116
|
+
console.warn(pairName, "overflowing queue length", queueLength);
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
if (queueWasOverflowing) {
|
|
120
|
+
console.debug(pairName, "ok queue length", queueLength);
|
|
121
|
+
queueWasOverflowing = false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
107
126
|
const readable = new ReadableStream<T>({
|
|
108
127
|
async start(controller) {
|
|
109
128
|
resolveEnqueue(controller.enqueue.bind(controller));
|
|
@@ -114,12 +133,26 @@ export function newStreamPair<T>(): [ReadableStream<T>, WritableStream<T>] {
|
|
|
114
133
|
console.log("Manually closing reader");
|
|
115
134
|
readerClosed = true;
|
|
116
135
|
},
|
|
117
|
-
})
|
|
136
|
+
}).pipeThrough(
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
138
|
+
new TransformStream<any, any>({
|
|
139
|
+
transform(
|
|
140
|
+
chunk: SyncMessage,
|
|
141
|
+
controller: { enqueue: (msg: SyncMessage) => void }
|
|
142
|
+
) {
|
|
143
|
+
queueLength -= 1;
|
|
144
|
+
maybeReportQueueLength();
|
|
145
|
+
controller.enqueue(chunk);
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
) as ReadableStream<T>;
|
|
118
149
|
|
|
119
150
|
let lastWritePromise = Promise.resolve();
|
|
120
151
|
|
|
121
152
|
const writable = new WritableStream<T>({
|
|
122
153
|
async write(chunk) {
|
|
154
|
+
queueLength += 1;
|
|
155
|
+
maybeReportQueueLength();
|
|
123
156
|
const enqueue = await enqueuePromise;
|
|
124
157
|
if (readerClosed) {
|
|
125
158
|
throw new Error("Reader closed");
|