@evolu/common 7.2.1 → 7.2.3
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/Object.d.ts +16 -1
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +16 -1
- package/dist/src/Order.d.ts +2 -1
- package/dist/src/Order.d.ts.map +1 -1
- package/dist/src/Order.js +1 -0
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +4 -1
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +5 -11
- package/dist/src/local-first/Diff.js +2 -2
- package/dist/src/local-first/Evolu.d.ts +14 -11
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +8 -5
- package/dist/src/local-first/Owner.d.ts +39 -42
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +22 -29
- package/dist/src/local-first/Protocol.d.ts +6 -6
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +2 -2
- package/dist/src/local-first/Public.d.ts +1 -0
- package/dist/src/local-first/Public.d.ts.map +1 -1
- package/dist/src/local-first/Storage.d.ts +3 -3
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Sync.d.ts +10 -17
- package/dist/src/local-first/Sync.d.ts.map +1 -1
- package/dist/src/local-first/Sync.js +108 -130
- package/dist/src/local-first/Timestamp.d.ts +13 -0
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +14 -1
- package/package.json +1 -1
- package/src/Object.ts +20 -1
- package/src/Order.ts +2 -1
- package/src/WebSocket.ts +6 -1
- package/src/local-first/Db.ts +5 -12
- package/src/local-first/Diff.ts +2 -2
- package/src/local-first/Evolu.ts +27 -15
- package/src/local-first/Owner.ts +47 -57
- package/src/local-first/Protocol.ts +8 -8
- package/src/local-first/Public.ts +1 -0
- package/src/local-first/Storage.ts +3 -3
- package/src/local-first/Sync.ts +123 -175
- package/src/local-first/Timestamp.ts +14 -1
package/src/local-first/Sync.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
+
appendToArray,
|
|
2
3
|
firstInArray,
|
|
3
4
|
isNonEmptyReadonlyArray,
|
|
4
5
|
NonEmptyArray,
|
|
5
6
|
NonEmptyReadonlyArray,
|
|
6
7
|
} from "../Array.js";
|
|
7
|
-
import { assert } from "../Assert.js";
|
|
8
|
+
import { assert, assertNonEmptyReadonlyArray } from "../Assert.js";
|
|
8
9
|
import { Brand } from "../Brand.js";
|
|
9
10
|
import { ConsoleDep } from "../Console.js";
|
|
10
11
|
import {
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
import { eqArrayNumber } from "../Eq.js";
|
|
16
17
|
import { createTransferableError, TransferableError } from "../Error.js";
|
|
17
18
|
import { constFalse, constTrue } from "../Function.js";
|
|
18
|
-
import { objectToEntries } from "../Object.js";
|
|
19
|
+
import { createRecord, objectToEntries } from "../Object.js";
|
|
19
20
|
import { RandomDep } from "../Random.js";
|
|
20
21
|
import { createResources } from "../Resources.js";
|
|
21
22
|
import { err, ok, Result } from "../Result.js";
|
|
@@ -26,12 +27,12 @@ import {
|
|
|
26
27
|
sqliteBooleanToBoolean,
|
|
27
28
|
SqliteDep,
|
|
28
29
|
SqliteError,
|
|
29
|
-
sqliteFalse,
|
|
30
30
|
SqliteValue,
|
|
31
31
|
} from "../Sqlite.js";
|
|
32
32
|
import { AbortError, createMutex } from "../Task.js";
|
|
33
33
|
import { TimeDep } from "../Time.js";
|
|
34
34
|
import {
|
|
35
|
+
Boolean,
|
|
35
36
|
DateIso,
|
|
36
37
|
IdBytes,
|
|
37
38
|
idBytesToId,
|
|
@@ -42,16 +43,12 @@ import { CreateWebSocketDep, WebSocket } from "../WebSocket.js";
|
|
|
42
43
|
import type { AppOwnerDep, PostMessageDep } from "./Db.js";
|
|
43
44
|
import {
|
|
44
45
|
AppOwner,
|
|
45
|
-
|
|
46
|
+
Owner,
|
|
46
47
|
OwnerId,
|
|
47
|
-
OwnerIdBytes,
|
|
48
48
|
ownerIdBytesToOwnerId,
|
|
49
49
|
ownerIdToOwnerIdBytes,
|
|
50
50
|
OwnerTransport,
|
|
51
|
-
|
|
52
|
-
ShardOwner,
|
|
53
|
-
SharedOwner,
|
|
54
|
-
SharedReadonlyOwner,
|
|
51
|
+
ReadonlyOwner,
|
|
55
52
|
} from "./Owner.js";
|
|
56
53
|
import {
|
|
57
54
|
applyProtocolMessageAsClient,
|
|
@@ -74,7 +71,6 @@ import {
|
|
|
74
71
|
getOwnerUsage,
|
|
75
72
|
getTimestampInsertStrategy,
|
|
76
73
|
Storage,
|
|
77
|
-
StorageInsertTimestampStrategy,
|
|
78
74
|
StorageWriteError,
|
|
79
75
|
updateOwnerUsage,
|
|
80
76
|
} from "./Storage.js";
|
|
@@ -97,9 +93,9 @@ export interface Sync extends Disposable {
|
|
|
97
93
|
/**
|
|
98
94
|
* Assigns or removes an owner to/from transports with reference counting.
|
|
99
95
|
*
|
|
100
|
-
* Owners are only
|
|
101
|
-
* `owner.transports` or falls back to
|
|
102
|
-
* increment/decrement reference counts (useful for React Hooks).
|
|
96
|
+
* Owners are only synced if assigned to at least one transport. Uses
|
|
97
|
+
* `owner.transports` or falls back to {@link SyncConfig} transports. Multiple
|
|
98
|
+
* calls increment/decrement reference counts (useful for React Hooks).
|
|
103
99
|
*/
|
|
104
100
|
readonly useOwner: (use: boolean, owner: SyncOwner) => void;
|
|
105
101
|
|
|
@@ -119,18 +115,13 @@ export interface SyncDep {
|
|
|
119
115
|
}
|
|
120
116
|
|
|
121
117
|
/**
|
|
122
|
-
* Represents an owner for sync operations.
|
|
123
|
-
* abstracts over the specific owner types ({@link ShardOwner},
|
|
124
|
-
* {@link SharedOwner}, {@link SharedReadonlyOwner}) for the sync layer.
|
|
118
|
+
* Represents an owner for sync operations.
|
|
125
119
|
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
120
|
+
* Includes readonly owner fields plus optional write key (for clients that
|
|
121
|
+
* write) and optional transports to override SyncConfig transports per owner.
|
|
128
122
|
*/
|
|
129
|
-
export interface SyncOwner {
|
|
130
|
-
readonly
|
|
131
|
-
readonly encryptionKey: OwnerEncryptionKey;
|
|
132
|
-
/** Optional for read-only owners like {@link SharedReadonlyOwner}. */
|
|
133
|
-
readonly writeKey?: OwnerWriteKey;
|
|
123
|
+
export interface SyncOwner extends ReadonlyOwner {
|
|
124
|
+
readonly writeKey?: Owner["writeKey"];
|
|
134
125
|
readonly transports?: ReadonlyArray<OwnerTransport>;
|
|
135
126
|
}
|
|
136
127
|
|
|
@@ -180,7 +171,7 @@ export const createSync =
|
|
|
180
171
|
/** Returns owner data only if actively assigned to at least one transport. */
|
|
181
172
|
const getSyncOwner = (ownerId: OwnerId): SyncOwner | null => {
|
|
182
173
|
if (isDisposed) return null;
|
|
183
|
-
return
|
|
174
|
+
return resources.getConsumer(ownerId);
|
|
184
175
|
};
|
|
185
176
|
|
|
186
177
|
const storageResult = createClientStorage({
|
|
@@ -191,24 +182,24 @@ export const createSync =
|
|
|
191
182
|
if (!storageResult.ok) return storageResult;
|
|
192
183
|
const storage = storageResult.value;
|
|
193
184
|
|
|
194
|
-
const createResource = (
|
|
195
|
-
const transportKey = createTransportKey(
|
|
185
|
+
const createResource = (transport: OwnerTransport): WebSocket => {
|
|
186
|
+
const transportKey = createTransportKey(transport);
|
|
196
187
|
|
|
197
188
|
deps.console.log("[sync]", "createWebSocket", {
|
|
198
189
|
transportKey,
|
|
199
|
-
url:
|
|
190
|
+
url: transport.url,
|
|
200
191
|
});
|
|
201
192
|
|
|
202
|
-
return deps.createWebSocket(
|
|
193
|
+
return deps.createWebSocket(transport.url, {
|
|
203
194
|
binaryType: "arraybuffer",
|
|
204
195
|
|
|
205
196
|
onOpen: () => {
|
|
206
197
|
if (isDisposed) return;
|
|
207
198
|
|
|
208
|
-
const webSocket =
|
|
199
|
+
const webSocket = resources.getResource(transportKey);
|
|
209
200
|
if (!webSocket) return;
|
|
210
201
|
|
|
211
|
-
const ownerIds =
|
|
202
|
+
const ownerIds = resources.getConsumersForResource(transportKey);
|
|
212
203
|
deps.console.log("[sync]", "onOpen", { transportKey, ownerIds });
|
|
213
204
|
|
|
214
205
|
for (const ownerId of ownerIds) {
|
|
@@ -239,7 +230,7 @@ export const createSync =
|
|
|
239
230
|
// Only handle ArrayBuffer data for sync messages
|
|
240
231
|
if (isDisposed || !(data instanceof ArrayBuffer)) return;
|
|
241
232
|
|
|
242
|
-
const webSocket =
|
|
233
|
+
const webSocket = resources.getResource(transportKey);
|
|
243
234
|
if (!webSocket) return;
|
|
244
235
|
|
|
245
236
|
const input = new Uint8Array(data);
|
|
@@ -277,7 +268,7 @@ export const createSync =
|
|
|
277
268
|
});
|
|
278
269
|
};
|
|
279
270
|
|
|
280
|
-
const
|
|
271
|
+
const resources = createResources<
|
|
281
272
|
WebSocket,
|
|
282
273
|
TransportKey,
|
|
283
274
|
OwnerTransport,
|
|
@@ -327,16 +318,16 @@ export const createSync =
|
|
|
327
318
|
}
|
|
328
319
|
|
|
329
320
|
deps.console.log("[sync]", "useOwner", { use, owner });
|
|
330
|
-
const
|
|
321
|
+
const transports = owner.transports ?? config.transports;
|
|
331
322
|
|
|
332
323
|
if (use) {
|
|
333
|
-
|
|
324
|
+
resources.addConsumer(owner, transports);
|
|
334
325
|
} else {
|
|
335
|
-
const result =
|
|
326
|
+
const result = resources.removeConsumer(owner, transports);
|
|
336
327
|
|
|
337
328
|
if (!result.ok) {
|
|
338
329
|
deps.console.warn("[sync]", "Failed to remove consumer", {
|
|
339
|
-
|
|
330
|
+
transports,
|
|
340
331
|
ownerId: owner.id,
|
|
341
332
|
error: result.error,
|
|
342
333
|
});
|
|
@@ -382,13 +373,13 @@ export const createSync =
|
|
|
382
373
|
messages,
|
|
383
374
|
);
|
|
384
375
|
|
|
385
|
-
const
|
|
376
|
+
const transports = owner.transports ?? config.transports;
|
|
386
377
|
|
|
387
378
|
// Send message to all transports for this owner
|
|
388
|
-
for (const
|
|
389
|
-
const transportKey = createTransportKey(
|
|
379
|
+
for (const transport of transports) {
|
|
380
|
+
const transportKey = createTransportKey(transport);
|
|
390
381
|
|
|
391
|
-
const webSocket =
|
|
382
|
+
const webSocket = resources.getResource(transportKey);
|
|
392
383
|
if (!webSocket) continue;
|
|
393
384
|
|
|
394
385
|
if (webSocket.isOpen()) {
|
|
@@ -404,7 +395,7 @@ export const createSync =
|
|
|
404
395
|
[Symbol.dispose]: () => {
|
|
405
396
|
if (isDisposed) return;
|
|
406
397
|
isDisposed = true;
|
|
407
|
-
|
|
398
|
+
resources[Symbol.dispose]();
|
|
408
399
|
},
|
|
409
400
|
};
|
|
410
401
|
|
|
@@ -459,8 +450,7 @@ const createClientStorage =
|
|
|
459
450
|
SqliteDep &
|
|
460
451
|
SymmetricCryptoDep &
|
|
461
452
|
TimeDep &
|
|
462
|
-
TimestampConfigDep
|
|
463
|
-
PostMessageDep,
|
|
453
|
+
TimestampConfigDep,
|
|
464
454
|
) =>
|
|
465
455
|
(config: {
|
|
466
456
|
onError: (
|
|
@@ -553,12 +543,6 @@ const createClientStorage =
|
|
|
553
543
|
if (!applyMessagesResult.ok) return applyMessagesResult;
|
|
554
544
|
}
|
|
555
545
|
|
|
556
|
-
// // Apply local mutations atomically with approved messages
|
|
557
|
-
// for (const change of localMutations) {
|
|
558
|
-
// const result = applyLocalOnlyChange(deps)(change);
|
|
559
|
-
// if (!result.ok) return result;
|
|
560
|
-
// }
|
|
561
|
-
|
|
562
546
|
return deps.clock.save(clockTimestamp);
|
|
563
547
|
});
|
|
564
548
|
|
|
@@ -600,11 +584,11 @@ const createClientStorage =
|
|
|
600
584
|
}
|
|
601
585
|
|
|
602
586
|
const { rows } = result.value;
|
|
603
|
-
|
|
587
|
+
assertNonEmptyReadonlyArray(rows, "Every timestamp must have rows");
|
|
604
588
|
|
|
605
|
-
const { table, id } = rows
|
|
606
|
-
const values
|
|
607
|
-
let isInsert
|
|
589
|
+
const { table, id } = firstInArray(rows);
|
|
590
|
+
const values = createRecord<string, SqliteValue>();
|
|
591
|
+
let isInsert;
|
|
608
592
|
let isDelete: boolean | null = null;
|
|
609
593
|
|
|
610
594
|
for (const r of rows) {
|
|
@@ -614,10 +598,13 @@ const createClientStorage =
|
|
|
614
598
|
case "createdAt":
|
|
615
599
|
isInsert = true;
|
|
616
600
|
break;
|
|
601
|
+
case "updatedAt":
|
|
602
|
+
isInsert = false;
|
|
603
|
+
break;
|
|
617
604
|
case "isDeleted":
|
|
618
605
|
assert(
|
|
619
606
|
SqliteBoolean.is(r.value),
|
|
620
|
-
"isDeleted column must contain a valid SqliteBoolean
|
|
607
|
+
"isDeleted column must contain a valid SqliteBoolean",
|
|
621
608
|
);
|
|
622
609
|
isDelete = sqliteBooleanToBoolean(r.value);
|
|
623
610
|
break;
|
|
@@ -626,6 +613,8 @@ const createClientStorage =
|
|
|
626
613
|
}
|
|
627
614
|
}
|
|
628
615
|
|
|
616
|
+
assert(Boolean.is(isInsert), "isInsert must be in evolu_history");
|
|
617
|
+
|
|
629
618
|
const message: CrdtMessage = {
|
|
630
619
|
timestamp: timestampBytesToTimestamp(timestamp),
|
|
631
620
|
change: DbChange.orThrow({
|
|
@@ -646,9 +635,27 @@ const createClientStorage =
|
|
|
646
635
|
|
|
647
636
|
type TransportKey = string & Brand<"TransportKey">;
|
|
648
637
|
|
|
649
|
-
/** Creates a unique identifier for a
|
|
650
|
-
const createTransportKey = (
|
|
651
|
-
return `${
|
|
638
|
+
/** Creates a unique identifier for a {@link OwnerTransport}. */
|
|
639
|
+
const createTransportKey = (transport: OwnerTransport): TransportKey => {
|
|
640
|
+
return `${transport.type}:${transport.url}` as TransportKey;
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const dbChangeToColumns = (change: DbChange, now: DateIso) => {
|
|
644
|
+
let values = objectToEntries(change.values);
|
|
645
|
+
|
|
646
|
+
// SystemColumns are not encoded in change.values.
|
|
647
|
+
values = appendToArray(values, [
|
|
648
|
+
change.isInsert ? "createdAt" : "updatedAt",
|
|
649
|
+
now,
|
|
650
|
+
]);
|
|
651
|
+
if (change.isDelete != null) {
|
|
652
|
+
values = appendToArray(values, [
|
|
653
|
+
"isDeleted",
|
|
654
|
+
booleanToSqliteBoolean(change.isDelete),
|
|
655
|
+
]);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return values;
|
|
652
659
|
};
|
|
653
660
|
|
|
654
661
|
export const applyLocalOnlyChange =
|
|
@@ -661,23 +668,16 @@ export const applyLocalOnlyChange =
|
|
|
661
668
|
`);
|
|
662
669
|
if (!result.ok) return result;
|
|
663
670
|
} else {
|
|
664
|
-
const now = deps.time.nowIso();
|
|
665
671
|
const ownerId = deps.appOwner.id;
|
|
672
|
+
const columns = dbChangeToColumns(change, deps.time.nowIso());
|
|
666
673
|
|
|
667
|
-
|
|
668
|
-
if (change.isDelete !== null) {
|
|
669
|
-
entries = [...entries, ["isDeleted", sqliteFalse]];
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
for (const [column, value] of entries) {
|
|
674
|
+
for (const [column, value] of columns) {
|
|
673
675
|
const result = deps.sqlite.exec(sql.prepared`
|
|
674
676
|
insert into ${sql.identifier(change.table)}
|
|
675
|
-
("ownerId", "id", ${sql.identifier(column)}
|
|
676
|
-
values (${ownerId}, ${change.id}, ${value}
|
|
677
|
+
("ownerId", "id", ${sql.identifier(column)})
|
|
678
|
+
values (${ownerId}, ${change.id}, ${value})
|
|
677
679
|
on conflict ("ownerId", "id") do update
|
|
678
|
-
set
|
|
679
|
-
${sql.identifier(column)} = ${value},
|
|
680
|
-
updatedAt = ${now};
|
|
680
|
+
set ${sql.identifier(column)} = ${value};
|
|
681
681
|
`);
|
|
682
682
|
if (!result.ok) return result;
|
|
683
683
|
}
|
|
@@ -694,35 +694,75 @@ const applyMessages =
|
|
|
694
694
|
): Result<void, SqliteError> => {
|
|
695
695
|
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
696
696
|
|
|
697
|
-
const
|
|
697
|
+
const usage = getOwnerUsage(deps)(
|
|
698
698
|
ownerIdBytes,
|
|
699
699
|
timestampToTimestampBytes(firstInArray(messages).timestamp),
|
|
700
700
|
);
|
|
701
|
-
if (!
|
|
702
|
-
|
|
703
|
-
let { firstTimestamp, lastTimestamp } =
|
|
701
|
+
if (!usage.ok) return usage;
|
|
702
|
+
|
|
703
|
+
let { firstTimestamp, lastTimestamp } = usage.value;
|
|
704
|
+
|
|
705
|
+
for (const { timestamp, change } of messages) {
|
|
706
|
+
const timestampBytes = timestampToTimestampBytes(timestamp);
|
|
707
|
+
const idBytes = idToIdBytes(change.id);
|
|
708
|
+
const columns = dbChangeToColumns(change, timestampToDateIso(timestamp));
|
|
709
|
+
|
|
710
|
+
for (const [column, value] of columns) {
|
|
711
|
+
const updateAppTable = deps.sqlite.exec(sql.prepared`
|
|
712
|
+
with
|
|
713
|
+
existingTimestamp as (
|
|
714
|
+
select 1
|
|
715
|
+
from evolu_history
|
|
716
|
+
where
|
|
717
|
+
"ownerId" = ${ownerIdBytes}
|
|
718
|
+
and "table" = ${change.table}
|
|
719
|
+
and "id" = ${idBytes}
|
|
720
|
+
and "column" = ${column}
|
|
721
|
+
and "timestamp" >= ${timestampBytes}
|
|
722
|
+
limit 1
|
|
723
|
+
)
|
|
724
|
+
insert into ${sql.identifier(change.table)}
|
|
725
|
+
("ownerId", "id", ${sql.identifier(column)})
|
|
726
|
+
select ${ownerId}, ${change.id}, ${value}
|
|
727
|
+
where not exists (select 1 from existingTimestamp)
|
|
728
|
+
on conflict ("ownerId", "id") do update
|
|
729
|
+
set ${sql.identifier(column)} = ${value}
|
|
730
|
+
where not exists (select 1 from existingTimestamp);
|
|
731
|
+
`);
|
|
704
732
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
733
|
+
if (!updateAppTable.ok) return updateAppTable;
|
|
734
|
+
|
|
735
|
+
const insertHistory = deps.sqlite.exec(sql.prepared`
|
|
736
|
+
insert into evolu_history
|
|
737
|
+
("ownerId", "table", "id", "column", "value", "timestamp")
|
|
738
|
+
values
|
|
739
|
+
(
|
|
740
|
+
${ownerIdBytes},
|
|
741
|
+
${change.table},
|
|
742
|
+
${idBytes},
|
|
743
|
+
${column},
|
|
744
|
+
${value},
|
|
745
|
+
${timestampBytes}
|
|
746
|
+
)
|
|
747
|
+
on conflict do nothing;
|
|
748
|
+
`);
|
|
709
749
|
|
|
710
|
-
|
|
750
|
+
if (!insertHistory.ok) return insertHistory;
|
|
751
|
+
}
|
|
711
752
|
|
|
712
753
|
let strategy;
|
|
713
754
|
[strategy, firstTimestamp, lastTimestamp] = getTimestampInsertStrategy(
|
|
714
|
-
|
|
755
|
+
timestampBytes,
|
|
715
756
|
firstTimestamp,
|
|
716
757
|
lastTimestamp,
|
|
717
758
|
);
|
|
718
759
|
|
|
719
|
-
const
|
|
760
|
+
const insertTimestamp = deps.storage.insertTimestamp(
|
|
720
761
|
ownerIdBytes,
|
|
721
|
-
|
|
762
|
+
timestampBytes,
|
|
722
763
|
strategy,
|
|
723
|
-
date,
|
|
724
764
|
);
|
|
725
|
-
if (!
|
|
765
|
+
if (!insertTimestamp.ok) return insertTimestamp;
|
|
726
766
|
}
|
|
727
767
|
|
|
728
768
|
/**
|
|
@@ -741,98 +781,6 @@ const applyMessages =
|
|
|
741
781
|
return ok();
|
|
742
782
|
};
|
|
743
783
|
|
|
744
|
-
const applyMessageToAppTable =
|
|
745
|
-
(deps: SqliteDep) =>
|
|
746
|
-
(
|
|
747
|
-
ownerIdBytes: OwnerIdBytes,
|
|
748
|
-
message: CrdtMessage,
|
|
749
|
-
date: DateIso,
|
|
750
|
-
): Result<void, SqliteError> => {
|
|
751
|
-
const ownerId = ownerIdBytesToOwnerId(ownerIdBytes);
|
|
752
|
-
const columns = dbChangeToColumns(message.change, date);
|
|
753
|
-
|
|
754
|
-
for (const [column, value] of columns) {
|
|
755
|
-
const result = deps.sqlite.exec(sql.prepared`
|
|
756
|
-
with
|
|
757
|
-
existingTimestamp as (
|
|
758
|
-
select 1
|
|
759
|
-
from evolu_history
|
|
760
|
-
where
|
|
761
|
-
"ownerId" = ${ownerIdBytes}
|
|
762
|
-
and "table" = ${message.change.table}
|
|
763
|
-
and "id" = ${idToIdBytes(message.change.id)}
|
|
764
|
-
and "column" = ${column}
|
|
765
|
-
and "timestamp" >= ${timestampToTimestampBytes(message.timestamp)}
|
|
766
|
-
limit 1
|
|
767
|
-
)
|
|
768
|
-
insert into ${sql.identifier(message.change.table)}
|
|
769
|
-
("ownerId", "id", ${sql.identifier(column)}, updatedAt)
|
|
770
|
-
select ${ownerId}, ${message.change.id}, ${value}, ${date}
|
|
771
|
-
where not exists (select 1 from existingTimestamp)
|
|
772
|
-
on conflict ("ownerId", "id") do update
|
|
773
|
-
set
|
|
774
|
-
${sql.identifier(column)} = ${value},
|
|
775
|
-
updatedAt = ${date}
|
|
776
|
-
where not exists (select 1 from existingTimestamp);
|
|
777
|
-
`);
|
|
778
|
-
|
|
779
|
-
if (!result.ok) return result;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
return ok();
|
|
783
|
-
};
|
|
784
|
-
|
|
785
|
-
export const applyMessageToTimestampAndHistoryTables =
|
|
786
|
-
(deps: ClientStorageDep & SqliteDep) =>
|
|
787
|
-
(
|
|
788
|
-
ownerId: OwnerIdBytes,
|
|
789
|
-
message: CrdtMessage,
|
|
790
|
-
strategy: StorageInsertTimestampStrategy,
|
|
791
|
-
date: DateIso,
|
|
792
|
-
): Result<void, SqliteError> => {
|
|
793
|
-
const timestamp = timestampToTimestampBytes(message.timestamp);
|
|
794
|
-
const id = idToIdBytes(message.change.id);
|
|
795
|
-
|
|
796
|
-
const result = deps.storage.insertTimestamp(ownerId, timestamp, strategy);
|
|
797
|
-
if (!result.ok) return result;
|
|
798
|
-
|
|
799
|
-
const columns = dbChangeToColumns(message.change, date);
|
|
800
|
-
|
|
801
|
-
for (const [column, value] of columns) {
|
|
802
|
-
const result = deps.sqlite.exec(sql.prepared`
|
|
803
|
-
insert into evolu_history
|
|
804
|
-
("ownerId", "table", "id", "column", "value", "timestamp")
|
|
805
|
-
values
|
|
806
|
-
(
|
|
807
|
-
${ownerId},
|
|
808
|
-
${message.change.table},
|
|
809
|
-
${id},
|
|
810
|
-
${column},
|
|
811
|
-
${value},
|
|
812
|
-
${timestamp}
|
|
813
|
-
)
|
|
814
|
-
on conflict do nothing;
|
|
815
|
-
`);
|
|
816
|
-
if (!result.ok) return result;
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
return ok();
|
|
820
|
-
};
|
|
821
|
-
|
|
822
|
-
const dbChangeToColumns = (
|
|
823
|
-
change: DbChange,
|
|
824
|
-
date: DateIso,
|
|
825
|
-
): Array<[string, SqliteValue | DateIso]> => {
|
|
826
|
-
const entries = [...objectToEntries(change.values)];
|
|
827
|
-
if (change.isInsert) {
|
|
828
|
-
entries.push(["createdAt", date]);
|
|
829
|
-
}
|
|
830
|
-
if (change.isDelete !== null) {
|
|
831
|
-
entries.push(["isDeleted", booleanToSqliteBoolean(change.isDelete)]);
|
|
832
|
-
}
|
|
833
|
-
return entries;
|
|
834
|
-
};
|
|
835
|
-
|
|
836
784
|
/**
|
|
837
785
|
* TODO: Rework for the new owners API.
|
|
838
786
|
*
|
|
@@ -336,8 +336,21 @@ export const timestampBytesToTimestamp = (
|
|
|
336
336
|
return { millis: Number(millis), counter, nodeId } as Timestamp;
|
|
337
337
|
};
|
|
338
338
|
|
|
339
|
+
/**
|
|
340
|
+
* An {@link Order} for {@link TimestampBytes}.
|
|
341
|
+
*
|
|
342
|
+
* This `Order` uses lexicographic byte order to compare serialized
|
|
343
|
+
* {@link TimestampBytes} produced by {@link timestampToTimestampBytes}. See
|
|
344
|
+
* {@link orderUint8Array} for the underlying implementation.
|
|
345
|
+
*/
|
|
339
346
|
export const orderTimestampBytes: Order<TimestampBytes> = orderUint8Array;
|
|
340
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Convert a {@link Timestamp} to an ISO 8601 {@link DateIso} string.
|
|
350
|
+
*
|
|
351
|
+
* The conversion uses the timestamp's `millis` (a {@link Millis} value) and
|
|
352
|
+
* `Date.prototype.toISOString()` to produce a `DateIso`.
|
|
353
|
+
*/
|
|
341
354
|
export const timestampToDateIso = (timestamp: Timestamp): DateIso =>
|
|
342
|
-
// `as DateIso` is safe because
|
|
355
|
+
// `as DateIso` is safe because Timestamp guarantees a valid `millis`
|
|
343
356
|
new Date(timestamp.millis).toISOString() as DateIso;
|