import { ClientMessage , ClientMessageType , CrdtType , Op , SerializedCrdtWithId , ServerMessage , ServerMessageType , } from "../src/live" ; import { LiveList } from "../src/LiveList" ; import { LiveMap } from "../src/LiveMap" ; import { LiveObject } from "../src/LiveObject" ; import { makePosition } from "../src/position" ; import { createRoom , defaultState , Effects , makeStateMachine , } from "../src/room" ; import { remove } from "../src/utils" ; export function objectToJson ( record ) { const result = { } ; const obj = record . toObject ( ) ; for ( const key in obj ) { result [ key ] = toJson ( obj [ key ] ) ; } return result ; } function listToJson < T > ( list ) { return list . toArray ( ) . map ( toJson ) ; } function mapToJson < TKey extends string , TValue > ( map ) { return Array . from ( map . entries ( ) ) . sort ( ( entryA , entryB ) => entryA [ 0 ] . localeCompare ( entryB [ 0 ] ) ) . map ( ( entry ) => [ entry [ 0 ] , toJson ( entry [ 1 ] ) ] ) ; } function toJson ( value ) { if ( value instanceof LiveObject ) { return objectToJson ( value ) ; } else if ( value instanceof LiveList ) { return listToJson ( value ) ; } else if ( value instanceof LiveMap ) { return mapToJson ( value ) ; } return value ; } export const FIRST_POSITION = makePosition ( ) ; export const SECOND_POSITION = makePosition ( FIRST_POSITION ) ; export const THIRD_POSITION = makePosition ( SECOND_POSITION ) ; export const FOURTH_POSITION = makePosition ( THIRD_POSITION ) ; export const FIFTH_POSITION = makePosition ( FOURTH_POSITION ) ; const defaultContext = { room : "room-id" , authEndpoint : "/api/auth" , throttleDelay : - 1 , liveblocksServer : "wss://live.liveblocks.io" , onError : ( ) => { } , } ; async function prepareRoomWithStorage < T > ( items , actor = 0 , onSend = ( ) => { } ) { const effects = mockEffects ( ) ; ( effects . send as jest . MockedFunction < any > ) . mockImplementation ( onSend ) ; const state = defaultState ( { } ) ; const machine = makeStateMachine ( state , defaultContext , effects ) ; const ws = new MockWebSocket ( "" ) ; machine . connect ( ) ; machine . authenticationSuccess ( { actor } , ws as any ) ; machine . onOpen ( ) ; const getStoragePromise = machine . getStorage < T > ( ) ; const clonedItems = JSON . parse ( JSON . stringify ( items ) ) ; machine . onMessage ( serverMessage ( { type : ServerMessageType . InitialStorageState , items : clonedItems , } ) ) ; const storage = await getStoragePromise ; return { storage , machine , } ; } export async function prepareIsolatedStorageTest < T > ( items , actor = 0 ) { const { machine , storage } = await prepareRoomWithStorage < T > ( items , actor ) ; return { root : storage . root , subscribe : machine . subscribe , assert : ( data ) => expect ( objectToJson ( storage . root ) ) . toEqual ( data ) , applyRemoteOperations : ( ops ) => machine . onMessage ( serverMessage ( { type : ServerMessageType . UpdateStorage , ops , } ) ) , } ; } /**
 * Create 2 rooms with a loaded storage
 * All operations made on the main room are forwarded to the other room
 * Assertion on the storage validate both rooms
 */ export async function prepareStorageTest < T > ( items , actor = 0 ) { const operations = [ ] ; const { machine : refMachine , storage : refStorage } = await prepareRoomWithStorage < T > ( items , - 1 ) ; const { machine , storage } = await prepareRoomWithStorage < T > ( items , actor , ( messages ) => { for ( const message of messages ) { if ( message . type === ClientMessageType . UpdateStorage ) { operations . push ( ... message . ops ) ; refMachine . onMessage ( serverMessage ( { type : ServerMessageType . UpdateStorage , ops : message . ops , } ) ) ; machine . onMessage ( serverMessage ( { type : ServerMessageType . UpdateStorage , ops : message . ops , } ) ) ; } else if ( message . type === ClientMessageType . UpdatePresence ) { refMachine . onMessage ( serverMessage ( { type : ServerMessageType . UpdatePresence , data : message . data , actor : 0 , } ) ) ; } } } ) ; const states = [ ] ; function assert ( data , shouldPushToStates = true ) { if ( shouldPushToStates ) { states . push ( data ) ; } const json = objectToJson ( storage . root ) ; expect ( json ) . toEqual ( data ) ; expect ( objectToJson ( refStorage . root ) ) . toEqual ( data ) ; expect ( machine . getItemsCount ( ) ) . toBe ( refMachine . getItemsCount ( ) ) ; } function assertUndoRedo ( ) { for ( let i = 0 ; i < states . length - 1 ; i ++ ) { machine . undo ( ) ; assert ( states [ states . length - 2 - i ] , false ) ; } for ( let i = 0 ; i < states . length - 1 ; i ++ ) { machine . redo ( ) ; assert ( states [ i + 1 ] , false ) ; } for ( let i = 0 ; i < states . length - 1 ; i ++ ) { machine . undo ( ) ; assert ( states [ states . length - 2 - i ] , false ) ; } } return { operations , storage , refStorage , assert , assertUndoRedo , updatePresence : machine . updatePresence , getUndoStack : machine . getUndoStack , getItemsCount : machine . getItemsCount , subscribe : machine . subscribe , refSubscribe : refMachine . subscribe , batch : machine . batch , undo : machine . undo , redo : machine . redo , applyRemoteOperations : ( ops ) => machine . onMessage ( serverMessage ( { type : ServerMessageType . UpdateStorage , ops , } ) ) , } ; } export function createSerializedObject ( id , data , parentId ? , parentKey ? ) { return [ id , { type : CrdtType . Object , data , parentId , parentKey , } , ] ; } export function createSerializedList ( id , parentId , parentKey ) { return [ id , { type : CrdtType . List , parentId , parentKey , } , ] ; } export function createSerializedMap ( id , parentId , parentKey ) { return [ id , { type : CrdtType . Map , parentId , parentKey , } , ] ; } export function createSerializedRegister ( id , parentId , parentKey , data ) { return [ id , { type : CrdtType . Register , parentId , parentKey , data , } , ] ; } export function mockEffects ( ) { return { authenticate : jest . fn ( ) , delayFlush : jest . fn ( ) , send : jest . fn ( ) , schedulePongTimeout : jest . fn ( ) , startHeartbeatInterval : jest . fn ( ) , scheduleReconnect : jest . fn ( ) , } ; } export function serverMessage ( message ) { return new MessageEvent ( "message" , { data : JSON . stringify ( message ) , } ) ; } export class MockWebSocket { static instances : MockWebSocket [ ] = [ ] ; isMock = true ; callbacks = { open : [ ] as Array < ( event ? : WebSocketEventMap [ "open" ] ) => void > , close : [ ] as Array < ( event ? : WebSocketEventMap [ "close" ] ) => void > , error : [ ] as Array < ( event ? : WebSocketEventMap [ "error" ] ) => void > , message : [ ] as Array < ( event ? : WebSocketEventMap [ "message" ] ) => void > , } ; sentMessages : string [ ] = [ ] ; readyState : number ; constructor ( public url , private onSend = ( ) => { } ) { this . readyState = WebSocket . CLOSED ; MockWebSocket . instances . push ( this ) ; } addEventListener < T extends "open" | "close" | "error" | "message" > ( event , callback ) { this . callbacks [ event ] . push ( callback as any ) ; } removeEventListener < T extends "open" | "close" | "error" | "message" > ( event , callback ) { remove ( this . callbacks [ event ] , callback as any ) ; } send ( message ) { this . sentMessages . push ( message ) ; this . onSend ( message ) ; } close ( ) { } } window . WebSocket = MockWebSocket as any ;