@evolu/common 8.13.0 → 8.15.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 (205) hide show
  1. package/dist/src/Crypto.d.ts +2 -1
  2. package/dist/src/Crypto.d.ts.map +1 -1
  3. package/dist/src/Redacted.d.ts +19 -39
  4. package/dist/src/Redacted.d.ts.map +1 -1
  5. package/dist/src/Redacted.js +18 -33
  6. package/dist/src/Type.d.ts +161 -0
  7. package/dist/src/Type.d.ts.map +1 -1
  8. package/dist/src/Type.js +152 -2
  9. package/dist/src/WebSocket.d.ts +16 -1
  10. package/dist/src/WebSocket.d.ts.map +1 -1
  11. package/dist/src/WebSocket.js +14 -2
  12. package/dist/src/intl/_en.d.ts +5 -1
  13. package/dist/src/intl/_en.d.ts.map +1 -1
  14. package/dist/src/intl/_en.js +4 -0
  15. package/dist/src/intl/ar.d.ts +5 -1
  16. package/dist/src/intl/ar.d.ts.map +1 -1
  17. package/dist/src/intl/ar.js +4 -0
  18. package/dist/src/intl/bn.d.ts +5 -1
  19. package/dist/src/intl/bn.d.ts.map +1 -1
  20. package/dist/src/intl/bn.js +4 -0
  21. package/dist/src/intl/ca.d.ts +5 -1
  22. package/dist/src/intl/ca.d.ts.map +1 -1
  23. package/dist/src/intl/ca.js +4 -0
  24. package/dist/src/intl/cs.d.ts +5 -1
  25. package/dist/src/intl/cs.d.ts.map +1 -1
  26. package/dist/src/intl/cs.js +4 -0
  27. package/dist/src/intl/da.d.ts +5 -1
  28. package/dist/src/intl/da.d.ts.map +1 -1
  29. package/dist/src/intl/da.js +4 -0
  30. package/dist/src/intl/de.d.ts +5 -1
  31. package/dist/src/intl/de.d.ts.map +1 -1
  32. package/dist/src/intl/de.js +4 -0
  33. package/dist/src/intl/el.d.ts +5 -1
  34. package/dist/src/intl/el.d.ts.map +1 -1
  35. package/dist/src/intl/el.js +4 -0
  36. package/dist/src/intl/es.d.ts +5 -1
  37. package/dist/src/intl/es.d.ts.map +1 -1
  38. package/dist/src/intl/es.js +4 -0
  39. package/dist/src/intl/fa.d.ts +5 -1
  40. package/dist/src/intl/fa.d.ts.map +1 -1
  41. package/dist/src/intl/fa.js +4 -0
  42. package/dist/src/intl/fi.d.ts +5 -1
  43. package/dist/src/intl/fi.d.ts.map +1 -1
  44. package/dist/src/intl/fi.js +4 -0
  45. package/dist/src/intl/fil.d.ts +5 -1
  46. package/dist/src/intl/fil.d.ts.map +1 -1
  47. package/dist/src/intl/fil.js +4 -0
  48. package/dist/src/intl/fr.d.ts +5 -1
  49. package/dist/src/intl/fr.d.ts.map +1 -1
  50. package/dist/src/intl/fr.js +4 -0
  51. package/dist/src/intl/he.d.ts +5 -1
  52. package/dist/src/intl/he.d.ts.map +1 -1
  53. package/dist/src/intl/he.js +4 -0
  54. package/dist/src/intl/hi.d.ts +5 -1
  55. package/dist/src/intl/hi.d.ts.map +1 -1
  56. package/dist/src/intl/hi.js +4 -0
  57. package/dist/src/intl/hr.d.ts +5 -1
  58. package/dist/src/intl/hr.d.ts.map +1 -1
  59. package/dist/src/intl/hr.js +4 -0
  60. package/dist/src/intl/hu.d.ts +3 -1
  61. package/dist/src/intl/hu.d.ts.map +1 -1
  62. package/dist/src/intl/hu.js +2 -0
  63. package/dist/src/intl/id.d.ts +5 -1
  64. package/dist/src/intl/id.d.ts.map +1 -1
  65. package/dist/src/intl/id.js +4 -0
  66. package/dist/src/intl/it.d.ts +5 -1
  67. package/dist/src/intl/it.d.ts.map +1 -1
  68. package/dist/src/intl/it.js +4 -0
  69. package/dist/src/intl/ja.d.ts +5 -1
  70. package/dist/src/intl/ja.d.ts.map +1 -1
  71. package/dist/src/intl/ja.js +4 -0
  72. package/dist/src/intl/ko.d.ts +5 -1
  73. package/dist/src/intl/ko.d.ts.map +1 -1
  74. package/dist/src/intl/ko.js +4 -0
  75. package/dist/src/intl/ml.d.ts +5 -1
  76. package/dist/src/intl/ml.d.ts.map +1 -1
  77. package/dist/src/intl/ml.js +4 -0
  78. package/dist/src/intl/mr.d.ts +5 -1
  79. package/dist/src/intl/mr.d.ts.map +1 -1
  80. package/dist/src/intl/mr.js +4 -0
  81. package/dist/src/intl/ms.d.ts +5 -1
  82. package/dist/src/intl/ms.d.ts.map +1 -1
  83. package/dist/src/intl/ms.js +4 -0
  84. package/dist/src/intl/nb.d.ts +3 -1
  85. package/dist/src/intl/nb.d.ts.map +1 -1
  86. package/dist/src/intl/nb.js +2 -0
  87. package/dist/src/intl/nl.d.ts +5 -1
  88. package/dist/src/intl/nl.d.ts.map +1 -1
  89. package/dist/src/intl/nl.js +4 -0
  90. package/dist/src/intl/pa.d.ts +5 -1
  91. package/dist/src/intl/pa.d.ts.map +1 -1
  92. package/dist/src/intl/pa.js +4 -0
  93. package/dist/src/intl/pl.d.ts +4 -0
  94. package/dist/src/intl/pl.d.ts.map +1 -1
  95. package/dist/src/intl/pl.js +4 -0
  96. package/dist/src/intl/pt-BR.d.ts +5 -1
  97. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  98. package/dist/src/intl/pt-BR.js +4 -0
  99. package/dist/src/intl/pt.d.ts +5 -1
  100. package/dist/src/intl/pt.d.ts.map +1 -1
  101. package/dist/src/intl/pt.js +4 -0
  102. package/dist/src/intl/ro.d.ts +5 -1
  103. package/dist/src/intl/ro.d.ts.map +1 -1
  104. package/dist/src/intl/ro.js +4 -0
  105. package/dist/src/intl/sk.d.ts +5 -1
  106. package/dist/src/intl/sk.d.ts.map +1 -1
  107. package/dist/src/intl/sk.js +4 -0
  108. package/dist/src/intl/sl.d.ts +5 -1
  109. package/dist/src/intl/sl.d.ts.map +1 -1
  110. package/dist/src/intl/sl.js +4 -0
  111. package/dist/src/intl/sv.d.ts +5 -1
  112. package/dist/src/intl/sv.d.ts.map +1 -1
  113. package/dist/src/intl/sv.js +4 -0
  114. package/dist/src/intl/sw.d.ts +2 -0
  115. package/dist/src/intl/sw.d.ts.map +1 -1
  116. package/dist/src/intl/sw.js +2 -0
  117. package/dist/src/intl/ta.d.ts +5 -1
  118. package/dist/src/intl/ta.d.ts.map +1 -1
  119. package/dist/src/intl/ta.js +4 -0
  120. package/dist/src/intl/te.d.ts +5 -1
  121. package/dist/src/intl/te.d.ts.map +1 -1
  122. package/dist/src/intl/te.js +4 -0
  123. package/dist/src/intl/th.d.ts +5 -1
  124. package/dist/src/intl/th.d.ts.map +1 -1
  125. package/dist/src/intl/th.js +4 -0
  126. package/dist/src/intl/tr.d.ts +5 -1
  127. package/dist/src/intl/tr.d.ts.map +1 -1
  128. package/dist/src/intl/tr.js +4 -0
  129. package/dist/src/intl/uk.d.ts +5 -1
  130. package/dist/src/intl/uk.d.ts.map +1 -1
  131. package/dist/src/intl/uk.js +4 -0
  132. package/dist/src/intl/ur.d.ts +5 -1
  133. package/dist/src/intl/ur.d.ts.map +1 -1
  134. package/dist/src/intl/ur.js +4 -0
  135. package/dist/src/intl/vi.d.ts +5 -1
  136. package/dist/src/intl/vi.d.ts.map +1 -1
  137. package/dist/src/intl/vi.js +4 -0
  138. package/dist/src/intl/zh-CN.d.ts +5 -1
  139. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  140. package/dist/src/intl/zh-CN.js +4 -0
  141. package/dist/src/intl/zh-TW.d.ts +5 -1
  142. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  143. package/dist/src/intl/zh-TW.js +4 -0
  144. package/dist/src/local-first/Evolu.d.ts +66 -2
  145. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  146. package/dist/src/local-first/Evolu.js +28 -4
  147. package/dist/src/local-first/Shared.d.ts +46 -39
  148. package/dist/src/local-first/Shared.d.ts.map +1 -1
  149. package/dist/src/local-first/Shared.js +30 -19
  150. package/package.json +1 -1
  151. package/src/Crypto.ts +2 -1
  152. package/src/Redacted.test.ts +231 -0
  153. package/src/Redacted.ts +36 -47
  154. package/src/Type.test.ts +166 -0
  155. package/src/Type.ts +198 -2
  156. package/src/WebSocket.ts +33 -4
  157. package/src/intl/_en.ts +10 -0
  158. package/src/intl/ar.ts +8 -0
  159. package/src/intl/bn.ts +10 -0
  160. package/src/intl/ca.ts +10 -0
  161. package/src/intl/cs.ts +10 -0
  162. package/src/intl/da.ts +10 -0
  163. package/src/intl/de.ts +10 -0
  164. package/src/intl/el.ts +10 -0
  165. package/src/intl/es.ts +10 -0
  166. package/src/intl/fa.ts +10 -0
  167. package/src/intl/fi.ts +10 -0
  168. package/src/intl/fil.ts +10 -0
  169. package/src/intl/fr.ts +10 -0
  170. package/src/intl/he.ts +10 -0
  171. package/src/intl/hi.ts +10 -0
  172. package/src/intl/hr.ts +10 -0
  173. package/src/intl/hu.ts +6 -0
  174. package/src/intl/id.ts +10 -0
  175. package/src/intl/intl.test.ts +88 -0
  176. package/src/intl/it.ts +10 -0
  177. package/src/intl/ja.ts +10 -0
  178. package/src/intl/ko.ts +10 -0
  179. package/src/intl/ml.ts +10 -0
  180. package/src/intl/mr.ts +10 -0
  181. package/src/intl/ms.ts +8 -0
  182. package/src/intl/nb.ts +6 -0
  183. package/src/intl/nl.ts +10 -0
  184. package/src/intl/pa.ts +10 -0
  185. package/src/intl/pl.ts +12 -0
  186. package/src/intl/pt-BR.ts +10 -0
  187. package/src/intl/pt.ts +8 -0
  188. package/src/intl/ro.ts +10 -0
  189. package/src/intl/sk.ts +8 -0
  190. package/src/intl/sl.ts +10 -0
  191. package/src/intl/sv.ts +10 -0
  192. package/src/intl/sw.ts +4 -0
  193. package/src/intl/ta.ts +10 -0
  194. package/src/intl/te.ts +10 -0
  195. package/src/intl/th.ts +10 -0
  196. package/src/intl/tr.ts +10 -0
  197. package/src/intl/uk.ts +10 -0
  198. package/src/intl/ur.ts +8 -0
  199. package/src/intl/vi.ts +8 -0
  200. package/src/intl/zh-CN.ts +10 -0
  201. package/src/intl/zh-TW.ts +10 -0
  202. package/src/local-first/Evolu.test.ts +162 -18
  203. package/src/local-first/Evolu.ts +102 -6
  204. package/src/local-first/Shared.test.ts +113 -30
  205. package/src/local-first/Shared.ts +73 -57
package/src/Type.test.ts CHANGED
@@ -103,6 +103,7 @@ import {
103
103
  Digit1To59,
104
104
  Digit1To99,
105
105
  discriminatedUnion,
106
+ Email,
106
107
  EvoluType,
107
108
  finite,
108
109
  FiniteNumber,
@@ -121,6 +122,7 @@ import {
121
122
  idBytesTypeValueLength,
122
123
  id,
123
124
  idToIdBytes,
125
+ idToUuid,
124
126
  json,
125
127
  Json,
126
128
  JsonArray,
@@ -216,6 +218,8 @@ import {
216
218
  UnknownResult,
217
219
  uint8ArrayToBase64Url,
218
220
  UrlSafeString,
221
+ Uuid,
222
+ uuidToId,
219
223
  zeroNonNegativeInt,
220
224
  type AnyType,
221
225
  type ArrayElementIssue,
@@ -239,6 +243,7 @@ import {
239
243
  type DiscriminatedUnionMemberError,
240
244
  type DiscriminatedUnionMemberIssue,
241
245
  type DiscriminatedUnionType,
246
+ type EmailError,
242
247
  type EvoluTypeError,
243
248
  type ExtractTyped,
244
249
  type FiniteError,
@@ -354,6 +359,7 @@ import {
354
359
  type UnionInputType,
355
360
  type UnionMemberError,
356
361
  type UnionType,
362
+ type UuidError,
357
363
  type ValidationOptions,
358
364
  type ValidateLiteral,
359
365
  type ValidateOutput,
@@ -545,6 +551,7 @@ describe("Type", () => {
545
551
  Digit1To51,
546
552
  Digit1To59,
547
553
  Digit1To99,
554
+ Email,
548
555
  EvoluType,
549
556
  FiniteNumber,
550
557
  Function,
@@ -601,6 +608,7 @@ describe("Type", () => {
601
608
  UnknownNextResult,
602
609
  UnknownResult,
603
610
  UrlSafeString,
611
+ Uuid,
604
612
  } as const satisfies Readonly<Record<ExportedTypeKey, TypeNode>>;
605
613
 
606
614
  const IntrospectionRoot = createType(
@@ -679,6 +687,7 @@ describe("Type", () => {
679
687
  "DateIsoFromDate",
680
688
  "DecimalString",
681
689
  "DiscriminatedUnion",
690
+ "Email",
682
691
  "EvoluType",
683
692
  "Finite",
684
693
  "Function",
@@ -753,6 +762,7 @@ describe("Type", () => {
753
762
  "Unknown",
754
763
  "Uppercased",
755
764
  "UrlSafeString",
765
+ "Uuid",
756
766
  ] as const;
757
767
 
758
768
  assertType<
@@ -10013,6 +10023,122 @@ describe("BrandFactory", () => {
10013
10023
  });
10014
10024
  });
10015
10025
 
10026
+ describe("Email", () => {
10027
+ // Single-address cases from the WebKit and Chromium
10028
+ // ValidityState-typeMismatch-email.js tests, without the cases that
10029
+ // test the browser's whitespace sanitization.
10030
+ it("accepts valid email addresses as defined by the HTML Standard", () => {
10031
+ for (const value of [
10032
+ "something@something.com",
10033
+ "someone@localhost.localdomain",
10034
+ "someone@127.0.0.1",
10035
+ "a@b.b",
10036
+ "a/b@domain.com",
10037
+ "{}@domain.com",
10038
+ "m*'!%@something.sa",
10039
+ "tu!!7n7.ad##0!!!@company.ca",
10040
+ "%@com.com",
10041
+ "!#$%&'*+/=?^_`{|}~.-@com.com",
10042
+ ".wooly@example.com",
10043
+ "wo..oly@example.com",
10044
+ "someone@do-ma-in.com",
10045
+ "somebody@example",
10046
+ "Ada@Example.COM",
10047
+ "a@xn--maana-pta.com",
10048
+ `${"a".repeat(64)}@p.com`,
10049
+ `a@${"p".repeat(63)}.${"c".repeat(63)}`,
10050
+ ]) {
10051
+ assertEqual(Email.fromUnknown(value), ok(value));
10052
+ }
10053
+ assertType<typeof Email.Output, string & Brand<"Email">>();
10054
+ });
10055
+
10056
+ it("rejects invalid email addresses", () => {
10057
+ for (const value of [
10058
+ "invalid:email@example.com",
10059
+ "@somewhere.com",
10060
+ "example.com",
10061
+ "@@example.com",
10062
+ "a space@example.com",
10063
+ "something@ex..ample.com",
10064
+ "a\b@c",
10065
+ "someone@somewhere.com.",
10066
+ '""test\blah""@example.com',
10067
+ '"testblah"@example.com',
10068
+ "someone@somewhere.com@",
10069
+ "someone@somewhere_com",
10070
+ "someone@some:where.com",
10071
+ ".",
10072
+ "F/s/f/a@feo+re.com",
10073
+ "some+long+email+address@some+host-weird-/looking.com",
10074
+ "a @p.com",
10075
+ "a\t@p.com",
10076
+ "a\u000B@p.com",
10077
+ "a\u000C@p.com",
10078
+ "a @p.com",
10079
+ "a @p.com",
10080
+ "ddjk-s-jk@asl-.com",
10081
+ "someone@do-.com",
10082
+ "somebody@-.com",
10083
+ `a@${"p".repeat(64)}.com`,
10084
+ `a@p.${"c".repeat(64)}`,
10085
+ " a@p.com",
10086
+ "a@p.com\n",
10087
+ "a@[127.0.0.1]",
10088
+ "mañana@example.com",
10089
+ "a@mañana.com",
10090
+ "",
10091
+ ]) {
10092
+ assertEqual(
10093
+ Email.fromUnknown(value),
10094
+ err({ type: "Email", value }),
10095
+ );
10096
+ }
10097
+ assertEqual(
10098
+ Email.formatError({ type: "Email", value: "example.com" }),
10099
+ 'The value "example.com" is not a valid email address.',
10100
+ );
10101
+ assertType<typeof Email.Error, EmailError>();
10102
+ });
10103
+ });
10104
+
10105
+ describe("Uuid", () => {
10106
+ it("accepts canonical lowercase UUIDs of every version and variant", () => {
10107
+ for (const value of [
10108
+ "20354d7a-e4fe-47af-8ff6-187bca92f3f9",
10109
+ "0190a6f4-8c3e-7b2a-9d41-5e6f7a8b9c0d",
10110
+ "00000000-0000-0000-0000-000000000000",
10111
+ "ffffffff-ffff-ffff-ffff-ffffffffffff",
10112
+ "01234567-89ab-0def-0123-456789abcdef",
10113
+ ]) {
10114
+ assertEqual(Uuid.fromUnknown(value), ok(value));
10115
+ }
10116
+ assertType<typeof Uuid.Output, string & Brand<"Uuid">>();
10117
+ });
10118
+
10119
+ it("rejects non-canonical text", () => {
10120
+ for (const value of [
10121
+ "20354D7A-E4FE-47AF-8FF6-187BCA92F3F9",
10122
+ "20354d7a-e4fe-47af-8ff6-187bca92f3F9",
10123
+ "{20354d7a-e4fe-47af-8ff6-187bca92f3f9}",
10124
+ "urn:uuid:20354d7a-e4fe-47af-8ff6-187bca92f3f9",
10125
+ "20354d7ae4fe47af8ff6187bca92f3f9",
10126
+ "20354d7a-e4fe-47af-8ff6-187bca92f3f",
10127
+ "20354d7a-e4fe-47af-8ff6-187bca92f3f9 ",
10128
+ "g0354d7a-e4fe-47af-8ff6-187bca92f3f9",
10129
+ "invalid",
10130
+ "",
10131
+ ]) {
10132
+ assertEqual(Uuid.fromUnknown(value), err({ type: "Uuid", value }));
10133
+ }
10134
+ assertEqual(
10135
+ Uuid.formatError({ type: "Uuid", value: "invalid" }),
10136
+ 'The value "invalid" is not a canonical lowercase UUID.',
10137
+ );
10138
+ assertType<typeof Uuid.Error, UuidError>();
10139
+ });
10140
+ });
10141
+
10016
10142
  describe("SimplePassword", () => {
10017
10143
  it("requires trimmed text containing between 8 and 64 characters", () => {
10018
10144
  assertEqual(
@@ -10195,6 +10321,46 @@ describe("BrandFactory", () => {
10195
10321
  assertEqual(idBytesTypeValueLength, 16);
10196
10322
  assertSame(idBytesToId(bytes), value);
10197
10323
  });
10324
+
10325
+ it("converts to and from a Uuid with the same bytes", () => {
10326
+ const deps = testCreateDeps();
10327
+ const uuid = Uuid.orThrow("0190a6f4-8c3e-7b2a-9d41-5e6f7a8b9c0d");
10328
+ const value = uuidToId(uuid);
10329
+
10330
+ assertEqual(value, "AZCm9Iw-eyqdQV5veoucDQ");
10331
+ assertEqualBytes(
10332
+ idToIdBytes(value),
10333
+ new globalThis.Uint8Array([
10334
+ 0x01, 0x90, 0xa6, 0xf4, 0x8c, 0x3e, 0x7b, 0x2a, 0x9d, 0x41, 0x5e,
10335
+ 0x6f, 0x7a, 0x8b, 0x9c, 0x0d,
10336
+ ]),
10337
+ );
10338
+ assertSame(idToUuid(value), uuid);
10339
+
10340
+ for (const id of [
10341
+ createId(deps),
10342
+ createIdAsUuidv7(deps),
10343
+ createIdFromString("uuid"),
10344
+ uuidToId(Uuid.orThrow("00000000-0000-0000-0000-000000000000")),
10345
+ uuidToId(Uuid.orThrow("ffffffff-ffff-ffff-ffff-ffffffffffff")),
10346
+ ]) {
10347
+ const converted = idToUuid(id);
10348
+ assertTrue(Uuid.is(converted));
10349
+ assertSame(uuidToId(converted), id);
10350
+ }
10351
+ assertEqual(idToUuid(createIdAsUuidv7(deps))[14], "7");
10352
+
10353
+ const todoId = uuidToId<"Todo">(uuid);
10354
+ assertType<typeof value, Id>();
10355
+ assertType<typeof todoId, Id & Brand<"Todo">>();
10356
+ assertType<ReturnType<typeof idToUuid>, Uuid>();
10357
+ void (() => {
10358
+ // @ts-expect-error A union would assign multiple brands to one Id.
10359
+ uuidToId<"Todo" | "User">(uuid);
10360
+ // @ts-expect-error uuidToId accepts only a validated Uuid.
10361
+ uuidToId("0190a6f4-8c3e-7b2a-9d41-5e6f7a8b9c0d");
10362
+ });
10363
+ });
10198
10364
  });
10199
10365
 
10200
10366
  describe("Int64String", () => {
package/src/Type.ts CHANGED
@@ -194,7 +194,7 @@
194
194
  * ```
195
195
  *
196
196
  * Evolu includes dozens of predefined Types and Type factories. Use Types such
197
- * as {@link Age}, {@link PositiveInt}, {@link DateIso},
197
+ * as {@link Age}, {@link PositiveInt}, {@link DateIso}, {@link Email}, {@link Uuid},
198
198
  * {@link NonEmptyTrimmedString100}, {@link Base64Url}, and {@link Json} directly.
199
199
  * Build domain Types with factories such as {@link brand}, {@link typed},
200
200
  * {@link minLength}, {@link maxLength}, {@link array}, {@link object},
@@ -448,7 +448,7 @@
448
448
  *
449
449
  * @module
450
450
  */
451
- import { utf8ToBytes } from "@noble/ciphers/utils.js";
451
+ import { bytesToHex, hexToBytes, utf8ToBytes } from "@noble/ciphers/utils.js";
452
452
  import { sha256 } from "@noble/hashes/sha2.js";
453
453
  import * as bip39 from "@scure/bip39";
454
454
  import { wordlist } from "@scure/bip39/wordlists/english.js";
@@ -7598,6 +7598,74 @@ export const Name = /*#__PURE__*/ brand(
7598
7598
  );
7599
7599
  export type Name = typeof Name.Output;
7600
7600
 
7601
+ /**
7602
+ * Error returned when a string is not a valid {@link Email}.
7603
+ *
7604
+ * @group String
7605
+ */
7606
+ export interface EmailError extends TypeError<"Email"> {
7607
+ readonly value: string;
7608
+ }
7609
+
7610
+ /**
7611
+ * Email address as defined by the WHATWG HTML Standard.
7612
+ *
7613
+ * Email accepts a [valid email
7614
+ * address](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address),
7615
+ * the syntax browsers enforce for `<input type="email">`. The HTML Standard
7616
+ * deliberately departs from RFC 5322: it rejects quoted local parts, comments,
7617
+ * IP address literals, and whitespace, and it accepts only ASCII. Use the ASCII
7618
+ * (`xn--`) form of internationalized domains.
7619
+ *
7620
+ * Email does not normalize. The local part is case-sensitive, so different
7621
+ * Email strings can still reach the same mailbox, and only a delivered message
7622
+ * proves that an address exists.
7623
+ *
7624
+ * The HTML Standard limits each domain label to 63 characters but sets no total
7625
+ * length. SMTP limits an address to 254 characters; compose
7626
+ * `maxLength(254)(Email)` when that limit matters.
7627
+ *
7628
+ * ### Example
7629
+ *
7630
+ * ```ts
7631
+ * import {
7632
+ * assertEqual,
7633
+ * assertErr,
7634
+ * assertOk,
7635
+ * Email,
7636
+ * maxLength,
7637
+ * } from "@evolu/common";
7638
+ *
7639
+ * assertOk(Email.fromUnknown("ada@example.com"), "ada@example.com");
7640
+ * assertOk(Email.fromUnknown("Ada@Example.com"), "Ada@Example.com");
7641
+ *
7642
+ * const invalid = Email.fromUnknown("Ada <ada@example.com>");
7643
+ * assertErr(invalid);
7644
+ * assertEqual(invalid.error, {
7645
+ * type: "Email",
7646
+ * value: "Ada <ada@example.com>",
7647
+ * });
7648
+ *
7649
+ * const SmtpEmail = maxLength(254)(Email);
7650
+ * assertErr(SmtpEmail.fromUnknown(`${"a".repeat(251)}@b.c`));
7651
+ * ```
7652
+ *
7653
+ * @group String
7654
+ */
7655
+ export const Email = /*#__PURE__*/ brand(
7656
+ "Email",
7657
+ String,
7658
+ (value) =>
7659
+ /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/u.test(
7660
+ value,
7661
+ )
7662
+ ? ok()
7663
+ : err<EmailError>({ type: "Email", value }),
7664
+ (error) =>
7665
+ `The value ${safelyStringifyUnknownValue(error.value)} is not a valid email address.`,
7666
+ );
7667
+ export type Email = typeof Email.Output;
7668
+
7601
7669
  /**
7602
7670
  * Stable valid {@link Name} for tests and internal fixtures.
7603
7671
  *
@@ -7967,6 +8035,134 @@ export const idToIdBytes = (value: Id): IdBytes =>
7967
8035
  export const idBytesToId = (value: IdBytes): Id =>
7968
8036
  uint8ArrayToBase64Url(value) as unknown as Id;
7969
8037
 
8038
+ /**
8039
+ * Error returned when a string is not a canonical {@link Uuid}.
8040
+ *
8041
+ * @group String
8042
+ */
8043
+ export interface UuidError extends TypeError<"Uuid"> {
8044
+ readonly value: string;
8045
+ }
8046
+
8047
+ /**
8048
+ * UUID in its canonical lowercase text form, as defined by [RFC
8049
+ * 9562](https://www.rfc-editor.org/rfc/rfc9562).
8050
+ *
8051
+ * Uuid accepts 32 lowercase hexadecimal digits in the 8-4-4-4-12 layout, as
8052
+ * produced by `crypto.randomUUID()`. It accepts every version and variant,
8053
+ * including the Nil and Max UUIDs, so every 128-bit value has exactly one
8054
+ * Uuid.
8055
+ *
8056
+ * RFC 9562 reads UUIDs in any case. Uuid requires lowercase so that equal UUIDs
8057
+ * are equal strings; lowercase text from other sources before validating it.
8058
+ *
8059
+ * Convert a Uuid to an {@link Id} with {@link uuidToId} and back with
8060
+ * {@link idToUuid}.
8061
+ *
8062
+ * ### Example
8063
+ *
8064
+ * ```ts
8065
+ * import { assertEqual, assertErr, assertOk, Uuid } from "@evolu/common";
8066
+ *
8067
+ * const value = "0190a6f4-8c3e-7b2a-9d41-5e6f7a8b9c0d";
8068
+ * assertOk(Uuid.fromUnknown(value), value);
8069
+ *
8070
+ * const uppercase = value.toUpperCase();
8071
+ * const invalid = Uuid.fromUnknown(uppercase);
8072
+ * assertErr(invalid);
8073
+ * assertEqual(invalid.error, { type: "Uuid", value: uppercase });
8074
+ * assertOk(Uuid.fromUnknown(uppercase.toLowerCase()), value);
8075
+ * ```
8076
+ *
8077
+ * @group String
8078
+ */
8079
+ export const Uuid = /*#__PURE__*/ brand(
8080
+ "Uuid",
8081
+ String,
8082
+ (value) =>
8083
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u.test(
8084
+ value,
8085
+ )
8086
+ ? ok()
8087
+ : err<UuidError>({ type: "Uuid", value }),
8088
+ (error) =>
8089
+ `The value ${safelyStringifyUnknownValue(error.value)} is not a canonical lowercase UUID.`,
8090
+ );
8091
+ export type Uuid = typeof Uuid.Output;
8092
+
8093
+ /**
8094
+ * Converts a {@link Uuid} to the {@link Id} with the same 16 bytes.
8095
+ *
8096
+ * Use this to store records whose external keys are UUIDs. Unlike
8097
+ * {@link createIdFromString}, the mapping is reversible with {@link idToUuid}, so
8098
+ * the original UUID does not need its own column. A time-based UUID, such as
8099
+ * version 1, 6, or 7, keeps its creation time in the Id.
8100
+ *
8101
+ * ### Example
8102
+ *
8103
+ * ```ts
8104
+ * import {
8105
+ * assertEqual,
8106
+ * assertType,
8107
+ * idToUuid,
8108
+ * Uuid,
8109
+ * uuidToId,
8110
+ * type Brand,
8111
+ * type Id,
8112
+ * } from "@evolu/common";
8113
+ *
8114
+ * const uuid = Uuid.orThrow("0190a6f4-8c3e-7b2a-9d41-5e6f7a8b9c0d");
8115
+ * const todoId = uuidToId<"Todo">(uuid);
8116
+ *
8117
+ * assertEqual(todoId, "AZCm9Iw-eyqdQV5veoucDQ");
8118
+ * assertType<typeof todoId, Id & Brand<"Todo">>();
8119
+ * assertEqual(idToUuid(todoId), uuid);
8120
+ * ```
8121
+ *
8122
+ * @group String
8123
+ */
8124
+ export const uuidToId = <B extends string = never>(
8125
+ value: Uuid,
8126
+ ..._validation: IdBrandValidation<B>
8127
+ ): CreatedId<B> =>
8128
+ idBytesToId(hexToBytes(value.replaceAll("-", "")) as IdBytes) as CreatedId<B>;
8129
+
8130
+ /**
8131
+ * Converts an {@link Id} to the {@link Uuid} with the same 16 bytes.
8132
+ *
8133
+ * The result is a Uuid of any version or variant, because Ids created by
8134
+ * {@link createId} are random 128-bit values. Ids created by
8135
+ * {@link createIdAsUuidv7} convert to version 7 UUIDs.
8136
+ *
8137
+ * ### Example
8138
+ *
8139
+ * ```ts
8140
+ * import {
8141
+ * assertEqual,
8142
+ * createIdAsUuidv7,
8143
+ * createRandomBytes,
8144
+ * createTime,
8145
+ * idToUuid,
8146
+ * uuidToId,
8147
+ * } from "@evolu/common";
8148
+ *
8149
+ * const id = createIdAsUuidv7({
8150
+ * randomBytes: createRandomBytes(),
8151
+ * time: createTime(),
8152
+ * });
8153
+ * const uuid = idToUuid(id);
8154
+ *
8155
+ * assertEqual(uuid[14], "7");
8156
+ * assertEqual(uuidToId(uuid), id);
8157
+ * ```
8158
+ *
8159
+ * @group String
8160
+ */
8161
+ export const idToUuid = (value: Id): Uuid => {
8162
+ const hex = bytesToHex(idToIdBytes(value));
8163
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}` as Uuid;
8164
+ };
8165
+
7970
8166
  /**
7971
8167
  * Error returned when a string is not a canonical {@link Int64String}.
7972
8168
  *
package/src/WebSocket.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  import { assert } from "./Assert.ts";
8
8
  import { constTrue } from "./Function.ts";
9
9
  import type { Result } from "./Result.ts";
10
- import { err, ok } from "./Result.ts";
10
+ import { err, ok, trySync } from "./Result.ts";
11
11
  import type { Schedule } from "./Schedule.ts";
12
12
  import { exponential, jitter, maxDelay } from "./Schedule.ts";
13
13
  import type { RetryError, Task } from "./Task.ts";
@@ -266,10 +266,24 @@ export interface WebSocketOptions {
266
266
  * @group Errors
267
267
  */
268
268
  export type WebSocketError =
269
+ | WebSocketCreateError
269
270
  | WebSocketConnectError
270
271
  | WebSocketConnectionError
271
272
  | RetryError<WebSocketRetryError>;
272
273
 
274
+ /**
275
+ * An error that occurs when the platform's WebSocket constructor throws, for
276
+ * example for a URL it rejects, a `ws:` URL on an `https:` page, or a Content
277
+ * Security Policy that blocks the URL. These fail the same way on every
278
+ * attempt, so the {@link WebSocket} stops connecting. The thrown value is not
279
+ * always an `Error`.
280
+ *
281
+ * @group Errors
282
+ */
283
+ export interface WebSocketCreateError extends Typed<"WebSocketCreateError"> {
284
+ readonly error: unknown;
285
+ }
286
+
273
287
  /**
274
288
  * An error that occurs when a connection cannot be established due to a network
275
289
  * error. Fires before `onclose`.
@@ -344,6 +358,9 @@ const defaultHealthyConnectionDuration = /*#__PURE__*/ durationToMillis("30s");
344
358
  /**
345
359
  * Create a new {@link WebSocket}.
346
360
  *
361
+ * When the platform's WebSocket constructor throws, `onError` receives a
362
+ * {@link WebSocketCreateError} and the WebSocket stops connecting.
363
+ *
347
364
  * @group Core
348
365
  */
349
366
  export const createWebSocket: CreateWebSocket =
@@ -403,10 +420,22 @@ export const createWebSocket: CreateWebSocket =
403
420
  closeSocket();
404
421
  resolveConnect = resolve;
405
422
 
406
- socket = new WebSocketConstructor(
407
- url,
408
- String.is(protocols) ? protocols : protocols && [...protocols],
423
+ const created = trySync(
424
+ () =>
425
+ new WebSocketConstructor(
426
+ url,
427
+ String.is(protocols) ? protocols : protocols && [...protocols],
428
+ ),
409
429
  );
430
+ // A throw that escaped this Task would panic the Run, which in Evolu's
431
+ // SharedWorker stops sync for every tab.
432
+ if (!created.ok) {
433
+ resolveConnect = null;
434
+ onError?.({ type: "WebSocketCreateError", error: created.error });
435
+ resolve(ok());
436
+ return;
437
+ }
438
+ socket = created.value;
410
439
 
411
440
  if (binaryType) socket.binaryType = binaryType;
412
441
 
package/src/intl/_en.ts CHANGED
@@ -32,6 +32,7 @@ import type {
32
32
  DateIsoFromDateError,
33
33
  DecimalStringError,
34
34
  DiscriminatedUnionError,
35
+ EmailError,
35
36
  EvoluTypeError,
36
37
  FiniteError,
37
38
  GreaterThanError,
@@ -83,6 +84,7 @@ import type {
83
84
  TypeOfError,
84
85
  UInt64Error,
85
86
  UnionError,
87
+ UuidError,
86
88
  } from "../Type.ts";
87
89
 
88
90
  const formatTypeOfError = (
@@ -268,6 +270,10 @@ export const formatBase64UrlError: TypeErrorFormatter<Base64UrlError> = (
268
270
  export const formatNameError: TypeErrorFormatter<NameError> = (error) =>
269
271
  `The value ${safelyStringifyUnknownValue(error.value)} is not a valid Name.`;
270
272
 
273
+ /** Formats an EmailError in English. */
274
+ export const formatEmailError: TypeErrorFormatter<EmailError> = (error) =>
275
+ `The value ${safelyStringifyUnknownValue(error.value)} is not a valid email address.`;
276
+
271
277
  /** Formats a MnemonicError in English. */
272
278
  export const formatMnemonicError: TypeErrorFormatter<MnemonicError> = (error) =>
273
279
  `The value ${safelyStringifyUnknownValue(error.value)} is not a valid English BIP39 mnemonic.`;
@@ -280,6 +286,10 @@ export const formatIdError: TypeErrorFormatter<IdError> = (error) =>
280
286
  export const formatTableIdError: TypeErrorFormatter<TableIdError> = (error) =>
281
287
  `The value ${safelyStringifyUnknownValue(error.value)} is not a valid Id for table ${error.table}.`;
282
288
 
289
+ /** Formats a UuidError in English. */
290
+ export const formatUuidError: TypeErrorFormatter<UuidError> = (error) =>
291
+ `The value ${safelyStringifyUnknownValue(error.value)} is not a canonical lowercase UUID.`;
292
+
283
293
  /** Formats a NonNegativeError in English. */
284
294
  export const formatNonNegativeError: TypeErrorFormatter<NonNegativeError> = (
285
295
  error,
package/src/intl/ar.ts CHANGED
@@ -23,6 +23,7 @@ import type {
23
23
  DateIsoFromDateError,
24
24
  DecimalStringError,
25
25
  DiscriminatedUnionError,
26
+ EmailError,
26
27
  EvoluTypeError,
27
28
  FiniteError,
28
29
  GreaterThanError,
@@ -74,6 +75,7 @@ import type {
74
75
  TypeOfError,
75
76
  UInt64Error,
76
77
  UnionError,
78
+ UuidError,
77
79
  } from "../Type.ts";
78
80
 
79
81
  const formatTypeOfError = (
@@ -225,6 +227,9 @@ export const formatBase64UrlError: TypeErrorFormatter<Base64UrlError> = (
225
227
  /** Formats a NameError in Arabic. */
226
228
  export const formatNameError: TypeErrorFormatter<NameError> = (error) =>
227
229
  `القيمة ${safelyStringifyUnknownValue(error.value)} ليست Name صالحة.`;
230
+ /** Formats an EmailError in Arabic. */
231
+ export const formatEmailError: TypeErrorFormatter<EmailError> = (error) =>
232
+ `القيمة ${safelyStringifyUnknownValue(error.value)} ليست عنوان بريد إلكتروني صالحاً.`;
228
233
  /** Formats a MnemonicError in Arabic. */
229
234
  export const formatMnemonicError: TypeErrorFormatter<MnemonicError> = (error) =>
230
235
  `القيمة ${safelyStringifyUnknownValue(error.value)} ليست عبارة BIP39 إنجليزية صالحة.`;
@@ -234,6 +239,9 @@ export const formatIdError: TypeErrorFormatter<IdError> = (error) =>
234
239
  /** Formats a TableIdError in Arabic. */
235
240
  export const formatTableIdError: TypeErrorFormatter<TableIdError> = (error) =>
236
241
  `القيمة ${safelyStringifyUnknownValue(error.value)} ليست Id صالحة للجدول ${error.table}.`;
242
+ /** Formats a UuidError in Arabic. */
243
+ export const formatUuidError: TypeErrorFormatter<UuidError> = (error) =>
244
+ `القيمة ${safelyStringifyUnknownValue(error.value)} ليست UUID معيارياً بأحرف صغيرة.`;
237
245
  /** Formats a NonNegativeError in Arabic. */
238
246
  export const formatNonNegativeError: TypeErrorFormatter<NonNegativeError> = (
239
247
  error,
package/src/intl/bn.ts CHANGED
@@ -23,6 +23,7 @@ import type {
23
23
  DateIsoFromDateError,
24
24
  DecimalStringError,
25
25
  DiscriminatedUnionError,
26
+ EmailError,
26
27
  EvoluTypeError,
27
28
  FiniteError,
28
29
  GreaterThanError,
@@ -74,6 +75,7 @@ import type {
74
75
  TypeOfError,
75
76
  UInt64Error,
76
77
  UnionError,
78
+ UuidError,
77
79
  } from "../Type.ts";
78
80
 
79
81
  const formatTypeOfError = (
@@ -257,6 +259,10 @@ export const formatBase64UrlError: TypeErrorFormatter<Base64UrlError> = (
257
259
  export const formatNameError: TypeErrorFormatter<NameError> = (error) =>
258
260
  `${safelyStringifyUnknownValue(error.value)} মানটি বৈধ Name নয়।`;
259
261
 
262
+ /** Formats an EmailError in Bengali. */
263
+ export const formatEmailError: TypeErrorFormatter<EmailError> = (error) =>
264
+ `${safelyStringifyUnknownValue(error.value)} মানটি বৈধ ইমেল ঠিকানা নয়।`;
265
+
260
266
  /** Formats a MnemonicError in Bengali. */
261
267
  export const formatMnemonicError: TypeErrorFormatter<MnemonicError> = (error) =>
262
268
  `${safelyStringifyUnknownValue(error.value)} মানটি বৈধ ইংরেজি BIP39 mnemonic নয়।`;
@@ -269,6 +275,10 @@ export const formatIdError: TypeErrorFormatter<IdError> = (error) =>
269
275
  export const formatTableIdError: TypeErrorFormatter<TableIdError> = (error) =>
270
276
  `${safelyStringifyUnknownValue(error.value)} মানটি ${error.table} table-এর জন্য বৈধ Id নয়।`;
271
277
 
278
+ /** Formats a UuidError in Bengali. */
279
+ export const formatUuidError: TypeErrorFormatter<UuidError> = (error) =>
280
+ `${safelyStringifyUnknownValue(error.value)} মানটি ছোট হাতের অক্ষরে লেখা canonical UUID নয়।`;
281
+
272
282
  /** Formats a NonNegativeError in Bengali. */
273
283
  export const formatNonNegativeError: TypeErrorFormatter<NonNegativeError> = (
274
284
  error,
package/src/intl/ca.ts CHANGED
@@ -23,6 +23,7 @@ import type {
23
23
  DateIsoFromDateError,
24
24
  DecimalStringError,
25
25
  DiscriminatedUnionError,
26
+ EmailError,
26
27
  EvoluTypeError,
27
28
  FiniteError,
28
29
  GreaterThanError,
@@ -74,6 +75,7 @@ import type {
74
75
  TypeOfError,
75
76
  UInt64Error,
76
77
  UnionError,
78
+ UuidError,
77
79
  } from "../Type.ts";
78
80
 
79
81
  const formatTypeOfError = (
@@ -259,6 +261,10 @@ export const formatBase64UrlError: TypeErrorFormatter<Base64UrlError> = (
259
261
  export const formatNameError: TypeErrorFormatter<NameError> = (error) =>
260
262
  `El valor ${safelyStringifyUnknownValue(error.value)} no és un Name vàlid.`;
261
263
 
264
+ /** Formata EmailError en català. */
265
+ export const formatEmailError: TypeErrorFormatter<EmailError> = (error) =>
266
+ `El valor ${safelyStringifyUnknownValue(error.value)} no és una adreça de correu electrònic vàlida.`;
267
+
262
268
  /** Formata MnemonicError en català. */
263
269
  export const formatMnemonicError: TypeErrorFormatter<MnemonicError> = (error) =>
264
270
  `El valor ${safelyStringifyUnknownValue(error.value)} no és una frase mnemotècnica BIP39 en anglès vàlida.`;
@@ -271,6 +277,10 @@ export const formatIdError: TypeErrorFormatter<IdError> = (error) =>
271
277
  export const formatTableIdError: TypeErrorFormatter<TableIdError> = (error) =>
272
278
  `El valor ${safelyStringifyUnknownValue(error.value)} no és un Id vàlid per a la taula ${error.table}.`;
273
279
 
280
+ /** Formata UuidError en català. */
281
+ export const formatUuidError: TypeErrorFormatter<UuidError> = (error) =>
282
+ `El valor ${safelyStringifyUnknownValue(error.value)} no és un UUID canònic en minúscules.`;
283
+
274
284
  /** Formata NonNegativeError en català. */
275
285
  export const formatNonNegativeError: TypeErrorFormatter<NonNegativeError> = (
276
286
  error,