import { IFluidHandle } from "@fluidframework/core-interfaces" ; import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions" ; import { IFluidSerializer , makeHandlesSerializable , parseHandles , ValueType } from "@fluidframework/shared-object-base" ; import { assert , TypedEventEmitter } from "@fluidframework/common-utils" ; import { ILocalValue , LocalValueMaker , makeSerializable , ValueTypeLocalValue , } from "./localValues" ; import { ISerializableValue , ISerializedValue , IValueChanged , IValueOpEmitter , IValueType , IValueTypeCreator , IValueTypeOperationValue , ISharedMapEvents , } from "./mapKernelInterfaces" ; /**
 * Defines the means to process and submit a given op on a map.
 */ interface IMapMessageHandler { param op param local param message param localOpMetadata process ( op , local , message , localOpMetadata ) : void ; param op param localOpMetadata submit ( op , localOpMetadata ) : void ; getStashedOpLocalMetadata ( op ) : unknown ; } /**
 * Describes an operation specific to a value type.
 */ export interface IMapValueTypeOperation { /**
     * String identifier of the operation type.
     */ type : "act" ; /**
     * Map key being modified.
     */ key : string ; alpha value : IValueTypeOperationValue ; } /**
 * Operation indicating a value should be set for a key.
 */ export interface IMapSetOperation { /**
     * String identifier of the operation type.
     */ type : "set" ; /**
     * Map key being modified.
     */ key : string ; /**
     * Value to be set on the key.
     */ value : ISerializableValue ; } /**
 * Operation indicating a key should be deleted from the map.
 */ export interface IMapDeleteOperation { /**
     * String identifier of the operation type.
     */ type : "delete" ; /**
     * Map key being modified.
     */ key : string ; } /**
 * Map key operations are one of several types.
 */ export type IMapKeyOperation = IMapValueTypeOperation | IMapSetOperation | IMapDeleteOperation ; /**
 * Operation indicating the map should be cleared.
 */ export interface IMapClearOperation { /**
     * String identifier of the operation type.
     */ type : "clear" ; } /**
 * Description of a map delta operation
 */ export type IMapOperation = IMapKeyOperation | IMapClearOperation ; /**
 * Defines the in-memory object structure to be used for the conversion to/from serialized.
 * Directly used in JSON.stringify, direct result from JSON.parse
 */ export interface IMapDataObjectSerializable { [ key ] : ISerializableValue ; } export interface IMapDataObjectSerialized { [ key ] : ISerializedValue ; } /**
 * A SharedMap is a map-like distributed data structure.
 */ export class MapKernel implements IValueTypeCreator { /**
     * The number of key/value pairs stored in the map.
     */ public get size ( ) : number { return this . data . size ; } /**
     * Mapping of op types to message handlers.
     */ private readonly messageHandlers : ReadonlyMap < string , IMapMessageHandler > = new Map ( ) ; /**
     * The in-memory data the map is storing.
     */ private readonly data = new Map < string , ILocalValue > ( ) ; /**
     * Keys that have been modified locally but not yet ack'd from the server.
     */ private readonly pendingKeys : Map < string , number > = new Map ( ) ; /**
     * This is used to assign a unique id to every outgoing operation and helps in tracking unack'd ops.
     */ private pendingMessageId : number = - 1 ; /**
     * If a clear has been performed locally but not yet ack'd from the server, then this stores the pending id
     * of that clear operation. Otherwise, is -1.
     */ private pendingClearMessageId : number = - 1 ; /**
     * Object to create encapsulations of the values stored in the map.
     */ private readonly localValueMaker : LocalValueMaker ; param serializer param handle param submitMessage param isAttached param valueTypes param eventEmitter constructor ( private readonly serializer , private readonly handle , private readonly submitMessage , private readonly isAttached , valueTypes , public readonly eventEmitter = new TypedEventEmitter < ISharedMapEvents > ( ) ) { this . localValueMaker = new LocalValueMaker ( serializer ) ; this . messageHandlers = this . getMessageHandlers ( ) ; for ( const type of valueTypes ) { this . localValueMaker . registerValueType ( type ) ; } } returns public keys ( ) { return this . data . keys ( ) ; } returns public entries ( ) { const localEntriesIterator = this . data . entries ( ) ; const iterator = { next ( ) { const nextVal = localEntriesIterator . next ( ) ; if ( nextVal . done ) { return { value : undefined , done : true } ; } else { return { value : [ nextVal . value [ 0 ] , nextVal . value [ 1 ] . value ] , done : false } ; } } , [ Symbol . iterator ] ( ) { return this ; } } ; return iterator ; } returns public values ( ) { const localValuesIterator = this . data . values ( ) ; const iterator = { next ( ) { const nextVal = localValuesIterator . next ( ) ; if ( nextVal . done ) { return { value : undefined , done : true } ; } else { return { value : nextVal . value . value , done : false } ; } } , [ Symbol . iterator ] ( ) { return this ; } } ; return iterator ; } returns public [ Symbol . iterator ] ( ) { return this . entries ( ) ; } param callbackFn public forEach ( callbackFn ) { this . data . forEach ( ( localValue , key , m ) => { callbackFn ( localValue . value , key , m ) ; } ) ; } /**
     * {@inheritDoc ISharedMap.get}
     */ public get < T = any > ( key ) { if ( ! this . data . has ( key ) ) { return undefined ; } const localValue = this . data . get ( key ) ! ; return localValue . value as T ; } /**
     * {@inheritDoc ISharedMap.wait}
     */ public async wait < T = any > ( key ) { if ( this . has ( key ) ) { return this . get < T > ( key ) ! ; } return new Promise < T > ( ( resolve ) => { const callback = ( changed ) => { if ( key === changed . key ) { resolve ( this . get < T > ( changed . key ) ! ) ; this . eventEmitter . removeListener ( "valueChanged" , callback ) ; } } ; this . eventEmitter . on ( "valueChanged" , callback ) ; } ) ; } param key returns public has ( key ) { return this . data . has ( key ) ; } /**
     * {@inheritDoc ISharedMap.set}
     */ public set ( key , value ) { if ( key === undefined || key === null ) { throw new Error ( "Undefined and null keys are not supported" ) ; } const localValue = this . localValueMaker . fromInMemory ( value ) ; const serializableValue = makeSerializable ( localValue , this . serializer , this . handle ) ; this . setCore ( key , localValue , true , undefined ) ; if ( ! this . isAttached ( ) ) { return ; } const op = { key , type : "set" , value : serializableValue } ; this . submitMapKeyMessage ( op ) ; } /**
     * {@inheritDoc IValueTypeCreator.createValueType}
     */ public createValueType ( key , type , params ) { const localValue = this . localValueMaker . makeValueType ( type , this . makeMapValueOpEmitter ( key ) , params ) ; const transformedValue = makeHandlesSerializable ( params , this . serializer , this . handle ) ; this . setCore ( key , localValue , true , undefined ) ; if ( ! this . isAttached ( ) ) { return this ; } const serializableValue = { type , value : transformedValue } ; const op = { key , type : "set" , value : serializableValue } ; this . submitMapKeyMessage ( op ) ; return this ; } param key returns public delete ( key ) { const successfullyRemoved = this . deleteCore ( key , true , undefined ) ; if ( ! this . isAttached ( ) ) { return successfullyRemoved ; } const op = { key , type : "delete" } ; this . submitMapKeyMessage ( op ) ; return successfullyRemoved ; } /**
     * Clear all data from the map.
     */ public clear ( ) { this . clearCore ( true , undefined ) ; if ( ! this . isAttached ( ) ) { return ; } const op = { type : "clear" } ; this . submitMapClearMessage ( op ) ; } param serializer returns public getSerializedStorage ( serializer ) { const serializableMapData = { } ; this . data . forEach ( ( localValue , key ) => { serializableMapData [ key ] = localValue . makeSerialized ( serializer , this . handle ) ; } ) ; return serializableMapData ; } public getSerializableStorage ( serializer ) { const serializableMapData = { } ; this . data . forEach ( ( localValue , key ) => { serializableMapData [ key ] = makeSerializable ( localValue , serializer , this . handle ) ; } ) ; return serializableMapData ; } public serialize ( serializer ) { return JSON . stringify ( this . getSerializableStorage ( serializer ) ) ; } param data public populateFromSerializable ( json ) { for ( const [ key , serializable ] of Object . entries ( json ) ) { const localValue = { key , value : this . makeLocal ( key , serializable ) } ; this . data . set ( localValue . key , localValue . value ) ; } } public populate ( json ) { this . populateFromSerializable ( JSON . parse ( json ) as IMapDataObjectSerializable ) ; } param op param localOpMetadata returns public trySubmitMessage ( op , localOpMetadata ) { const type = op . type ; if ( this . messageHandlers . has ( type ) ) { this . messageHandlers . get ( type ) ! . submit ( op as IMapOperation , localOpMetadata ) ; return true ; } return false ; } public tryGetStashedOpLocalMetadata ( op ) { const type = op . type ; if ( this . messageHandlers . has ( type ) ) { return this . messageHandlers . get ( type ) ! . getStashedOpLocalMetadata ( op as IMapOperation ) ; } throw new Error ( "no apply stashed op handler" ) ; } param message param local param localOpMetadata returns public tryProcessMessage ( op , local , message , localOpMetadata ) { if ( this . messageHandlers . has ( op . type ) ) { this . messageHandlers . get ( op . type ) ! . process ( op , local , message , localOpMetadata ) ; return true ; } return false ; } param key param value param local param op private setCore ( key , value , local , op ) { const previousValue = this . get ( key ) ; this . data . set ( key , value ) ; const event = { key , previousValue } ; this . eventEmitter . emit ( "valueChanged" , event , local , op , this . eventEmitter ) ; } param local param op private clearCore ( local , op ) { this . data . clear ( ) ; this . eventEmitter . emit ( "clear" , local , op , this . eventEmitter ) ; } param key param local param op returns private deleteCore ( key , local , op ) { const previousValue = this . get ( key ) ; const successfullyRemoved = this . data . delete ( key ) ; if ( successfullyRemoved ) { const event = { key , previousValue } ; this . eventEmitter . emit ( "valueChanged" , event , local , op , this . eventEmitter ) ; } return successfullyRemoved ; } /**
     * Clear all keys in memory in response to a remote clear, but retain keys we have modified but not yet been ack'd.
     */ private clearExceptPendingKeys ( ) { const temp = new Map < string , ILocalValue > ( ) ; this . pendingKeys . forEach ( ( value , key ) => { temp . set ( key , this . data . get ( key ) ! ) ; } ) ; this . data . clear ( ) ; temp . forEach ( ( value , key ) => { this . data . set ( key , value ) ; } ) ; } param key param serializable returns private makeLocal ( key , serializable ) { if ( serializable . type === ValueType [ ValueType . Plain ] || serializable . type === ValueType [ ValueType . Shared ] ) { return this . localValueMaker . fromSerializable ( serializable ) ; } else { return this . localValueMaker . fromSerializableValueType ( serializable , this . makeMapValueOpEmitter ( key ) ) ; } } param op param local param message param localOpMetadata returns private needProcessKeyOperation ( op , local , localOpMetadata ) { if ( this . pendingClearMessageId !== - 1 ) { if ( local ) { assert ( localOpMetadata !== undefined && localOpMetadata as number < this . pendingClearMessageId , 0x1f1 ) ; } return false ; } if ( this . pendingKeys . has ( op . key ) ) { if ( local ) { assert ( localOpMetadata !== undefined , 0x1f2 ) ; const pendingMessageId = localOpMetadata as number ; const pendingKeyMessageId = this . pendingKeys . get ( op . key ) ; if ( pendingKeyMessageId === pendingMessageId ) { this . pendingKeys . delete ( op . key ) ; } } return false ; } return ! local ; } returns private getMessageHandlers ( ) { const messageHandlers = new Map < string , IMapMessageHandler > ( ) ; messageHandlers . set ( "clear" , { process : ( op , local , message , localOpMetadata ) => { if ( local ) { assert ( localOpMetadata !== undefined , 0x1f3 ) ; const pendingMessageId = localOpMetadata as number ; if ( this . pendingClearMessageId === pendingMessageId ) { this . pendingClearMessageId = - 1 ; } return ; } if ( this . pendingKeys . size !== 0 ) { this . clearExceptPendingKeys ( ) ; return ; } this . clearCore ( local , message ) ; } , submit : ( op , localOpMetadata ) => { this . submitMapClearMessage ( op ) ; } , getStashedOpLocalMetadata : ( op ) => { return this . getMapClearMessageLocalMetadata ( op ) ; } } ) ; messageHandlers . set ( "delete" , { process : ( op , local , message , localOpMetadata ) => { if ( ! this . needProcessKeyOperation ( op , local , localOpMetadata ) ) { return ; } this . deleteCore ( op . key , local , message ) ; } , submit : ( op , localOpMetadata ) => { this . submitMapKeyMessage ( op ) ; } , getStashedOpLocalMetadata : ( op ) => { return this . getMapKeyMessageLocalMetadata ( op ) ; } } ) ; messageHandlers . set ( "set" , { process : ( op , local , message , localOpMetadata ) => { if ( ! this . needProcessKeyOperation ( op , local , localOpMetadata ) ) { return ; } const context = this . makeLocal ( op . key , op . value ) ; this . setCore ( op . key , context , local , message ) ; } , submit : ( op , localOpMetadata ) => { this . submitMapKeyMessage ( op ) ; } , getStashedOpLocalMetadata : ( op ) => { return this . getMapKeyMessageLocalMetadata ( op ) ; } } ) ; messageHandlers . set ( "act" , { process : ( op , local , message , localOpMetadata ) => { const localValue = this . data . get ( op . key ) as ValueTypeLocalValue ; if ( ! localValue ) { return ; } const handler = localValue . getOpHandler ( op . value . opName ) ; const previousValue = localValue . value ; const translatedValue = parseHandles ( op . value . value , this . serializer ) ; handler . process ( previousValue , translatedValue , local , message ) ; const event = { key : op . key , previousValue } ; this . eventEmitter . emit ( "valueChanged" , event , local , message , this . eventEmitter ) ; } , submit : ( op , localOpMetadata ) => { this . submitMessage ( op , localOpMetadata ) ; } , getStashedOpLocalMetadata : ( op ) => { assert ( false , 0x016 ) ; } } ) ; return messageHandlers ; } private getMapClearMessageLocalMetadata ( op ) { const pendingMessageId = ++ this . pendingMessageId ; this . pendingClearMessageId = pendingMessageId ; return pendingMessageId ; } param op private submitMapClearMessage ( op ) { const pendingMessageId = this . getMapClearMessageLocalMetadata ( op ) ; this . submitMessage ( op , pendingMessageId ) ; } private getMapKeyMessageLocalMetadata ( op ) { const pendingMessageId = ++ this . pendingMessageId ; this . pendingKeys . set ( op . key , pendingMessageId ) ; return pendingMessageId ; } param op private submitMapKeyMessage ( op ) { const pendingMessageId = this . getMapKeyMessageLocalMetadata ( op ) ; this . submitMessage ( op , pendingMessageId ) ; } alpha param key returns private makeMapValueOpEmitter ( key ) { const emit = ( opName , previousValue , params ) => { const translatedParams = makeHandlesSerializable ( params , this . serializer , this . handle ) ; const op = { key , type : "act" , value : { opName , value : translatedParams } } ; this . submitMessage ( op , undefined ) ; const event = { key , previousValue } ; this . eventEmitter . emit ( "valueChanged" , event , true , null , this . eventEmitter ) ; } ; return { emit } ; } }