@evolu/common 8.7.0 → 8.9.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 (302) hide show
  1. package/dist/src/Array.d.ts +21 -21
  2. package/dist/src/Array.js +5 -5
  3. package/dist/src/Assert.d.ts +498 -141
  4. package/dist/src/Assert.d.ts.map +1 -1
  5. package/dist/src/Assert.js +648 -99
  6. package/dist/src/{Buffer.d.ts → Binary.d.ts} +85 -12
  7. package/dist/src/Binary.d.ts.map +1 -0
  8. package/dist/src/{Buffer.js → Binary.js} +206 -24
  9. package/dist/src/Console.d.ts +2 -10
  10. package/dist/src/Console.d.ts.map +1 -1
  11. package/dist/src/Console.js +6 -12
  12. package/dist/src/Crypto.d.ts +5 -5
  13. package/dist/src/Crypto.js +1 -1
  14. package/dist/src/Eq.d.ts +35 -40
  15. package/dist/src/Eq.d.ts.map +1 -1
  16. package/dist/src/Eq.js +51 -87
  17. package/dist/src/Function.d.ts +9 -14
  18. package/dist/src/Function.d.ts.map +1 -1
  19. package/dist/src/Function.js +7 -9
  20. package/dist/src/Http.d.ts +6 -7
  21. package/dist/src/Http.d.ts.map +1 -1
  22. package/dist/src/LeakDetector.d.ts.map +1 -1
  23. package/dist/src/LeakDetector.js +1 -0
  24. package/dist/src/LockManager.d.ts +1 -1
  25. package/dist/src/LockManager.js +1 -1
  26. package/dist/src/Lookup.d.ts +3 -2
  27. package/dist/src/Lookup.d.ts.map +1 -1
  28. package/dist/src/Lookup.js +4 -3
  29. package/dist/src/Number.d.ts +1 -1
  30. package/dist/src/Number.d.ts.map +1 -1
  31. package/dist/src/Number.js +1 -1
  32. package/dist/src/Object.d.ts +4 -4
  33. package/dist/src/Object.d.ts.map +1 -1
  34. package/dist/src/Object.js +5 -4
  35. package/dist/src/Option.d.ts +1 -2
  36. package/dist/src/Option.d.ts.map +1 -1
  37. package/dist/src/Option.js +1 -2
  38. package/dist/src/Platform.js +2 -0
  39. package/dist/src/Polyfills.d.ts +6 -0
  40. package/dist/src/Polyfills.d.ts.map +1 -1
  41. package/dist/src/Polyfills.js +10 -1
  42. package/dist/src/Random.d.ts +1 -1
  43. package/dist/src/Result.d.ts +49 -54
  44. package/dist/src/Result.d.ts.map +1 -1
  45. package/dist/src/Result.js +17 -22
  46. package/dist/src/Set.d.ts +6 -6
  47. package/dist/src/Set.js +2 -2
  48. package/dist/src/Sqlite.js +1 -1
  49. package/dist/src/String.js +3 -2
  50. package/dist/src/Task.d.ts +32 -32
  51. package/dist/src/Task.d.ts.map +1 -1
  52. package/dist/src/Task.js +20 -22
  53. package/dist/src/Test.d.ts +31 -3
  54. package/dist/src/Test.d.ts.map +1 -1
  55. package/dist/src/Test.js +46 -3
  56. package/dist/src/Time.d.ts +3 -11
  57. package/dist/src/Time.d.ts.map +1 -1
  58. package/dist/src/Time.js +10 -18
  59. package/dist/src/Type.d.ts +87 -85
  60. package/dist/src/Type.d.ts.map +1 -1
  61. package/dist/src/Type.js +70 -63
  62. package/dist/src/Types.d.ts +12 -12
  63. package/dist/src/Types.js +1 -1
  64. package/dist/src/WebSocket.d.ts +4 -7
  65. package/dist/src/WebSocket.d.ts.map +1 -1
  66. package/dist/src/Worker.d.ts +1 -1
  67. package/dist/src/Worker.js +7 -7
  68. package/dist/src/index.d.ts +1 -1
  69. package/dist/src/index.js +1 -1
  70. package/dist/src/intl/_en.d.ts.map +1 -1
  71. package/dist/src/intl/_en.js +1 -1
  72. package/dist/src/intl/ar.d.ts.map +1 -1
  73. package/dist/src/intl/ar.js +1 -1
  74. package/dist/src/intl/bn.d.ts.map +1 -1
  75. package/dist/src/intl/bn.js +1 -1
  76. package/dist/src/intl/ca.d.ts.map +1 -1
  77. package/dist/src/intl/ca.js +1 -1
  78. package/dist/src/intl/cs.d.ts.map +1 -1
  79. package/dist/src/intl/cs.js +1 -1
  80. package/dist/src/intl/da.d.ts.map +1 -1
  81. package/dist/src/intl/da.js +1 -1
  82. package/dist/src/intl/de.d.ts.map +1 -1
  83. package/dist/src/intl/de.js +1 -1
  84. package/dist/src/intl/el.d.ts.map +1 -1
  85. package/dist/src/intl/el.js +1 -1
  86. package/dist/src/intl/es.d.ts.map +1 -1
  87. package/dist/src/intl/es.js +1 -1
  88. package/dist/src/intl/fa.d.ts.map +1 -1
  89. package/dist/src/intl/fa.js +1 -1
  90. package/dist/src/intl/fi.d.ts.map +1 -1
  91. package/dist/src/intl/fi.js +1 -1
  92. package/dist/src/intl/fil.d.ts.map +1 -1
  93. package/dist/src/intl/fil.js +1 -1
  94. package/dist/src/intl/fr.d.ts.map +1 -1
  95. package/dist/src/intl/fr.js +1 -1
  96. package/dist/src/intl/he.d.ts.map +1 -1
  97. package/dist/src/intl/he.js +1 -1
  98. package/dist/src/intl/hi.d.ts.map +1 -1
  99. package/dist/src/intl/hi.js +1 -1
  100. package/dist/src/intl/hr.d.ts.map +1 -1
  101. package/dist/src/intl/hr.js +1 -1
  102. package/dist/src/intl/hu.d.ts.map +1 -1
  103. package/dist/src/intl/hu.js +1 -1
  104. package/dist/src/intl/id.d.ts.map +1 -1
  105. package/dist/src/intl/id.js +1 -1
  106. package/dist/src/intl/it.d.ts.map +1 -1
  107. package/dist/src/intl/it.js +1 -1
  108. package/dist/src/intl/ja.d.ts.map +1 -1
  109. package/dist/src/intl/ja.js +1 -1
  110. package/dist/src/intl/ko.d.ts.map +1 -1
  111. package/dist/src/intl/ko.js +1 -1
  112. package/dist/src/intl/ml.d.ts.map +1 -1
  113. package/dist/src/intl/ml.js +1 -1
  114. package/dist/src/intl/mr.d.ts.map +1 -1
  115. package/dist/src/intl/mr.js +1 -1
  116. package/dist/src/intl/ms.d.ts.map +1 -1
  117. package/dist/src/intl/ms.js +1 -1
  118. package/dist/src/intl/nb.d.ts.map +1 -1
  119. package/dist/src/intl/nb.js +1 -1
  120. package/dist/src/intl/nl.d.ts.map +1 -1
  121. package/dist/src/intl/nl.js +1 -1
  122. package/dist/src/intl/pa.d.ts.map +1 -1
  123. package/dist/src/intl/pa.js +1 -1
  124. package/dist/src/intl/pl.d.ts.map +1 -1
  125. package/dist/src/intl/pl.js +1 -1
  126. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  127. package/dist/src/intl/pt-BR.js +1 -1
  128. package/dist/src/intl/pt.d.ts.map +1 -1
  129. package/dist/src/intl/pt.js +1 -1
  130. package/dist/src/intl/ro.d.ts.map +1 -1
  131. package/dist/src/intl/ro.js +1 -1
  132. package/dist/src/intl/sk.d.ts.map +1 -1
  133. package/dist/src/intl/sk.js +1 -1
  134. package/dist/src/intl/sl.d.ts.map +1 -1
  135. package/dist/src/intl/sl.js +1 -1
  136. package/dist/src/intl/sv.d.ts.map +1 -1
  137. package/dist/src/intl/sv.js +1 -1
  138. package/dist/src/intl/sw.d.ts.map +1 -1
  139. package/dist/src/intl/sw.js +1 -1
  140. package/dist/src/intl/ta.d.ts.map +1 -1
  141. package/dist/src/intl/ta.js +1 -1
  142. package/dist/src/intl/te.d.ts.map +1 -1
  143. package/dist/src/intl/te.js +1 -1
  144. package/dist/src/intl/th.d.ts.map +1 -1
  145. package/dist/src/intl/th.js +1 -1
  146. package/dist/src/intl/tr.d.ts.map +1 -1
  147. package/dist/src/intl/tr.js +1 -1
  148. package/dist/src/intl/uk.d.ts.map +1 -1
  149. package/dist/src/intl/uk.js +1 -1
  150. package/dist/src/intl/ur.d.ts.map +1 -1
  151. package/dist/src/intl/ur.js +1 -1
  152. package/dist/src/intl/vi.d.ts.map +1 -1
  153. package/dist/src/intl/vi.js +1 -1
  154. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  155. package/dist/src/intl/zh-CN.js +1 -1
  156. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  157. package/dist/src/intl/zh-TW.js +1 -1
  158. package/dist/src/local-first/Evolu.d.ts +12 -12
  159. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  160. package/dist/src/local-first/Evolu.js +2 -2
  161. package/dist/src/local-first/Protocol.d.ts +14 -76
  162. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  163. package/dist/src/local-first/Protocol.js +29 -206
  164. package/dist/src/local-first/Query.d.ts +11 -11
  165. package/dist/src/local-first/Query.js +6 -6
  166. package/dist/src/local-first/Relay.d.ts +2 -2
  167. package/dist/src/local-first/Schema.d.ts +3 -5
  168. package/dist/src/local-first/Schema.d.ts.map +1 -1
  169. package/dist/src/local-first/Schema.js +2 -4
  170. package/dist/src/local-first/Shared.d.ts.map +1 -1
  171. package/dist/src/local-first/Shared.js +5 -5
  172. package/dist/src/local-first/Storage.js +4 -4
  173. package/dist/src/local-first/Timestamp.d.ts +9 -0
  174. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  175. package/dist/src/local-first/Timestamp.js +13 -10
  176. package/package.json +7 -4
  177. package/src/Array.test.ts +809 -0
  178. package/src/Array.ts +21 -21
  179. package/src/Assert.test.ts +1232 -0
  180. package/src/Assert.ts +1023 -227
  181. package/src/BigInt.test.ts +28 -0
  182. package/src/Binary.test.ts +1128 -0
  183. package/src/{Buffer.ts → Binary.ts} +273 -46
  184. package/src/Brand.test.ts +90 -0
  185. package/src/Cache.test.ts +183 -0
  186. package/src/Callbacks.test.ts +66 -0
  187. package/src/Console.test.ts +809 -0
  188. package/src/Console.ts +9 -17
  189. package/src/Crypto.test.ts +138 -0
  190. package/src/Crypto.ts +5 -5
  191. package/src/Eq.test.ts +333 -0
  192. package/src/Eq.ts +110 -176
  193. package/src/Error.test.ts +118 -0
  194. package/src/Function.test.ts +204 -0
  195. package/src/Function.ts +10 -15
  196. package/src/Http.test.ts +816 -0
  197. package/src/Http.ts +6 -7
  198. package/src/LeakDetector.test.ts +210 -0
  199. package/src/LeakDetector.ts +1 -0
  200. package/src/LockManager.ts +2 -2
  201. package/src/Lookup.test.ts +418 -0
  202. package/src/Lookup.ts +4 -3
  203. package/src/Microtask.test.ts +140 -0
  204. package/src/Number.test.ts +218 -0
  205. package/src/Number.ts +2 -2
  206. package/src/Object.test.ts +293 -0
  207. package/src/Object.ts +5 -4
  208. package/src/Option.test.ts +104 -0
  209. package/src/Option.ts +1 -2
  210. package/src/Order.test.ts +56 -0
  211. package/src/Platform.ts +2 -0
  212. package/src/Polyfills.ts +9 -4
  213. package/src/Random.test.ts +89 -0
  214. package/src/Random.ts +1 -1
  215. package/src/Ref.test.ts +139 -0
  216. package/src/RefCount.test.ts +184 -0
  217. package/src/Relation.test.ts +505 -0
  218. package/src/Resource.test.ts +4623 -0
  219. package/src/Result.test.ts +1829 -0
  220. package/src/Result.ts +49 -54
  221. package/src/Schedule.test.ts +1554 -0
  222. package/src/Set.test.ts +279 -0
  223. package/src/Set.ts +6 -6
  224. package/src/Sqlite.ts +1 -1
  225. package/src/Store.test.ts +288 -0
  226. package/src/String.test.ts +54 -0
  227. package/src/String.ts +3 -3
  228. package/src/Task.test.ts +10820 -0
  229. package/src/Task.ts +38 -43
  230. package/src/Test.test.ts +88 -0
  231. package/src/Test.ts +49 -3
  232. package/src/Time.test.ts +926 -0
  233. package/src/Time.ts +13 -21
  234. package/src/Type.base64.test.ts +88 -0
  235. package/src/Type.test.ts +20465 -0
  236. package/src/Type.ts +114 -111
  237. package/src/Types.test.ts +324 -0
  238. package/src/Types.ts +13 -13
  239. package/src/WebSocket.ts +4 -7
  240. package/src/Worker.test.ts +755 -0
  241. package/src/Worker.ts +13 -18
  242. package/src/index.ts +1 -1
  243. package/src/intl/_en.ts +1 -1
  244. package/src/intl/ar.ts +1 -1
  245. package/src/intl/bn.ts +1 -1
  246. package/src/intl/ca.ts +1 -1
  247. package/src/intl/cs.ts +1 -1
  248. package/src/intl/da.ts +1 -1
  249. package/src/intl/de.ts +1 -1
  250. package/src/intl/el.ts +1 -1
  251. package/src/intl/es.ts +1 -1
  252. package/src/intl/fa.ts +1 -1
  253. package/src/intl/fi.ts +1 -1
  254. package/src/intl/fil.ts +1 -1
  255. package/src/intl/fr.ts +1 -1
  256. package/src/intl/he.ts +1 -1
  257. package/src/intl/hi.ts +1 -1
  258. package/src/intl/hr.ts +1 -1
  259. package/src/intl/hu.ts +1 -1
  260. package/src/intl/id.ts +1 -1
  261. package/src/intl/intl.test.ts +3106 -0
  262. package/src/intl/it.ts +1 -1
  263. package/src/intl/ja.ts +1 -1
  264. package/src/intl/ko.ts +1 -1
  265. package/src/intl/ml.ts +1 -1
  266. package/src/intl/mr.ts +1 -1
  267. package/src/intl/ms.ts +1 -1
  268. package/src/intl/nb.ts +1 -1
  269. package/src/intl/nl.ts +1 -1
  270. package/src/intl/pa.ts +1 -1
  271. package/src/intl/pl.ts +1 -1
  272. package/src/intl/pt-BR.ts +1 -1
  273. package/src/intl/pt.ts +1 -1
  274. package/src/intl/ro.ts +1 -1
  275. package/src/intl/sk.ts +1 -1
  276. package/src/intl/sl.ts +1 -1
  277. package/src/intl/sv.ts +1 -1
  278. package/src/intl/sw.ts +1 -1
  279. package/src/intl/ta.ts +1 -1
  280. package/src/intl/te.ts +1 -1
  281. package/src/intl/th.ts +1 -1
  282. package/src/intl/tr.ts +1 -1
  283. package/src/intl/uk.ts +1 -1
  284. package/src/intl/ur.ts +1 -1
  285. package/src/intl/vi.ts +1 -1
  286. package/src/intl/zh-CN.ts +1 -1
  287. package/src/intl/zh-TW.ts +1 -1
  288. package/src/local-first/Evolu.test.ts +1921 -0
  289. package/src/local-first/Evolu.ts +18 -14
  290. package/src/local-first/Owner.test.ts +83 -0
  291. package/src/local-first/Protocol.test.ts +1037 -0
  292. package/src/local-first/Protocol.ts +58 -261
  293. package/src/local-first/Query.test.ts +349 -0
  294. package/src/local-first/Query.ts +11 -11
  295. package/src/local-first/Relay.ts +2 -2
  296. package/src/local-first/Schema.ts +3 -5
  297. package/src/local-first/Shared.test.ts +1828 -0
  298. package/src/local-first/Shared.ts +9 -8
  299. package/src/local-first/Storage.ts +4 -4
  300. package/src/local-first/Timestamp.test.ts +374 -0
  301. package/src/local-first/Timestamp.ts +22 -10
  302. package/dist/src/Buffer.d.ts.map +0 -1
@@ -0,0 +1,1128 @@
1
+ import { describe, it, mock } from "node:test";
2
+ import {
3
+ assertEqual,
4
+ assertEqualBytes,
5
+ assertInstanceOf,
6
+ assertSame,
7
+ assertThrowsInstanceOf,
8
+ assertTrue,
9
+ } from "./Assert.ts";
10
+
11
+ import {
12
+ BufferError,
13
+ createBuffer,
14
+ createRunLengthEncoder,
15
+ decodeFlags,
16
+ decodeJsonValue,
17
+ decodeLength,
18
+ decodeNonNegativeInt,
19
+ decodeNumber,
20
+ decodeRle,
21
+ decodeString,
22
+ encodeFlags,
23
+ encodeJsonValue,
24
+ encodeLength,
25
+ encodeNonNegativeInt,
26
+ encodeNumber,
27
+ encodeString,
28
+ } from "./Binary.ts";
29
+ import {
30
+ assertType,
31
+ FiniteNumber,
32
+ JsonValue,
33
+ NonNegativeInt,
34
+ Object,
35
+ PositiveInt,
36
+ zeroNonNegativeInt,
37
+ } from "./Type.ts";
38
+
39
+ const encodeJson = (value: unknown): Uint8Array => {
40
+ const result = JsonValue.fromUnknown(value);
41
+ if (!result.ok) throw new Error("Expected a valid JsonValue test fixture");
42
+
43
+ const buffer = createBuffer();
44
+ encodeJsonValue(buffer, result.value);
45
+ return buffer.unwrap();
46
+ };
47
+
48
+ const decodeJson = (bytes: Uint8Array | ReadonlyArray<number>) =>
49
+ decodeJsonValue(createBuffer(bytes));
50
+
51
+ const float64Bytes = (value: number): Uint8Array => {
52
+ const bytes = new Uint8Array(9);
53
+ bytes[0] = 0xcb;
54
+ new DataView(bytes.buffer).setFloat64(1, value);
55
+ return bytes;
56
+ };
57
+
58
+ const createJsonObject = (length: number): Record<string, number> => {
59
+ const value: Record<string, number> = {};
60
+ for (let index = 0; index < length; index++) value[`k${index}`] = index;
61
+ return value;
62
+ };
63
+
64
+ describe("BufferError", () => {
65
+ it("is an Error with its name and message", () => {
66
+ const error = new BufferError("test error");
67
+ assertInstanceOf(error, Error);
68
+ assertInstanceOf(error, BufferError);
69
+ assertEqual(error.name, "BufferError");
70
+ assertEqual(error.message, "test error");
71
+ });
72
+ });
73
+
74
+ describe("Buffer", () => {
75
+ it("extends, shifts, and resets bytes", () => {
76
+ const buffer = createBuffer();
77
+
78
+ assertEqual(buffer.getLength(), 0);
79
+ assertEqual(buffer.getCapacity(), 512);
80
+ assertEqualBytes(buffer.unwrap(), []);
81
+
82
+ const a256 = new Uint8Array(256);
83
+ buffer.extend(a256);
84
+ assertEqual(buffer.getLength(), 256);
85
+ assertEqual(buffer.getCapacity(), 512);
86
+ assertEqual(buffer.unwrap(), a256);
87
+
88
+ buffer.extend(new Uint8Array(512));
89
+ assertEqual(buffer.getLength(), 768);
90
+ assertEqual(buffer.getCapacity(), 1024);
91
+
92
+ buffer.extend(buffer.unwrap());
93
+ assertEqual(buffer.getLength(), 1536);
94
+ assertEqual(buffer.getCapacity(), 2048);
95
+
96
+ const buffer2 = createBuffer([1]);
97
+ assertEqualBytes(buffer2.unwrap(), [1]);
98
+
99
+ assertEqual(buffer2.shift(), 1);
100
+ assertEqualBytes(buffer2.unwrap(), []);
101
+
102
+ buffer2.extend([1, 2, 3]);
103
+ assertEqualBytes(buffer2.shiftN(2 as NonNegativeInt), [1, 2]);
104
+
105
+ assertEqualBytes(buffer2.unwrap(), [3]);
106
+
107
+ {
108
+ const thrown = assertThrowsInstanceOf(
109
+ () => buffer2.shiftN(2 as NonNegativeInt),
110
+ BufferError,
111
+ );
112
+
113
+ assertTrue(thrown.message.includes("Buffer parse ended prematurely"));
114
+ }
115
+
116
+ buffer2.shift();
117
+
118
+ {
119
+ const thrown = assertThrowsInstanceOf(() => buffer2.shift(), BufferError);
120
+
121
+ assertTrue(thrown.message.includes("Buffer parse ended prematurely"));
122
+ }
123
+
124
+ assertEqualBytes(buffer2.shiftN(0 as NonNegativeInt), []);
125
+
126
+ buffer2.extend([1]);
127
+ buffer2.reset();
128
+ assertEqual(buffer2.getLength(), 0);
129
+ assertEqualBytes(buffer2.unwrap(), []);
130
+ });
131
+
132
+ it("uses the input length as its initial capacity", () => {
133
+ const buffer = createBuffer(new Uint8Array(300));
134
+ assertEqual(buffer.getLength(), 300);
135
+ // Should match input, not 512
136
+ assertEqual(buffer.getCapacity(), 300);
137
+ });
138
+
139
+ for (const length of [-1, 1.5, Infinity, NaN]) {
140
+ it(`rejects invalid ArrayLike length ${length}`, () => {
141
+ const arrayLike: ArrayLike<number> = { length };
142
+
143
+ {
144
+ const thrown = assertThrowsInstanceOf(
145
+ () => createBuffer(arrayLike),
146
+ BufferError,
147
+ );
148
+
149
+ assertTrue(
150
+ thrown.message.includes(
151
+ "arrayLike.length must be a non-negative safe integer.",
152
+ ),
153
+ );
154
+ }
155
+
156
+ const buffer = createBuffer();
157
+ {
158
+ const thrown = assertThrowsInstanceOf(
159
+ () => buffer.extend(arrayLike),
160
+ BufferError,
161
+ );
162
+
163
+ assertTrue(
164
+ thrown.message.includes(
165
+ "arg.length must be a non-negative safe integer.",
166
+ ),
167
+ );
168
+ }
169
+ });
170
+ }
171
+
172
+ it("rejects an unsafe resulting length", () => {
173
+ const buffer = createBuffer([0]);
174
+ const extend = () =>
175
+ buffer.extend({
176
+ length: Number.MAX_SAFE_INTEGER,
177
+ });
178
+
179
+ {
180
+ const thrown = assertThrowsInstanceOf(extend, BufferError);
181
+
182
+ assertTrue(
183
+ thrown.message.includes(
184
+ "Buffer length must be a non-negative safe integer.",
185
+ ),
186
+ );
187
+ }
188
+ });
189
+
190
+ it("exposes its internal bytes through unwrap", () => {
191
+ const buffer = createBuffer([1, 2, 3]);
192
+ const view = buffer.unwrap();
193
+ view[0] = 99;
194
+ assertEqualBytes(buffer.unwrap(), [99, 2, 3]);
195
+ });
196
+
197
+ it("truncates its contents", () => {
198
+ const buffer = createBuffer([1, 2, 3, 4, 5]);
199
+ assertEqual(buffer.getLength(), 5);
200
+ assertEqualBytes(buffer.unwrap(), [1, 2, 3, 4, 5]);
201
+
202
+ buffer.truncate(3 as NonNegativeInt);
203
+ assertEqual(buffer.getLength(), 3);
204
+ assertEqualBytes(buffer.unwrap(), [1, 2, 3]);
205
+
206
+ buffer.truncate(0 as NonNegativeInt);
207
+ assertEqual(buffer.getLength(), 0);
208
+ assertEqualBytes(buffer.unwrap(), []);
209
+
210
+ {
211
+ const thrown = assertThrowsInstanceOf(() => {
212
+ buffer.truncate(1 as NonNegativeInt);
213
+ }, BufferError);
214
+
215
+ assertTrue(
216
+ thrown.message.includes(
217
+ "Cannot truncate to a length greater than current",
218
+ ),
219
+ );
220
+ }
221
+
222
+ buffer.extend([6, 7, 8]);
223
+ assertEqual(buffer.getLength(), 3);
224
+ assertEqualBytes(buffer.unwrap(), [6, 7, 8]);
225
+
226
+ buffer.truncate(2 as NonNegativeInt);
227
+ assertEqual(buffer.getLength(), 2);
228
+ assertEqualBytes(buffer.unwrap(), [6, 7]);
229
+ });
230
+ });
231
+
232
+ describe("number encoding", () => {
233
+ it("encodes and decodes finite numbers", () => {
234
+ const values = [
235
+ FiniteNumber.orThrow(-0),
236
+ FiniteNumber.orThrow(0),
237
+ FiniteNumber.orThrow(42),
238
+ FiniteNumber.orThrow(-123),
239
+ // oxlint-disable-next-line oxc/approx-constant -- Pins this exact decimal's serialized bytes below.
240
+ FiniteNumber.orThrow(3.14159),
241
+ FiniteNumber.orThrow(Number.MAX_SAFE_INTEGER),
242
+ FiniteNumber.orThrow(Number.MIN_SAFE_INTEGER),
243
+ ];
244
+
245
+ const buffer = createBuffer();
246
+
247
+ for (const value of values) {
248
+ encodeNumber(buffer, value);
249
+ const encoded = createBuffer();
250
+ encodeNumber(encoded, value);
251
+ assertSame(decodeNumber(encoded), value);
252
+ assertEqual(encoded.getLength(), 0);
253
+ }
254
+
255
+ assertEqualBytes(
256
+ buffer.unwrap(),
257
+ [
258
+ 203, 128, 0, 0, 0, 0, 0, 0, 0, 0, 42, 208, 133, 203, 64, 9, 33, 249,
259
+ 240, 27, 134, 110, 203, 67, 63, 255, 255, 255, 255, 255, 255, 203, 195,
260
+ 63, 255, 255, 255, 255, 255, 255,
261
+ ],
262
+ );
263
+ });
264
+
265
+ // Non-finite numbers indicate an app bug. Evolu prevents them from being
266
+ // stored or synced; this covers legacy data created before that protection.
267
+ it("rejects a non-finite MessagePack number", () => {
268
+ const error = assertThrowsInstanceOf(
269
+ () => decodeNumber(createBuffer([203, 127, 240, 0, 0, 0, 0, 0, 0])),
270
+ BufferError,
271
+ );
272
+ assertEqual(error.message, "A decoded JSON number must be finite.");
273
+ });
274
+
275
+ it("rejects a non-number MessagePack value", () => {
276
+ const error = assertThrowsInstanceOf(
277
+ () => decodeNumber(createBuffer([0xc0])),
278
+ BufferError,
279
+ );
280
+ assertEqual(error.message, "Expected an encoded number.");
281
+ });
282
+ });
283
+
284
+ describe("flag encoding", () => {
285
+ it("encodes and decodes up to eight boolean flags", () => {
286
+ const cases: ReadonlyArray<{
287
+ readonly flags: ReadonlyArray<boolean>;
288
+ readonly expected: number;
289
+ }> = [
290
+ { flags: [true], expected: 1 },
291
+ { flags: [false], expected: 0 },
292
+ { flags: [true, false], expected: 1 },
293
+ { flags: [false, true], expected: 2 },
294
+ { flags: [true, true], expected: 3 },
295
+ { flags: [true, false, true, false, true], expected: 0b10101 },
296
+ {
297
+ flags: [true, true, true, true, true, true, true, true],
298
+ expected: 0xff,
299
+ },
300
+ ];
301
+
302
+ for (const { flags, expected } of cases) {
303
+ const buffer = createBuffer();
304
+ encodeFlags(buffer, flags);
305
+ assertEqual(buffer.unwrap()[0], expected);
306
+
307
+ assertEqual(
308
+ decodeFlags(
309
+ createBuffer(buffer.unwrap()),
310
+ PositiveInt.orThrow(flags.length),
311
+ ),
312
+ flags,
313
+ );
314
+ }
315
+
316
+ const buffer = createBuffer();
317
+ encodeFlags(buffer, [true, false, true, false, true, false, true, false]);
318
+ assertEqual(decodeFlags(buffer, PositiveInt.orThrow(9)), [
319
+ true,
320
+ false,
321
+ true,
322
+ false,
323
+ true,
324
+ false,
325
+ true,
326
+ false,
327
+ ]);
328
+ });
329
+ });
330
+
331
+ describe("non-negative integer encoding", () => {
332
+ it("encodes and decodes variable-length integers", () => {
333
+ const cases: ReadonlyArray<{
334
+ readonly input: NonNegativeInt;
335
+ readonly expected: ReadonlyArray<number>;
336
+ }> = [
337
+ { input: 0 as NonNegativeInt, expected: [0] },
338
+ { input: 1 as NonNegativeInt, expected: [1] },
339
+ { input: 127 as NonNegativeInt, expected: [127] },
340
+ { input: 128 as NonNegativeInt, expected: [128, 1] },
341
+ { input: 129 as NonNegativeInt, expected: [129, 1] },
342
+ { input: 255 as NonNegativeInt, expected: [255, 1] },
343
+ { input: 16383 as NonNegativeInt, expected: [255, 127] },
344
+ { input: 16384 as NonNegativeInt, expected: [128, 128, 1] },
345
+ { input: 32767 as NonNegativeInt, expected: [255, 255, 1] },
346
+ { input: 2097151 as NonNegativeInt, expected: [255, 255, 127] },
347
+ { input: 2097152 as NonNegativeInt, expected: [128, 128, 128, 1] },
348
+ {
349
+ input: 268435455 as NonNegativeInt,
350
+ expected: [255, 255, 255, 127],
351
+ },
352
+ {
353
+ input: Number.MAX_SAFE_INTEGER as NonNegativeInt,
354
+ expected: [255, 255, 255, 255, 255, 255, 255, 15],
355
+ },
356
+ {
357
+ input: (Number.MAX_SAFE_INTEGER - 1) as NonNegativeInt,
358
+ expected: [254, 255, 255, 255, 255, 255, 255, 15],
359
+ },
360
+ ];
361
+
362
+ for (const { input, expected } of cases) {
363
+ const encoded = createBuffer();
364
+ encodeNonNegativeInt(encoded, input);
365
+ assertEqualBytes(encoded.unwrap(), expected);
366
+ assertEqual(decodeNonNegativeInt(encoded), input);
367
+ }
368
+ });
369
+
370
+ it("rejects values outside the non-negative safe integer range", () => {
371
+ const buffer = createBuffer();
372
+ encodeNonNegativeInt(
373
+ buffer,
374
+ (Number.MAX_SAFE_INTEGER + 1) as NonNegativeInt,
375
+ );
376
+
377
+ const error = assertThrowsInstanceOf(
378
+ () => decodeNonNegativeInt(buffer),
379
+ BufferError,
380
+ );
381
+ assertEqual(
382
+ error.message,
383
+ "Decoded integer must be a non-negative safe integer.",
384
+ );
385
+ });
386
+
387
+ it("rejects input without a terminating byte", () => {
388
+ for (const byte of [0x80, 0xff]) {
389
+ const error = assertThrowsInstanceOf(
390
+ () => decodeNonNegativeInt(createBuffer(new Uint8Array(8).fill(byte))),
391
+ BufferError,
392
+ );
393
+ assertEqual(
394
+ error.message,
395
+ "Variable-length quantity must terminate within 8 bytes.",
396
+ );
397
+ }
398
+ });
399
+
400
+ it("rejects truncated input", () => {
401
+ const error = assertThrowsInstanceOf(
402
+ () => decodeNonNegativeInt(createBuffer([128])),
403
+ BufferError,
404
+ );
405
+ assertEqual(error.message, "Buffer parse ended prematurely");
406
+ });
407
+ });
408
+
409
+ describe("length encoding", () => {
410
+ it("encodes and decodes array-like lengths", () => {
411
+ let buffer = createBuffer();
412
+ encodeLength(buffer, []);
413
+ assertEqual(decodeLength(buffer), 0);
414
+
415
+ buffer = createBuffer();
416
+ encodeLength(buffer, [1, 2, 3]);
417
+ assertEqual(decodeLength(buffer), 3);
418
+ });
419
+ });
420
+
421
+ describe("string encoding", () => {
422
+ it("encodes and decodes length-prefixed UTF-8", () => {
423
+ const value = "Hello, world!";
424
+ const buffer = createBuffer();
425
+ encodeString(buffer, value);
426
+
427
+ assertEqualBytes(
428
+ buffer.unwrap(),
429
+ [13, 72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33],
430
+ );
431
+ assertEqual(decodeString(buffer), value);
432
+ });
433
+ });
434
+
435
+ describe("run-length encoding", () => {
436
+ it("encodes consecutive equal values", () => {
437
+ const encoder =
438
+ createRunLengthEncoder<NonNegativeInt>(encodeNonNegativeInt);
439
+
440
+ encoder.add(5 as NonNegativeInt);
441
+ encoder.add(5 as NonNegativeInt);
442
+ encoder.add(7 as NonNegativeInt);
443
+
444
+ assertEqualBytes(encoder.unwrap(), [5, 2, 7, 1]);
445
+ assertEqual(encoder.getLength(), 4);
446
+ });
447
+
448
+ it("distinguishes 0 from -0 when grouping runs", () => {
449
+ const encoder = createRunLengthEncoder<FiniteNumber>(encodeNumber);
450
+ encoder.add(FiniteNumber.orThrow(0));
451
+ encoder.add(FiniteNumber.orThrow(-0));
452
+
453
+ const buffer = createBuffer(encoder.unwrap());
454
+ const values = decodeRle(buffer, NonNegativeInt.orThrow(2), () =>
455
+ decodeNumber(buffer),
456
+ );
457
+
458
+ assertSame(values[0], 0);
459
+ assertSame(values[1], -0);
460
+ });
461
+
462
+ it("decodes a valid numeric run", () => {
463
+ const buffer = createBuffer();
464
+ encodeNonNegativeInt(buffer, NonNegativeInt.orThrow(5));
465
+ encodeNonNegativeInt(buffer, NonNegativeInt.orThrow(3));
466
+
467
+ assertEqual(
468
+ decodeRle(buffer, NonNegativeInt.orThrow(3), () =>
469
+ decodeNonNegativeInt(buffer),
470
+ ),
471
+ [5, 5, 5],
472
+ );
473
+ assertEqual(buffer.getLength(), 0);
474
+ });
475
+
476
+ it("supports non-integer values", () => {
477
+ const buffer = createBuffer();
478
+ encodeString(buffer, "value");
479
+ encodeNonNegativeInt(buffer, NonNegativeInt.orThrow(3));
480
+
481
+ assertEqual(
482
+ decodeRle(buffer, NonNegativeInt.orThrow(3), () => decodeString(buffer)),
483
+ ["value", "value", "value"],
484
+ );
485
+ assertEqual(buffer.getLength(), 0);
486
+ });
487
+
488
+ it("rejects a run length exceeding the remaining output length", () => {
489
+ const buffer = createBuffer();
490
+ encodeNonNegativeInt(buffer, NonNegativeInt.orThrow(1));
491
+ encodeNonNegativeInt(buffer, NonNegativeInt.orThrow(100000));
492
+
493
+ const error = assertThrowsInstanceOf(
494
+ () =>
495
+ decodeRle(buffer, NonNegativeInt.orThrow(2), () =>
496
+ decodeNonNegativeInt(buffer),
497
+ ),
498
+ BufferError,
499
+ );
500
+ assertEqual(
501
+ error.message,
502
+ "Invalid RLE encoding: runLength 100000 exceeds remaining 2",
503
+ );
504
+ });
505
+
506
+ it("rejects a zero run length", () => {
507
+ const buffer = createBuffer();
508
+ encodeNonNegativeInt(buffer, NonNegativeInt.orThrow(1));
509
+ encodeNonNegativeInt(buffer, zeroNonNegativeInt);
510
+
511
+ const error = assertThrowsInstanceOf(
512
+ () =>
513
+ decodeRle(buffer, NonNegativeInt.orThrow(1), () =>
514
+ decodeNonNegativeInt(buffer),
515
+ ),
516
+ BufferError,
517
+ );
518
+ assertEqual(
519
+ error.message,
520
+ "Invalid RLE encoding: runLength must be positive",
521
+ );
522
+ });
523
+ });
524
+
525
+ describe("JSON binary codec", () => {
526
+ it("matches fixed msgpackr 2.0.5 fixtures", () => {
527
+ const fixtures: ReadonlyArray<readonly [unknown, ReadonlyArray<number>]> = [
528
+ [false, [194]],
529
+ [
530
+ { name: "Ada", nested: { ok: true }, values: [1, 2, 3] },
531
+ [
532
+ 131, 164, 110, 97, 109, 101, 163, 65, 100, 97, 166, 110, 101, 115,
533
+ 116, 101, 100, 129, 162, 111, 107, 195, 166, 118, 97, 108, 117, 101,
534
+ 115, 147, 1, 2, 3,
535
+ ],
536
+ ],
537
+ ];
538
+
539
+ for (const [value, bytes] of fixtures) {
540
+ assertEqualBytes(encodeJson(value), bytes);
541
+ assertEqual(decodeJson(bytes), value);
542
+ }
543
+ });
544
+
545
+ it("encodes every integer boundary", () => {
546
+ const fixtures: ReadonlyArray<readonly [number, ReadonlyArray<number>]> = [
547
+ [0, [0]],
548
+ [127, [127]],
549
+ [128, [0xcc, 0x80]],
550
+ [255, [0xcc, 0xff]],
551
+ [256, [0xcd, 0x01, 0x00]],
552
+ [65535, [0xcd, 0xff, 0xff]],
553
+ [65536, [0xce, 0x00, 0x01, 0x00, 0x00]],
554
+ [0xffffffff, [0xce, 0xff, 0xff, 0xff, 0xff]],
555
+ [-1, [0xff]],
556
+ [-32, [0xe0]],
557
+ [-33, [0xd0, 0xdf]],
558
+ [-128, [0xd0, 0x80]],
559
+ [-129, [0xd1, 0xff, 0x7f]],
560
+ [-32768, [0xd1, 0x80, 0x00]],
561
+ [-32769, [0xd2, 0xff, 0xff, 0x7f, 0xff]],
562
+ [-2147483648, [0xd2, 0x80, 0x00, 0x00, 0x00]],
563
+ ];
564
+
565
+ for (const [value, bytes] of fixtures) {
566
+ assertEqualBytes(encodeJson(value), bytes);
567
+ assertSame(decodeJson(bytes), value);
568
+ }
569
+ });
570
+
571
+ it("uses float64 for other finite numbers", () => {
572
+ for (const value of [1.25, -2147483649, 0x100000000, Number.MAX_VALUE]) {
573
+ assertEqual(encodeJson(value), float64Bytes(value));
574
+ assertSame(decodeJson(float64Bytes(value)), value);
575
+ }
576
+
577
+ const negativeZeroBytes = new Uint8Array([0xcb, 0x80, 0, 0, 0, 0, 0, 0, 0]);
578
+ assertEqual(encodeJson(-0), negativeZeroBytes);
579
+ assertSame(decodeJson(negativeZeroBytes), -0);
580
+ });
581
+
582
+ it("decodes finite float32 values", () => {
583
+ assertEqual(decodeJson([0xca, 0x3f, 0xc0, 0, 0]), 1.5);
584
+ assertSame(decodeJson([0xca, 0x80, 0, 0, 0]), -0);
585
+ });
586
+
587
+ for (const { name, bytes } of [
588
+ { name: "float32 NaN", bytes: [0xca, 0x7f, 0xc0, 0, 0] },
589
+ { name: "positive float32 Infinity", bytes: [0xca, 0x7f, 0x80, 0, 0] },
590
+ { name: "negative float32 Infinity", bytes: [0xca, 0xff, 0x80, 0, 0] },
591
+ {
592
+ name: "float64 NaN",
593
+ bytes: [0xcb, 0x7f, 0xf8, 0, 0, 0, 0, 0, 0],
594
+ },
595
+ {
596
+ name: "positive float64 Infinity",
597
+ bytes: [0xcb, 0x7f, 0xf0, 0, 0, 0, 0, 0, 0],
598
+ },
599
+ {
600
+ name: "negative float64 Infinity",
601
+ bytes: [0xcb, 0xff, 0xf0, 0, 0, 0, 0, 0, 0],
602
+ },
603
+ ]) {
604
+ it(`rejects ${name}`, () => {
605
+ {
606
+ const thrown = assertThrowsInstanceOf(
607
+ () => decodeJson(bytes),
608
+ BufferError,
609
+ );
610
+
611
+ assertTrue(
612
+ thrown.message.includes("A decoded JSON number must be finite."),
613
+ );
614
+ }
615
+ });
616
+ }
617
+
618
+ it("encodes strings by UTF-8 byte length", () => {
619
+ assertEqualBytes(encodeJson(""), [0xa0]);
620
+ assertEqualBytes(encodeJson("abc"), [0xa3, 0x61, 0x62, 0x63]);
621
+ assertEqualBytes(
622
+ encodeJson("é€🌍"),
623
+ [0xa9, 0xc3, 0xa9, 0xe2, 0x82, 0xac, 0xf0, 0x9f, 0x8c, 0x8d],
624
+ );
625
+ assertEqualBytes(encodeJson("é".repeat(16)).subarray(0, 2), [0xd9, 32]);
626
+ });
627
+
628
+ it("supports every string header boundary", () => {
629
+ const fixtures: ReadonlyArray<readonly [number, ReadonlyArray<number>]> = [
630
+ [31, [0xbf]],
631
+ [32, [0xd9, 32]],
632
+ [255, [0xd9, 255]],
633
+ [256, [0xda, 1, 0]],
634
+ [65535, [0xda, 255, 255]],
635
+ [65536, [0xdb, 0, 1, 0, 0]],
636
+ ];
637
+
638
+ for (const [length, header] of fixtures) {
639
+ const value = "a".repeat(length);
640
+ const bytes = encodeJson(value);
641
+ assertEqualBytes(bytes.subarray(0, header.length), header);
642
+ assertSame(decodeJson(bytes), value);
643
+ }
644
+
645
+ const expandedToStr32 = "€".repeat(21846);
646
+ assertEqualBytes(
647
+ encodeJson(expandedToStr32).subarray(0, 5),
648
+ [0xdb, 0, 1, 0, 2],
649
+ );
650
+
651
+ const expandedToStr16 = "€".repeat(86);
652
+ const expandedToStr16Bytes = encodeJson(expandedToStr16);
653
+ assertEqualBytes(expandedToStr16Bytes.subarray(0, 3), [0xda, 1, 2]);
654
+ assertSame(decodeJson(expandedToStr16Bytes), expandedToStr16);
655
+ });
656
+
657
+ it("decodes every short ASCII length and non-ASCII fallback position", () => {
658
+ for (let length = 0; length < 16; length++) {
659
+ const value = "a".repeat(length);
660
+ assertSame(decodeJson(encodeJson(value)), value);
661
+ }
662
+
663
+ for (let nonAsciiIndex = 0; nonAsciiIndex < 14; nonAsciiIndex++) {
664
+ const value = `${"a".repeat(nonAsciiIndex)}é${"a".repeat(13 - nonAsciiIndex)}`;
665
+ assertEqual(encodeJson(value)[0], 0xaf);
666
+ assertSame(decodeJson(encodeJson(value)), value);
667
+ }
668
+
669
+ const error = assertThrowsInstanceOf(
670
+ () => decodeJson([0xaf, ...Array.from({ length: 14 }, () => 0x61), 0xc2]),
671
+ Error,
672
+ );
673
+ assertTrue(error.message.includes("Invalid UTF-8 string encoding."));
674
+ });
675
+
676
+ // Evolu's MessagePack profile uses WTF-8 for lone surrogates so every
677
+ // JsonValue string round-trips without loss. Other malformed UTF-8 is rejected.
678
+ it("round-trips surrogate pairs and lone surrogates", () => {
679
+ const fixtures: ReadonlyArray<readonly [string, ReadonlyArray<number>]> = [
680
+ ["\uD83C\uDF0D", [0xa4, 0xf0, 0x9f, 0x8c, 0x8d]],
681
+ ["\uD800", [0xa3, 0xed, 0xa0, 0x80]],
682
+ ["\uDFFF", [0xa3, 0xed, 0xbf, 0xbf]],
683
+ ];
684
+
685
+ for (const [value, bytes] of fixtures) {
686
+ assertEqualBytes(encodeJson(value), bytes);
687
+ assertSame(decodeJson(bytes), value);
688
+ }
689
+
690
+ const longValue = `${"a".repeat(64)}\uD800`;
691
+ assertSame(decodeJson(encodeJson(longValue)), longValue);
692
+ });
693
+
694
+ for (const { name, bytes } of [
695
+ { name: "a stray UTF-8 continuation byte", bytes: [0xa1, 0x80] },
696
+ { name: "an overlong two-byte UTF-8 sequence", bytes: [0xa2, 0xc0, 0x80] },
697
+ {
698
+ name: "an invalid two-byte UTF-8 continuation",
699
+ bytes: [0xa2, 0xc2, 0x41],
700
+ },
701
+ {
702
+ name: "an overlong three-byte UTF-8 sequence",
703
+ bytes: [0xa3, 0xe0, 0x80, 0x80],
704
+ },
705
+ {
706
+ name: "an invalid three-byte UTF-8 continuation",
707
+ bytes: [0xa3, 0xe1, 0x80, 0x41],
708
+ },
709
+ {
710
+ name: "an overlong four-byte UTF-8 sequence",
711
+ bytes: [0xa4, 0xf0, 0x80, 0x80, 0x80],
712
+ },
713
+ {
714
+ name: "an invalid four-byte UTF-8 continuation",
715
+ bytes: [0xa4, 0xf1, 0x80, 0x80, 0x41],
716
+ },
717
+ {
718
+ name: "a UTF-8 code point above U+10FFFF",
719
+ bytes: [0xa4, 0xf4, 0x90, 0x80, 0x80],
720
+ },
721
+ { name: "UTF-8 leading byte 0xf5", bytes: [0xa1, 0xf5] },
722
+ { name: "UTF-8 leading byte 0xf8", bytes: [0xa1, 0xf8] },
723
+ { name: "UTF-8 leading byte 0xfc", bytes: [0xa1, 0xfc] },
724
+ ]) {
725
+ it(`rejects ${name}`, () => {
726
+ {
727
+ const thrown = assertThrowsInstanceOf(
728
+ () => decodeJson(bytes),
729
+ BufferError,
730
+ );
731
+
732
+ assertTrue(thrown.message.includes("Invalid UTF-8 string encoding."));
733
+ }
734
+ });
735
+ }
736
+
737
+ it("rejects UTF-8 truncated within string data", () => {
738
+ const twoByteError = assertThrowsInstanceOf(
739
+ () => decodeJson([0xa1, 0xc2]),
740
+ Error,
741
+ );
742
+ assertTrue(twoByteError.message.includes("Invalid UTF-8 string encoding."));
743
+ const threeByteError = assertThrowsInstanceOf(
744
+ () => decodeJson([0xa2, 0xe1, 0x80]),
745
+ Error,
746
+ );
747
+ assertTrue(
748
+ threeByteError.message.includes("Invalid UTF-8 string encoding."),
749
+ );
750
+ const fourByteError = assertThrowsInstanceOf(
751
+ () => decodeJson([0xa3, 0xf1, 0x80, 0x80]),
752
+ Error,
753
+ );
754
+ assertTrue(
755
+ fourByteError.message.includes("Invalid UTF-8 string encoding."),
756
+ );
757
+ });
758
+
759
+ it("accepts scalar and canonical WTF-8 boundaries", () => {
760
+ assertEqual(decodeJson([0xa3, 0xed, 0x9f, 0xbf]), "\uD7FF");
761
+ assertEqual(decodeJson([0xa3, 0xed, 0xa0, 0x80]), "\uD800");
762
+ assertEqual(decodeJson([0xa4, 0xf4, 0x8f, 0xbf, 0xbf]), "\u{10FFFF}");
763
+ });
764
+
765
+ it("supports every array header boundary", () => {
766
+ const fixtures: ReadonlyArray<readonly [number, ReadonlyArray<number>]> = [
767
+ [0, [0x90]],
768
+ [15, [0x9f]],
769
+ [16, [0xdc, 0, 16]],
770
+ [65535, [0xdc, 255, 255]],
771
+ [65536, [0xdd, 0, 1, 0, 0]],
772
+ ];
773
+
774
+ for (const [length, header] of fixtures) {
775
+ const value = Array.from({ length }, () => null);
776
+ const bytes = encodeJson(value);
777
+ assertEqualBytes(bytes.subarray(0, header.length), header);
778
+ assertEqual(decodeJson(bytes), value);
779
+ }
780
+
781
+ assertEqual(decodeJson(encodeJson([[], [null, [true]]])), [
782
+ [],
783
+ [null, [true]],
784
+ ]);
785
+ assertEqual(decodeJson([0xdc, 0, 0]), []);
786
+ assertEqual(decodeJson([0xdd, 0, 0, 0, 0]), []);
787
+ });
788
+
789
+ it("supports every map header boundary", () => {
790
+ const fixtures: ReadonlyArray<readonly [number, ReadonlyArray<number>]> = [
791
+ [0, [0x80]],
792
+ [15, [0x8f]],
793
+ [16, [0xde, 0, 16]],
794
+ [65535, [0xde, 255, 255]],
795
+ [65536, [0xdf, 0, 1, 0, 0]],
796
+ ];
797
+
798
+ for (const [length, header] of fixtures) {
799
+ const value = createJsonObject(length);
800
+ const bytes = encodeJson(value);
801
+ assertEqualBytes(bytes.subarray(0, header.length), header);
802
+ assertEqual(decodeJson(bytes), value);
803
+ }
804
+
805
+ assertEqual(decodeJson(encodeJson({ nested: { empty: {} } })), {
806
+ nested: { empty: {} },
807
+ });
808
+ assertEqual(decodeJson([0xde, 0, 0]), {});
809
+ assertEqual(decodeJson([0xdf, 0, 0, 0, 0]), {});
810
+ });
811
+
812
+ it("limits JSON nesting consistently", () => {
813
+ const maxDepth = 1_000;
814
+ let value: unknown = null;
815
+
816
+ for (let index = 0; index < maxDepth; index++) value = [value];
817
+
818
+ const bytes = encodeJson(value);
819
+ let decoded: unknown = decodeJson(bytes);
820
+
821
+ for (let index = 0; index < maxDepth; index++) {
822
+ if (!Array.isArray(decoded) || decoded.length !== 1) {
823
+ throw new Error("Expected a single-item nested array.");
824
+ }
825
+ decoded = decoded[0];
826
+ }
827
+ assertSame(decoded, null);
828
+
829
+ value = [value];
830
+ const encodingError = assertThrowsInstanceOf(
831
+ () => encodeJson(value),
832
+ Error,
833
+ );
834
+ assertTrue(
835
+ encodingError.message.includes(
836
+ "JSON nesting exceeds the maximum depth of 1000.",
837
+ ),
838
+ );
839
+
840
+ const tooDeepArrayBytes = new Uint8Array(maxDepth + 2);
841
+ tooDeepArrayBytes.fill(0x91, 0, maxDepth + 1);
842
+ tooDeepArrayBytes[maxDepth + 1] = 0xc0;
843
+ const tooDeepArrayBuffer = createBuffer(tooDeepArrayBytes);
844
+ const arrayDecodingError = assertThrowsInstanceOf(
845
+ () => decodeJsonValue(tooDeepArrayBuffer),
846
+ Error,
847
+ );
848
+ assertTrue(
849
+ arrayDecodingError.message.includes(
850
+ "JSON nesting exceeds the maximum depth of 1000.",
851
+ ),
852
+ );
853
+ assertEqual(tooDeepArrayBuffer.unwrap(), tooDeepArrayBytes);
854
+
855
+ const tooDeepMapBytes = new Uint8Array((maxDepth + 1) * 3 + 1);
856
+ let position = 0;
857
+ for (let index = 0; index <= maxDepth; index++) {
858
+ tooDeepMapBytes[position++] = 0x81;
859
+ tooDeepMapBytes[position++] = 0xa1;
860
+ tooDeepMapBytes[position++] = 0x76;
861
+ }
862
+ tooDeepMapBytes[position] = 0xc0;
863
+ const tooDeepMapBuffer = createBuffer(tooDeepMapBytes);
864
+ const mapDecodingError = assertThrowsInstanceOf(
865
+ () => decodeJsonValue(tooDeepMapBuffer),
866
+ Error,
867
+ );
868
+ assertTrue(
869
+ mapDecodingError.message.includes(
870
+ "JSON nesting exceeds the maximum depth of 1000.",
871
+ ),
872
+ );
873
+ assertEqual(tooDeepMapBuffer.unwrap(), tooDeepMapBytes);
874
+ });
875
+
876
+ it("encodes null-prototype objects", () => {
877
+ const value = globalThis.Object.assign(
878
+ globalThis.Object.create(null) as Record<string, unknown>,
879
+ {
880
+ key: "value",
881
+ },
882
+ );
883
+ const decoded = decodeJson(encodeJson(value));
884
+
885
+ assertEqual(decoded, { key: "value" });
886
+ assertSame(
887
+ globalThis.Object.getPrototypeOf(decoded),
888
+ globalThis.Object.prototype,
889
+ );
890
+ });
891
+
892
+ it("creates an own __proto__ data property", () => {
893
+ const value = globalThis.Object.create(null) as Record<string, unknown>;
894
+ value.__proto__ = { safe: true };
895
+
896
+ const decoded = decodeJson(encodeJson(value));
897
+
898
+ assertSame(
899
+ globalThis.Object.getPrototypeOf(decoded),
900
+ globalThis.Object.prototype,
901
+ );
902
+ assertTrue(globalThis.Object.hasOwn(decoded as object, "__proto__"));
903
+ assertEqual(
904
+ globalThis.Object.getOwnPropertyDescriptor(decoded, "__proto__"),
905
+ {
906
+ value: { safe: true },
907
+ configurable: true,
908
+ enumerable: true,
909
+ writable: true,
910
+ },
911
+ );
912
+ });
913
+
914
+ it("uses the last value for duplicate map keys", () => {
915
+ assertEqual(decodeJson([0x82, 0xa1, 0x61, 1, 0xa1, 0x61, 2]), {
916
+ a: 2,
917
+ });
918
+ const decoded = decodeJson([
919
+ 0x82, 0xa9, 0x5f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x5f, 0x5f, 1, 0xa9,
920
+ 0x5f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x5f, 0x5f, 2,
921
+ ]);
922
+ assertType(Object, decoded);
923
+ assertTrue("__proto__" in decoded);
924
+ });
925
+
926
+ it("decodes every string header for object keys", () => {
927
+ assertEqual(decodeJson([0x81, 0xd9, 1, 0x61, 1]), { a: 1 });
928
+ assertEqual(decodeJson([0x81, 0xda, 0, 1, 0x61, 1]), { a: 1 });
929
+ assertEqual(decodeJson([0x81, 0xdb, 0, 0, 0, 1, 0x61, 1]), { a: 1 });
930
+
931
+ const longKey = "a".repeat(33);
932
+ assertEqual(decodeJson(encodeJson({ [longKey]: 1 })), {
933
+ [longKey]: 1,
934
+ });
935
+ });
936
+
937
+ it("handles object key cache collisions", () => {
938
+ assertEqual(decodeJson(encodeJson({ "": 0 })), { "": 0 });
939
+ assertEqual(decodeJson(encodeJson({ " ": 1 })), { " ": 1 });
940
+ assertEqual(decodeJson(encodeJson({ abc: 2 })), { abc: 2 });
941
+ assertEqual(decodeJson(encodeJson({ abd: 3 })), { abd: 3 });
942
+ assertEqual(decodeJson(encodeJson({ abc: 4 })), { abc: 4 });
943
+ });
944
+
945
+ it("rejects maps with non-string keys", () => {
946
+ {
947
+ const thrown = assertThrowsInstanceOf(
948
+ () => decodeJson([0x81, 1, 2]),
949
+ BufferError,
950
+ );
951
+
952
+ assertTrue(
953
+ thrown.message.includes("A decoded JSON object key must be a string."),
954
+ );
955
+ }
956
+ });
957
+
958
+ for (const marker of [
959
+ 0xc1, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xcf, 0xd3, 0xd4, 0xd5, 0xd6,
960
+ 0xd7, 0xd8,
961
+ ]) {
962
+ it(`rejects unsupported marker 0x${marker.toString(16)}`, () => {
963
+ {
964
+ const thrown = assertThrowsInstanceOf(
965
+ () => decodeJson([marker]),
966
+ BufferError,
967
+ );
968
+
969
+ assertTrue(thrown.message.includes("Unsupported MessagePack marker"));
970
+ }
971
+ });
972
+ }
973
+
974
+ for (const { name, bytes } of [
975
+ { name: "empty data", bytes: [] },
976
+ { name: "a truncated uint16 value", bytes: [0xcd, 0] },
977
+ { name: "a truncated uint32 value", bytes: [0xce, 0, 0, 0] },
978
+ { name: "a truncated int16 value", bytes: [0xd1, 0] },
979
+ { name: "a truncated int32 value", bytes: [0xd2, 0, 0, 0] },
980
+ { name: "a truncated float32 value", bytes: [0xca, 0, 0, 0] },
981
+ {
982
+ name: "a truncated float64 value",
983
+ bytes: [0xcb, 0, 0, 0, 0, 0, 0, 0],
984
+ },
985
+ { name: "a truncated str8 header", bytes: [0xd9] },
986
+ { name: "a truncated str16 header", bytes: [0xda, 0] },
987
+ { name: "a truncated str32 header", bytes: [0xdb, 0, 0, 0] },
988
+ { name: "a truncated array16 header", bytes: [0xdc, 0] },
989
+ { name: "a truncated array32 header", bytes: [0xdd, 0, 0, 0] },
990
+ { name: "a truncated map16 header", bytes: [0xde, 0] },
991
+ { name: "a truncated map32 header", bytes: [0xdf, 0, 0, 0] },
992
+ { name: "missing fixstr data", bytes: [0xa1] },
993
+ ]) {
994
+ it(`rejects ${name}`, () => {
995
+ {
996
+ const thrown = assertThrowsInstanceOf(
997
+ () => decodeJson(bytes),
998
+ BufferError,
999
+ );
1000
+
1001
+ assertTrue(thrown.message.includes("Buffer parse ended prematurely"));
1002
+ }
1003
+ });
1004
+ }
1005
+
1006
+ it("rejects impossible declared lengths", () => {
1007
+ const array32Error = assertThrowsInstanceOf(
1008
+ () => decodeJson([0xdd, 0xff, 0xff, 0xff, 0xff]),
1009
+ Error,
1010
+ );
1011
+ assertTrue(array32Error.message.includes("Buffer parse ended prematurely"));
1012
+ const map32Error = assertThrowsInstanceOf(
1013
+ () => decodeJson([0xdf, 0xff, 0xff, 0xff, 0xff]),
1014
+ Error,
1015
+ );
1016
+ assertTrue(map32Error.message.includes("Buffer parse ended prematurely"));
1017
+ const string32Error = assertThrowsInstanceOf(
1018
+ () => decodeJson([0xdb, 0xff, 0xff, 0xff, 0xff]),
1019
+ Error,
1020
+ );
1021
+ assertTrue(
1022
+ string32Error.message.includes("Buffer parse ended prematurely"),
1023
+ );
1024
+ const array16Error = assertThrowsInstanceOf(
1025
+ () => decodeJson([0xdc, 0, 2, 0xc0]),
1026
+ Error,
1027
+ );
1028
+ assertTrue(array16Error.message.includes("Buffer parse ended prematurely"));
1029
+ const map16Error = assertThrowsInstanceOf(
1030
+ () => decodeJson([0xde, 0, 1, 0xa0]),
1031
+ Error,
1032
+ );
1033
+ assertTrue(map16Error.message.includes("Buffer parse ended prematurely"));
1034
+ });
1035
+
1036
+ it("decodes exactly one value at a time", () => {
1037
+ const buffer = createBuffer();
1038
+ encodeJsonValue(buffer, JsonValue.orThrow({ first: true }));
1039
+ encodeJsonValue(buffer, JsonValue.orThrow(["second"]));
1040
+
1041
+ assertEqual(decodeJsonValue(buffer), { first: true });
1042
+ assertEqual(decodeJsonValue(buffer), ["second"]);
1043
+ assertEqual(buffer.getLength(), 0);
1044
+ });
1045
+
1046
+ it("leaves the Buffer unchanged after decoding fails", () => {
1047
+ const bytes = new Uint8Array([0x92, 1]);
1048
+ const buffer = createBuffer(bytes);
1049
+
1050
+ assertThrowsInstanceOf(() => decodeJsonValue(buffer), BufferError);
1051
+ assertEqual(buffer.unwrap(), bytes);
1052
+ });
1053
+
1054
+ it("resets reusable encoder state after appending fails", () => {
1055
+ const failedBuffer = {
1056
+ ...createBuffer(),
1057
+ extend: () => {
1058
+ throw new BufferError("append failed");
1059
+ },
1060
+ };
1061
+
1062
+ const error = assertThrowsInstanceOf(
1063
+ () => encodeJsonValue(failedBuffer, JsonValue.orThrow("first")),
1064
+ Error,
1065
+ );
1066
+ assertTrue(error.message.includes("append failed"));
1067
+ assertEqualBytes(
1068
+ encodeJson("second"),
1069
+ [0xa6, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64],
1070
+ );
1071
+ });
1072
+
1073
+ it("rejects reentrant encoding without corrupting encoder state", () => {
1074
+ const reentrantBuffer = {
1075
+ ...createBuffer(),
1076
+ extend: () => {
1077
+ encodeJsonValue(createBuffer(), JsonValue.orThrow("nested"));
1078
+ },
1079
+ };
1080
+
1081
+ const error = assertThrowsInstanceOf(
1082
+ () => encodeJsonValue(reentrantBuffer, JsonValue.orThrow({ a: true })),
1083
+ Error,
1084
+ );
1085
+ assertTrue(
1086
+ error.message.includes("Reentrant JSON encoding is not supported."),
1087
+ );
1088
+ assertEqualBytes(encodeJson({ a: true }), [0x81, 0xa1, 0x61, 0xc3]);
1089
+ });
1090
+
1091
+ it("rejects collection lengths beyond MessagePack uint32", () => {
1092
+ const value = {};
1093
+ const objectKeys = globalThis.Object.keys;
1094
+ const keysSpy = mock.method(
1095
+ globalThis.Object,
1096
+ "keys",
1097
+ (object: object): Array<string> =>
1098
+ object === value
1099
+ ? ({ length: 0x100000000 } as unknown as Array<string>)
1100
+ : objectKeys(object),
1101
+ );
1102
+
1103
+ try {
1104
+ const error = assertThrowsInstanceOf(() => encodeJson(value), Error);
1105
+ assertTrue(
1106
+ error.message.includes(
1107
+ "Collection length exceeds the MessagePack uint32 limit.",
1108
+ ),
1109
+ );
1110
+ } finally {
1111
+ keysSpy.mock.restore();
1112
+ }
1113
+ });
1114
+
1115
+ it("converts unexpected decoder errors to BufferError", () => {
1116
+ const errorBuffer = {
1117
+ ...createBuffer([0xc0]),
1118
+ shiftN: () => {
1119
+ throw new Error("unexpected");
1120
+ },
1121
+ };
1122
+ const error = assertThrowsInstanceOf(
1123
+ () => decodeJsonValue(errorBuffer),
1124
+ Error,
1125
+ );
1126
+ assertTrue(error.message.includes("Invalid MessagePack data"));
1127
+ });
1128
+ });