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.
@@ -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
- } & MapOpPayload<K, V>;
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
- ops: {
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?: { ignorePrivateTransactions: true },
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.ops = {};
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
- for (const { txID, changes, madeAt } of core.getValidSortedTransactions(
60
- options,
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 = this.ops[change.key];
168
+ let entries = ops[change.key];
68
169
  if (!entries) {
69
170
  entries = [];
70
- this.ops[change.key] = entries;
171
+ ops[change.key] = entries;
71
172
  }
72
173
  entries.push({
73
174
  txID,
74
175
  madeAt,
75
176
  changeIdx,
76
- ...change,
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
- const clone = Object.create(this) as this;
95
- clone.id = this.id;
96
- clone.type = this.type;
97
- clone.core = this.core;
98
- clone.ops = this.ops;
99
- clone.atTimeFilter = time;
100
- return clone;
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
- if (this.atTimeFilter) {
112
- return this.ops[key]?.filter((op) => op.madeAt <= this.atTimeFilter!);
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.ops[key];
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.ops) as K[]).filter((key) => {
124
- const ops = this.ops[key];
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
- const includeUntil = this.atTimeFilter;
130
- const lastEntry = includeUntil
131
- ? ops.findLast((entry) => entry.madeAt <= includeUntil)
132
- : ops[ops.length - 1]!;
239
+ if (!latestChange) {
240
+ return false;
241
+ }
133
242
 
134
- if (lastEntry?.op === "del") {
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 ops = this.ops[key];
149
- if (!ops) {
257
+ const latestChange = this.latest[key];
258
+ if (!latestChange) {
150
259
  return undefined;
151
260
  }
152
261
 
153
- const includeUntil = this.atTimeFilter;
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 lastEntry?.value;
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 this.keys()) {
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: K,
195
- n: number,
196
- ):
197
- | {
198
- by: RawAccountID | AgentID;
199
- tx: TransactionID;
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
- const entry = ops[n]!;
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
- if (!ops || ops.length === 0) {
326
+ const lastEntry = ops?.[ops.length - 1];
327
+
328
+ if (!lastEntry) {
236
329
  return undefined;
237
330
  }
238
- return this.nthEditAt(key, ops.length - 1);
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 (let i = 0; i < ops.length; i++) {
249
- yield this.nthEditAt(key, i)!;
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
- const after = new RawCoMap(this.core) as this;
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
- const after = new RawCoMap(this.core) as this;
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
+ }
@@ -59,46 +59,47 @@ export function determineValidTransactions(
59
59
  throw new Error("Group must be a map");
60
60
  }
61
61
 
62
- return [...coValue.sessionLogs.entries()].flatMap(
63
- ([sessionID, sessionLog]) => {
64
- const transactor = accountOrAgentIDfromSessionID(sessionID);
65
-
66
- return sessionLog.transactions
67
- .filter((tx) => {
68
- const groupAtTime = groupContent.atTime(tx.madeAt);
69
- const effectiveTransactor = agentInAccountOrMemberInGroup(
70
- transactor,
71
- groupAtTime,
72
- );
73
-
74
- if (!effectiveTransactor) {
75
- return false;
76
- }
77
-
78
- const transactorRoleAtTxTime =
79
- groupAtTime.roleOfInternal(effectiveTransactor)?.role ||
80
- groupAtTime.roleOfInternal(EVERYONE)?.role;
81
-
82
- return (
83
- transactorRoleAtTxTime === "admin" ||
84
- transactorRoleAtTxTime === "writer"
85
- );
86
- })
87
- .map((tx, txIndex) => ({
88
- txID: { sessionID: sessionID, txIndex },
89
- tx,
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
- return [...coValue.sessionLogs.entries()].flatMap(
95
- ([sessionID, sessionLog]) => {
96
- return sessionLog.transactions.map((tx, txIndex) => ({
97
- txID: { sessionID: sessionID, txIndex },
98
- tx,
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 " +
@@ -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", () => {