cojson 0.20.7 → 0.20.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.
Files changed (209) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +26 -0
  3. package/dist/SyncStateManager.d.ts.map +1 -1
  4. package/dist/SyncStateManager.js +0 -2
  5. package/dist/SyncStateManager.js.map +1 -1
  6. package/dist/base64url.d.ts +15 -0
  7. package/dist/base64url.d.ts.map +1 -1
  8. package/dist/base64url.js +101 -5
  9. package/dist/base64url.js.map +1 -1
  10. package/dist/base64url.test.js +76 -1
  11. package/dist/base64url.test.js.map +1 -1
  12. package/dist/coValue.d.ts +2 -1
  13. package/dist/coValue.d.ts.map +1 -1
  14. package/dist/coValue.js.map +1 -1
  15. package/dist/coValueCore/coValueCore.d.ts +9 -11
  16. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  17. package/dist/coValueCore/coValueCore.js +92 -65
  18. package/dist/coValueCore/coValueCore.js.map +1 -1
  19. package/dist/coValueCore/verifiedState.d.ts +38 -7
  20. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  21. package/dist/coValueCore/verifiedState.js +226 -30
  22. package/dist/coValueCore/verifiedState.js.map +1 -1
  23. package/dist/coValues/binaryCoStream.d.ts +63 -0
  24. package/dist/coValues/binaryCoStream.d.ts.map +1 -0
  25. package/dist/coValues/binaryCoStream.js +125 -0
  26. package/dist/coValues/binaryCoStream.js.map +1 -0
  27. package/dist/coValues/coList.d.ts +3 -1
  28. package/dist/coValues/coList.d.ts.map +1 -1
  29. package/dist/coValues/coList.js +15 -6
  30. package/dist/coValues/coList.js.map +1 -1
  31. package/dist/coValues/coMap.d.ts +1 -1
  32. package/dist/coValues/coMap.d.ts.map +1 -1
  33. package/dist/coValues/coMap.js +2 -2
  34. package/dist/coValues/coMap.js.map +1 -1
  35. package/dist/coValues/coStream.d.ts +0 -38
  36. package/dist/coValues/coStream.d.ts.map +1 -1
  37. package/dist/coValues/coStream.js +0 -86
  38. package/dist/coValues/coStream.js.map +1 -1
  39. package/dist/coValues/group.d.ts +44 -6
  40. package/dist/coValues/group.d.ts.map +1 -1
  41. package/dist/coValues/group.js +198 -17
  42. package/dist/coValues/group.js.map +1 -1
  43. package/dist/coreToCoValue.d.ts +2 -1
  44. package/dist/coreToCoValue.d.ts.map +1 -1
  45. package/dist/coreToCoValue.js +2 -1
  46. package/dist/coreToCoValue.js.map +1 -1
  47. package/dist/crypto/NapiCrypto.d.ts +18 -24
  48. package/dist/crypto/NapiCrypto.d.ts.map +1 -1
  49. package/dist/crypto/NapiCrypto.js +98 -60
  50. package/dist/crypto/NapiCrypto.js.map +1 -1
  51. package/dist/crypto/RNCrypto.d.ts +16 -3
  52. package/dist/crypto/RNCrypto.d.ts.map +1 -1
  53. package/dist/crypto/RNCrypto.js +117 -54
  54. package/dist/crypto/RNCrypto.js.map +1 -1
  55. package/dist/crypto/WasmCrypto.d.ts +18 -24
  56. package/dist/crypto/WasmCrypto.d.ts.map +1 -1
  57. package/dist/crypto/WasmCrypto.js +100 -61
  58. package/dist/crypto/WasmCrypto.js.map +1 -1
  59. package/dist/crypto/crypto.d.ts +55 -19
  60. package/dist/crypto/crypto.d.ts.map +1 -1
  61. package/dist/crypto/crypto.js +14 -3
  62. package/dist/crypto/crypto.js.map +1 -1
  63. package/dist/exports.d.ts +7 -3
  64. package/dist/exports.d.ts.map +1 -1
  65. package/dist/exports.js +4 -2
  66. package/dist/exports.js.map +1 -1
  67. package/dist/localNode.d.ts +3 -1
  68. package/dist/localNode.d.ts.map +1 -1
  69. package/dist/localNode.js +10 -3
  70. package/dist/localNode.js.map +1 -1
  71. package/dist/media.d.ts +1 -1
  72. package/dist/media.d.ts.map +1 -1
  73. package/dist/permissions.d.ts +2 -1
  74. package/dist/permissions.d.ts.map +1 -1
  75. package/dist/permissions.js +19 -3
  76. package/dist/permissions.js.map +1 -1
  77. package/dist/storage/sqliteAsync/client.d.ts +24 -12
  78. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  79. package/dist/storage/sqliteAsync/client.js +70 -58
  80. package/dist/storage/sqliteAsync/client.js.map +1 -1
  81. package/dist/storage/sqliteAsync/types.d.ts +1 -1
  82. package/dist/storage/sqliteAsync/types.d.ts.map +1 -1
  83. package/dist/storage/types.d.ts +1 -0
  84. package/dist/storage/types.d.ts.map +1 -1
  85. package/dist/sync.d.ts.map +1 -1
  86. package/dist/sync.js +7 -1
  87. package/dist/sync.js.map +1 -1
  88. package/dist/tests/CojsonMessageChannel.test.js +2 -2
  89. package/dist/tests/SQLiteClientAsync.test.d.ts +2 -0
  90. package/dist/tests/SQLiteClientAsync.test.d.ts.map +1 -0
  91. package/dist/tests/SQLiteClientAsync.test.js +64 -0
  92. package/dist/tests/SQLiteClientAsync.test.js.map +1 -0
  93. package/dist/tests/StorageApiAsync.test.js +2 -8
  94. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  95. package/dist/tests/SyncStateManager.test.js +2 -2
  96. package/dist/tests/WasmCrypto.test.js +1 -15
  97. package/dist/tests/WasmCrypto.test.js.map +1 -1
  98. package/dist/tests/coList.test.js +24 -5
  99. package/dist/tests/coList.test.js.map +1 -1
  100. package/dist/tests/coStream.test.js +4 -3
  101. package/dist/tests/coStream.test.js.map +1 -1
  102. package/dist/tests/coValueCore.initTransaction.test.d.ts +2 -0
  103. package/dist/tests/coValueCore.initTransaction.test.d.ts.map +1 -0
  104. package/dist/tests/coValueCore.initTransaction.test.js +438 -0
  105. package/dist/tests/coValueCore.initTransaction.test.js.map +1 -0
  106. package/dist/tests/coValueCore.test.js +11 -19
  107. package/dist/tests/coValueCore.test.js.map +1 -1
  108. package/dist/tests/crypto.test.js +83 -0
  109. package/dist/tests/crypto.test.js.map +1 -1
  110. package/dist/tests/deleteCoValue.test.js +5 -5
  111. package/dist/tests/deleteCoValue.test.js.map +1 -1
  112. package/dist/tests/group.inheritance.test.js +11 -0
  113. package/dist/tests/group.inheritance.test.js.map +1 -1
  114. package/dist/tests/group.test.js +24 -1
  115. package/dist/tests/group.test.js.map +1 -1
  116. package/dist/tests/groupSealer.test.d.ts +2 -0
  117. package/dist/tests/groupSealer.test.d.ts.map +1 -0
  118. package/dist/tests/groupSealer.test.js +913 -0
  119. package/dist/tests/groupSealer.test.js.map +1 -0
  120. package/dist/tests/setup.js +5 -0
  121. package/dist/tests/setup.js.map +1 -1
  122. package/dist/tests/sync.auth.test.js +10 -10
  123. package/dist/tests/sync.concurrentLoad.test.js +12 -12
  124. package/dist/tests/sync.deleted.test.js +8 -8
  125. package/dist/tests/sync.garbageCollection.test.js +10 -10
  126. package/dist/tests/sync.invite.test.js +12 -12
  127. package/dist/tests/sync.known.test.js +2 -2
  128. package/dist/tests/sync.load.test.js +107 -107
  129. package/dist/tests/sync.mesh.test.js +164 -46
  130. package/dist/tests/sync.mesh.test.js.map +1 -1
  131. package/dist/tests/sync.multipleServers.test.js +43 -43
  132. package/dist/tests/sync.peerReconciliation.test.js +29 -29
  133. package/dist/tests/sync.sharding.test.js +3 -3
  134. package/dist/tests/sync.storage.test.js +104 -104
  135. package/dist/tests/sync.storage.test.js.map +1 -1
  136. package/dist/tests/sync.storageAsync.test.js +56 -56
  137. package/dist/tests/sync.upload.test.js +22 -22
  138. package/dist/tests/testStorage.d.ts +2 -0
  139. package/dist/tests/testStorage.d.ts.map +1 -1
  140. package/dist/tests/testStorage.js +30 -6
  141. package/dist/tests/testStorage.js.map +1 -1
  142. package/dist/typeUtils/isCoValue.js +1 -1
  143. package/dist/typeUtils/isCoValue.js.map +1 -1
  144. package/package.json +4 -4
  145. package/src/SyncStateManager.ts +0 -2
  146. package/src/base64url.test.ts +89 -1
  147. package/src/base64url.ts +134 -6
  148. package/src/coValue.ts +2 -1
  149. package/src/coValueCore/coValueCore.ts +126 -84
  150. package/src/coValueCore/verifiedState.ts +335 -53
  151. package/src/coValues/binaryCoStream.ts +217 -0
  152. package/src/coValues/coList.ts +21 -8
  153. package/src/coValues/coMap.ts +3 -0
  154. package/src/coValues/coStream.ts +0 -170
  155. package/src/coValues/group.ts +270 -21
  156. package/src/coreToCoValue.ts +2 -1
  157. package/src/crypto/NapiCrypto.ts +198 -95
  158. package/src/crypto/RNCrypto.ts +229 -102
  159. package/src/crypto/WasmCrypto.ts +201 -95
  160. package/src/crypto/crypto.ts +118 -45
  161. package/src/exports.ts +11 -5
  162. package/src/localNode.ts +17 -1
  163. package/src/media.ts +1 -1
  164. package/src/permissions.ts +30 -7
  165. package/src/storage/sqliteAsync/client.ts +136 -115
  166. package/src/storage/sqliteAsync/types.ts +3 -1
  167. package/src/storage/types.ts +4 -0
  168. package/src/sync.ts +10 -1
  169. package/src/tests/CojsonMessageChannel.test.ts +2 -2
  170. package/src/tests/SQLiteClientAsync.test.ts +75 -0
  171. package/src/tests/StorageApiAsync.test.ts +4 -9
  172. package/src/tests/SyncStateManager.test.ts +2 -2
  173. package/src/tests/WasmCrypto.test.ts +1 -25
  174. package/src/tests/coList.test.ts +39 -5
  175. package/src/tests/coStream.test.ts +4 -5
  176. package/src/tests/coValueCore.initTransaction.test.ts +836 -0
  177. package/src/tests/coValueCore.test.ts +11 -22
  178. package/src/tests/crypto.test.ts +107 -0
  179. package/src/tests/deleteCoValue.test.ts +5 -5
  180. package/src/tests/group.inheritance.test.ts +16 -0
  181. package/src/tests/group.test.ts +29 -1
  182. package/src/tests/groupSealer.test.ts +1473 -0
  183. package/src/tests/setup.ts +6 -0
  184. package/src/tests/sync.auth.test.ts +10 -10
  185. package/src/tests/sync.concurrentLoad.test.ts +12 -12
  186. package/src/tests/sync.deleted.test.ts +8 -8
  187. package/src/tests/sync.garbageCollection.test.ts +10 -10
  188. package/src/tests/sync.invite.test.ts +12 -12
  189. package/src/tests/sync.known.test.ts +2 -2
  190. package/src/tests/sync.load.test.ts +107 -107
  191. package/src/tests/sync.mesh.test.ts +189 -46
  192. package/src/tests/sync.multipleServers.test.ts +43 -43
  193. package/src/tests/sync.peerReconciliation.test.ts +29 -29
  194. package/src/tests/sync.sharding.test.ts +3 -3
  195. package/src/tests/sync.storage.test.ts +104 -104
  196. package/src/tests/sync.storageAsync.test.ts +56 -56
  197. package/src/tests/sync.upload.test.ts +22 -22
  198. package/src/tests/testStorage.ts +39 -9
  199. package/src/typeUtils/isCoValue.ts +1 -1
  200. package/dist/coValueCore/SessionMap.d.ts +0 -55
  201. package/dist/coValueCore/SessionMap.d.ts.map +0 -1
  202. package/dist/coValueCore/SessionMap.js +0 -206
  203. package/dist/coValueCore/SessionMap.js.map +0 -1
  204. package/dist/tests/coreWasm.test.d.ts +0 -2
  205. package/dist/tests/coreWasm.test.d.ts.map +0 -1
  206. package/dist/tests/coreWasm.test.js +0 -203
  207. package/dist/tests/coreWasm.test.js.map +0 -1
  208. package/src/coValueCore/SessionMap.ts +0 -394
  209. package/src/tests/coreWasm.test.ts +0 -452
@@ -1,5 +1,9 @@
1
1
  import { expect, test } from "vitest";
2
- import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
2
+ import {
3
+ base64URLtoBytes,
4
+ bytesToBase64url,
5
+ bytesToBase64,
6
+ } from "./base64url.js";
3
7
 
4
8
  const txt = new TextEncoder();
5
9
 
@@ -31,3 +35,87 @@ test("Test our Base64 URL encoding and decoding", () => {
31
35
  "V2hhdCBkb2VzIDIgKyAyLjEgZXF1YWw_PyB-IDQ=",
32
36
  );
33
37
  });
38
+
39
+ test("Single special bytes: 0x00 and 0xFF", () => {
40
+ const zero = new Uint8Array([0x00]);
41
+ const encoded0 = bytesToBase64url(zero);
42
+ expect(base64URLtoBytes(encoded0)).toEqual(zero);
43
+
44
+ const ff = new Uint8Array([0xff]);
45
+ const encodedFF = bytesToBase64url(ff);
46
+ expect(base64URLtoBytes(encodedFF)).toEqual(ff);
47
+ });
48
+
49
+ test("All 256 byte values round-trip", () => {
50
+ const allBytes = new Uint8Array(256);
51
+ for (let i = 0; i < 256; i++) allBytes[i] = i;
52
+ const encoded = bytesToBase64url(allBytes);
53
+ expect(base64URLtoBytes(encoded)).toEqual(allBytes);
54
+ });
55
+
56
+ test("Decoding without padding", () => {
57
+ expect(base64URLtoBytes("Zg")).toEqual(base64URLtoBytes("Zg=="));
58
+ expect(base64URLtoBytes("Zm8")).toEqual(base64URLtoBytes("Zm8="));
59
+ expect(base64URLtoBytes("Zm9vYg")).toEqual(base64URLtoBytes("Zm9vYg=="));
60
+ expect(base64URLtoBytes("Zm9vYmE")).toEqual(base64URLtoBytes("Zm9vYmE="));
61
+ });
62
+
63
+ test("Larger binary data round-trip (4KB)", () => {
64
+ const size = 4096;
65
+ const data = new Uint8Array(size);
66
+ // Deterministic pseudo-random fill
67
+ let seed = 42;
68
+ for (let i = 0; i < size; i++) {
69
+ seed = (seed * 1103515245 + 12345) & 0x7fffffff;
70
+ data[i] = seed & 0xff;
71
+ }
72
+ const encoded = bytesToBase64url(data);
73
+ expect(base64URLtoBytes(encoded)).toEqual(data);
74
+ });
75
+
76
+ test("URL-safe characters: - and _ instead of + and /", () => {
77
+ // 0xFB, 0xEF, 0xBE → standard base64 "++--", base64url "----"
78
+ const bytes1 = new Uint8Array([0xfb, 0xef, 0xbe]);
79
+ const encoded1 = bytesToBase64url(bytes1);
80
+ expect(encoded1).not.toContain("+");
81
+ expect(encoded1).not.toContain("/");
82
+ expect(encoded1).toContain("-");
83
+ expect(base64URLtoBytes(encoded1)).toEqual(bytes1);
84
+
85
+ // 0xFF, 0xFF, 0xFE → standard base64 "///+", base64url "___-"
86
+ const bytes2 = new Uint8Array([0xff, 0xff, 0xfe]);
87
+ const encoded2 = bytesToBase64url(bytes2);
88
+ expect(encoded2).not.toContain("+");
89
+ expect(encoded2).not.toContain("/");
90
+ expect(encoded2).toContain("_");
91
+ expect(encoded2).toContain("-");
92
+ expect(base64URLtoBytes(encoded2)).toEqual(bytes2);
93
+ });
94
+
95
+ test("bytesToBase64 produces standard base64 with + and /", () => {
96
+ // 0xFB, 0xEF, 0xBE → standard base64 should contain "-" characters that become "+"
97
+ const bytes1 = new Uint8Array([0xfb, 0xef, 0xbe]);
98
+ const encoded1 = bytesToBase64(bytes1);
99
+ expect(encoded1).not.toContain("-");
100
+ expect(encoded1).not.toContain("_");
101
+ expect(encoded1).toContain("+");
102
+
103
+ // 0xFF, 0xFF, 0xFE → standard base64 "///+"
104
+ const bytes2 = new Uint8Array([0xff, 0xff, 0xfe]);
105
+ const encoded2 = bytesToBase64(bytes2);
106
+ expect(encoded2).not.toContain("-");
107
+ expect(encoded2).not.toContain("_");
108
+ expect(encoded2).toContain("/");
109
+ expect(encoded2).toContain("+");
110
+ expect(encoded2).toEqual("///+");
111
+ });
112
+
113
+ test("bytesToBase64 RFC test vectors", () => {
114
+ expect(bytesToBase64(new Uint8Array([]))).toEqual("");
115
+ expect(bytesToBase64(txt.encode("f"))).toEqual("Zg==");
116
+ expect(bytesToBase64(txt.encode("fo"))).toEqual("Zm8=");
117
+ expect(bytesToBase64(txt.encode("foo"))).toEqual("Zm9v");
118
+ expect(bytesToBase64(txt.encode("foob"))).toEqual("Zm9vYg==");
119
+ expect(bytesToBase64(txt.encode("fooba"))).toEqual("Zm9vYmE=");
120
+ expect(bytesToBase64(txt.encode("foobar"))).toEqual("Zm9vYmFy");
121
+ });
package/src/base64url.ts CHANGED
@@ -1,7 +1,104 @@
1
1
  const encoder = new TextEncoder();
2
2
  const decoder = new TextDecoder();
3
3
 
4
- export function base64URLtoBytes(base64: string) {
4
+ // Check for native base64 support (available in modern runtimes)
5
+ const hasNativeBase64 =
6
+ typeof (Uint8Array.prototype as unknown as { toBase64?: unknown })
7
+ .toBase64 === "function" &&
8
+ typeof (Uint8Array as unknown as { fromBase64?: unknown }).fromBase64 ===
9
+ "function";
10
+
11
+ // Native implementation hooks for React Native (set via setNativeBase64Implementation)
12
+ let nativeBytesToBase64url: ((bytes: ArrayBuffer) => string) | undefined;
13
+ let nativeBase64urlToBytes: ((base64: string) => ArrayBuffer) | undefined;
14
+ let nativeBytesToBase64: ((bytes: ArrayBuffer) => string) | undefined;
15
+
16
+ /**
17
+ * Set native base64 implementation for React Native.
18
+ * Called by RNCrypto.create() to register native Rust implementations.
19
+ * @internal
20
+ */
21
+ export function setNativeBase64Implementation(impl: {
22
+ bytesToBase64url: (bytes: ArrayBuffer) => string;
23
+ base64urlToBytes: (base64: string) => ArrayBuffer;
24
+ bytesToBase64: (bytes: ArrayBuffer) => string;
25
+ }): void {
26
+ nativeBytesToBase64url = impl.bytesToBase64url;
27
+ nativeBase64urlToBytes = impl.base64urlToBytes;
28
+ nativeBytesToBase64 = impl.bytesToBase64;
29
+ }
30
+
31
+ /**
32
+ * Convert Uint8Array to ArrayBuffer, handling views correctly.
33
+ */
34
+ function toArrayBuffer(view: Uint8Array): ArrayBuffer {
35
+ if (
36
+ view.byteOffset === 0 &&
37
+ view.byteLength === view.buffer.byteLength &&
38
+ view.buffer instanceof ArrayBuffer
39
+ ) {
40
+ return view.buffer;
41
+ }
42
+ const buffer = new ArrayBuffer(view.byteLength);
43
+ new Uint8Array(buffer).set(view);
44
+ return buffer;
45
+ }
46
+
47
+ export function base64URLtoBytes(base64: string): Uint8Array {
48
+ // Use React Native native implementation if available
49
+ if (nativeBase64urlToBytes) {
50
+ return new Uint8Array(nativeBase64urlToBytes(base64));
51
+ }
52
+ // Use browser native implementation if available
53
+ if (hasNativeBase64) {
54
+ return (
55
+ Uint8Array as unknown as {
56
+ fromBase64: (s: string, opts: { alphabet: string }) => Uint8Array;
57
+ }
58
+ ).fromBase64(base64, { alphabet: "base64url" });
59
+ }
60
+ return base64URLtoBytesFallback(base64);
61
+ }
62
+
63
+ export function bytesToBase64url(bytes: Uint8Array): string {
64
+ // Use React Native native implementation if available
65
+ if (nativeBytesToBase64url) {
66
+ return nativeBytesToBase64url(toArrayBuffer(bytes));
67
+ }
68
+ // Use browser native implementation if available
69
+ if (hasNativeBase64) {
70
+ return (
71
+ bytes as unknown as {
72
+ toBase64: (opts: { alphabet: string }) => string;
73
+ }
74
+ ).toBase64({ alphabet: "base64url" });
75
+ }
76
+ return bytesToBase64urlFallback(bytes);
77
+ }
78
+
79
+ /**
80
+ * Encode bytes to standard base64 (not URL-safe).
81
+ * Use this for data URLs and other contexts requiring standard base64.
82
+ */
83
+ export function bytesToBase64(bytes: Uint8Array): string {
84
+ // Use React Native native implementation if available
85
+ if (nativeBytesToBase64) {
86
+ return nativeBytesToBase64(toArrayBuffer(bytes));
87
+ }
88
+ // Use browser native implementation if available
89
+ if (hasNativeBase64) {
90
+ return (
91
+ bytes as unknown as {
92
+ toBase64: () => string;
93
+ }
94
+ ).toBase64();
95
+ }
96
+ return bytesToBase64Fallback(bytes);
97
+ }
98
+
99
+ // --- Fallback implementations ---
100
+
101
+ function base64URLtoBytesFallback(base64: string): Uint8Array {
5
102
  base64 = base64.replace(/=/g, "");
6
103
  const n = base64.length;
7
104
  const rem = n % 4;
@@ -24,8 +121,7 @@ export function base64URLtoBytes(base64: string) {
24
121
  return new Uint8Array(encoded.buffer, 0, m);
25
122
  }
26
123
 
27
- export function bytesToBase64url(bytes: Uint8Array) {
28
- // const before = performance.now();
124
+ function bytesToBase64urlFallback(bytes: Uint8Array): string {
29
125
  const m = bytes.length;
30
126
  const k = m % 3;
31
127
  const n = Math.floor(m / 3) * 4 + (k && k + 1);
@@ -47,16 +143,48 @@ export function bytesToBase64url(bytes: Uint8Array) {
47
143
  return base64;
48
144
  }
49
145
 
50
- const alphabet =
146
+ function bytesToBase64Fallback(bytes: Uint8Array): string {
147
+ const m = bytes.length;
148
+ const k = m % 3;
149
+ const n = Math.floor(m / 3) * 4 + (k && k + 1);
150
+ const N = Math.ceil(m / 3) * 4;
151
+ const encoded = new Uint8Array(N);
152
+
153
+ for (let i = 0, j = 0; j < m; i += 4, j += 3) {
154
+ const y = (bytes[j]! << 16) + (bytes[j + 1]! << 8) + (bytes[j + 2]! | 0);
155
+ encoded[i] = encodeLookupStd[y >> 18]!;
156
+ encoded[i + 1] = encodeLookupStd[(y >> 12) & 0x3f]!;
157
+ encoded[i + 2] = encodeLookupStd[(y >> 6) & 0x3f]!;
158
+ encoded[i + 3] = encodeLookupStd[y & 0x3f]!;
159
+ }
160
+
161
+ let base64 = decoder.decode(new Uint8Array(encoded.buffer, 0, n));
162
+ if (k === 1) base64 += "==";
163
+ if (k === 2) base64 += "=";
164
+
165
+ return base64;
166
+ }
167
+
168
+ // base64url alphabet (RFC 4648 §5)
169
+ const alphabetUrl =
51
170
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
52
171
 
172
+ // Standard base64 alphabet (RFC 4648 §4)
173
+ const alphabetStd =
174
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
175
+
53
176
  const lookup = new Uint8Array(128);
54
- for (const [i, a] of Array.from(alphabet).entries()) {
177
+ for (const [i, a] of Array.from(alphabetUrl).entries()) {
55
178
  lookup[a.charCodeAt(0)] = i;
56
179
  }
57
180
  lookup["=".charCodeAt(0)] = 0;
58
181
 
59
182
  const encodeLookup = new Uint8Array(64);
60
- for (const [i, a] of Array.from(alphabet).entries()) {
183
+ for (const [i, a] of Array.from(alphabetUrl).entries()) {
61
184
  encodeLookup[i] = a.charCodeAt(0);
62
185
  }
186
+
187
+ const encodeLookupStd = new Uint8Array(64);
188
+ for (const [i, a] of Array.from(alphabetStd).entries()) {
189
+ encodeLookupStd[i] = a.charCodeAt(0);
190
+ }
package/src/coValue.ts CHANGED
@@ -3,7 +3,8 @@ import { RawProfile as Profile, RawAccount } from "./coValues/account.js";
3
3
  import { RawCoList } from "./coValues/coList.js";
4
4
  import { RawCoMap } from "./coValues/coMap.js";
5
5
  import { RawCoPlainText } from "./coValues/coPlainText.js";
6
- import { RawBinaryCoStream, RawCoStream } from "./coValues/coStream.js";
6
+ import { RawBinaryCoStream } from "./coValues/binaryCoStream.js";
7
+ import { RawCoStream } from "./coValues/coStream.js";
7
8
  import { RawGroup } from "./coValues/group.js";
8
9
  import { RawCoID } from "./ids.js";
9
10
  import { JsonObject, JsonValue } from "./jsonValue.js";
@@ -42,7 +42,6 @@ import {
42
42
  Uniqueness,
43
43
  VerifiedState,
44
44
  } from "./verifiedState.js";
45
- import { SessionMap } from "./SessionMap.js";
46
45
  import {
47
46
  MergeCommit,
48
47
  BranchPointerCommit,
@@ -65,45 +64,6 @@ import {
65
64
  } from "../knownState.js";
66
65
  import { safeParseJSON } from "../jsonStringify.js";
67
66
 
68
- export type ValidationValue =
69
- | { isOk: true }
70
- | {
71
- isOk: false;
72
- message: string;
73
- };
74
-
75
- function validateUniqueness(uniqueness: Uniqueness): ValidationValue {
76
- if (typeof uniqueness === "number" && !Number.isInteger(uniqueness)) {
77
- return {
78
- isOk: false,
79
- message: "Uniqueness cannot be a non-integer number, got " + uniqueness,
80
- };
81
- }
82
-
83
- if (Array.isArray(uniqueness)) {
84
- return {
85
- isOk: false,
86
- message: "Uniqueness cannot be an array, got " + uniqueness,
87
- };
88
- }
89
-
90
- if (typeof uniqueness === "object" && uniqueness !== null) {
91
- for (let [key, value] of Object.entries(uniqueness)) {
92
- if (typeof value !== "string") {
93
- return {
94
- isOk: false,
95
- message:
96
- "Uniqueness object values must be a string, got " +
97
- value +
98
- " for key " +
99
- key,
100
- };
101
- }
102
- }
103
- }
104
- return { isOk: true };
105
- }
106
-
107
67
  export function idforHeader(
108
68
  header: CoValueHeader,
109
69
  crypto: CryptoProvider,
@@ -137,13 +97,17 @@ export class VerifiedTransaction {
137
97
  changes: JsonValue[] | undefined;
138
98
  // The decoded meta information of the transaction
139
99
  meta: JsonObject | undefined;
140
- isValidated: boolean = false;
141
100
  // Whether the transaction is valid, as per membership rules
142
101
  isValid: boolean = false;
143
102
  // The error message that caused the transaction to be invalid
144
103
  validationErrorMessage: string | undefined = undefined;
145
104
  // The previous verified transaction for the same session
146
105
  previous: VerifiedTransaction | undefined;
106
+ // Transaction processing stage:
107
+ // - "to-validate": Transaction is pending validation on permissions checks
108
+ // - "validated": Transaction has been validated but not yet applied to content
109
+ // - "processed": Transaction has been validated and applied to content
110
+ stage: "to-validate" | "validated" | "processed" = "to-validate";
147
111
 
148
112
  constructor(
149
113
  coValueId: RawCoID,
@@ -214,18 +178,31 @@ export class VerifiedTransaction {
214
178
  return Boolean(this.isValid && this.changes);
215
179
  }
216
180
 
181
+ isProcessable(includeInvalidMetaTransactions: boolean): this is {
182
+ changes: JsonValue[];
183
+ } {
184
+ return Boolean(
185
+ this.changes && (includeInvalidMetaTransactions || this.isValid),
186
+ );
187
+ }
188
+
217
189
  markValid() {
190
+ const validityChanged = this.isValid === false;
218
191
  this.isValid = true;
219
192
  this.validationErrorMessage = undefined;
220
193
 
221
- if (!this.isValidated) {
222
- this.isValidated = true;
194
+ if (this.stage === "to-validate") {
195
+ this.stage = "validated";
196
+ this.dispatchTransaction(this);
197
+ }
198
+
199
+ if (this.stage === "processed" && validityChanged) {
223
200
  this.dispatchTransaction(this);
224
201
  }
225
202
  }
226
203
 
227
204
  markInvalid(errorMessage: string, attributes?: Record<string, JsonValue>) {
228
- this.isValidated = true;
205
+ const validityChanged = this.isValid === true;
229
206
  this.isValid = false;
230
207
 
231
208
  this.validationErrorMessage = errorMessage;
@@ -237,6 +214,22 @@ export class VerifiedTransaction {
237
214
  ...attributes,
238
215
  });
239
216
  }
217
+
218
+ if (this.stage === "processed" && validityChanged) {
219
+ this.dispatchTransaction(this);
220
+ }
221
+
222
+ if (this.stage === "to-validate") {
223
+ this.stage = "validated";
224
+ }
225
+ }
226
+
227
+ markAsProcessed() {
228
+ this.stage = "processed";
229
+ }
230
+
231
+ markAsToValidate() {
232
+ this.stage = "to-validate";
240
233
  }
241
234
  }
242
235
 
@@ -586,36 +579,34 @@ export class CoValueCore {
586
579
  streamingKnownState?: KnownStateSessions,
587
580
  skipVerify?: boolean,
588
581
  ) {
589
- if (!skipVerify) {
590
- const validation = validateUniqueness(header.uniqueness);
591
- if (!validation.isOk) {
592
- logger.error("Invalid uniqueness", {
593
- header,
594
- errorMessage: validation.message,
595
- });
596
- return false;
597
- }
598
-
599
- const expectedId = idforHeader(header, this.node.crypto);
582
+ if (this._verified?.sessionCount) {
583
+ throw new Error(
584
+ "CoValueCore: provideHeader called on coValue with verified sessions present!",
585
+ );
586
+ }
600
587
 
601
- if (this.id !== expectedId) {
602
- return false;
603
- }
588
+ // Create VerifiedState - Rust validates uniqueness and id match unless skipVerify is true
589
+ try {
590
+ this._verified = new VerifiedState(
591
+ this.id,
592
+ this.node.crypto,
593
+ header,
594
+ streamingKnownState,
595
+ skipVerify,
596
+ );
597
+ } catch (e) {
598
+ // Rust validation failed (invalid uniqueness or id mismatch)
599
+ logger.error("Header validation failed", {
600
+ id: this.id,
601
+ header,
602
+ error: e instanceof Error ? e.message : String(e),
603
+ });
604
+ return false;
604
605
  }
605
606
 
607
+ // Only add dependencies after successful validation
606
608
  this.addDependencyFromHeader(header);
607
609
 
608
- if (this._verified?.sessions.size) {
609
- throw new Error(
610
- "CoValueCore: provideHeader called on coValue with verified sessions present!",
611
- );
612
- }
613
- this._verified = new VerifiedState(
614
- this.id,
615
- this.node.crypto,
616
- header,
617
- new SessionMap(this.id, this.node.crypto, streamingKnownState),
618
- );
619
610
  // Clean up if transitioning from garbageCollected/onlyKnownState
620
611
  if (this.isAvailable()) {
621
612
  this.cleanupLastKnownState();
@@ -651,7 +642,7 @@ export class CoValueCore {
651
642
  */
652
643
  knownStateWithStreaming(): CoValueKnownState {
653
644
  if (this.verified) {
654
- return this.verified.immutableKnownStateWithStreaming();
645
+ return this.verified.knownStateWithStreaming();
655
646
  }
656
647
 
657
648
  return this.knownState();
@@ -660,16 +651,12 @@ export class CoValueCore {
660
651
  /**
661
652
  * Returns the known state of the CoValue
662
653
  *
663
- * The return value identity is going to be stable as long as the CoValue is not modified.
664
- *
665
- * On change the knownState is invalidated and a new object is returned.
666
- *
667
654
  * For garbageCollected/onlyKnownState CoValues, returns the cached knownState.
668
655
  */
669
656
  knownState(): CoValueKnownState {
670
657
  // 1. If we have verified content in memory, use that (authoritative)
671
658
  if (this.verified) {
672
- return this.verified.immutableKnownState();
659
+ return this.verified.knownState();
673
660
  }
674
661
 
675
662
  // 2. If we have last known state (GC'd or onlyKnownState), use that
@@ -730,7 +717,7 @@ export class CoValueCore {
730
717
 
731
718
  return {
732
719
  sessionID,
733
- txIndex: this.verified.sessions.get(sessionID)?.transactions.length || 0,
720
+ txIndex: this.verified.getTransactionsCount(sessionID) || 0,
734
721
  };
735
722
  }
736
723
 
@@ -761,8 +748,7 @@ export class CoValueCore {
761
748
  let deleteTransaction: Transaction | undefined = undefined;
762
749
 
763
750
  if (isDeleteSessionID(sessionID)) {
764
- const txCount =
765
- this.verified.sessions.get(sessionID)?.transactions.length ?? 0;
751
+ const txCount = this.verified.getTransactionsCount(sessionID) ?? 0;
766
752
  if (txCount > 0 || newTransactions.length > 1) {
767
753
  return {
768
754
  value: true,
@@ -1051,6 +1037,20 @@ export class CoValueCore {
1051
1037
  }
1052
1038
  }
1053
1039
 
1040
+ #isContentRebuildScheduled = false;
1041
+ scheduleContentRebuild() {
1042
+ if (!this._cachedContent || this.#isContentRebuildScheduled) {
1043
+ return;
1044
+ }
1045
+
1046
+ this.#isContentRebuildScheduled = true;
1047
+
1048
+ queueMicrotask(() => {
1049
+ this.#isContentRebuildScheduled = false;
1050
+ this._cachedContent?.rebuildFromCore();
1051
+ });
1052
+ }
1053
+
1054
1054
  #isNotifyUpdatePaused = false;
1055
1055
  pauseNotifyUpdate() {
1056
1056
  this.#isNotifyUpdatePaused = true;
@@ -1319,7 +1319,7 @@ export class CoValueCore {
1319
1319
  // Store the validity of the transactions before resetting the parsed transactions
1320
1320
  const validityBeforeReset = new Array<boolean>(verifiedTransactions.length);
1321
1321
  this.verifiedTransactions.forEach((transaction, index) => {
1322
- transaction.isValidated = false;
1322
+ transaction.markAsToValidate();
1323
1323
  validityBeforeReset[index] = transaction.isValidTransactionWithChanges();
1324
1324
  });
1325
1325
 
@@ -1327,6 +1327,7 @@ export class CoValueCore {
1327
1327
  this.toProcessTransactions = [];
1328
1328
  this.toDecryptTransactions = [];
1329
1329
  this.toParseMetaTransactions = [];
1330
+ this.#fwwWinners.clear();
1330
1331
 
1331
1332
  this.parseNewTransactions(false);
1332
1333
 
@@ -1378,7 +1379,7 @@ export class CoValueCore {
1378
1379
 
1379
1380
  const isBranched = this.isBranched();
1380
1381
 
1381
- for (const [sessionID, sessionLog] of this.verified.sessions.entries()) {
1382
+ for (const [sessionID, sessionLog] of this.verified.sessionEntries()) {
1382
1383
  const count = this.verifiedTransactionsKnownSessions[sessionID] ?? 0;
1383
1384
 
1384
1385
  for (
@@ -1427,11 +1428,16 @@ export class CoValueCore {
1427
1428
  }
1428
1429
 
1429
1430
  dispatchTransaction = (transaction: VerifiedTransaction) => {
1430
- if (!transaction.isValidated) {
1431
+ if (transaction.stage === "to-validate") {
1431
1432
  this.toValidateTransactions.push(transaction);
1432
1433
  return;
1433
1434
  }
1434
1435
 
1436
+ if (transaction.stage === "processed") {
1437
+ this.scheduleContentRebuild();
1438
+ return;
1439
+ }
1440
+
1435
1441
  if (transaction.changes) {
1436
1442
  this.toProcessTransactions.push(transaction);
1437
1443
  } else {
@@ -1451,6 +1457,8 @@ export class CoValueCore {
1451
1457
  this.toValidateTransactions = [];
1452
1458
  }
1453
1459
 
1460
+ #fwwWinners: Map<string, VerifiedTransaction> = new Map();
1461
+
1454
1462
  /**
1455
1463
  * Parses the meta information of a transaction, and set the branchStart and mergeCommits.
1456
1464
  */
@@ -1489,6 +1497,30 @@ export class CoValueCore {
1489
1497
  this.mergeCommits.push(mergeCommit);
1490
1498
  }
1491
1499
 
1500
+ if ("fww" in transaction.meta) {
1501
+ const fwwKey = transaction.meta.fww as string;
1502
+ const currentWinner = this.#fwwWinners.get(fwwKey);
1503
+
1504
+ // First-writer-wins: keep the transaction with the smallest madeAt
1505
+ // compareTransactions returns < 0 if transaction is earlier than currentWinner
1506
+ if (
1507
+ !currentWinner ||
1508
+ this.compareTransactions(transaction, currentWinner) < 0
1509
+ ) {
1510
+ if (currentWinner) {
1511
+ currentWinner.markInvalid(
1512
+ `Transaction is not the first writer for fww key "${fwwKey}"`,
1513
+ );
1514
+ }
1515
+
1516
+ this.#fwwWinners.set(fwwKey, transaction);
1517
+ } else {
1518
+ transaction.markInvalid(
1519
+ `Transaction is not the first writer for fww key "${fwwKey}"`,
1520
+ );
1521
+ }
1522
+ }
1523
+
1492
1524
  // Check if the transaction has been merged from a branch
1493
1525
  if ("mi" in transaction.meta) {
1494
1526
  const meta = transaction.meta as MergedTransactionMetadata;
@@ -1570,7 +1602,7 @@ export class CoValueCore {
1570
1602
  from?: CoValueKnownState["sessions"];
1571
1603
  to?: CoValueKnownState["sessions"];
1572
1604
  knownTransactions?: Record<RawCoID, number>;
1573
-
1605
+ includeInvalidMetaTransactions?: boolean;
1574
1606
  // If true, the branch source transactions will be skipped. Used to gather the transactions for the merge operation.
1575
1607
  skipBranchSource?: boolean;
1576
1608
  }): DecryptedTransaction[] {
@@ -1589,6 +1621,11 @@ export class CoValueCore {
1589
1621
 
1590
1622
  const knownTransactions = options?.knownTransactions?.[this.id] ?? 0;
1591
1623
 
1624
+ // Include invalid transactions in the result (only transactions invalidated by metadata parsing are included e.g. init transactions)
1625
+ // permission errors are still not included
1626
+ const includeInvalidMetaTransactions =
1627
+ options?.includeInvalidMetaTransactions ?? false;
1628
+
1592
1629
  for (
1593
1630
  let i = knownTransactions;
1594
1631
  i < this.toProcessTransactions.length;
@@ -1596,7 +1633,7 @@ export class CoValueCore {
1596
1633
  ) {
1597
1634
  const transaction = this.toProcessTransactions[i]!;
1598
1635
 
1599
- if (!transaction.isValidTransactionWithChanges()) {
1636
+ if (!transaction.isProcessable(includeInvalidMetaTransactions)) {
1600
1637
  continue;
1601
1638
  }
1602
1639
 
@@ -1613,6 +1650,7 @@ export class CoValueCore {
1613
1650
  continue;
1614
1651
  }
1615
1652
 
1653
+ transaction.markAsProcessed();
1616
1654
  matchingTransactions.push(transaction);
1617
1655
  }
1618
1656
 
@@ -1623,6 +1661,7 @@ export class CoValueCore {
1623
1661
  // If this is a branch, we load the valid transactions from the source
1624
1662
  if (source && this.branchStart && !options?.skipBranchSource) {
1625
1663
  const sourceTransactions = source.getValidTransactions({
1664
+ includeInvalidMetaTransactions,
1626
1665
  knownTransactions: options?.knownTransactions,
1627
1666
  to: this.branchStart,
1628
1667
  ignorePrivateTransactions: options?.ignorePrivateTransactions ?? false,
@@ -1781,6 +1820,9 @@ export class CoValueCore {
1781
1820
 
1782
1821
  // The transactions that have already been processed, used for the incremental builds of the content views
1783
1822
  knownTransactions?: Record<RawCoID, number>;
1823
+
1824
+ // Whether to include invalid transactions in the result (only transactions invalidated by metadata parsing are included e.g. init transactions)
1825
+ includeInvalidMetaTransactions?: boolean;
1784
1826
  }): DecryptedTransaction[] {
1785
1827
  const allTransactions = this.getValidTransactions(options);
1786
1828
 
@@ -1903,7 +1945,7 @@ export class CoValueCore {
1903
1945
  }
1904
1946
 
1905
1947
  getTx(txID: TransactionID): Transaction | undefined {
1906
- return this.verified?.sessions.get(txID.sessionID)?.transactions[
1948
+ return this.verified?.getSession(txID.sessionID)?.transactions[
1907
1949
  txID.txIndex
1908
1950
  ];
1909
1951
  }