Model

Model

Models are replicated 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

Since:
  • 0.4.1

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 as of now, 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 replicated 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}

Since:
  • 0.4.1

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) types()

Static declaration of how to serialize non-model classes.

The Croquet snapshot mechanism only knows about Model subclasses. 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() can be almost anything. 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 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.

Serialization types supported in addition to JSON:

  • -0, NaN, Infinity
  • undefined
  • ArrayBuffer, DataView, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array
  • Set, Map

How to serialize...

  • Functions: wrap them in non-model class, serialize as source-code, deserialize using new Function(...)
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: When your model instance is no longer needed, you must destroy it.

Parameters:
Name Type Attributes Description
options Object <optional>

there are no system-defined options, you're free to define your own

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)

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

Parameters:
Name Type Description
scope String

see subscribe

event String

see subscribe

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. 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();
}

random() → {Number}

Generate a replicated 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 replicated 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}

See:

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 replicated 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.

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; }