cojson 0.1.8 → 0.1.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/dist/account.d.ts +4 -2
- package/dist/account.js +4 -2
- package/dist/account.js.map +1 -1
- package/dist/coValue.d.ts +44 -81
- package/dist/coValue.js +4 -348
- package/dist/coValue.js.map +1 -1
- package/dist/coValueCore.d.ts +84 -0
- package/dist/coValueCore.js +351 -0
- package/dist/coValueCore.js.map +1 -0
- package/dist/coValues/coList.d.ts +113 -0
- package/dist/{contentTypes → coValues}/coList.js +59 -19
- package/dist/coValues/coList.js.map +1 -0
- package/dist/{contentTypes → coValues}/coMap.d.ts +25 -7
- package/dist/{contentTypes → coValues}/coMap.js +34 -15
- package/dist/coValues/coMap.js.map +1 -0
- package/dist/coValues/coStream.d.ts +14 -0
- package/dist/coValues/coStream.js +22 -0
- package/dist/coValues/coStream.js.map +1 -0
- package/dist/coValues/static.d.ts +14 -0
- package/dist/coValues/static.js +20 -0
- package/dist/coValues/static.js.map +1 -0
- package/dist/group.d.ts +54 -9
- package/dist/group.js +68 -28
- package/dist/group.js.map +1 -1
- package/dist/index.d.ts +17 -10
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +59 -5
- package/dist/node.js +36 -15
- package/dist/node.js.map +1 -1
- package/dist/permissions.d.ts +2 -2
- package/dist/permissions.js +1 -1
- package/dist/permissions.js.map +1 -1
- package/dist/sync.d.ts +3 -3
- package/dist/sync.js +2 -2
- package/dist/sync.js.map +1 -1
- package/dist/testUtils.d.ts +2 -2
- package/dist/testUtils.js +1 -1
- package/dist/testUtils.js.map +1 -1
- package/package.json +2 -2
- package/src/account.test.ts +1 -1
- package/src/account.ts +6 -4
- package/src/coValue.test.ts +233 -130
- package/src/coValue.ts +50 -576
- package/src/coValueCore.test.ts +181 -0
- package/src/coValueCore.ts +588 -0
- package/src/{contentTypes → coValues}/coList.ts +89 -40
- package/src/{contentTypes → coValues}/coMap.ts +40 -20
- package/src/coValues/coStream.ts +33 -0
- package/src/coValues/static.ts +31 -0
- package/src/group.ts +87 -50
- package/src/index.ts +30 -28
- package/src/node.ts +47 -26
- package/src/permissions.test.ts +32 -32
- package/src/permissions.ts +5 -5
- package/src/sync.test.ts +77 -77
- package/src/sync.ts +5 -5
- package/src/testUtils.ts +1 -1
- package/tsconfig.json +1 -2
- package/dist/contentType.d.ts +0 -15
- package/dist/contentType.js +0 -7
- package/dist/contentType.js.map +0 -1
- package/dist/contentTypes/coList.d.ts +0 -77
- package/dist/contentTypes/coList.js.map +0 -1
- package/dist/contentTypes/coMap.js.map +0 -1
- package/dist/contentTypes/coStream.d.ts +0 -11
- package/dist/contentTypes/coStream.js +0 -16
- package/dist/contentTypes/coStream.js.map +0 -1
- package/dist/contentTypes/static.d.ts +0 -11
- package/dist/contentTypes/static.js +0 -14
- package/dist/contentTypes/static.js.map +0 -1
- package/src/contentType.test.ts +0 -284
- package/src/contentType.ts +0 -26
- package/src/contentTypes/coStream.ts +0 -24
- package/src/contentTypes/static.ts +0 -22
package/src/group.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { CoID,
|
|
2
|
-
import { CoMap } from "./
|
|
1
|
+
import { CoID, CoValueImpl } from "./coValue.js";
|
|
2
|
+
import { CoMap } from "./coValues/coMap.js";
|
|
3
3
|
import { JsonObject, JsonValue } from "./jsonValue.js";
|
|
4
4
|
import {
|
|
5
5
|
Encrypted,
|
|
@@ -17,14 +17,10 @@ import {
|
|
|
17
17
|
} from "./crypto.js";
|
|
18
18
|
import { LocalNode } from "./node.js";
|
|
19
19
|
import { AgentID, SessionID, isAgentID } from "./ids.js";
|
|
20
|
-
import {
|
|
21
|
-
AccountID,
|
|
22
|
-
GeneralizedControlledAccount,
|
|
23
|
-
Profile,
|
|
24
|
-
} from "./account.js";
|
|
20
|
+
import { AccountID, GeneralizedControlledAccount, Profile } from "./account.js";
|
|
25
21
|
import { Role } from "./permissions.js";
|
|
26
22
|
import { base58 } from "@scure/base";
|
|
27
|
-
import { CoList } from "./
|
|
23
|
+
import { CoList } from "./coValues/coList.js";
|
|
28
24
|
|
|
29
25
|
export type GroupContent = {
|
|
30
26
|
profile: CoID<Profile> | null;
|
|
@@ -38,7 +34,7 @@ export type GroupContent = {
|
|
|
38
34
|
};
|
|
39
35
|
|
|
40
36
|
export function expectGroupContent(
|
|
41
|
-
content:
|
|
37
|
+
content: CoValueImpl
|
|
42
38
|
): CoMap<GroupContent, JsonObject | null> {
|
|
43
39
|
if (content.type !== "comap") {
|
|
44
40
|
throw new Error("Expected map");
|
|
@@ -47,43 +43,71 @@ export function expectGroupContent(
|
|
|
47
43
|
return content as CoMap<GroupContent, JsonObject | null>;
|
|
48
44
|
}
|
|
49
45
|
|
|
46
|
+
/** A `Group` is a scope for permissions of its members (`"reader" | "writer" | "admin"`), applying to objects owned by that group.
|
|
47
|
+
*
|
|
48
|
+
* A `Group` object exposes methods for permission management and allows you to create new CoValues owned by that group.
|
|
49
|
+
*
|
|
50
|
+
* (Internally, a `Group` is also just a `CoMap`, mapping member accounts to roles and containing some
|
|
51
|
+
* state management for making cryptographic keys available to current members)
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* You typically get a group from a CoValue that you already have loaded:
|
|
55
|
+
*
|
|
56
|
+
* ```typescript
|
|
57
|
+
* const group = coMap.group;
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* Or, you can create a new group with a `LocalNode`:
|
|
62
|
+
*
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const localNode.createGroup();
|
|
65
|
+
* ```
|
|
66
|
+
* */
|
|
50
67
|
export class Group {
|
|
51
|
-
|
|
68
|
+
underlyingMap: CoMap<GroupContent, JsonObject | null>;
|
|
69
|
+
/** @internal */
|
|
52
70
|
node: LocalNode;
|
|
53
71
|
|
|
72
|
+
/** @internal */
|
|
54
73
|
constructor(
|
|
55
|
-
|
|
74
|
+
underlyingMap: CoMap<GroupContent, JsonObject | null>,
|
|
56
75
|
node: LocalNode
|
|
57
76
|
) {
|
|
58
|
-
this.
|
|
77
|
+
this.underlyingMap = underlyingMap;
|
|
59
78
|
this.node = node;
|
|
60
79
|
}
|
|
61
80
|
|
|
81
|
+
/** Returns the `CoID` of the `Group`. */
|
|
62
82
|
get id(): CoID<CoMap<GroupContent, JsonObject | null>> {
|
|
63
|
-
return this.
|
|
83
|
+
return this.underlyingMap.id;
|
|
64
84
|
}
|
|
65
85
|
|
|
86
|
+
/** Returns the current role of a given account. */
|
|
66
87
|
roleOf(accountID: AccountID): Role | undefined {
|
|
67
88
|
return this.roleOfInternal(accountID);
|
|
68
89
|
}
|
|
69
90
|
|
|
70
91
|
/** @internal */
|
|
71
92
|
roleOfInternal(accountID: AccountID | AgentID): Role | undefined {
|
|
72
|
-
return this.
|
|
93
|
+
return this.underlyingMap.get(accountID);
|
|
73
94
|
}
|
|
74
95
|
|
|
96
|
+
/** Returns the role of the current account in the group. */
|
|
75
97
|
myRole(): Role | undefined {
|
|
76
98
|
return this.roleOfInternal(this.node.account.id);
|
|
77
99
|
}
|
|
78
100
|
|
|
101
|
+
/** Directly grants a new member a role in the group. The current account must be an
|
|
102
|
+
* admin to be able to do so. Throws otherwise. */
|
|
79
103
|
addMember(accountID: AccountID, role: Role) {
|
|
80
104
|
this.addMemberInternal(accountID, role);
|
|
81
105
|
}
|
|
82
106
|
|
|
83
107
|
/** @internal */
|
|
84
108
|
addMemberInternal(accountID: AccountID | AgentID, role: Role) {
|
|
85
|
-
this.
|
|
86
|
-
const currentReadKey = this.
|
|
109
|
+
this.underlyingMap = this.underlyingMap.edit((map) => {
|
|
110
|
+
const currentReadKey = this.underlyingMap.core.getCurrentReadKey();
|
|
87
111
|
|
|
88
112
|
if (!currentReadKey.secret) {
|
|
89
113
|
throw new Error("Can't add member without read key secret");
|
|
@@ -104,11 +128,11 @@ export class Group {
|
|
|
104
128
|
`${currentReadKey.id}_for_${accountID}`,
|
|
105
129
|
seal(
|
|
106
130
|
currentReadKey.secret,
|
|
107
|
-
this.
|
|
131
|
+
this.underlyingMap.core.node.account.currentSealerSecret(),
|
|
108
132
|
getAgentSealerID(agent),
|
|
109
133
|
{
|
|
110
|
-
in: this.
|
|
111
|
-
tx: this.
|
|
134
|
+
in: this.underlyingMap.core.id,
|
|
135
|
+
tx: this.underlyingMap.core.nextTransactionID(),
|
|
112
136
|
}
|
|
113
137
|
),
|
|
114
138
|
"trusting"
|
|
@@ -116,30 +140,24 @@ export class Group {
|
|
|
116
140
|
});
|
|
117
141
|
}
|
|
118
142
|
|
|
119
|
-
|
|
120
|
-
const secretSeed = newRandomSecretSeed();
|
|
121
|
-
|
|
122
|
-
const inviteSecret = agentSecretFromSecretSeed(secretSeed);
|
|
123
|
-
const inviteID = getAgentID(inviteSecret);
|
|
124
|
-
|
|
125
|
-
this.addMemberInternal(inviteID, `${role}Invite` as Role);
|
|
126
|
-
|
|
127
|
-
return inviteSecretFromSecretSeed(secretSeed);
|
|
128
|
-
}
|
|
129
|
-
|
|
143
|
+
/** @internal */
|
|
130
144
|
rotateReadKey() {
|
|
131
|
-
const currentlyPermittedReaders = this.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
145
|
+
const currentlyPermittedReaders = this.underlyingMap
|
|
146
|
+
.keys()
|
|
147
|
+
.filter((key) => {
|
|
148
|
+
if (key.startsWith("co_") || isAgentID(key)) {
|
|
149
|
+
const role = this.underlyingMap.get(key);
|
|
150
|
+
return (
|
|
151
|
+
role === "admin" ||
|
|
152
|
+
role === "writer" ||
|
|
153
|
+
role === "reader"
|
|
154
|
+
);
|
|
155
|
+
} else {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}) as (AccountID | AgentID)[];
|
|
159
|
+
|
|
160
|
+
const maybeCurrentReadKey = this.underlyingMap.core.getCurrentReadKey();
|
|
143
161
|
|
|
144
162
|
if (!maybeCurrentReadKey.secret) {
|
|
145
163
|
throw new Error(
|
|
@@ -154,7 +172,7 @@ export class Group {
|
|
|
154
172
|
|
|
155
173
|
const newReadKey = newRandomKeySecret();
|
|
156
174
|
|
|
157
|
-
this.
|
|
175
|
+
this.underlyingMap = this.underlyingMap.edit((map) => {
|
|
158
176
|
for (const readerID of currentlyPermittedReaders) {
|
|
159
177
|
const reader = this.node.resolveAccountAgent(
|
|
160
178
|
readerID,
|
|
@@ -165,11 +183,11 @@ export class Group {
|
|
|
165
183
|
`${newReadKey.id}_for_${readerID}`,
|
|
166
184
|
seal(
|
|
167
185
|
newReadKey.secret,
|
|
168
|
-
this.
|
|
186
|
+
this.underlyingMap.core.node.account.currentSealerSecret(),
|
|
169
187
|
getAgentSealerID(reader),
|
|
170
188
|
{
|
|
171
|
-
in: this.
|
|
172
|
-
tx: this.
|
|
189
|
+
in: this.underlyingMap.core.id,
|
|
190
|
+
tx: this.underlyingMap.core.nextTransactionID(),
|
|
173
191
|
}
|
|
174
192
|
),
|
|
175
193
|
"trusting"
|
|
@@ -189,19 +207,36 @@ export class Group {
|
|
|
189
207
|
});
|
|
190
208
|
}
|
|
191
209
|
|
|
210
|
+
/** Strips the specified member of all roles (preventing future writes in
|
|
211
|
+
* the group and owned values) and rotates the read encryption key for that group
|
|
212
|
+
* (preventing reads of new content in the group and owned values) */
|
|
192
213
|
removeMember(accountID: AccountID) {
|
|
193
214
|
this.removeMemberInternal(accountID);
|
|
194
215
|
}
|
|
195
216
|
|
|
196
217
|
/** @internal */
|
|
197
218
|
removeMemberInternal(accountID: AccountID | AgentID) {
|
|
198
|
-
this.
|
|
219
|
+
this.underlyingMap = this.underlyingMap.edit((map) => {
|
|
199
220
|
map.set(accountID, "revoked", "trusting");
|
|
200
221
|
});
|
|
201
222
|
|
|
202
223
|
this.rotateReadKey();
|
|
203
224
|
}
|
|
204
225
|
|
|
226
|
+
/** Creates an invite for new members to indirectly join the group, allowing them to grant themselves the specified role with the InviteSecret (a string starting with "inviteSecret_") - use `LocalNode.acceptInvite()` for this purpose. */
|
|
227
|
+
createInvite(role: "reader" | "writer" | "admin"): InviteSecret {
|
|
228
|
+
const secretSeed = newRandomSecretSeed();
|
|
229
|
+
|
|
230
|
+
const inviteSecret = agentSecretFromSecretSeed(secretSeed);
|
|
231
|
+
const inviteID = getAgentID(inviteSecret);
|
|
232
|
+
|
|
233
|
+
this.addMemberInternal(inviteID, `${role}Invite` as Role);
|
|
234
|
+
|
|
235
|
+
return inviteSecretFromSecretSeed(secretSeed);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Creates a new `CoMap` within this group, with the specified specialized
|
|
239
|
+
* `CoMap` type `M` and optional static metadata. */
|
|
205
240
|
createMap<M extends CoMap<{ [key: string]: JsonValue }, JsonObject | null>>(
|
|
206
241
|
meta?: M["meta"]
|
|
207
242
|
): M {
|
|
@@ -210,7 +245,7 @@ export class Group {
|
|
|
210
245
|
type: "comap",
|
|
211
246
|
ruleset: {
|
|
212
247
|
type: "ownedByGroup",
|
|
213
|
-
group: this.
|
|
248
|
+
group: this.underlyingMap.id,
|
|
214
249
|
},
|
|
215
250
|
meta: meta || null,
|
|
216
251
|
...createdNowUnique(),
|
|
@@ -218,6 +253,8 @@ export class Group {
|
|
|
218
253
|
.getCurrentContent() as M;
|
|
219
254
|
}
|
|
220
255
|
|
|
256
|
+
/** Creates a new `CoList` within this group, with the specified specialized
|
|
257
|
+
* `CoList` type `L` and optional static metadata. */
|
|
221
258
|
createList<L extends CoList<JsonValue, JsonObject | null>>(
|
|
222
259
|
meta?: L["meta"]
|
|
223
260
|
): L {
|
|
@@ -226,7 +263,7 @@ export class Group {
|
|
|
226
263
|
type: "colist",
|
|
227
264
|
ruleset: {
|
|
228
265
|
type: "ownedByGroup",
|
|
229
|
-
group: this.
|
|
266
|
+
group: this.underlyingMap.id,
|
|
230
267
|
},
|
|
231
268
|
meta: meta || null,
|
|
232
269
|
...createdNowUnique(),
|
|
@@ -241,7 +278,7 @@ export class Group {
|
|
|
241
278
|
): Group {
|
|
242
279
|
return new Group(
|
|
243
280
|
expectGroupContent(
|
|
244
|
-
this.
|
|
281
|
+
this.underlyingMap.core
|
|
245
282
|
.testWithDifferentAccount(account, sessionId)
|
|
246
283
|
.getCurrentContent()
|
|
247
284
|
),
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CoValueCore, newRandomSessionID } from "./coValueCore.js";
|
|
2
2
|
import { LocalNode } from "./node.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import type { CoValue, ReadableCoValue } from "./coValue.js";
|
|
4
|
+
import { CoMap, WriteableCoMap } from "./coValues/coMap.js";
|
|
5
|
+
import { CoList, WriteableCoList } from "./coValues/coList.js";
|
|
5
6
|
import {
|
|
6
7
|
agentSecretFromBytes,
|
|
7
8
|
agentSecretToBytes,
|
|
@@ -15,24 +16,19 @@ import {
|
|
|
15
16
|
import { connectedPeers } from "./streamUtils.js";
|
|
16
17
|
import { AnonymousControlledAccount, ControlledAccount } from "./account.js";
|
|
17
18
|
import { rawCoIDtoBytes, rawCoIDfromBytes } from "./ids.js";
|
|
18
|
-
import { Group, expectGroupContent } from "./group.js"
|
|
19
|
+
import { Group, expectGroupContent } from "./group.js";
|
|
19
20
|
|
|
20
21
|
import type { SessionID, AgentID } from "./ids.js";
|
|
21
|
-
import type { CoID,
|
|
22
|
+
import type { CoID, CoValueImpl } from "./coValue.js";
|
|
22
23
|
import type { JsonValue } from "./jsonValue.js";
|
|
23
24
|
import type { SyncMessage, Peer } from "./sync.js";
|
|
24
25
|
import type { AgentSecret } from "./crypto.js";
|
|
25
|
-
import type {
|
|
26
|
-
AccountID,
|
|
27
|
-
AccountContent,
|
|
28
|
-
ProfileContent,
|
|
29
|
-
ProfileMeta,
|
|
30
|
-
Profile,
|
|
31
|
-
} from "./account.js";
|
|
26
|
+
import type { AccountID, Profile } from "./account.js";
|
|
32
27
|
import type { InviteSecret } from "./group.js";
|
|
33
28
|
|
|
34
|
-
type Value = JsonValue |
|
|
29
|
+
type Value = JsonValue | CoValueImpl;
|
|
35
30
|
|
|
31
|
+
/** @hidden */
|
|
36
32
|
export const cojsonInternals = {
|
|
37
33
|
agentSecretFromBytes,
|
|
38
34
|
agentSecretToBytes,
|
|
@@ -46,35 +42,36 @@ export const cojsonInternals = {
|
|
|
46
42
|
agentSecretFromSecretSeed,
|
|
47
43
|
secretSeedLength,
|
|
48
44
|
shortHashLength,
|
|
49
|
-
expectGroupContent
|
|
45
|
+
expectGroupContent,
|
|
50
46
|
};
|
|
51
47
|
|
|
52
48
|
export {
|
|
53
49
|
LocalNode,
|
|
54
|
-
|
|
50
|
+
Group,
|
|
55
51
|
CoMap,
|
|
52
|
+
WriteableCoMap,
|
|
56
53
|
CoList,
|
|
54
|
+
WriteableCoList,
|
|
55
|
+
CoValueCore,
|
|
57
56
|
AnonymousControlledAccount,
|
|
58
57
|
ControlledAccount,
|
|
59
|
-
Group
|
|
60
58
|
};
|
|
61
59
|
|
|
62
60
|
export type {
|
|
63
61
|
Value,
|
|
64
62
|
JsonValue,
|
|
65
|
-
|
|
63
|
+
CoValue,
|
|
64
|
+
ReadableCoValue,
|
|
65
|
+
CoValueImpl,
|
|
66
66
|
CoID,
|
|
67
|
-
AgentSecret,
|
|
68
|
-
SessionID,
|
|
69
|
-
SyncMessage,
|
|
70
|
-
AgentID,
|
|
71
67
|
AccountID,
|
|
72
|
-
Peer,
|
|
73
|
-
AccountContent,
|
|
74
68
|
Profile,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
SessionID,
|
|
70
|
+
Peer,
|
|
71
|
+
AgentID,
|
|
72
|
+
AgentSecret,
|
|
73
|
+
InviteSecret,
|
|
74
|
+
SyncMessage,
|
|
78
75
|
};
|
|
79
76
|
|
|
80
77
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
@@ -84,8 +81,13 @@ export namespace CojsonInternalTypes {
|
|
|
84
81
|
export type KnownStateMessage = import("./sync.js").KnownStateMessage;
|
|
85
82
|
export type LoadMessage = import("./sync.js").LoadMessage;
|
|
86
83
|
export type NewContentMessage = import("./sync.js").NewContentMessage;
|
|
87
|
-
export type CoValueHeader = import("./
|
|
88
|
-
export type Transaction = import("./
|
|
84
|
+
export type CoValueHeader = import("./coValueCore.js").CoValueHeader;
|
|
85
|
+
export type Transaction = import("./coValueCore.js").Transaction;
|
|
89
86
|
export type Signature = import("./crypto.js").Signature;
|
|
90
87
|
export type RawCoID = import("./ids.js").RawCoID;
|
|
88
|
+
export type AccountContent = import("./account.js").AccountContent;
|
|
89
|
+
export type ProfileContent = import("./account.js").ProfileContent;
|
|
90
|
+
export type ProfileMeta = import("./account.js").ProfileMeta;
|
|
91
|
+
export type SealerSecret = import("./crypto.js").SealerSecret;
|
|
92
|
+
export type SignerSecret = import("./crypto.js").SignerSecret;
|
|
91
93
|
}
|
package/src/node.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
newRandomKeySecret,
|
|
10
10
|
seal,
|
|
11
11
|
} from "./crypto.js";
|
|
12
|
-
import {
|
|
12
|
+
import { CoValueCore, CoValueHeader, newRandomSessionID } from "./coValueCore.js";
|
|
13
13
|
import {
|
|
14
14
|
InviteSecret,
|
|
15
15
|
Group,
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from "./group.js";
|
|
20
20
|
import { Peer, SyncManager } from "./sync.js";
|
|
21
21
|
import { AgentID, RawCoID, SessionID, isAgentID } from "./ids.js";
|
|
22
|
-
import { CoID,
|
|
22
|
+
import { CoID, CoValueImpl } from "./coValue.js";
|
|
23
23
|
import {
|
|
24
24
|
Account,
|
|
25
25
|
AccountMeta,
|
|
@@ -34,6 +34,17 @@ import {
|
|
|
34
34
|
} from "./account.js";
|
|
35
35
|
import { CoMap } from "./index.js";
|
|
36
36
|
|
|
37
|
+
/** A `LocalNode` represents a local view of a set of loaded `CoValue`s, from the perspective of a particular account (or primitive cryptographic agent).
|
|
38
|
+
|
|
39
|
+
A `LocalNode` can have peers that it syncs to, for example some form of local persistence, or a sync server, such as `sync.jazz.tools` (Jazz Global Mesh).
|
|
40
|
+
|
|
41
|
+
@example
|
|
42
|
+
You typically get hold of a `LocalNode` using `jazz-react`'s `useJazz()`:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
const { localNode } = useJazz();
|
|
46
|
+
```
|
|
47
|
+
*/
|
|
37
48
|
export class LocalNode {
|
|
38
49
|
/** @internal */
|
|
39
50
|
coValues: { [key: RawCoID]: CoValueState } = {};
|
|
@@ -111,8 +122,8 @@ export class LocalNode {
|
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
/** @internal */
|
|
114
|
-
createCoValue(header: CoValueHeader):
|
|
115
|
-
const coValue = new
|
|
125
|
+
createCoValue(header: CoValueHeader): CoValueCore {
|
|
126
|
+
const coValue = new CoValueCore(header, this);
|
|
116
127
|
this.coValues[coValue.id] = { state: "loaded", coValue: coValue };
|
|
117
128
|
|
|
118
129
|
void this.sync.syncCoValue(coValue);
|
|
@@ -121,7 +132,7 @@ export class LocalNode {
|
|
|
121
132
|
}
|
|
122
133
|
|
|
123
134
|
/** @internal */
|
|
124
|
-
loadCoValue(id: RawCoID): Promise<
|
|
135
|
+
loadCoValue(id: RawCoID): Promise<CoValueCore> {
|
|
125
136
|
let entry = this.coValues[id];
|
|
126
137
|
if (!entry) {
|
|
127
138
|
entry = newLoadingState();
|
|
@@ -136,10 +147,19 @@ export class LocalNode {
|
|
|
136
147
|
return entry.done;
|
|
137
148
|
}
|
|
138
149
|
|
|
139
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Loads a CoValue's content, syncing from peers as necessary and resolving the returned
|
|
152
|
+
* promise once a first version has been loaded. See `coValue.subscribe()` and `node.useTelepathicData()`
|
|
153
|
+
* for listening to subsequent updates to the CoValue.
|
|
154
|
+
*/
|
|
155
|
+
async load<T extends CoValueImpl>(id: CoID<T>): Promise<T> {
|
|
140
156
|
return (await this.loadCoValue(id)).getCurrentContent() as T;
|
|
141
157
|
}
|
|
142
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Loads a profile associated with an account. `Profile` is at least a `CoMap<{string: name}>`,
|
|
161
|
+
* but might contain other, app-specific properties.
|
|
162
|
+
*/
|
|
143
163
|
async loadProfile(id: AccountID): Promise<Profile> {
|
|
144
164
|
const account = await this.load<AccountMap>(id);
|
|
145
165
|
const profileID = account.get("profile");
|
|
@@ -152,20 +172,20 @@ export class LocalNode {
|
|
|
152
172
|
).getCurrentContent() as Profile;
|
|
153
173
|
}
|
|
154
174
|
|
|
155
|
-
async acceptInvite<T extends
|
|
175
|
+
async acceptInvite<T extends CoValueImpl>(
|
|
156
176
|
groupOrOwnedValueID: CoID<T>,
|
|
157
177
|
inviteSecret: InviteSecret
|
|
158
178
|
): Promise<void> {
|
|
159
179
|
const groupOrOwnedValue = await this.load(groupOrOwnedValueID);
|
|
160
180
|
|
|
161
|
-
if (groupOrOwnedValue.
|
|
181
|
+
if (groupOrOwnedValue.core.header.ruleset.type === "ownedByGroup") {
|
|
162
182
|
return this.acceptInvite(
|
|
163
|
-
groupOrOwnedValue.
|
|
183
|
+
groupOrOwnedValue.core.header.ruleset.group as CoID<
|
|
164
184
|
CoMap<GroupContent>
|
|
165
185
|
>,
|
|
166
186
|
inviteSecret
|
|
167
187
|
);
|
|
168
|
-
} else if (groupOrOwnedValue.
|
|
188
|
+
} else if (groupOrOwnedValue.core.header.ruleset.type !== "group") {
|
|
169
189
|
throw new Error("Can only accept invites to groups");
|
|
170
190
|
}
|
|
171
191
|
|
|
@@ -177,7 +197,7 @@ export class LocalNode {
|
|
|
177
197
|
const inviteAgentID = getAgentID(inviteAgentSecret);
|
|
178
198
|
|
|
179
199
|
const inviteRole = await new Promise((resolve, reject) => {
|
|
180
|
-
group.
|
|
200
|
+
group.underlyingMap.subscribe((groupMap) => {
|
|
181
201
|
const role = groupMap.get(inviteAgentID);
|
|
182
202
|
if (role) {
|
|
183
203
|
resolve(role);
|
|
@@ -196,7 +216,7 @@ export class LocalNode {
|
|
|
196
216
|
throw new Error("No invite found");
|
|
197
217
|
}
|
|
198
218
|
|
|
199
|
-
const existingRole = group.
|
|
219
|
+
const existingRole = group.underlyingMap.get(this.account.id);
|
|
200
220
|
|
|
201
221
|
if (
|
|
202
222
|
existingRole === "admin" ||
|
|
@@ -222,16 +242,16 @@ export class LocalNode {
|
|
|
222
242
|
: "reader"
|
|
223
243
|
);
|
|
224
244
|
|
|
225
|
-
group.
|
|
226
|
-
group.
|
|
245
|
+
group.underlyingMap.core._sessions = groupAsInvite.underlyingMap.core.sessions;
|
|
246
|
+
group.underlyingMap.core._cachedContent = undefined;
|
|
227
247
|
|
|
228
|
-
for (const groupListener of group.
|
|
229
|
-
groupListener(group.
|
|
248
|
+
for (const groupListener of group.underlyingMap.core.listeners) {
|
|
249
|
+
groupListener(group.underlyingMap.core.getCurrentContent());
|
|
230
250
|
}
|
|
231
251
|
}
|
|
232
252
|
|
|
233
253
|
/** @internal */
|
|
234
|
-
expectCoValueLoaded(id: RawCoID, expectation?: string):
|
|
254
|
+
expectCoValueLoaded(id: RawCoID, expectation?: string): CoValueCore {
|
|
235
255
|
const entry = this.coValues[id];
|
|
236
256
|
if (!entry) {
|
|
237
257
|
throw new Error(
|
|
@@ -284,7 +304,7 @@ export class LocalNode {
|
|
|
284
304
|
account.node
|
|
285
305
|
);
|
|
286
306
|
|
|
287
|
-
accountAsGroup.
|
|
307
|
+
accountAsGroup.underlyingMap.edit((editable) => {
|
|
288
308
|
editable.set(getAgentID(agentSecret), "admin", "trusting");
|
|
289
309
|
|
|
290
310
|
const readKey = newRandomKeySecret();
|
|
@@ -320,13 +340,13 @@ export class LocalNode {
|
|
|
320
340
|
editable.set("name", name, "trusting");
|
|
321
341
|
});
|
|
322
342
|
|
|
323
|
-
accountAsGroup.
|
|
343
|
+
accountAsGroup.underlyingMap.edit((editable) => {
|
|
324
344
|
editable.set("profile", profile.id, "trusting");
|
|
325
345
|
});
|
|
326
346
|
|
|
327
347
|
const accountOnThisNode = this.expectCoValueLoaded(account.id);
|
|
328
348
|
|
|
329
|
-
accountOnThisNode._sessions = {...accountAsGroup.
|
|
349
|
+
accountOnThisNode._sessions = {...accountAsGroup.underlyingMap.core.sessions};
|
|
330
350
|
accountOnThisNode._cachedContent = undefined;
|
|
331
351
|
|
|
332
352
|
return controlledAccount;
|
|
@@ -360,6 +380,7 @@ export class LocalNode {
|
|
|
360
380
|
).getCurrentAgentID();
|
|
361
381
|
}
|
|
362
382
|
|
|
383
|
+
/** Creates a new group (with the current account as the group's first admin). */
|
|
363
384
|
createGroup(): Group {
|
|
364
385
|
const groupCoValue = this.createCoValue({
|
|
365
386
|
type: "comap",
|
|
@@ -422,7 +443,7 @@ export class LocalNode {
|
|
|
422
443
|
continue;
|
|
423
444
|
}
|
|
424
445
|
|
|
425
|
-
const newCoValue = new
|
|
446
|
+
const newCoValue = new CoValueCore(entry.coValue.header, newNode, {...entry.coValue.sessions});
|
|
426
447
|
|
|
427
448
|
newNode.coValues[coValueID as RawCoID] = {
|
|
428
449
|
state: "loaded",
|
|
@@ -441,16 +462,16 @@ export class LocalNode {
|
|
|
441
462
|
type CoValueState =
|
|
442
463
|
| {
|
|
443
464
|
state: "loading";
|
|
444
|
-
done: Promise<
|
|
445
|
-
resolve: (coValue:
|
|
465
|
+
done: Promise<CoValueCore>;
|
|
466
|
+
resolve: (coValue: CoValueCore) => void;
|
|
446
467
|
}
|
|
447
|
-
| { state: "loaded"; coValue:
|
|
468
|
+
| { state: "loaded"; coValue: CoValueCore };
|
|
448
469
|
|
|
449
470
|
/** @internal */
|
|
450
471
|
export function newLoadingState(): CoValueState {
|
|
451
|
-
let resolve: (coValue:
|
|
472
|
+
let resolve: (coValue: CoValueCore) => void;
|
|
452
473
|
|
|
453
|
-
const promise = new Promise<
|
|
474
|
+
const promise = new Promise<CoValueCore>((r) => {
|
|
454
475
|
resolve = r;
|
|
455
476
|
});
|
|
456
477
|
|