phetsims / tandem

Simulation-side code for PhET-iO
MIT License
0 stars 5 forks source link

Transform IOType into a type alias, which can be implemented statically/non-statically by core classes #275

Closed samreid closed 1 year ago

samreid commented 1 year ago

Starting with https://github.com/phetsims/tandem/issues/263 and its side issues https://github.com/phetsims/tandem/issues/274 and https://github.com/phetsims/tandem/issues/273, and related to https://github.com/phetsims/tandem/issues/261, I would like to experiment with combining IO Type information into the core classes. Now that we have TypeScript, this can be accomplished by:

export const MyClass: StaticInterface = class implements InstanceInterface {
}

This is also partially in hopes to address the dev team's expression of difficulty in using IO Types. @pixelzoom and @jbphet mentioned difficulty with IO Types yesterday, and @zepumph and I have previously had discussions about whether IO Types should be more combined with the core types.

Here are the known pros and cons of the proposed approach:

Pros:

Cons:

pixelzoom commented 1 year ago
  • Marking IOType as an interface

Why should IOType be an interface, rather than a type?

In https://github.com/phetsims/chipper/issues/1280, https://github.com/phetsims/phet-info/issues/200, we concluded that PhET should prefer type over interface. Also in that issue, we converted many (all?) interfaces to types. See also https://github.com/phetsims/phet-info/issues/200.

samreid commented 1 year ago

Thanks, I updated the title and text.

samreid commented 1 year ago

To support https://github.com/phetsims/tandem/issues/275, how can I say that an abstract class has a subclass that returns instances that match an interface? So far I have:

type HasName = { getName: () => string };

export type CreatesHasName = { new( ...args: IntentionalAny[] ): HasName; };

class Person implements HasName { public constructor() {

// blank

}

public getName(): string { return 'Me!!'; } }

const P: CreatesHasName = Person;

console.log( P ); This all works great, but if I mark class Person as abstract, then I get this error:

TS2322: Type ‘typeof Person’ is not assignable to type ‘CreatesHasName’. Cannot assign an abstract constructor type to a non-abstract constructor type.

Maybe this will do it? https://stackoverflow.com/questions/36886082/abstract-constructor-type-in-typescript

export type CreatesHasName = abstract new( ...args: IntentionalAny[] ) => HasName;
samreid commented 1 year ago

What is preferable, and least-surprise. Type parameters always appear in the same order? Or each set of type parameters makes the most sense for its case? Which is preferable, Directory1 or Directory2?

type GuidanceCounselor<Person, Career> = {
  getCareer: ( p: Person ) => Career;
};

type Directory1<Person, Career> = {
  getPerson: ( c: Career ) => Person;
};

type Directory2<Career, Person> = {
  getPerson: ( c: Career ) => Person;
};

@pixelzoom replied:

Whatever makes sense for the thing being parameterized. In your example, I can’t tell if there’s any relationship between GuidanceCounselor and Directory. If there’s no relationship, then the order of Directory parameters should definitely not be affected by the order of GuidanceCounselor parameters. If there is a relationship, I still think it’s preferable to use what is most appropriate for each class. For Directory1 and Directory2 specifically, I have a slight preference for Directory1, because a Person has a Career. I don’t think it’s significant (or necessary) that the left-to-right order of generic params matches their left-to-right use by getPerson. There could be other things that are parameterized in those classes that doesn’t match that order.

samreid commented 1 year ago

Current progress, IOType looks like this:

// Copyright 2020-2022, University of Colorado Boulder

/**
 * IO Types form a synthetic type system used to describe PhET-iO Elements. A PhET-iO Element is an instrumented PhetioObject
 * that is interoperable from the "wrapper" frame (outside the sim frame). An IO Type includes documentation, methods,
 * names, serialization, etc.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 */

import TandemConstants, { PhetioObjectMetadata } from '../TandemConstants.js';
import IntentionalAny from '../../../phet-core/js/types/IntentionalAny.js';

export type IOTypeMethod = {
  returnType: IOType;
  parameterTypes: IOType[];

  //the function to execute when this method is called. This function's parameters will be based on `parameterTypes`,
  // and should return the type specified by `returnType`
  implementation: ( ...args: IntentionalAny[] ) => unknown;
  documentation: string;

  // by default, all methods are invocable for all elements. However, for some read-only elements, certain methods
  // should not be invocable. In that case, they are marked as invocableForReadOnlyElements: false.
  invocableForReadOnlyElements?: boolean;
};

type IOType = {

  typeName: string;

  supertype?: IOType | null;
  events?: string[];
  dataDefaults?: Record<string, unknown>;
  metadataDefaults?: Partial<PhetioObjectMetadata>;
  documentation?: string;
  methods?: Record<string, IOTypeMethod>;
  methodOrder?: string[];
  parameterTypes?: IOType[];
  isFunctionType?: boolean;
};
export default IOType;

export const ObjectIO: IOType = {
  typeName: TandemConstants.OBJECT_IO_TYPE_NAME,
  supertype: null,
  documentation: 'The root of the IO Type hierarchy',
  metadataDefaults: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS
};

// Static methods
export type FromStateObject<T, S> = { fromStateObject: ( s: S ) => T };
export type StateToArgsForConstructor<S> = { stateToArgsForConstructor: ( s: S ) => unknown[] }; // TODO: instead of unknown this is the second parameter type for PhetioDynamicElementContainer. How? https://github.com/phetsims/tandem/issues/261
export type HasStateSchema = {
  stateSchema: Record<string, IOType>;
};

// These methods appear on the instance
export type ToStateObject<T> = { toStateObject: () => T };
export type ApplyState<S> = { applyState: ( s: S ) => void };
export type AddChildElement<T, S> = { addChildElement: ( componentName: string, s: S ) => T };

export type ConstructsToStateObject<T, S> = abstract new( ...args: IntentionalAny[] ) => ToStateObject<S>;
export type ConstructsApplyState<T, S> = abstract new( ...args: IntentionalAny[] ) => ApplyState<S>;
export type ConstructsAddChildElement<T, S> = abstract new( ...args: IntentionalAny[] ) => AddChildElement<T, S>;

export type ReferenceIOType<T, S> = ConstructsToStateObject<T, S> & ConstructsApplyState<T, S> & IOType;

And LayersModel in greenhouse will change from:

  public static readonly LayersModelIO: IOType = new IOType( 'LayersModelIO', {
    valueType: LayersModel,
    stateSchema: LayersModel.STATE_SCHEMA,
    toStateObject: ( a: LayersModel ) => a.toStateObject(),
    applyState: ( a: LayersModel, b: LayersModelStateObject ) => a.applyState( b )
  } );

to

  public static readonly LayersModelIO: ReferenceIOType<LayersModel, LayersModelStateObject> = LayersModel;
  public static typeName = 'LayersModelIO';
  public static stateSchema = {
    emEnergyPackets: ArrayIO( EMEnergyPacket.EMEnergyPacketIO )
  };
pixelzoom commented 1 year ago

I'm not clear on the changes in greenhouse above. These are static fields in what class? And does that class now implement IOType?

Also wondering if this major change should be reviewed by @zepumph before being pushed to all PhET-iO sims. Or maybe this is something that you've already discussed with him.

samreid commented 1 year ago

I'm not clear on the changes in greenhouse above. These are static fields in what class?

LayersModel. Here is a patch with more context:

```diff Index: js/common/model/LayersModel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/js/common/model/LayersModel.ts b/js/common/model/LayersModel.ts --- a/js/common/model/LayersModel.ts (revision 4c5d7982f78e0de525f22211b7ea16fcbaa174f1) +++ b/js/common/model/LayersModel.ts (date 1664569608419) @@ -17,7 +17,7 @@ import optionize, { combineOptions } from '../../../../phet-core/js/optionize.js'; import Tandem from '../../../../tandem/js/Tandem.js'; import ArrayIO from '../../../../tandem/js/types/ArrayIO.js'; -import IOType from '../../../../tandem/js/types/IOType.js'; +import IOType, { ConstructsApplyState, ConstructsToStateObject, HasStateSchema, ReferenceIOType, ToStateObject } from '../../../../tandem/js/types/IOType.js'; import greenhouseEffect from '../../greenhouseEffect.js'; import GreenhouseEffectConstants from '../GreenhouseEffectConstants.js'; import GreenhouseEffectQueryParameters from '../GreenhouseEffectQueryParameters.js'; @@ -376,13 +376,6 @@ ); } - /** - * Returns a map of state keys and their associated IOTypes, see IOType for details. - */ - public static STATE_SCHEMA: Record = { - emEnergyPackets: ArrayIO( EMEnergyPacket.EMEnergyPacketIO ) - }; - // statics public static readonly HEIGHT_OF_ATMOSPHERE = HEIGHT_OF_ATMOSPHERE; public static readonly SUNLIGHT_SPAN = SUNLIGHT_SPAN; @@ -394,12 +387,11 @@ * serialization', as described in the Serialization section of * https://github.com/phetsims/phet-io/blob/master/doc/phet-io-instrumentation-technical-guide.md#serialization */ - public static readonly LayersModelIO: IOType = new IOType( 'LayersModelIO', { - valueType: LayersModel, - stateSchema: LayersModel.STATE_SCHEMA, - toStateObject: ( a: LayersModel ) => a.toStateObject(), - applyState: ( a: LayersModel, b: LayersModelStateObject ) => a.applyState( b ) - } ); + public static readonly LayersModelIO: ReferenceIOType = LayersModel; + public static typeName = 'LayersModelIO'; + public static stateSchema = { + emEnergyPackets: ArrayIO( EMEnergyPacket.EMEnergyPacketIO ) + }; } type LayersModelStateObject = { ```

And does that class now implement IOType

No, the proposal is that classes would not implement IOType, but that typeof MyClass implements IOType. That is because some of the parts, such as typeName, documentation, events, etc should be shared between all instances. That type is expressed on this line:

LayersModelIO: ReferenceIOType<LayersModel, LayersModelStateObject> = LayersModel;

Also wondering if this major change should be reviewed by @zepumph before being pushed to all PhET-iO sims. Or maybe this is something that you've already discussed with him.

Yes, I labeled it "epic" and it would be good to discuss.

samreid commented 1 year ago

There is already something very much like this called PhetioType in TandemConstants:

export type PhetioType = {
  methods: Methods;
  supertype?: string; // no supertype for root of hierarchy
  typeName: string;
  documentation?: string;
  events: string[];
  metadataDefaults?: Partial<PhetioObjectMetadata>;
  dataDefaults?: Record<string, unknown>;
  methodOrder?: string[];
  stateSchema?: PhetioObjectState;
  parameterTypes?: string[]; // each typeName
};
zepumph commented 1 year ago

Very interesting stuff! I like it. Can we discuss sometime soon?

samreid commented 1 year ago

I reached out to @zepumph and we are hoping to discuss today. Here's my current patch for my reference:

```diff Index: main/tandem/js/types/ReferenceIO.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/tandem/js/types/ReferenceIO.ts b/main/tandem/js/types/ReferenceIO.ts --- a/main/tandem/js/types/ReferenceIO.ts (revision 49718a238a00b94a938cf00fe3e7f3fadfdf454d) +++ b/main/tandem/js/types/ReferenceIO.ts (date 1664589528536) @@ -8,11 +8,11 @@ * @author Michael Kauzmann (PhET Interactive Simulations) */ -import Validation from '../../../axon/js/Validation.js'; import CouldNotYetDeserializeError from '../CouldNotYetDeserializeError.js'; import tandemNamespace from '../tandemNamespace.js'; -import IOType from './IOType.js'; +import IOType, { DataIOType } from './IOType.js'; import StringIO from './StringIO.js'; +import PhetioObject from '../PhetioObject.js'; // {Map.} - Cache each parameterized ReferenceIO so that it is only created once const cache = new Map(); @@ -29,34 +29,38 @@ if ( !cache.has( cacheKey ) ) { assert && assert( typeof parameterType.typeName === 'string', 'type name should be a string' ); - cache.set( cacheKey, new IOType( `ReferenceIO<${parameterType.typeName}>`, { - isValidValue: value => Validation.isValueValid( value, parameterType.validator ), - documentation: 'Uses reference identity for serializing and deserializing, and validates based on its parameter IO Type.', - parameterTypes: [ parameterType ], + + const t = parameterType.typeName; + + // TODO: Use class or object here? see https://github.com/phetsims/tandem/issues/275 + const ref: DataIOType = class R extends PhetioObject { + public static typeName = `ReferenceIO<${t}>`; + public static documentation = 'Uses reference identity for serializing and deserializing, and validates based on its parameter IO Type.'; + public static parameterTypes = [ parameterType ]; /** * Return the json that ReferenceIO is wrapping. This can be overridden by subclasses, or types can use ReferenceIO type * directly to use this implementation. */ - toStateObject( phetioObject ): ReferenceIOState { + public toStateObject(): ReferenceIOState { // NOTE: We cannot assert that phetioObject.phetioState === false here because sometimes ReferenceIO is used statically like // ReferenceIO( Vector2IO ).toStateObject( myVector ); return { - phetioID: phetioObject.tandem.phetioID + phetioID: this.tandem.phetioID }; - }, + } - stateSchema: { + public static stateSchema = { phetioID: StringIO - }, + }; /** * Decodes the object from a state, used in PhetioStateEngine.setState. This can be overridden by subclasses, or types can * use ReferenceIO type directly to use this implementation. * @throws CouldNotYetDeserializeError */ - fromStateObject( stateObject: ReferenceIOState ) { + public static fromStateObject( stateObject: ReferenceIOState ): PhetioObject { assert && assert( stateObject && typeof stateObject.phetioID === 'string', 'phetioID should be a string' ); if ( phet.phetio.phetioEngine.hasPhetioObject( stateObject.phetioID ) ) { return phet.phetio.phetioEngine.getPhetioObject( stateObject.phetioID ); @@ -64,15 +68,10 @@ else { throw new CouldNotYetDeserializeError(); } - }, - - /** - * References should be using fromStateObject to get a copy of the PhET-iO element. - */ - applyState( coreObject ) { - assert && assert( false, `ReferenceIO is meant to be used as DataType serialization (see fromStateObject) for phetioID: ${coreObject.tandem.phetioID}` ); } - } ) ); + }; + + cache.set( cacheKey, ref ); } return cache.get( cacheKey )!; Index: main/tandem/js/types/IOType.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/tandem/js/types/IOType.ts b/main/tandem/js/types/IOType.ts --- a/main/tandem/js/types/IOType.ts (revision 49718a238a00b94a938cf00fe3e7f3fadfdf454d) +++ b/main/tandem/js/types/IOType.ts (date 1664589253067) @@ -8,30 +8,8 @@ * @author Sam Reid (PhET Interactive Simulations) */ -import validate from '../../../axon/js/validate.js'; -import Validation, { Validator } from '../../../axon/js/Validation.js'; -import optionize from '../../../phet-core/js/optionize.js'; -import PhetioConstants from '../PhetioConstants.js'; import TandemConstants, { PhetioObjectMetadata } from '../TandemConstants.js'; -import tandemNamespace from '../tandemNamespace.js'; -import StateSchema, { CompositeStateObjectType } from './StateSchema.js'; -import PhetioObject from '../PhetioObject.js'; import IntentionalAny from '../../../phet-core/js/types/IntentionalAny.js'; -import PhetioDynamicElementContainer from '../PhetioDynamicElementContainer.js'; - -// constants -const VALIDATE_OPTIONS_FALSE = { validateValidator: false }; - -/** - * Estimate the core type name from a given IO Type name. - */ -const getCoreTypeName = ( ioTypeName: string ): string => { - const index = ioTypeName.indexOf( PhetioConstants.IO_TYPE_SUFFIX ); - assert && assert( index >= 0, 'IO should be in the type name' ); - return ioTypeName.substring( 0, index ); -}; - -type AddChildElement = ( group: PhetioDynamicElementContainer, componentName: string, stateObject: unknown ) => PhetioObject; export type IOTypeMethod = { returnType: IOType; @@ -47,9 +25,10 @@ invocableForReadOnlyElements?: boolean; }; -type DeserializationType = 'fromStateObject' | 'applyState'; +type IOType = { -type MainOptions = { + typeName: string; + supertype?: IOType | null; events?: string[]; dataDefaults?: Record; @@ -60,440 +39,30 @@ parameterTypes?: IOType[]; isFunctionType?: boolean; }; - -type StateSchemaOption = ( - ( ioType: IOType ) => StateSchema ) | - StateSchema | - Record | - null; - -type StateOptions = { - - // Should be required, but sometimes this is only in the parent IOType, like in DerivedPropertyIO - stateSchema?: StateSchemaOption; - toStateObject?: ( t: T ) => StateType; - - // If it is serializable, then it is optionally deserializable via one of these methods - fromStateObject?: ( s: StateType ) => T; - stateToArgsForConstructor?: ( s: StateType ) => unknown[]; - applyState?: ( t: T, state: StateType ) => void; - defaultDeserializationMethod?: DeserializationType; - addChildElement?: AddChildElement; -} | { - - // Otherwise it cannot have any stateful parts - toStateObject?: never; - stateSchema?: never; - fromStateObject?: never; - stateToArgsForConstructor?: never; - applyState?: never; - defaultDeserializationMethod?: never; - addChildElement?: never; -}; - -type SelfOptions = - MainOptions & - StateOptions; - -type IOTypeOptions = SelfOptions & Validator; - -// TODO: not any, but do we have to serialize type parameters? https://github.com/phetsims/tandem/issues/263 -export default class IOType { // eslint-disable-line @typescript-eslint/no-explicit-any - public readonly supertype?: IOType; - public readonly documentation?: string; - - // The public methods available for this IO Type. Each method is not just a function, - // but a collection of metadata about the method to be able to serialize parameters and return types and provide - // better documentation. - public readonly methods?: Record; - - // The list of events that can be emitted at this level (does not include events from supertypes). - public readonly events: string[]; - - // Key/value pairs indicating the defaults for the IO Type metadata. - public readonly metadataDefaults?: Partial; - - // Key/value pairs indicating the defaults for the IO Type data. - public readonly dataDefaults?: Record; - - // IO Types can specify the order that methods appear in the documentation by putting their names in this - // list. This list is only for the methods defined at this level in the type hierarchy. After the methodOrder - // specified, the methods follow in the order declared in the implementation (which isn't necessarily stable). - public readonly methodOrder?: string[]; - - // For parametric types, they must indicate the types of the parameters here. Empty array if non-parametric - public readonly parameterTypes?: IOType[]; - - public readonly toStateObject: ( t: T ) => StateType; - public readonly fromStateObject: ( state: StateType ) => T; - public readonly stateToArgsForConstructor: ( s: StateType ) => unknown[]; // TODO: instead of unknown this is the second parameter type for PhetioDynamicElementContainer. How? https://github.com/phetsims/tandem/issues/261 - public readonly applyState: ( object: T, state: StateType ) => void; - public readonly addChildElement: AddChildElement; - public readonly validator: Validator; - public readonly defaultDeserializationMethod: DeserializationType; - - // The schema for how this IOType is serialized. Just for this level in the IOType hierarchy, - // see getAllStateSchema(). - // TODO: https://github.com/phetsims/tandem/issues/263 Should null be allowed here? - public readonly stateSchema: StateSchema; - public readonly isFunctionType: boolean; +export default IOType; - public static ObjectIO: IOType; - - /** - * @param typeName - The name that this IOType will have in the public PhET-iO API. In general, this should - * only be word characters, ending in "IO". Parametric types are a special subset of IOTypes that include their - * parameters in their typeName. If an IOType's parameters are other IO Type(s), then they should be included within - * angle brackets, like "PropertyIO". Some other types use a more custom format for displaying their - * parameter types, in this case the parameter section of the type name (immediately following "IO") should begin - * with an open paren, "(". Thus the schema for a typeName could be defined (using regex) as `[A-Z]\w*IO([(<].*){0,1}`. - * Parameterized types should also include a `parameterTypes` field on the IOType. - * @param providedOptions - */ - public constructor( public readonly typeName: string, providedOptions: IOTypeOptions ) { - - // For reference in the options - const supertype = providedOptions.supertype || IOType.ObjectIO; - const toStateObjectSupplied = !!( providedOptions.toStateObject ); - const applyStateSupplied = !!( providedOptions.applyState ); - const stateSchemaSupplied = !!( providedOptions.stateSchema ); - - const options = optionize, SelfOptions>()( { - - supertype: IOType.ObjectIO, - methods: {}, - events: [], - - // If anything is provided here, then corresponding PhetioObjects that use this IOType should override - // PhetioObject.getMetadata() to add what keys they need for their specific type. Cannot specify redundant values - // (that an ancestor already specified). - metadataDefaults: {}, - - // Most likely this will remain PhET-iO internal, and shouldn't need to be used when creating IOTypes outside of tandem/. - dataDefaults: {}, - methodOrder: [], - parameterTypes: [], - - // Documentation that appears in PhET-iO Studio, supports HTML markup. - documentation: `IO Type for ${getCoreTypeName( typeName )}`, - - // Functions cannot be sent from one iframe to another, so must be wrapped. See phetioCommandProcessor.wrapFunction - isFunctionType: false, - - /**** STATE ****/ - - // Serialize the core object. Most often this looks like an object literal that holds - // data about the PhetioObject instance. This is likely superfluous to just providing a stateSchema of composite - // key/IOType values, which will create a default toStateObject based on the schema. - toStateObject: supertype && supertype.toStateObject, - - // For Data Type Deserialization. Decodes the object from a state (see toStateObject) - // into an instance of the core type. - // see https://github.com/phetsims/phet-io/blob/master/doc/phet-io-instrumentation-technical-guide.md#three-types-of-deserialization - fromStateObject: supertype && supertype.fromStateObject, - - // For Dynamic Element Deserialization: converts the state object to arguments - // for a `create` function in PhetioGroup or other PhetioDynamicElementContainer creation function. Note that - // other non-serialized args (not dealt with here) may be supplied as closure variables. This function only needs - // to be implemented on IO Types who's core type is phetioDynamicElement: true, such as PhetioDynamicElementContainer - // elements. - // see https://github.com/phetsims/phet-io/blob/master/doc/phet-io-instrumentation-technical-guide.md#three-types-of-deserialization - stateToArgsForConstructor: supertype && supertype.stateToArgsForConstructor, - - // For Reference Type Deserialization: Applies the state (see toStateObject) - // value to the instance. When setting PhET-iO state, this function will be called on an instrumented instance to set the - // stateObject's value to it. StateSchema makes this method often superfluous. A composite stateSchema can be used - // to automatically formulate the applyState function. If using stateSchema for the applyState method, make sure that - // each compose IOType has the correct defaultDeserializationMethod. Most of the time, composite IOTypes use fromStateObject - // to deserialize each sub-component, but in some circumstances, you will want your child to deserialize by also using applyState. - // See options.defaultDeserializationMethod to configure this case. - // see https://github.com/phetsims/phet-io/blob/master/doc/phet-io-instrumentation-technical-guide.md#three-types-of-deserialization - applyState: supertype && supertype.applyState, - - // the specification for how the - // PhET-iO state will look for instances of this type. null specifies that the object is not serialized. A composite - // StateSchema can supply a toStateObject and applyState serialization strategy. This default serialization strategy - // only applies to this level, and does not recurse to parents. If you need to add serialization from parent levels, - // this can be done by manually implementing a custom toStateObject. By default, it will assume that each composite - // child of this stateSchema deserializes via "fromStateObject", if instead it uses applyState, please specify that - // per IOType with defaultDeserializationMethod. - stateSchema: null, - - // For use when this IOType is pare of a composite stateSchema in another IOType. When - // using serialization methods by supplying only stateSchema, then deserialization - // can take a variety of forms, and this will vary based on the IOType. In most cases deserialization of a component - // is done via fromStateObject. If not, specify this option so that the stateSchema will be able to know to call - // the appropriate deserialization method when deserializing something of this IOType. - defaultDeserializationMethod: 'fromStateObject', - - // For dynamic element containers, see examples in IOTypes for PhetioDynamicElementContainer classes - addChildElement: supertype && supertype.addChildElement - }, providedOptions ); - - if ( assert && supertype ) { - Object.keys( options.metadataDefaults ).forEach( metadataDefaultKey => { - assert && supertype.getAllMetadataDefaults().hasOwnProperty( metadataDefaultKey ) && - - // @ts-ignore - assert( supertype.getAllMetadataDefaults()[ metadataDefaultKey ] !== options.metadataDefaults[ metadataDefaultKey ], - `${metadataDefaultKey} should not have the same default value as the ancestor metadata default.` ); - } ); - } - this.supertype = supertype; - this.documentation = options.documentation; - this.methods = options.methods; - this.events = options.events; - this.metadataDefaults = options.metadataDefaults; // just for this level, see getAllMetadataDefaults() - this.dataDefaults = options.dataDefaults; // just for this level, see getAllDataDefaults() - this.methodOrder = options.methodOrder; - this.parameterTypes = options.parameterTypes; - - // Validation - this.validator = _.pick( options, Validation.VALIDATOR_KEYS ); - this.validator.validationMessage = this.validator.validationMessage || `Validation failed IOType Validator: ${this.typeName}`; - - this.defaultDeserializationMethod = options.defaultDeserializationMethod; - - if ( options.stateSchema !== null && !( options.stateSchema instanceof StateSchema ) ) { - const compositeSchema = typeof options.stateSchema === 'function' ? options.stateSchema( this ) : options.stateSchema; - - // @ts-ignore - this.stateSchema = new StateSchema( { compositeSchema: compositeSchema } ); - } - else { - - // @ts-ignore - this.stateSchema = options.stateSchema; - } - - // Assert that toStateObject method is provided for value StateSchemas. Do this with the following logic: - // 1. It is acceptable to not provide a stateSchema (for IOTypes that aren't stateful) - // 2. You must either provide a toStateObject, or have a composite StateSchema. Composite state schemas support default serialization methods. - assert && assert( !this.stateSchema || ( toStateObjectSupplied || this.stateSchema.isComposite() ), - 'toStateObject method must be provided for value StateSchemas' ); - - this.toStateObject = ( coreObject: T ) => { - validate( coreObject, this.validator, VALIDATE_OPTIONS_FALSE ); - - let toStateObject; - - // Only do this non-standard toStateObject function if there is a stateSchema but no toStateObject provided - if ( !toStateObjectSupplied && stateSchemaSupplied && this.stateSchema.isComposite() ) { - toStateObject = this.stateSchema.defaultToStateObject( coreObject ); - } - else { - toStateObject = options.toStateObject( coreObject ); - } - - // Validate, but only if this IOType instance has more to validate than the supertype - if ( toStateObjectSupplied || stateSchemaSupplied ) { - - // Only validate the stateObject if it is phetioState:true. - // This is an n*m algorithm because for each time toStateObject is called and needs validation, this.validateStateObject - // looks all the way up the IOType hierarchy. This is not efficient, but gains us the ability to make sure that - // the stateObject doesn't have any superfluous, unexpected keys. The "m" portion is based on how many sub-properties - // in a state call `toStateObject`, and the "n" portion is based on how many IOTypes in the hierarchy define a - // toStateObject or stateSchema. In the future we could potentially improve performance by having validateStateObject - // only check against the schema at this level, but then extra keys in the stateObject would not be caught. From work done in https://github.com/phetsims/phet-io/issues/1774 - assert && this.validateStateObject( toStateObject ); - } - return toStateObject; - }; - this.fromStateObject = options.fromStateObject; - this.stateToArgsForConstructor = options.stateToArgsForConstructor; - - this.applyState = ( coreObject: T, stateObject: StateType ) => { - validate( coreObject, this.validator, VALIDATE_OPTIONS_FALSE ); - - // Validate, but only if this IOType instance has more to validate than the supertype - if ( applyStateSupplied || stateSchemaSupplied ) { - - // Validate that the provided stateObject is of the expected schema - // NOTE: Cannot use this.validateStateObject because options adopts supertype.applyState, which is bounds to the - // parent IO Type. This prevents correct validation because the supertype doesn't know about the subtype schemas. - // @ts-ignore - assert && coreObject.phetioType.validateStateObject( stateObject ); - } - - // Only do this non-standard applyState function from stateSchema if there is a stateSchema but no applyState provided - if ( !applyStateSupplied && stateSchemaSupplied && this.stateSchema.isComposite() ) { - this.stateSchema.defaultApplyState( coreObject, stateObject as CompositeStateObjectType ); - } - else { - options.applyState( coreObject, stateObject ); - } - }; - - this.isFunctionType = options.isFunctionType; - this.addChildElement = options.addChildElement; - - if ( assert ) { - - assert && assert( supertype || this.typeName === 'ObjectIO', 'supertype is required' ); - assert && assert( !this.typeName.includes( '.' ), 'Dots should not appear in type names' ); - assert && assert( this.typeName.split( /[<(]/ )[ 0 ].endsWith( PhetioConstants.IO_TYPE_SUFFIX ), `IO Type name must end with ${PhetioConstants.IO_TYPE_SUFFIX}` ); - assert && assert( this.hasOwnProperty( 'typeName' ), 'this.typeName is required' ); - - // assert that each public method adheres to the expected schema - this.methods && Object.values( this.methods ).forEach( ( methodObject: IOTypeMethod ) => { - if ( typeof methodObject === 'object' ) { - assert && methodObject.invocableForReadOnlyElements && assert( typeof methodObject.invocableForReadOnlyElements === 'boolean', - `invocableForReadOnlyElements must be of type boolean: ${methodObject.invocableForReadOnlyElements}` ); - } - } ); - assert && assert( this.documentation.length > 0, 'documentation must be provided' ); - - this.methods && this.hasOwnProperty( 'methodOrder' ) && this.methodOrder.forEach( methodName => { - assert && assert( this.methods![ methodName ], `methodName not in public methods: ${methodName}` ); - } ); - - if ( supertype ) { - const typeHierarchy = supertype.getTypeHierarchy(); - assert && this.events && this.events.forEach( event => { - - // Make sure events are not listed again - assert && assert( !_.some( typeHierarchy, t => t.events.includes( event ) ), `IOType should not declare event that parent also has: ${event}` ); - } ); - } - else { - - // The root IOType must supply all 4 state methods. - assert && assert( typeof options.toStateObject === 'function', 'toStateObject must be defined' ); - assert && assert( typeof options.fromStateObject === 'function', 'fromStateObject must be defined' ); - assert && assert( typeof options.stateToArgsForConstructor === 'function', 'stateToArgsForConstructor must be defined' ); - assert && assert( typeof options.applyState === 'function', 'applyState must be defined' ); - } - } - } - - /** - * Gets an array of IOTypes of the self type and all the supertype ancestors. - */ - private getTypeHierarchy(): IOType[] { - const array = []; - - // @ts-ignore - let ioType: IOType = this; // eslint-disable-line - while ( ioType ) { - array.push( ioType ); - ioType = ioType.supertype!; - } - return array; - } - - /** - * Returns true if this IOType is a subtype of the passed-in type (or if they are the same). - */ - public extends( type: IOType ): boolean { - - // memory-based implementation OK since this method is only used in assertions - return this.getTypeHierarchy().includes( type ); - } - - /** - * Return all the metadata defaults (for the entire IO Type hierarchy) - */ - public getAllMetadataDefaults(): Partial { - return _.merge( {}, this.supertype ? this.supertype.getAllMetadataDefaults() : {}, this.metadataDefaults ); - } - - /** - * Return all the data defaults (for the entire IO Type hierarchy) - */ - public getAllDataDefaults(): Record { - return _.merge( {}, this.supertype ? this.supertype.getAllDataDefaults() : {}, this.dataDefaults ); - } - - /** - * @param stateObject - the stateObject to validate against - * @param toAssert=false - whether or not to assert when invalid - * @param publicSchemaKeys=[] - * @param privateSchemaKeys=[] - * @returns if the stateObject is valid or not. - */ - public isStateObjectValid( stateObject: StateType, toAssert = false, publicSchemaKeys: string[] = [], privateSchemaKeys: string[] = [] ): boolean { - - // Set to false when invalid - let valid = true; - - // make sure the stateObject has everything the schema requires and nothing more - if ( this.stateSchema ) { - const validSoFar = this.stateSchema.checkStateObjectValid( stateObject, toAssert, publicSchemaKeys, privateSchemaKeys ); - - // null as a marker to keep checking up the hierarchy, otherwise we reached our based case because the stateSchema was a value, not a composite - if ( validSoFar !== null ) { - return validSoFar; - } - } - - if ( this.supertype && !( this.stateSchema && this.stateSchema.isComposite() ) ) { - return valid && this.supertype.isStateObjectValid( stateObject, toAssert, publicSchemaKeys, privateSchemaKeys ); - } - - // When we reach the root, make sure there isn't anything in the stateObject that isn't described by a schema - if ( !this.supertype && stateObject && typeof stateObject !== 'string' && !Array.isArray( stateObject ) ) { - - const check = ( type: 'public' | 'private', key: string ) => { - const keys = type === 'public' ? publicSchemaKeys : privateSchemaKeys; - const keyValid = keys.includes( key ); - if ( !keyValid ) { - valid = false; - } - assert && toAssert && assert( keyValid, `stateObject provided a ${type} key that is not in the schema: ${key}` ); - }; - - // Visit the public state - Object.keys( stateObject ).filter( key => key !== '_private' ).forEach( key => check( 'public', key ) ); - - // Visit the private state, if any - // @ts-ignore stateObjects can take a variety of forms, they don't have to be a record, thus, it is challenging to be graceful to a `_private` key - stateObject._private && Object.keys( stateObject._private ).forEach( key => check( 'private', key ) ); - - return valid; - } - return true; - } - - /** - * Assert if the provided stateObject is not valid to this IOType's stateSchema - */ - public validateStateObject( stateObject: StateType ): void { - this.isStateObjectValid( stateObject, true ); - } - - public toString(): string { - return this.typeName; - } -} - -// default state value -const DEFAULT_STATE = null; - -IOType.ObjectIO = new IOType( TandemConstants.OBJECT_IO_TYPE_NAME, { - isValidValue: () => true, +export const ObjectIO: IOType = { + typeName: TandemConstants.OBJECT_IO_TYPE_NAME, supertype: null, documentation: 'The root of the IO Type hierarchy', - toStateObject: ( coreObject: PhetioObject ) => { + metadataDefaults: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS +}; - assert && assert( coreObject.tandem, 'coreObject must be PhET-iO object' ); +// Static methods +export type FromStateObject = { fromStateObject: ( s: S ) => T }; +export type StateToArgsForConstructor = { stateToArgsForConstructor: ( s: S ) => unknown[] }; // TODO: instead of unknown this is the second parameter type for PhetioDynamicElementContainer. How? https://github.com/phetsims/tandem/issues/261 +export type HasStateSchema = { + stateSchema: Record; +}; - assert && assert( !coreObject.phetioState, - `fell back to root serialization state for ${coreObject.tandem.phetioID}. Potential solutions: - * mark the type as phetioState: false - * create a custom toStateObject method in your IO Type - * perhaps you have everything right, but forgot to pass in the IOType via phetioType in the constructor` ); - return DEFAULT_STATE; - }, - fromStateObject: stateObject => { - throw new Error( 'ObjectIO.fromStateObject should not be called' ); - }, - stateToArgsForConstructor: stateObject => [], - applyState: _.noop, - metadataDefaults: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS, - dataDefaults: { - initialState: DEFAULT_STATE - }, - stateSchema: null -} ); +// These methods appear on the instance +export type ToStateObject = { toStateObject: () => T }; +export type ApplyState = { applyState: ( s: S ) => void }; +export type AddChildElement = { addChildElement: ( componentName: string, s: S ) => T }; -tandemNamespace.register( 'IOType', IOType ); \ No newline at end of file +export type ConstructsToStateObject = abstract new( ...args: IntentionalAny[] ) => ToStateObject; +export type ConstructsApplyState = abstract new( ...args: IntentionalAny[] ) => ApplyState; +export type ConstructsAddChildElement = abstract new( ...args: IntentionalAny[] ) => AddChildElement; + +export type ReferenceIOType = ConstructsToStateObject & ConstructsApplyState & IOType; +export type DataIOType = ConstructsToStateObject & FromStateObject & IOType; \ No newline at end of file Index: main/phet-io/js/phetioEngine.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/phet-io/js/phetioEngine.ts b/main/phet-io/js/phetioEngine.ts --- a/main/phet-io/js/phetioEngine.ts (revision 546046240fe6f47ce5c122da8b1297239cafb698) +++ b/main/phet-io/js/phetioEngine.ts (date 1664677262489) @@ -549,12 +549,12 @@ level = level[ componentName ]; } ); - const metadataDefaults = phetioObject.phetioType.getAllMetadataDefaults(); + const metadataDefaults = phetioObject.phetioType.metadataDefaults || {}; const metadata = phetioObject.getMetadata(); level[ Tandem.METADATA_KEY ] = {} as PhetioObjectMetadata; const metadataObject = level[ Tandem.METADATA_KEY ]; - const dataDefaults = phetioObject.phetioType.getAllDataDefaults(); + const dataDefaults = phetioObject.phetioType.dataDefaults || {}; const initialState = this.phetioStateEngine.getStateForObject( phetioID ); assert && assert( dataDefaults.hasOwnProperty( 'initialState' ), 'initialState expected to have a default on the IOType' ); if ( dataDefaults.initialState !== initialState && metadata.phetioState ) { @@ -833,330 +833,326 @@ phetioEngineBuffer.phetioObjects.length = 0; } -// IO Type for PhET-iO main interface, with high-level methods for communicating with the simulation. - public static PhetioEngineIO = new IOType( 'PhetioEngineIO', { - valueType: PhetioEngine, - documentation: 'Mediator for the phet-io module, with system-wide methods for communicating with the sim and other globals', - events: [ + public static readonly typeName = 'PhetioEngineIO'; + public static documentation = 'Mediator for the phet-io module, with system-wide methods for communicating with the sim and other globals'; + public static events = [ - // When the sim is launched - 'simStarted', + // When the sim is launched + 'simStarted', - // The entire state for the sim, for the first frame and for keyframes - 'state' - ], - methodOrder: [ - 'addEventListener', - 'getScreenshotDataURL', - 'getPhetioIDs', - 'getValues', - 'getState', - 'setState', - 'getChangedState', - 'setChangedState', - 'getStateForObject', - 'getPhetioElementMetadata', - 'setRandomSeed', - 'getRandomSeed', - // 'getEngagementMetrics', // commented out because https://github.com/phetsims/joist/issues/553 is deferred for after GQIO-oneone - 'setDisplaySize', + // The entire state for the sim, for the first frame and for keyframes + 'state' + ]; + public static methodOrder = [ + 'addEventListener', + 'getScreenshotDataURL', + 'getPhetioIDs', + 'getValues', + 'getState', + 'setState', + 'getChangedState', + 'setChangedState', + 'getStateForObject', + 'getPhetioElementMetadata', + 'setRandomSeed', + 'getRandomSeed', + // 'getEngagementMetrics', // commented out because https://github.com/phetsims/joist/issues/553 is deferred for after GQIO-oneone + 'setDisplaySize', - 'setPlaybackMode', - 'setInteractive', - 'isInteractive', + 'setPlaybackMode', + 'setInteractive', + 'isInteractive', - 'triggerEvent', + 'triggerEvent', - 'launchSimulation', - 'displaySim', - 'invokeControllerInputEvent', - 'setSimStartedMetadata', - 'simulateError' - ], - methods: { + 'launchSimulation', + 'displaySim', + 'invokeControllerInputEvent', + 'setSimStartedMetadata', + 'simulateError' + ]; + public static methods = { - getValues: { - documentation: 'Get the current values for multiple Property/DerivedProperty elements at the same time.' + - ' Useful for collecting data to be plotted, so values will be consistent.', - returnType: ObjectLiteralIO, - parameterTypes: [ ArrayIO( StringIO ) ], - implementation: function( phetioIDs: string[] ) { - const values: Record = {}; - for ( let i = 0; i < phetioIDs.length; i++ ) { - const phetioID = phetioIDs[ i ]; + getValues: { + documentation: 'Get the current values for multiple Property/DerivedProperty elements at the same time.' + + ' Useful for collecting data to be plotted, so values will be consistent.', + returnType: ObjectLiteralIO, + parameterTypes: [ ArrayIO( StringIO ) ], + implementation: function( phetioIDs: string[] ) { + const values: Record = {}; + for ( let i = 0; i < phetioIDs.length; i++ ) { + const phetioID = phetioIDs[ i ]; - // @ts-ignore - const phetioObject = this.phetioEngine.getPhetioObject( phetioID ); - values[ phetioID ] = phetioObject.getValue(); - } - return values; - } - }, + // @ts-ignore + const phetioObject = this.phetioEngine.getPhetioObject( phetioID ); + values[ phetioID ] = phetioObject.getValue(); + } + return values; + } + }, - // commented out because https://github.com/phetsims/joist/issues/553 is deferred for after GQIO-oneone - // getEngagementMetrics: { - // returnType: ObjectLiteralIO, - // parameterTypes: [], - // implementation: function() { - // return phet.joist.sim.engagementMetrics.getEngagementMetrics(); - // }, - // documentation: 'Note that this is powered on the data stream, and can have weird behavior if not emitting high frequency events.' - // }, +// commented out because https://github.com/phetsims/joist/issues/553 is deferred for after GQIO-oneone +// getEngagementMetrics: { +// returnType: ObjectLiteralIO, +// parameterTypes: [], +// implementation: function() { +// return phet.joist.sim.engagementMetrics.getEngagementMetrics(); +// }, +// documentation: 'Note that this is powered on the data stream, and can have weird behavior if not emitting high frequency events.' +// }, - displaySim: { - returnType: VoidIO, - parameterTypes: [], - implementation: function() { - ( this as unknown as PhetioEngine ).isReadyForDisplay = true; + displaySim: { + returnType: VoidIO, + parameterTypes: [], + implementation: function() { + ( this as unknown as PhetioEngine ).isReadyForDisplay = true; - // dispose clears the reference to window.phetSplashScreen so this function can be called multiple times gracefully. - window.phetSplashScreen && window.phetSplashScreen.dispose(); - }, - documentation: 'Signifies that customization has been completed and the sim is therefore ready for display' - }, - addEventListener: { - returnType: VoidIO, - parameterTypes: [ FunctionIO( VoidIO, [ ObjectLiteralIO ] ) ], - implementation: function( listener ) { - assert && assert( dataStream, 'cannot add dataStream listener because dataStream is not defined' ); + // dispose clears the reference to window.phetSplashScreen so this function can be called multiple times gracefully. + window.phetSplashScreen && window.phetSplashScreen.dispose(); + }, + documentation: 'Signifies that customization has been completed and the sim is therefore ready for display' + }, + addEventListener: { + returnType: VoidIO, + parameterTypes: [ FunctionIO( VoidIO, [ ObjectLiteralIO ] ) ], + implementation: function( listener ) { + assert && assert( dataStream, 'cannot add dataStream listener because dataStream is not defined' ); - // Send all new messages - dataStream.addEventListener( listener ); - }, - documentation: 'Adds a listener to the PhET-iO dataStream, which can be used to respond to events or for data analysis. ' + - 'Unlike Client.launchSim( {onEvent} ) which is called recursively for each parsed child event, ' + - 'this is only called with stringified top-level events.' - }, + // Send all new messages + dataStream.addEventListener( listener ); + }, + documentation: 'Adds a listener to the PhET-iO dataStream, which can be used to respond to events or for data analysis. ' + + 'Unlike Client.launchSim( {onEvent} ) which is called recursively for each parsed child event, ' + + 'this is only called with stringified top-level events.' + }, - setInteractive: { - returnType: VoidIO, - parameterTypes: [ BooleanIO ], - implementation: function( interactive ) { - phet.joist.display.interactive = interactive; - }, - documentation: 'Sets whether the sim can be interacted with (via mouse/touch/keyboard). When set to false, the ' + - 'sim animations and model will still step.', - invocableForReadOnlyElements: false - }, + setInteractive: { + returnType: VoidIO, + parameterTypes: [ BooleanIO ], + implementation: function( interactive ) { + phet.joist.display.interactive = interactive; + }, + documentation: 'Sets whether the sim can be interacted with (via mouse/touch/keyboard). When set to false, the ' + + 'sim animations and model will still step.', + invocableForReadOnlyElements: false + }, - isInteractive: { - returnType: BooleanIO, - parameterTypes: [], - implementation: function() { - return phet.joist.display.interactive; - }, - documentation: 'Gets whether the sim can be interacted with (via mouse/touch/keyboard).' - }, + isInteractive: { + returnType: BooleanIO, + parameterTypes: [], + implementation: function() { + return phet.joist.display.interactive; + }, + documentation: 'Gets whether the sim can be interacted with (via mouse/touch/keyboard).' + }, - setPlaybackMode: { - returnType: VoidIO, - parameterTypes: [ BooleanIO ], - implementation: function( playbackModeEnabled: boolean ) { - assert && assert( !phet.joist.sim, 'must be called before Sim is instantiated' ); - phet.joist.playbackModeEnabledProperty.value = playbackModeEnabled; - }, - documentation: 'Sets whether the sim is in "playback mode", which is used for playing back recorded events. In ' + - 'this mode, the simulation clock will only advance based on the played back events.', - invocableForReadOnlyElements: false - }, + setPlaybackMode: { + returnType: VoidIO, + parameterTypes: [ BooleanIO ], + implementation: function( playbackModeEnabled: boolean ) { + assert && assert( !phet.joist.sim, 'must be called before Sim is instantiated' ); + phet.joist.playbackModeEnabledProperty.value = playbackModeEnabled; + }, + documentation: 'Sets whether the sim is in "playback mode", which is used for playing back recorded events. In ' + + 'this mode, the simulation clock will only advance based on the played back events.', + invocableForReadOnlyElements: false + }, - invokeControllerInputEvent: { - returnType: VoidIO, - parameterTypes: [ ObjectLiteralIO ], - implementation: function( options ) { - phet.joist.display._input.invokeControllerInputEvent( options ); - }, - documentation: 'Plays back a recorded input event into the simulation.', - invocableForReadOnlyElements: false - }, + invokeControllerInputEvent: { + returnType: VoidIO, + parameterTypes: [ ObjectLiteralIO ], + implementation: function( options ) { + phet.joist.display._input.invokeControllerInputEvent( options ); + }, + documentation: 'Plays back a recorded input event into the simulation.', + invocableForReadOnlyElements: false + }, - getPhetioIDs: { - returnType: ArrayIO( StringIO ), - parameterTypes: [], - implementation: PhetioEngine.prototype.getPhetioIDs, - documentation: 'Gets a list of all of the PhET-iO elements.' - }, + getPhetioIDs: { + returnType: ArrayIO( StringIO ), + parameterTypes: [], + implementation: PhetioEngine.prototype.getPhetioIDs, + documentation: 'Gets a list of all of the PhET-iO elements.' + }, - getFeaturedPhetioIDs: { - returnType: ArrayIO( StringIO ), - parameterTypes: [], - implementation: PhetioEngine.prototype.getFeaturedPhetioIDs, - documentation: 'Gets a list of the featured PhET-iO elements.' - }, + getFeaturedPhetioIDs: { + returnType: ArrayIO( StringIO ), + parameterTypes: [], + implementation: PhetioEngine.prototype.getFeaturedPhetioIDs, + documentation: 'Gets a list of the featured PhET-iO elements.' + }, - getState: { - returnType: ObjectLiteralIO, - parameterTypes: [], - implementation: function() { - return ( this as unknown as PhetioEngine ).phetioStateEngine.getState(); - }, - documentation: 'Gets the full state of the simulation, including parts that have not changed from startup.' - }, + getState: { + returnType: ObjectLiteralIO, + parameterTypes: [], + implementation: function() { + return ( this as unknown as PhetioEngine ).phetioStateEngine.getState(); + }, + documentation: 'Gets the full state of the simulation, including parts that have not changed from startup.' + }, - getInitialState: { - returnType: ObjectLiteralIO, - parameterTypes: [], - implementation: function() { - return ( this as unknown as PhetioEngine ).phetioStateEngine.initialState; - }, - documentation: 'Gets the state of the simulation when it finished launching' - }, + getInitialState: { + returnType: ObjectLiteralIO, + parameterTypes: [], + implementation: function() { + return ( this as unknown as PhetioEngine ).phetioStateEngine.initialState; + }, + documentation: 'Gets the state of the simulation when it finished launching' + }, - getChangedState: { - returnType: ObjectLiteralIO, - parameterTypes: [], - implementation: function() { - return ( this as unknown as PhetioEngine ).phetioStateEngine.getChangedState(); - }, - documentation: 'Gets the state of the simulation, only returning values that have changed from their initial state. ' + - 'PhET-iO elements that have been deleted will be displayed with the value "DELETED".' - }, + getChangedState: { + returnType: ObjectLiteralIO, + parameterTypes: [], + implementation: function() { + return ( this as unknown as PhetioEngine ).phetioStateEngine.getChangedState(); + }, + documentation: 'Gets the state of the simulation, only returning values that have changed from their initial state. ' + + 'PhET-iO elements that have been deleted will be displayed with the value "DELETED".' + }, - setChangedState: { - returnType: VoidIO, - parameterTypes: [ ObjectLiteralIO ], - implementation: function( changedState ) { - return ( this as unknown as PhetioEngine ).phetioStateEngine.setChangedState( changedState ); - }, - documentation: 'Sets the state of the simulation based on the changes from the initial state.' - }, + setChangedState: { + returnType: VoidIO, + parameterTypes: [ ObjectLiteralIO ], + implementation: function( changedState ) { + return ( this as unknown as PhetioEngine ).phetioStateEngine.setChangedState( changedState ); + }, + documentation: 'Sets the state of the simulation based on the changes from the initial state.' + }, - getStateForObject: { - returnType: NullableIO( ObjectLiteralIO ), - parameterTypes: [ StringIO ], - implementation: function( phetioID ) { - return ( this as unknown as PhetioEngine ).phetioStateEngine.getStateForObject( phetioID ); - }, - documentation: 'Gets the state object for a PhET-iO element or null if phetioID does not exist.' - }, + getStateForObject: { + returnType: NullableIO( ObjectLiteralIO ), + parameterTypes: [ StringIO ], + implementation: function( phetioID ) { + return ( this as unknown as PhetioEngine ).phetioStateEngine.getStateForObject( phetioID ); + }, + documentation: 'Gets the state object for a PhET-iO element or null if phetioID does not exist.' + }, - setState: { - returnType: VoidIO, - parameterTypes: [ ObjectLiteralIO ], - implementation: function( state ) { - ( this as unknown as PhetioEngine ).phetioStateEngine.setFullState( state ); - }, - documentation: 'Sets the simulation state based on the keys provided. The parameter is a map where the keys are ' + - 'phetioIDs and the values are the corresponding states for each PhET-iO element. This method expects' + - 'a complete list of state supported phetioIDs, which can be found by calling getState(). This method ' + - 'should not be called until the sim has been initialized (after or during the Client.onSimInitialized() ' + - 'hook).', - invocableForReadOnlyElements: false - }, + setState: { + returnType: VoidIO, + parameterTypes: [ ObjectLiteralIO ], + implementation: function( state ) { + ( this as unknown as PhetioEngine ).phetioStateEngine.setFullState( state ); + }, + documentation: 'Sets the simulation state based on the keys provided. The parameter is a map where the keys are ' + + 'phetioIDs and the values are the corresponding states for each PhET-iO element. This method expects' + + 'a complete list of state supported phetioIDs, which can be found by calling getState(). This method ' + + 'should not be called until the sim has been initialized (after or during the Client.onSimInitialized() ' + + 'hook).', + invocableForReadOnlyElements: false + }, - getPhetioElementMetadata: { - returnType: ObjectLiteralIO, - parameterTypes: [ StringIO ], - implementation: function( phetioID ) { + getPhetioElementMetadata: { + returnType: ObjectLiteralIO, + parameterTypes: [ StringIO ], + implementation: function( phetioID ) { - // this is the same implementation as the third argument in the phetioElementAddedEmitter callback, these should stay in sync. - return ( this as unknown as PhetioEngine ).getPhetioObject( phetioID ).getMetadata(); - }, - documentation: 'Get metadata about the PhET-iO element. This includes the following keys:
    ' + - '
  • phetioTypeName: The name of the PhET-iO Type\n
  • ' + - '
  • phetioState: default - true. When true, includes the PhET-iO element in the PhET-iO state\n
  • ' + - '
  • phetioReadOnly: default - false. When true, you can only get values from the PhET-iO element; no setting allowed.\n
  • ' + - '
  • phetioDocumentation: default - null. Useful notes about a PhET-iO element, shown in the PhET-iO Studio Wrapper
' - }, + // this is the same implementation as the third argument in the phetioElementAddedEmitter callback, these should stay in sync. + return ( this as unknown as PhetioEngine ).getPhetioObject( phetioID ).getMetadata(); + }, + documentation: 'Get metadata about the PhET-iO element. This includes the following keys:
    ' + + '
  • phetioTypeName: The name of the PhET-iO Type\n
  • ' + + '
  • phetioState: default - true. When true, includes the PhET-iO element in the PhET-iO state\n
  • ' + + '
  • phetioReadOnly: default - false. When true, you can only get values from the PhET-iO element; no setting allowed.\n
  • ' + + '
  • phetioDocumentation: default - null. Useful notes about a PhET-iO element, shown in the PhET-iO Studio Wrapper
' + }, - triggerEvent: { - returnType: VoidIO, - parameterTypes: [ ObjectLiteralIO ], - implementation: function( event ) { - dataStream.trigger( EventType.WRAPPER, event.phetioID, event.componentType, event.name, event.data, event.metadata ); - }, - documentation: 'Start and end a message from the wrapper, interleaving it into the PhET-iO Simulation\'s data stream. Takes an object with entries like:
' + - '{string} phetioID - the id of the specific element that created the event in camelCasing like \'frictionCheckbox\'
' + - '{{typeName: string, events: String[]}} type - The Type that is emitting the event. The event being triggered must be included in the "events" array.
' + - '{string} event - the name of the action that occurred, in camelCase, like \'pressed\'
' + - '{Object} [data] - key/value pairs of arguments for the event, to provide further description of the event.' + - ' It is the programmer\'s responsibility to make sure the optional arguments don\'t collide with the other key names
.', - invocableForReadOnlyElements: false - }, + triggerEvent: { + returnType: VoidIO, + parameterTypes: [ ObjectLiteralIO ], + implementation: function( event ) { + dataStream.trigger( EventType.WRAPPER, event.phetioID, event.componentType, event.name, event.data, event.metadata ); + }, + documentation: 'Start and end a message from the wrapper, interleaving it into the PhET-iO Simulation\'s data stream. Takes an object with entries like:
' + + '{string} phetioID - the id of the specific element that created the event in camelCasing like \'frictionCheckbox\'
' + + '{{typeName: string, events: String[]}} type - The Type that is emitting the event. The event being triggered must be included in the "events" array.
' + + '{string} event - the name of the action that occurred, in camelCase, like \'pressed\'
' + + '{Object} [data] - key/value pairs of arguments for the event, to provide further description of the event.' + + ' It is the programmer\'s responsibility to make sure the optional arguments don\'t collide with the other key names
.', + invocableForReadOnlyElements: false + }, - launchSimulation: { - returnType: VoidIO, - parameterTypes: [], - implementation: function() { - window.phet.joist.launchSimulation(); - }, - documentation: 'Finishes launching the simulation, called from a wrapper after all cross-frame initialization is ' + - 'complete. Note: cannot be invoked with other commands.', - invocableForReadOnlyElements: false - }, + launchSimulation: { + returnType: VoidIO, + parameterTypes: [], + implementation: function() { + window.phet.joist.launchSimulation(); + }, + documentation: 'Finishes launching the simulation, called from a wrapper after all cross-frame initialization is ' + + 'complete. Note: cannot be invoked with other commands.', + invocableForReadOnlyElements: false + }, - getRandomSeed: { - returnType: NumberIO, - parameterTypes: [], - implementation: function() { - return dotRandom.getSeed(); - }, - documentation: 'Gets the random seed, used for replicable playbacks' - }, + getRandomSeed: { + returnType: NumberIO, + parameterTypes: [], + implementation: function() { + return dotRandom.getSeed(); + }, + documentation: 'Gets the random seed, used for replicable playbacks' + }, - setRandomSeed: { - returnType: VoidIO, - parameterTypes: [ NumberIO ], - implementation: function( randomSeed ) { - assert && assert( typeof randomSeed === 'number', 'random seed should be a number' ); - assert && assert( dotRandom.numberOfCalls === 0, ' should not produce random before changing seed to support PhET-iO' ); - dotRandom.setSeed( randomSeed ); + setRandomSeed: { + returnType: VoidIO, + parameterTypes: [ NumberIO ], + implementation: function( randomSeed ) { + assert && assert( typeof randomSeed === 'number', 'random seed should be a number' ); + assert && assert( dotRandom.numberOfCalls === 0, ' should not produce random before changing seed to support PhET-iO' ); + dotRandom.setSeed( randomSeed ); - console.log( `set the seed to ${randomSeed}` ); - }, - documentation: 'Sets the random seed so that the simulation will have reproducible "randomness" between runs. This ' + - 'must be set before the PhET-iO simulation is launched so that all of the random variables will ' + - 'take on deterministic values.', - invocableForReadOnlyElements: false - }, + console.log( `set the seed to ${randomSeed}` ); + }, + documentation: 'Sets the random seed so that the simulation will have reproducible "randomness" between runs. This ' + + 'must be set before the PhET-iO simulation is launched so that all of the random variables will ' + + 'take on deterministic values.', + invocableForReadOnlyElements: false + }, - setDisplaySize: { - returnType: VoidIO, - parameterTypes: [ NumberIO, NumberIO ], - implementation: PhetioEngine.prototype.setDisplaySize, - documentation: 'Sets the size of the visible region for the simulation. In most cases, it would be recommended to ' + - 'set the size of the iframe, but this method can be used to set the size of the display inside the ' + - 'iframe.', - invocableForReadOnlyElements: false - }, + setDisplaySize: { + returnType: VoidIO, + parameterTypes: [ NumberIO, NumberIO ], + implementation: PhetioEngine.prototype.setDisplaySize, + documentation: 'Sets the size of the visible region for the simulation. In most cases, it would be recommended to ' + + 'set the size of the iframe, but this method can be used to set the size of the display inside the ' + + 'iframe.', + invocableForReadOnlyElements: false + }, - setSimStartedMetadata: { - returnType: VoidIO, - parameterTypes: [ ObjectLiteralIO ], - implementation: function( simStartedMetadata ) { + setSimStartedMetadata: { + returnType: VoidIO, + parameterTypes: [ ObjectLiteralIO ], + implementation: function( simStartedMetadata ) { - assert && assert( Tandem.PHET_IO_ENABLED, 'phet-io is somehow not supported; likely a bad bug!' ); + assert && assert( Tandem.PHET_IO_ENABLED, 'phet-io is somehow not supported; likely a bad bug!' ); - // See simInfo.js for the use of this global. - window.phet.preloads.phetio.simStartedMetadata = simStartedMetadata; - }, - documentation: 'Sets additional data that is added to the simStarted event, which will appear in the PhET-iO event ' + - 'stream. It can be used to record startup parameters or other information specified by the wrapper.', - invocableForReadOnlyElements: false - }, + // See simInfo.js for the use of this global. + window.phet.preloads.phetio.simStartedMetadata = simStartedMetadata; + }, + documentation: 'Sets additional data that is added to the simStarted event, which will appear in the PhET-iO event ' + + 'stream. It can be used to record startup parameters or other information specified by the wrapper.', + invocableForReadOnlyElements: false + }, - simulateError: { - returnType: VoidIO, - parameterTypes: [ NullableIO( StringIO ) ], - implementation: function( message ) { - message = message || 'simulated error'; - throw new Error( message ); - }, - documentation: 'Simulates an error in the simulation frame for testing purposes.', - invocableForReadOnlyElements: false - }, + simulateError: { + returnType: VoidIO, + parameterTypes: [ NullableIO( StringIO ) ], + implementation: function( message ) { + message = message || 'simulated error'; + throw new Error( message ); + }, + documentation: 'Simulates an error in the simulation frame for testing purposes.', + invocableForReadOnlyElements: false + }, - getScreenshotDataURL: { - returnType: StringIO, - parameterTypes: [], - implementation: function() { - return window.phet.joist.ScreenshotGenerator.generateScreenshot( phet.joist.sim ); - }, - documentation: 'Gets a base64 string representation of a screenshot of the simulation as a data url.' - } - } - } ); - + getScreenshotDataURL: { + returnType: StringIO, + parameterTypes: [], + implementation: function() { + return window.phet.joist.ScreenshotGenerator.generateScreenshot( phet.joist.sim ); + }, + documentation: 'Gets a base64 string representation of a screenshot of the simulation as a data url.' + } + } } // Cache the types related to a given type for fast access. Index: main/greenhouse-effect/js/common/model/LayersModel.ts IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/main/greenhouse-effect/js/common/model/LayersModel.ts b/main/greenhouse-effect/js/common/model/LayersModel.ts --- a/main/greenhouse-effect/js/common/model/LayersModel.ts (revision 4c5d7982f78e0de525f22211b7ea16fcbaa174f1) +++ b/main/greenhouse-effect/js/common/model/LayersModel.ts (date 1664589543202) @@ -17,7 +17,7 @@ import optionize, { combineOptions } from '../../../../phet-core/js/optionize.js'; import Tandem from '../../../../tandem/js/Tandem.js'; import ArrayIO from '../../../../tandem/js/types/ArrayIO.js'; -import IOType from '../../../../tandem/js/types/IOType.js'; +import { ReferenceIOType } from '../../../../tandem/js/types/IOType.js'; import greenhouseEffect from '../../greenhouseEffect.js'; import GreenhouseEffectConstants from '../GreenhouseEffectConstants.js'; import GreenhouseEffectQueryParameters from '../GreenhouseEffectQueryParameters.js'; @@ -376,13 +376,6 @@ ); } - /** - * Returns a map of state keys and their associated IOTypes, see IOType for details. - */ - public static STATE_SCHEMA: Record = { - emEnergyPackets: ArrayIO( EMEnergyPacket.EMEnergyPacketIO ) - }; - // statics public static readonly HEIGHT_OF_ATMOSPHERE = HEIGHT_OF_ATMOSPHERE; public static readonly SUNLIGHT_SPAN = SUNLIGHT_SPAN; @@ -394,12 +387,11 @@ * serialization', as described in the Serialization section of * https://github.com/phetsims/phet-io/blob/master/doc/phet-io-instrumentation-technical-guide.md#serialization */ - public static readonly LayersModelIO: IOType = new IOType( 'LayersModelIO', { - valueType: LayersModel, - stateSchema: LayersModel.STATE_SCHEMA, - toStateObject: ( a: LayersModel ) => a.toStateObject(), - applyState: ( a: LayersModel, b: LayersModelStateObject ) => a.applyState( b ) - } ); + public static readonly LayersModelIO: ReferenceIOType = LayersModel; + public static typeName = 'LayersModelIO'; + public static stateSchema = { + emEnergyPackets: ArrayIO( EMEnergyPacket.EMEnergyPacketIO ) + }; } type LayersModelStateObject = { ```
samreid commented 1 year ago

At today's quarterly planning meeting, we acknowledged that even though this may be a good long-term direction, we need to focus on other priorities this quarter. Unassigning for now.

samreid commented 1 year ago

The cons about excess property checking sound problematic. I don't think this proposal is the way of the future. Closing.