@evolu/common 7.2.3 → 7.3.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.
- package/dist/src/Function.d.ts +61 -0
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +19 -0
- package/dist/src/Object.d.ts +16 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +16 -0
- package/dist/src/Result.d.ts +36 -29
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Sqlite.d.ts +9 -7
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +8 -0
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +6 -4
- package/dist/src/Type.d.ts +30 -2
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +82 -0
- package/dist/src/local-first/Db.d.ts +1 -5
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +109 -108
- package/dist/src/local-first/Evolu.d.ts +3 -0
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +6 -7
- package/dist/src/local-first/LocalAuth.d.ts +2 -2
- package/dist/src/local-first/Owner.d.ts +3 -0
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +10 -10
- package/dist/src/local-first/PublicKysely.js +1 -1
- package/dist/src/local-first/Query.d.ts +54 -6
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Query.js +130 -11
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +17 -18
- package/dist/src/local-first/Schema.d.ts +15 -14
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +43 -51
- package/dist/src/local-first/Storage.d.ts +2 -2
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +2 -6
- package/dist/src/local-first/Sync.d.ts +9 -4
- package/dist/src/local-first/Sync.d.ts.map +1 -1
- package/dist/src/local-first/Sync.js +132 -59
- package/dist/src/local-first/index.d.ts +0 -1
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +0 -1
- package/package.json +1 -1
- package/src/Function.ts +74 -0
- package/src/Object.ts +20 -0
- package/src/Result.ts +38 -29
- package/src/Sqlite.ts +18 -7
- package/src/Task.ts +6 -4
- package/src/Type.ts +134 -1
- package/src/local-first/Db.ts +181 -209
- package/src/local-first/Evolu.ts +9 -6
- package/src/local-first/LocalAuth.ts +2 -2
- package/src/local-first/Owner.ts +4 -0
- package/src/local-first/Protocol.ts +10 -16
- package/src/local-first/PublicKysely.ts +1 -1
- package/src/local-first/Query.ts +216 -21
- package/src/local-first/Relay.ts +20 -24
- package/src/local-first/Schema.ts +83 -70
- package/src/local-first/Storage.ts +3 -8
- package/src/local-first/Sync.ts +190 -66
- package/src/local-first/index.ts +0 -1
- package/dist/src/local-first/Diff.d.ts +0 -43
- package/dist/src/local-first/Diff.d.ts.map +0 -1
- package/dist/src/local-first/Diff.js +0 -97
- package/src/local-first/Diff.ts +0 -144
package/src/local-first/Sync.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import { eqArrayNumber } from "../Eq.js";
|
|
17
17
|
import { createTransferableError, TransferableError } from "../Error.js";
|
|
18
18
|
import { constFalse, constTrue } from "../Function.js";
|
|
19
|
-
import { createRecord, objectToEntries } from "../Object.js";
|
|
19
|
+
import { createRecord, getProperty, objectToEntries } from "../Object.js";
|
|
20
20
|
import { RandomDep } from "../Random.js";
|
|
21
21
|
import { createResources } from "../Resources.js";
|
|
22
22
|
import { err, ok, Result } from "../Result.js";
|
|
@@ -34,17 +34,19 @@ import { TimeDep } from "../Time.js";
|
|
|
34
34
|
import {
|
|
35
35
|
Boolean,
|
|
36
36
|
DateIso,
|
|
37
|
+
Id,
|
|
37
38
|
IdBytes,
|
|
38
39
|
idBytesToId,
|
|
39
40
|
idToIdBytes,
|
|
40
41
|
PositiveInt,
|
|
41
42
|
} from "../Type.js";
|
|
42
43
|
import { CreateWebSocketDep, WebSocket } from "../WebSocket.js";
|
|
43
|
-
import type { AppOwnerDep, PostMessageDep } from "./Db.js";
|
|
44
44
|
import {
|
|
45
45
|
AppOwner,
|
|
46
|
+
AppOwnerDep,
|
|
46
47
|
Owner,
|
|
47
48
|
OwnerId,
|
|
49
|
+
OwnerIdBytes,
|
|
48
50
|
ownerIdBytesToOwnerId,
|
|
49
51
|
ownerIdToOwnerIdBytes,
|
|
50
52
|
OwnerTransport,
|
|
@@ -62,7 +64,7 @@ import {
|
|
|
62
64
|
ProtocolTimestampMismatchError,
|
|
63
65
|
SubscriptionFlags,
|
|
64
66
|
} from "./Protocol.js";
|
|
65
|
-
import { MutationChange } from "./Schema.js";
|
|
67
|
+
import { DbSchemaDep, MutationChange, systemColumns } from "./Schema.js";
|
|
66
68
|
import {
|
|
67
69
|
BaseSqliteStorage,
|
|
68
70
|
CrdtMessage,
|
|
@@ -80,6 +82,7 @@ import {
|
|
|
80
82
|
receiveTimestamp,
|
|
81
83
|
sendTimestamp,
|
|
82
84
|
Timestamp,
|
|
85
|
+
TimestampBytes,
|
|
83
86
|
timestampBytesToTimestamp,
|
|
84
87
|
TimestampConfigDep,
|
|
85
88
|
TimestampCounterOverflowError,
|
|
@@ -157,7 +160,8 @@ export const createSync =
|
|
|
157
160
|
deps: ClockDep &
|
|
158
161
|
ConsoleDep &
|
|
159
162
|
CreateWebSocketDep &
|
|
160
|
-
|
|
163
|
+
DbSchemaDep &
|
|
164
|
+
// PostMessageDep &
|
|
161
165
|
RandomBytesDep &
|
|
162
166
|
RandomDep &
|
|
163
167
|
SqliteDep &
|
|
@@ -324,7 +328,6 @@ export const createSync =
|
|
|
324
328
|
resources.addConsumer(owner, transports);
|
|
325
329
|
} else {
|
|
326
330
|
const result = resources.removeConsumer(owner, transports);
|
|
327
|
-
|
|
328
331
|
if (!result.ok) {
|
|
329
332
|
deps.console.warn("[sync]", "Failed to remove consumer", {
|
|
330
333
|
transports,
|
|
@@ -445,6 +448,7 @@ export interface ClientStorageDep {
|
|
|
445
448
|
const createClientStorage =
|
|
446
449
|
(
|
|
447
450
|
deps: ClockDep &
|
|
451
|
+
DbSchemaDep &
|
|
448
452
|
GetSyncOwnerDep &
|
|
449
453
|
RandomDep &
|
|
450
454
|
SqliteDep &
|
|
@@ -484,7 +488,7 @@ const createClientStorage =
|
|
|
484
488
|
const ownerId = ownerIdBytesToOwnerId(ownerIdBytes);
|
|
485
489
|
|
|
486
490
|
// Everything is sync now, but we will need async crypto in the future.
|
|
487
|
-
const
|
|
491
|
+
const result = await mutex.withLock<
|
|
488
492
|
boolean,
|
|
489
493
|
| AbortError
|
|
490
494
|
| ProtocolInvalidDataError
|
|
@@ -536,11 +540,11 @@ const createClientStorage =
|
|
|
536
540
|
}
|
|
537
541
|
|
|
538
542
|
if (isNonEmptyReadonlyArray(messages)) {
|
|
539
|
-
const
|
|
543
|
+
const result = applyMessages({ ...deps, storage })(
|
|
540
544
|
owner.id,
|
|
541
545
|
messages,
|
|
542
546
|
);
|
|
543
|
-
if (!
|
|
547
|
+
if (!result.ok) return result;
|
|
544
548
|
}
|
|
545
549
|
|
|
546
550
|
return deps.clock.save(clockTimestamp);
|
|
@@ -551,9 +555,9 @@ const createClientStorage =
|
|
|
551
555
|
return ok(true);
|
|
552
556
|
})();
|
|
553
557
|
|
|
554
|
-
if (!
|
|
555
|
-
if (
|
|
556
|
-
config.onError(
|
|
558
|
+
if (!result.ok) {
|
|
559
|
+
if (result.error.type !== "AbortError") {
|
|
560
|
+
config.onError(result.error);
|
|
557
561
|
}
|
|
558
562
|
return err<StorageWriteError>({ type: "StorageWriteError", ownerId });
|
|
559
563
|
}
|
|
@@ -569,22 +573,30 @@ const createClientStorage =
|
|
|
569
573
|
if (!owner) return null;
|
|
570
574
|
|
|
571
575
|
const result = deps.sqlite.exec<{
|
|
572
|
-
table: string;
|
|
573
|
-
id: IdBytes;
|
|
574
|
-
column: string;
|
|
575
|
-
value: SqliteValue;
|
|
576
|
+
readonly table: string;
|
|
577
|
+
readonly id: IdBytes;
|
|
578
|
+
readonly column: string;
|
|
579
|
+
readonly value: SqliteValue;
|
|
576
580
|
}>(sql`
|
|
577
581
|
select "table", "id", "column", "value"
|
|
578
582
|
from evolu_history
|
|
583
|
+
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp}
|
|
584
|
+
union all
|
|
585
|
+
select "table", "id", "column", "value"
|
|
586
|
+
from evolu_message_quarantine
|
|
579
587
|
where "ownerId" = ${ownerId} and "timestamp" = ${timestamp};
|
|
580
588
|
`);
|
|
589
|
+
|
|
581
590
|
if (!result.ok) {
|
|
582
591
|
config.onError(result.error);
|
|
583
592
|
return null;
|
|
584
593
|
}
|
|
585
594
|
|
|
586
595
|
const { rows } = result.value;
|
|
587
|
-
assertNonEmptyReadonlyArray(
|
|
596
|
+
assertNonEmptyReadonlyArray(
|
|
597
|
+
rows,
|
|
598
|
+
"Every timestamp must have rows in evolu_history or quarantine",
|
|
599
|
+
);
|
|
588
600
|
|
|
589
601
|
const { table, id } = firstInArray(rows);
|
|
590
602
|
const values = createRecord<string, SqliteValue>();
|
|
@@ -687,7 +699,7 @@ export const applyLocalOnlyChange =
|
|
|
687
699
|
};
|
|
688
700
|
|
|
689
701
|
const applyMessages =
|
|
690
|
-
(deps: ClientStorageDep & ClockDep & RandomDep & SqliteDep) =>
|
|
702
|
+
(deps: ClientStorageDep & ClockDep & DbSchemaDep & RandomDep & SqliteDep) =>
|
|
691
703
|
(
|
|
692
704
|
ownerId: OwnerId,
|
|
693
705
|
messages: NonEmptyReadonlyArray<CrdtMessage>,
|
|
@@ -703,51 +715,40 @@ const applyMessages =
|
|
|
703
715
|
let { firstTimestamp, lastTimestamp } = usage.value;
|
|
704
716
|
|
|
705
717
|
for (const { timestamp, change } of messages) {
|
|
706
|
-
const timestampBytes = timestampToTimestampBytes(timestamp);
|
|
707
|
-
const idBytes = idToIdBytes(change.id);
|
|
708
718
|
const columns = dbChangeToColumns(change, timestampToDateIso(timestamp));
|
|
719
|
+
const idBytes = idToIdBytes(change.id);
|
|
720
|
+
const timestampBytes = timestampToTimestampBytes(timestamp);
|
|
709
721
|
|
|
710
722
|
for (const [column, value] of columns) {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
${ownerIdBytes},
|
|
741
|
-
${change.table},
|
|
742
|
-
${idBytes},
|
|
743
|
-
${column},
|
|
744
|
-
${value},
|
|
745
|
-
${timestampBytes}
|
|
746
|
-
)
|
|
747
|
-
on conflict do nothing;
|
|
748
|
-
`);
|
|
749
|
-
|
|
750
|
-
if (!insertHistory.ok) return insertHistory;
|
|
723
|
+
if (validateColumnValue(deps)(change.table, column, value)) {
|
|
724
|
+
const result = applyColumnChange(deps)(
|
|
725
|
+
ownerIdBytes,
|
|
726
|
+
ownerId,
|
|
727
|
+
change.table,
|
|
728
|
+
idBytes,
|
|
729
|
+
change.id,
|
|
730
|
+
column,
|
|
731
|
+
value,
|
|
732
|
+
timestampBytes,
|
|
733
|
+
);
|
|
734
|
+
if (!result.ok) return result;
|
|
735
|
+
} else {
|
|
736
|
+
const result = deps.sqlite.exec(sql.prepared`
|
|
737
|
+
insert into evolu_message_quarantine
|
|
738
|
+
("ownerId", "timestamp", "table", "id", "column", "value")
|
|
739
|
+
values
|
|
740
|
+
(
|
|
741
|
+
${ownerIdBytes},
|
|
742
|
+
${timestampBytes},
|
|
743
|
+
${change.table},
|
|
744
|
+
${idBytes},
|
|
745
|
+
${column},
|
|
746
|
+
${value}
|
|
747
|
+
)
|
|
748
|
+
on conflict do nothing;
|
|
749
|
+
`);
|
|
750
|
+
if (!result.ok) return result;
|
|
751
|
+
}
|
|
751
752
|
}
|
|
752
753
|
|
|
753
754
|
let strategy;
|
|
@@ -757,26 +758,149 @@ const applyMessages =
|
|
|
757
758
|
lastTimestamp,
|
|
758
759
|
);
|
|
759
760
|
|
|
760
|
-
const
|
|
761
|
+
const result = deps.storage.insertTimestamp(
|
|
761
762
|
ownerIdBytes,
|
|
762
763
|
timestampBytes,
|
|
763
764
|
strategy,
|
|
764
765
|
);
|
|
765
|
-
if (!
|
|
766
|
+
if (!result.ok) return result;
|
|
766
767
|
}
|
|
767
768
|
|
|
768
769
|
/**
|
|
769
|
-
* TODO: Implement proper storedBytes tracking for client using
|
|
770
|
-
* message sizes
|
|
771
|
-
* client...).
|
|
770
|
+
* TODO: Implement proper storedBytes tracking for client using received and
|
|
771
|
+
* sent encrypted message sizes.
|
|
772
772
|
*/
|
|
773
|
-
|
|
773
|
+
return updateOwnerUsage(deps)(
|
|
774
774
|
ownerIdBytes,
|
|
775
775
|
1 as PositiveInt, // Placeholder until proper tracking implemented
|
|
776
776
|
firstTimestamp,
|
|
777
777
|
lastTimestamp,
|
|
778
778
|
);
|
|
779
|
-
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* System columns that can appear in sync messages. Excludes `ownerId` because
|
|
783
|
+
* it's handled separately (stored per-row, not per-column in messages).
|
|
784
|
+
*/
|
|
785
|
+
const systemColumnsWithoutOwnerId = systemColumns.difference(
|
|
786
|
+
new Set(["ownerId"]),
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
const validateColumnValue =
|
|
790
|
+
(deps: DbSchemaDep) =>
|
|
791
|
+
(table: string, column: string, _value: SqliteValue): boolean => {
|
|
792
|
+
const schemaColumns = getProperty(deps.dbSchema.tables, table);
|
|
793
|
+
return (
|
|
794
|
+
schemaColumns != null &&
|
|
795
|
+
(systemColumnsWithoutOwnerId.has(column) || schemaColumns.has(column))
|
|
796
|
+
);
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
const applyColumnChange =
|
|
800
|
+
(deps: SqliteDep) =>
|
|
801
|
+
(
|
|
802
|
+
ownerIdBytes: OwnerIdBytes,
|
|
803
|
+
ownerId: OwnerId,
|
|
804
|
+
table: string,
|
|
805
|
+
idBytes: IdBytes,
|
|
806
|
+
id: Id,
|
|
807
|
+
column: string,
|
|
808
|
+
value: SqliteValue,
|
|
809
|
+
timestampBytes: TimestampBytes,
|
|
810
|
+
): Result<void, SqliteError> => {
|
|
811
|
+
const result = deps.sqlite.exec(sql.prepared`
|
|
812
|
+
with
|
|
813
|
+
existingTimestamp as (
|
|
814
|
+
select 1
|
|
815
|
+
from evolu_history
|
|
816
|
+
where
|
|
817
|
+
"ownerId" = ${ownerIdBytes}
|
|
818
|
+
and "table" = ${table}
|
|
819
|
+
and "id" = ${idBytes}
|
|
820
|
+
and "column" = ${column}
|
|
821
|
+
and "timestamp" >= ${timestampBytes}
|
|
822
|
+
limit 1
|
|
823
|
+
)
|
|
824
|
+
insert into ${sql.identifier(table)}
|
|
825
|
+
("ownerId", "id", ${sql.identifier(column)})
|
|
826
|
+
select ${ownerId}, ${id}, ${value}
|
|
827
|
+
where not exists (select 1 from existingTimestamp)
|
|
828
|
+
on conflict ("ownerId", "id") do update
|
|
829
|
+
set ${sql.identifier(column)} = ${value}
|
|
830
|
+
where not exists (select 1 from existingTimestamp);
|
|
831
|
+
`);
|
|
832
|
+
if (!result.ok) return result;
|
|
833
|
+
|
|
834
|
+
{
|
|
835
|
+
const result = deps.sqlite.exec(sql.prepared`
|
|
836
|
+
insert into evolu_history
|
|
837
|
+
("ownerId", "table", "id", "column", "value", "timestamp")
|
|
838
|
+
values
|
|
839
|
+
(
|
|
840
|
+
${ownerIdBytes},
|
|
841
|
+
${table},
|
|
842
|
+
${idBytes},
|
|
843
|
+
${column},
|
|
844
|
+
${value},
|
|
845
|
+
${timestampBytes}
|
|
846
|
+
)
|
|
847
|
+
on conflict do nothing;
|
|
848
|
+
`);
|
|
849
|
+
if (!result.ok) return result;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
return ok();
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Attempts to apply quarantined messages that may now be valid after a schema
|
|
857
|
+
* update. Messages are quarantined when they reference tables or columns that
|
|
858
|
+
* don't exist in the current schema (e.g., from a newer app version).
|
|
859
|
+
*/
|
|
860
|
+
export const tryApplyQuarantinedMessages =
|
|
861
|
+
(deps: DbSchemaDep & SqliteDep) => (): Result<void, SqliteError> => {
|
|
862
|
+
const rows = deps.sqlite.exec<{
|
|
863
|
+
readonly ownerId: OwnerIdBytes;
|
|
864
|
+
readonly timestamp: TimestampBytes;
|
|
865
|
+
readonly table: string;
|
|
866
|
+
readonly id: IdBytes;
|
|
867
|
+
readonly column: string;
|
|
868
|
+
readonly value: SqliteValue;
|
|
869
|
+
}>(sql`
|
|
870
|
+
select "ownerId", "timestamp", "table", "id", "column", "value"
|
|
871
|
+
from evolu_message_quarantine;
|
|
872
|
+
`);
|
|
873
|
+
if (!rows.ok) return rows;
|
|
874
|
+
|
|
875
|
+
for (const row of rows.value.rows) {
|
|
876
|
+
if (!validateColumnValue(deps)(row.table, row.column, row.value))
|
|
877
|
+
continue;
|
|
878
|
+
|
|
879
|
+
const result = applyColumnChange(deps)(
|
|
880
|
+
row.ownerId,
|
|
881
|
+
ownerIdBytesToOwnerId(row.ownerId),
|
|
882
|
+
row.table,
|
|
883
|
+
row.id,
|
|
884
|
+
idBytesToId(row.id),
|
|
885
|
+
row.column,
|
|
886
|
+
row.value,
|
|
887
|
+
row.timestamp,
|
|
888
|
+
);
|
|
889
|
+
if (!result.ok) return result;
|
|
890
|
+
|
|
891
|
+
{
|
|
892
|
+
const result = deps.sqlite.exec(sql`
|
|
893
|
+
delete from evolu_message_quarantine
|
|
894
|
+
where
|
|
895
|
+
"ownerId" = ${row.ownerId}
|
|
896
|
+
and "timestamp" = ${row.timestamp}
|
|
897
|
+
and "table" = ${row.table}
|
|
898
|
+
and "id" = ${row.id}
|
|
899
|
+
and "column" = ${row.column};
|
|
900
|
+
`);
|
|
901
|
+
if (!result.ok) return result;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
780
904
|
|
|
781
905
|
return ok();
|
|
782
906
|
};
|
package/src/local-first/index.ts
CHANGED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { Query, Row } from "./Query.js";
|
|
2
|
-
export interface QueryPatches {
|
|
3
|
-
readonly query: Query;
|
|
4
|
-
readonly patches: ReadonlyArray<Patch>;
|
|
5
|
-
}
|
|
6
|
-
export type Patch = ReplaceAllPatch | ReplaceAtPatch;
|
|
7
|
-
export interface ReplaceAllPatch {
|
|
8
|
-
readonly op: "replaceAll";
|
|
9
|
-
readonly value: ReadonlyArray<Row>;
|
|
10
|
-
}
|
|
11
|
-
export interface ReplaceAtPatch {
|
|
12
|
-
readonly op: "replaceAt";
|
|
13
|
-
readonly index: number;
|
|
14
|
-
readonly value: Row;
|
|
15
|
-
}
|
|
16
|
-
export declare const applyPatches: (patches: ReadonlyArray<Patch>, current: ReadonlyArray<Row>) => ReadonlyArray<Row>;
|
|
17
|
-
/**
|
|
18
|
-
* We detect only changes in the whole result and in-place edits. In the future,
|
|
19
|
-
* we will add more heuristics. We will probably not implement the Myers diff
|
|
20
|
-
* algorithm because it's faster to rerender all than to compute many detailed
|
|
21
|
-
* patches. We will only implement logic a developer would implement manually,
|
|
22
|
-
* if necessary.
|
|
23
|
-
*/
|
|
24
|
-
export declare const makePatches: (previousRows: ReadonlyArray<Row> | undefined, nextRows: ReadonlyArray<Row>) => ReadonlyArray<Patch>;
|
|
25
|
-
/**
|
|
26
|
-
* A unique identifier prepended to JSON-encoded strings. This allows safe
|
|
27
|
-
* detection and parsing of only those columns that require JSON.parse.
|
|
28
|
-
*
|
|
29
|
-
* The identifier is a cryptographically random Evolu Id, ensuring uniqueness
|
|
30
|
-
* and preventing malicious actors from inserting fake data that could be
|
|
31
|
-
* misinterpreted as JSON by the application.
|
|
32
|
-
*
|
|
33
|
-
* Note: The same queries created by different browser tabs will have different
|
|
34
|
-
* identifiers and thus be considered different and cached separately. This is
|
|
35
|
-
* usually not a big deal, but if needed, the DB cache can be optimized by
|
|
36
|
-
* passing the kyselyJsonIdentifier into the DB worker during initialization,
|
|
37
|
-
* allowing queries to be grouped and recognized across tabs or sessions.
|
|
38
|
-
*
|
|
39
|
-
* See: https://github.com/kysely-org/kysely/issues/1372#issuecomment-2702773948
|
|
40
|
-
*/
|
|
41
|
-
export declare const kyselyJsonIdentifier: string & import("../Brand.js").Brand<"Id">;
|
|
42
|
-
export declare const parseSqliteJsonArray: <T>(arr: ReadonlyArray<T>) => ReadonlyArray<T>;
|
|
43
|
-
//# sourceMappingURL=Diff.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Diff.d.ts","sourceRoot":"","sources":["../../../src/local-first/Diff.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC;CACxC;AAED,MAAM,MAAM,KAAK,GAAG,eAAe,GAAG,cAAc,CAAC;AAErD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,YAAY,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC;CACrB;AAED,eAAO,MAAM,YAAY,GACvB,SAAS,aAAa,CAAC,KAAK,CAAC,EAC7B,SAAS,aAAa,CAAC,GAAG,CAAC,KAC1B,aAAa,CAAC,GAAG,CAUP,CAAC;AAEd;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GACtB,cAAc,aAAa,CAAC,GAAG,CAAC,GAAG,SAAS,EAC5C,UAAU,aAAa,CAAC,GAAG,CAAC,KAC3B,aAAa,CAAC,KAAK,CA6BrB,CAAC;AAUF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,oBAAoB,4CAE/B,CAAC;AAEH,eAAO,MAAM,oBAAoB,GAAI,CAAC,EACpC,KAAK,aAAa,CAAC,CAAC,CAAC,KACpB,aAAa,CAAC,CAAC,CAMjB,CAAC"}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { createRandomBytes } from "../Crypto.js";
|
|
2
|
-
import { createRecord, isPlainObject } from "../Object.js";
|
|
3
|
-
import { orderUint8Array } from "../Order.js";
|
|
4
|
-
import { createId, String } from "../Type.js";
|
|
5
|
-
export const applyPatches = (patches, current) => patches.reduce((next, patch) => {
|
|
6
|
-
switch (patch.op) {
|
|
7
|
-
case "replaceAll":
|
|
8
|
-
return parseSqliteJsonArray(patch.value);
|
|
9
|
-
case "replaceAt": {
|
|
10
|
-
const parsedRow = parseSqliteJsonArray([patch.value])[0];
|
|
11
|
-
return next.toSpliced(patch.index, 1, parsedRow);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
}, current);
|
|
15
|
-
/**
|
|
16
|
-
* We detect only changes in the whole result and in-place edits. In the future,
|
|
17
|
-
* we will add more heuristics. We will probably not implement the Myers diff
|
|
18
|
-
* algorithm because it's faster to rerender all than to compute many detailed
|
|
19
|
-
* patches. We will only implement logic a developer would implement manually,
|
|
20
|
-
* if necessary.
|
|
21
|
-
*/
|
|
22
|
-
export const makePatches = (previousRows, nextRows) => {
|
|
23
|
-
if (previousRows === undefined)
|
|
24
|
-
return [{ op: "replaceAll", value: nextRows }];
|
|
25
|
-
// TODO: Detect prepend and append, it's cheap.
|
|
26
|
-
if (previousRows.length !== nextRows.length) {
|
|
27
|
-
return [{ op: "replaceAll", value: nextRows }];
|
|
28
|
-
}
|
|
29
|
-
const length = previousRows.length;
|
|
30
|
-
const replaceAtPatches = [];
|
|
31
|
-
for (let i = 0; i < length; i++) {
|
|
32
|
-
const previousRow = previousRows[i];
|
|
33
|
-
const nextRow = nextRows[i];
|
|
34
|
-
// We expect the same shape for both rows.
|
|
35
|
-
for (const key in previousRow)
|
|
36
|
-
if (!areEqual(previousRow[key], nextRow[key])) {
|
|
37
|
-
replaceAtPatches.push({ op: "replaceAt", value: nextRow, index: i });
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
if (length > 0 && replaceAtPatches.length === length) {
|
|
42
|
-
return [{ op: "replaceAll", value: nextRows }];
|
|
43
|
-
}
|
|
44
|
-
return replaceAtPatches;
|
|
45
|
-
};
|
|
46
|
-
// TODO: Replace with eqSqliteValue.
|
|
47
|
-
const areEqual = (a, b) => {
|
|
48
|
-
if (a instanceof Uint8Array && b instanceof Uint8Array) {
|
|
49
|
-
return orderUint8Array(a, b) === 0;
|
|
50
|
-
}
|
|
51
|
-
return a === b;
|
|
52
|
-
};
|
|
53
|
-
/**
|
|
54
|
-
* A unique identifier prepended to JSON-encoded strings. This allows safe
|
|
55
|
-
* detection and parsing of only those columns that require JSON.parse.
|
|
56
|
-
*
|
|
57
|
-
* The identifier is a cryptographically random Evolu Id, ensuring uniqueness
|
|
58
|
-
* and preventing malicious actors from inserting fake data that could be
|
|
59
|
-
* misinterpreted as JSON by the application.
|
|
60
|
-
*
|
|
61
|
-
* Note: The same queries created by different browser tabs will have different
|
|
62
|
-
* identifiers and thus be considered different and cached separately. This is
|
|
63
|
-
* usually not a big deal, but if needed, the DB cache can be optimized by
|
|
64
|
-
* passing the kyselyJsonIdentifier into the DB worker during initialization,
|
|
65
|
-
* allowing queries to be grouped and recognized across tabs or sessions.
|
|
66
|
-
*
|
|
67
|
-
* See: https://github.com/kysely-org/kysely/issues/1372#issuecomment-2702773948
|
|
68
|
-
*/
|
|
69
|
-
export const kyselyJsonIdentifier = createId({
|
|
70
|
-
randomBytes: createRandomBytes(),
|
|
71
|
-
});
|
|
72
|
-
export const parseSqliteJsonArray = (arr) => {
|
|
73
|
-
const result = new Array(arr.length);
|
|
74
|
-
for (let i = 0; i < arr.length; ++i) {
|
|
75
|
-
result[i] = parse(arr[i]);
|
|
76
|
-
}
|
|
77
|
-
return result;
|
|
78
|
-
};
|
|
79
|
-
const parse = (obj) => {
|
|
80
|
-
if (String.is(obj) && obj.startsWith(kyselyJsonIdentifier)) {
|
|
81
|
-
return JSON.parse(obj.slice(kyselyJsonIdentifier.length));
|
|
82
|
-
}
|
|
83
|
-
if (Array.isArray(obj)) {
|
|
84
|
-
return parseSqliteJsonArray(obj);
|
|
85
|
-
}
|
|
86
|
-
if (isPlainObject(obj)) {
|
|
87
|
-
return parseObject(obj);
|
|
88
|
-
}
|
|
89
|
-
return obj;
|
|
90
|
-
};
|
|
91
|
-
const parseObject = (obj) => {
|
|
92
|
-
const result = createRecord();
|
|
93
|
-
for (const key in obj) {
|
|
94
|
-
result[key] = parse(obj[key]);
|
|
95
|
-
}
|
|
96
|
-
return result;
|
|
97
|
-
};
|
package/src/local-first/Diff.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { createRandomBytes } from "../Crypto.js";
|
|
2
|
-
import { createRecord, isPlainObject, ReadonlyRecord } from "../Object.js";
|
|
3
|
-
import { orderUint8Array } from "../Order.js";
|
|
4
|
-
import { SqliteValue } from "../Sqlite.js";
|
|
5
|
-
import { createId, String } from "../Type.js";
|
|
6
|
-
import { Query, Row } from "./Query.js";
|
|
7
|
-
|
|
8
|
-
export interface QueryPatches {
|
|
9
|
-
readonly query: Query;
|
|
10
|
-
readonly patches: ReadonlyArray<Patch>;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export type Patch = ReplaceAllPatch | ReplaceAtPatch;
|
|
14
|
-
|
|
15
|
-
export interface ReplaceAllPatch {
|
|
16
|
-
readonly op: "replaceAll";
|
|
17
|
-
readonly value: ReadonlyArray<Row>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface ReplaceAtPatch {
|
|
21
|
-
readonly op: "replaceAt";
|
|
22
|
-
readonly index: number;
|
|
23
|
-
readonly value: Row;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const applyPatches = (
|
|
27
|
-
patches: ReadonlyArray<Patch>,
|
|
28
|
-
current: ReadonlyArray<Row>,
|
|
29
|
-
): ReadonlyArray<Row> =>
|
|
30
|
-
patches.reduce((next, patch) => {
|
|
31
|
-
switch (patch.op) {
|
|
32
|
-
case "replaceAll":
|
|
33
|
-
return parseSqliteJsonArray(patch.value);
|
|
34
|
-
case "replaceAt": {
|
|
35
|
-
const parsedRow = parseSqliteJsonArray([patch.value])[0];
|
|
36
|
-
return next.toSpliced(patch.index, 1, parsedRow);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}, current);
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* We detect only changes in the whole result and in-place edits. In the future,
|
|
43
|
-
* we will add more heuristics. We will probably not implement the Myers diff
|
|
44
|
-
* algorithm because it's faster to rerender all than to compute many detailed
|
|
45
|
-
* patches. We will only implement logic a developer would implement manually,
|
|
46
|
-
* if necessary.
|
|
47
|
-
*/
|
|
48
|
-
export const makePatches = (
|
|
49
|
-
previousRows: ReadonlyArray<Row> | undefined,
|
|
50
|
-
nextRows: ReadonlyArray<Row>,
|
|
51
|
-
): ReadonlyArray<Patch> => {
|
|
52
|
-
if (previousRows === undefined)
|
|
53
|
-
return [{ op: "replaceAll", value: nextRows }];
|
|
54
|
-
// TODO: Detect prepend and append, it's cheap.
|
|
55
|
-
if (previousRows.length !== nextRows.length) {
|
|
56
|
-
return [{ op: "replaceAll", value: nextRows }];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const length = previousRows.length;
|
|
60
|
-
const replaceAtPatches: Array<ReplaceAtPatch> = [];
|
|
61
|
-
|
|
62
|
-
for (let i = 0; i < length; i++) {
|
|
63
|
-
const previousRow = previousRows[i];
|
|
64
|
-
const nextRow = nextRows[i];
|
|
65
|
-
|
|
66
|
-
// We expect the same shape for both rows.
|
|
67
|
-
for (const key in previousRow)
|
|
68
|
-
if (
|
|
69
|
-
!areEqual(previousRow[key] as SqliteValue, nextRow[key] as SqliteValue)
|
|
70
|
-
) {
|
|
71
|
-
replaceAtPatches.push({ op: "replaceAt", value: nextRow, index: i });
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (length > 0 && replaceAtPatches.length === length) {
|
|
77
|
-
return [{ op: "replaceAll", value: nextRows }];
|
|
78
|
-
}
|
|
79
|
-
return replaceAtPatches;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// TODO: Replace with eqSqliteValue.
|
|
83
|
-
const areEqual = (a: SqliteValue, b: SqliteValue): boolean => {
|
|
84
|
-
if (a instanceof Uint8Array && b instanceof Uint8Array) {
|
|
85
|
-
return orderUint8Array(a, b) === 0;
|
|
86
|
-
}
|
|
87
|
-
return a === b;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* A unique identifier prepended to JSON-encoded strings. This allows safe
|
|
92
|
-
* detection and parsing of only those columns that require JSON.parse.
|
|
93
|
-
*
|
|
94
|
-
* The identifier is a cryptographically random Evolu Id, ensuring uniqueness
|
|
95
|
-
* and preventing malicious actors from inserting fake data that could be
|
|
96
|
-
* misinterpreted as JSON by the application.
|
|
97
|
-
*
|
|
98
|
-
* Note: The same queries created by different browser tabs will have different
|
|
99
|
-
* identifiers and thus be considered different and cached separately. This is
|
|
100
|
-
* usually not a big deal, but if needed, the DB cache can be optimized by
|
|
101
|
-
* passing the kyselyJsonIdentifier into the DB worker during initialization,
|
|
102
|
-
* allowing queries to be grouped and recognized across tabs or sessions.
|
|
103
|
-
*
|
|
104
|
-
* See: https://github.com/kysely-org/kysely/issues/1372#issuecomment-2702773948
|
|
105
|
-
*/
|
|
106
|
-
export const kyselyJsonIdentifier = createId({
|
|
107
|
-
randomBytes: createRandomBytes(),
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
export const parseSqliteJsonArray = <T>(
|
|
111
|
-
arr: ReadonlyArray<T>,
|
|
112
|
-
): ReadonlyArray<T> => {
|
|
113
|
-
const result = new Array<T>(arr.length);
|
|
114
|
-
for (let i = 0; i < arr.length; ++i) {
|
|
115
|
-
result[i] = parse(arr[i]) as T;
|
|
116
|
-
}
|
|
117
|
-
return result;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const parse = (obj: unknown): unknown => {
|
|
121
|
-
if (String.is(obj) && obj.startsWith(kyselyJsonIdentifier)) {
|
|
122
|
-
return JSON.parse(obj.slice(kyselyJsonIdentifier.length));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (Array.isArray(obj)) {
|
|
126
|
-
return parseSqliteJsonArray(obj);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (isPlainObject(obj)) {
|
|
130
|
-
return parseObject(obj);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return obj;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const parseObject = (
|
|
137
|
-
obj: ReadonlyRecord<string, unknown>,
|
|
138
|
-
): ReadonlyRecord<string, unknown> => {
|
|
139
|
-
const result = createRecord();
|
|
140
|
-
for (const key in obj) {
|
|
141
|
-
result[key] = parse(obj[key]);
|
|
142
|
-
}
|
|
143
|
-
return result as ReadonlyRecord<string, unknown>;
|
|
144
|
-
};
|