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.
Name | Type | Attributes | Description |
---|---|---|---|
options |
Object |
<optional> |
option object to be passed to init().
There are no system-defined |
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.
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. |
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.
Name | Type | Description |
---|---|---|
name |
String | the name given in beWellKnownAs() |
the model if found, or undefined
- Type
- Model
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.
Name | Type | Description |
---|---|---|
func |
function | function to evaluate |
- Since:
- 1.1.0
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()
andread()
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 toread()
.
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
, plainObject
-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 (whereread
would compile the source saved bywrite
), 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)
class MyModel extends Croquet.Model {
static types() {
return {
"SomeUniqueName": MyNonModelClass,
"THREE.Vector3": THREE.Vector3, // serialized as '{"x":...,"y":...,"z":...}'
"THREE.Quaternion": THREE.Quaternion,
};
}
}
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.
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.
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.
Name | Type | Attributes | Description |
---|---|---|---|
scope |
String | see subscribe() |
|
event |
String | see subscribe() |
|
data |
* |
<optional> |
can be any value or object |
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).
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 |
- Type
- this
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.
Name | Type | Attributes | Description |
---|---|---|---|
scope |
String | see subscribe |
|
event |
String | see subscribe |
|
handler |
function |
<nullable> |
(optional) the handler to unsubscribe (added in 1.1) |
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.
Name | Type | Default | Description |
---|---|---|---|
tOffset |
Number | 0 | time offset in milliseconds, must be >= 0 |
- Type
- this
this.future(3000).say("hello", "world");
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.
Name | Type | Description |
---|---|---|
method |
function | the method (must be a method of |
- Since:
- 1.1.0
true if the message was found and canceled, false if it was not found
- Type
- Boolean
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.
- 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:
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.
Name | Type | Description |
---|---|---|
name |
String | a name for the model |
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
Name | Type | Description |
---|---|---|
id |
String | the model's |
the model if found, or undefined
- Type
- Model
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.
Name | Type | Description |
---|---|---|
name |
String | the name given in beWellKnownAs() |
the model if found, or undefined
- Type
- Model
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.
Name | Type | Attributes | Description |
---|---|---|---|
msg |
String |
<optional> |
error message to display |
Error if called from view
true (otherwise, throws Error)
- Type
- Boolean
get foo() { return this._foo; }
set foo(value) { this.modelOnly(); this._foo = value; }