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");

# activeSubscription

Scope, event, and source of the currently executing subscription handler.

The source is either "model" or "view".

Since:
  • 2.0
Example:
// this.subscribe("*", "*", this.logEvents)
logEvents(data: any) {
    const {scope, event, source} = this.activeSubscription;
    console.log(`${this.now()} Event in model from ${source} ${scope}:${event} with`, data);
}

# 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 the "view-join" event.

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 is increased by 1 before every "view-join" event and decreased by 1 before every "view-exit" event handler is executed.

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, persistentDataopt)

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.

persistentData Object <optional>

passed to init(), if provided.

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) isExecuting() → {Boolean}

Check if currently executing code is inside a model.

Since:
  • 2.0
Returns:

true if currently executing code is inside a model

Type
Boolean

# (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), and (since v2.0) writeStatic() and readStatic() to serialize and restore static properties.
  • 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:

  • plain Object, Array, number, string, boolean, null: just like JSON
  • -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.
  • WeakMap, WeakSet: these are not enumerable and can not be serialized.
  • Symbol: these are unique and can not be serialized.
  • Function, Promise, Generator etc: 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 {
     "SomeUniqueName": {
         cls: MyNonModelClass,
         write: obj => obj.serialize(),  // answer a serializable type, see above
         read: state => MyNonModelClass.deserialize(state), // answer a new instance
         writeStatic: () => ({foo: MyNonModelClass.foo}),
         readStatic: state => MyNonModelClass.foo = state.foo,
      },
      "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, persistentDataopt)

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

If you called persistSession in a previous session (same name, same options, different code base), that data will be passed as persistentData to your root model's init(). Based on that data you should re-create submodels, subscriptions, future messages etc. to start the new session in a state similar to when it was last saved.

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

persistentData Object <optional>

data previously stored by persistSession

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

If you subscribe inside the model to an event that is published by the model, the handler will be called immediately, before the publish method returns. If you want to have it handled asynchronously, you can use a future message:

this.future(0).publish("scope", "event", data);

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 "view-join" event and "view-exit" event 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; }

# createQFunc(env, func) → {function}

Create a serializable function that can be stored in the model.

Plain functions can not be serialized because they may contain closures that can not be introspected by the snapshot mechanism. This method creates a serializable "QFunc" from a regular function. It can be stored in the model and called like the original function.

The function can only access global references (like classes), all local references must be passed in the env object. They are captured as constants at the time the QFunc is created. Since they are constants, re-assignments will throw an error.

In a fat-arrow function, this is bound to the model that called createQFunc, even in a different lexical scope. It is okay to call a model's createQFunc from anywhere, e.g. from a view. QFuncs can be passed from view to model as arguments in publish() (provided their environment is serializable).

Warning: Minification can change the names of local variables and functions, but the env will still use the unminified names. You need to disable minification for source code that creates QFuncs with env. Alternatively, you can pass the function's source code as a string, which will not be minified.

Behind the scenes, the function is stored as a string and compiled when needed. The env needs to be constant because the serializer would not able to capture the values if they were allowed to change.

Parameters:
Name Type Description
env Object

an object with references used by the function

func function | String

the function to be wrapped, or a string with the function's source code

Since:
  • 2.0
Returns:

a serializable function bound to the given environment

Type
function
Example
const template = { greeting: "Hi there," };
this.greet = this.createQFunc({template}, (name) => console.log(template.greeting, name));
this.greet(this, "friend"); // logs "Hi there, friend"
template.greeting = "Bye now,";
this.greet(this, "friend"); // logs "Bye now, friend"

# persistSession(collectDataFunc)

Store an application-defined JSON representation of this session to be loaded into future sessions. This will be passed into the root model's init method if resuming a session that is not currently ongoing (e.g. due to changes in the model code).

Note: You should design the JSON in a way to be loadable in newer versions of your app. To help migrating incompatible data, you may want to include a version identifier so a future version of your init can decide what to do.

Warning Do NOT use JSON.stringify because the result is not guaranteed to have the same ordering of keys everywhere. Instead, store the JSON data directly and let Croquet apply its stable stringification.

Also you must only call persistSession() from your root model. If there are submodels, your collectDataFunc should collect data from all submodels. Similarly, only your root model's init will receive that persisted data. It should recreate submodels as necessary.

Croquet will not interpret this data in any way (e.g. not even the version property in the example below). It is stringified, encrypted, and stored.

Parameters:
Name Type Description
collectDataFunc function

method returning information to be stored, will be stringified as JSON

Example
class SimpleAppRootModel {
    init(options, persisted) {
        ...                         // regular setup
        if (persisted) {
            if (persisted.version === 1) {
                ...                 // init from persisted data
            }
        }
    }

    save() {
        this.persistSession(() => {
            const data = {
               version: 1,         // for future migrations
               ...                 // data to persist
            };
            return data;
        });
    }
}