UNPKG

@wordpress/core-data

Version:
8 lines (7 loc) 14.1 kB
{ "version": 3, "sources": ["../../src/awareness/awareness-state.ts"], "sourcesContent": ["/**\n * Internal dependencies\n */\nimport { REMOVAL_DELAY_IN_MS } from './config';\nimport { TypedAwareness } from './typed-awareness';\nimport type { EnhancedState, EqualityFieldCheck } from './types';\nimport { getTypedKeys, areMapsEqual } from './utils';\n\ntype AwarenessClientID = number;\n\ninterface AwarenessStateChange {\n\tadded: AwarenessClientID[];\n\tupdated: AwarenessClientID[];\n\tremoved: AwarenessClientID[];\n}\n\nabstract class AwarenessWithEqualityChecks<\n\tState extends object,\n> extends TypedAwareness< State > {\n\t/** OVERRIDDEN METHODS */\n\n\t/**\n\t * Set a local state field on an awareness document. Calling this method may\n\t * trigger rerenders of any subscribed components.\n\t *\n\t * Equality checks are provided by the abstract `equalityFieldChecks` property.\n\t * @param field - The field to set.\n\t * @param value - The value to set.\n\t */\n\tpublic setLocalStateField< FieldName extends string & keyof State >(\n\t\tfield: FieldName,\n\t\tvalue: State[ FieldName ]\n\t): void {\n\t\tif (\n\t\t\tthis.isFieldEqual(\n\t\t\t\tfield,\n\t\t\t\tvalue,\n\t\t\t\tthis.getLocalStateField( field ) ?? undefined\n\t\t\t)\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tsuper.setLocalStateField( field, value );\n\t}\n\n\t/** ABSTRACT PROPERTIES */\n\n\t/**\n\t * Extending classes must implement equality checks for each awareness state\n\t * field they manage.\n\t */\n\tprotected abstract equalityFieldChecks: {\n\t\t[ FieldName in keyof State ]: EqualityFieldCheck< State, FieldName >;\n\t};\n\n\t/** CUSTOM METHODS */\n\n\t/**\n\t * Determine if a field value has changed using the provided equality checks.\n\t * @param field - The field to check.\n\t * @param value1 - The first value to compare.\n\t * @param value2 - The second value to compare.\n\t */\n\tprotected isFieldEqual< FieldName extends keyof State >(\n\t\tfield: FieldName,\n\t\tvalue1?: State[ FieldName ],\n\t\tvalue2?: State[ FieldName ]\n\t): boolean {\n\t\tif (\n\t\t\t[ 'clientId', 'isConnected', 'isMe' ].includes( field as string )\n\t\t) {\n\t\t\treturn value1 === value2;\n\t\t}\n\n\t\tif ( field in this.equalityFieldChecks ) {\n\t\t\tconst fn = this.equalityFieldChecks[ field ];\n\t\t\treturn fn( value1, value2 );\n\t\t}\n\n\t\tthrow new Error(\n\t\t\t`No equality check implemented for awareness state field \"${ field.toString() }\".`\n\t\t);\n\t}\n\n\t/**\n\t * Determine if two states are equal by comparing each field using the\n\t * provided equality checks.\n\t * @param state1 - The first state to compare.\n\t * @param state2 - The second state to compare.\n\t */\n\tprotected isStateEqual( state1: State, state2: State ): boolean {\n\t\treturn [\n\t\t\t...new Set< keyof State >( [\n\t\t\t\t...getTypedKeys( state1 ),\n\t\t\t\t...getTypedKeys( state2 ),\n\t\t\t] ),\n\t\t].every( ( field ) => {\n\t\t\tconst value1 = state1[ field ];\n\t\t\tconst value2 = state2[ field ];\n\n\t\t\treturn this.isFieldEqual( field, value1, value2 );\n\t\t} );\n\t}\n}\n\n/**\n * Abstract class to manage awareness and allow external code to subscribe to\n * state updates.\n */\nexport abstract class AwarenessState<\n\tState extends object = {},\n> extends AwarenessWithEqualityChecks< State > {\n\t/** CUSTOM PROPERTIES */\n\n\t/**\n\t * Whether the setUp method has been called, to avoid running it multiple\n\t * times.\n\t */\n\tprivate hasSetupRun = false;\n\n\t/**\n\t * We keep track of all seen states during the current session for two reasons:\n\t *\n\t * 1. So that we can represent recently disconnected collaborators in our UI, even\n\t * after they have been removed from the awareness document.\n\t * 2. So that we can provide debug information about all collaborators seen during\n\t * the session.\n\t */\n\tprivate disconnectedCollaborators: Set< number > = new Set();\n\tprivate seenStates: Map< number, State > = new Map();\n\n\t/**\n\t * Hold a snapshot of the previous awareness state allows us to compare the\n\t * state values and avoid unnecessary updates to subscribers.\n\t */\n\tprivate previousSnapshot = new Map< number, EnhancedState< State > >();\n\tprivate stateSubscriptions: Array<\n\t\t( newState: EnhancedState< State >[] ) => void\n\t> = [];\n\n\t/**\n\t * In some cases, we may want to throttle setting local state fields to avoid\n\t * overwhelming the awareness document with rapid updates. At the same time, we\n\t * want to ensure that when we read our own state locally, we get the latest\n\t * value -- even if it hasn't yet been set on the awareness instance.\n\t */\n\tprivate myThrottledState: Partial< State > = {};\n\tprivate throttleTimeouts: Map< string, NodeJS.Timeout > = new Map();\n\n\t/** CUSTOM METHODS */\n\n\t/**\n\t * Set up the awareness state. This method is idempotent and will only run\n\t * once. Subclasses should override `onSetUp()` instead of this method to\n\t * add their own setup logic.\n\t *\n\t * This is defined as a readonly arrow function property to prevent\n\t * subclasses from overriding it.\n\t */\n\tpublic readonly setUp = (): void => {\n\t\tif ( this.hasSetupRun ) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.hasSetupRun = true;\n\n\t\tthis.onSetUp();\n\n\t\tthis.on(\n\t\t\t'change',\n\t\t\t( { added, removed, updated }: AwarenessStateChange ) => {\n\t\t\t\t[ ...added, ...updated ].forEach( ( id ) => {\n\t\t\t\t\tthis.disconnectedCollaborators.delete( id );\n\t\t\t\t} );\n\n\t\t\t\tremoved.forEach( ( id ) => {\n\t\t\t\t\tthis.disconnectedCollaborators.add( id );\n\n\t\t\t\t\tsetTimeout( () => {\n\t\t\t\t\t\tthis.disconnectedCollaborators.delete( id );\n\t\t\t\t\t\tthis.updateSubscribers( true /* force update */ );\n\t\t\t\t\t}, REMOVAL_DELAY_IN_MS );\n\t\t\t\t} );\n\n\t\t\t\t// Do not force-update the store here, since this change handler can be\n\t\t\t\t// called even when there are no actual state changes.\n\t\t\t\tthis.updateSubscribers();\n\t\t\t}\n\t\t);\n\t};\n\n\t/**\n\t * Hook method for subclasses to add their own setup logic. This is called\n\t * once after the base class setup completes. All subclasses must implement\n\t * this method. If extending a class that already implements `onSetUp()`,\n\t * call `super.onSetUp()` to ensure parent setup runs.\n\t */\n\tprotected abstract onSetUp(): void;\n\n\t/**\n\t * Get the most recent state from the last processed change event.\n\t *\n\t * @return An array of EnhancedState< State >.\n\t */\n\tpublic getCurrentState(): EnhancedState< State >[] {\n\t\treturn Array.from( this.previousSnapshot.values() );\n\t}\n\n\t/**\n\t * Get all seen states in this session to enable debug reporting.\n\t */\n\tpublic getSeenStates(): Map< number, State > {\n\t\treturn this.seenStates;\n\t}\n\n\t/**\n\t * Allow external code to subscribe to awareness state changes.\n\t * @param callback - The callback to subscribe to.\n\t */\n\tpublic onStateChange(\n\t\tcallback: ( newState: EnhancedState< State >[] ) => void\n\t): () => void {\n\t\tthis.stateSubscriptions.push( callback );\n\n\t\treturn () => {\n\t\t\tthis.stateSubscriptions = this.stateSubscriptions.filter(\n\t\t\t\t( cb ) => cb !== callback\n\t\t\t);\n\t\t};\n\t}\n\n\t/**\n\t * Set a local state field on an awareness document with throttle. See caveats\n\t * of this.setLocalStateField.\n\t * @param field - The field to set.\n\t * @param value - The value to set.\n\t * @param wait - The wait time in milliseconds.\n\t */\n\tpublic setThrottledLocalStateField<\n\t\tFieldName extends string & keyof State,\n\t>( field: FieldName, value: State[ FieldName ], wait: number ): void {\n\t\tthis.setLocalStateField( field, value );\n\n\t\tthis.throttleTimeouts.set(\n\t\t\tfield,\n\t\t\tsetTimeout( () => {\n\t\t\t\tthis.throttleTimeouts.delete( field );\n\t\t\t\tif ( this.myThrottledState[ field ] ) {\n\t\t\t\t\tthis.setLocalStateField(\n\t\t\t\t\t\tfield,\n\t\t\t\t\t\tthis.myThrottledState[ field ]\n\t\t\t\t\t);\n\n\t\t\t\t\tdelete this.myThrottledState[ field ];\n\t\t\t\t}\n\t\t\t}, wait )\n\t\t);\n\t}\n\n\t/**\n\t * Set the current collaborator's connection status as awareness state.\n\t * @param isConnected - The connection status.\n\t */\n\tpublic setConnectionStatus( isConnected: boolean ): void {\n\t\tif ( isConnected ) {\n\t\t\tthis.disconnectedCollaborators.delete( this.clientID );\n\t\t} else {\n\t\t\tthis.disconnectedCollaborators.add( this.clientID );\n\t\t}\n\n\t\tthis.updateSubscribers( true /* force update */ );\n\t}\n\n\t/**\n\t * Update all subscribed listeners with the latest awareness state.\n\t * @param forceUpdate - Whether to force an update.\n\t */\n\tprotected updateSubscribers( forceUpdate = false ): void {\n\t\tif ( ! this.stateSubscriptions.length ) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst states = this.getStates();\n\n\t\tthis.seenStates = new Map< number, State >( [\n\t\t\t...this.seenStates.entries(),\n\t\t\t...states.entries(),\n\t\t] );\n\n\t\tconst updatedStates = new Map< number, EnhancedState< State > >(\n\t\t\t[ ...this.disconnectedCollaborators, ...states.keys() ]\n\t\t\t\t.filter( ( clientId ) => {\n\t\t\t\t\t// Exclude any collaborators with empty awareness state. This can happen from\n\t\t\t\t\t// the Yjs inspector.\n\t\t\t\t\treturn (\n\t\t\t\t\t\tObject.keys( this.seenStates.get( clientId ) ?? {} )\n\t\t\t\t\t\t\t.length > 0\n\t\t\t\t\t);\n\t\t\t\t} )\n\t\t\t\t.map( ( clientId ) => {\n\t\t\t\t\t// The filter above ensures that seenStates has the clientId.\n\t\t\t\t\tconst rawState: State = this.seenStates.get( clientId )!;\n\n\t\t\t\t\tconst isConnected =\n\t\t\t\t\t\t! this.disconnectedCollaborators.has( clientId );\n\t\t\t\t\tconst isMe = clientId === this.clientID;\n\t\t\t\t\tconst myState: Partial< State > = isMe\n\t\t\t\t\t\t? this.myThrottledState\n\t\t\t\t\t\t: {};\n\t\t\t\t\tconst state: EnhancedState< State > = {\n\t\t\t\t\t\t...rawState,\n\t\t\t\t\t\t...myState,\n\t\t\t\t\t\tclientId,\n\t\t\t\t\t\tisConnected,\n\t\t\t\t\t\tisMe,\n\t\t\t\t\t};\n\n\t\t\t\t\treturn [ clientId, state ];\n\t\t\t\t} )\n\t\t);\n\n\t\tif ( ! forceUpdate ) {\n\t\t\tif (\n\t\t\t\tareMapsEqual(\n\t\t\t\t\tthis.previousSnapshot,\n\t\t\t\t\tupdatedStates,\n\t\t\t\t\tthis.isStateEqual.bind( this )\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\t// Awareness state unchanged, do not update subscribers.\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// Update subscribers.\n\t\tthis.previousSnapshot = updatedStates;\n\t\tthis.stateSubscriptions.forEach( ( callback ) => {\n\t\t\tcallback( Array.from( updatedStates.values() ) );\n\t\t} );\n\t}\n}\n"], "mappings": "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", "names": [] }