@evolu/common 6.0.1-preview.33 → 6.0.1-preview.34

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 (48) hide show
  1. package/README.md +2 -2
  2. package/dist/src/Array.d.ts +3 -3
  3. package/dist/src/Array.js +3 -3
  4. package/dist/src/Crypto.d.ts +6 -8
  5. package/dist/src/Crypto.d.ts.map +1 -1
  6. package/dist/src/Crypto.js +9 -9
  7. package/dist/src/Evolu/Db.d.ts.map +1 -1
  8. package/dist/src/Evolu/Db.js +4 -4
  9. package/dist/src/Evolu/Evolu.d.ts +9 -4
  10. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  11. package/dist/src/Evolu/Evolu.js +11 -11
  12. package/dist/src/Evolu/Protocol.d.ts +24 -1
  13. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  14. package/dist/src/Evolu/Protocol.js +59 -30
  15. package/dist/src/Evolu/Query.d.ts +1 -1
  16. package/dist/src/Evolu/Query.d.ts.map +1 -1
  17. package/dist/src/Evolu/Query.js +1 -1
  18. package/dist/src/Evolu/Schema.d.ts +24 -20
  19. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  20. package/dist/src/Evolu/Schema.js +36 -27
  21. package/dist/src/Evolu/Storage.d.ts +10 -2
  22. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  23. package/dist/src/Evolu/Storage.js +18 -4
  24. package/dist/src/Evolu/Sync.d.ts +2 -1
  25. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  26. package/dist/src/Evolu/Sync.js +45 -33
  27. package/dist/src/Evolu/Timestamp.d.ts +10 -14
  28. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  29. package/dist/src/Evolu/Timestamp.js +3 -16
  30. package/dist/src/Object.d.ts +10 -4
  31. package/dist/src/Object.d.ts.map +1 -1
  32. package/dist/src/Object.js +9 -3
  33. package/dist/src/Type.d.ts +56 -14
  34. package/dist/src/Type.d.ts.map +1 -1
  35. package/dist/src/Type.js +57 -31
  36. package/package.json +2 -2
  37. package/src/Array.ts +3 -3
  38. package/src/Crypto.ts +11 -9
  39. package/src/Evolu/Db.ts +7 -7
  40. package/src/Evolu/Evolu.ts +21 -20
  41. package/src/Evolu/Protocol.ts +70 -35
  42. package/src/Evolu/Query.ts +2 -2
  43. package/src/Evolu/Schema.ts +44 -32
  44. package/src/Evolu/Storage.ts +40 -2
  45. package/src/Evolu/Sync.ts +54 -32
  46. package/src/Evolu/Timestamp.ts +5 -25
  47. package/src/Object.ts +13 -5
  48. package/src/Type.ts +57 -35
package/src/Evolu/Sync.ts CHANGED
@@ -22,7 +22,13 @@ import { err, ok, Result } from "../Result.js";
22
22
  import { sql, SqliteDep, SqliteError, SqliteValue } from "../Sqlite.js";
23
23
  import { AbortError, createMutex } from "../Task.js";
24
24
  import { TimeDep } from "../Time.js";
25
- import { IdBytes, idBytesToId, idToIdBytes, PositiveInt } from "../Type.js";
25
+ import {
26
+ DateIso,
27
+ IdBytes,
28
+ idBytesToId,
29
+ idToIdBytes,
30
+ PositiveInt,
31
+ } from "../Type.js";
26
32
  import { CreateWebSocketDep, WebSocket } from "../WebSocket.js";
27
33
  import type { PostMessageDep } from "./Db.js";
28
34
  import {
@@ -74,8 +80,8 @@ import {
74
80
  TimestampCounterOverflowError,
75
81
  TimestampDriftError,
76
82
  TimestampTimeOutOfRangeError,
83
+ timestampToDateIso,
77
84
  timestampToTimestampBytes,
78
- timestampToTimestampString,
79
85
  } from "./Timestamp.js";
80
86
 
81
87
  export interface Sync extends Disposable {
@@ -341,7 +347,10 @@ export const createSync =
341
347
  clockTimestamp = nextTimestamp.value;
342
348
 
343
349
  const { ownerId = config.appOwner.id, ...dbChange } = change;
344
- const message = { timestamp: clockTimestamp, change: dbChange };
350
+ const message: CrdtMessage = {
351
+ timestamp: clockTimestamp,
352
+ change: dbChange,
353
+ };
345
354
 
346
355
  const messages = ownerMessages.get(ownerId);
347
356
  if (messages) messages.push(message);
@@ -397,7 +406,6 @@ export interface ClockDep {
397
406
  readonly clock: Clock;
398
407
  }
399
408
 
400
- // HLC
401
409
  export interface Clock {
402
410
  readonly get: () => Timestamp;
403
411
  readonly save: (timestamp: Timestamp) => Result<void, SqliteError>;
@@ -413,9 +421,9 @@ export const createClock =
413
421
  save: (timestamp) => {
414
422
  currentTimestamp = timestamp;
415
423
 
416
- const timestampString = timestampToTimestampString(timestamp);
417
424
  const result = deps.sqlite.exec(sql.prepared`
418
- update evolu_config set "clock" = ${timestampString};
425
+ update evolu_config
426
+ set "clock" = ${timestampToTimestampBytes(timestamp)};
419
427
  `);
420
428
  if (!result.ok) return result;
421
429
 
@@ -587,20 +595,26 @@ const createClientStorage =
587
595
 
588
596
  const { table, id } = rows[0];
589
597
  const values: Record<string, SqliteValue> = {};
598
+ let isInsert = false;
590
599
 
591
600
  for (const r of rows) {
592
601
  assert(r.table === table, "All rows must have the same table");
593
602
  assert(eqArrayNumber(r.id, id), "All rows must have the same Id");
594
- values[r.column] = r.value;
603
+ if (r.column === "createdAt") {
604
+ isInsert = true;
605
+ } else {
606
+ values[r.column] = r.value;
607
+ }
595
608
  }
596
609
 
597
610
  const message: CrdtMessage = {
598
611
  timestamp: timestampBytesToTimestamp(timestamp),
599
- change: {
612
+ change: DbChange.orThrow({
600
613
  table: rows[0].table,
601
614
  id: idBytesToId(rows[0].id),
602
615
  values,
603
- },
616
+ isInsert,
617
+ }),
604
618
  };
605
619
 
606
620
  return encodeAndEncryptDbChange(deps)(message, owner.encryptionKey);
@@ -620,33 +634,27 @@ const createTransportKey = (transportConfig: OwnerTransport): TransportKey => {
620
634
  export const applyLocalOnlyChange =
621
635
  (deps: SqliteDep & TimeDep) =>
622
636
  (change: MutationChange): Result<void, SqliteError> => {
623
- const dbChange: DbChange = {
624
- table: change.table,
625
- id: change.id,
626
- values: change.values,
627
- };
628
-
629
637
  const isDeletion =
630
- "isDeleted" in dbChange.values && dbChange.values.isDeleted === 1;
638
+ "isDeleted" in change.values && change.values.isDeleted === 1;
631
639
 
632
640
  if (isDeletion) {
633
641
  const result = deps.sqlite.exec(sql`
634
- delete from ${sql.identifier(dbChange.table)}
635
- where id = ${dbChange.id};
642
+ delete from ${sql.identifier(change.table)}
643
+ where id = ${change.id};
636
644
  `);
637
645
  if (!result.ok) return result;
638
646
  } else {
639
- const date = new Date(deps.time.now()).toISOString();
647
+ const now = deps.time.nowIso();
640
648
 
641
- for (const [column, value] of objectToEntries(dbChange.values)) {
649
+ for (const [column, value] of objectToEntries(change.values)) {
642
650
  const result = deps.sqlite.exec(sql.prepared`
643
- insert into ${sql.identifier(dbChange.table)}
651
+ insert into ${sql.identifier(change.table)}
644
652
  ("id", ${sql.identifier(column)}, createdAt, updatedAt)
645
- values (${dbChange.id}, ${value}, ${date}, ${date})
653
+ values (${change.id}, ${value}, ${now}, ${now})
646
654
  on conflict ("id") do update
647
655
  set
648
656
  ${sql.identifier(column)} = ${value},
649
- updatedAt = ${date};
657
+ updatedAt = ${now};
650
658
  `);
651
659
  if (!result.ok) return result;
652
660
  }
@@ -672,7 +680,8 @@ const applyMessages =
672
680
  let { firstTimestamp, lastTimestamp } = usageResult.value;
673
681
 
674
682
  for (const message of messages) {
675
- const result1 = applyMessageToAppTable(deps)(ownerIdBytes, message);
683
+ const date = timestampToDateIso(message.timestamp);
684
+ const result1 = applyMessageToAppTable(deps)(ownerIdBytes, message, date);
676
685
  if (!result1.ok) return result1;
677
686
 
678
687
  const timestamp = timestampToTimestampBytes(message.timestamp);
@@ -688,6 +697,7 @@ const applyMessages =
688
697
  ownerIdBytes,
689
698
  message,
690
699
  strategy,
700
+ date,
691
701
  );
692
702
  if (!result2.ok) return result2;
693
703
  }
@@ -710,11 +720,17 @@ const applyMessages =
710
720
 
711
721
  const applyMessageToAppTable =
712
722
  (deps: SqliteDep) =>
713
- (ownerId: OwnerIdBytes, message: CrdtMessage): Result<void, SqliteError> => {
714
- const timestamp = timestampToTimestampBytes(message.timestamp);
715
- const updatedAt = new Date(message.timestamp.millis).toISOString();
723
+ (
724
+ ownerId: OwnerIdBytes,
725
+ message: CrdtMessage,
726
+ date: DateIso,
727
+ ): Result<void, SqliteError> => {
728
+ let entries = objectToEntries(message.change.values);
729
+ if (message.change.isInsert) {
730
+ entries = [...entries, ["createdAt", date]];
731
+ }
716
732
 
717
- for (const [column, value] of objectToEntries(message.change.values)) {
733
+ for (const [column, value] of entries) {
718
734
  const result = deps.sqlite.exec(sql.prepared`
719
735
  with
720
736
  existingTimestamp as (
@@ -725,17 +741,17 @@ const applyMessageToAppTable =
725
741
  and "table" = ${message.change.table}
726
742
  and "id" = ${idToIdBytes(message.change.id)}
727
743
  and "column" = ${column}
728
- and "timestamp" >= ${timestamp}
744
+ and "timestamp" >= ${timestampToTimestampBytes(message.timestamp)}
729
745
  limit 1
730
746
  )
731
747
  insert into ${sql.identifier(message.change.table)}
732
748
  ("id", ${sql.identifier(column)}, updatedAt)
733
- select ${message.change.id}, ${value}, ${updatedAt}
749
+ select ${message.change.id}, ${value}, ${date}
734
750
  where not exists (select 1 from existingTimestamp)
735
751
  on conflict ("id") do update
736
752
  set
737
753
  ${sql.identifier(column)} = ${value},
738
- updatedAt = ${updatedAt}
754
+ updatedAt = ${date}
739
755
  where not exists (select 1 from existingTimestamp);
740
756
  `);
741
757
 
@@ -751,6 +767,7 @@ export const applyMessageToTimestampAndHistoryTables =
751
767
  ownerId: OwnerIdBytes,
752
768
  message: CrdtMessage,
753
769
  strategy: StorageInsertTimestampStrategy,
770
+ date: DateIso,
754
771
  ): Result<void, SqliteError> => {
755
772
  const timestamp = timestampToTimestampBytes(message.timestamp);
756
773
  const id = idToIdBytes(message.change.id);
@@ -758,7 +775,12 @@ export const applyMessageToTimestampAndHistoryTables =
758
775
  const result = deps.storage.insertTimestamp(ownerId, timestamp, strategy);
759
776
  if (!result.ok) return result;
760
777
 
761
- for (const [column, value] of Object.entries(message.change.values)) {
778
+ let entries = objectToEntries(message.change.values);
779
+ if (message.change.isInsert) {
780
+ entries = [...entries, ["createdAt", date]];
781
+ }
782
+
783
+ for (const [column, value] of entries) {
762
784
  const result = deps.sqlite.exec(sql.prepared`
763
785
  insert into evolu_history
764
786
  ("ownerId", "table", "id", "column", "value", "timestamp")
@@ -1,5 +1,3 @@
1
- import { assert } from "../Assert.js";
2
- import { Brand } from "../Brand.js";
3
1
  import { bytesToHex } from "../Buffer.js";
4
2
  import { RandomBytesDep } from "../Crypto.js";
5
3
  import { createEqObject, eqNumber, eqString } from "../Eq.js";
@@ -9,6 +7,7 @@ import { err, ok, Result } from "../Result.js";
9
7
  import { TimeDep } from "../Time.js";
10
8
  import {
11
9
  brand,
10
+ DateIso,
12
11
  InferType,
13
12
  lessThanOrEqualTo,
14
13
  NonNegativeInt,
@@ -192,29 +191,6 @@ export const createInitialTimestamp = (deps: RandomBytesDep): Timestamp => {
192
191
  return createTimestamp({ nodeId });
193
192
  };
194
193
 
195
- /** Sortable string representation of {@link Timestamp}. */
196
- export type TimestampString = string & Brand<"TimestampString">;
197
-
198
- export const timestampToTimestampString = (t: Timestamp): TimestampString =>
199
- [
200
- new Date(t.millis).toISOString(),
201
- t.counter.toString(16).toUpperCase().padStart(4, "0"),
202
- t.nodeId,
203
- ].join("-") as TimestampString;
204
-
205
- export const timestampStringToTimestamp = (
206
- timestampString: TimestampString,
207
- ): Timestamp => {
208
- const array = timestampString.split("-");
209
- const timestamp = {
210
- millis: Date.parse(array.slice(0, 3).join("-")).valueOf(),
211
- counter: parseInt(array[3], 16),
212
- nodeId: array[4],
213
- };
214
- assert(Timestamp.is(timestamp), "timestampString is malformed");
215
- return timestamp;
216
- };
217
-
218
194
  const getNextMillis =
219
195
  (deps: TimeDep & TimestampConfigDep) =>
220
196
  (
@@ -361,3 +337,7 @@ export const timestampBytesToTimestamp = (
361
337
  };
362
338
 
363
339
  export const orderTimestampBytes: Order<TimestampBytes> = orderUint8Array;
340
+
341
+ export const timestampToDateIso = (timestamp: Timestamp): DateIso =>
342
+ // `as DateIso` is safe because the timestamp is always valid
343
+ new Date(timestamp.millis).toISOString() as DateIso;
package/src/Object.ts CHANGED
@@ -25,14 +25,22 @@ export type ReadonlyRecord<K extends keyof any, V> = Readonly<Record<K, V>>;
25
25
  type StringKeyOf<T> = Extract<keyof T, string>;
26
26
 
27
27
  /**
28
- * Converts a record to entries, preserving branded string key types (e.g.,
29
- * `type Id = 'id' & string`) via `StringKeyOf<T>`, unlike `Object.entries`
30
- * which widens keys to `string`.
28
+ * Like `Object.entries` but preserves branded keys.
29
+ *
30
+ * ### Example
31
+ *
32
+ * ```ts
33
+ * type UserId = string & { readonly __brand: "UserId" };
34
+ * const users: Record<UserId, string> = {};
35
+ * const entries = objectToEntries(users); // [UserId, string][]
36
+ * ```
31
37
  */
32
38
  export const objectToEntries = <T extends Record<string, any>>(
33
39
  record: T,
34
- ): Array<[StringKeyOf<T>, T[StringKeyOf<T>]]> =>
35
- Object.entries(record) as Array<[StringKeyOf<T>, T[StringKeyOf<T>]]>;
40
+ ): ReadonlyArray<[StringKeyOf<T>, T[StringKeyOf<T>]]> =>
41
+ Object.entries(record) as Array<
42
+ [StringKeyOf<T>, T[StringKeyOf<T>]]
43
+ > as ReadonlyArray<[StringKeyOf<T>, T[StringKeyOf<T>]]>;
36
44
 
37
45
  /**
38
46
  * Maps a `ReadonlyRecord<K, V>` to a new `ReadonlyRecord<K, U>`, preserving
package/src/Type.ts CHANGED
@@ -3,10 +3,7 @@
3
3
  *
4
4
  * Evolu {@link Type} is like a type guard that returns typed errors (via
5
5
  * {@link Result}) instead of throwing. We either get a safely typed value or a
6
- * precise, composable error value telling us exactly why validation failed.
7
- *
8
- * Evolu Type supports [Standard Schema](https://standardschema.dev/) for
9
- * interoperability with 40+ validation-compatible tools and frameworks.
6
+ * composable typed error telling us exactly why validation failed.
10
7
  *
11
8
  * Why another validation library?
12
9
  *
@@ -15,16 +12,19 @@
15
12
  * messages.
16
13
  * - **Consistent constraints via {@link Brand}** – every constraint becomes part
17
14
  * of the type.
18
- * - **Selective validation** – parent validations are skipped when already proved
19
- * by typing.
20
- * - **Simple, top-down implementation** – readable source code from top to bottom
21
- * with no hidden magic; just plain functions and composition.
15
+ * - **Skippable validation** – parent validations can be skipped when already
16
+ * proved by types.
17
+ * - **Simple, top-down implementation** – readable source code from top to
18
+ * bottom.
19
+ * - **No user-land chaining DSL** – prepared for TC39 Hack pipes.
20
+ *
21
+ * A distinctive feature of Evolu Type compared to other validation libraries is
22
+ * that it returns typed errors rather than string messages. This allows
23
+ * TypeScript to enforce that all validation errors are handled by type
24
+ * checking, significantly improving the developer experience.
22
25
  *
23
- * TODO: Refactor Type factories and composition for Hack pipes compatibility.
24
- * Currently, brand factories like `minLength(min)(Type)` use constraint-first
25
- * currying. For optimal Hack pipe support, these should be refactored to
26
- * `minLength(Type, min)` to enable: `Type |> minLength(%, 1) |> maxLength(%,
27
- * 100)`
26
+ * Evolu Type supports [Standard Schema](https://standardschema.dev/) for
27
+ * interoperability with 40+ validation-compatible tools and frameworks.
28
28
  *
29
29
  * ### Base Types Quick Start
30
30
  *
@@ -175,6 +175,29 @@
175
175
  * reverse transforms would not buy much. We may revisit this if we can design a
176
176
  * minimal, 100% safe API that preserves simplicity.
177
177
  *
178
+ * ### Prepared for TC39 Hack Pipes
179
+ *
180
+ * Take a look how `SimplePassword` is defined:
181
+ *
182
+ * ```ts
183
+ * export const SimplePassword = brand(
184
+ * "SimplePassword",
185
+ * minLength(8)(maxLength(64)(TrimmedString)),
186
+ * );
187
+ * ```
188
+ *
189
+ * Nested functions are often OK (if not, make a helper) and read well, but with
190
+ * TC39 Hack pipes it would be clearer:
191
+ *
192
+ * ```ts
193
+ * // TrimmedString
194
+ * // |> minLength(8)(%)
195
+ * // |> maxLength(64)(%)
196
+ * // |> brand("SimplePassword", %)
197
+ * ```
198
+ *
199
+ * Note `minLength` and `maxLength` are curried because they are factories.
200
+ *
178
201
  * @module
179
202
  */
180
203
 
@@ -1501,7 +1524,7 @@ export const base64UrlToUint8Array: (str: Base64Url) => Uint8Array =
1501
1524
  * Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
1502
1525
  * `_`). See `UrlSafeString` for details.
1503
1526
  *
1504
- * The string must be between 1 and 42 characters.
1527
+ * The string must be between 1 and 64 characters.
1505
1528
  *
1506
1529
  * ### Example
1507
1530
  *
@@ -1517,7 +1540,7 @@ export const base64UrlToUint8Array: (str: Base64Url) => Uint8Array =
1517
1540
  * @category String
1518
1541
  */
1519
1542
  export const SimpleName = brand("SimpleName", UrlSafeString, (value) =>
1520
- value.length >= 1 && value.length <= 42
1543
+ value.length >= 1 && value.length <= 64
1521
1544
  ? ok(value)
1522
1545
  : err<SimpleNameError>({ type: "SimpleName", value }),
1523
1546
  );
@@ -1527,6 +1550,25 @@ export interface SimpleNameError extends TypeError<"SimpleName"> {}
1527
1550
  /**
1528
1551
  * Trimmed string between 8 and 64 characters, branded as `SimplePassword`.
1529
1552
  *
1553
+ * Take a look how `SimplePassword` is defined:
1554
+ *
1555
+ * ```ts
1556
+ * export const SimplePassword = brand(
1557
+ * "SimplePassword",
1558
+ * minLength(8)(maxLength(64)(TrimmedString)),
1559
+ * );
1560
+ * ```
1561
+ *
1562
+ * Nested functions are often OK (if not, make a helper), but with TC39 Hack
1563
+ * pipes it would be clearer:
1564
+ *
1565
+ * ```ts
1566
+ * // TrimmedString
1567
+ * // |> minLength(8)(%)
1568
+ * // |> maxLength(64)(%)
1569
+ * // |> brand("SimplePassword", %)
1570
+ * ```
1571
+ *
1530
1572
  * @category String
1531
1573
  */
1532
1574
  export const SimplePassword = brand(
@@ -3432,26 +3474,6 @@ export const formatInt64Error = createTypeErrorFormatter<Int64Error>(
3432
3474
  `The value ${error.value} is not a valid 64-bit signed integer (Int64).`,
3433
3475
  );
3434
3476
 
3435
- // // co s timhle? je to string, ze ktereho lze udelat bigint
3436
-
3437
- // export const BigIntFromString = transform(
3438
- // String,
3439
- // BigInt,
3440
- // (value) =>
3441
- // trySync(
3442
- // () => globalThis.BigInt(value),
3443
- // (): BigIntFromStringError => ({ type: "BigIntFromString", value }),
3444
- // ),
3445
- // (value) => value.toString(),
3446
- // );
3447
-
3448
- // export interface BigIntFromStringError extends TypeError<"BigIntFromString"> {}
3449
-
3450
- // export const formatBigIntFromStringError =
3451
- // createTypeErrorFormatter<BigIntFromStringError>(
3452
- // (error) => `The value ${error.value} could not be converted to a BigInt.`,
3453
- // );
3454
-
3455
3477
  /**
3456
3478
  * Stringified {@link Int64}.
3457
3479
  *