import { each , IIterator , map , toArray , toObject , iterItems } from '@lumino/algorithm' ; import { LinkedList } from '@lumino/collections' ; import { UUID } from '@lumino/coreutils' ; import { Datastore , Schema , Record , validateSchema } from '@lumino/datastore' ; import { IMessageHandler , Message , MessageLoop , ConflatableMessage } from '@lumino/messaging' ; import { ISignal , Signal } from '@lumino/signaling' ; import { IDatastore , ITable } from '../interface' ; import { UndoStack } from './stack' ; import { Table } from './table' ; /**
 * An in-memory only store, with stack-based (LIFO) undo/redo history.
 */ export class HistoryStore implements IDatastore , IMessageHandler { static create ( options ) { let { schemas , maxHistory } = options ; Private . validateSchemas ( schemas ) ; let context = { inTransaction : false , transactionId : '' , version : 0 , storeId : 0 , change : { } , } ; let tables = { } as { [ key ] : Table < Schema > ; } ; if ( options . restoreState ) { let state = JSON . parse ( options . restoreState ) ; each ( schemas , s => { tables [ s . id ] = Table . recreate ( s , context , state [ s . id ] || [ ] ) ; } ) ; } else { each ( schemas , s => { tables [ s . id ] = Table . create ( s , context ) ; } ) ; } return new HistoryStore ( context , tables , maxHistory ) ; } /**
     * Dispose of the resources held by the datastore.
     */ dispose ( ) { if ( this . _disposed ) { return ; } this . _disposed = true ; Signal . clearData ( this ) ; } /**
     * Whether the datastore has been disposed.
     */ get isDisposed ( ) : boolean { return this . _disposed ; } /**
     * A signal emitted when changes are made to the store.
     *
     * #### Notes
     * This signal is emitted either at the end of a local mutation,
     * or after a remote mutation has been applied. The storeId can
     * be used to determine its source.
     *
     * The payload represents the set of local changes that were made
     * to bring the store to its current state.
     */ get changed ( ) : ISignal < IDatastore , Datastore . IChangedArgs > { return this . _changed ; } /**
     * The unique id of the store.
     *
     * #### Notes
     * The id is unique among all other collaborating peers.
     */ get id ( ) : number { return 0 ; } /**
     * Whether a transaction is currently in progress.
     */ get inTransaction ( ) : boolean { return this . _context . inTransaction ; } /**
     * The current version of the datastore.
     *
     * #### Notes
     * This version is automatically increased for each transaction
     * to the store. However, it might not increase linearly (i.e.
     * it might make jumps).
     */ get version ( ) : number { return this . _context . version ; } returns iter ( ) { return map ( Object . keys ( this . _tables ) , key => this . _tables [ key ] ) ; } param schema returns throws get < S extends Schema > ( schema ) { let t = this . _tables [ schema . id ] ; if ( t === undefined ) { throw new Error ( `No table found for schema with id: ${ schema . id }` ) ; } return t as Table < S > ; } returns throws beginTransaction ( ) { let newVersion = this . _context . version + 1 ; let id = UUID . uuid4 ( ) ; this . _initTransaction ( id , newVersion ) ; MessageLoop . postMessage ( this , new ConflatableMessage ( 'transaction-begun' ) ) ; return id ; } /**
     * Completes a transaction.
     *
     * #### Notes
     * This completes a transaction previously started with
     * `beginTransaction`. If a change has occurred, the
     * `changed` signal will be emitted.
     */ endTransaction ( ) { this . _finalizeTransaction ( ) ; let { change , transactionId } = this . _context ; if ( ! Private . isChangeEmpty ( change ) ) { const args = { storeId : 0 , transactionId , type : 'transaction' , change , } ; this . _historyStack . push ( args ) ; this . _changed . emit ( args ) ; } } /**
     * Handle a message.
     */ processMessage ( msg ) { switch ( msg . type ) { case 'transaction-begun' : if ( this . _context . inTransaction ) { console . warn ( `Automatically ending transaction (did you forget to end it?): ${ this . _context . transactionId }` ) ; this . endTransaction ( ) ; } break ; case 'queued-transaction' : this . _processQueue ( ) ; break ; default : break ; } } param transactionId returns throws undo ( transactionId ) { const target = this . _historyStack . previous ; if ( target === undefined ) { throw new Error ( 'No actions to undo' ) ; } else if ( target . transactionId !== transactionId ) { throw new Error ( 'Can only undo the latest action' ) ; } const change = this . _historyStack . undo ( ) ; this . _processUndoRedo ( change , 'undo' ) ; return Promise . resolve ( ) ; } param transactionId returns throws redo ( transactionId ) { const target = this . _historyStack . next ; if ( target === undefined ) { throw new Error ( 'No actions to redo' ) ; } else if ( target . transactionId !== transactionId ) { throw new Error ( 'Can only redo the previously undone action' ) ; } const change = this . _historyStack . redo ( ) ; this . _processUndoRedo ( change , 'redo' ) ; return Promise . resolve ( ) ; } returns toString ( ) { return JSON . stringify ( toObject ( map ( this , ( table ) : [ string , Record < Schema > [ ] ] => { return [ table . schema . id , toArray ( table ) ] ; } ) ) ) ; } param id param tables private constructor ( context , tables , maxHistory ? ) { this . _context = context ; this . _tables = tables ; this . _historyStack = new UndoStack ( maxHistory ) ; } /**
     * Apply an undo/redo to the datastore.
     *
     * #### Notes
     * If changes are made, the `changed` signal will be emitted.
     */ private _processUndoRedo ( change , type ) { const { transactionId } = change ; try { this . _initTransaction ( transactionId , this . _context . version ) ; } catch ( e ) { this . _queueUndoRedo ( change , type ) ; return ; } let resultingChange = { } ; try { each ( iterItems ( change . change ) , ( [ schemaId , tablePatch ] ) => { let table = this . _tables [ schemaId ] ; if ( table === undefined ) { console . warn ( `Missing table for schema id '${ schemaId }'` ) ; this . _finalizeTransaction ( ) ; return ; } if ( type === 'redo' ) { resultingChange [ schemaId ] = Table . patch ( table , tablePatch ) ; } else if ( type === 'undo' ) { resultingChange [ schemaId ] = Table . unpatch ( table , tablePatch ) ; } } ) ; } finally { this . _finalizeTransaction ( ) ; } if ( ! Private . isChangeEmpty ( resultingChange ) ) { this . _changed . emit ( { storeId : 0 , transactionId , type , change : resultingChange , } ) ; } } param transaction private _queueUndoRedo ( change , type ) { this . _undoQueue . addLast ( [ change , type ] ) ; MessageLoop . postMessage ( this , new ConflatableMessage ( 'queued-transaction' ) ) ; } /**
     * Process all transactions currently queued.
     */ private _processQueue ( ) { let queue = this . _undoQueue ; if ( queue . isEmpty ) { return ; } let sentinel = { } ; queue . addLast ( sentinel as any ) ; while ( true ) { let [ change , type ] = queue . removeFirst ( ) ! ; if ( change === sentinel ) { return ; } this . _processUndoRedo ( change , type ) ; } } param id param newVersion throws private _initTransaction ( id , newVersion ) { let context = this . _context as Private . MutableContext ; if ( context . inTransaction ) { throw new Error ( `Already in a transaction: ${ this . _context . transactionId }` ) ; } context . inTransaction = true ; context . change = { } ; context . transactionId = id ; context . version = newVersion ; } throws private _finalizeTransaction ( ) { let context = this . _context as Private . MutableContext ; if ( ! context . inTransaction ) { throw new Error ( 'No transaction in progress.' ) ; } context . inTransaction = false ; } private _disposed = false ; private _tables : { [ key ] : Table < Schema > ; } ; private _context : HistoryStore . Context ; private _changed = new Signal < IDatastore , Datastore . IChangedArgs > ( this ) ; private _historyStack : UndoStack < IDatastore . IChangedArgs > ; private _undoQueue = new LinkedList < [ IDatastore . IChangedArgs , HistoryStore . UndoType ] > ( ) ; } export namespace HistoryStore { export type Context = Readonly < Private . MutableContext > ; export interface IOptions { /**
         * The table schemas of the datastore.
         */ schemas : ReadonlyArray < Schema > ; /**
         * Initialize the state to a previously serialized one.
         */ restoreState ? : string ; /**
         * The maximum number of history entries to store.
         */ maxHistory ? : number ; } export type UndoType = 'undo' | 'redo' ; } namespace Private { /**
     * Validates all schemas, and throws an error if any are invalid.
     */ export function validateSchemas ( schemas ) { let errors = [ ] ; for ( let s of schemas ) { let err = validateSchema ( s ) ; if ( err . length ) { errors . push ( `Schema '${ s . id }' validation failed: \n${ err . join ( '\n' ) }` ) ; } } if ( errors . length ) { throw new Error ( errors . join ( '\n\n' ) ) ; } } export type MutableContext = { /**
         * Whether the datastore currently in a transaction.
         */ inTransaction : boolean ; /**
         * The id of the current transaction.
         */ transactionId : string ; /**
         * The current version of the datastore.
         */ version : number ; /**
         * The unique id of the datastore.
         */ storeId : number ; /**
         * The current change object of the transaction.
         */ change : Datastore . MutableChange ; } ; /**
     * Checks if a change is empty.
     */ export function isChangeEmpty ( change ) { return Object . keys ( change ) . length === 0 ; } }