@evolu/common 8.7.0 → 8.8.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 (300) 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 +478 -141
  4. package/dist/src/Assert.d.ts.map +1 -1
  5. package/dist/src/Assert.js +644 -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.js +4 -1
  40. package/dist/src/Random.d.ts +1 -1
  41. package/dist/src/Result.d.ts +49 -54
  42. package/dist/src/Result.d.ts.map +1 -1
  43. package/dist/src/Result.js +17 -22
  44. package/dist/src/Set.d.ts +6 -6
  45. package/dist/src/Set.js +2 -2
  46. package/dist/src/Sqlite.js +1 -1
  47. package/dist/src/String.js +3 -2
  48. package/dist/src/Task.d.ts +32 -32
  49. package/dist/src/Task.d.ts.map +1 -1
  50. package/dist/src/Task.js +20 -22
  51. package/dist/src/Test.d.ts +31 -3
  52. package/dist/src/Test.d.ts.map +1 -1
  53. package/dist/src/Test.js +46 -3
  54. package/dist/src/Time.d.ts +3 -11
  55. package/dist/src/Time.d.ts.map +1 -1
  56. package/dist/src/Time.js +10 -18
  57. package/dist/src/Type.d.ts +87 -85
  58. package/dist/src/Type.d.ts.map +1 -1
  59. package/dist/src/Type.js +70 -63
  60. package/dist/src/Types.d.ts +12 -12
  61. package/dist/src/Types.js +1 -1
  62. package/dist/src/WebSocket.d.ts +4 -7
  63. package/dist/src/WebSocket.d.ts.map +1 -1
  64. package/dist/src/Worker.d.ts +1 -1
  65. package/dist/src/Worker.js +7 -7
  66. package/dist/src/index.d.ts +1 -1
  67. package/dist/src/index.js +1 -1
  68. package/dist/src/intl/_en.d.ts.map +1 -1
  69. package/dist/src/intl/_en.js +1 -1
  70. package/dist/src/intl/ar.d.ts.map +1 -1
  71. package/dist/src/intl/ar.js +1 -1
  72. package/dist/src/intl/bn.d.ts.map +1 -1
  73. package/dist/src/intl/bn.js +1 -1
  74. package/dist/src/intl/ca.d.ts.map +1 -1
  75. package/dist/src/intl/ca.js +1 -1
  76. package/dist/src/intl/cs.d.ts.map +1 -1
  77. package/dist/src/intl/cs.js +1 -1
  78. package/dist/src/intl/da.d.ts.map +1 -1
  79. package/dist/src/intl/da.js +1 -1
  80. package/dist/src/intl/de.d.ts.map +1 -1
  81. package/dist/src/intl/de.js +1 -1
  82. package/dist/src/intl/el.d.ts.map +1 -1
  83. package/dist/src/intl/el.js +1 -1
  84. package/dist/src/intl/es.d.ts.map +1 -1
  85. package/dist/src/intl/es.js +1 -1
  86. package/dist/src/intl/fa.d.ts.map +1 -1
  87. package/dist/src/intl/fa.js +1 -1
  88. package/dist/src/intl/fi.d.ts.map +1 -1
  89. package/dist/src/intl/fi.js +1 -1
  90. package/dist/src/intl/fil.d.ts.map +1 -1
  91. package/dist/src/intl/fil.js +1 -1
  92. package/dist/src/intl/fr.d.ts.map +1 -1
  93. package/dist/src/intl/fr.js +1 -1
  94. package/dist/src/intl/he.d.ts.map +1 -1
  95. package/dist/src/intl/he.js +1 -1
  96. package/dist/src/intl/hi.d.ts.map +1 -1
  97. package/dist/src/intl/hi.js +1 -1
  98. package/dist/src/intl/hr.d.ts.map +1 -1
  99. package/dist/src/intl/hr.js +1 -1
  100. package/dist/src/intl/hu.d.ts.map +1 -1
  101. package/dist/src/intl/hu.js +1 -1
  102. package/dist/src/intl/id.d.ts.map +1 -1
  103. package/dist/src/intl/id.js +1 -1
  104. package/dist/src/intl/it.d.ts.map +1 -1
  105. package/dist/src/intl/it.js +1 -1
  106. package/dist/src/intl/ja.d.ts.map +1 -1
  107. package/dist/src/intl/ja.js +1 -1
  108. package/dist/src/intl/ko.d.ts.map +1 -1
  109. package/dist/src/intl/ko.js +1 -1
  110. package/dist/src/intl/ml.d.ts.map +1 -1
  111. package/dist/src/intl/ml.js +1 -1
  112. package/dist/src/intl/mr.d.ts.map +1 -1
  113. package/dist/src/intl/mr.js +1 -1
  114. package/dist/src/intl/ms.d.ts.map +1 -1
  115. package/dist/src/intl/ms.js +1 -1
  116. package/dist/src/intl/nb.d.ts.map +1 -1
  117. package/dist/src/intl/nb.js +1 -1
  118. package/dist/src/intl/nl.d.ts.map +1 -1
  119. package/dist/src/intl/nl.js +1 -1
  120. package/dist/src/intl/pa.d.ts.map +1 -1
  121. package/dist/src/intl/pa.js +1 -1
  122. package/dist/src/intl/pl.d.ts.map +1 -1
  123. package/dist/src/intl/pl.js +1 -1
  124. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  125. package/dist/src/intl/pt-BR.js +1 -1
  126. package/dist/src/intl/pt.d.ts.map +1 -1
  127. package/dist/src/intl/pt.js +1 -1
  128. package/dist/src/intl/ro.d.ts.map +1 -1
  129. package/dist/src/intl/ro.js +1 -1
  130. package/dist/src/intl/sk.d.ts.map +1 -1
  131. package/dist/src/intl/sk.js +1 -1
  132. package/dist/src/intl/sl.d.ts.map +1 -1
  133. package/dist/src/intl/sl.js +1 -1
  134. package/dist/src/intl/sv.d.ts.map +1 -1
  135. package/dist/src/intl/sv.js +1 -1
  136. package/dist/src/intl/sw.d.ts.map +1 -1
  137. package/dist/src/intl/sw.js +1 -1
  138. package/dist/src/intl/ta.d.ts.map +1 -1
  139. package/dist/src/intl/ta.js +1 -1
  140. package/dist/src/intl/te.d.ts.map +1 -1
  141. package/dist/src/intl/te.js +1 -1
  142. package/dist/src/intl/th.d.ts.map +1 -1
  143. package/dist/src/intl/th.js +1 -1
  144. package/dist/src/intl/tr.d.ts.map +1 -1
  145. package/dist/src/intl/tr.js +1 -1
  146. package/dist/src/intl/uk.d.ts.map +1 -1
  147. package/dist/src/intl/uk.js +1 -1
  148. package/dist/src/intl/ur.d.ts.map +1 -1
  149. package/dist/src/intl/ur.js +1 -1
  150. package/dist/src/intl/vi.d.ts.map +1 -1
  151. package/dist/src/intl/vi.js +1 -1
  152. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  153. package/dist/src/intl/zh-CN.js +1 -1
  154. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  155. package/dist/src/intl/zh-TW.js +1 -1
  156. package/dist/src/local-first/Evolu.d.ts +12 -12
  157. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  158. package/dist/src/local-first/Evolu.js +2 -2
  159. package/dist/src/local-first/Protocol.d.ts +14 -76
  160. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  161. package/dist/src/local-first/Protocol.js +29 -206
  162. package/dist/src/local-first/Query.d.ts +11 -11
  163. package/dist/src/local-first/Query.js +6 -6
  164. package/dist/src/local-first/Relay.d.ts +2 -2
  165. package/dist/src/local-first/Schema.d.ts +3 -5
  166. package/dist/src/local-first/Schema.d.ts.map +1 -1
  167. package/dist/src/local-first/Schema.js +2 -4
  168. package/dist/src/local-first/Shared.d.ts.map +1 -1
  169. package/dist/src/local-first/Shared.js +5 -5
  170. package/dist/src/local-first/Storage.js +4 -4
  171. package/dist/src/local-first/Timestamp.d.ts +9 -0
  172. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  173. package/dist/src/local-first/Timestamp.js +13 -10
  174. package/package.json +7 -4
  175. package/src/Array.test.ts +809 -0
  176. package/src/Array.ts +21 -21
  177. package/src/Assert.test.ts +1181 -0
  178. package/src/Assert.ts +971 -227
  179. package/src/BigInt.test.ts +28 -0
  180. package/src/Binary.test.ts +1128 -0
  181. package/src/{Buffer.ts → Binary.ts} +273 -46
  182. package/src/Brand.test.ts +90 -0
  183. package/src/Cache.test.ts +183 -0
  184. package/src/Callbacks.test.ts +66 -0
  185. package/src/Console.test.ts +809 -0
  186. package/src/Console.ts +9 -17
  187. package/src/Crypto.test.ts +138 -0
  188. package/src/Crypto.ts +5 -5
  189. package/src/Eq.test.ts +333 -0
  190. package/src/Eq.ts +110 -176
  191. package/src/Error.test.ts +118 -0
  192. package/src/Function.test.ts +204 -0
  193. package/src/Function.ts +10 -15
  194. package/src/Http.test.ts +816 -0
  195. package/src/Http.ts +6 -7
  196. package/src/LeakDetector.test.ts +210 -0
  197. package/src/LeakDetector.ts +1 -0
  198. package/src/LockManager.ts +2 -2
  199. package/src/Lookup.test.ts +418 -0
  200. package/src/Lookup.ts +4 -3
  201. package/src/Microtask.test.ts +140 -0
  202. package/src/Number.test.ts +218 -0
  203. package/src/Number.ts +2 -2
  204. package/src/Object.test.ts +293 -0
  205. package/src/Object.ts +5 -4
  206. package/src/Option.test.ts +104 -0
  207. package/src/Option.ts +1 -2
  208. package/src/Order.test.ts +56 -0
  209. package/src/Platform.ts +2 -0
  210. package/src/Polyfills.ts +3 -4
  211. package/src/Random.test.ts +89 -0
  212. package/src/Random.ts +1 -1
  213. package/src/Ref.test.ts +139 -0
  214. package/src/RefCount.test.ts +184 -0
  215. package/src/Relation.test.ts +505 -0
  216. package/src/Resource.test.ts +4623 -0
  217. package/src/Result.test.ts +1829 -0
  218. package/src/Result.ts +49 -54
  219. package/src/Schedule.test.ts +1554 -0
  220. package/src/Set.test.ts +279 -0
  221. package/src/Set.ts +6 -6
  222. package/src/Sqlite.ts +1 -1
  223. package/src/Store.test.ts +288 -0
  224. package/src/String.test.ts +54 -0
  225. package/src/String.ts +3 -3
  226. package/src/Task.test.ts +10820 -0
  227. package/src/Task.ts +38 -43
  228. package/src/Test.test.ts +88 -0
  229. package/src/Test.ts +49 -3
  230. package/src/Time.test.ts +926 -0
  231. package/src/Time.ts +13 -21
  232. package/src/Type.base64.test.ts +88 -0
  233. package/src/Type.test.ts +20465 -0
  234. package/src/Type.ts +114 -111
  235. package/src/Types.test.ts +324 -0
  236. package/src/Types.ts +13 -13
  237. package/src/WebSocket.ts +4 -7
  238. package/src/Worker.test.ts +755 -0
  239. package/src/Worker.ts +13 -18
  240. package/src/index.ts +1 -1
  241. package/src/intl/_en.ts +1 -1
  242. package/src/intl/ar.ts +1 -1
  243. package/src/intl/bn.ts +1 -1
  244. package/src/intl/ca.ts +1 -1
  245. package/src/intl/cs.ts +1 -1
  246. package/src/intl/da.ts +1 -1
  247. package/src/intl/de.ts +1 -1
  248. package/src/intl/el.ts +1 -1
  249. package/src/intl/es.ts +1 -1
  250. package/src/intl/fa.ts +1 -1
  251. package/src/intl/fi.ts +1 -1
  252. package/src/intl/fil.ts +1 -1
  253. package/src/intl/fr.ts +1 -1
  254. package/src/intl/he.ts +1 -1
  255. package/src/intl/hi.ts +1 -1
  256. package/src/intl/hr.ts +1 -1
  257. package/src/intl/hu.ts +1 -1
  258. package/src/intl/id.ts +1 -1
  259. package/src/intl/intl.test.ts +3106 -0
  260. package/src/intl/it.ts +1 -1
  261. package/src/intl/ja.ts +1 -1
  262. package/src/intl/ko.ts +1 -1
  263. package/src/intl/ml.ts +1 -1
  264. package/src/intl/mr.ts +1 -1
  265. package/src/intl/ms.ts +1 -1
  266. package/src/intl/nb.ts +1 -1
  267. package/src/intl/nl.ts +1 -1
  268. package/src/intl/pa.ts +1 -1
  269. package/src/intl/pl.ts +1 -1
  270. package/src/intl/pt-BR.ts +1 -1
  271. package/src/intl/pt.ts +1 -1
  272. package/src/intl/ro.ts +1 -1
  273. package/src/intl/sk.ts +1 -1
  274. package/src/intl/sl.ts +1 -1
  275. package/src/intl/sv.ts +1 -1
  276. package/src/intl/sw.ts +1 -1
  277. package/src/intl/ta.ts +1 -1
  278. package/src/intl/te.ts +1 -1
  279. package/src/intl/th.ts +1 -1
  280. package/src/intl/tr.ts +1 -1
  281. package/src/intl/uk.ts +1 -1
  282. package/src/intl/ur.ts +1 -1
  283. package/src/intl/vi.ts +1 -1
  284. package/src/intl/zh-CN.ts +1 -1
  285. package/src/intl/zh-TW.ts +1 -1
  286. package/src/local-first/Evolu.test.ts +1921 -0
  287. package/src/local-first/Evolu.ts +18 -14
  288. package/src/local-first/Owner.test.ts +83 -0
  289. package/src/local-first/Protocol.test.ts +1037 -0
  290. package/src/local-first/Protocol.ts +58 -261
  291. package/src/local-first/Query.test.ts +349 -0
  292. package/src/local-first/Query.ts +11 -11
  293. package/src/local-first/Relay.ts +2 -2
  294. package/src/local-first/Schema.ts +3 -5
  295. package/src/local-first/Shared.test.ts +1828 -0
  296. package/src/local-first/Shared.ts +9 -8
  297. package/src/local-first/Storage.ts +4 -4
  298. package/src/local-first/Timestamp.test.ts +374 -0
  299. package/src/local-first/Timestamp.ts +22 -10
  300. package/dist/src/Buffer.d.ts.map +0 -1
@@ -0,0 +1,1037 @@
1
+ import * as fc from "fast-check";
2
+ import { describe, it, test } from "node:test";
3
+ import type { NonEmptyReadonlyArray } from "../Array.ts";
4
+ import {
5
+ assertEqual,
6
+ assertEqualBytes,
7
+ assertFalse,
8
+ assertInstanceOf,
9
+ assertNonNullable,
10
+ assertOk,
11
+ assertSame,
12
+ assertThrowsInstanceOf,
13
+ assertTrue,
14
+ } from "../Assert.ts";
15
+ import { createBuffer, encodeNonNegativeInt } from "../Binary.ts";
16
+ import { EncryptionKey } from "../Crypto.ts";
17
+ import { constFalse, constTrue } from "../Function.ts";
18
+ import { SqliteValue } from "../Sqlite.ts";
19
+ import {
20
+ ownerIdToOwnerIdBytes,
21
+ testAppOwner,
22
+ type OwnerIdBytes,
23
+ } from "./Owner.ts";
24
+ import {
25
+ applyProtocolMessageAsClient,
26
+ applyProtocolMessageAsRelay,
27
+ createProtocolMessageBuffer,
28
+ createProtocolMessageFromCrdtMessages,
29
+ createTimestampsBuffer,
30
+ decodeSqliteValue,
31
+ decryptAndDecodeDbChange,
32
+ encodeSqliteValue,
33
+ encodeAndEncryptDbChange,
34
+ MessageType,
35
+ parseProtocolHeader,
36
+ ProtocolValueType,
37
+ protocolVersion,
38
+ SubscriptionFlags,
39
+ } from "./Protocol.ts";
40
+ import type {
41
+ CrdtMessage,
42
+ EncryptedCrdtMessage,
43
+ EncryptedDbChange,
44
+ StorageDep,
45
+ } from "./Storage.ts";
46
+ import { DbChange, InfiniteUpperBound, RangeType } from "./Storage.ts";
47
+ import { err, getOrThrow, ok } from "../Result.ts";
48
+ import {
49
+ testCreateDeps,
50
+ testCreateRun,
51
+ type RunDefaultDeps,
52
+ type TestRunDefaultDeps as TestDeps,
53
+ } from "../Task.ts";
54
+ import { maxMillis, Millis } from "../Time.ts";
55
+ import {
56
+ createId,
57
+ DateIsoFromDate,
58
+ FiniteNumber,
59
+ NonNegativeInt,
60
+ PositiveInt,
61
+ } from "../Type.ts";
62
+ import {
63
+ createInitialTimestamp,
64
+ createTimestamp,
65
+ maxCounter,
66
+ maxNodeId,
67
+ timestampBytesToTimestamp,
68
+ timestampToTimestampBytes,
69
+ } from "./Timestamp.ts";
70
+
71
+ const testTimestampsAsc = [
72
+ timestampToTimestampBytes(createTimestamp()),
73
+ timestampToTimestampBytes(createTimestamp({ millis: Millis.orThrow(1) })),
74
+ ] as const;
75
+
76
+ const maxTimestamp = timestampToTimestampBytes(
77
+ createTimestamp({
78
+ millis: maxMillis,
79
+ counter: maxCounter,
80
+ nodeId: maxNodeId,
81
+ }),
82
+ );
83
+
84
+ test("protocolVersion", () => {
85
+ assertEqual(protocolVersion, 1);
86
+ });
87
+
88
+ test("ProtocolValueType", () => {
89
+ assertEqual(ProtocolValueType, {
90
+ Base64Url: 32,
91
+ Bytes: 23,
92
+ DateIsoWithNegativeTime: 36,
93
+ DateIsoWithNonNegativeTime: 35,
94
+ EmptyString: 31,
95
+ Id: 33,
96
+ Json: 34,
97
+ NonNegativeInt: 30,
98
+ Null: 22,
99
+ Number: 21,
100
+ String: 20,
101
+ });
102
+ });
103
+
104
+ test("encodeSqliteValue/decodeSqliteValue", () => {
105
+ const deps = testCreateDeps();
106
+ const id = createId(deps);
107
+ const testCasesSuccess: Array<[SqliteValue, number]> = [
108
+ // empty string optimization - 1 byte vs 2 bytes (50% reduction)
109
+ ["", 1],
110
+ // encodeNumber
111
+ [FiniteNumber.orThrow(123.5), 10],
112
+ // encodeNumber
113
+ [FiniteNumber.orThrow(-123), 3],
114
+ [null, 1],
115
+ [new Uint8Array([1, 2, 3]), 5],
116
+ [id, 17],
117
+ // small ints 0-19
118
+ [FiniteNumber.orThrow(0), 1],
119
+ // small ints 0-19
120
+ [FiniteNumber.orThrow(19), 1],
121
+ // NonNegativeInt
122
+ [FiniteNumber.orThrow(123), 2],
123
+ // NonNegativeInt
124
+ [FiniteNumber.orThrow(16383), 3],
125
+ // 18 bytes MessagePack + 2 bytes protocol overhead
126
+ ['{"compact":true,"schema":0}', 20],
127
+ // Protocol encoding ensures 6 bytes till the year 2108.
128
+ [
129
+ getOrThrow(
130
+ DateIsoFromDate.from.parent(new Date("0000-01-01T00:00:00.000Z")),
131
+ ),
132
+ 10,
133
+ ],
134
+ [
135
+ getOrThrow(
136
+ DateIsoFromDate.from.parent(new Date("2024-10-31T00:00:00.000Z")),
137
+ ),
138
+ 7,
139
+ ],
140
+ [
141
+ getOrThrow(
142
+ DateIsoFromDate.from.parent(new Date("2108-10-31T00:00:00.000Z")),
143
+ ),
144
+ 7,
145
+ ],
146
+ [
147
+ getOrThrow(
148
+ DateIsoFromDate.from.parent(new Date("2109-10-31T00:00:00.000Z")),
149
+ ),
150
+ 8,
151
+ ],
152
+ [
153
+ getOrThrow(
154
+ DateIsoFromDate.from.parent(new Date("9999-12-31T23:59:59.999Z")),
155
+ ),
156
+ 8,
157
+ ],
158
+ ];
159
+
160
+ const buffer = createBuffer();
161
+ testCasesSuccess.forEach(([value, bytesLength]) => {
162
+ const encoded = createBuffer();
163
+ encodeSqliteValue(encoded, value);
164
+ buffer.extend(encoded.unwrap());
165
+
166
+ assertEqual(encoded.getLength(), bytesLength);
167
+ assertEqual(decodeSqliteValue(encoded), value);
168
+ });
169
+ assertEqualBytes(
170
+ buffer.unwrap(),
171
+ [
172
+ 31, 21, 203, 64, 94, 224, 0, 0, 0, 0, 0, 21, 208, 133, 22, 23, 3, 1, 2, 3,
173
+ 33, 157, 202, 140, 67, 91, 176, 119, 159, 179, 127, 150, 10, 81, 180, 247,
174
+ 84, 0, 19, 30, 123, 30, 255, 127, 34, 18, 130, 167, 99, 111, 109, 112, 97,
175
+ 99, 116, 195, 166, 115, 99, 104, 101, 109, 97, 0, 36, 203, 194, 204, 69,
176
+ 55, 130, 48, 0, 0, 35, 128, 232, 252, 254, 173, 50, 35, 128, 168, 131,
177
+ 232, 192, 127, 35, 128, 128, 200, 165, 182, 128, 1, 35, 255, 183, 255,
178
+ 144, 253, 206, 57,
179
+ ],
180
+ );
181
+ });
182
+
183
+ test("encodeSqliteValue/decodeSqliteValue preserves an own __proto__ JSON object key", () => {
184
+ const value = '{"__proto__":{"safe":true}}';
185
+ const buffer = createBuffer();
186
+
187
+ encodeSqliteValue(buffer, value);
188
+
189
+ assertEqual(decodeSqliteValue(buffer), value);
190
+ });
191
+
192
+ test("encodeSqliteValue/decodeSqliteValue property tests", () => {
193
+ const deps = testCreateDeps();
194
+ // Property test: round-trip encoding/decoding should preserve the value
195
+ fc.assert(
196
+ fc.property(
197
+ fc.oneof(
198
+ // Test all SqliteValue types
199
+ fc.constant(null),
200
+ // Regular strings
201
+ fc.string(),
202
+ // Numbers (exclude NaN)
203
+ fc.double().filter(Number.isFinite),
204
+ // Binary data
205
+ fc.uint8Array(),
206
+
207
+ // Small ints (0-19) - special encoding
208
+ fc.integer({ min: 0, max: 19 }),
209
+ // Non-negative ints
210
+ fc.integer({ min: 20, max: Number.MAX_SAFE_INTEGER }),
211
+ // Negative numbers
212
+ fc.integer({ min: Number.MIN_SAFE_INTEGER, max: -1 }),
213
+ // Regular floats
214
+ fc.float({ min: -1000, max: 1000 }).filter(Number.isFinite),
215
+
216
+ // Id optimization cases
217
+ // Valid Id
218
+ fc.constantFrom(createId(deps)),
219
+ fc
220
+ .string({ minLength: 21, maxLength: 21 })
221
+ // Id-like strings
222
+ .map((s) => s.replaceAll(/[^A-Za-z0-9_-]/gu, "a")),
223
+
224
+ // URL-safe strings with length % 4 === 0 (Base64Url optimization)
225
+ fc
226
+ .stringMatching(/^[A-Za-z0-9_-]*$/u)
227
+ .filter((s) => s.length % 4 === 0 && s.length > 0),
228
+ // URL-safe strings with length % 4 !== 0 (should use regular string encoding)
229
+ fc
230
+ .stringMatching(/^[A-Za-z0-9_-]*$/u)
231
+ .filter((s) => s.length % 4 !== 0 && s.length > 0),
232
+
233
+ // Base64Url edge cases
234
+ // Empty string (optimization)
235
+ fc.constant(""),
236
+ fc
237
+ .stringMatching(/^[A-Za-z0-9_-]{4,}$/u)
238
+ // Valid Base64Url
239
+ .filter((s) => s.length % 4 === 0),
240
+ // Invalid Base64Url chars
241
+ fc.string().filter((s) => /[^A-Za-z0-9_-]/u.test(s)),
242
+
243
+ // JSON optimization cases
244
+ fc
245
+ .record({
246
+ name: fc.string(),
247
+ value: fc.oneof(fc.string(), fc.integer(), fc.boolean()),
248
+ })
249
+ .map((obj) => JSON.stringify(obj)),
250
+ fc
251
+ .array(fc.oneof(fc.string(), fc.integer(), fc.boolean()))
252
+ .map((arr) => JSON.stringify(arr)),
253
+ // Simple JSON
254
+ fc.constantFrom('{"a":1}', "[]", "null", "true", "false", '"string"'),
255
+ fc.string().filter((s) => {
256
+ try {
257
+ JSON.parse(s);
258
+ return false;
259
+ } catch {
260
+ return true;
261
+ }
262
+ // Non-JSON strings
263
+ }),
264
+
265
+ // Date ISO strings - both valid and invalid
266
+ fc
267
+ .date({ min: new Date("1970-01-01"), max: new Date("2100-01-01") })
268
+ .filter((d) => !isNaN(d.getTime()))
269
+ .map((d) => d.toISOString()),
270
+ fc
271
+ .date({ min: new Date("0000-01-01"), max: new Date("9999-12-31") })
272
+ .filter((d) => !isNaN(d.getTime()))
273
+ .map((d) => d.toISOString()),
274
+ fc.constantFrom(
275
+ "0000-01-01T00:00:00.000Z",
276
+ "9999-12-31T23:59:59.999Z",
277
+ // Invalid date format
278
+ "not-a-date-2024-01-01T00:00:00.000Z",
279
+ // Invalid month
280
+ "2024-13-01T00:00:00.000Z",
281
+ ),
282
+
283
+ // Binary data edge cases
284
+ // Empty binary
285
+ fc.constant(new Uint8Array(0)),
286
+ // Variable size binary
287
+ fc.uint8Array({ minLength: 1, maxLength: 1000 }),
288
+ // Large binary with pattern
289
+ fc.constant(new Uint8Array(1000).fill(255)),
290
+ // Small binary pattern
291
+ fc.constant(new Uint8Array([0, 1, 2, 3, 4, 5])),
292
+ ),
293
+ (value) => {
294
+ const sqliteValue = SqliteValue.orThrow(value);
295
+ const buffer = createBuffer();
296
+ encodeSqliteValue(buffer, sqliteValue);
297
+ const decoded = decodeSqliteValue(buffer);
298
+
299
+ // Handle special cases for comparison
300
+ if (
301
+ sqliteValue instanceof Uint8Array &&
302
+ decoded instanceof Uint8Array
303
+ ) {
304
+ return (
305
+ sqliteValue.length === decoded.length &&
306
+ sqliteValue.every((byte, i) => byte === decoded[i])
307
+ );
308
+ }
309
+
310
+ return Object.is(decoded, sqliteValue);
311
+ },
312
+ ),
313
+ { numRuns: 10000 },
314
+ );
315
+ });
316
+
317
+ test("decodeSqliteValue rejects trailing data in a JSON frame", () => {
318
+ const buffer = createBuffer([ProtocolValueType.Json, 2, 0xc0, 0xc0]);
319
+
320
+ const error = assertThrowsInstanceOf(() => decodeSqliteValue(buffer), Error);
321
+ assertEqual(error.message, "Invalid JSON MessagePack length");
322
+ });
323
+
324
+ test("decodeSqliteValue does not decode beyond a JSON frame", () => {
325
+ const bytesAfterFrame = new Uint8Array([0xc0, 0xc0]);
326
+ const buffer = createBuffer([
327
+ ProtocolValueType.Json,
328
+ 5,
329
+ 0xdd,
330
+ 0,
331
+ 0,
332
+ 0,
333
+ 2,
334
+ ...bytesAfterFrame,
335
+ ]);
336
+
337
+ const error = assertThrowsInstanceOf(() => decodeSqliteValue(buffer), Error);
338
+ assertEqual(error.message, "Buffer parse ended prematurely");
339
+ assertEqualBytes(buffer.unwrap(), bytesAfterFrame);
340
+ });
341
+
342
+ test("encodeSqliteValue/decodeSqliteValue specific failing case from property tests", () => {
343
+ // This was the specific failing case from property tests before the DateIsoString fix
344
+ const failingInput = `["0 ( ",-100000000]`;
345
+
346
+ const buffer = createBuffer();
347
+ encodeSqliteValue(buffer, failingInput);
348
+ const decoded = decodeSqliteValue(buffer);
349
+
350
+ // After the DateIsoString round-trip fix, this should now work correctly
351
+ // The input should be treated as a regular string (not DateIso) and round-trip properly
352
+ assertEqual(decoded, failingInput);
353
+ });
354
+
355
+ const createDbChange = (deps: RunDefaultDeps) =>
356
+ DbChange.orThrow({
357
+ table: "employee",
358
+ id: createId(deps),
359
+ values: {
360
+ name: "Victoria",
361
+ hiredAt: getOrThrow(DateIsoFromDate.from.parent(new Date("2024-10-31"))),
362
+ officeId: createId(deps),
363
+ },
364
+ isInsert: true,
365
+ isDelete: null,
366
+ });
367
+
368
+ const createTestCrdtMessage = (deps: TestDeps): CrdtMessage => ({
369
+ timestamp: createInitialTimestamp(deps),
370
+ change: createDbChange(deps),
371
+ });
372
+
373
+ const createEncryptedDbChange = (
374
+ deps: RunDefaultDeps,
375
+ message: CrdtMessage,
376
+ ): EncryptedDbChange =>
377
+ encodeAndEncryptDbChange(deps)(message, testAppOwner.encryptionKey);
378
+
379
+ const createEncryptedCrdtMessage = (
380
+ deps: RunDefaultDeps,
381
+ message: CrdtMessage,
382
+ ): EncryptedCrdtMessage => ({
383
+ timestamp: message.timestamp,
384
+ change: createEncryptedDbChange(deps, message),
385
+ });
386
+
387
+ test("encodeAndEncryptDbChange/decryptAndDecodeDbChange", () => {
388
+ const deps = testCreateDeps();
389
+ const crdtMessage = createTestCrdtMessage(deps);
390
+ const encryptedMessage = createEncryptedCrdtMessage(deps, crdtMessage);
391
+ assertEqualBytes(
392
+ encryptedMessage.change,
393
+ [
394
+ 61, 17, 38, 101, 206, 230, 156, 196, 117, 122, 86, 130, 104, 17, 74, 160,
395
+ 137, 87, 251, 80, 60, 4, 187, 82, 120, 9, 50, 95, 6, 212, 192, 215, 212,
396
+ 240, 254, 11, 101, 113, 29, 11, 162, 146, 20, 200, 144, 145, 81, 15, 109,
397
+ 39, 41, 118, 215, 227, 212, 2, 217, 19, 197, 97, 11, 243, 146, 145, 67,
398
+ 17, 119, 86, 239, 234, 243, 1, 204, 183, 182, 137, 63, 25, 61, 156, 207,
399
+ 208, 27, 245, 222, 183, 68, 23, 42, 207, 92, 45, 184, 244, 180, 51, 216,
400
+ 127, 250, 165, 184, 202, 205, 155, 238, 38, 175, 27, 116, 71, 192, 59,
401
+ 240, 72, 174, 147, 90, 149, 89, 211, 36, 206, 34, 16, 160, 198, 99, 70,
402
+ 123, 16, 151, 77, 131, 12, 246, 54, 44, 72, 34, 252, 143, 75, 40, 144, 1,
403
+ ],
404
+ );
405
+ const decrypted = getOrThrow(
406
+ decryptAndDecodeDbChange(encryptedMessage, testAppOwner.encryptionKey),
407
+ );
408
+ assertEqual(decrypted, crdtMessage.change);
409
+
410
+ const wrongKey = EncryptionKey.orThrow(new Uint8Array(32).fill(42));
411
+ const decryptedWithWrongKey = decryptAndDecodeDbChange(
412
+ encryptedMessage,
413
+ wrongKey,
414
+ );
415
+ assertFalse(decryptedWithWrongKey.ok);
416
+ assertEqual(
417
+ decryptedWithWrongKey.error.type,
418
+ "DecryptWithXChaCha20Poly1305Error",
419
+ );
420
+
421
+ const corruptedCiphertext = new Uint8Array(
422
+ encryptedMessage.change,
423
+ ) as EncryptedDbChange;
424
+ if (corruptedCiphertext.length > 10) {
425
+ // Modify a byte
426
+ corruptedCiphertext[10] = (corruptedCiphertext[10] + 1) % 256;
427
+ }
428
+ const corruptedMessage: EncryptedCrdtMessage = {
429
+ timestamp: encryptedMessage.timestamp,
430
+ change: corruptedCiphertext,
431
+ };
432
+ const decryptedCorrupted = decryptAndDecodeDbChange(
433
+ corruptedMessage,
434
+ testAppOwner.encryptionKey,
435
+ );
436
+ assertFalse(decryptedCorrupted.ok);
437
+ assertEqual(
438
+ decryptedCorrupted.error.type,
439
+ "DecryptWithXChaCha20Poly1305Error",
440
+ );
441
+ });
442
+
443
+ test("encodeAndEncryptDbChange preserves null values", () => {
444
+ const deps = testCreateDeps();
445
+ const message: CrdtMessage = {
446
+ timestamp: createInitialTimestamp(deps),
447
+ change: DbChange.orThrow({
448
+ table: "employee",
449
+ id: createId(deps),
450
+ values: { officeId: null },
451
+ isInsert: false,
452
+ isDelete: null,
453
+ }),
454
+ };
455
+ const encryptedMessage = createEncryptedCrdtMessage(deps, message);
456
+
457
+ assertEqual(
458
+ getOrThrow(
459
+ decryptAndDecodeDbChange(encryptedMessage, testAppOwner.encryptionKey),
460
+ ),
461
+ message.change,
462
+ );
463
+ });
464
+
465
+ test("encodeAndEncryptDbChange preserves negative zero", () => {
466
+ const deps = testCreateDeps();
467
+ const message: CrdtMessage = {
468
+ timestamp: createInitialTimestamp(deps),
469
+ change: DbChange.orThrow({
470
+ table: "employee",
471
+ id: createId(deps),
472
+ values: { score: FiniteNumber.orThrow(-0) },
473
+ isInsert: false,
474
+ isDelete: null,
475
+ }),
476
+ };
477
+ const encryptedMessage = createEncryptedCrdtMessage(deps, message);
478
+ const decrypted = getOrThrow(
479
+ decryptAndDecodeDbChange(encryptedMessage, testAppOwner.encryptionKey),
480
+ );
481
+
482
+ assertSame(decrypted.values.score, -0);
483
+ });
484
+
485
+ test("decryptAndDecodeDbChange timestamp tamper-proofing", () => {
486
+ const deps = testCreateDeps();
487
+ const crdtMessage = createTestCrdtMessage(deps);
488
+ const encryptedMessage = createEncryptedCrdtMessage(deps, crdtMessage);
489
+
490
+ // Create a different timestamp
491
+ const wrongTimestamp = createInitialTimestamp(deps);
492
+
493
+ // Create a message with the wrong timestamp but same encrypted change
494
+ const tamperedMessage: EncryptedCrdtMessage = {
495
+ timestamp: wrongTimestamp,
496
+ change: encryptedMessage.change,
497
+ };
498
+
499
+ // Attempt to decrypt with wrong timestamp should fail with ProtocolTimestampMismatchError
500
+ const decryptedWithWrongTimestamp = decryptAndDecodeDbChange(
501
+ tamperedMessage,
502
+ testAppOwner.encryptionKey,
503
+ );
504
+
505
+ assertEqual(
506
+ decryptedWithWrongTimestamp,
507
+ err({
508
+ type: "ProtocolTimestampMismatchError",
509
+ expected: wrongTimestamp,
510
+ timestamp: crdtMessage.timestamp,
511
+ }),
512
+ );
513
+ });
514
+
515
+ const shouldNotBeCalled = () => {
516
+ throw new Error("should not be called");
517
+ };
518
+
519
+ const shouldNotBeCalledStorageDep: StorageDep = {
520
+ storage: {
521
+ getSize: shouldNotBeCalled,
522
+ fingerprint: shouldNotBeCalled,
523
+ fingerprintRanges: shouldNotBeCalled,
524
+ findLowerBound: shouldNotBeCalled,
525
+ iterate: shouldNotBeCalled,
526
+ validateWriteKey: shouldNotBeCalled,
527
+ setWriteKey: shouldNotBeCalled,
528
+ writeMessages: shouldNotBeCalled,
529
+ readDbChange: shouldNotBeCalled,
530
+ deleteOwner: shouldNotBeCalled,
531
+ },
532
+ };
533
+
534
+ test("createTimestampsBuffer maxTimestamp", () => {
535
+ const buffer = createTimestampsBuffer();
536
+ buffer.add(timestampBytesToTimestamp(maxTimestamp));
537
+ assertEqual(buffer.getLength(), 21);
538
+ });
539
+
540
+ describe("createProtocolMessageBuffer", () => {
541
+ it("should allow no ranges", () => {
542
+ const buffer = createProtocolMessageBuffer(testAppOwner.id, {
543
+ messageType: MessageType.Request,
544
+ });
545
+ assertEqualBytes(
546
+ buffer.unwrap(),
547
+ [
548
+ 1, 5, 39, 254, 242, 108, 77, 142, 9, 59, 219, 32, 254, 15, 186, 235,
549
+ 212, 0, 0, 0, 0,
550
+ ],
551
+ );
552
+ });
553
+
554
+ it("should allow single range with InfiniteUpperBound", () => {
555
+ const buffer = createProtocolMessageBuffer(testAppOwner.id, {
556
+ messageType: MessageType.Request,
557
+ });
558
+ buffer.addRange({
559
+ type: RangeType.Skip,
560
+ upperBound: InfiniteUpperBound,
561
+ });
562
+ buffer.unwrap();
563
+ });
564
+
565
+ it("should reject single range without InfiniteUpperBound", () => {
566
+ const buffer = createProtocolMessageBuffer(testAppOwner.id, {
567
+ messageType: MessageType.Request,
568
+ });
569
+ buffer.addRange({
570
+ type: RangeType.Skip,
571
+ upperBound: testTimestampsAsc[0],
572
+ });
573
+ const error = assertThrowsInstanceOf(() => buffer.unwrap(), Error);
574
+ assertEqual(
575
+ error.message,
576
+ "The last range's upperBound must be InfiniteUpperBound",
577
+ );
578
+ });
579
+
580
+ it("should allow multiple ranges with only last InfiniteUpperBound", () => {
581
+ const buffer = createProtocolMessageBuffer(testAppOwner.id, {
582
+ messageType: MessageType.Request,
583
+ });
584
+ buffer.addRange({
585
+ type: RangeType.Skip,
586
+ upperBound: testTimestampsAsc[0],
587
+ });
588
+ buffer.addRange({
589
+ type: RangeType.Skip,
590
+ upperBound: testTimestampsAsc[1],
591
+ });
592
+ buffer.addRange({
593
+ type: RangeType.Skip,
594
+ upperBound: InfiniteUpperBound,
595
+ });
596
+ buffer.unwrap();
597
+ });
598
+
599
+ it("should reject range added after InfiniteUpperBound", () => {
600
+ const buffer = createProtocolMessageBuffer(testAppOwner.id, {
601
+ messageType: MessageType.Request,
602
+ });
603
+ buffer.addRange({
604
+ type: RangeType.Skip,
605
+ upperBound: InfiniteUpperBound,
606
+ });
607
+ const error = assertThrowsInstanceOf(() => {
608
+ buffer.addRange({
609
+ type: RangeType.Skip,
610
+ upperBound: testTimestampsAsc[0],
611
+ });
612
+ }, Error);
613
+ assertEqual(
614
+ error.message,
615
+ "Cannot add a range after an InfiniteUpperBound range",
616
+ );
617
+ });
618
+
619
+ it("should reject multiple InfiniteUpperBounds", () => {
620
+ const buffer = createProtocolMessageBuffer(testAppOwner.id, {
621
+ messageType: MessageType.Request,
622
+ });
623
+ buffer.addRange({
624
+ type: RangeType.Skip,
625
+ upperBound: testTimestampsAsc[0],
626
+ });
627
+ buffer.addRange({
628
+ type: RangeType.Skip,
629
+ upperBound: InfiniteUpperBound,
630
+ });
631
+ const error = assertThrowsInstanceOf(() => {
632
+ buffer.addRange({
633
+ type: RangeType.Skip,
634
+ upperBound: InfiniteUpperBound,
635
+ });
636
+ }, Error);
637
+ assertEqual(
638
+ error.message,
639
+ "Cannot add a range after an InfiniteUpperBound range",
640
+ );
641
+ });
642
+ });
643
+
644
+ test("parseProtocolHeader parses supported headers and rejects malformed ones", () => {
645
+ const requestHeader = parseProtocolHeader(
646
+ createProtocolMessageBuffer(testAppOwner.id, {
647
+ messageType: MessageType.Request,
648
+ subscriptionFlag: SubscriptionFlags.Subscribe,
649
+ }).unwrap(),
650
+ );
651
+ assertEqual(
652
+ requestHeader,
653
+ ok({
654
+ type: "ProtocolHeader",
655
+ version: 1,
656
+ ownerId: testAppOwner.id,
657
+ messageType: MessageType.Request,
658
+ }),
659
+ );
660
+
661
+ const responseHeader = parseProtocolHeader(
662
+ createProtocolMessageBuffer(testAppOwner.id, {
663
+ messageType: MessageType.Response,
664
+ errorCode: 0,
665
+ }).unwrap(),
666
+ );
667
+ assertEqual(
668
+ responseHeader,
669
+ ok({
670
+ type: "ProtocolHeader",
671
+ version: 1,
672
+ ownerId: testAppOwner.id,
673
+ messageType: MessageType.Response,
674
+ }),
675
+ );
676
+
677
+ const broadcastHeader = parseProtocolHeader(
678
+ createProtocolMessageBuffer(testAppOwner.id, {
679
+ messageType: MessageType.Broadcast,
680
+ }).unwrap(),
681
+ );
682
+ assertEqual(
683
+ broadcastHeader,
684
+ ok({
685
+ type: "ProtocolHeader",
686
+ version: 1,
687
+ ownerId: testAppOwner.id,
688
+ messageType: MessageType.Broadcast,
689
+ }),
690
+ );
691
+
692
+ const invalidVersionMessage = createProtocolMessageBuffer(testAppOwner.id, {
693
+ version: PositiveInt.orThrow(2),
694
+ messageType: MessageType.Request,
695
+ }).unwrap();
696
+ const invalidVersion = parseProtocolHeader(invalidVersionMessage);
697
+ assertFalse(invalidVersion.ok);
698
+ if (!invalidVersion.ok) {
699
+ assertEqual(invalidVersion.error.type, "ProtocolInvalidDataError");
700
+ assertInstanceOf(invalidVersion.error.error, Error);
701
+ }
702
+
703
+ const invalidTypeMessage = createBuffer();
704
+ encodeNonNegativeInt(invalidTypeMessage, protocolVersion);
705
+ invalidTypeMessage.extend(ownerIdToOwnerIdBytes(testAppOwner.id));
706
+ invalidTypeMessage.extend([255]);
707
+
708
+ const invalidType = parseProtocolHeader(invalidTypeMessage.unwrap());
709
+ assertFalse(invalidType.ok);
710
+ if (!invalidType.ok) {
711
+ assertEqual(invalidType.error.type, "ProtocolInvalidDataError");
712
+ assertInstanceOf(invalidType.error.error, Error);
713
+ }
714
+ });
715
+
716
+ describe("E2E versioning", () => {
717
+ it("same versions", async () => {
718
+ await using run = testCreateRun(shouldNotBeCalledStorageDep);
719
+ const v0 = 0 as NonNegativeInt;
720
+
721
+ const clientMessage = createProtocolMessageBuffer(testAppOwner.id, {
722
+ version: v0,
723
+ messageType: MessageType.Request,
724
+ }).unwrap();
725
+
726
+ const relayResponse = await run.orThrow(
727
+ applyProtocolMessageAsRelay(clientMessage, {}, v0),
728
+ );
729
+ assertEqual(relayResponse.message.length, 20);
730
+ });
731
+
732
+ it("non-initiator version is higher", async () => {
733
+ await using run = testCreateRun(shouldNotBeCalledStorageDep);
734
+ const v0 = 0 as NonNegativeInt;
735
+ const v1 = 1 as NonNegativeInt;
736
+
737
+ const clientMessage = createProtocolMessageBuffer(testAppOwner.id, {
738
+ version: v0,
739
+ messageType: MessageType.Request,
740
+ }).unwrap();
741
+
742
+ const relayResponse = await run.orThrow(
743
+ applyProtocolMessageAsRelay(clientMessage, {}, v1),
744
+ );
745
+
746
+ const clientResult = await run(
747
+ applyProtocolMessageAsClient(relayResponse.message, {
748
+ version: v0,
749
+ }),
750
+ );
751
+ assertEqual(
752
+ clientResult,
753
+ err({
754
+ type: "ProtocolVersionError",
755
+ version: 1,
756
+ isInitiator: true,
757
+ ownerId: testAppOwner.id,
758
+ }),
759
+ );
760
+ });
761
+
762
+ it("initiator version is higher", async () => {
763
+ await using run = testCreateRun(shouldNotBeCalledStorageDep);
764
+ const v0 = 0 as NonNegativeInt;
765
+ const v1 = 1 as NonNegativeInt;
766
+
767
+ const clientMessage = createProtocolMessageBuffer(testAppOwner.id, {
768
+ version: v1,
769
+ messageType: MessageType.Request,
770
+ }).unwrap();
771
+
772
+ const relayResponse = await run.orThrow(
773
+ applyProtocolMessageAsRelay(clientMessage, {}, v0),
774
+ );
775
+
776
+ const clientResult = await run(
777
+ applyProtocolMessageAsClient(relayResponse.message, {
778
+ version: v1,
779
+ }),
780
+ );
781
+ assertEqual(
782
+ clientResult,
783
+ err({
784
+ type: "ProtocolVersionError",
785
+ version: 0,
786
+ isInitiator: false,
787
+ ownerId: testAppOwner.id,
788
+ }),
789
+ );
790
+ });
791
+ });
792
+
793
+ describe("E2E errors", () => {
794
+ it("ProtocolInvalidDataError", async () => {
795
+ await using run = testCreateRun(shouldNotBeCalledStorageDep);
796
+ const malformedMessage = createBuffer();
797
+ // Only version, no ownerId
798
+ encodeNonNegativeInt(malformedMessage, 1 as NonNegativeInt);
799
+
800
+ const clientResult = await run(
801
+ applyProtocolMessageAsClient(malformedMessage.unwrap(), {
802
+ version: 0 as NonNegativeInt,
803
+ }),
804
+ );
805
+
806
+ assertFalse(clientResult.ok);
807
+ assertEqual(clientResult.error.type, "ProtocolInvalidDataError");
808
+ });
809
+
810
+ it("ProtocolWriteKeyError", async () => {
811
+ const deps = testCreateDeps();
812
+ const timestamp = timestampBytesToTimestamp(testTimestampsAsc[0]);
813
+ const dbChange = createDbChange(deps);
814
+
815
+ const messages: NonEmptyReadonlyArray<CrdtMessage> = [
816
+ { timestamp, change: dbChange },
817
+ ];
818
+
819
+ const initiatorMessage = createProtocolMessageFromCrdtMessages(deps)(
820
+ testAppOwner,
821
+ messages,
822
+ );
823
+
824
+ let responseMessage: Uint8Array;
825
+ {
826
+ await using run = testCreateRun({
827
+ storage: {
828
+ ...shouldNotBeCalledStorageDep.storage,
829
+ validateWriteKey: constFalse,
830
+ },
831
+ });
832
+ const response = await run.orThrow(
833
+ applyProtocolMessageAsRelay(initiatorMessage),
834
+ );
835
+ assertEqualBytes(
836
+ response.message,
837
+ [
838
+ 1, 5, 39, 254, 242, 108, 77, 142, 9, 59, 219, 32, 254, 15, 186, 235,
839
+ 212, 1, 1, 0,
840
+ ],
841
+ );
842
+ responseMessage = response.message;
843
+ }
844
+
845
+ await using run = testCreateRun(shouldNotBeCalledStorageDep);
846
+ const clientResult = await run(
847
+ applyProtocolMessageAsClient(responseMessage),
848
+ );
849
+ assertEqual(
850
+ clientResult,
851
+ err({ type: "ProtocolWriteKeyError", ownerId: testAppOwner.id }),
852
+ );
853
+ });
854
+ });
855
+
856
+ describe("E2E relay options", () => {
857
+ it("subscribe", async () => {
858
+ await using run = testCreateRun(shouldNotBeCalledStorageDep);
859
+ const message = createProtocolMessageBuffer(testAppOwner.id, {
860
+ messageType: MessageType.Request,
861
+ subscriptionFlag: SubscriptionFlags.Subscribe,
862
+ }).unwrap();
863
+ let subscribeCalledWithOwnerId: string | null = null;
864
+
865
+ await run(
866
+ applyProtocolMessageAsRelay(message, {
867
+ subscribe: (ownerId) => {
868
+ subscribeCalledWithOwnerId = ownerId;
869
+ },
870
+ }),
871
+ );
872
+
873
+ assertEqual(subscribeCalledWithOwnerId, testAppOwner.id);
874
+ });
875
+
876
+ it("unsubscribe", async () => {
877
+ await using run = testCreateRun(shouldNotBeCalledStorageDep);
878
+ const message = createProtocolMessageBuffer(testAppOwner.id, {
879
+ messageType: MessageType.Request,
880
+ subscriptionFlag: SubscriptionFlags.Unsubscribe,
881
+ }).unwrap();
882
+ let unsubscribeCalledWithOwnerId: string | null = null;
883
+
884
+ await run(
885
+ applyProtocolMessageAsRelay(message, {
886
+ unsubscribe: (ownerId) => {
887
+ unsubscribeCalledWithOwnerId = ownerId;
888
+ },
889
+ }),
890
+ );
891
+
892
+ assertEqual(unsubscribeCalledWithOwnerId, testAppOwner.id);
893
+ });
894
+
895
+ it("no subscription flag (None)", async () => {
896
+ await using run = testCreateRun(shouldNotBeCalledStorageDep);
897
+ const message = createProtocolMessageBuffer(testAppOwner.id, {
898
+ messageType: MessageType.Request,
899
+ subscriptionFlag: SubscriptionFlags.None,
900
+ }).unwrap();
901
+ let subscribeWasCalled = false;
902
+ let unsubscribeWasCalled = false;
903
+
904
+ await run(
905
+ applyProtocolMessageAsRelay(message, {
906
+ subscribe: () => {
907
+ subscribeWasCalled = true;
908
+ },
909
+ unsubscribe: () => {
910
+ unsubscribeWasCalled = true;
911
+ },
912
+ }),
913
+ );
914
+
915
+ assertFalse(subscribeWasCalled);
916
+ assertFalse(unsubscribeWasCalled);
917
+ });
918
+
919
+ it("default subscription flag (undefined)", async () => {
920
+ await using run = testCreateRun(shouldNotBeCalledStorageDep);
921
+ const message = createProtocolMessageBuffer(testAppOwner.id, {
922
+ messageType: MessageType.Request,
923
+ // No subscriptionFlag provided, should default to None
924
+ }).unwrap();
925
+ let subscribeWasCalled = false;
926
+ let unsubscribeWasCalled = false;
927
+
928
+ await run(
929
+ applyProtocolMessageAsRelay(message, {
930
+ subscribe: () => {
931
+ subscribeWasCalled = true;
932
+ },
933
+ unsubscribe: () => {
934
+ unsubscribeWasCalled = true;
935
+ },
936
+ }),
937
+ );
938
+
939
+ assertFalse(subscribeWasCalled);
940
+ assertFalse(unsubscribeWasCalled);
941
+ });
942
+
943
+ it("broadcast message", async () => {
944
+ const deps = testCreateDeps();
945
+ const timestamp = timestampBytesToTimestamp(testTimestampsAsc[0]);
946
+ const dbChange = createDbChange(deps);
947
+ const messages: NonEmptyReadonlyArray<CrdtMessage> = [
948
+ { timestamp, change: dbChange },
949
+ ];
950
+
951
+ const initiatorMessage = createProtocolMessageFromCrdtMessages(deps)(
952
+ testAppOwner,
953
+ messages,
954
+ );
955
+
956
+ assertEqualBytes(
957
+ initiatorMessage,
958
+ [
959
+ 1, 5, 39, 254, 242, 108, 77, 142, 9, 59, 219, 32, 254, 15, 186, 235,
960
+ 212, 0, 1, 251, 175, 113, 170, 127, 245, 31, 104, 159, 23, 7, 74, 106,
961
+ 187, 137, 180, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 145, 1, 138,
962
+ 125, 164, 134, 128, 69, 100, 218, 61, 17, 38, 101, 206, 230, 156, 196,
963
+ 117, 122, 86, 130, 104, 17, 74, 160, 120, 248, 71, 117, 163, 185, 102,
964
+ 74, 58, 39, 102, 235, 141, 227, 226, 129, 183, 209, 55, 45, 27, 183,
965
+ 134, 35, 187, 95, 52, 188, 199, 131, 202, 240, 112, 108, 240, 162, 130,
966
+ 149, 239, 24, 101, 96, 218, 224, 225, 5, 94, 58, 141, 225, 132, 168,
967
+ 118, 184, 251, 105, 226, 136, 35, 94, 9, 34, 3, 128, 195, 153, 50, 60,
968
+ 181, 19, 194, 98, 84, 196, 202, 168, 140, 1, 127, 77, 88, 238, 211, 184,
969
+ 92, 117, 128, 144, 232, 210, 17, 27, 14, 157, 88, 96, 109, 76, 12, 230,
970
+ 193, 52, 89, 185, 127, 233, 137, 151, 101, 46, 245, 56, 125, 57, 126,
971
+ 177, 79, 113, 20, 30, 50,
972
+ ],
973
+ );
974
+
975
+ let broadcastedMessage = null as Uint8Array | null;
976
+
977
+ await using run = testCreateRun({
978
+ storage: {
979
+ ...shouldNotBeCalledStorageDep.storage,
980
+ validateWriteKey: constTrue,
981
+ writeMessages: () => () => ok(),
982
+ },
983
+ });
984
+ await run(
985
+ applyProtocolMessageAsRelay(initiatorMessage, {
986
+ broadcast: (ownerId, message) => {
987
+ assertEqual(ownerId, testAppOwner.id);
988
+ broadcastedMessage = message;
989
+ },
990
+ }),
991
+ );
992
+
993
+ assertNonNullable(broadcastedMessage);
994
+ // Added error and removed writeKey, added subscription flag
995
+ assertEqualBytes(
996
+ broadcastedMessage,
997
+ [
998
+ 1, 5, 39, 254, 242, 108, 77, 142, 9, 59, 219, 32, 254, 15, 186, 235,
999
+ 212, 2, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 145, 1, 138, 125, 164,
1000
+ 134, 128, 69, 100, 218, 61, 17, 38, 101, 206, 230, 156, 196, 117, 122,
1001
+ 86, 130, 104, 17, 74, 160, 120, 248, 71, 117, 163, 185, 102, 74, 58, 39,
1002
+ 102, 235, 141, 227, 226, 129, 183, 209, 55, 45, 27, 183, 134, 35, 187,
1003
+ 95, 52, 188, 199, 131, 202, 240, 112, 108, 240, 162, 130, 149, 239, 24,
1004
+ 101, 96, 218, 224, 225, 5, 94, 58, 141, 225, 132, 168, 118, 184, 251,
1005
+ 105, 226, 136, 35, 94, 9, 34, 3, 128, 195, 153, 50, 60, 181, 19, 194,
1006
+ 98, 84, 196, 202, 168, 140, 1, 127, 77, 88, 238, 211, 184, 92, 117, 128,
1007
+ 144, 232, 210, 17, 27, 14, 157, 88, 96, 109, 76, 12, 230, 193, 52, 89,
1008
+ 185, 127, 233, 137, 151, 101, 46, 245, 56, 125, 57, 126, 177, 79, 113,
1009
+ 20, 30, 50,
1010
+ ],
1011
+ );
1012
+
1013
+ let writeMessagesCalled = false;
1014
+ {
1015
+ await using run = testCreateRun({
1016
+ storage: {
1017
+ ...shouldNotBeCalledStorageDep.storage,
1018
+ writeMessages:
1019
+ (
1020
+ _ownerId: OwnerIdBytes,
1021
+ encryptedMessages: NonEmptyReadonlyArray<EncryptedCrdtMessage>,
1022
+ ) =>
1023
+ () => {
1024
+ writeMessagesCalled = true;
1025
+ assertEqual(encryptedMessages.length, messages.length);
1026
+ return ok();
1027
+ },
1028
+ },
1029
+ });
1030
+ const result = await run(
1031
+ applyProtocolMessageAsClient(broadcastedMessage),
1032
+ );
1033
+ assertOk(result, { type: "NoResponse" });
1034
+ }
1035
+ assertTrue(writeMessagesCalled);
1036
+ });
1037
+ });