11import {
2+ appendToArray ,
23 firstInArray ,
34 isNonEmptyReadonlyArray ,
45 NonEmptyArray ,
56 NonEmptyReadonlyArray ,
67} from "../Array.js" ;
7- import { assert } from "../Assert.js" ;
8+ import { assert , assertNonEmptyReadonlyArray } from "../Assert.js" ;
89import { Brand } from "../Brand.js" ;
910import { ConsoleDep } from "../Console.js" ;
1011import {
@@ -26,12 +27,18 @@ import {
2627 sqliteBooleanToBoolean ,
2728 SqliteDep ,
2829 SqliteError ,
29- sqliteFalse ,
3030 SqliteValue ,
3131} from "../Sqlite.js" ;
3232import { AbortError , createMutex } from "../Task.js" ;
3333import { TimeDep } from "../Time.js" ;
34- import { IdBytes , idBytesToId , idToIdBytes , PositiveInt } from "../Type.js" ;
34+ import {
35+ Boolean ,
36+ DateIso ,
37+ IdBytes ,
38+ idBytesToId ,
39+ idToIdBytes ,
40+ PositiveInt ,
41+ } from "../Type.js" ;
3542import { CreateWebSocketDep , WebSocket } from "../WebSocket.js" ;
3643import type { AppOwnerDep , PostMessageDep } from "./Db.js" ;
3744import {
@@ -443,8 +450,7 @@ const createClientStorage =
443450 SqliteDep &
444451 SymmetricCryptoDep &
445452 TimeDep &
446- TimestampConfigDep &
447- PostMessageDep ,
453+ TimestampConfigDep ,
448454 ) =>
449455 ( config : {
450456 onError : (
@@ -578,11 +584,11 @@ const createClientStorage =
578584 }
579585
580586 const { rows } = result . value ;
581- assert ( rows . length > 0 , "Rows must not be empty " ) ;
587+ assertNonEmptyReadonlyArray ( rows , "Every timestamp must have rows " ) ;
582588
583- const { table, id } = rows [ 0 ] ;
589+ const { table, id } = firstInArray ( rows ) ;
584590 const values = createRecord < string , SqliteValue > ( ) ;
585- let isInsert = false ;
591+ let isInsert ;
586592 let isDelete : boolean | null = null ;
587593
588594 for ( const r of rows ) {
@@ -592,10 +598,13 @@ const createClientStorage =
592598 case "createdAt" :
593599 isInsert = true ;
594600 break ;
601+ case "updatedAt" :
602+ isInsert = false ;
603+ break ;
595604 case "isDeleted" :
596605 assert (
597606 SqliteBoolean . is ( r . value ) ,
598- "isDeleted column must contain a valid SqliteBoolean (0 or 1) " ,
607+ "isDeleted column must contain a valid SqliteBoolean" ,
599608 ) ;
600609 isDelete = sqliteBooleanToBoolean ( r . value ) ;
601610 break ;
@@ -604,6 +613,8 @@ const createClientStorage =
604613 }
605614 }
606615
616+ assert ( Boolean . is ( isInsert ) , "isInsert must be in evolu_history" ) ;
617+
607618 const message : CrdtMessage = {
608619 timestamp : timestampBytesToTimestamp ( timestamp ) ,
609620 change : DbChange . orThrow ( {
@@ -629,6 +640,24 @@ const createTransportKey = (transport: OwnerTransport): TransportKey => {
629640 return `${ transport . type } :${ transport . url } ` as TransportKey ;
630641} ;
631642
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 ;
659+ } ;
660+
632661export const applyLocalOnlyChange =
633662 ( deps : SqliteDep & TimeDep & AppOwnerDep ) =>
634663 ( change : MutationChange ) : Result < void , SqliteError > => {
@@ -639,23 +668,16 @@ export const applyLocalOnlyChange =
639668 ` ) ;
640669 if ( ! result . ok ) return result ;
641670 } else {
642- const now = deps . time . nowIso ( ) ;
643671 const ownerId = deps . appOwner . id ;
672+ const columns = dbChangeToColumns ( change , deps . time . nowIso ( ) ) ;
644673
645- let entries = objectToEntries ( change . values ) ;
646- if ( change . isDelete !== null ) {
647- entries = [ ...entries , [ "isDeleted" , sqliteFalse ] ] ;
648- }
649-
650- for ( const [ column , value ] of entries ) {
674+ for ( const [ column , value ] of columns ) {
651675 const result = deps . sqlite . exec ( sql . prepared `
652676 insert into ${ sql . identifier ( change . table ) }
653- ("ownerId", "id", ${ sql . identifier ( column ) } , createdAt, updatedAt )
654- values (${ ownerId } , ${ change . id } , ${ value } , ${ now } , ${ now } )
677+ ("ownerId", "id", ${ sql . identifier ( column ) } )
678+ values (${ ownerId } , ${ change . id } , ${ value } )
655679 on conflict ("ownerId", "id") do update
656- set
657- ${ sql . identifier ( column ) } = ${ value } ,
658- updatedAt = ${ now } ;
680+ set ${ sql . identifier ( column ) } = ${ value } ;
659681 ` ) ;
660682 if ( ! result . ok ) return result ;
661683 }
@@ -681,22 +703,11 @@ const applyMessages =
681703 let { firstTimestamp, lastTimestamp } = usage . value ;
682704
683705 for ( const { timestamp, change } of messages ) {
684- const dateIso = timestampToDateIso ( timestamp ) ;
685706 const timestampBytes = timestampToTimestampBytes ( timestamp ) ;
686707 const idBytes = idToIdBytes ( change . id ) ;
708+ const columns = dbChangeToColumns ( change , timestampToDateIso ( timestamp ) ) ;
687709
688- const values = [ ...objectToEntries ( change . values ) ] ;
689-
690- // SystemColumns are not encoded in change.values.
691- if ( change . isInsert ) {
692- values . push ( [ "createdAt" , dateIso ] ) ;
693- }
694- if ( change . isDelete !== null ) {
695- values . push ( [ "isDeleted" , booleanToSqliteBoolean ( change . isDelete ) ] ) ;
696- }
697- // No `ownerId` and `updatedAt` because they are evolu_history columns.
698-
699- for ( const [ column , value ] of values ) {
710+ for ( const [ column , value ] of columns ) {
700711 const updateAppTable = deps . sqlite . exec ( sql . prepared `
701712 with
702713 existingTimestamp as (
@@ -711,13 +722,11 @@ const applyMessages =
711722 limit 1
712723 )
713724 insert into ${ sql . identifier ( change . table ) }
714- ("ownerId", "id", ${ sql . identifier ( column ) } , "updatedAt" )
715- select ${ ownerId } , ${ change . id } , ${ value } , ${ dateIso }
725+ ("ownerId", "id", ${ sql . identifier ( column ) } )
726+ select ${ ownerId } , ${ change . id } , ${ value }
716727 where not exists (select 1 from existingTimestamp)
717728 on conflict ("ownerId", "id") do update
718- set
719- ${ sql . identifier ( column ) } = ${ value } ,
720- "updatedAt" = ${ dateIso }
729+ set ${ sql . identifier ( column ) } = ${ value }
721730 where not exists (select 1 from existingTimestamp);
722731 ` ) ;
723732
0 commit comments