cojson 0.19.4 → 0.19.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +15 -0
- package/dist/coValues/coMap.d.ts +3 -2
- package/dist/coValues/coMap.d.ts.map +1 -1
- package/dist/coValues/coMap.js +8 -10
- package/dist/coValues/coMap.js.map +1 -1
- package/dist/coValues/group.d.ts +7 -3
- package/dist/coValues/group.d.ts.map +1 -1
- package/dist/coValues/group.js +70 -24
- package/dist/coValues/group.js.map +1 -1
- package/dist/crypto/PureJSCrypto.d.ts +6 -3
- package/dist/crypto/PureJSCrypto.d.ts.map +1 -1
- package/dist/crypto/PureJSCrypto.js +6 -3
- package/dist/crypto/PureJSCrypto.js.map +1 -1
- package/dist/tests/account.test.js +21 -39
- package/dist/tests/account.test.js.map +1 -1
- package/dist/tests/group.parentGroupCache.test.d.ts +2 -0
- package/dist/tests/group.parentGroupCache.test.d.ts.map +1 -0
- package/dist/tests/group.parentGroupCache.test.js +216 -0
- package/dist/tests/group.parentGroupCache.test.js.map +1 -0
- package/package.json +3 -3
- package/src/coValues/coMap.ts +15 -11
- package/src/coValues/group.ts +99 -24
- package/src/crypto/PureJSCrypto.ts +12 -4
- package/src/tests/account.test.ts +24 -43
- package/src/tests/group.parentGroupCache.test.ts +293 -0
|
@@ -29,7 +29,11 @@ import {
|
|
|
29
29
|
} from "./crypto.js";
|
|
30
30
|
import { ControlledAccountOrAgent } from "../coValues/account.js";
|
|
31
31
|
|
|
32
|
-
type Blake3State =
|
|
32
|
+
export type Blake3State = {
|
|
33
|
+
update: (buf: Uint8Array) => Blake3State;
|
|
34
|
+
digest: () => Uint8Array;
|
|
35
|
+
clone: () => Blake3State;
|
|
36
|
+
};
|
|
33
37
|
|
|
34
38
|
const x25519SharedSecretCache = new Map<string, Uint8Array>();
|
|
35
39
|
|
|
@@ -67,6 +71,10 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
|
|
|
67
71
|
return new PureJSCrypto();
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
createStreamingHash(): Blake3State {
|
|
75
|
+
return blake3.create({});
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
blake3HashOnce(data: Uint8Array) {
|
|
71
79
|
return blake3(data);
|
|
72
80
|
}
|
|
@@ -75,7 +83,7 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
|
|
|
75
83
|
data: Uint8Array,
|
|
76
84
|
{ context }: { context: Uint8Array },
|
|
77
85
|
) {
|
|
78
|
-
return
|
|
86
|
+
return this.createStreamingHash().update(context).update(data).digest();
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
generateNonce(input: Uint8Array): Uint8Array {
|
|
@@ -224,7 +232,7 @@ export class PureJSSessionLog implements SessionLogImpl {
|
|
|
224
232
|
private readonly signerID: SignerID | undefined,
|
|
225
233
|
private readonly crypto: PureJSCrypto,
|
|
226
234
|
) {
|
|
227
|
-
this.streamingHash =
|
|
235
|
+
this.streamingHash = crypto.createStreamingHash();
|
|
228
236
|
}
|
|
229
237
|
|
|
230
238
|
clone(): SessionLogImpl {
|
|
@@ -271,7 +279,7 @@ export class PureJSSessionLog implements SessionLogImpl {
|
|
|
271
279
|
|
|
272
280
|
if (!this.crypto.verify(newSignature, newHashEncoded, this.signerID)) {
|
|
273
281
|
// Rebuild the streaming hash to the original state
|
|
274
|
-
this.streamingHash =
|
|
282
|
+
this.streamingHash = this.crypto.createStreamingHash();
|
|
275
283
|
for (const tx of this.transactions) {
|
|
276
284
|
this.streamingHash.update(textEncoder.encode(tx));
|
|
277
285
|
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import { expect, test } from "vitest";
|
|
1
|
+
import { beforeEach, expect, test } from "vitest";
|
|
2
2
|
import { expectAccount } from "../coValues/account.js";
|
|
3
3
|
import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
|
4
4
|
import { LocalNode } from "../localNode.js";
|
|
5
5
|
import { connectedPeers } from "../streamUtils.js";
|
|
6
6
|
import { createAsyncStorage } from "./testStorage.js";
|
|
7
|
+
import { setupTestAccount, setupTestNode } from "./testUtils.js";
|
|
7
8
|
|
|
8
9
|
const Crypto = await WasmCrypto.create();
|
|
9
10
|
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
await setupTestNode({ isSyncServer: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
10
15
|
test("Can create a node while creating a new account with profile", async () => {
|
|
11
16
|
const { node, accountID, accountSecret, sessionID } =
|
|
12
17
|
await LocalNode.withNewlyCreatedAccount({
|
|
@@ -75,66 +80,42 @@ test("Can create account with one node, and then load it on another", async () =
|
|
|
75
80
|
});
|
|
76
81
|
|
|
77
82
|
test("Should migrate the root from private to trusting", async () => {
|
|
78
|
-
const {
|
|
79
|
-
await LocalNode.withNewlyCreatedAccount({
|
|
80
|
-
creationProps: { name: "Hermes Puggington" },
|
|
81
|
-
crypto: Crypto,
|
|
82
|
-
});
|
|
83
|
+
const session1 = await setupTestAccount({ connected: true });
|
|
83
84
|
|
|
84
|
-
const group =
|
|
85
|
+
const group = session1.node.createGroup();
|
|
85
86
|
expect(group).not.toBeNull();
|
|
86
87
|
|
|
87
88
|
const map = group.createMap();
|
|
88
89
|
map.set("foo", "bar", "private");
|
|
89
90
|
expect(map.get("foo")).toEqual("bar");
|
|
90
91
|
|
|
91
|
-
const
|
|
92
|
-
peer1role: "server",
|
|
93
|
-
peer2role: "client",
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const account = await node.load(accountID);
|
|
92
|
+
const account = await session1.node.load(session1.accountID);
|
|
97
93
|
if (account === "unavailable") throw new Error("Account unavailable");
|
|
98
94
|
|
|
99
95
|
account.set("root", map.id, "private");
|
|
100
96
|
|
|
101
|
-
|
|
97
|
+
// Waiting to ensure that the migration is always applied on a different timestamp
|
|
98
|
+
// to make the test more stable
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, 4));
|
|
102
100
|
|
|
103
|
-
const
|
|
104
|
-
accountID,
|
|
105
|
-
accountSecret,
|
|
106
|
-
sessionID: Crypto.newRandomSessionID(accountID),
|
|
107
|
-
peers: [peers1[0]],
|
|
108
|
-
crypto: Crypto,
|
|
109
|
-
});
|
|
101
|
+
const session2 = await session1.spawnNewSession();
|
|
110
102
|
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
103
|
+
const accountInNewSession = await session2.node.load(session1.accountID);
|
|
104
|
+
if (accountInNewSession === "unavailable")
|
|
105
|
+
throw new Error("Account unavailable");
|
|
113
106
|
|
|
114
|
-
expect(
|
|
115
|
-
|
|
116
|
-
node2.gracefulShutdown(); // Stop getting updates from node1
|
|
117
|
-
|
|
118
|
-
const peers2 = connectedPeers("node2", "node3", {
|
|
119
|
-
peer1role: "server",
|
|
120
|
-
peer2role: "client",
|
|
121
|
-
});
|
|
107
|
+
expect(accountInNewSession.getRaw("root")?.trusting).toEqual(true);
|
|
122
108
|
|
|
123
|
-
|
|
109
|
+
await accountInNewSession.core.waitForSync();
|
|
124
110
|
|
|
125
|
-
const
|
|
126
|
-
accountID,
|
|
127
|
-
accountSecret,
|
|
128
|
-
sessionID: Crypto.newRandomSessionID(accountID),
|
|
129
|
-
peers: [peers2[0]],
|
|
130
|
-
crypto: Crypto,
|
|
131
|
-
});
|
|
111
|
+
const session3 = await session2.spawnNewSession();
|
|
132
112
|
|
|
133
|
-
const
|
|
134
|
-
if (
|
|
113
|
+
const accountInNewSession2 = await session3.node.load(session1.accountID);
|
|
114
|
+
if (accountInNewSession2 === "unavailable")
|
|
115
|
+
throw new Error("Account unavailable");
|
|
135
116
|
|
|
136
|
-
expect(
|
|
137
|
-
expect(
|
|
117
|
+
expect(accountInNewSession2.getRaw("root")?.trusting).toEqual(true);
|
|
118
|
+
expect(accountInNewSession2.ops).toEqual(accountInNewSession.ops); // No new transactions were made
|
|
138
119
|
});
|
|
139
120
|
|
|
140
121
|
test("myRole returns 'admin' for the current account", async () => {
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
setupTestAccount,
|
|
4
|
+
setupTestNode,
|
|
5
|
+
hotSleep,
|
|
6
|
+
loadCoValueOrFail,
|
|
7
|
+
} from "./testUtils";
|
|
8
|
+
|
|
9
|
+
let jazzCloud: ReturnType<typeof setupTestNode>;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
jazzCloud = setupTestNode({ isSyncServer: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("Parent Group Cache", () => {
|
|
16
|
+
describe("Property 1: Parent group cache update correctness", () => {
|
|
17
|
+
test("cache contains correct entries after processing parent group reference transactions", async () => {
|
|
18
|
+
const account = await setupTestAccount({
|
|
19
|
+
connected: true,
|
|
20
|
+
});
|
|
21
|
+
const parentGroup = account.node.createGroup();
|
|
22
|
+
const childGroup = account.node.createGroup();
|
|
23
|
+
|
|
24
|
+
// Initially no parent groups
|
|
25
|
+
expect(childGroup.getParentGroups()).toEqual([]);
|
|
26
|
+
|
|
27
|
+
// Extend parent group
|
|
28
|
+
childGroup.extend(parentGroup);
|
|
29
|
+
expect(childGroup.getParentGroups()).toEqual([parentGroup]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("cache handles multiple updates to same parent group", async () => {
|
|
33
|
+
const account = await setupTestAccount({
|
|
34
|
+
connected: true,
|
|
35
|
+
});
|
|
36
|
+
const parentGroup = account.node.createGroup();
|
|
37
|
+
const childGroup = account.node.createGroup();
|
|
38
|
+
|
|
39
|
+
childGroup.extend(parentGroup);
|
|
40
|
+
expect(childGroup.getParentGroups()).toEqual([parentGroup]);
|
|
41
|
+
|
|
42
|
+
// Revoke and re-extend
|
|
43
|
+
childGroup.revokeExtend(parentGroup);
|
|
44
|
+
expect(childGroup.getParentGroups()).toEqual([]);
|
|
45
|
+
|
|
46
|
+
childGroup.extend(parentGroup, "admin");
|
|
47
|
+
expect(childGroup.getParentGroups()).toEqual([parentGroup]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("cache correctly handles empty state (no parent groups)", async () => {
|
|
51
|
+
const account = await setupTestAccount({
|
|
52
|
+
connected: true,
|
|
53
|
+
});
|
|
54
|
+
const group = account.node.createGroup();
|
|
55
|
+
|
|
56
|
+
expect(group.getParentGroups()).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("cache only contains parent groups that have reference transactions", async () => {
|
|
60
|
+
const account = await setupTestAccount({
|
|
61
|
+
connected: true,
|
|
62
|
+
});
|
|
63
|
+
const parentGroup1 = account.node.createGroup();
|
|
64
|
+
const parentGroup2 = account.node.createGroup();
|
|
65
|
+
const childGroup = account.node.createGroup();
|
|
66
|
+
|
|
67
|
+
// Only extend one parent group
|
|
68
|
+
childGroup.extend(parentGroup1);
|
|
69
|
+
|
|
70
|
+
const parentGroups = childGroup.getParentGroups();
|
|
71
|
+
expect(parentGroups).toHaveLength(1);
|
|
72
|
+
expect(parentGroups[0]?.id).toEqual(parentGroup1.id);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("cache handles multiple parent groups", async () => {
|
|
76
|
+
const account = await setupTestAccount({
|
|
77
|
+
connected: true,
|
|
78
|
+
});
|
|
79
|
+
const parentGroup1 = account.node.createGroup();
|
|
80
|
+
const parentGroup2 = account.node.createGroup();
|
|
81
|
+
const parentGroup3 = account.node.createGroup();
|
|
82
|
+
const childGroup = account.node.createGroup();
|
|
83
|
+
|
|
84
|
+
childGroup.extend(parentGroup1);
|
|
85
|
+
childGroup.extend(parentGroup2);
|
|
86
|
+
childGroup.extend(parentGroup3);
|
|
87
|
+
|
|
88
|
+
const parentGroups = childGroup.getParentGroups();
|
|
89
|
+
expect(parentGroups).toHaveLength(3);
|
|
90
|
+
expect(parentGroups.map((g) => g.id).sort()).toEqual(
|
|
91
|
+
[parentGroup1.id, parentGroup2.id, parentGroup3.id].sort(),
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("Property 2: Parent group cache chronological ordering", () => {
|
|
97
|
+
test("multiple parent group changes are stored in chronological order", async () => {
|
|
98
|
+
const account = await setupTestAccount({
|
|
99
|
+
connected: true,
|
|
100
|
+
});
|
|
101
|
+
const parentGroup = account.node.createGroup();
|
|
102
|
+
const childGroup = account.node.createGroup();
|
|
103
|
+
|
|
104
|
+
const t1 = hotSleep(10);
|
|
105
|
+
childGroup.extend(parentGroup, "reader");
|
|
106
|
+
const t2 = hotSleep(10);
|
|
107
|
+
childGroup.extend(parentGroup, "writer");
|
|
108
|
+
const t3 = hotSleep(10);
|
|
109
|
+
childGroup.extend(parentGroup, "admin");
|
|
110
|
+
|
|
111
|
+
// Check time travel queries return correct historical states
|
|
112
|
+
expect(childGroup.atTime(t1).getParentGroups()).toEqual([]);
|
|
113
|
+
expect(childGroup.atTime(t2).getParentGroups()).toEqual([
|
|
114
|
+
parentGroup.atTime(t2),
|
|
115
|
+
]);
|
|
116
|
+
expect(childGroup.atTime(t3).getParentGroups()).toEqual([
|
|
117
|
+
parentGroup.atTime(t3),
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
// Current state should have admin role
|
|
121
|
+
expect(childGroup.getParentGroups()).toEqual([parentGroup]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("out-of-order transaction processing maintains chronological order", async () => {
|
|
125
|
+
const account = await setupTestAccount({
|
|
126
|
+
connected: true,
|
|
127
|
+
});
|
|
128
|
+
const parentGroup = account.node.createGroup();
|
|
129
|
+
const childGroup = account.node.createGroup();
|
|
130
|
+
|
|
131
|
+
// Create transactions with different timestamps
|
|
132
|
+
const t1 = hotSleep(10);
|
|
133
|
+
const t2 = hotSleep(10);
|
|
134
|
+
|
|
135
|
+
childGroup.core.makeTransaction(
|
|
136
|
+
[
|
|
137
|
+
{
|
|
138
|
+
op: "set",
|
|
139
|
+
key: `parent_${parentGroup.id}`,
|
|
140
|
+
value: "revoked",
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
"private",
|
|
144
|
+
undefined,
|
|
145
|
+
t2,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
childGroup.core.makeTransaction(
|
|
149
|
+
[
|
|
150
|
+
{
|
|
151
|
+
op: "set",
|
|
152
|
+
key: `parent_${parentGroup.id}`,
|
|
153
|
+
value: "extend",
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
"private",
|
|
157
|
+
undefined,
|
|
158
|
+
t1,
|
|
159
|
+
);
|
|
160
|
+
// Verify chronological ordering through time travel
|
|
161
|
+
const groupAtT1 = childGroup.atTime(t1);
|
|
162
|
+
const groupAtT2 = childGroup.atTime(t2);
|
|
163
|
+
|
|
164
|
+
expect(groupAtT1.getParentGroups()).toEqual([parentGroup.atTime(t1)]);
|
|
165
|
+
expect(groupAtT2.getParentGroups()).toEqual([]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("chronological ordering after rebuild", async () => {
|
|
169
|
+
const account = await setupTestAccount({
|
|
170
|
+
connected: true,
|
|
171
|
+
});
|
|
172
|
+
const parentGroup = account.node.createGroup();
|
|
173
|
+
const childGroup = account.node.createGroup();
|
|
174
|
+
|
|
175
|
+
const t1 = hotSleep(10);
|
|
176
|
+
childGroup.extend(parentGroup, "reader");
|
|
177
|
+
const t2 = hotSleep(10);
|
|
178
|
+
childGroup.extend(parentGroup, "writer");
|
|
179
|
+
|
|
180
|
+
// Rebuild should maintain chronological order
|
|
181
|
+
childGroup.rebuildFromCore();
|
|
182
|
+
|
|
183
|
+
const groupAtT1 = childGroup.atTime(t1);
|
|
184
|
+
const groupAtT2 = childGroup.atTime(t2);
|
|
185
|
+
|
|
186
|
+
expect(groupAtT1.getParentGroups()).toEqual([]);
|
|
187
|
+
expect(groupAtT2.getParentGroups()).toEqual([parentGroup.atTime(t2)]);
|
|
188
|
+
expect(childGroup.getParentGroups()).toEqual([parentGroup]);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("Property 3: Rebuild round-trip", () => {
|
|
193
|
+
test("cache is cleared after rebuildFromCore", async () => {
|
|
194
|
+
const account = await setupTestAccount({
|
|
195
|
+
connected: true,
|
|
196
|
+
});
|
|
197
|
+
const parentGroup = account.node.createGroup();
|
|
198
|
+
const childGroup = account.node.createGroup();
|
|
199
|
+
|
|
200
|
+
childGroup.extend(parentGroup);
|
|
201
|
+
expect(childGroup.getParentGroups()).toEqual([parentGroup]);
|
|
202
|
+
|
|
203
|
+
// Rebuild should clear cache and rebuild it
|
|
204
|
+
childGroup.rebuildFromCore();
|
|
205
|
+
|
|
206
|
+
// Cache should be rebuilt and still work
|
|
207
|
+
expect(childGroup.getParentGroups()).toEqual([parentGroup]);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("cache is rebuilt correctly after clear", async () => {
|
|
211
|
+
const account = await setupTestAccount({
|
|
212
|
+
connected: true,
|
|
213
|
+
});
|
|
214
|
+
const parentGroup1 = account.node.createGroup();
|
|
215
|
+
const parentGroup2 = account.node.createGroup();
|
|
216
|
+
const childGroup = account.node.createGroup();
|
|
217
|
+
|
|
218
|
+
childGroup.extend(parentGroup1);
|
|
219
|
+
childGroup.extend(parentGroup2);
|
|
220
|
+
|
|
221
|
+
expect(childGroup.getParentGroups()).toHaveLength(2);
|
|
222
|
+
|
|
223
|
+
// Rebuild
|
|
224
|
+
childGroup.rebuildFromCore();
|
|
225
|
+
|
|
226
|
+
// Cache should be rebuilt with all parent groups
|
|
227
|
+
const parentGroups = childGroup.getParentGroups();
|
|
228
|
+
expect(parentGroups).toHaveLength(2);
|
|
229
|
+
expect(parentGroups.map((g) => g.id).sort()).toEqual(
|
|
230
|
+
[parentGroup1.id, parentGroup2.id].sort(),
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("parent group lookups work correctly after rebuild", async () => {
|
|
235
|
+
const alice = await setupTestAccount({
|
|
236
|
+
connected: true,
|
|
237
|
+
});
|
|
238
|
+
const bob = await setupTestAccount({
|
|
239
|
+
connected: true,
|
|
240
|
+
});
|
|
241
|
+
const parentGroup = alice.node.createGroup();
|
|
242
|
+
const childGroup = alice.node.createGroup();
|
|
243
|
+
|
|
244
|
+
parentGroup.addMember(
|
|
245
|
+
await loadCoValueOrFail(alice.node, bob.accountID),
|
|
246
|
+
"writer",
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
childGroup.extend(parentGroup);
|
|
250
|
+
|
|
251
|
+
// Check role inheritance before rebuild
|
|
252
|
+
expect(childGroup.roleOf(bob.accountID)).toEqual("writer");
|
|
253
|
+
|
|
254
|
+
// Rebuild
|
|
255
|
+
childGroup.rebuildFromCore();
|
|
256
|
+
|
|
257
|
+
// Role inheritance should still work after rebuild
|
|
258
|
+
expect(childGroup.roleOf(bob.accountID)).toEqual("writer");
|
|
259
|
+
expect(childGroup.getParentGroups()).toEqual([parentGroup]);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("rebuild maintains chronological ordering", async () => {
|
|
263
|
+
const alice = await setupTestAccount({
|
|
264
|
+
connected: true,
|
|
265
|
+
});
|
|
266
|
+
const bob = await setupTestAccount({
|
|
267
|
+
connected: true,
|
|
268
|
+
});
|
|
269
|
+
const parentGroup = alice.node.createGroup();
|
|
270
|
+
const childGroup = alice.node.createGroup();
|
|
271
|
+
|
|
272
|
+
parentGroup.addMember(
|
|
273
|
+
await loadCoValueOrFail(alice.node, bob.accountID),
|
|
274
|
+
"writer",
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const t1 = hotSleep(10);
|
|
278
|
+
childGroup.extend(parentGroup, "reader");
|
|
279
|
+
const t2 = hotSleep(10);
|
|
280
|
+
childGroup.extend(parentGroup, "writer");
|
|
281
|
+
|
|
282
|
+
// Rebuild
|
|
283
|
+
childGroup.rebuildFromCore();
|
|
284
|
+
|
|
285
|
+
// Chronological ordering should be maintained
|
|
286
|
+
expect(childGroup.atTime(t1).roleOf(bob.accountID)).toEqual(undefined);
|
|
287
|
+
expect(childGroup.atTime(t2).roleOf(bob.accountID)).toEqual("reader");
|
|
288
|
+
expect(childGroup.atTime(Date.now()).roleOf(bob.accountID)).toEqual(
|
|
289
|
+
"writer",
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
});
|