@evolu/common 6.0.1-preview.30 → 6.0.1-preview.31
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/Array.d.ts +14 -3
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +14 -3
- package/dist/src/Evolu/Db.d.ts +3 -0
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.d.ts +13 -4
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +13 -18
- package/dist/src/Evolu/Storage.d.ts +42 -14
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +125 -85
- package/dist/src/Evolu/Sync.d.ts +2 -3
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +27 -8
- package/dist/src/Type.d.ts +3 -2
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +3 -2
- package/package.json +1 -1
- package/src/Array.ts +17 -3
- package/src/Evolu/Db.ts +3 -0
- package/src/Evolu/Owner.ts +15 -29
- package/src/Evolu/Relay.ts +36 -20
- package/src/Evolu/Storage.ts +184 -120
- package/src/Evolu/Sync.ts +57 -11
- package/src/Type.ts +3 -2
package/src/Evolu/Storage.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
firstInArray,
|
|
4
|
+
isNonEmptyReadonlyArray,
|
|
5
|
+
NonEmptyReadonlyArray,
|
|
6
|
+
} from "../Array.js";
|
|
3
7
|
import { assert } from "../Assert.js";
|
|
4
8
|
import { Brand } from "../Brand.js";
|
|
5
9
|
import { concatBytes } from "../Buffer.js";
|
|
@@ -22,7 +26,6 @@ import {
|
|
|
22
26
|
Owner,
|
|
23
27
|
OwnerId,
|
|
24
28
|
OwnerIdBytes,
|
|
25
|
-
ownerIdBytesToOwnerId,
|
|
26
29
|
OwnerWriteKey,
|
|
27
30
|
} from "./Owner.js";
|
|
28
31
|
import { orderTimestampBytes, Timestamp, TimestampBytes } from "./Timestamp.js";
|
|
@@ -32,16 +35,16 @@ export interface StorageConfig {
|
|
|
32
35
|
* Callback called before an attempt to write, to check if an {@link OwnerId}
|
|
33
36
|
* has sufficient quota for the write.
|
|
34
37
|
*
|
|
35
|
-
* The callback receives the {@link OwnerId} and the
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
+
* The callback receives the {@link OwnerId} and the total bytes that would be
|
|
39
|
+
* stored after the write (current stored bytes plus incoming bytes), and
|
|
40
|
+
* returns a {@link MaybeAsync} boolean: `true` to allow the write, or `false`
|
|
41
|
+
* to deny it due to quota limits.
|
|
38
42
|
*
|
|
39
43
|
* The callback can be synchronous (for SQLite or in-memory checks) or
|
|
40
44
|
* asynchronous (for calling remote APIs).
|
|
41
45
|
*
|
|
42
|
-
* The callback returns a boolean rather than an error
|
|
43
|
-
*
|
|
44
|
-
* implementation.
|
|
46
|
+
* The callback returns a boolean rather than an error because error handling
|
|
47
|
+
* and logging are the responsibility of the callback implementation.
|
|
45
48
|
*
|
|
46
49
|
* ### Example
|
|
47
50
|
*
|
|
@@ -299,16 +302,11 @@ export interface BaseSqliteStorage
|
|
|
299
302
|
| "iterate"
|
|
300
303
|
| "deleteOwner"
|
|
301
304
|
> {
|
|
302
|
-
/**
|
|
303
|
-
* Inserts a timestamp for an owner into the skiplist-based storage.
|
|
304
|
-
*
|
|
305
|
-
* Must be idempotent - inserting the same timestamp multiple times has no
|
|
306
|
-
* effect after the first insertion. This is crucial for sync reliability as
|
|
307
|
-
* messages may be received and processed multiple times.
|
|
308
|
-
*/
|
|
305
|
+
/** Inserts a timestamp for an owner into the skiplist-based storage. */
|
|
309
306
|
readonly insertTimestamp: (
|
|
310
307
|
ownerId: OwnerIdBytes,
|
|
311
308
|
timestamp: TimestampBytes,
|
|
309
|
+
strategy: StorageInsertTimestampStrategy,
|
|
312
310
|
) => Result<void, SqliteError>;
|
|
313
311
|
|
|
314
312
|
/**
|
|
@@ -331,19 +329,62 @@ export interface CreateBaseSqliteStorageConfig extends StorageConfig {
|
|
|
331
329
|
onStorageError: (error: SqliteError) => void;
|
|
332
330
|
}
|
|
333
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Creates a {@link BaseSqliteStorage} implementation.
|
|
334
|
+
*
|
|
335
|
+
* # Stateless Design
|
|
336
|
+
*
|
|
337
|
+
* This implementation is fully stateless - it requires no in-memory state
|
|
338
|
+
* between invocations. All necessary metadata (timestamp bounds for insertion
|
|
339
|
+
* strategy optimization) is persisted in the evolu_usage table. This makes
|
|
340
|
+
* Evolu Relay suitable for stateless serverless environments like AWS Lambda,
|
|
341
|
+
* Cloudflare Workers with Durable Objects, and other platforms where memory
|
|
342
|
+
* doesn't persist between requests. While not extensively tested in all these
|
|
343
|
+
* environments yet, the stateless design should work well across them.
|
|
344
|
+
*/
|
|
334
345
|
export const createBaseSqliteStorage =
|
|
335
346
|
(deps: SqliteStorageDeps) =>
|
|
336
347
|
(config: CreateBaseSqliteStorageConfig): BaseSqliteStorage => {
|
|
337
|
-
// TODO: Use evolu_usage table.
|
|
338
|
-
const ownerStats = new Map<
|
|
339
|
-
OwnerId,
|
|
340
|
-
{
|
|
341
|
-
minT: TimestampBytes;
|
|
342
|
-
maxT: TimestampBytes;
|
|
343
|
-
}
|
|
344
|
-
>();
|
|
345
|
-
|
|
346
348
|
return {
|
|
349
|
+
insertTimestamp: (
|
|
350
|
+
ownerId: OwnerIdBytes,
|
|
351
|
+
timestamp: TimestampBytes,
|
|
352
|
+
strategy: StorageInsertTimestampStrategy,
|
|
353
|
+
) => {
|
|
354
|
+
const level = randomSkiplistLevel(deps);
|
|
355
|
+
return insertTimestamp(deps)(ownerId, timestamp, level, strategy);
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
getExistingTimestamps: (ownerIdBytes, timestampsBytes) => {
|
|
359
|
+
const concatenatedTimestamps = concatBytes(...timestampsBytes);
|
|
360
|
+
|
|
361
|
+
const result = deps.sqlite.exec<{
|
|
362
|
+
timestampBytes: TimestampBytes;
|
|
363
|
+
}>(sql`
|
|
364
|
+
with recursive
|
|
365
|
+
split_timestamps(timestampBytes, pos) as (
|
|
366
|
+
select
|
|
367
|
+
substr(${concatenatedTimestamps}, 1, 16),
|
|
368
|
+
17 as pos
|
|
369
|
+
union all
|
|
370
|
+
select
|
|
371
|
+
substr(${concatenatedTimestamps}, pos, 16),
|
|
372
|
+
pos + 16
|
|
373
|
+
from split_timestamps
|
|
374
|
+
where pos <= length(${concatenatedTimestamps})
|
|
375
|
+
)
|
|
376
|
+
select s.timestampBytes
|
|
377
|
+
from
|
|
378
|
+
split_timestamps s
|
|
379
|
+
join evolu_timestamp t
|
|
380
|
+
on t.ownerId = ${ownerIdBytes} and s.timestampBytes = t.t;
|
|
381
|
+
`);
|
|
382
|
+
|
|
383
|
+
if (!result.ok) return result;
|
|
384
|
+
|
|
385
|
+
return ok(result.value.rows.map((row) => row.timestampBytes));
|
|
386
|
+
},
|
|
387
|
+
|
|
347
388
|
getSize: (ownerId) => {
|
|
348
389
|
const size = getSize(deps)(ownerId);
|
|
349
390
|
if (!size.ok) {
|
|
@@ -439,75 +480,6 @@ export const createBaseSqliteStorage =
|
|
|
439
480
|
}
|
|
440
481
|
return true;
|
|
441
482
|
},
|
|
442
|
-
|
|
443
|
-
insertTimestamp: (ownerId: OwnerIdBytes, timestamp: TimestampBytes) => {
|
|
444
|
-
const ownerIdString = ownerIdBytesToOwnerId(ownerId);
|
|
445
|
-
const level = randomSkiplistLevel(deps);
|
|
446
|
-
|
|
447
|
-
let stats = ownerStats.get(ownerIdString);
|
|
448
|
-
|
|
449
|
-
if (!stats) {
|
|
450
|
-
const result = deps.sqlite.exec<{
|
|
451
|
-
maxT: TimestampBytes | null;
|
|
452
|
-
minT: TimestampBytes | null;
|
|
453
|
-
}>(sql.prepared`
|
|
454
|
-
select min(t) as minT, max(t) as maxT
|
|
455
|
-
from evolu_timestamp
|
|
456
|
-
where ownerId = ${ownerId};
|
|
457
|
-
`);
|
|
458
|
-
if (!result.ok) return result;
|
|
459
|
-
|
|
460
|
-
stats = {
|
|
461
|
-
minT: result.value.rows[0].minT ?? timestamp,
|
|
462
|
-
maxT: result.value.rows[0].maxT ?? timestamp,
|
|
463
|
-
};
|
|
464
|
-
ownerStats.set(ownerIdString, stats);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
let strategy: InsertTimestampStrategy;
|
|
468
|
-
|
|
469
|
-
if (orderTimestampBytes(timestamp, stats.maxT) === 1) {
|
|
470
|
-
strategy = "append";
|
|
471
|
-
stats.maxT = timestamp;
|
|
472
|
-
} else if (orderTimestampBytes(timestamp, stats.minT) === -1) {
|
|
473
|
-
strategy = "prepend";
|
|
474
|
-
stats.minT = timestamp;
|
|
475
|
-
} else {
|
|
476
|
-
strategy = "insert";
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
return insertTimestamp(deps)(ownerId, timestamp, level, strategy);
|
|
480
|
-
},
|
|
481
|
-
|
|
482
|
-
getExistingTimestamps: (ownerIdBytes, timestampsBytes) => {
|
|
483
|
-
const concatenatedTimestamps = concatBytes(...timestampsBytes);
|
|
484
|
-
|
|
485
|
-
const result = deps.sqlite.exec<{
|
|
486
|
-
timestampBytes: TimestampBytes;
|
|
487
|
-
}>(sql`
|
|
488
|
-
with recursive
|
|
489
|
-
split_timestamps(timestampBytes, pos) as (
|
|
490
|
-
select
|
|
491
|
-
substr(${concatenatedTimestamps}, 1, 16),
|
|
492
|
-
17 as pos
|
|
493
|
-
union all
|
|
494
|
-
select
|
|
495
|
-
substr(${concatenatedTimestamps}, pos, 16),
|
|
496
|
-
pos + 16
|
|
497
|
-
from split_timestamps
|
|
498
|
-
where pos <= length(${concatenatedTimestamps})
|
|
499
|
-
)
|
|
500
|
-
select s.timestampBytes
|
|
501
|
-
from
|
|
502
|
-
split_timestamps s
|
|
503
|
-
join evolu_timestamp t
|
|
504
|
-
on t.ownerId = ${ownerIdBytes} and s.timestampBytes = t.t;
|
|
505
|
-
`);
|
|
506
|
-
|
|
507
|
-
if (!result.ok) return result;
|
|
508
|
-
|
|
509
|
-
return ok(result.value.rows.map((row) => row.timestampBytes));
|
|
510
|
-
},
|
|
511
483
|
};
|
|
512
484
|
};
|
|
513
485
|
|
|
@@ -533,13 +505,7 @@ export const createBaseSqliteStorageTables = (
|
|
|
533
505
|
* - `t` – TimestampBytes
|
|
534
506
|
* - `h1`/`h2` – 12-byte fingerprint split into two integers for fast XOR
|
|
535
507
|
* - `c` – incremental count
|
|
536
|
-
* - `l` – Skiplist level (1 to
|
|
537
|
-
*
|
|
538
|
-
* For scaling or isolation, sharding is possible—each owner can have a
|
|
539
|
-
* separate SQLite database.
|
|
540
|
-
*
|
|
541
|
-
* Maybe we could use an integer surrogate key for ownerId, but it's fast
|
|
542
|
-
* enough even without it.
|
|
508
|
+
* - `l` – Skiplist level (1 to 10)
|
|
543
509
|
*/
|
|
544
510
|
sql`
|
|
545
511
|
create table evolu_timestamp (
|
|
@@ -572,20 +538,15 @@ export const createBaseSqliteStorageTables = (
|
|
|
572
538
|
*
|
|
573
539
|
* - `ownerId` – OwnerIdBytes (primary key)
|
|
574
540
|
* - `storedBytes` – total bytes stored in database
|
|
575
|
-
* - `
|
|
576
|
-
* - `
|
|
577
|
-
* - `firstTimestamp` – TODO: Decide how to use (nullable)
|
|
578
|
-
* - `lastTimestamp` – TODO: Decide how to use (nullable)
|
|
541
|
+
* - `firstTimestamp` – for timestamp insertion strategies
|
|
542
|
+
* - `lastTimestamp` – for timestamp insertion strategies
|
|
579
543
|
*/
|
|
580
544
|
sql`
|
|
581
545
|
create table evolu_usage (
|
|
582
546
|
"ownerId" blob primary key,
|
|
583
|
-
"storedBytes" integer not null
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
-- "sentBytes" integer not null,
|
|
587
|
-
-- "firstTimestamp" blob,
|
|
588
|
-
-- "lastTimestamp" blob
|
|
547
|
+
"storedBytes" integer not null,
|
|
548
|
+
"firstTimestamp" blob,
|
|
549
|
+
"lastTimestamp" blob
|
|
589
550
|
)
|
|
590
551
|
strict;
|
|
591
552
|
`,
|
|
@@ -596,23 +557,53 @@ export const createBaseSqliteStorageTables = (
|
|
|
596
557
|
return ok();
|
|
597
558
|
};
|
|
598
559
|
|
|
599
|
-
type
|
|
560
|
+
export type StorageInsertTimestampStrategy = "append" | "prepend" | "insert";
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Determines the insertion strategy for a timestamp based on its position
|
|
564
|
+
* relative to the current first and last timestamps.
|
|
565
|
+
*
|
|
566
|
+
* Returns a tuple with the strategy and updated timestamp bounds.
|
|
567
|
+
*/
|
|
568
|
+
export const getTimestampInsertStrategy = (
|
|
569
|
+
timestamp: TimestampBytes,
|
|
570
|
+
firstTimestamp: TimestampBytes,
|
|
571
|
+
lastTimestamp: TimestampBytes,
|
|
572
|
+
): [
|
|
573
|
+
strategy: StorageInsertTimestampStrategy,
|
|
574
|
+
firstTimestamp: TimestampBytes,
|
|
575
|
+
lastTimestamp: TimestampBytes,
|
|
576
|
+
] => {
|
|
577
|
+
if (orderTimestampBytes(timestamp, lastTimestamp) === 1) {
|
|
578
|
+
return ["append", firstTimestamp, timestamp];
|
|
579
|
+
}
|
|
580
|
+
if (orderTimestampBytes(timestamp, firstTimestamp) === -1) {
|
|
581
|
+
return ["prepend", timestamp, lastTimestamp];
|
|
582
|
+
}
|
|
583
|
+
return ["insert", firstTimestamp, lastTimestamp];
|
|
584
|
+
};
|
|
600
585
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
586
|
+
/**
|
|
587
|
+
* AFAIK, we can't do both insert and update in one query, and that's probably
|
|
588
|
+
* why append is 2x faster than insert. Prepend also has to update parents, but
|
|
589
|
+
* it's constantly fast. Insert degrades for reversed (yet LIMIT X magically
|
|
590
|
+
* fixes that) but it's OK for append.
|
|
591
|
+
*
|
|
592
|
+
* Note: SQL operations are idempotent (using `on conflict do nothing` and
|
|
593
|
+
* `changes() > 0`), but this is no longer required here since we use
|
|
594
|
+
* {@link BaseSqliteStorage.getExistingTimestamps} to filter out duplicates
|
|
595
|
+
* before insertion, which we need for quota checks anyway.
|
|
596
|
+
*
|
|
597
|
+
* TODO: Remove idempotency (`on conflict do nothing` and `changes() > 0`) since
|
|
598
|
+
* duplicates are now filtered before insertion.
|
|
599
|
+
*/
|
|
609
600
|
const insertTimestamp =
|
|
610
601
|
(deps: SqliteDep) =>
|
|
611
602
|
(
|
|
612
603
|
ownerId: OwnerIdBytes,
|
|
613
604
|
timestamp: TimestampBytes,
|
|
614
605
|
level: PositiveInt,
|
|
615
|
-
strategy:
|
|
606
|
+
strategy: StorageInsertTimestampStrategy,
|
|
616
607
|
): Result<void, SqliteError> => {
|
|
617
608
|
const [h1, h2] = fingerprintToSqliteFingerprint(
|
|
618
609
|
timestampBytesToFingerprint(timestamp),
|
|
@@ -1612,3 +1603,76 @@ export const getTimestampByIndex =
|
|
|
1612
1603
|
if (!result.ok) return result;
|
|
1613
1604
|
return ok(result.value.rows[0].pt);
|
|
1614
1605
|
};
|
|
1606
|
+
|
|
1607
|
+
/** Retrieves usage information for an owner from the evolu_usage table. */
|
|
1608
|
+
export const getOwnerUsage =
|
|
1609
|
+
(deps: SqliteDep) =>
|
|
1610
|
+
(
|
|
1611
|
+
ownerIdBytes: OwnerIdBytes,
|
|
1612
|
+
initialTimestamp: TimestampBytes,
|
|
1613
|
+
): Result<
|
|
1614
|
+
{
|
|
1615
|
+
storedBytes: NonNegativeInt | null;
|
|
1616
|
+
firstTimestamp: TimestampBytes;
|
|
1617
|
+
lastTimestamp: TimestampBytes;
|
|
1618
|
+
},
|
|
1619
|
+
SqliteError
|
|
1620
|
+
> => {
|
|
1621
|
+
const result = deps.sqlite.exec<{
|
|
1622
|
+
storedBytes: NonNegativeInt;
|
|
1623
|
+
firstTimestamp: TimestampBytes | null;
|
|
1624
|
+
lastTimestamp: TimestampBytes | null;
|
|
1625
|
+
}>(sql`
|
|
1626
|
+
select storedBytes, firstTimestamp, lastTimestamp
|
|
1627
|
+
from evolu_usage
|
|
1628
|
+
where ownerId = ${ownerIdBytes};
|
|
1629
|
+
`);
|
|
1630
|
+
if (!result.ok) return result;
|
|
1631
|
+
|
|
1632
|
+
if (!isNonEmptyReadonlyArray(result.value.rows)) {
|
|
1633
|
+
return ok({
|
|
1634
|
+
storedBytes: null,
|
|
1635
|
+
firstTimestamp: initialTimestamp,
|
|
1636
|
+
lastTimestamp: initialTimestamp,
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
const row = firstInArray(result.value.rows);
|
|
1641
|
+
assert(row.firstTimestamp, "not null");
|
|
1642
|
+
assert(row.lastTimestamp, "not null");
|
|
1643
|
+
|
|
1644
|
+
return ok({
|
|
1645
|
+
storedBytes: row.storedBytes,
|
|
1646
|
+
firstTimestamp: row.firstTimestamp,
|
|
1647
|
+
lastTimestamp: row.lastTimestamp,
|
|
1648
|
+
});
|
|
1649
|
+
};
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* Updates timestamp bounds in evolu_usage table.
|
|
1653
|
+
*
|
|
1654
|
+
* Used by both relay and client to maintain firstTimestamp/lastTimestamp after
|
|
1655
|
+
* processing messages.
|
|
1656
|
+
*/
|
|
1657
|
+
export const updateOwnerUsage =
|
|
1658
|
+
(deps: SqliteDep) =>
|
|
1659
|
+
(
|
|
1660
|
+
ownerIdBytes: OwnerIdBytes,
|
|
1661
|
+
storedBytes: PositiveInt,
|
|
1662
|
+
firstTimestamp: TimestampBytes,
|
|
1663
|
+
lastTimestamp: TimestampBytes,
|
|
1664
|
+
): Result<void, SqliteError> => {
|
|
1665
|
+
const result = deps.sqlite.exec(sql`
|
|
1666
|
+
insert into evolu_usage
|
|
1667
|
+
("ownerId", "storedBytes", "firstTimestamp", "lastTimestamp")
|
|
1668
|
+
values
|
|
1669
|
+
(${ownerIdBytes}, ${storedBytes}, ${firstTimestamp}, ${lastTimestamp})
|
|
1670
|
+
on conflict (ownerId) do update
|
|
1671
|
+
set
|
|
1672
|
+
storedBytes = ${storedBytes},
|
|
1673
|
+
firstTimestamp = ${firstTimestamp},
|
|
1674
|
+
lastTimestamp = ${lastTimestamp};
|
|
1675
|
+
`);
|
|
1676
|
+
if (!result.ok) return result;
|
|
1677
|
+
return ok();
|
|
1678
|
+
};
|
package/src/Evolu/Sync.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
firstInArray,
|
|
3
|
+
isNonEmptyReadonlyArray,
|
|
4
|
+
NonEmptyArray,
|
|
5
|
+
NonEmptyReadonlyArray,
|
|
6
|
+
} from "../Array.js";
|
|
2
7
|
import { assert } from "../Assert.js";
|
|
3
8
|
import { Brand } from "../Brand.js";
|
|
4
9
|
import { ConsoleDep } from "../Console.js";
|
|
@@ -17,7 +22,7 @@ import { err, ok, Result } from "../Result.js";
|
|
|
17
22
|
import { sql, SqliteDep, SqliteError, SqliteValue } from "../Sqlite.js";
|
|
18
23
|
import { AbortError, createMutex } from "../Task.js";
|
|
19
24
|
import { TimeDep } from "../Time.js";
|
|
20
|
-
import { IdBytes, idBytesToId, idToIdBytes } from "../Type.js";
|
|
25
|
+
import { IdBytes, idBytesToId, idToIdBytes, PositiveInt } from "../Type.js";
|
|
21
26
|
import { CreateWebSocketDep, WebSocket } from "../WebSocket.js";
|
|
22
27
|
import type { PostMessageDep } from "./Db.js";
|
|
23
28
|
import {
|
|
@@ -51,8 +56,12 @@ import {
|
|
|
51
56
|
CrdtMessage,
|
|
52
57
|
createBaseSqliteStorage,
|
|
53
58
|
DbChange,
|
|
59
|
+
getOwnerUsage,
|
|
60
|
+
getTimestampInsertStrategy,
|
|
54
61
|
Storage,
|
|
62
|
+
StorageInsertTimestampStrategy,
|
|
55
63
|
StorageWriteError,
|
|
64
|
+
updateOwnerUsage,
|
|
56
65
|
} from "./Storage.js";
|
|
57
66
|
import {
|
|
58
67
|
createInitialTimestamp,
|
|
@@ -519,11 +528,13 @@ const createClientStorage =
|
|
|
519
528
|
clockTimestamp = nextTimestamp.value;
|
|
520
529
|
}
|
|
521
530
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
531
|
+
if (isNonEmptyReadonlyArray(messages)) {
|
|
532
|
+
const applyMessagesResult = applyMessages({ ...deps, storage })(
|
|
533
|
+
owner.id,
|
|
534
|
+
messages,
|
|
535
|
+
);
|
|
536
|
+
if (!applyMessagesResult.ok) return applyMessagesResult;
|
|
537
|
+
}
|
|
527
538
|
|
|
528
539
|
// // Apply local mutations atomically with approved messages
|
|
529
540
|
// for (const change of localMutations) {
|
|
@@ -644,25 +655,56 @@ export const applyLocalOnlyChange =
|
|
|
644
655
|
return ok();
|
|
645
656
|
};
|
|
646
657
|
|
|
647
|
-
|
|
658
|
+
const applyMessages =
|
|
648
659
|
(deps: ClientStorageDep & ClockDep & RandomDep & SqliteDep) =>
|
|
649
660
|
(
|
|
650
661
|
ownerId: OwnerId,
|
|
651
|
-
messages:
|
|
662
|
+
messages: NonEmptyReadonlyArray<CrdtMessage>,
|
|
652
663
|
): Result<void, SqliteError> => {
|
|
653
664
|
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
654
665
|
|
|
666
|
+
const usageResult = getOwnerUsage(deps)(
|
|
667
|
+
ownerIdBytes,
|
|
668
|
+
timestampToTimestampBytes(firstInArray(messages).timestamp),
|
|
669
|
+
);
|
|
670
|
+
if (!usageResult.ok) return usageResult;
|
|
671
|
+
|
|
672
|
+
let { firstTimestamp, lastTimestamp } = usageResult.value;
|
|
673
|
+
|
|
655
674
|
for (const message of messages) {
|
|
656
675
|
const result1 = applyMessageToAppTable(deps)(ownerIdBytes, message);
|
|
657
676
|
if (!result1.ok) return result1;
|
|
658
677
|
|
|
678
|
+
const timestamp = timestampToTimestampBytes(message.timestamp);
|
|
679
|
+
|
|
680
|
+
let strategy;
|
|
681
|
+
[strategy, firstTimestamp, lastTimestamp] = getTimestampInsertStrategy(
|
|
682
|
+
timestamp,
|
|
683
|
+
firstTimestamp,
|
|
684
|
+
lastTimestamp,
|
|
685
|
+
);
|
|
686
|
+
|
|
659
687
|
const result2 = applyMessageToTimestampAndHistoryTables(deps)(
|
|
660
688
|
ownerIdBytes,
|
|
661
689
|
message,
|
|
690
|
+
strategy,
|
|
662
691
|
);
|
|
663
692
|
if (!result2.ok) return result2;
|
|
664
693
|
}
|
|
665
694
|
|
|
695
|
+
/**
|
|
696
|
+
* TODO: Implement proper storedBytes tracking for client using encrypted
|
|
697
|
+
* message sizes (need to figure out how to reuse received or postpone
|
|
698
|
+
* client...).
|
|
699
|
+
*/
|
|
700
|
+
const updateUsage = updateOwnerUsage(deps)(
|
|
701
|
+
ownerIdBytes,
|
|
702
|
+
1 as PositiveInt, // Placeholder until proper tracking implemented
|
|
703
|
+
firstTimestamp,
|
|
704
|
+
lastTimestamp,
|
|
705
|
+
);
|
|
706
|
+
if (!updateUsage.ok) return updateUsage;
|
|
707
|
+
|
|
666
708
|
return ok();
|
|
667
709
|
};
|
|
668
710
|
|
|
@@ -705,11 +747,15 @@ const applyMessageToAppTable =
|
|
|
705
747
|
|
|
706
748
|
export const applyMessageToTimestampAndHistoryTables =
|
|
707
749
|
(deps: ClientStorageDep & SqliteDep) =>
|
|
708
|
-
(
|
|
750
|
+
(
|
|
751
|
+
ownerId: OwnerIdBytes,
|
|
752
|
+
message: CrdtMessage,
|
|
753
|
+
strategy: StorageInsertTimestampStrategy,
|
|
754
|
+
): Result<void, SqliteError> => {
|
|
709
755
|
const timestamp = timestampToTimestampBytes(message.timestamp);
|
|
710
756
|
const id = idToIdBytes(message.change.id);
|
|
711
757
|
|
|
712
|
-
const result = deps.storage.insertTimestamp(ownerId, timestamp);
|
|
758
|
+
const result = deps.storage.insertTimestamp(ownerId, timestamp, strategy);
|
|
713
759
|
if (!result.ok) return result;
|
|
714
760
|
|
|
715
761
|
for (const [column, value] of Object.entries(message.change.values)) {
|
package/src/Type.ts
CHANGED
|
@@ -1641,8 +1641,9 @@ export const createId = <B extends string = never>(
|
|
|
1641
1641
|
* });
|
|
1642
1642
|
* ```
|
|
1643
1643
|
*
|
|
1644
|
-
* **Important**: This transformation
|
|
1645
|
-
*
|
|
1644
|
+
* **Important**: This transformation uses the first 16 bytes of SHA-256 hash of
|
|
1645
|
+
* the string bytes, therefore it's not possible to recover the original
|
|
1646
|
+
* external string from the generated {@link Id}. If you need to preserve the
|
|
1646
1647
|
* original external ID, store it in a separate column.
|
|
1647
1648
|
*
|
|
1648
1649
|
* @category String
|