cojson 0.18.6 → 0.18.8
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +18 -0
- package/dist/coValueContentMessage.d.ts +2 -0
- package/dist/coValueContentMessage.d.ts.map +1 -1
- package/dist/coValueContentMessage.js +7 -0
- package/dist/coValueContentMessage.js.map +1 -1
- package/dist/coValueCore/SessionMap.d.ts +2 -2
- package/dist/coValueCore/SessionMap.d.ts.map +1 -1
- package/dist/coValueCore/SessionMap.js +2 -4
- package/dist/coValueCore/SessionMap.js.map +1 -1
- package/dist/coValueCore/branching.d.ts +31 -9
- package/dist/coValueCore/branching.d.ts.map +1 -1
- package/dist/coValueCore/branching.js +50 -100
- package/dist/coValueCore/branching.js.map +1 -1
- package/dist/coValueCore/coValueCore.d.ts +12 -8
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +93 -23
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/coValueCore/verifiedState.d.ts +4 -2
- package/dist/coValueCore/verifiedState.d.ts.map +1 -1
- package/dist/coValueCore/verifiedState.js +6 -4
- package/dist/coValueCore/verifiedState.js.map +1 -1
- package/dist/coValues/coList.d.ts.map +1 -1
- package/dist/coValues/coList.js +10 -1
- package/dist/coValues/coList.js.map +1 -1
- package/dist/coValues/coMap.d.ts +2 -2
- package/dist/coValues/coMap.d.ts.map +1 -1
- package/dist/coValues/coMap.js +8 -8
- package/dist/coValues/coMap.js.map +1 -1
- package/dist/coValues/group.d.ts.map +1 -1
- package/dist/coValues/group.js +14 -1
- package/dist/coValues/group.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/crypto/PureJSCrypto.d.ts.map +1 -1
- package/dist/crypto/PureJSCrypto.js +14 -6
- package/dist/crypto/PureJSCrypto.js.map +1 -1
- package/dist/exports.d.ts +3 -2
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +2 -2
- package/dist/exports.js.map +1 -1
- package/dist/localNode.d.ts +1 -0
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +10 -2
- package/dist/localNode.js.map +1 -1
- package/dist/storage/storageAsync.d.ts.map +1 -1
- package/dist/storage/storageAsync.js.map +1 -1
- package/dist/sync.d.ts +3 -3
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +29 -19
- package/dist/sync.js.map +1 -1
- package/dist/tests/branching.test.js +107 -9
- package/dist/tests/branching.test.js.map +1 -1
- package/dist/tests/coValueCore.test.js +45 -1
- package/dist/tests/coValueCore.test.js.map +1 -1
- package/dist/tests/sync.content.test.d.ts +2 -0
- package/dist/tests/sync.content.test.d.ts.map +1 -0
- package/dist/tests/sync.content.test.js +120 -0
- package/dist/tests/sync.content.test.js.map +1 -0
- package/dist/tests/sync.load.test.js +15 -2
- package/dist/tests/sync.load.test.js.map +1 -1
- package/dist/tests/sync.storage.test.js +1 -1
- package/dist/tests/sync.upload.test.js +2 -2
- package/package.json +2 -2
- package/src/coValueContentMessage.ts +13 -0
- package/src/coValueCore/SessionMap.ts +2 -2
- package/src/coValueCore/branching.ts +94 -149
- package/src/coValueCore/coValueCore.ts +121 -27
- package/src/coValueCore/verifiedState.ts +8 -0
- package/src/coValues/coList.ts +12 -1
- package/src/coValues/coMap.ts +10 -12
- package/src/coValues/group.ts +14 -1
- package/src/config.ts +9 -0
- package/src/crypto/PureJSCrypto.ts +25 -13
- package/src/exports.ts +7 -1
- package/src/localNode.ts +12 -2
- package/src/storage/storageAsync.ts +0 -1
- package/src/sync.ts +37 -33
- package/src/tests/branching.test.ts +158 -9
- package/src/tests/coValueCore.test.ts +62 -2
- package/src/tests/sync.content.test.ts +153 -0
- package/src/tests/sync.load.test.ts +19 -2
- package/src/tests/sync.storage.test.ts +1 -1
- package/src/tests/sync.upload.test.ts +2 -2
package/src/coValues/coMap.ts
CHANGED
|
@@ -49,10 +49,17 @@ export class RawCoMapView<
|
|
|
49
49
|
latest: {
|
|
50
50
|
[Key in keyof Shape & string]?: MapOp<Key, Shape[Key]>;
|
|
51
51
|
};
|
|
52
|
+
|
|
52
53
|
/** @internal */
|
|
53
|
-
latestTxMadeAt: number
|
|
54
|
+
get latestTxMadeAt(): number {
|
|
55
|
+
return this.core.latestTxMadeAt;
|
|
56
|
+
}
|
|
57
|
+
|
|
54
58
|
/** @internal */
|
|
55
|
-
earliestTxMadeAt: number
|
|
59
|
+
get earliestTxMadeAt(): number {
|
|
60
|
+
return this.core.earliestTxMadeAt;
|
|
61
|
+
}
|
|
62
|
+
|
|
56
63
|
/** @internal */
|
|
57
64
|
ops: {
|
|
58
65
|
[Key in keyof Shape & string]?: MapOp<Key, Shape[Key]>[];
|
|
@@ -80,8 +87,7 @@ export class RawCoMapView<
|
|
|
80
87
|
) {
|
|
81
88
|
this.id = core.id as CoID<this>;
|
|
82
89
|
this.core = core;
|
|
83
|
-
|
|
84
|
-
this.earliestTxMadeAt = null;
|
|
90
|
+
|
|
85
91
|
this.ignorePrivateTransactions =
|
|
86
92
|
options?.ignorePrivateTransactions ?? false;
|
|
87
93
|
this.ops = {};
|
|
@@ -105,10 +111,6 @@ export class RawCoMapView<
|
|
|
105
111
|
return;
|
|
106
112
|
}
|
|
107
113
|
|
|
108
|
-
if (this.earliestTxMadeAt === null && newValidTransactions[0]) {
|
|
109
|
-
this.earliestTxMadeAt = newValidTransactions[0].madeAt;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
114
|
const { ops } = this;
|
|
113
115
|
|
|
114
116
|
const changedEntries = new Map<
|
|
@@ -117,10 +119,6 @@ export class RawCoMapView<
|
|
|
117
119
|
>();
|
|
118
120
|
|
|
119
121
|
for (const { txID, changes, madeAt, tx } of newValidTransactions) {
|
|
120
|
-
if (madeAt > this.latestTxMadeAt) {
|
|
121
|
-
this.latestTxMadeAt = madeAt;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
122
|
for (let changeIdx = 0; changeIdx < changes.length; changeIdx++) {
|
|
125
123
|
const change = changes[changeIdx] as MapOpPayload<
|
|
126
124
|
keyof Shape & string,
|
package/src/coValues/group.ts
CHANGED
|
@@ -1070,6 +1070,9 @@ export class RawGroup<
|
|
|
1070
1070
|
|
|
1071
1071
|
if (init) {
|
|
1072
1072
|
map.assign(init, initPrivacy);
|
|
1073
|
+
} else if (!uniqueness.createdAt) {
|
|
1074
|
+
// If the createdAt is not set, we need to make a trusting transaction to set the createdAt
|
|
1075
|
+
map.core.makeTransaction([], "trusting");
|
|
1073
1076
|
}
|
|
1074
1077
|
|
|
1075
1078
|
return map;
|
|
@@ -1101,6 +1104,9 @@ export class RawGroup<
|
|
|
1101
1104
|
|
|
1102
1105
|
if (init?.length) {
|
|
1103
1106
|
list.appendItems(init, undefined, initPrivacy);
|
|
1107
|
+
} else if (!uniqueness.createdAt) {
|
|
1108
|
+
// If the createdAt is not set, we need to make a trusting transaction to set the createdAt
|
|
1109
|
+
list.core.makeTransaction([], "trusting");
|
|
1104
1110
|
}
|
|
1105
1111
|
|
|
1106
1112
|
return list;
|
|
@@ -1141,7 +1147,7 @@ export class RawGroup<
|
|
|
1141
1147
|
meta?: C["headerMeta"],
|
|
1142
1148
|
uniqueness: CoValueUniqueness = this.crypto.createdNowUnique(),
|
|
1143
1149
|
): C {
|
|
1144
|
-
|
|
1150
|
+
const stream = this.core.node
|
|
1145
1151
|
.createCoValue({
|
|
1146
1152
|
type: "costream",
|
|
1147
1153
|
ruleset: {
|
|
@@ -1152,6 +1158,13 @@ export class RawGroup<
|
|
|
1152
1158
|
...uniqueness,
|
|
1153
1159
|
})
|
|
1154
1160
|
.getCurrentContent() as C;
|
|
1161
|
+
|
|
1162
|
+
if (!uniqueness.createdAt) {
|
|
1163
|
+
// If the createdAt is not set, we need to make a trusting transaction to set the createdAt
|
|
1164
|
+
stream.core.makeTransaction([], "trusting");
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
return stream;
|
|
1155
1168
|
}
|
|
1156
1169
|
|
|
1157
1170
|
/** @category 3. Value creation */
|
package/src/config.ts
CHANGED
|
@@ -7,12 +7,21 @@
|
|
|
7
7
|
**/
|
|
8
8
|
export const TRANSACTION_CONFIG = {
|
|
9
9
|
MAX_RECOMMENDED_TX_SIZE: 100 * 1024,
|
|
10
|
+
/**
|
|
11
|
+
* Messages larger than this will be rejected when creating a transaction.
|
|
12
|
+
* The current limit is set at 1MB because that's the limit imposed by Cloudflare to Websocket messages.
|
|
13
|
+
*/
|
|
14
|
+
MAX_TX_SIZE_BYTES: 1 * 1024 * 1024,
|
|
10
15
|
};
|
|
11
16
|
|
|
12
17
|
export function setMaxRecommendedTxSize(size: number) {
|
|
13
18
|
TRANSACTION_CONFIG.MAX_RECOMMENDED_TX_SIZE = size;
|
|
14
19
|
}
|
|
15
20
|
|
|
21
|
+
export function setMaxTxSizeBytes(size: number) {
|
|
22
|
+
TRANSACTION_CONFIG.MAX_TX_SIZE_BYTES = size;
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
export const CO_VALUE_LOADING_CONFIG = {
|
|
17
26
|
MAX_RETRIES: 1,
|
|
18
27
|
TIMEOUT: 30_000,
|
|
@@ -32,6 +32,29 @@ import { ControlledAccountOrAgent } from "../coValues/account.js";
|
|
|
32
32
|
|
|
33
33
|
type Blake3State = ReturnType<typeof blake3.create>;
|
|
34
34
|
|
|
35
|
+
const x25519SharedSecretCache = new Map<string, Uint8Array>();
|
|
36
|
+
|
|
37
|
+
function getx25519SharedSecret(
|
|
38
|
+
privateKeyA: SealerSecret,
|
|
39
|
+
publicKeyB: SealerID,
|
|
40
|
+
): Uint8Array {
|
|
41
|
+
const cacheKey = `${privateKeyA}-${publicKeyB}`;
|
|
42
|
+
let sharedSecret = x25519SharedSecretCache.get(cacheKey);
|
|
43
|
+
|
|
44
|
+
if (!sharedSecret) {
|
|
45
|
+
const privateKeyABytes = base58.decode(
|
|
46
|
+
privateKeyA.substring("sealerSecret_z".length),
|
|
47
|
+
);
|
|
48
|
+
const publicKeyBBytes = base58.decode(
|
|
49
|
+
publicKeyB.substring("sealer_z".length),
|
|
50
|
+
);
|
|
51
|
+
sharedSecret = x25519.getSharedSecret(privateKeyABytes, publicKeyBBytes);
|
|
52
|
+
x25519SharedSecretCache.set(cacheKey, sharedSecret);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return sharedSecret;
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
/**
|
|
36
59
|
* Pure JavaScript implementation of the CryptoProvider interface using noble-curves and noble-ciphers libraries.
|
|
37
60
|
* This provides a fallback implementation that doesn't require WebAssembly, offering:
|
|
@@ -164,16 +187,10 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
|
|
|
164
187
|
to: SealerID;
|
|
165
188
|
nOnceMaterial: { in: RawCoID; tx: TransactionID };
|
|
166
189
|
}): Sealed<T> {
|
|
190
|
+
const sharedSecret = getx25519SharedSecret(from, to);
|
|
167
191
|
const nOnce = this.generateJsonNonce(nOnceMaterial);
|
|
168
|
-
|
|
169
|
-
const sealerPub = base58.decode(to.substring("sealer_z".length));
|
|
170
|
-
|
|
171
|
-
const senderPriv = base58.decode(from.substring("sealerSecret_z".length));
|
|
172
|
-
|
|
173
192
|
const plaintext = textEncoder.encode(stableStringify(message));
|
|
174
193
|
|
|
175
|
-
const sharedSecret = x25519.getSharedSecret(senderPriv, sealerPub);
|
|
176
|
-
|
|
177
194
|
const sealedBytes = xsalsa20poly1305(sharedSecret, nOnce).encrypt(
|
|
178
195
|
plaintext,
|
|
179
196
|
);
|
|
@@ -189,14 +206,9 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
|
|
|
189
206
|
): T | undefined {
|
|
190
207
|
const nOnce = this.generateJsonNonce(nOnceMaterial);
|
|
191
208
|
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
const senderPub = base58.decode(from.substring("sealer_z".length));
|
|
195
|
-
|
|
209
|
+
const sharedSecret = getx25519SharedSecret(sealer, from);
|
|
196
210
|
const sealedBytes = base64URLtoBytes(sealed.substring("sealed_U".length));
|
|
197
211
|
|
|
198
|
-
const sharedSecret = x25519.getSharedSecret(sealerPriv, senderPub);
|
|
199
|
-
|
|
200
212
|
const plaintext = xsalsa20poly1305(sharedSecret, nOnce).decrypt(
|
|
201
213
|
sealedBytes,
|
|
202
214
|
);
|
package/src/exports.ts
CHANGED
|
@@ -63,7 +63,12 @@ import type { JsonObject, JsonValue } from "./jsonValue.js";
|
|
|
63
63
|
import type * as Media from "./media.js";
|
|
64
64
|
import { disablePermissionErrors } from "./permissions.js";
|
|
65
65
|
import type { Peer, SyncMessage } from "./sync.js";
|
|
66
|
-
import {
|
|
66
|
+
import {
|
|
67
|
+
DisconnectedError,
|
|
68
|
+
SyncManager,
|
|
69
|
+
emptyKnownState,
|
|
70
|
+
hwrServerPeerSelector,
|
|
71
|
+
} from "./sync.js";
|
|
67
72
|
|
|
68
73
|
import {
|
|
69
74
|
getContentMessageSize,
|
|
@@ -163,6 +168,7 @@ export {
|
|
|
163
168
|
LogLevel,
|
|
164
169
|
base64URLtoBytes,
|
|
165
170
|
bytesToBase64url,
|
|
171
|
+
hwrServerPeerSelector,
|
|
166
172
|
};
|
|
167
173
|
|
|
168
174
|
export type {
|
package/src/localNode.ts
CHANGED
|
@@ -97,6 +97,10 @@ export class LocalNode {
|
|
|
97
97
|
this.storage = undefined;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
hasCoValue(id: RawCoID) {
|
|
101
|
+
return this.coValues.has(id);
|
|
102
|
+
}
|
|
103
|
+
|
|
100
104
|
getCoValue(id: RawCoID) {
|
|
101
105
|
let entry = this.coValues.get(id);
|
|
102
106
|
|
|
@@ -479,8 +483,14 @@ export class LocalNode {
|
|
|
479
483
|
return branch.getCurrentContent() as T;
|
|
480
484
|
}
|
|
481
485
|
|
|
482
|
-
//
|
|
483
|
-
|
|
486
|
+
// Do a synchronous check to see if the branch exists, if not we don't need to try to load the branch
|
|
487
|
+
if (!source.hasBranch(branchName, branchOwnerID)) {
|
|
488
|
+
return source
|
|
489
|
+
.createBranch(branchName, branchOwnerID)
|
|
490
|
+
.getCurrentContent() as T;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
await this.loadCoValueCore(branch.id);
|
|
484
494
|
|
|
485
495
|
if (!branch.isAvailable()) {
|
|
486
496
|
return source
|
package/src/sync.ts
CHANGED
|
@@ -139,6 +139,13 @@ export class SyncManager {
|
|
|
139
139
|
// the transactions have already been verified by the [trusted] peer that sent them.
|
|
140
140
|
private skipVerify: boolean = false;
|
|
141
141
|
|
|
142
|
+
// When true, coValues that arrive from server peers will be ignored if they had not
|
|
143
|
+
// explicitly been requested via a load message.
|
|
144
|
+
private _ignoreUnknownCoValuesFromServers: boolean = false;
|
|
145
|
+
ignoreUnknownCoValuesFromServers() {
|
|
146
|
+
this._ignoreUnknownCoValuesFromServers = true;
|
|
147
|
+
}
|
|
148
|
+
|
|
142
149
|
peersCounter = metrics.getMeter("cojson").createUpDownCounter("jazz.peers", {
|
|
143
150
|
description: "Amount of connected peers",
|
|
144
151
|
valueType: ValueType.INT,
|
|
@@ -191,7 +198,28 @@ export class SyncManager {
|
|
|
191
198
|
msg,
|
|
192
199
|
});
|
|
193
200
|
return;
|
|
194
|
-
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Prevent core shards from storing content that belongs to other shards.
|
|
204
|
+
//
|
|
205
|
+
// This can happen because a covalue "miss" on a core shard will cause a load message to
|
|
206
|
+
// be sent to the original unsharded core. The original core, treating the peer as a client,
|
|
207
|
+
// will respond with the covalue and its dependencies. Those dependencies might not belong
|
|
208
|
+
// to this shard, so they should be ignored.
|
|
209
|
+
//
|
|
210
|
+
// TODO: remove once core has been sharded.
|
|
211
|
+
if (
|
|
212
|
+
peer.role === "server" &&
|
|
213
|
+
this._ignoreUnknownCoValuesFromServers &&
|
|
214
|
+
!this.local.hasCoValue(msg.id)
|
|
215
|
+
) {
|
|
216
|
+
logger.warn(
|
|
217
|
+
`Ignoring message ${msg.action} on unknown coValue ${msg.id} from peer ${peer.id}`,
|
|
218
|
+
);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (this.local.getCoValue(msg.id).isErroredInPeer(peer.id)) {
|
|
195
223
|
logger.warn(
|
|
196
224
|
`Skipping message ${msg.action} on errored coValue ${msg.id} from peer ${peer.id}`,
|
|
197
225
|
);
|
|
@@ -219,28 +247,7 @@ export class SyncManager {
|
|
|
219
247
|
}
|
|
220
248
|
}
|
|
221
249
|
|
|
222
|
-
|
|
223
|
-
id: RawCoID,
|
|
224
|
-
peer: PeerState,
|
|
225
|
-
seen: Set<RawCoID> = new Set(),
|
|
226
|
-
) {
|
|
227
|
-
this.sendNewContent(id, peer, seen, true);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
sendNewContentWithoutDependencies(
|
|
231
|
-
id: RawCoID,
|
|
232
|
-
peer: PeerState,
|
|
233
|
-
seen: Set<RawCoID> = new Set(),
|
|
234
|
-
) {
|
|
235
|
-
this.sendNewContent(id, peer, seen, false);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private sendNewContent(
|
|
239
|
-
id: RawCoID,
|
|
240
|
-
peer: PeerState,
|
|
241
|
-
seen: Set<RawCoID> = new Set(),
|
|
242
|
-
includeDependencies: boolean,
|
|
243
|
-
) {
|
|
250
|
+
sendNewContent(id: RawCoID, peer: PeerState, seen: Set<RawCoID> = new Set()) {
|
|
244
251
|
if (seen.has(id)) {
|
|
245
252
|
return;
|
|
246
253
|
}
|
|
@@ -253,9 +260,10 @@ export class SyncManager {
|
|
|
253
260
|
return;
|
|
254
261
|
}
|
|
255
262
|
|
|
263
|
+
const includeDependencies = peer.role !== "server";
|
|
256
264
|
if (includeDependencies) {
|
|
257
265
|
for (const dependency of coValue.getDependedOnCoValues()) {
|
|
258
|
-
this.
|
|
266
|
+
this.sendNewContent(dependency, peer, seen);
|
|
259
267
|
}
|
|
260
268
|
}
|
|
261
269
|
|
|
@@ -434,7 +442,7 @@ export class SyncManager {
|
|
|
434
442
|
const coValue = this.local.getCoValue(msg.id);
|
|
435
443
|
|
|
436
444
|
if (coValue.isAvailable()) {
|
|
437
|
-
this.
|
|
445
|
+
this.sendNewContent(msg.id, peer);
|
|
438
446
|
return;
|
|
439
447
|
}
|
|
440
448
|
|
|
@@ -444,7 +452,7 @@ export class SyncManager {
|
|
|
444
452
|
|
|
445
453
|
const handleLoadResult = () => {
|
|
446
454
|
if (coValue.isAvailable()) {
|
|
447
|
-
this.
|
|
455
|
+
this.sendNewContent(msg.id, peer);
|
|
448
456
|
return;
|
|
449
457
|
}
|
|
450
458
|
|
|
@@ -478,11 +486,7 @@ export class SyncManager {
|
|
|
478
486
|
}
|
|
479
487
|
|
|
480
488
|
if (coValue.isAvailable()) {
|
|
481
|
-
|
|
482
|
-
this.sendNewContentWithoutDependencies(msg.id, peer);
|
|
483
|
-
} else {
|
|
484
|
-
this.sendNewContentIncludingDependencies(msg.id, peer);
|
|
485
|
-
}
|
|
489
|
+
this.sendNewContent(msg.id, peer);
|
|
486
490
|
}
|
|
487
491
|
}
|
|
488
492
|
|
|
@@ -777,7 +781,7 @@ export class SyncManager {
|
|
|
777
781
|
|
|
778
782
|
// We directly forward the new content to peers that have an active subscription
|
|
779
783
|
if (peer.optimisticKnownStates.has(coValue.id)) {
|
|
780
|
-
this.
|
|
784
|
+
this.sendNewContent(coValue.id, peer);
|
|
781
785
|
syncedPeers.push(peer);
|
|
782
786
|
} else if (
|
|
783
787
|
peer.role === "server" &&
|
|
@@ -808,7 +812,7 @@ export class SyncManager {
|
|
|
808
812
|
handleCorrection(msg: KnownStateMessage, peer: PeerState) {
|
|
809
813
|
peer.setKnownState(msg.id, knownStateIn(msg));
|
|
810
814
|
|
|
811
|
-
return this.
|
|
815
|
+
return this.sendNewContent(msg.id, peer);
|
|
812
816
|
}
|
|
813
817
|
|
|
814
818
|
private syncQueue = new LocalTransactionsSyncQueue((content) =>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, test } from "vitest";
|
|
1
|
+
import { assert, beforeEach, describe, expect, test } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
createTestNode,
|
|
4
4
|
setupTestNode,
|
|
5
5
|
loadCoValueOrFail,
|
|
6
6
|
} from "./testUtils.js";
|
|
7
7
|
import { expectList, expectMap, expectPlainText } from "../coValue.js";
|
|
8
|
+
import { RawCoMap } from "../exports.js";
|
|
8
9
|
|
|
9
10
|
let jazzCloud: ReturnType<typeof setupTestNode>;
|
|
10
11
|
|
|
@@ -94,7 +95,7 @@ describe("Branching Logic", () => {
|
|
|
94
95
|
const result = expectMap(branch.core.mergeBranch().getCurrentContent());
|
|
95
96
|
|
|
96
97
|
// Verify only one merge commit was created
|
|
97
|
-
expect(
|
|
98
|
+
expect(branch.core.mergeCommits.length).toBe(1);
|
|
98
99
|
|
|
99
100
|
// Verify source contains branch transactions
|
|
100
101
|
expect(result.get("key1")).toBe("branchValue1");
|
|
@@ -154,7 +155,7 @@ describe("Branching Logic", () => {
|
|
|
154
155
|
branch.core.mergeBranch();
|
|
155
156
|
|
|
156
157
|
// Verify two merge commits exist
|
|
157
|
-
expect(
|
|
158
|
+
expect(branch.core.mergeCommits.length).toBe(2);
|
|
158
159
|
|
|
159
160
|
// Verify both changes are now in original map
|
|
160
161
|
expect(originalMap.get("key1")).toBe("branchValue1");
|
|
@@ -210,6 +211,8 @@ describe("Branching Logic", () => {
|
|
|
210
211
|
.getCurrentContent(),
|
|
211
212
|
);
|
|
212
213
|
|
|
214
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
215
|
+
|
|
213
216
|
// Add different items to second branch
|
|
214
217
|
branch2.appendItems(["apples", "oranges", "carrots"]);
|
|
215
218
|
|
|
@@ -233,18 +236,20 @@ describe("Branching Logic", () => {
|
|
|
233
236
|
|
|
234
237
|
expect(list.toJSON()).toEqual([
|
|
235
238
|
"bread",
|
|
236
|
-
"cheese",
|
|
237
239
|
"apples",
|
|
238
240
|
"oranges",
|
|
239
241
|
"carrots",
|
|
240
242
|
"tomatoes",
|
|
241
243
|
"cucumber",
|
|
244
|
+
"cheese",
|
|
242
245
|
]);
|
|
243
246
|
});
|
|
244
247
|
|
|
245
|
-
test("should work with co.plainText when
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
+
test("should work with co.plainText when merging the same branch twice on different sessions", async () => {
|
|
249
|
+
const client = setupTestNode({
|
|
250
|
+
connected: true,
|
|
251
|
+
});
|
|
252
|
+
const group = client.node.createGroup();
|
|
248
253
|
const plainText = group.createPlainText();
|
|
249
254
|
|
|
250
255
|
plainText.insertAfter(0, "hello");
|
|
@@ -255,12 +260,28 @@ describe("Branching Logic", () => {
|
|
|
255
260
|
.getCurrentContent(),
|
|
256
261
|
);
|
|
257
262
|
|
|
263
|
+
branch.insertAfter("hello".length, " world");
|
|
264
|
+
|
|
265
|
+
const anotherSession = client.spawnNewSession();
|
|
266
|
+
|
|
267
|
+
const loadedBranch = await loadCoValueOrFail(
|
|
268
|
+
anotherSession.node,
|
|
269
|
+
branch.id,
|
|
270
|
+
);
|
|
271
|
+
assert(loadedBranch);
|
|
272
|
+
|
|
273
|
+
anotherSession.connectToSyncServer().peerState.gracefulShutdown();
|
|
274
|
+
|
|
258
275
|
// Add more items to the branch
|
|
259
|
-
|
|
276
|
+
loadedBranch.insertAfter("hello world".length, " people");
|
|
260
277
|
|
|
261
278
|
branch.core.mergeBranch();
|
|
279
|
+
const loadedBranchMergeResult = loadedBranch.core.mergeBranch();
|
|
262
280
|
|
|
263
|
-
|
|
281
|
+
anotherSession.connectToSyncServer();
|
|
282
|
+
await loadedBranchMergeResult.waitForSync();
|
|
283
|
+
|
|
284
|
+
expect(plainText.toString()).toEqual("hello world people");
|
|
264
285
|
});
|
|
265
286
|
});
|
|
266
287
|
|
|
@@ -527,4 +548,132 @@ describe("Branching Logic", () => {
|
|
|
527
548
|
expect(aliceBranch.get("bob")).toBe(true);
|
|
528
549
|
});
|
|
529
550
|
});
|
|
551
|
+
|
|
552
|
+
test("should alias the txID when a transaction comes from a merge", async () => {
|
|
553
|
+
const client = setupTestNode({
|
|
554
|
+
connected: true,
|
|
555
|
+
});
|
|
556
|
+
const group = client.node.createGroup();
|
|
557
|
+
const map = group.createMap();
|
|
558
|
+
|
|
559
|
+
map.set("key", "value");
|
|
560
|
+
|
|
561
|
+
const branch = map.core
|
|
562
|
+
.createBranch("feature-branch", group.id)
|
|
563
|
+
.getCurrentContent() as RawCoMap;
|
|
564
|
+
branch.set("branchKey", "branchValue");
|
|
565
|
+
|
|
566
|
+
const originalTxID = branch.core
|
|
567
|
+
.getValidTransactions({
|
|
568
|
+
skipBranchSource: true,
|
|
569
|
+
ignorePrivateTransactions: false,
|
|
570
|
+
})
|
|
571
|
+
.at(-1)?.txID;
|
|
572
|
+
|
|
573
|
+
branch.core.mergeBranch();
|
|
574
|
+
|
|
575
|
+
map.set("key2", "value2");
|
|
576
|
+
|
|
577
|
+
const validSortedTransactions = map.core.getValidSortedTransactions();
|
|
578
|
+
|
|
579
|
+
// Only the merged transaction should have the txId changed
|
|
580
|
+
const mergedTransactionIdx = validSortedTransactions.findIndex(
|
|
581
|
+
(tx) => tx.txID.branch,
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
expect(validSortedTransactions[mergedTransactionIdx - 1]?.txID.branch).toBe(
|
|
585
|
+
undefined,
|
|
586
|
+
);
|
|
587
|
+
expect(validSortedTransactions[mergedTransactionIdx]?.txID).toEqual(
|
|
588
|
+
originalTxID,
|
|
589
|
+
);
|
|
590
|
+
expect(validSortedTransactions[mergedTransactionIdx + 1]?.txID.branch).toBe(
|
|
591
|
+
undefined,
|
|
592
|
+
);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
describe("hasBranch", () => {
|
|
596
|
+
test("should work when the branch owner is the source owner", () => {
|
|
597
|
+
const client = setupTestNode({
|
|
598
|
+
connected: true,
|
|
599
|
+
});
|
|
600
|
+
const group = client.node.createGroup();
|
|
601
|
+
const map = group.createMap();
|
|
602
|
+
|
|
603
|
+
map.set("key", "value");
|
|
604
|
+
|
|
605
|
+
const branch = map.core.createBranch("feature-branch", group.id);
|
|
606
|
+
|
|
607
|
+
expect(map.core.hasBranch("feature-branch")).toBe(true);
|
|
608
|
+
expect(map.core.hasBranch("feature-branch", group.id)).toBe(true);
|
|
609
|
+
expect(branch.hasBranch("feature-branch")).toBe(false);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test("should work when the branch onwer is implicit", () => {
|
|
613
|
+
const client = setupTestNode({
|
|
614
|
+
connected: true,
|
|
615
|
+
});
|
|
616
|
+
const group = client.node.createGroup();
|
|
617
|
+
const map = group.createMap();
|
|
618
|
+
|
|
619
|
+
map.set("key", "value");
|
|
620
|
+
|
|
621
|
+
const branch = map.core.createBranch("feature-branch");
|
|
622
|
+
|
|
623
|
+
expect(map.core.hasBranch("feature-branch")).toBe(true);
|
|
624
|
+
expect(map.core.hasBranch("feature-branch", group.id)).toBe(true);
|
|
625
|
+
expect(branch.hasBranch("feature-branch")).toBe(false);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test("should return false for non-existent branch name", () => {
|
|
629
|
+
const client = setupTestNode({
|
|
630
|
+
connected: true,
|
|
631
|
+
});
|
|
632
|
+
const group = client.node.createGroup();
|
|
633
|
+
const map = group.createMap();
|
|
634
|
+
|
|
635
|
+
map.set("key", "value");
|
|
636
|
+
|
|
637
|
+
expect(map.core.hasBranch("non-existent-branch")).toBe(false);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test("should work with explicit ownerId parameter", () => {
|
|
641
|
+
const client = setupTestNode({
|
|
642
|
+
connected: true,
|
|
643
|
+
});
|
|
644
|
+
const group = client.node.createGroup();
|
|
645
|
+
const map = group.createMap();
|
|
646
|
+
|
|
647
|
+
map.set("key", "value");
|
|
648
|
+
|
|
649
|
+
const differentGroup = client.node.createGroup();
|
|
650
|
+
|
|
651
|
+
map.core.createBranch("feature-branch", differentGroup.id);
|
|
652
|
+
|
|
653
|
+
// Test with explicit ownerId
|
|
654
|
+
expect(map.core.hasBranch("feature-branch", differentGroup.id)).toBe(
|
|
655
|
+
true,
|
|
656
|
+
);
|
|
657
|
+
expect(map.core.hasBranch("feature-branch")).toBe(false);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test("should work when the transactions have not been parsed yet", async () => {
|
|
661
|
+
const client = setupTestNode({
|
|
662
|
+
connected: true,
|
|
663
|
+
});
|
|
664
|
+
const group = client.node.createGroup();
|
|
665
|
+
const map = group.createMap();
|
|
666
|
+
|
|
667
|
+
map.set("key", "value");
|
|
668
|
+
|
|
669
|
+
map.core.createBranch("feature-branch", group.id);
|
|
670
|
+
|
|
671
|
+
await map.core.waitForSync();
|
|
672
|
+
|
|
673
|
+
const newSession = client.spawnNewSession();
|
|
674
|
+
const loadedMapCore = await newSession.node.loadCoValueCore(map.core.id);
|
|
675
|
+
|
|
676
|
+
expect(loadedMapCore.hasBranch("feature-branch", group.id)).toBe(true);
|
|
677
|
+
});
|
|
678
|
+
});
|
|
530
679
|
});
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
vi,
|
|
9
9
|
} from "vitest";
|
|
10
10
|
import { CoValueCore } from "../coValueCore/coValueCore.js";
|
|
11
|
-
import { Transaction } from "../coValueCore/verifiedState.js";
|
|
12
11
|
import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
|
13
12
|
import { stableStringify } from "../jsonStringify.js";
|
|
14
13
|
import { LocalNode } from "../localNode.js";
|
|
@@ -24,7 +23,7 @@ import {
|
|
|
24
23
|
tearDownTestMetricReader,
|
|
25
24
|
} from "./testUtils.js";
|
|
26
25
|
import { CO_VALUE_PRIORITY } from "../priority.js";
|
|
27
|
-
import {
|
|
26
|
+
import { setMaxTxSizeBytes } from "../config.js";
|
|
28
27
|
|
|
29
28
|
const Crypto = await WasmCrypto.create();
|
|
30
29
|
|
|
@@ -57,6 +56,7 @@ test("transactions with wrong signature are rejected", () => {
|
|
|
57
56
|
node.getCurrentAgent(),
|
|
58
57
|
[{ hello: "world" }],
|
|
59
58
|
undefined,
|
|
59
|
+
Date.now(),
|
|
60
60
|
);
|
|
61
61
|
|
|
62
62
|
transaction.madeAt = Date.now() + 1000;
|
|
@@ -87,6 +87,66 @@ test("transactions with wrong signature are rejected", () => {
|
|
|
87
87
|
expect(newEntry.getValidSortedTransactions().length).toBe(0);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
describe("transactions that exceed the byte size limit are rejected", () => {
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
setMaxTxSizeBytes(1 * 1024);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
setMaxTxSizeBytes(1 * 1024 * 1024);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("makeTransaction should throw error when transaction exceeds byte size limit", () => {
|
|
100
|
+
const [agent, sessionID] = randomAgentAndSessionID();
|
|
101
|
+
const node = new LocalNode(agent.agentSecret, sessionID, Crypto);
|
|
102
|
+
|
|
103
|
+
const coValue = node.createCoValue({
|
|
104
|
+
type: "costream",
|
|
105
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
106
|
+
meta: null,
|
|
107
|
+
...Crypto.createdNowUnique(),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const largeBinaryData = "x".repeat(1024 + 100);
|
|
111
|
+
|
|
112
|
+
expect(() => {
|
|
113
|
+
coValue.makeTransaction(
|
|
114
|
+
[
|
|
115
|
+
{
|
|
116
|
+
data: largeBinaryData,
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
"trusting",
|
|
120
|
+
);
|
|
121
|
+
}).toThrow(/Transaction is too large to be synced/);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("makeTransaction should work for transactions under byte size limit", () => {
|
|
125
|
+
const [agent, sessionID] = randomAgentAndSessionID();
|
|
126
|
+
const node = new LocalNode(agent.agentSecret, sessionID, Crypto);
|
|
127
|
+
|
|
128
|
+
const coValue = node.createCoValue({
|
|
129
|
+
type: "costream",
|
|
130
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
131
|
+
meta: null,
|
|
132
|
+
...Crypto.createdNowUnique(),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const smallData = "Hello, world!";
|
|
136
|
+
|
|
137
|
+
const success = coValue.makeTransaction(
|
|
138
|
+
[
|
|
139
|
+
{
|
|
140
|
+
data: smallData,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
"trusting",
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(success).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
90
150
|
test("New transactions in a group correctly update owned values, including subscriptions", async () => {
|
|
91
151
|
const [agent, sessionID] = randomAgentAndSessionID();
|
|
92
152
|
const node = new LocalNode(agent.agentSecret, sessionID, Crypto);
|