cojson 0.13.28 โ†’ 0.13.30

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 (44) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +14 -0
  3. package/dist/coValueCore/coValueCore.d.ts +1 -0
  4. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  5. package/dist/coValueCore/coValueCore.js +17 -2
  6. package/dist/coValueCore/coValueCore.js.map +1 -1
  7. package/dist/coValues/coPlainText.d.ts +0 -1
  8. package/dist/coValues/coPlainText.d.ts.map +1 -1
  9. package/dist/coValues/coPlainText.js +10 -10
  10. package/dist/coValues/coPlainText.js.map +1 -1
  11. package/dist/coValues/group.d.ts.map +1 -1
  12. package/dist/coValues/group.js +1 -1
  13. package/dist/coValues/group.js.map +1 -1
  14. package/dist/crypto/PureJSCrypto.d.ts +1 -1
  15. package/dist/crypto/PureJSCrypto.js +3 -3
  16. package/dist/crypto/PureJSCrypto.js.map +1 -1
  17. package/dist/exports.d.ts +1 -0
  18. package/dist/exports.d.ts.map +1 -1
  19. package/dist/exports.js +3 -0
  20. package/dist/exports.js.map +1 -1
  21. package/dist/localNode.d.ts.map +1 -1
  22. package/dist/localNode.js +7 -1
  23. package/dist/localNode.js.map +1 -1
  24. package/dist/tests/coPlainText.test.js +68 -10
  25. package/dist/tests/coPlainText.test.js.map +1 -1
  26. package/dist/tests/crypto.test.js +5 -5
  27. package/dist/tests/crypto.test.js.map +1 -1
  28. package/dist/tests/group.addMember.test.d.ts +2 -0
  29. package/dist/tests/group.addMember.test.d.ts.map +1 -0
  30. package/dist/tests/group.addMember.test.js +272 -0
  31. package/dist/tests/group.addMember.test.js.map +1 -0
  32. package/dist/tests/testUtils.d.ts +1 -1
  33. package/dist/tests/testUtils.d.ts.map +1 -1
  34. package/package.json +6 -5
  35. package/src/coValueCore/coValueCore.ts +21 -2
  36. package/src/coValues/coPlainText.ts +10 -19
  37. package/src/coValues/group.ts +3 -6
  38. package/src/crypto/PureJSCrypto.ts +3 -3
  39. package/src/exports.ts +3 -0
  40. package/src/localNode.ts +11 -3
  41. package/src/tests/coPlainText.test.ts +79 -18
  42. package/src/tests/crypto.test.ts +5 -5
  43. package/src/tests/group.addMember.test.ts +432 -0
  44. package/src/tests/testUtils.ts +6 -6
package/package.json CHANGED
@@ -25,20 +25,21 @@
25
25
  },
26
26
  "type": "module",
27
27
  "license": "MIT",
28
- "version": "0.13.28",
28
+ "version": "0.13.30",
29
29
  "devDependencies": {
30
30
  "@opentelemetry/sdk-metrics": "^2.0.0",
31
31
  "typescript": "5.6.2"
32
32
  },
33
33
  "dependencies": {
34
- "@noble/ciphers": "^0.1.3",
35
- "@noble/curves": "^1.3.0",
36
- "@noble/hashes": "^1.4.0",
34
+ "@noble/ciphers": "^1.3.0",
35
+ "@noble/curves": "^1.9.1",
36
+ "@noble/hashes": "^1.8.0",
37
37
  "@opentelemetry/api": "^1.9.0",
38
38
  "@scure/base": "1.2.1",
39
39
  "jazz-crypto-rs": "0.0.7",
40
40
  "neverthrow": "^7.0.1",
41
- "queueueue": "^4.1.2"
41
+ "queueueue": "^4.1.2",
42
+ "unicode-segmenter": "^0.12.0"
42
43
  },
43
44
  "gitHead": "33c27053293b4801b968c61d5c4c989f93a67d13",
44
45
  "scripts": {
@@ -67,7 +67,7 @@ export type AvailableCoValueCore = CoValueCore & { verified: VerifiedState };
67
67
  export const CO_VALUE_LOADING_CONFIG = {
68
68
  MAX_RETRIES: 1,
69
69
  TIMEOUT: 30_000,
70
- RETRY_DELAY: 300,
70
+ RETRY_DELAY: 3000,
71
71
  };
72
72
 
73
73
  export class CoValueCore {
@@ -193,6 +193,20 @@ export class CoValueCore {
193
193
  });
194
194
  }
195
195
 
196
+ waitForAvailable(): Promise<CoValueCore> {
197
+ return new Promise<CoValueCore>((resolve) => {
198
+ const listener = (core: CoValueCore) => {
199
+ if (core.isAvailable()) {
200
+ resolve(core);
201
+ this.listeners.delete(listener);
202
+ }
203
+ };
204
+
205
+ this.listeners.add(listener);
206
+ listener(this);
207
+ });
208
+ }
209
+
196
210
  getStateForPeer(peerId: PeerID) {
197
211
  return this.peers.get(peerId);
198
212
  }
@@ -1030,6 +1044,11 @@ export class CoValueCore {
1030
1044
  });
1031
1045
  peer.trackLoadRequestSent(this.id);
1032
1046
 
1047
+ const timeoutDuration =
1048
+ peer.role === "storage"
1049
+ ? CO_VALUE_LOADING_CONFIG.TIMEOUT * 10
1050
+ : CO_VALUE_LOADING_CONFIG.TIMEOUT;
1051
+
1033
1052
  return new Promise<void>((resolve) => {
1034
1053
  const markNotFound = () => {
1035
1054
  if (this.peers.get(peer.id)?.type === "pending") {
@@ -1041,7 +1060,7 @@ export class CoValueCore {
1041
1060
  }
1042
1061
  };
1043
1062
 
1044
- const timeout = setTimeout(markNotFound, CO_VALUE_LOADING_CONFIG.TIMEOUT);
1063
+ const timeout = setTimeout(markNotFound, timeoutDuration);
1045
1064
  const removeCloseListener = peer.addCloseListener(markNotFound);
1046
1065
 
1047
1066
  const listener = (state: CoValueCore) => {
@@ -1,7 +1,5 @@
1
- import {
2
- AvailableCoValueCore,
3
- CoValueCore,
4
- } from "../coValueCore/coValueCore.js";
1
+ import { splitGraphemes } from "unicode-segmenter/grapheme";
2
+ import { AvailableCoValueCore } from "../coValueCore/coValueCore.js";
5
3
  import { JsonObject } from "../jsonValue.js";
6
4
  import { DeletionOpPayload, OpID, RawCoList } from "./coList.js";
7
5
 
@@ -57,8 +55,6 @@ export class RawCoPlainText<
57
55
  /** @category 6. Meta */
58
56
  type = "coplaintext" as const;
59
57
 
60
- private _segmenter: Intl.Segmenter;
61
-
62
58
  _cachedMapping: WeakMap<
63
59
  NonNullable<typeof this._cachedEntries>,
64
60
  PlaintextIdxMapping
@@ -67,11 +63,6 @@ export class RawCoPlainText<
67
63
  constructor(core: AvailableCoValueCore) {
68
64
  super(core);
69
65
  this._cachedMapping = new WeakMap();
70
- if (!Intl.Segmenter) {
71
- throw new Error(
72
- "Intl.Segmenter is not supported. Use a polyfill to get coPlainText support in Jazz. (eg. https://formatjs.github.io/docs/polyfills/intl-segmenter/)",
73
- );
74
- }
75
66
 
76
67
  // Use locale from meta if provided, fallback to browser locale, or 'en' as last resort
77
68
  const effectiveLocale =
@@ -81,10 +72,6 @@ export class RawCoPlainText<
81
72
  ? (core.verified.header.meta.locale as string)
82
73
  : undefined) ||
83
74
  (typeof navigator !== "undefined" ? navigator.language : "en");
84
-
85
- this._segmenter = new Intl.Segmenter(effectiveLocale, {
86
- granularity: "grapheme",
87
- });
88
75
  }
89
76
 
90
77
  get mapping() {
@@ -104,7 +91,7 @@ export class RawCoPlainText<
104
91
  let idxBefore = 0;
105
92
 
106
93
  for (const entry of entries) {
107
- const idxAfter = idxBefore + entry.value.length;
94
+ const idxAfter = idxBefore + 1;
108
95
 
109
96
  mapping.opIDafterIdx[idxBefore] = entry.opID;
110
97
  mapping.opIDbeforeIdx[idxAfter] = entry.opID;
@@ -138,7 +125,7 @@ export class RawCoPlainText<
138
125
  text: string,
139
126
  privacy: "private" | "trusting" = "private",
140
127
  ) {
141
- const graphemes = [...this._segmenter.segment(text)].map((g) => g.segment);
128
+ const graphemes = [...splitGraphemes(text)];
142
129
 
143
130
  if (idx === 0) {
144
131
  // For insertions at start, prepend each character in reverse
@@ -164,8 +151,12 @@ export class RawCoPlainText<
164
151
  text: string,
165
152
  privacy: "private" | "trusting" = "private",
166
153
  ) {
167
- const graphemes = [...this._segmenter.segment(text)].map((g) => g.segment);
168
- this.appendItems(graphemes, idx, privacy);
154
+ const graphemes = [...splitGraphemes(text)];
155
+ if (idx >= this.entries().length) {
156
+ this.appendItems(graphemes, idx - 1, privacy);
157
+ } else {
158
+ this.appendItems(graphemes, idx, privacy);
159
+ }
169
160
  }
170
161
 
171
162
  deleteRange(
@@ -1,9 +1,6 @@
1
1
  import { base58 } from "@scure/base";
2
2
  import { CoID } from "../coValue.js";
3
- import {
4
- AvailableCoValueCore,
5
- CoValueCore,
6
- } from "../coValueCore/coValueCore.js";
3
+ import { AvailableCoValueCore } from "../coValueCore/coValueCore.js";
7
4
  import { CoValueUniqueness } from "../coValueCore/verifiedState.js";
8
5
  import {
9
6
  CryptoProvider,
@@ -27,7 +24,6 @@ import { logger } from "../logger.js";
27
24
  import { AccountRole, Role } from "../permissions.js";
28
25
  import { expectGroup } from "../typeUtils/expectGroup.js";
29
26
  import {
30
- ControlledAccount,
31
27
  ControlledAccountOrAgent,
32
28
  RawAccount,
33
29
  RawAccountID,
@@ -332,6 +328,8 @@ export class RawGroup<
332
328
  if (role === "writeOnly" || role === "writeOnlyInvite") {
333
329
  const previousRole = this.get(memberKey);
334
330
 
331
+ this.set(memberKey, role, "trusting");
332
+
335
333
  if (
336
334
  previousRole === "reader" ||
337
335
  previousRole === "writer" ||
@@ -340,7 +338,6 @@ export class RawGroup<
340
338
  this.rotateReadKey();
341
339
  }
342
340
 
343
- this.set(memberKey, role, "trusting");
344
341
  this.internalCreateWriteOnlyKeyForMember(memberKey, agent);
345
342
  } else {
346
343
  const currentReadKey = this.core.getCurrentReadKey();
@@ -1,4 +1,4 @@
1
- import { xsalsa20, xsalsa20_poly1305 } from "@noble/ciphers/salsa";
1
+ import { xsalsa20, xsalsa20poly1305 } from "@noble/ciphers/salsa";
2
2
  import { ed25519, x25519 } from "@noble/curves/ed25519";
3
3
  import { blake3 } from "@noble/hashes/blake3";
4
4
  import { base58 } from "@scure/base";
@@ -165,7 +165,7 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
165
165
 
166
166
  const sharedSecret = x25519.getSharedSecret(senderPriv, sealerPub);
167
167
 
168
- const sealedBytes = xsalsa20_poly1305(sharedSecret, nOnce).encrypt(
168
+ const sealedBytes = xsalsa20poly1305(sharedSecret, nOnce).encrypt(
169
169
  plaintext,
170
170
  );
171
171
 
@@ -188,7 +188,7 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
188
188
 
189
189
  const sharedSecret = x25519.getSharedSecret(sealerPriv, senderPub);
190
190
 
191
- const plaintext = xsalsa20_poly1305(sharedSecret, nOnce).decrypt(
191
+ const plaintext = xsalsa20poly1305(sharedSecret, nOnce).decrypt(
192
192
  sealedBytes,
193
193
  );
194
194
 
package/src/exports.ts CHANGED
@@ -103,6 +103,9 @@ export const cojsonInternals = {
103
103
  getGroupDependentKey,
104
104
  disablePermissionErrors,
105
105
  CO_VALUE_LOADING_CONFIG,
106
+ setCoValueLoadingRetryDelay(delay: number) {
107
+ CO_VALUE_LOADING_CONFIG.RETRY_DELAY = delay;
108
+ },
106
109
  };
107
110
 
108
111
  export {
package/src/localNode.ts CHANGED
@@ -345,6 +345,10 @@ export class LocalNode {
345
345
  while (true) {
346
346
  const coValue = this.getCoValue(id);
347
347
 
348
+ if (coValue.isAvailable()) {
349
+ return coValue;
350
+ }
351
+
348
352
  if (
349
353
  coValue.loadingState === "unknown" ||
350
354
  coValue.loadingState === "unavailable"
@@ -365,6 +369,7 @@ export class LocalNode {
365
369
  }
366
370
 
367
371
  const result = await coValue.waitForAvailableOrUnavailable();
372
+
368
373
  if (
369
374
  result.isAvailable() ||
370
375
  retries >= CO_VALUE_LOADING_CONFIG.MAX_RETRIES
@@ -372,9 +377,12 @@ export class LocalNode {
372
377
  return result;
373
378
  }
374
379
 
375
- await new Promise((resolve) =>
376
- setTimeout(resolve, CO_VALUE_LOADING_CONFIG.RETRY_DELAY),
377
- );
380
+ await Promise.race([
381
+ new Promise((resolve) =>
382
+ setTimeout(resolve, CO_VALUE_LOADING_CONFIG.RETRY_DELAY),
383
+ ),
384
+ coValue.waitForAvailable(), // Stop waiting if the coValue becomes available
385
+ ]);
378
386
 
379
387
  retries++;
380
388
  }
@@ -1,28 +1,12 @@
1
1
  import { afterEach, expect, test, vi } from "vitest";
2
2
  import { expectPlainText } from "../coValue.js";
3
3
  import { WasmCrypto } from "../crypto/WasmCrypto.js";
4
- import { LocalNode } from "../localNode.js";
5
- import {
6
- nodeWithRandomAgentAndSessionID,
7
- randomAgentAndSessionID,
8
- } from "./testUtils.js";
4
+ import { nodeWithRandomAgentAndSessionID } from "./testUtils.js";
9
5
 
10
6
  const Crypto = await WasmCrypto.create();
11
7
 
12
8
  afterEach(() => void vi.unstubAllGlobals());
13
9
 
14
- test("should throw on creation if Intl.Segmenter is not available", () => {
15
- vi.stubGlobal("Intl", {
16
- Segmenter: undefined,
17
- });
18
-
19
- const node = nodeWithRandomAgentAndSessionID();
20
- const group = node.createGroup();
21
- expect(() => group.createPlainText()).toThrow(
22
- "Intl.Segmenter is not supported. Use a polyfill to get coPlainText support in Jazz. (eg. https://formatjs.github.io/docs/polyfills/intl-segmenter/)",
23
- );
24
- });
25
-
26
10
  test("Empty CoPlainText works", () => {
27
11
  const node = nodeWithRandomAgentAndSessionID();
28
12
 
@@ -86,7 +70,7 @@ test("Can insert and delete in CoPlainText", () => {
86
70
  content.insertBefore(2, "๐Ÿ˜", "trusting");
87
71
  expect(content.toString()).toEqual("He๐Ÿ˜llo, world");
88
72
 
89
- content.deleteRange({ from: 2, to: 4 }, "trusting");
73
+ content.deleteRange({ from: 2, to: 3 }, "trusting");
90
74
  expect(content.toString()).toEqual("Hello, world");
91
75
  });
92
76
 
@@ -204,3 +188,80 @@ test("insertBefore and insertAfter work as expected", () => {
204
188
  content.insertBefore(0, "!", "trusting"); // "!hey"
205
189
  expect(content.toString()).toEqual("!hey");
206
190
  });
191
+
192
+ test("Can delete a single grapheme", () => {
193
+ const node = nodeWithRandomAgentAndSessionID();
194
+ const coValue = node.createCoValue({
195
+ type: "coplaintext",
196
+ ruleset: { type: "unsafeAllowAll" },
197
+ meta: null,
198
+ ...Crypto.createdNowUnique(),
199
+ });
200
+ const content = expectPlainText(coValue.getCurrentContent());
201
+
202
+ content.insertAfter(0, "aฬeฬoฬˆฬฒ", "trusting"); // 3 graphemes
203
+ content.deleteRange({ from: 1, to: 2 }, "trusting"); // delete the second grapheme
204
+ expect(content.toString()).toEqual("aฬoฬˆฬฒ");
205
+ });
206
+
207
+ test("Handles complex grapheme clusters correctly", () => {
208
+ const node = nodeWithRandomAgentAndSessionID();
209
+ const coValue = node.createCoValue({
210
+ type: "coplaintext",
211
+ ruleset: { type: "unsafeAllowAll" },
212
+ meta: null,
213
+ ...Crypto.createdNowUnique(),
214
+ });
215
+ const content = expectPlainText(coValue.getCurrentContent());
216
+
217
+ // Combining marks (should be treated as one grapheme each)
218
+ const combining = "aฬeฬoฬˆฬฒ"; // 3 graphemes: [aฬ][eฬ][oฬˆฬฒ]
219
+ content.insertAfter(0, combining, "trusting");
220
+ expect(content.toString()).toEqual(combining);
221
+ content.deleteRange({ from: 1, to: 2 }, "trusting");
222
+ expect(content.toString()).toEqual("aฬoฬˆฬฒ");
223
+
224
+ // ZWJ emoji (family)
225
+ const family = "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ"; // 1 grapheme
226
+ content.insertAfter(2, family, "trusting");
227
+ expect(content.toString()).toEqual("aฬoฬˆฬฒ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ");
228
+ content.deleteRange({ from: 2, to: 3 }, "trusting");
229
+ expect(content.toString()).toEqual("aฬoฬˆฬฒ");
230
+
231
+ // Flag emoji (regional indicators)
232
+ const flag = "๐Ÿ‡บ๐Ÿ‡ธ"; // 1 grapheme
233
+ content.insertAfter(2, flag, "trusting");
234
+ expect(content.toString()).toEqual("aฬoฬˆฬฒ๐Ÿ‡บ๐Ÿ‡ธ");
235
+ content.deleteRange({ from: 2, to: 3 }, "trusting");
236
+ expect(content.toString()).toEqual("aฬoฬˆฬฒ");
237
+
238
+ // Emoji with skin tone modifier
239
+ const thumbsUp = "๐Ÿ‘๐Ÿฝ"; // 1 grapheme
240
+ content.insertAfter(2, thumbsUp, "trusting");
241
+ expect(content.toString()).toEqual("aฬoฬˆฬฒ๐Ÿ‘๐Ÿฝ");
242
+ content.deleteRange({ from: 2, to: 3 }, "trusting");
243
+ expect(content.toString()).toEqual("aฬoฬˆฬฒ");
244
+ });
245
+
246
+ test("Handle deletion of complex grapheme clusters correctly", () => {
247
+ const node = nodeWithRandomAgentAndSessionID();
248
+ const coValue = node.createCoValue({
249
+ type: "coplaintext",
250
+ ruleset: { type: "unsafeAllowAll" },
251
+ meta: null,
252
+ ...Crypto.createdNowUnique(),
253
+ });
254
+ const content = expectPlainText(coValue.getCurrentContent());
255
+
256
+ // Combining marks (should be treated as one grapheme each)
257
+ content.insertAfter(0, "๐Ÿ‘‹ ์•ˆ๋…•!", "trusting");
258
+ expect(content.toString()).toEqual("๐Ÿ‘‹ ์•ˆ๋…•!");
259
+
260
+ // Delete the first grapheme
261
+ content.deleteRange({ from: 0, to: 1 }, "trusting");
262
+ expect(content.toString()).toEqual(" ์•ˆ๋…•!");
263
+
264
+ // Delete the second grapheme
265
+ content.deleteRange({ from: 1, to: 2 }, "trusting");
266
+ expect(content.toString()).toEqual(" ๋…•!");
267
+ });
@@ -1,4 +1,4 @@
1
- import { xsalsa20_poly1305 } from "@noble/ciphers/salsa";
1
+ import { xsalsa20poly1305 } from "@noble/ciphers/salsa";
2
2
  import { x25519 } from "@noble/curves/ed25519";
3
3
  import { blake3 } from "@noble/hashes/blake3";
4
4
  import { base58, base64url } from "@scure/base";
@@ -68,7 +68,7 @@ const pureJSCrypto = await PureJSCrypto.create();
68
68
  crypto.getSealerID(sender),
69
69
  nOnceMaterial,
70
70
  ),
71
- ).toThrow(/Wrong tag/);
71
+ ).toThrow(name === "PureJSCrypto" ? "invalid tag" : "Wrong tag");
72
72
 
73
73
  // trying with wrong sealer secret, by hand
74
74
  const nOnce = blake3(
@@ -84,8 +84,8 @@ const pureJSCrypto = await PureJSCrypto.create();
84
84
  const sharedSecret = x25519.getSharedSecret(sealer3priv, senderPub);
85
85
 
86
86
  expect(() => {
87
- const _ = xsalsa20_poly1305(sharedSecret, nOnce).decrypt(sealedBytes);
88
- }).toThrow("Wrong tag");
87
+ const _ = xsalsa20poly1305(sharedSecret, nOnce).decrypt(sealedBytes);
88
+ }).toThrow("invalid tag");
89
89
  });
90
90
 
91
91
  test(`Hashing is deterministic [${name}]`, () => {
@@ -211,7 +211,7 @@ const pureJSCrypto = await PureJSCrypto.create();
211
211
 
212
212
  const plaintext = new TextEncoder().encode(data);
213
213
  const sharedSecret = x25519.getSharedSecret(senderPriv, sealerPub);
214
- const sealedBytes = xsalsa20_poly1305(sharedSecret, nOnce).encrypt(
214
+ const sealedBytes = xsalsa20poly1305(sharedSecret, nOnce).encrypt(
215
215
  plaintext,
216
216
  );
217
217
  const sealed = `sealed_U${base64url.encode(sealedBytes)}`;