cojson 0.20.7 → 0.20.9
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 +26 -0
- package/dist/SyncStateManager.d.ts.map +1 -1
- package/dist/SyncStateManager.js +0 -2
- package/dist/SyncStateManager.js.map +1 -1
- package/dist/base64url.d.ts +15 -0
- package/dist/base64url.d.ts.map +1 -1
- package/dist/base64url.js +101 -5
- package/dist/base64url.js.map +1 -1
- package/dist/base64url.test.js +76 -1
- package/dist/base64url.test.js.map +1 -1
- package/dist/coValue.d.ts +2 -1
- package/dist/coValue.d.ts.map +1 -1
- package/dist/coValue.js.map +1 -1
- package/dist/coValueCore/coValueCore.d.ts +9 -11
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +92 -65
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/coValueCore/verifiedState.d.ts +38 -7
- package/dist/coValueCore/verifiedState.d.ts.map +1 -1
- package/dist/coValueCore/verifiedState.js +226 -30
- package/dist/coValueCore/verifiedState.js.map +1 -1
- package/dist/coValues/binaryCoStream.d.ts +63 -0
- package/dist/coValues/binaryCoStream.d.ts.map +1 -0
- package/dist/coValues/binaryCoStream.js +125 -0
- package/dist/coValues/binaryCoStream.js.map +1 -0
- package/dist/coValues/coList.d.ts +3 -1
- package/dist/coValues/coList.d.ts.map +1 -1
- package/dist/coValues/coList.js +15 -6
- package/dist/coValues/coList.js.map +1 -1
- package/dist/coValues/coMap.d.ts +1 -1
- package/dist/coValues/coMap.d.ts.map +1 -1
- package/dist/coValues/coMap.js +2 -2
- package/dist/coValues/coMap.js.map +1 -1
- package/dist/coValues/coStream.d.ts +0 -38
- package/dist/coValues/coStream.d.ts.map +1 -1
- package/dist/coValues/coStream.js +0 -86
- package/dist/coValues/coStream.js.map +1 -1
- package/dist/coValues/group.d.ts +44 -6
- package/dist/coValues/group.d.ts.map +1 -1
- package/dist/coValues/group.js +198 -17
- package/dist/coValues/group.js.map +1 -1
- package/dist/coreToCoValue.d.ts +2 -1
- package/dist/coreToCoValue.d.ts.map +1 -1
- package/dist/coreToCoValue.js +2 -1
- package/dist/coreToCoValue.js.map +1 -1
- package/dist/crypto/NapiCrypto.d.ts +18 -24
- package/dist/crypto/NapiCrypto.d.ts.map +1 -1
- package/dist/crypto/NapiCrypto.js +98 -60
- package/dist/crypto/NapiCrypto.js.map +1 -1
- package/dist/crypto/RNCrypto.d.ts +16 -3
- package/dist/crypto/RNCrypto.d.ts.map +1 -1
- package/dist/crypto/RNCrypto.js +117 -54
- package/dist/crypto/RNCrypto.js.map +1 -1
- package/dist/crypto/WasmCrypto.d.ts +18 -24
- package/dist/crypto/WasmCrypto.d.ts.map +1 -1
- package/dist/crypto/WasmCrypto.js +100 -61
- package/dist/crypto/WasmCrypto.js.map +1 -1
- package/dist/crypto/crypto.d.ts +55 -19
- package/dist/crypto/crypto.d.ts.map +1 -1
- package/dist/crypto/crypto.js +14 -3
- package/dist/crypto/crypto.js.map +1 -1
- package/dist/exports.d.ts +7 -3
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +4 -2
- package/dist/exports.js.map +1 -1
- package/dist/localNode.d.ts +3 -1
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +10 -3
- package/dist/localNode.js.map +1 -1
- package/dist/media.d.ts +1 -1
- package/dist/media.d.ts.map +1 -1
- package/dist/permissions.d.ts +2 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +19 -3
- package/dist/permissions.js.map +1 -1
- package/dist/storage/sqliteAsync/client.d.ts +24 -12
- package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
- package/dist/storage/sqliteAsync/client.js +70 -58
- package/dist/storage/sqliteAsync/client.js.map +1 -1
- package/dist/storage/sqliteAsync/types.d.ts +1 -1
- package/dist/storage/sqliteAsync/types.d.ts.map +1 -1
- package/dist/storage/types.d.ts +1 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +7 -1
- package/dist/sync.js.map +1 -1
- package/dist/tests/CojsonMessageChannel.test.js +2 -2
- package/dist/tests/SQLiteClientAsync.test.d.ts +2 -0
- package/dist/tests/SQLiteClientAsync.test.d.ts.map +1 -0
- package/dist/tests/SQLiteClientAsync.test.js +64 -0
- package/dist/tests/SQLiteClientAsync.test.js.map +1 -0
- package/dist/tests/StorageApiAsync.test.js +2 -8
- package/dist/tests/StorageApiAsync.test.js.map +1 -1
- package/dist/tests/SyncStateManager.test.js +2 -2
- package/dist/tests/WasmCrypto.test.js +1 -15
- package/dist/tests/WasmCrypto.test.js.map +1 -1
- package/dist/tests/coList.test.js +24 -5
- package/dist/tests/coList.test.js.map +1 -1
- package/dist/tests/coStream.test.js +4 -3
- package/dist/tests/coStream.test.js.map +1 -1
- package/dist/tests/coValueCore.initTransaction.test.d.ts +2 -0
- package/dist/tests/coValueCore.initTransaction.test.d.ts.map +1 -0
- package/dist/tests/coValueCore.initTransaction.test.js +438 -0
- package/dist/tests/coValueCore.initTransaction.test.js.map +1 -0
- package/dist/tests/coValueCore.test.js +11 -19
- package/dist/tests/coValueCore.test.js.map +1 -1
- package/dist/tests/crypto.test.js +83 -0
- package/dist/tests/crypto.test.js.map +1 -1
- package/dist/tests/deleteCoValue.test.js +5 -5
- package/dist/tests/deleteCoValue.test.js.map +1 -1
- package/dist/tests/group.inheritance.test.js +11 -0
- package/dist/tests/group.inheritance.test.js.map +1 -1
- package/dist/tests/group.test.js +24 -1
- package/dist/tests/group.test.js.map +1 -1
- package/dist/tests/groupSealer.test.d.ts +2 -0
- package/dist/tests/groupSealer.test.d.ts.map +1 -0
- package/dist/tests/groupSealer.test.js +913 -0
- package/dist/tests/groupSealer.test.js.map +1 -0
- package/dist/tests/setup.js +5 -0
- package/dist/tests/setup.js.map +1 -1
- package/dist/tests/sync.auth.test.js +10 -10
- package/dist/tests/sync.concurrentLoad.test.js +12 -12
- package/dist/tests/sync.deleted.test.js +8 -8
- package/dist/tests/sync.garbageCollection.test.js +10 -10
- package/dist/tests/sync.invite.test.js +12 -12
- package/dist/tests/sync.known.test.js +2 -2
- package/dist/tests/sync.load.test.js +107 -107
- package/dist/tests/sync.mesh.test.js +164 -46
- package/dist/tests/sync.mesh.test.js.map +1 -1
- package/dist/tests/sync.multipleServers.test.js +43 -43
- package/dist/tests/sync.peerReconciliation.test.js +29 -29
- package/dist/tests/sync.sharding.test.js +3 -3
- package/dist/tests/sync.storage.test.js +104 -104
- package/dist/tests/sync.storage.test.js.map +1 -1
- package/dist/tests/sync.storageAsync.test.js +56 -56
- package/dist/tests/sync.upload.test.js +22 -22
- package/dist/tests/testStorage.d.ts +2 -0
- package/dist/tests/testStorage.d.ts.map +1 -1
- package/dist/tests/testStorage.js +30 -6
- package/dist/tests/testStorage.js.map +1 -1
- package/dist/typeUtils/isCoValue.js +1 -1
- package/dist/typeUtils/isCoValue.js.map +1 -1
- package/package.json +4 -4
- package/src/SyncStateManager.ts +0 -2
- package/src/base64url.test.ts +89 -1
- package/src/base64url.ts +134 -6
- package/src/coValue.ts +2 -1
- package/src/coValueCore/coValueCore.ts +126 -84
- package/src/coValueCore/verifiedState.ts +335 -53
- package/src/coValues/binaryCoStream.ts +217 -0
- package/src/coValues/coList.ts +21 -8
- package/src/coValues/coMap.ts +3 -0
- package/src/coValues/coStream.ts +0 -170
- package/src/coValues/group.ts +270 -21
- package/src/coreToCoValue.ts +2 -1
- package/src/crypto/NapiCrypto.ts +198 -95
- package/src/crypto/RNCrypto.ts +229 -102
- package/src/crypto/WasmCrypto.ts +201 -95
- package/src/crypto/crypto.ts +118 -45
- package/src/exports.ts +11 -5
- package/src/localNode.ts +17 -1
- package/src/media.ts +1 -1
- package/src/permissions.ts +30 -7
- package/src/storage/sqliteAsync/client.ts +136 -115
- package/src/storage/sqliteAsync/types.ts +3 -1
- package/src/storage/types.ts +4 -0
- package/src/sync.ts +10 -1
- package/src/tests/CojsonMessageChannel.test.ts +2 -2
- package/src/tests/SQLiteClientAsync.test.ts +75 -0
- package/src/tests/StorageApiAsync.test.ts +4 -9
- package/src/tests/SyncStateManager.test.ts +2 -2
- package/src/tests/WasmCrypto.test.ts +1 -25
- package/src/tests/coList.test.ts +39 -5
- package/src/tests/coStream.test.ts +4 -5
- package/src/tests/coValueCore.initTransaction.test.ts +836 -0
- package/src/tests/coValueCore.test.ts +11 -22
- package/src/tests/crypto.test.ts +107 -0
- package/src/tests/deleteCoValue.test.ts +5 -5
- package/src/tests/group.inheritance.test.ts +16 -0
- package/src/tests/group.test.ts +29 -1
- package/src/tests/groupSealer.test.ts +1473 -0
- package/src/tests/setup.ts +6 -0
- package/src/tests/sync.auth.test.ts +10 -10
- package/src/tests/sync.concurrentLoad.test.ts +12 -12
- package/src/tests/sync.deleted.test.ts +8 -8
- package/src/tests/sync.garbageCollection.test.ts +10 -10
- package/src/tests/sync.invite.test.ts +12 -12
- package/src/tests/sync.known.test.ts +2 -2
- package/src/tests/sync.load.test.ts +107 -107
- package/src/tests/sync.mesh.test.ts +189 -46
- package/src/tests/sync.multipleServers.test.ts +43 -43
- package/src/tests/sync.peerReconciliation.test.ts +29 -29
- package/src/tests/sync.sharding.test.ts +3 -3
- package/src/tests/sync.storage.test.ts +104 -104
- package/src/tests/sync.storageAsync.test.ts +56 -56
- package/src/tests/sync.upload.test.ts +22 -22
- package/src/tests/testStorage.ts +39 -9
- package/src/typeUtils/isCoValue.ts +1 -1
- package/dist/coValueCore/SessionMap.d.ts +0 -55
- package/dist/coValueCore/SessionMap.d.ts.map +0 -1
- package/dist/coValueCore/SessionMap.js +0 -206
- package/dist/coValueCore/SessionMap.js.map +0 -1
- package/dist/tests/coreWasm.test.d.ts +0 -2
- package/dist/tests/coreWasm.test.d.ts.map +0 -1
- package/dist/tests/coreWasm.test.js +0 -203
- package/dist/tests/coreWasm.test.js.map +0 -1
- package/src/coValueCore/SessionMap.ts +0 -394
- package/src/tests/coreWasm.test.ts +0 -452
package/src/permissions.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
isInheritableRole,
|
|
11
11
|
isSelfExtension,
|
|
12
12
|
} from "./coValues/group.js";
|
|
13
|
-
import { KeyID } from "./crypto/crypto.js";
|
|
13
|
+
import { KeyID, SealerID } from "./crypto/crypto.js";
|
|
14
14
|
import {
|
|
15
15
|
AgentID,
|
|
16
16
|
ParentGroupReference,
|
|
@@ -273,6 +273,7 @@ function determineValidTransactionsForGroup(
|
|
|
273
273
|
const change = changes[0] as
|
|
274
274
|
| MapOpPayload<RawAccountID | AgentID | Everyone, Role>
|
|
275
275
|
| MapOpPayload<"readKey", JsonValue>
|
|
276
|
+
| MapOpPayload<"groupSealer", SealerID>
|
|
276
277
|
| MapOpPayload<"profile", CoID<RawProfile>>
|
|
277
278
|
| MapOpPayload<"root", CoID<RawCoMap>>
|
|
278
279
|
| MapOpPayload<`parent_${CoID<RawGroup>}`, CoID<RawGroup>>
|
|
@@ -294,6 +295,14 @@ function determineValidTransactionsForGroup(
|
|
|
294
295
|
continue;
|
|
295
296
|
}
|
|
296
297
|
|
|
298
|
+
transaction.markValid();
|
|
299
|
+
continue;
|
|
300
|
+
} else if (change.key === "groupSealer") {
|
|
301
|
+
if (!canAdmin(transactorRole)) {
|
|
302
|
+
transaction.markInvalid("Only admins can set groupSealer");
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
297
306
|
transaction.markValid();
|
|
298
307
|
continue;
|
|
299
308
|
} else if (change.key === "profile") {
|
|
@@ -314,7 +323,8 @@ function determineValidTransactionsForGroup(
|
|
|
314
323
|
continue;
|
|
315
324
|
} else if (
|
|
316
325
|
isKeyForKeyField(change.key) ||
|
|
317
|
-
isKeyForAccountField(change.key)
|
|
326
|
+
isKeyForAccountField(change.key) ||
|
|
327
|
+
isKeySealedForGroupField(change.key)
|
|
318
328
|
) {
|
|
319
329
|
if (
|
|
320
330
|
transactorRole !== "admin" &&
|
|
@@ -596,6 +606,12 @@ export function isKeyForAccountField(
|
|
|
596
606
|
);
|
|
597
607
|
}
|
|
598
608
|
|
|
609
|
+
export function isKeySealedForGroupField(
|
|
610
|
+
co: string,
|
|
611
|
+
): co is `${KeyID}_sealedFor_${SealerID}` {
|
|
612
|
+
return co.startsWith("key_") && co.includes("_sealedFor_sealer");
|
|
613
|
+
}
|
|
614
|
+
|
|
599
615
|
function isParentExtension(key: string): key is `parent_${CoID<RawGroup>}` {
|
|
600
616
|
return key.startsWith("parent_");
|
|
601
617
|
}
|
|
@@ -605,15 +621,22 @@ function isChildExtension(key: string): key is `child_${CoID<RawGroup>}` {
|
|
|
605
621
|
}
|
|
606
622
|
|
|
607
623
|
function isOwnWriteKeyRevelation(
|
|
608
|
-
key: `${KeyID}_for_${string}`,
|
|
624
|
+
key: `${KeyID}_for_${string}` | `${KeyID}_sealedFor_${SealerID}`,
|
|
609
625
|
memberKey: RawAccountID | AgentID,
|
|
610
626
|
writeOnlyKeys: Record<RawAccountID | AgentID, KeyID>,
|
|
611
|
-
): key is
|
|
612
|
-
|
|
613
|
-
|
|
627
|
+
): key is
|
|
628
|
+
| `${KeyID}_for_${RawAccountID | AgentID}`
|
|
629
|
+
| `${KeyID}_sealedFor_${SealerID}` {
|
|
630
|
+
let i = key.indexOf("_for_");
|
|
631
|
+
if (i === -1) {
|
|
632
|
+
i = key.indexOf("_sealedFor_");
|
|
614
633
|
}
|
|
615
634
|
|
|
616
|
-
const keyID = key.slice(0,
|
|
635
|
+
const keyID = key.slice(0, i);
|
|
636
|
+
|
|
637
|
+
if (!keyID) {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
617
640
|
|
|
618
641
|
return writeOnlyKeys[memberKey] === keyID;
|
|
619
642
|
}
|
|
@@ -38,15 +38,138 @@ export function getErrorMessage(error: unknown) {
|
|
|
38
38
|
return error instanceof Error ? error.message : "Unknown error";
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Executes storage operations inside a single DB transaction.
|
|
43
|
+
*/
|
|
44
|
+
export class SQLiteTransactionAsync implements DBTransactionInterfaceAsync {
|
|
45
|
+
constructor(private readonly tx: SQLiteDatabaseDriverAsync) {}
|
|
46
|
+
|
|
47
|
+
async getSingleCoValueSession(
|
|
48
|
+
coValueRowId: number,
|
|
49
|
+
sessionID: SessionID,
|
|
50
|
+
): Promise<StoredSessionRow | undefined> {
|
|
51
|
+
return this.tx.get<StoredSessionRow>(
|
|
52
|
+
"SELECT * FROM sessions WHERE coValue = ? AND sessionID = ?",
|
|
53
|
+
[coValueRowId, sessionID],
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async markCoValueAsDeleted(id: RawCoID): Promise<void> {
|
|
58
|
+
await this.tx.run(
|
|
59
|
+
`INSERT INTO deletedCoValues (coValueID) VALUES (?) ON CONFLICT(coValueID) DO NOTHING`,
|
|
60
|
+
[id],
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async addSessionUpdate({
|
|
65
|
+
sessionUpdate,
|
|
66
|
+
}: {
|
|
67
|
+
sessionUpdate: SessionRow;
|
|
68
|
+
sessionRow?: StoredSessionRow;
|
|
69
|
+
}): Promise<number> {
|
|
70
|
+
const result = await this.tx.get<{ rowID: number }>(
|
|
71
|
+
`INSERT INTO sessions (coValue, sessionID, lastIdx, lastSignature, bytesSinceLastSignature) VALUES (?, ?, ?, ?, ?)
|
|
72
|
+
ON CONFLICT(coValue, sessionID) DO UPDATE SET lastIdx=excluded.lastIdx, lastSignature=excluded.lastSignature, bytesSinceLastSignature=excluded.bytesSinceLastSignature
|
|
73
|
+
RETURNING rowID`,
|
|
74
|
+
[
|
|
75
|
+
sessionUpdate.coValue,
|
|
76
|
+
sessionUpdate.sessionID,
|
|
77
|
+
sessionUpdate.lastIdx,
|
|
78
|
+
sessionUpdate.lastSignature,
|
|
79
|
+
sessionUpdate.bytesSinceLastSignature,
|
|
80
|
+
],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (!result) {
|
|
84
|
+
throw new Error("Failed to add session update");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result.rowID;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async addTransaction(
|
|
91
|
+
sessionRowID: number,
|
|
92
|
+
nextIdx: number,
|
|
93
|
+
newTransaction: Transaction,
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
await this.tx.run(
|
|
96
|
+
"INSERT INTO transactions (ses, idx, tx) VALUES (?, ?, ?)",
|
|
97
|
+
[sessionRowID, nextIdx, JSON.stringify(newTransaction)],
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async addSignatureAfter({
|
|
102
|
+
sessionRowID,
|
|
103
|
+
idx,
|
|
104
|
+
signature,
|
|
105
|
+
}: {
|
|
106
|
+
sessionRowID: number;
|
|
107
|
+
idx: number;
|
|
108
|
+
signature: Signature;
|
|
109
|
+
}): Promise<void> {
|
|
110
|
+
await this.tx.run(
|
|
111
|
+
"INSERT INTO signatureAfter (ses, idx, signature) VALUES (?, ?, ?)",
|
|
112
|
+
[sessionRowID, idx, signature],
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async deleteCoValueContent(
|
|
117
|
+
coValueRow: Pick<StoredCoValueRow, "rowID" | "id">,
|
|
118
|
+
): Promise<void> {
|
|
119
|
+
await this.tx.run(
|
|
120
|
+
`DELETE FROM transactions
|
|
121
|
+
WHERE ses IN (
|
|
122
|
+
SELECT rowID FROM sessions
|
|
123
|
+
WHERE coValue = ?
|
|
124
|
+
AND sessionID NOT LIKE '%$'
|
|
125
|
+
)`,
|
|
126
|
+
[coValueRow.rowID],
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
await this.tx.run(
|
|
130
|
+
`DELETE FROM signatureAfter
|
|
131
|
+
WHERE ses IN (
|
|
132
|
+
SELECT rowID FROM sessions
|
|
133
|
+
WHERE coValue = ?
|
|
134
|
+
AND sessionID NOT LIKE '%$'
|
|
135
|
+
)`,
|
|
136
|
+
[coValueRow.rowID],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
await this.tx.run(
|
|
140
|
+
`DELETE FROM sessions
|
|
141
|
+
WHERE coValue = ?
|
|
142
|
+
AND sessionID NOT LIKE '%$'`,
|
|
143
|
+
[coValueRow.rowID],
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
await this.tx.run(
|
|
147
|
+
`INSERT INTO deletedCoValues (coValueID, status) VALUES (?, ?)
|
|
148
|
+
ON CONFLICT(coValueID) DO UPDATE SET status=?`,
|
|
149
|
+
[
|
|
150
|
+
coValueRow.id,
|
|
151
|
+
DeletedCoValueDeletionStatus.Done,
|
|
152
|
+
DeletedCoValueDeletionStatus.Done,
|
|
153
|
+
],
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export class SQLiteClientAsync implements DBClientInterfaceAsync {
|
|
44
159
|
private readonly db: SQLiteDatabaseDriverAsync;
|
|
160
|
+
/** Serialize transactions to avoid SQLITE_BUSY errors */
|
|
161
|
+
private txQueue = Promise.resolve() as Promise<unknown>;
|
|
45
162
|
|
|
46
163
|
constructor(db: SQLiteDatabaseDriverAsync) {
|
|
47
164
|
this.db = db;
|
|
48
165
|
}
|
|
49
166
|
|
|
167
|
+
private enqueueTx<T>(fn: () => Promise<T>): Promise<T> {
|
|
168
|
+
const next = this.txQueue.then(fn, fn);
|
|
169
|
+
this.txQueue = next;
|
|
170
|
+
return next;
|
|
171
|
+
}
|
|
172
|
+
|
|
50
173
|
async getCoValue(coValueId: RawCoID): Promise<StoredCoValueRow | undefined> {
|
|
51
174
|
const coValueRow = await this.db.get<RawCoValueRow & { rowID: number }>(
|
|
52
175
|
"SELECT * FROM coValues WHERE id = ?",
|
|
@@ -80,16 +203,6 @@ export class SQLiteClientAsync
|
|
|
80
203
|
);
|
|
81
204
|
}
|
|
82
205
|
|
|
83
|
-
async getSingleCoValueSession(
|
|
84
|
-
coValueRowId: number,
|
|
85
|
-
sessionID: SessionID,
|
|
86
|
-
): Promise<StoredSessionRow | undefined> {
|
|
87
|
-
return this.db.get<StoredSessionRow>(
|
|
88
|
-
"SELECT * FROM sessions WHERE coValue = ? AND sessionID = ?",
|
|
89
|
-
[coValueRowId, sessionID],
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
206
|
async getNewTransactionInSession(
|
|
94
207
|
sessionRowId: number,
|
|
95
208
|
fromIdx: number,
|
|
@@ -151,15 +264,6 @@ export class SQLiteClientAsync
|
|
|
151
264
|
return result.rowID;
|
|
152
265
|
}
|
|
153
266
|
|
|
154
|
-
async markCoValueAsDeleted(id: RawCoID) {
|
|
155
|
-
// Work queue entry. Table only stores the coValueID.
|
|
156
|
-
// Idempotent by design.
|
|
157
|
-
await this.db.run(
|
|
158
|
-
`INSERT INTO deletedCoValues (coValueID) VALUES (?) ON CONFLICT(coValueID) DO NOTHING`,
|
|
159
|
-
[id],
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
267
|
async eraseCoValueButKeepTombstone(coValueId: RawCoID) {
|
|
164
268
|
const coValueRow = await this.db.get<RawCoValueRow & { rowID: number }>(
|
|
165
269
|
"SELECT * FROM coValues WHERE id = ?",
|
|
@@ -171,112 +275,29 @@ export class SQLiteClientAsync
|
|
|
171
275
|
return;
|
|
172
276
|
}
|
|
173
277
|
|
|
174
|
-
await this.transaction(async () => {
|
|
175
|
-
await
|
|
176
|
-
`DELETE FROM transactions
|
|
177
|
-
WHERE ses IN (
|
|
178
|
-
SELECT rowID FROM sessions
|
|
179
|
-
WHERE coValue = ?
|
|
180
|
-
AND sessionID NOT LIKE '%$'
|
|
181
|
-
)`,
|
|
182
|
-
[coValueRow.rowID],
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
await this.db.run(
|
|
186
|
-
`DELETE FROM signatureAfter
|
|
187
|
-
WHERE ses IN (
|
|
188
|
-
SELECT rowID FROM sessions
|
|
189
|
-
WHERE coValue = ?
|
|
190
|
-
AND sessionID NOT LIKE '%$'
|
|
191
|
-
)`,
|
|
192
|
-
[coValueRow.rowID],
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
await this.db.run(
|
|
196
|
-
`DELETE FROM sessions
|
|
197
|
-
WHERE coValue = ?
|
|
198
|
-
AND sessionID NOT LIKE '%$'`,
|
|
199
|
-
[coValueRow.rowID],
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
await this.db.run(
|
|
203
|
-
`INSERT INTO deletedCoValues (coValueID, status) VALUES (?, ?)
|
|
204
|
-
ON CONFLICT(coValueID) DO UPDATE SET status=?`,
|
|
205
|
-
[
|
|
206
|
-
coValueId,
|
|
207
|
-
DeletedCoValueDeletionStatus.Done,
|
|
208
|
-
DeletedCoValueDeletionStatus.Done,
|
|
209
|
-
],
|
|
210
|
-
);
|
|
278
|
+
await this.transaction(async (tx) => {
|
|
279
|
+
await tx.deleteCoValueContent(coValueRow);
|
|
211
280
|
});
|
|
212
281
|
}
|
|
213
282
|
|
|
214
283
|
async getAllCoValuesWaitingForDelete(): Promise<RawCoID[]> {
|
|
215
284
|
const rows = await this.db.query<DeletedCoValueQueueRow>(
|
|
216
285
|
`SELECT coValueID as id
|
|
217
|
-
|
|
218
|
-
|
|
286
|
+
FROM deletedCoValues
|
|
287
|
+
WHERE status = ?`,
|
|
219
288
|
[DeletedCoValueDeletionStatus.Pending],
|
|
220
289
|
);
|
|
221
290
|
return rows.map((r) => r.id);
|
|
222
291
|
}
|
|
223
292
|
|
|
224
|
-
async addSessionUpdate({
|
|
225
|
-
sessionUpdate,
|
|
226
|
-
}: {
|
|
227
|
-
sessionUpdate: SessionRow;
|
|
228
|
-
}): Promise<number> {
|
|
229
|
-
const result = await this.db.get<{ rowID: number }>(
|
|
230
|
-
`INSERT INTO sessions (coValue, sessionID, lastIdx, lastSignature, bytesSinceLastSignature) VALUES (?, ?, ?, ?, ?)
|
|
231
|
-
ON CONFLICT(coValue, sessionID) DO UPDATE SET lastIdx=excluded.lastIdx, lastSignature=excluded.lastSignature, bytesSinceLastSignature=excluded.bytesSinceLastSignature
|
|
232
|
-
RETURNING rowID`,
|
|
233
|
-
[
|
|
234
|
-
sessionUpdate.coValue,
|
|
235
|
-
sessionUpdate.sessionID,
|
|
236
|
-
sessionUpdate.lastIdx,
|
|
237
|
-
sessionUpdate.lastSignature,
|
|
238
|
-
sessionUpdate.bytesSinceLastSignature,
|
|
239
|
-
],
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
if (!result) {
|
|
243
|
-
throw new Error("Failed to add session update");
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return result.rowID;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
addTransaction(
|
|
250
|
-
sessionRowID: number,
|
|
251
|
-
nextIdx: number,
|
|
252
|
-
newTransaction: Transaction,
|
|
253
|
-
) {
|
|
254
|
-
this.db.run("INSERT INTO transactions (ses, idx, tx) VALUES (?, ?, ?)", [
|
|
255
|
-
sessionRowID,
|
|
256
|
-
nextIdx,
|
|
257
|
-
JSON.stringify(newTransaction),
|
|
258
|
-
]);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async addSignatureAfter({
|
|
262
|
-
sessionRowID,
|
|
263
|
-
idx,
|
|
264
|
-
signature,
|
|
265
|
-
}: {
|
|
266
|
-
sessionRowID: number;
|
|
267
|
-
idx: number;
|
|
268
|
-
signature: Signature;
|
|
269
|
-
}) {
|
|
270
|
-
this.db.run(
|
|
271
|
-
"INSERT INTO signatureAfter (ses, idx, signature) VALUES (?, ?, ?)",
|
|
272
|
-
[sessionRowID, idx, signature],
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
293
|
async transaction(
|
|
277
294
|
operationsCallback: (tx: DBTransactionInterfaceAsync) => Promise<unknown>,
|
|
278
|
-
) {
|
|
279
|
-
return this.
|
|
295
|
+
): Promise<unknown> {
|
|
296
|
+
return this.enqueueTx(() =>
|
|
297
|
+
this.db.transaction((tx) =>
|
|
298
|
+
operationsCallback(new SQLiteTransactionAsync(tx)),
|
|
299
|
+
),
|
|
300
|
+
);
|
|
280
301
|
}
|
|
281
302
|
|
|
282
303
|
async getUnsyncedCoValueIDs(): Promise<RawCoID[]> {
|
|
@@ -3,7 +3,9 @@ export interface SQLiteDatabaseDriverAsync {
|
|
|
3
3
|
run(sql: string, params: unknown[]): Promise<void>;
|
|
4
4
|
query<T>(sql: string, params: unknown[]): Promise<T[]>;
|
|
5
5
|
get<T>(sql: string, params: unknown[]): Promise<T | undefined>;
|
|
6
|
-
transaction(
|
|
6
|
+
transaction(
|
|
7
|
+
callback: (tx: SQLiteDatabaseDriverAsync) => unknown,
|
|
8
|
+
): Promise<unknown>;
|
|
7
9
|
closeDb(): Promise<unknown>;
|
|
8
10
|
getMigrationVersion?(): Promise<number>;
|
|
9
11
|
saveMigrationVersion?(version: number): Promise<void>;
|
package/src/storage/types.ts
CHANGED
|
@@ -172,6 +172,10 @@ export interface DBTransactionInterfaceAsync {
|
|
|
172
172
|
idx: number;
|
|
173
173
|
signature: Signature;
|
|
174
174
|
}): Promise<unknown>;
|
|
175
|
+
|
|
176
|
+
deleteCoValueContent(
|
|
177
|
+
coValueRow: Pick<StoredCoValueRow, "rowID" | "id">,
|
|
178
|
+
): Promise<unknown>;
|
|
175
179
|
}
|
|
176
180
|
|
|
177
181
|
export interface DBClientInterfaceAsync {
|
package/src/sync.ts
CHANGED
|
@@ -626,6 +626,13 @@ export class SyncManager {
|
|
|
626
626
|
action: "known",
|
|
627
627
|
...storageKnownState,
|
|
628
628
|
});
|
|
629
|
+
|
|
630
|
+
// Subscribe to server peers (e.g., core) to receive future updates.
|
|
631
|
+
// Even though we responded with KNOWN (client has everything), we need
|
|
632
|
+
// to establish a subscription so that updates from core flow to us.
|
|
633
|
+
const serverPeers = this.getServerPeers(msg.id, peer.id);
|
|
634
|
+
coValue.loadFromPeers(serverPeers);
|
|
635
|
+
|
|
629
636
|
return;
|
|
630
637
|
}
|
|
631
638
|
|
|
@@ -888,6 +895,8 @@ export class SyncManager {
|
|
|
888
895
|
|
|
889
896
|
let wasAlreadyDeleted = coValue.isDeleted;
|
|
890
897
|
|
|
898
|
+
const knownState = coValue.knownState();
|
|
899
|
+
|
|
891
900
|
/**
|
|
892
901
|
* The coValue is in memory, load the transactions from the content message
|
|
893
902
|
*/
|
|
@@ -901,7 +910,7 @@ export class SyncManager {
|
|
|
901
910
|
|
|
902
911
|
const newTransactions = getNewTransactionsFromContentMessage(
|
|
903
912
|
newContentForSession,
|
|
904
|
-
|
|
913
|
+
knownState,
|
|
905
914
|
sessionID,
|
|
906
915
|
);
|
|
907
916
|
|
|
@@ -57,9 +57,9 @@ describe("CojsonMessageChannel", () => {
|
|
|
57
57
|
).toMatchInlineSnapshot(`
|
|
58
58
|
[
|
|
59
59
|
"server -> client | LOAD Map sessions: empty",
|
|
60
|
-
"client -> server | CONTENT Group header: true new: After: 0 New:
|
|
60
|
+
"client -> server | CONTENT Group header: true new: After: 0 New: 6",
|
|
61
61
|
"client -> server | CONTENT Map header: true new: After: 0 New: 1",
|
|
62
|
-
"server -> client | KNOWN Group sessions: header/
|
|
62
|
+
"server -> client | KNOWN Group sessions: header/6",
|
|
63
63
|
"server -> client | KNOWN Map sessions: header/1",
|
|
64
64
|
]
|
|
65
65
|
`);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "vitest";
|
|
2
|
+
import { getDbPath } from "./testStorage.js";
|
|
3
|
+
import { setupTestNode } from "./testUtils.js";
|
|
4
|
+
import { DBClientInterfaceAsync } from "../exports.js";
|
|
5
|
+
|
|
6
|
+
describe("SQLiteClientAsync", () => {
|
|
7
|
+
describe("transaction", () => {
|
|
8
|
+
let dbClient: DBClientInterfaceAsync;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
const node = setupTestNode();
|
|
12
|
+
const { storage } = await node.addAsyncStorage({
|
|
13
|
+
ourName: "test",
|
|
14
|
+
storageName: "test-storage",
|
|
15
|
+
filename: getDbPath(),
|
|
16
|
+
});
|
|
17
|
+
// @ts-expect-error - dbClient is private
|
|
18
|
+
dbClient = storage.dbClient;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("serializes concurrent transactions to avoid SQLITE_BUSY errors", async () => {
|
|
22
|
+
const times = Array.from({ length: 10 });
|
|
23
|
+
await Promise.all(
|
|
24
|
+
times.map(async (_, i) => {
|
|
25
|
+
return dbClient.transaction(async (tx) => {
|
|
26
|
+
// Sleep between 0 and 100ms to force interleaving
|
|
27
|
+
await new Promise((r) => setTimeout(r, Math.random() * 100));
|
|
28
|
+
return tx.addSignatureAfter({
|
|
29
|
+
sessionRowID: 0,
|
|
30
|
+
idx: i,
|
|
31
|
+
signature: `signature_z${i}`,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const signatures = await dbClient.getSignatures(0, 0);
|
|
38
|
+
expect(signatures.length).toBe(10);
|
|
39
|
+
signatures.forEach(async ({ signature }, i) => {
|
|
40
|
+
expect(signature).toBe(`signature_z${i}`);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("continues to serialize transactions even if one fails", async () => {
|
|
45
|
+
// First transaction succeeds
|
|
46
|
+
await dbClient.transaction(async (tx) => {
|
|
47
|
+
return tx.addSignatureAfter({
|
|
48
|
+
sessionRowID: 0,
|
|
49
|
+
idx: 0,
|
|
50
|
+
signature: `signature_z0`,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
// Second transaction fails (duplicate primary key)
|
|
54
|
+
await expect(
|
|
55
|
+
dbClient.transaction(async (tx) => {
|
|
56
|
+
return tx.addSignatureAfter({
|
|
57
|
+
sessionRowID: 0,
|
|
58
|
+
idx: 0,
|
|
59
|
+
signature: `signature_z0`,
|
|
60
|
+
});
|
|
61
|
+
}),
|
|
62
|
+
).rejects.toThrow(
|
|
63
|
+
/UNIQUE constraint failed: signatureAfter\.ses, signatureAfter\.idx/,
|
|
64
|
+
);
|
|
65
|
+
// Third transaction succeeds
|
|
66
|
+
await dbClient.transaction(async (tx) => {
|
|
67
|
+
return tx.addSignatureAfter({
|
|
68
|
+
sessionRowID: 0,
|
|
69
|
+
idx: 1,
|
|
70
|
+
signature: `signature_z1`,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -945,15 +945,10 @@ describe("StorageApiAsync", () => {
|
|
|
945
945
|
return true;
|
|
946
946
|
});
|
|
947
947
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
let firstTxStartedResolve!: () => void;
|
|
954
|
-
const firstTxStarted = new Promise<void>((resolve) => {
|
|
955
|
-
firstTxStartedResolve = resolve;
|
|
956
|
-
});
|
|
948
|
+
const { promise: barrier, resolve: releaseBarrier } =
|
|
949
|
+
Promise.withResolvers<void>();
|
|
950
|
+
const { promise: firstTxStarted, resolve: firstTxStartedResolve } =
|
|
951
|
+
Promise.withResolvers<void>();
|
|
957
952
|
|
|
958
953
|
// @ts-expect-error - dbClient is private
|
|
959
954
|
const dbClient = storage.dbClient;
|
|
@@ -307,8 +307,8 @@ describe("SyncStateManager", () => {
|
|
|
307
307
|
[
|
|
308
308
|
"server -> client | CONTENT Map header: true new: After: 0 New: 1",
|
|
309
309
|
"client -> server | LOAD Group sessions: empty",
|
|
310
|
-
"server -> client | CONTENT Group header: true new: After: 0 New:
|
|
311
|
-
"client -> server | KNOWN Group sessions: header/
|
|
310
|
+
"server -> client | CONTENT Group header: true new: After: 0 New: 4",
|
|
311
|
+
"client -> server | KNOWN Group sessions: header/4",
|
|
312
312
|
"client -> server | KNOWN Map sessions: header/1",
|
|
313
313
|
]
|
|
314
314
|
`);
|
|
@@ -183,7 +183,7 @@ describe("WasmCrypto", () => {
|
|
|
183
183
|
const mapInOtherSession = await loadCoValueOrFail(session2.node, map.id);
|
|
184
184
|
|
|
185
185
|
const transferredMeta = JSON.parse(
|
|
186
|
-
mapInOtherSession.core.verified.
|
|
186
|
+
mapInOtherSession.core.verified.getSession(client.node.currentSessionID)
|
|
187
187
|
?.transactions[0]?.meta!,
|
|
188
188
|
);
|
|
189
189
|
|
|
@@ -193,28 +193,4 @@ describe("WasmCrypto", () => {
|
|
|
193
193
|
},
|
|
194
194
|
});
|
|
195
195
|
});
|
|
196
|
-
|
|
197
|
-
it("fails to verify signatures without a signer ID", async () => {
|
|
198
|
-
const agentSecret = wasmCrypto.newRandomAgentSecret();
|
|
199
|
-
const sessionID = wasmCrypto.newRandomSessionID(
|
|
200
|
-
wasmCrypto.getAgentID(agentSecret),
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
const sessionLog = wasmCrypto.createSessionLog("co_z12345678", sessionID);
|
|
204
|
-
expect(() =>
|
|
205
|
-
sessionLog.tryAdd(
|
|
206
|
-
[
|
|
207
|
-
{
|
|
208
|
-
privacy: "trusting",
|
|
209
|
-
changes: JSON.stringify([
|
|
210
|
-
{ op: "set", key: "count", value: 1 },
|
|
211
|
-
]) as Stringified<JsonValue[]>,
|
|
212
|
-
madeAt: Date.now(),
|
|
213
|
-
},
|
|
214
|
-
],
|
|
215
|
-
"signature_z12345678",
|
|
216
|
-
false,
|
|
217
|
-
),
|
|
218
|
-
).toThrow(expect.stringContaining("Signature verification failed"));
|
|
219
|
-
});
|
|
220
196
|
});
|
package/src/tests/coList.test.ts
CHANGED
|
@@ -210,6 +210,7 @@ test("init the list correctly", () => {
|
|
|
210
210
|
"universe",
|
|
211
211
|
"hello",
|
|
212
212
|
]);
|
|
213
|
+
expect(content.core.verified.header.createdAt).toBeDefined();
|
|
213
214
|
});
|
|
214
215
|
|
|
215
216
|
test("Items prepended to start appear with latest first", () => {
|
|
@@ -904,6 +905,8 @@ describe("CoList Branching", () => {
|
|
|
904
905
|
// Client1 adds items to the branch
|
|
905
906
|
aliceBranch.append("eggs", undefined, "trusting");
|
|
906
907
|
|
|
908
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
909
|
+
|
|
907
910
|
// Client2 loads the branch from a different session
|
|
908
911
|
const branchOnClient2 = await loadCoValueOrFail(
|
|
909
912
|
client2.node,
|
|
@@ -917,11 +920,11 @@ describe("CoList Branching", () => {
|
|
|
917
920
|
"trusting",
|
|
918
921
|
);
|
|
919
922
|
|
|
920
|
-
// Merge the branch back to source
|
|
921
923
|
branchOnClient2.core.mergeBranch();
|
|
922
924
|
|
|
923
|
-
// Wait for sync
|
|
924
|
-
await
|
|
925
|
+
// Wait for all coValues to sync on both nodes
|
|
926
|
+
await client2.node.syncManager.waitForAllCoValuesSync();
|
|
927
|
+
await client1.node.syncManager.waitForAllCoValuesSync();
|
|
925
928
|
|
|
926
929
|
// Source list should contain the final state
|
|
927
930
|
expect(groceryList.toJSON()).toEqual(["milk", "eggs", "cheese"]);
|
|
@@ -960,6 +963,8 @@ describe("CoList Branching", () => {
|
|
|
960
963
|
// Client2 adds different items to second branch
|
|
961
964
|
bobBranch.append("eggs", undefined, "trusting");
|
|
962
965
|
|
|
966
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
967
|
+
|
|
963
968
|
// Client2 loads first branch and modifies it
|
|
964
969
|
const aliceBranchOnClient2 = await loadCoValueOrFail(
|
|
965
970
|
client2.node,
|
|
@@ -975,8 +980,9 @@ describe("CoList Branching", () => {
|
|
|
975
980
|
|
|
976
981
|
bobBranch.core.mergeBranch();
|
|
977
982
|
|
|
978
|
-
// Wait for sync
|
|
979
|
-
await
|
|
983
|
+
// Wait for all coValues to sync on both nodes
|
|
984
|
+
await client2.node.syncManager.waitForAllCoValuesSync();
|
|
985
|
+
await client1.node.syncManager.waitForAllCoValuesSync();
|
|
980
986
|
|
|
981
987
|
// Source list should contain all changes
|
|
982
988
|
expect(groceryList.toJSON()).toMatchInlineSnapshot(`
|
|
@@ -1017,3 +1023,31 @@ test("the list should rebuild when the group permissions change", async () => {
|
|
|
1017
1023
|
expect(listOnBob.version).toEqual(1);
|
|
1018
1024
|
expect(listOnBob.totalValidTransactions).toEqual(1);
|
|
1019
1025
|
});
|
|
1026
|
+
|
|
1027
|
+
test("items appended after a losing init transaction are preserved", async () => {
|
|
1028
|
+
const alice = setupTestNode({ connected: true });
|
|
1029
|
+
const bob = setupTestNode({ connected: true });
|
|
1030
|
+
|
|
1031
|
+
const group = alice.node.createGroup();
|
|
1032
|
+
group.addMember("everyone", "writer");
|
|
1033
|
+
|
|
1034
|
+
const list = group.createList(
|
|
1035
|
+
["alice-init"],
|
|
1036
|
+
undefined,
|
|
1037
|
+
"trusting",
|
|
1038
|
+
undefined,
|
|
1039
|
+
{ fww: "init" },
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
1043
|
+
|
|
1044
|
+
const listOnBob = await loadCoValueOrFail(bob.node, list.id);
|
|
1045
|
+
|
|
1046
|
+
listOnBob.appendItems(["bob-init"], undefined, "trusting", { fww: "init" });
|
|
1047
|
+
listOnBob.appendItems(["bob-update"], undefined, "trusting");
|
|
1048
|
+
|
|
1049
|
+
await waitFor(() => {
|
|
1050
|
+
expect(listOnBob.toJSON()).toEqual(["alice-init", "bob-update"]);
|
|
1051
|
+
expect(list.toJSON()).toEqual(["alice-init", "bob-update"]);
|
|
1052
|
+
});
|
|
1053
|
+
});
|