cojson 0.1.11 → 0.2.0

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.
Files changed (56) hide show
  1. package/dist/base64url.js +15 -2
  2. package/dist/base64url.js.map +1 -1
  3. package/dist/coValue.d.ts +1 -1
  4. package/dist/coValueCore.d.ts +9 -3
  5. package/dist/coValueCore.js +100 -7
  6. package/dist/coValueCore.js.map +1 -1
  7. package/dist/coValues/coList.js +2 -1
  8. package/dist/coValues/coList.js.map +1 -1
  9. package/dist/coValues/coMap.d.ts +7 -12
  10. package/dist/coValues/coMap.js +2 -1
  11. package/dist/coValues/coMap.js.map +1 -1
  12. package/dist/coValues/coStream.d.ts +11 -2
  13. package/dist/coValues/coStream.js +59 -15
  14. package/dist/coValues/coStream.js.map +1 -1
  15. package/dist/crypto.d.ts +11 -3
  16. package/dist/crypto.js +51 -16
  17. package/dist/crypto.js.map +1 -1
  18. package/dist/group.d.ts +1 -1
  19. package/dist/index.d.ts +6 -3
  20. package/dist/index.js +5 -3
  21. package/dist/index.js.map +1 -1
  22. package/dist/jsonStringify.d.ts +6 -0
  23. package/dist/jsonStringify.js +57 -0
  24. package/dist/jsonStringify.js.map +1 -0
  25. package/dist/jsonValue.d.ts +1 -1
  26. package/dist/media.d.ts +8 -0
  27. package/dist/media.js +2 -0
  28. package/dist/media.js.map +1 -0
  29. package/dist/node.js +1 -1
  30. package/dist/permissions.js +4 -2
  31. package/dist/permissions.js.map +1 -1
  32. package/dist/sync.js +8 -2
  33. package/dist/sync.js.map +1 -1
  34. package/package.json +3 -4
  35. package/src/account.test.ts +5 -0
  36. package/src/base64url.ts +16 -6
  37. package/src/coValue.test.ts +28 -0
  38. package/src/coValue.ts +1 -1
  39. package/src/coValueCore.test.ts +16 -10
  40. package/src/coValueCore.ts +148 -9
  41. package/src/coValues/coList.ts +2 -1
  42. package/src/coValues/coMap.ts +11 -12
  43. package/src/coValues/coStream.ts +83 -21
  44. package/src/crypto.test.ts +5 -0
  45. package/src/crypto.ts +68 -19
  46. package/src/group.test.ts +5 -1
  47. package/src/group.ts +1 -1
  48. package/src/index.ts +7 -1
  49. package/src/jsonStringify.ts +66 -0
  50. package/src/jsonValue.ts +1 -1
  51. package/src/media.ts +9 -0
  52. package/src/node.ts +1 -1
  53. package/src/permissions.test.ts +5 -1
  54. package/src/permissions.ts +5 -2
  55. package/src/sync.test.ts +22 -24
  56. package/src/sync.ts +16 -2
@@ -1,9 +1,12 @@
1
1
  import { JsonObject, JsonValue } from "../jsonValue.js";
2
2
  import { CoID, ReadableCoValue, WriteableCoValue } from "../coValue.js";
3
- import { CoValueCore } from "../coValueCore.js";
3
+ import { CoValueCore, accountOrAgentIDfromSessionID } from "../coValueCore.js";
4
4
  import { Group } from "../group.js";
5
5
  import { SessionID } from "../ids.js";
6
6
  import { base64URLtoBytes, bytesToBase64url } from "../base64url.js";
7
+ import { AccountID } from "../index.js";
8
+ import { isAccountID } from "../account.js";
9
+ import { parseJSON } from "../jsonStringify.js";
7
10
 
8
11
  export type BinaryChunkInfo = {
9
12
  mimeType: string;
@@ -17,7 +20,7 @@ export type BinaryStreamStart = {
17
20
 
18
21
  export type BinaryStreamChunk = {
19
22
  type: "chunk";
20
- chunk: `U${string}`;
23
+ chunk: `binary_U${string}`;
21
24
  };
22
25
 
23
26
  export type BinaryStreamEnd = {
@@ -40,7 +43,7 @@ export class CoStream<
40
43
  type = "costream" as const;
41
44
  core: CoValueCore;
42
45
  items: {
43
- [key: SessionID]: T[];
46
+ [key: SessionID]: {item: T, madeAt: number}[];
44
47
  };
45
48
 
46
49
  constructor(core: CoValueCore) {
@@ -64,16 +67,17 @@ export class CoStream<
64
67
 
65
68
  for (const {
66
69
  txID,
70
+ madeAt,
67
71
  changes,
68
72
  } of this.core.getValidSortedTransactions()) {
69
- for (const changeUntyped of changes) {
73
+ for (const changeUntyped of parseJSON(changes)) {
70
74
  const change = changeUntyped as T;
71
75
  let entries = this.items[txID.sessionID];
72
76
  if (!entries) {
73
77
  entries = [];
74
78
  this.items[txID.sessionID] = entries;
75
79
  }
76
- entries.push(change);
80
+ entries.push({item: change, madeAt});
77
81
  }
78
82
  }
79
83
  }
@@ -87,13 +91,57 @@ export class CoStream<
87
91
  );
88
92
  }
89
93
 
90
- return Object.values(this.items)[0];
94
+ return Object.values(this.items)[0]?.map(item => item.item);
95
+ }
96
+
97
+ getLastItemsPerAccount(): {[account: AccountID]: T | undefined} {
98
+ const result: {[account: AccountID]: {item: T, madeAt: number} | undefined} = {};
99
+
100
+ for (const [sessionID, items] of Object.entries(this.items)) {
101
+ const account = accountOrAgentIDfromSessionID(sessionID as SessionID);
102
+ if (!isAccountID(account)) continue;
103
+ if (items.length > 0) {
104
+ const lastItemOfSession = items[items.length - 1]!;
105
+ if (!result[account] || lastItemOfSession.madeAt > result[account]!.madeAt) {
106
+ result[account] = lastItemOfSession;
107
+ }
108
+ }
109
+ }
110
+
111
+ return Object.fromEntries(Object.entries(result).map(([account, item]) =>
112
+ [account, item?.item]
113
+ ));
114
+ }
115
+
116
+ getLastItemFrom(account: AccountID): T | undefined {
117
+ let lastItem: {item: T, madeAt: number} | undefined;
118
+
119
+ for (const [sessionID, items] of Object.entries(this.items)) {
120
+ if (sessionID.startsWith(account)) {
121
+ if (items.length > 0) {
122
+ const lastItemOfSession = items[items.length - 1]!;
123
+ if (!lastItem || lastItemOfSession.madeAt > lastItem.madeAt) {
124
+ lastItem = lastItemOfSession;
125
+ }
126
+ }
127
+ }
128
+ }
129
+
130
+ return lastItem?.item;
131
+ }
132
+
133
+ getLastItemFromMe(): T | undefined {
134
+ const myAccountID = this.core.node.account.id;
135
+ if (!isAccountID(myAccountID)) return undefined;
136
+ return this.getLastItemFrom(myAccountID);
91
137
  }
92
138
 
93
139
  toJSON(): {
94
140
  [key: SessionID]: T[];
95
141
  } {
96
- return this.items;
142
+ return Object.fromEntries(Object.entries(this.items).map(([sessionID, items]) =>
143
+ [sessionID, items.map(item => item.item)]
144
+ ));
97
145
  }
98
146
 
99
147
  subscribe(listener: (coMap: CoStream<T, Meta>) => void): () => void {
@@ -111,6 +159,8 @@ export class CoStream<
111
159
  }
112
160
  }
113
161
 
162
+ const binary_U_prefixLength = 8; // "binary_U".length;
163
+
114
164
  export class BinaryCoStream<
115
165
  Meta extends BinaryCoStreamMeta = { type: "binary" }
116
166
  >
@@ -122,6 +172,7 @@ export class BinaryCoStream<
122
172
  getBinaryChunks():
123
173
  | (BinaryChunkInfo & { chunks: Uint8Array[]; finished: boolean })
124
174
  | undefined {
175
+ const before = performance.now();
125
176
  const items = this.getSingleStream();
126
177
 
127
178
  if (!items) return;
@@ -135,15 +186,13 @@ export class BinaryCoStream<
135
186
 
136
187
  const chunks: Uint8Array[] = [];
137
188
 
189
+ let finished = false;
190
+ let totalLength = 0;
191
+
138
192
  for (const item of items.slice(1)) {
139
193
  if (item.type === "end") {
140
- return {
141
- mimeType: start.mimeType,
142
- fileName: start.fileName,
143
- totalSizeBytes: start.totalSizeBytes,
144
- chunks,
145
- finished: true,
146
- };
194
+ finished = true;
195
+ break;
147
196
  }
148
197
 
149
198
  if (item.type !== "chunk") {
@@ -151,15 +200,25 @@ export class BinaryCoStream<
151
200
  return undefined;
152
201
  }
153
202
 
154
- chunks.push(base64URLtoBytes(item.chunk.slice(1)));
203
+ const chunk = base64URLtoBytes(
204
+ item.chunk.slice(binary_U_prefixLength)
205
+ );
206
+ totalLength += chunk.length;
207
+ chunks.push(chunk);
155
208
  }
156
209
 
210
+ const after = performance.now();
211
+ console.log(
212
+ "getBinaryChunks bandwidth in MB/s",
213
+ (1000 * totalLength) / (after - before) / (1024 * 1024)
214
+ );
215
+
157
216
  return {
158
217
  mimeType: start.mimeType,
159
218
  fileName: start.fileName,
160
219
  totalSizeBytes: start.totalSizeBytes,
161
220
  chunks,
162
- finished: false,
221
+ finished,
163
222
  };
164
223
  }
165
224
 
@@ -206,10 +265,7 @@ export class WriteableBinaryCoStream<
206
265
  }
207
266
 
208
267
  /** @internal */
209
- push(
210
- item: BinaryStreamItem,
211
- privacy: "private" | "trusting" = "private"
212
- ) {
268
+ push(item: BinaryStreamItem, privacy: "private" | "trusting" = "private") {
213
269
  WriteableCoStream.prototype.push.call(this, item, privacy);
214
270
  }
215
271
 
@@ -230,13 +286,19 @@ export class WriteableBinaryCoStream<
230
286
  chunk: Uint8Array,
231
287
  privacy: "private" | "trusting" = "private"
232
288
  ) {
289
+ const before = performance.now();
233
290
  this.push(
234
291
  {
235
292
  type: "chunk",
236
- chunk: `U${bytesToBase64url(chunk)}`,
293
+ chunk: `binary_U${bytesToBase64url(chunk)}`,
237
294
  } satisfies BinaryStreamChunk,
238
295
  privacy
239
296
  );
297
+ const after = performance.now();
298
+ console.log(
299
+ "pushBinaryStreamChunk bandwidth in MB/s",
300
+ (1000 * chunk.length) / (after - before) / (1024 * 1024)
301
+ );
240
302
  }
241
303
 
242
304
  endBinaryStream(privacy: "private" | "trusting" = "private") {
@@ -21,6 +21,11 @@ import { xsalsa20_poly1305 } from "@noble/ciphers/salsa";
21
21
  import { blake3 } from "@noble/hashes/blake3";
22
22
  import stableStringify from "fast-json-stable-stringify";
23
23
  import { SessionID } from './ids.js';
24
+ import { cojsonReady } from './index.js';
25
+
26
+ beforeEach(async () => {
27
+ await cojsonReady;
28
+ });
24
29
 
25
30
  test("Signatures round-trip and use stable stringify", () => {
26
31
  const data = { b: "world", a: "hello" };
package/src/crypto.ts CHANGED
@@ -2,12 +2,39 @@ import { ed25519, x25519 } from "@noble/curves/ed25519";
2
2
  import { xsalsa20_poly1305, xsalsa20 } from "@noble/ciphers/salsa";
3
3
  import { JsonValue } from "./jsonValue.js";
4
4
  import { base58 } from "@scure/base";
5
- import stableStringify from "fast-json-stable-stringify";
6
- import { blake3 } from "@noble/hashes/blake3";
7
5
  import { randomBytes } from "@noble/ciphers/webcrypto/utils";
8
6
  import { AgentID, RawCoID, TransactionID } from "./ids.js";
9
7
  import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
10
8
 
9
+ import { createBLAKE3 } from 'hash-wasm';
10
+ import { Stringified, parseJSON, stableStringify } from "./jsonStringify.js";
11
+
12
+ let blake3Instance: Awaited<ReturnType<typeof createBLAKE3>>;
13
+ let blake3HashOnce: (data: Uint8Array) => Uint8Array;
14
+ let blake3HashOnceWithContext: (data: Uint8Array, {context}: {context: Uint8Array}) => Uint8Array;
15
+ let blake3incrementalUpdateSLOW_WITH_DEVTOOLS: (state: Uint8Array, data: Uint8Array) => Uint8Array;
16
+ let blake3digestForState: (state: Uint8Array) => Uint8Array;
17
+
18
+ export const cryptoReady = new Promise<void>((resolve) => {
19
+ createBLAKE3().then(bl3 => {
20
+ blake3Instance = bl3;
21
+ blake3HashOnce = (data) => {
22
+ return bl3.init().update(data).digest('binary');
23
+ }
24
+ blake3HashOnceWithContext = (data, {context}) => {
25
+ return bl3.init().update(context).update(data).digest('binary');
26
+ }
27
+ blake3incrementalUpdateSLOW_WITH_DEVTOOLS = (state, data) => {
28
+ bl3.load(state).update(data);
29
+ return bl3.save();
30
+ }
31
+ blake3digestForState = (state) => {
32
+ return bl3.load(state).digest('binary');
33
+ }
34
+ resolve();
35
+ })
36
+ });
37
+
11
38
  export type SignerSecret = `signerSecret_z${string}`;
12
39
  export type SignerID = `signer_z${string}`;
13
40
  export type Signature = `signature_z${string}`;
@@ -128,7 +155,7 @@ export function seal<T extends JsonValue>(
128
155
  to: SealerID,
129
156
  nOnceMaterial: { in: RawCoID; tx: TransactionID }
130
157
  ): Sealed<T> {
131
- const nOnce = blake3(
158
+ const nOnce = blake3HashOnce(
132
159
  textEncoder.encode(stableStringify(nOnceMaterial))
133
160
  ).slice(0, 24);
134
161
 
@@ -153,7 +180,7 @@ export function unseal<T extends JsonValue>(
153
180
  from: SealerID,
154
181
  nOnceMaterial: { in: RawCoID; tx: TransactionID }
155
182
  ): T | undefined {
156
- const nOnce = blake3(
183
+ const nOnce = blake3HashOnce(
157
184
  textEncoder.encode(stableStringify(nOnceMaterial))
158
185
  ).slice(0, 24);
159
186
 
@@ -181,28 +208,32 @@ export type Hash = `hash_z${string}`;
181
208
 
182
209
  export function secureHash(value: JsonValue): Hash {
183
210
  return `hash_z${base58.encode(
184
- blake3(textEncoder.encode(stableStringify(value)))
211
+ blake3HashOnce(textEncoder.encode(stableStringify(value)))
185
212
  )}`;
186
213
  }
187
214
 
188
215
  export class StreamingHash {
189
- state: ReturnType<typeof blake3.create>;
216
+ state: Uint8Array;
190
217
 
191
- constructor(fromClone?: ReturnType<typeof blake3.create>) {
192
- this.state = fromClone || blake3.create({});
218
+ constructor(fromClone?: Uint8Array) {
219
+ this.state = fromClone || blake3Instance.init().save();
193
220
  }
194
221
 
195
222
  update(value: JsonValue) {
196
- this.state.update(textEncoder.encode(stableStringify(value)));
223
+ const encoded = textEncoder.encode(stableStringify(value))
224
+ // const before = performance.now();
225
+ this.state = blake3incrementalUpdateSLOW_WITH_DEVTOOLS(this.state, encoded);
226
+ // const after = performance.now();
227
+ // console.log(`Hashing throughput in MB/s`, 1000 * (encoded.length / (after - before)) / (1024 * 1024));
197
228
  }
198
229
 
199
230
  digest(): Hash {
200
- const hash = this.state.digest();
231
+ const hash = blake3digestForState(this.state);
201
232
  return `hash_z${base58.encode(hash)}`;
202
233
  }
203
234
 
204
235
  clone(): StreamingHash {
205
- return new StreamingHash(this.state.clone());
236
+ return new StreamingHash(new Uint8Array(this.state));
206
237
  }
207
238
  }
208
239
 
@@ -211,7 +242,7 @@ export const shortHashLength = 19;
211
242
 
212
243
  export function shortHash(value: JsonValue): ShortHash {
213
244
  return `shortHash_z${base58.encode(
214
- blake3(textEncoder.encode(stableStringify(value))).slice(
245
+ blake3HashOnce(textEncoder.encode(stableStringify(value))).slice(
215
246
  0,
216
247
  shortHashLength
217
248
  )
@@ -241,7 +272,7 @@ function encrypt<T extends JsonValue, N extends JsonValue>(
241
272
  const keySecretBytes = base58.decode(
242
273
  keySecret.substring("keySecret_z".length)
243
274
  );
244
- const nOnce = blake3(
275
+ const nOnce = blake3HashOnce(
245
276
  textEncoder.encode(stableStringify(nOnceMaterial))
246
277
  ).slice(0, 24);
247
278
 
@@ -285,15 +316,15 @@ export function encryptKeySecret(keys: {
285
316
  };
286
317
  }
287
318
 
288
- function decrypt<T extends JsonValue, N extends JsonValue>(
319
+ function decryptRaw<T extends JsonValue, N extends JsonValue>(
289
320
  encrypted: Encrypted<T, N>,
290
321
  keySecret: KeySecret,
291
322
  nOnceMaterial: N
292
- ): T | undefined {
323
+ ): Stringified<T> {
293
324
  const keySecretBytes = base58.decode(
294
325
  keySecret.substring("keySecret_z".length)
295
326
  );
296
- const nOnce = blake3(
327
+ const nOnce = blake3HashOnce(
297
328
  textEncoder.encode(stableStringify(nOnceMaterial))
298
329
  ).slice(0, 24);
299
330
 
@@ -302,13 +333,31 @@ function decrypt<T extends JsonValue, N extends JsonValue>(
302
333
  );
303
334
  const plaintext = xsalsa20(keySecretBytes, nOnce, ciphertext);
304
335
 
336
+ return textDecoder.decode(plaintext) as Stringified<T>;
337
+
338
+ }
339
+
340
+ function decrypt<T extends JsonValue, N extends JsonValue>(
341
+ encrypted: Encrypted<T, N>,
342
+ keySecret: KeySecret,
343
+ nOnceMaterial: N
344
+ ): T | undefined {
305
345
  try {
306
- return JSON.parse(textDecoder.decode(plaintext));
346
+ return parseJSON(decryptRaw(encrypted, keySecret, nOnceMaterial));
307
347
  } catch (e) {
348
+ console.error("Decryption error", e)
308
349
  return undefined;
309
350
  }
310
351
  }
311
352
 
353
+ export function decryptRawForTransaction<T extends JsonValue>(
354
+ encrypted: Encrypted<T, { in: RawCoID; tx: TransactionID }>,
355
+ keySecret: KeySecret,
356
+ nOnceMaterial: { in: RawCoID; tx: TransactionID }
357
+ ): Stringified<T> | undefined {
358
+ return decryptRaw(encrypted, keySecret, nOnceMaterial);
359
+ }
360
+
312
361
  export function decryptForTransaction<T extends JsonValue>(
313
362
  encrypted: Encrypted<T, { in: RawCoID; tx: TransactionID }>,
314
363
  keySecret: KeySecret,
@@ -365,11 +414,11 @@ export function agentSecretFromSecretSeed(secretSeed: Uint8Array): AgentSecret {
365
414
  }
366
415
 
367
416
  return `sealerSecret_z${base58.encode(
368
- blake3(secretSeed, {
417
+ blake3HashOnceWithContext(secretSeed, {
369
418
  context: textEncoder.encode("seal"),
370
419
  })
371
420
  )}/signerSecret_z${base58.encode(
372
- blake3(secretSeed, {
421
+ blake3HashOnceWithContext(secretSeed, {
373
422
  context: textEncoder.encode("sign"),
374
423
  })
375
424
  )}`;
package/src/group.test.ts CHANGED
@@ -1,6 +1,10 @@
1
- import { LocalNode, CoMap, CoList, CoStream, BinaryCoStream } from "./index";
1
+ import { LocalNode, CoMap, CoList, CoStream, BinaryCoStream, cojsonReady } from "./index";
2
2
  import { randomAnonymousAccountAndSessionID } from "./testUtils";
3
3
 
4
+ beforeEach(async () => {
5
+ await cojsonReady;
6
+ });
7
+
4
8
  test("Can create a CoMap in a group", () => {
5
9
  const node = new LocalNode(...randomAnonymousAccountAndSessionID());
6
10
 
package/src/group.ts CHANGED
@@ -238,7 +238,7 @@ export class Group {
238
238
 
239
239
  /** Creates a new `CoMap` within this group, with the specified specialized
240
240
  * `CoMap` type `M` and optional static metadata. */
241
- createMap<M extends CoMap<{ [key: string]: JsonValue }, JsonObject | null>>(
241
+ createMap<M extends CoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>>(
242
242
  meta?: M["meta"]
243
243
  ): M {
244
244
  return this.node
package/src/index.ts CHANGED
@@ -18,12 +18,14 @@ import {
18
18
  agentSecretFromSecretSeed,
19
19
  secretSeedLength,
20
20
  shortHashLength,
21
+ cryptoReady
21
22
  } from "./crypto.js";
22
23
  import { connectedPeers } from "./streamUtils.js";
23
24
  import { AnonymousControlledAccount, ControlledAccount } from "./account.js";
24
25
  import { rawCoIDtoBytes, rawCoIDfromBytes } from "./ids.js";
25
26
  import { Group, expectGroupContent } from "./group.js";
26
27
  import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
28
+ import { parseJSON } from "./jsonStringify.js";
27
29
 
28
30
  import type { SessionID, AgentID } from "./ids.js";
29
31
  import type { CoID, CoValueImpl } from "./coValue.js";
@@ -33,6 +35,7 @@ import type { SyncMessage, Peer } from "./sync.js";
33
35
  import type { AgentSecret } from "./crypto.js";
34
36
  import type { AccountID, Profile } from "./account.js";
35
37
  import type { InviteSecret } from "./group.js";
38
+ import type * as Media from "./media.js";
36
39
 
37
40
  type Value = JsonValue | CoValueImpl;
38
41
 
@@ -52,7 +55,8 @@ export const cojsonInternals = {
52
55
  shortHashLength,
53
56
  expectGroupContent,
54
57
  base64URLtoBytes,
55
- bytesToBase64url
58
+ bytesToBase64url,
59
+ parseJSON
56
60
  };
57
61
 
58
62
  export {
@@ -69,6 +73,7 @@ export {
69
73
  CoValueCore,
70
74
  AnonymousControlledAccount,
71
75
  ControlledAccount,
76
+ cryptoReady as cojsonReady,
72
77
  };
73
78
 
74
79
  export type {
@@ -88,6 +93,7 @@ export type {
88
93
  AgentSecret,
89
94
  InviteSecret,
90
95
  SyncMessage,
96
+ Media
91
97
  };
92
98
 
93
99
  // eslint-disable-next-line @typescript-eslint/no-namespace
@@ -0,0 +1,66 @@
1
+ // adapted from fast-json-stable-stringify (https://github.com/epoberezkin/fast-json-stable-stringify)
2
+
3
+ export type Stringified<T> = string & { __type: T };
4
+
5
+ export function stableStringify<T>(data: T): Stringified<T>
6
+ export function stableStringify(data: undefined): undefined
7
+ export function stableStringify<T>(data: T | undefined): Stringified<T> | undefined {
8
+ const cycles = false;
9
+
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ const seen: any[] = [];
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ let node = data as any;
14
+
15
+ if (node && node.toJSON && typeof node.toJSON === "function") {
16
+ node = node.toJSON();
17
+ }
18
+
19
+ if (node === undefined) return;
20
+ if (typeof node == "number")
21
+ return (isFinite(node) ? "" + node : "null") as Stringified<T>;
22
+ if (typeof node !== "object") {
23
+ if (
24
+ typeof node === "string" &&
25
+ (node.startsWith("encrypted_U") || node.startsWith("binary_U"))
26
+ ) {
27
+ return `"${node}"` as Stringified<T>;
28
+ }
29
+ return JSON.stringify(node) as Stringified<T>;
30
+ }
31
+
32
+ let i, out;
33
+ if (Array.isArray(node)) {
34
+ out = "[";
35
+ for (i = 0; i < node.length; i++) {
36
+ if (i) out += ",";
37
+ out += stableStringify(node[i]) || "null";
38
+ }
39
+ return (out + "]") as Stringified<T>;
40
+ }
41
+
42
+ if (node === null) return "null" as Stringified<T>;
43
+
44
+ if (seen.indexOf(node) !== -1) {
45
+ if (cycles) return JSON.stringify("__cycle__") as Stringified<T>;
46
+ throw new TypeError("Converting circular structure to JSON");
47
+ }
48
+
49
+ const seenIndex = seen.push(node) - 1;
50
+ const keys = Object.keys(node).sort();
51
+ out = "";
52
+ for (i = 0; i < keys.length; i++) {
53
+ const key = keys[i]!;
54
+ const value = stableStringify(node[key]);
55
+
56
+ if (!value) continue;
57
+ if (out) out += ",";
58
+ out += JSON.stringify(key) + ":" + value;
59
+ }
60
+ seen.splice(seenIndex, 1);
61
+ return ("{" + out + "}") as Stringified<T>;
62
+ }
63
+
64
+ export function parseJSON<T>(json: Stringified<T>): T {
65
+ return JSON.parse(json);
66
+ }
package/src/jsonValue.ts CHANGED
@@ -3,4 +3,4 @@ import { RawCoID } from './ids.js';
3
3
  export type JsonAtom = string | number | boolean | null;
4
4
  export type JsonValue = JsonAtom | JsonArray | JsonObject | RawCoID;
5
5
  export type JsonArray = JsonValue[];
6
- export type JsonObject = { [key: string]: JsonValue; };
6
+ export type JsonObject = { [key: string]: JsonValue | undefined; };
package/src/media.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { CoMap } from './coValues/coMap.js'
2
+ import { CoID } from './coValue.js'
3
+ import { BinaryCoStream } from './coValues/coStream.js'
4
+
5
+ export type ImageDefinition = CoMap<{
6
+ originalSize: [number, number];
7
+ placeholderDataURL?: string;
8
+ [res: `${number}x${number}`]: CoID<BinaryCoStream>;
9
+ }>;
package/src/node.ts CHANGED
@@ -208,7 +208,7 @@ export class LocalNode {
208
208
  reject(
209
209
  new Error("Couldn't find invite before timeout")
210
210
  ),
211
- 1000
211
+ 2000
212
212
  );
213
213
  });
214
214
 
@@ -17,7 +17,11 @@ import {
17
17
  groupWithTwoAdmins,
18
18
  groupWithTwoAdminsHighLevel,
19
19
  } from "./testUtils.js";
20
- import { AnonymousControlledAccount } from "./index.js";
20
+ import { AnonymousControlledAccount, cojsonReady } from "./index.js";
21
+
22
+ beforeEach(async () => {
23
+ await cojsonReady;
24
+ });
21
25
 
22
26
  test("Initial admin can add another admin to a group", () => {
23
27
  groupWithTwoAdmins();
@@ -15,6 +15,7 @@ import {
15
15
  AccountID,
16
16
  Profile,
17
17
  } from "./account.js";
18
+ import { parseJSON } from "./jsonStringify.js";
18
19
 
19
20
  export type PermissionsDef =
20
21
  | { type: "group"; initialAdmin: AccountID | AgentID }
@@ -76,11 +77,13 @@ export function determineValidTransactions(
76
77
  // console.log("before", { memberState, validTransactions });
77
78
  const transactor = accountOrAgentIDfromSessionID(sessionID);
78
79
 
79
- const change = tx.changes[0] as
80
+ const changes = parseJSON(tx.changes)
81
+
82
+ const change = changes[0] as
80
83
  | MapOpPayload<AccountID | AgentID, Role>
81
84
  | MapOpPayload<"readKey", JsonValue>
82
85
  | MapOpPayload<"profile", CoID<Profile>>;
83
- if (tx.changes.length !== 1) {
86
+ if (changes.length !== 1) {
84
87
  console.warn("Group transaction must have exactly one change");
85
88
  continue;
86
89
  }