Model

Model

Models are synchronized objects in Croquet.

They are automatically kept in sync for each user in the same session. Models receive input by subscribing to events published in a View. Their output is handled by views subscribing to events published by a model. Models advance time by sending messages into their future.

Instance Creation and Initialization

To create a new instance, use create(), for example:

this.foo = FooModel.create({answer: 123});

To initialize an instance, override init(), for example:

class FooModel extends Croquet.Model {
    init(options={}) {
        this.answer = options.answer || 42;
    }
}

The reason for this is that Models are only initialized by calling init() the first time the object comes into existence in the session. After that, when joining a session, the models are deserialized from the snapshot, which restores all properties automatically without calling init(). A constructor would be called all the time, not just when starting a session.

Members

# id :String

Each model has an id which can be used to scope events. It is unique within the session.

This property is read-only. There will be an error if you try to assign to it.

It is assigned in Model.create before calling init.

Type:
  • String
Example:
this.publish(this.id, "changed");

# sessionId :String

Identifies the shared session of all users
(as opposed to the viewId which identifies the non-shared views of each user).

The session id is used as "global" scope for events like "view-join".

See Session.join for how the session id is generated.

If your app has several sessions at the same time, each session id will be different.

Type:
  • String
Example:
this.subscribe(this.sessionId, "view-join", this.addUser);

# viewCount :Number

The number of users currently in this session.

All users in a session share the same Model (meaning all model objects) but each user has a different View (meaning all the non-model state). This is the number of views currently sharing this model. It increases by 1 for every "view-join" and decreases by 1 for every "view-exit" event.

Type:
  • Number
Example:
this.subscribe(this.sessionId, "view-join", this.showUsers);
this.subscribe(this.sessionId, "view-exit", this.showUsers);
showUsers() { this.publish(this.sessionId, "view-count", this.viewCount); }

Methods

# (static) create(optionsopt)

Create an instance of a Model subclass.

The instance will be registered for automatical snapshotting, and is assigned an id.

Then it will call the user-defined init() method to initialize the instance, passing the options.

Note: When your model instance is no longer needed, you must destroy it. Otherwise it will be kept in the snapshot forever.

Warning: never create a Model instance using new, or override its constructor. See above.

Parameters:
Name Type Attributes Description
options Object <optional>

option object to be passed to init(). There are no system-defined options, you're free to define your own.

Example
this.foo = FooModel.create({answer: 123});

# (static) register(classId)

Registers this model subclass with Croquet

It is necessary to register all Model subclasses so the serializer can recreate their instances from a snapshot. Since source code minification can change the actual class name, you have to pass a classId explicitly.

Secondly, the session id is derived by hashing the source code of all registered classes. This ensures that only clients running the same source code can be in the same session, so that the synchronized computations are identical for each client.

Important: for the hashing to work reliably across browsers, be sure to specify charset="utf-8" for your <html> or all <script> tags.

Parameters:
Name Type Description
classId String

Id for this model class. Must be unique. If you use the same class name in two files, use e.g. "file1/MyModel" and "file2/MyModel".

Example
class MyModel extends Croquet.Model {
  ...
}
MyModel.register("MyModel")

# (static) wellKnownModel(name) → (nullable) {Model}

Static version of wellKnownModel() for currently executing model.

This can be used to emulate static accessors, e.g. for lazy initialization.

WARNING! Do not store the result in a static variable. Like any global state, that can lead to divergence.

Will throw an error if called from outside model code.

Parameters:
Name Type Description
name String

the name given in beWellKnownAs()

Returns:

the model if found, or undefined

Type
Model
Example
static get Default() {
    let default = this.wellKnownModel("DefaultModel");
    if (!default) {
        console.log("Creating default")
        default = MyModel.create();
        default.beWellKnownAs("DefaultModel");
    }
    return default;
}

# (static) evaluate(func) → {*}

Evaluates func inside of a temporary VM to get bit-identical results, e.g. to init Constants.

Parameters:
Name Type Description
func function

function to evaluate

Since:
  • 1.1.0
Returns:

result of func

Type
*

# (static) types()

Static declaration of how to serialize non-model classes.

The Croquet snapshot mechanism knows about Model subclasses, as well as many JS built-in types (see below), it handles circular references, and it works recursively by converting all non-JSON types to JSON.

If you want to store instances of non-model classes in your model, override this method.

types() needs to return an Object that maps names to class descriptions:

  • the name can be any string, it just has to be unique within your app
  • the class description can either be just the class itself (if the serializer should snapshot all its fields, see first example below), or an object with write() and read() methods to convert instances from and to their serializable form (see second example below).
  • the serialized form answered by write() should return a simpler representation, but it can still contain references to other objects, which will be resolved by the serializer. E.g. if it answers an Array of objects then the serializer will be called for each of those objects. Conversely, these objects will be deserialized before passing the reconstructed Array to read().

Declaring a type in any class makes that declaration available globally. The types only need to be declared once, even if several different Model subclasses are using them.

NOTE: This is currently the only way to customize serialization (for example to keep snapshots fast and small). The serialization of Model subclasses themselves can not be customized, except through "dollar properties":

All properties starting with $ are ignored, e.g. $foo. This can be used for caching big objects that should not appear in the snapshot, but care needs to be taken to make sure that the cache is reconstructed whenever used.

Serialization types supported:

  • all JSON types: number, string, boolean, null, Array, plain Object
  • -0, NaN, Infinity, -Infinity
  • BigInt (since 1.1.0)
  • undefined
  • ArrayBuffer, DataView, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array
  • Set, Map

Not supported:

  • Date: the built-in Date type is dangerous because it implicitly depends on the current timezone which can lead to divergence.
  • RegExp: this has built-in state that can not be introspected and recreated in JS.
  • Function: there is no generic way to serialize functions because closures can not be introspected in JS. Even just for the source code, browsers differ in how they convert functions to strings. If you need to store functions in the model (e.g. for live coding), either wrap the source and function in a custom type (where read would compile the source saved by write), or store the source in a regular property, the function in a dollar property, and have an accessor that compiles the function lazily when needed. (see the source of croquet.io/live for a simple live-coding example)
Examples

To use the default serializer just declare the class:

class MyModel extends Croquet.Model {
  static types() {
    return {
      "SomeUniqueName": MyNonModelClass,
      "THREE.Vector3": THREE.Vector3,        // serialized as '{"x":...,"y":...,"z":...}'
      "THREE.Quaternion": THREE.Quaternion,
    };
  }
}

To define your own serializer, declare read and write functions:

class MyModel extends Croquet.Model {
  static types() {
    return {
      "THREE.Vector3": {
        cls: THREE.Vector3,
        write: v => [v.x, v.y, v.z],        // serialized as '[...,...,...]' which is shorter than the default above
        read: v => new THREE.Vector3(v[0], v[1], v[2]),
      },
      "THREE.Color": {
        cls: THREE.Color,
        write: color => '#' + color.getHexString(),
        read: state => new THREE.Color(state)
      },
    }
  }
}

# init(optionsopt)

This is called by create() to initialize a model instance.

In your Model subclass this is the place to subscribe to events, or start a future message chain.

If you pass {options:...} to Session.join, these will be passed to your root model's init(). Note that options affect the session's persistentId – in most cases, using Croquet.Constants is a better choice to customize what happens in init().

Note: When your model instance is no longer needed, you must destroy it.

Parameters:
Name Type Attributes Description
options Object <optional>

if passed to Session.join

# destroy()

Unsubscribes all subscriptions this model has, unschedules all future messages, and removes it from future snapshots.

Example
removeChild(child) {
   const index = this.children.indexOf(child);
   this.children.splice(index, 1);
   child.destroy();
}

# publish(scope, event, dataopt)

Publish an event to a scope.

Events are the main form of communication between models and views in Croquet. Both models and views can publish events, and subscribe to each other's events. Model-to-model and view-to-view subscriptions are possible, too.

See Model.subscribe() for a discussion of scopes and event names. Refer to View.subscribe() for invoking event handlers asynchronously or immediately.

Optionally, you can pass some data along with the event. For events published by a model, this can be any arbitrary value or object. See View's publish method for restrictions in passing data from a view to a model.

Note that there is no way of testing whether subscriptions exist or not (because models can exist independent of views). Publishing an event that has no subscriptions is about as cheap as that test would be, so feel free to always publish, there is very little overhead.

Parameters:
Name Type Attributes Description
scope String

see subscribe()

event String

see subscribe()

data * <optional>

can be any value or object

Example
this.publish("something", "changed");
this.publish(this.id, "moved", this.pos);

# subscribe(scope, event, handler) → {this}

Register an event handler for an event published to a scope.

Both scope and event can be arbitrary strings. Typically, the scope would select the object (or groups of objects) to respond to the event, and the event name would select which operation to perform.

A commonly used scope is this.id (in a model) and model.id (in a view) to establish a communication channel between a model and its corresponding view.

You can use any literal string as a global scope, or use this.sessionId for a session-global scope (if your application supports multipe sessions at the same time). The predefined events "view-join" and "view-exit" use this session scope.

The handler must be a method of this, e.g. subscribe("scope", "event", this.methodName) will schedule the invocation of this["methodName"](data) whenever publish("scope", "event", data) is executed.

If data was passed to the publish call, it will be passed as an argument to the handler method. You can have at most one argument. To pass multiple values, pass an Object or Array containing those values. Note that views can only pass serializable data to models, because those events are routed via a reflector server (see [View.publish)View#publish).

Parameters:
Name Type Description
scope String

the event scope (to distinguish between events of the same name used by different objects)

event String

the event name (user-defined or system-defined)

handler function

the event handler (must be a method of this)

Returns:
Type
this
Examples
this.subscribe("something", "changed", this.update);
this.subscribe(this.id, "moved", this.handleMove);
class MyModel extends Croquet.Model {
  init() {
    this.subscribe(this.id, "moved", this.handleMove);
  }
  handleMove({x,y}) {
    this.x = x;
    this.y = y;
  }
}
class MyView extends Croquet.View {
  constructor(model) {
    this.modelId = model.id;
  }
  onpointermove(evt) {
     const x = evt.x;
     const y = evt.y;
     this.publish(this.modelId, "moved", {x,y});
  }
}

# unsubscribe(scope, event, handlernullable)

Unsubscribes this model's handler(s) for the given event in the given scope.

To unsubscribe only a specific handler, pass it as the third argument.

Parameters:
Name Type Attributes Description
scope String

see subscribe

event String

see subscribe

handler function <nullable>

(optional) the handler to unsubscribe (added in 1.1)

Example
this.unsubscribe("something", "changed");
this.unsubscribe(this.id, "moved", this.handleMove);

# unsubscribeAll()

Unsubscribes all of this model's handlers for any event in any scope.

# future(tOffset) → {this}

Schedule a message for future execution

Use a future message to automatically advance time in a model, for example for animations. The execution will be scheduled tOffset milliseconds into the future. It will run at precisely this.now() + tOffset.

Use the form this.future(100).methodName(args) to schedule the execution of this.methodName(args) at time this.now() + tOffset.

Hint: This would be an unusual use of future(), but the tOffset given may be 0, in which case the execution will happen asynchronously before advancing time. This is the only way for asynchronous execution in the model since you must not use Promises or async functions in model code (because a snapshot may happen at any time and it would not capture those executions).

Note: the recommended form given above is equivalent to this.future(100, "methodName", arg1, arg2) but makes it more clear that "methodName" is not just a string but the name of a method of this object. Also, this will survive minification. Technically, it answers a Proxy that captures the name and arguments of .methodName(args) for later execution.

See this tutorial for a complete example.

Parameters:
Name Type Default Description
tOffset Number 0

time offset in milliseconds, must be >= 0

Returns:
Type
this
Examples

single invocation with two arguments

this.future(3000).say("hello", "world");

repeated invocation with no arguments

tick() {
    this.n++;
    this.publish(this.id, "count", {time: this.now(), count: this.n)});
    this.future(100).tick();
}

# cancelFuture(method) → {Boolean}

Cancel a previously scheduled future message

This unschedules the invocation of a message that was scheduled with future. It is okay to call this method even if the message was already executed or if it was never scheduled.

Note: as with future, the recommended form is to pass the method itself, but you can also pass the name of the method as a string.

Parameters:
Name Type Description
method function

the method (must be a method of this) or "*" to cancel all of this object's future messages

Since:
  • 1.1.0
Returns:

true if the message was found and canceled, false if it was not found

Type
Boolean
Example
this.future(3000).say("hello", "world");
...
this.cancelFuture(this.say);

# random() → {Number}

Generate a synchronized pseudo-random number

This returns a floating-point, pseudo-random number in the range 0–1 (inclusive of 0, but not 1) with approximately uniform distribution over that range (just like Math.random).

Since the model computation is synchronized for every user on their device, the sequence of random numbers generated must also be exactly the same for everyone. This method provides access to such a random number generator.

Returns:
Type
Number

# now() → {Number}

The model's current time

Time is discreet in Croquet, meaning it advances in steps. Every user's device performs the exact same computation at the exact same virtual time. This is what allows Croquet to do perfectly synchronized computation.

Every event handler and future message is run at a precisely defined moment in virtual model time, and time stands still while this execution is happening. That means if you were to access this.now() in a loop, it would never answer a different value.

The unit of now is milliseconds (1/1000 second) but the value can be fractional, it is a floating-point value.

See:
Returns:

the model's time in milliseconds since the first user created the session.

Type
Number

# beWellKnownAs(name)

Make this model globally accessible under the given name. It can be retrieved from any other model in the same session using wellKnownModel().

Hint: Another way to make a model well-known is to pass a name as second argument to Model.create().

Note: The instance of your root Model class is automatically made well-known as "modelRoot" and passed to the constructor of your root View during Session.join.

Parameters:
Name Type Description
name String

a name for the model

Example
class FooManager extends Croquet.Model {
  init() {
    this.beWellKnownAs("UberFoo");
  }
}
class Underlings extends Croquet.Model {
  reportToManager(something) {
    this.wellKnownModel("UberFoo").report(something);
  }
}

# getModel(id) → {Model}

Look up a model in the current session given its id

Parameters:
Name Type Description
id String

the model's id

Returns:

the model if found, or undefined

Type
Model
Example
const otherModel = this.getModel(otherId);

# wellKnownModel(name) → (nullable) {Model}

Access a model that was registered previously using beWellKnownAs().

Note: The instance of your root Model class is automatically made well-known as "modelRoot" and passed to the constructor of your root View during Session.join.

Parameters:
Name Type Description
name String

the name given in beWellKnownAs()

Returns:

the model if found, or undefined

Type
Model
Example
const topModel = this.wellKnownModel("modelRoot");

# modelOnly(msgopt) → {Boolean}

This methods checks if it is being called from a model, and throws an Error otherwise.

Use this to protect some model code against accidentally being called from a view.

Parameters:
Name Type Attributes Description
msg String <optional>

error message to display

Throws:

Error if called from view

Returns:

true (otherwise, throws Error)

Type
Boolean
Example
get foo() { return this._foo; }
set foo(value) { this.modelOnly(); this._foo = value; }