cojson 0.8.35 → 0.8.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/native/coValues/coMap.js +119 -51
- package/dist/native/coValues/coMap.js.map +1 -1
- package/dist/native/permissions.js +19 -18
- package/dist/native/permissions.js.map +1 -1
- package/dist/web/coValues/coMap.js +119 -51
- package/dist/web/coValues/coMap.js.map +1 -1
- package/dist/web/permissions.js +19 -18
- package/dist/web/permissions.js.map +1 -1
- package/package.json +1 -1
- package/src/coValueCore.ts +1 -1
- package/src/coValues/coMap.ts +175 -73
- package/src/permissions.ts +40 -39
- package/src/tests/coMap.test.ts +24 -0
package/src/coValues/coMap.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CoID, RawCoValue } from "../coValue.js";
|
|
2
|
-
import { CoValueCore } from "../coValueCore.js";
|
|
2
|
+
import { CoValueCore, DecryptedTransaction } from "../coValueCore.js";
|
|
3
3
|
import { AgentID, TransactionID } from "../ids.js";
|
|
4
4
|
import { JsonObject, JsonValue } from "../jsonValue.js";
|
|
5
5
|
import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
|
|
@@ -11,7 +11,8 @@ type MapOp<K extends string, V extends JsonValue | undefined> = {
|
|
|
11
11
|
txID: TransactionID;
|
|
12
12
|
madeAt: number;
|
|
13
13
|
changeIdx: number;
|
|
14
|
-
|
|
14
|
+
change: MapOpPayload<K, V>;
|
|
15
|
+
};
|
|
15
16
|
// TODO: add after TransactionID[] for conflicts/ordering
|
|
16
17
|
|
|
17
18
|
export type MapOpPayload<K extends string, V extends JsonValue | undefined> =
|
|
@@ -39,10 +40,21 @@ export class RawCoMapView<
|
|
|
39
40
|
/** @category 6. Meta */
|
|
40
41
|
core: CoValueCore;
|
|
41
42
|
/** @internal */
|
|
42
|
-
|
|
43
|
+
latest: {
|
|
44
|
+
[Key in keyof Shape & string]?: MapOp<Key, Shape[Key]>;
|
|
45
|
+
};
|
|
46
|
+
/** @internal */
|
|
47
|
+
latestTxMadeAt: number;
|
|
48
|
+
/** @internal */
|
|
49
|
+
cachedOps?: {
|
|
43
50
|
[Key in keyof Shape & string]?: MapOp<Key, Shape[Key]>[];
|
|
44
51
|
};
|
|
45
52
|
/** @internal */
|
|
53
|
+
validSortedTransactions?: DecryptedTransaction[];
|
|
54
|
+
|
|
55
|
+
/** @internal */
|
|
56
|
+
options?: { ignorePrivateTransactions: boolean; atTime?: number };
|
|
57
|
+
/** @internal */
|
|
46
58
|
atTimeFilter?: number = undefined;
|
|
47
59
|
/** @category 6. Meta */
|
|
48
60
|
readonly _shape!: Shape;
|
|
@@ -50,33 +62,126 @@ export class RawCoMapView<
|
|
|
50
62
|
/** @internal */
|
|
51
63
|
constructor(
|
|
52
64
|
core: CoValueCore,
|
|
53
|
-
options?: {
|
|
65
|
+
options?: {
|
|
66
|
+
ignorePrivateTransactions: boolean;
|
|
67
|
+
atTime?: number;
|
|
68
|
+
validSortedTransactions?: DecryptedTransaction[];
|
|
69
|
+
cachedOps?: {
|
|
70
|
+
[Key in keyof Shape & string]?: MapOp<Key, Shape[Key]>[];
|
|
71
|
+
};
|
|
72
|
+
},
|
|
54
73
|
) {
|
|
55
74
|
this.id = core.id as CoID<this>;
|
|
56
75
|
this.core = core;
|
|
57
|
-
this.
|
|
76
|
+
this.latest = {};
|
|
77
|
+
this.latestTxMadeAt = 0;
|
|
78
|
+
this.options = options;
|
|
79
|
+
this.cachedOps = options?.cachedOps;
|
|
80
|
+
this.validSortedTransactions = options?.validSortedTransactions;
|
|
81
|
+
|
|
82
|
+
this.processLatestTransactions();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @internal */
|
|
86
|
+
private getValidSortedTransactions() {
|
|
87
|
+
if (this.validSortedTransactions) {
|
|
88
|
+
return this.validSortedTransactions;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const validSortedTransactions = this.core.getValidSortedTransactions({
|
|
92
|
+
ignorePrivateTransactions:
|
|
93
|
+
this.options?.ignorePrivateTransactions ?? false,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
this.validSortedTransactions = validSortedTransactions;
|
|
97
|
+
|
|
98
|
+
return validSortedTransactions;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private resetCachedValues() {
|
|
102
|
+
this.validSortedTransactions = undefined;
|
|
103
|
+
this.cachedOps = undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private processLatestTransactions() {
|
|
107
|
+
// Reset all internal state and cached values
|
|
108
|
+
this.latest = {};
|
|
109
|
+
this.latestTxMadeAt = 0;
|
|
110
|
+
|
|
111
|
+
const { latest } = this;
|
|
112
|
+
|
|
113
|
+
const atTimeFilter = this.options?.atTime;
|
|
114
|
+
|
|
115
|
+
for (const { txID, changes, madeAt } of this.getValidSortedTransactions()) {
|
|
116
|
+
if (atTimeFilter && madeAt > atTimeFilter) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (madeAt > this.latestTxMadeAt) {
|
|
121
|
+
this.latestTxMadeAt = madeAt;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (let changeIdx = 0; changeIdx < changes.length; changeIdx++) {
|
|
125
|
+
const change = changes[changeIdx] as MapOpPayload<
|
|
126
|
+
keyof Shape & string,
|
|
127
|
+
Shape[keyof Shape & string]
|
|
128
|
+
>;
|
|
129
|
+
let entry = latest[change.key];
|
|
130
|
+
if (!entry) {
|
|
131
|
+
entry = {
|
|
132
|
+
txID,
|
|
133
|
+
madeAt,
|
|
134
|
+
changeIdx,
|
|
135
|
+
change,
|
|
136
|
+
};
|
|
137
|
+
latest[change.key] = entry;
|
|
138
|
+
} else if (madeAt >= entry.madeAt) {
|
|
139
|
+
entry.txID = txID;
|
|
140
|
+
entry.madeAt = madeAt;
|
|
141
|
+
entry.changeIdx = changeIdx;
|
|
142
|
+
entry.change = change;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
58
147
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
148
|
+
revalidateTransactions() {
|
|
149
|
+
this.resetCachedValues();
|
|
150
|
+
this.processLatestTransactions();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private getOps() {
|
|
154
|
+
if (this.cachedOps) {
|
|
155
|
+
return this.cachedOps;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const ops: {
|
|
159
|
+
[Key in keyof Shape & string]?: MapOp<Key, Shape[Key]>[];
|
|
160
|
+
} = {};
|
|
161
|
+
|
|
162
|
+
for (const { txID, changes, madeAt } of this.getValidSortedTransactions()) {
|
|
62
163
|
for (let changeIdx = 0; changeIdx < changes.length; changeIdx++) {
|
|
63
164
|
const change = changes[changeIdx] as MapOpPayload<
|
|
64
165
|
keyof Shape & string,
|
|
65
166
|
Shape[keyof Shape & string]
|
|
66
167
|
>;
|
|
67
|
-
let entries =
|
|
168
|
+
let entries = ops[change.key];
|
|
68
169
|
if (!entries) {
|
|
69
170
|
entries = [];
|
|
70
|
-
|
|
171
|
+
ops[change.key] = entries;
|
|
71
172
|
}
|
|
72
173
|
entries.push({
|
|
73
174
|
txID,
|
|
74
175
|
madeAt,
|
|
75
176
|
changeIdx,
|
|
76
|
-
|
|
177
|
+
change,
|
|
77
178
|
});
|
|
78
179
|
}
|
|
79
180
|
}
|
|
181
|
+
|
|
182
|
+
this.cachedOps = ops;
|
|
183
|
+
|
|
184
|
+
return ops;
|
|
80
185
|
}
|
|
81
186
|
|
|
82
187
|
/** @category 6. Meta */
|
|
@@ -91,13 +196,19 @@ export class RawCoMapView<
|
|
|
91
196
|
|
|
92
197
|
/** @category 4. Time travel */
|
|
93
198
|
atTime(time: number): this {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
199
|
+
if (time >= this.latestTxMadeAt) {
|
|
200
|
+
return this;
|
|
201
|
+
} else {
|
|
202
|
+
const clone = new RawCoMapView(this.core, {
|
|
203
|
+
ignorePrivateTransactions:
|
|
204
|
+
this.options?.ignorePrivateTransactions ?? false,
|
|
205
|
+
atTime: time,
|
|
206
|
+
cachedOps: this.cachedOps,
|
|
207
|
+
validSortedTransactions: this.validSortedTransactions,
|
|
208
|
+
});
|
|
209
|
+
Object.setPrototypeOf(clone, this);
|
|
210
|
+
return clone as this;
|
|
211
|
+
}
|
|
101
212
|
}
|
|
102
213
|
|
|
103
214
|
/** @internal */
|
|
@@ -108,10 +219,12 @@ export class RawCoMapView<
|
|
|
108
219
|
return undefined;
|
|
109
220
|
}
|
|
110
221
|
|
|
111
|
-
|
|
112
|
-
|
|
222
|
+
const atTimeFilter = this.options?.atTime;
|
|
223
|
+
|
|
224
|
+
if (atTimeFilter) {
|
|
225
|
+
return this.getOps()[key]?.filter((op) => op.madeAt <= atTimeFilter);
|
|
113
226
|
} else {
|
|
114
|
-
return this.
|
|
227
|
+
return this.getOps()[key];
|
|
115
228
|
}
|
|
116
229
|
}
|
|
117
230
|
|
|
@@ -120,18 +233,14 @@ export class RawCoMapView<
|
|
|
120
233
|
*
|
|
121
234
|
* @category 1. Reading */
|
|
122
235
|
keys<K extends keyof Shape & string = keyof Shape & string>(): K[] {
|
|
123
|
-
return (Object.keys(this.
|
|
124
|
-
const
|
|
125
|
-
if (!ops) {
|
|
126
|
-
return undefined;
|
|
127
|
-
}
|
|
236
|
+
return (Object.keys(this.latest) as K[]).filter((key) => {
|
|
237
|
+
const latestChange = this.latest[key];
|
|
128
238
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
: ops[ops.length - 1]!;
|
|
239
|
+
if (!latestChange) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
133
242
|
|
|
134
|
-
if (
|
|
243
|
+
if (latestChange.change.op === "del") {
|
|
135
244
|
return false;
|
|
136
245
|
} else {
|
|
137
246
|
return true;
|
|
@@ -145,20 +254,15 @@ export class RawCoMapView<
|
|
|
145
254
|
* @category 1. Reading
|
|
146
255
|
**/
|
|
147
256
|
get<K extends keyof Shape & string>(key: K): Shape[K] | undefined {
|
|
148
|
-
const
|
|
149
|
-
if (!
|
|
257
|
+
const latestChange = this.latest[key];
|
|
258
|
+
if (!latestChange) {
|
|
150
259
|
return undefined;
|
|
151
260
|
}
|
|
152
261
|
|
|
153
|
-
|
|
154
|
-
const lastEntry = includeUntil
|
|
155
|
-
? ops.findLast((entry) => entry.madeAt <= includeUntil)
|
|
156
|
-
: ops[ops.length - 1]!;
|
|
157
|
-
|
|
158
|
-
if (lastEntry?.op === "del") {
|
|
262
|
+
if (latestChange.change.op === "del") {
|
|
159
263
|
return undefined;
|
|
160
264
|
} else {
|
|
161
|
-
return
|
|
265
|
+
return latestChange.change.value as Shape[K];
|
|
162
266
|
}
|
|
163
267
|
}
|
|
164
268
|
|
|
@@ -170,7 +274,7 @@ export class RawCoMapView<
|
|
|
170
274
|
[K in keyof Shape & string]: Shape[K];
|
|
171
275
|
}> = {};
|
|
172
276
|
|
|
173
|
-
for (const key of
|
|
277
|
+
for (const key of Object.keys(this.latest) as (keyof Shape & string)[]) {
|
|
174
278
|
const value = this.get(key);
|
|
175
279
|
if (value !== undefined) {
|
|
176
280
|
object[key] = value;
|
|
@@ -190,34 +294,21 @@ export class RawCoMapView<
|
|
|
190
294
|
}
|
|
191
295
|
|
|
192
296
|
/** @category 5. Edit history */
|
|
193
|
-
nthEditAt<K extends keyof Shape & string>(
|
|
194
|
-
key
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
at: Date;
|
|
201
|
-
value?: Shape[K];
|
|
202
|
-
}
|
|
203
|
-
| undefined {
|
|
204
|
-
const ops = this.timeFilteredOps(key);
|
|
205
|
-
if (!ops || ops.length <= n) {
|
|
297
|
+
nthEditAt<K extends keyof Shape & string>(key: K, n: number) {
|
|
298
|
+
const ops = this.getOps()[key];
|
|
299
|
+
|
|
300
|
+
const atTimeFilter = this.options?.atTime;
|
|
301
|
+
const entry = ops?.[n];
|
|
302
|
+
|
|
303
|
+
if (!entry) {
|
|
206
304
|
return undefined;
|
|
207
305
|
}
|
|
208
306
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (this.atTimeFilter && entry.madeAt > this.atTimeFilter) {
|
|
307
|
+
if (atTimeFilter && entry.madeAt > atTimeFilter) {
|
|
212
308
|
return undefined;
|
|
213
309
|
}
|
|
214
310
|
|
|
215
|
-
return
|
|
216
|
-
by: accountOrAgentIDfromSessionID(entry.txID.sessionID),
|
|
217
|
-
tx: entry.txID,
|
|
218
|
-
at: new Date(entry.madeAt),
|
|
219
|
-
value: entry.op === "del" ? undefined : entry.value,
|
|
220
|
-
};
|
|
311
|
+
return operationToEditEntry(entry);
|
|
221
312
|
}
|
|
222
313
|
|
|
223
314
|
/** @category 5. Edit history */
|
|
@@ -232,10 +323,13 @@ export class RawCoMapView<
|
|
|
232
323
|
}
|
|
233
324
|
| undefined {
|
|
234
325
|
const ops = this.timeFilteredOps(key);
|
|
235
|
-
|
|
326
|
+
const lastEntry = ops?.[ops.length - 1];
|
|
327
|
+
|
|
328
|
+
if (!lastEntry) {
|
|
236
329
|
return undefined;
|
|
237
330
|
}
|
|
238
|
-
|
|
331
|
+
|
|
332
|
+
return operationToEditEntry(lastEntry);
|
|
239
333
|
}
|
|
240
334
|
|
|
241
335
|
/** @category 5. Edit history */
|
|
@@ -245,8 +339,8 @@ export class RawCoMapView<
|
|
|
245
339
|
return;
|
|
246
340
|
}
|
|
247
341
|
|
|
248
|
-
for (
|
|
249
|
-
yield
|
|
342
|
+
for (const entry of ops) {
|
|
343
|
+
yield operationToEditEntry(entry);
|
|
250
344
|
}
|
|
251
345
|
}
|
|
252
346
|
|
|
@@ -292,9 +386,7 @@ export class RawCoMap<
|
|
|
292
386
|
privacy,
|
|
293
387
|
);
|
|
294
388
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
this.ops = after.ops;
|
|
389
|
+
this.revalidateTransactions();
|
|
298
390
|
}
|
|
299
391
|
|
|
300
392
|
/** Delete the given key (setting it to undefined).
|
|
@@ -319,8 +411,18 @@ export class RawCoMap<
|
|
|
319
411
|
privacy,
|
|
320
412
|
);
|
|
321
413
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
this.ops = after.ops;
|
|
414
|
+
this.revalidateTransactions();
|
|
325
415
|
}
|
|
326
416
|
}
|
|
417
|
+
|
|
418
|
+
export function operationToEditEntry<
|
|
419
|
+
K extends string,
|
|
420
|
+
V extends JsonValue | undefined,
|
|
421
|
+
>(op: MapOp<K, V>) {
|
|
422
|
+
return {
|
|
423
|
+
by: accountOrAgentIDfromSessionID(op.txID.sessionID),
|
|
424
|
+
tx: op.txID,
|
|
425
|
+
at: new Date(op.madeAt),
|
|
426
|
+
value: op.change.op === "del" ? undefined : op.change.value,
|
|
427
|
+
};
|
|
428
|
+
}
|
package/src/permissions.ts
CHANGED
|
@@ -59,46 +59,47 @@ export function determineValidTransactions(
|
|
|
59
59
|
throw new Error("Group must be a map");
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
62
|
+
const validTransactions: ValidTransactionsResult[] = [];
|
|
63
|
+
|
|
64
|
+
for (const [sessionID, sessionLog] of coValue.sessionLogs.entries()) {
|
|
65
|
+
const transactor = accountOrAgentIDfromSessionID(sessionID);
|
|
66
|
+
|
|
67
|
+
sessionLog.transactions.forEach((tx, txIndex) => {
|
|
68
|
+
const groupAtTime = groupContent.atTime(tx.madeAt);
|
|
69
|
+
const effectiveTransactor = agentInAccountOrMemberInGroup(
|
|
70
|
+
transactor,
|
|
71
|
+
groupAtTime,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (!effectiveTransactor) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const transactorRoleAtTxTime =
|
|
79
|
+
groupAtTime.roleOfInternal(effectiveTransactor)?.role ||
|
|
80
|
+
groupAtTime.roleOfInternal(EVERYONE)?.role;
|
|
81
|
+
|
|
82
|
+
if (
|
|
83
|
+
transactorRoleAtTxTime !== "admin" &&
|
|
84
|
+
transactorRoleAtTxTime !== "writer"
|
|
85
|
+
) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return validTransactions;
|
|
93
94
|
} else if (coValue.header.ruleset.type === "unsafeAllowAll") {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
95
|
+
const validTransactions: ValidTransactionsResult[] = [];
|
|
96
|
+
|
|
97
|
+
for (const [sessionID, sessionLog] of coValue.sessionLogs.entries()) {
|
|
98
|
+
sessionLog.transactions.forEach((tx, txIndex) => {
|
|
99
|
+
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return validTransactions;
|
|
102
103
|
} else {
|
|
103
104
|
throw new Error(
|
|
104
105
|
"Unknown ruleset type " +
|
package/src/tests/coMap.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { expect, test } from "vitest";
|
|
2
2
|
import { expectMap } from "../coValue.js";
|
|
3
|
+
import { operationToEditEntry } from "../coValues/coMap.js";
|
|
3
4
|
import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
|
4
5
|
import { LocalNode } from "../localNode.js";
|
|
5
6
|
import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
|
|
@@ -82,6 +83,29 @@ test("Can get CoMap entry values at different points in time", () => {
|
|
|
82
83
|
expect(content.atTime(beforeA).get("hello")).toEqual(undefined);
|
|
83
84
|
expect(content.atTime(beforeB).get("hello")).toEqual("A");
|
|
84
85
|
expect(content.atTime(beforeC).get("hello")).toEqual("B");
|
|
86
|
+
|
|
87
|
+
const ops = content.timeFilteredOps("hello");
|
|
88
|
+
|
|
89
|
+
expect(content.atTime(beforeC).lastEditAt("hello")).toEqual(
|
|
90
|
+
operationToEditEntry(ops![1]!),
|
|
91
|
+
);
|
|
92
|
+
expect(content.atTime(beforeC).nthEditAt("hello", 0)).toEqual(
|
|
93
|
+
operationToEditEntry(ops![0]!),
|
|
94
|
+
);
|
|
95
|
+
expect(content.atTime(beforeC).nthEditAt("hello", 2)).toEqual(undefined);
|
|
96
|
+
|
|
97
|
+
expect([...content.atTime(beforeC).editsAt("hello")]).toEqual([
|
|
98
|
+
operationToEditEntry(ops![0]!),
|
|
99
|
+
operationToEditEntry(ops![1]!),
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
expect(content.atTime(beforeB).asObject()).toEqual({
|
|
103
|
+
hello: "A",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(content.atTime(beforeC).asObject()).toEqual({
|
|
107
|
+
hello: "B",
|
|
108
|
+
});
|
|
85
109
|
});
|
|
86
110
|
|
|
87
111
|
test("Can get all historic values of key in CoMap", () => {
|