Skip to content

Migrating to version 0.15 (from 0.14)

See version 0.15 announcement blog post.


Schema's .triggerAll() has been deprecated

Now, whenever you register an onAdd() callback, it is called immediately for already existing items - thus, making the usage of .triggerAll() not necessary anymore.

You can prevent the onAdd() callback from being triggered automatically, though, if necessary.

state.players.onAdd(() => {
    /*
     * a player has been added
     */
}, false);

By providing false in the second argument, the onAdd() callback is not going to be triggered for already existing items.


Schema callbacks API slightly changed

Now, instead of assigning a single callback per onAdd/onChange/onRemove, you attach them by calling as a method instead. You can attach more than one callback this way.

See example below:

// 0.14 (old)
state.players.onAdd = function(value, key) {/* do stuff */};
state.players.onChange = function(value, key) {/* do stuff */};
state.players.onRemove = function(value, key) {/* do stuff */};

// 0.15 (new)
state.players.onAdd(function(value, key) {/* do stuff */});
state.players.onChange(function(value, key) {/* do stuff */});
state.players.onRemove(function(value, key) {/* do stuff */});
// 0.14 (old)
state.players.OnAdd += (key, value) => {/* do stuff */};
state.players.OnChange += (key, value) => {/* do stuff */};
state.players.OnRemove += (key, value) => {/* do stuff */};

// 0.15 (new)
state.players.OnAdd((key, value) => {/* do stuff */})
state.players.OnChange((key, value) => {/* do stuff */})
state.players.OnRemove((key, value) => {/* do stuff */})
-- 0.14 (old)
state.players.on_add = function(value, key) --[[ do stuff ]] end
state.players.on_change = function(value, key) --[[ do stuff ]] end
state.players.on_remove = function(value, key) --[[ do stuff ]] end

-- 0.15 (new)
-- ATTENTION: this is a method call. make sure to use `:` instead of `.` here.
state.players:on_add(function(value, key) --[[ do stuff ]] end)
state.players:on_change(function(value, key) --[[ do stuff ]] end)
state.players:on_remove(function(value, key) --[[ do stuff ]] end)
// 0.14 (old)
state.players.onAdd = function(value, key) {/* do stuff */};
state.players.onChange = function(value, key) {/* do stuff */};
state.players.onRemove = function(value, key) {/* do stuff */};

// 0.15 (new)
state.players.onAdd(function(value, key) {/* do stuff */});
state.players.onChange(function(value, key) {/* do stuff */});
state.players.onRemove(function(value, key) {/* do stuff */});

The return value of onAdd()/onChange()/onRemove() is a function that can detach the callback that have been added.

const detachCallback = state.players.onAdd(function(value, key) {/* do stuff */});

// detaches the onAdd callback.
detachCallback();
var detachCallback = state.players.OnAdd((key, value) => {/* do stuff */})

// detaches the onAdd callback.
detachCallback();
local detach_callback = state.players:on_add(function(value, key) --[[ do stuff ]] end)

-- detaches the onAdd callback.
detach_callback();
var detachCallback = state.players.onAdd(function(value, key) {/* do stuff */});

// detaches the onAdd callback.
detachCallback();

Schema's onChange behaviour change

Previously, the onChange callback wouldn't be fired during onAdd and onRemove when attached to a collection of items (MapSchema, ArraySchema, etc).

Now, onChange is triggered alongside with onAdd and onRemove.


MapSchema is now strict on property accessors

Only JavaScript/TypeScript is affected. If you use a client-side SDK other than JavaScript/TypeScript, no change is needed on the client-side for you.

// 0.14 (old)
this.state.players[client.sessionId] = new Player();

// 0.15 (new)
this.state.players.set(client.sessionId, new Player());

Reasoning: MapSchema used to be treated as a regular JavaScript object in the early days. Since version 0.14 MapSchema uses a real Map internally, with a "proxy" compatibility layer in order to avoid breaking existing projects. Now the "proxy" layer has been removed, improving the performance slightly.


client.reconnect() API slightly changed

The previous reconnection implementation had a security vulnerability, although very unlikely to be explored, we had to update its implementation to make it secure.

// 0.14 (old)
client.reconnect(cachedRoomId, cachedSessionId)

// 0.15 (new)
client.reconnect(cachedReconnectionToken)

Insterad of providing the previously active room.roomId and room.sessionId for reconnection, you only provide the room.reconnectionToken instead.

Reconnection tokens are unique and private for each client.


allowReconnection(): second argument is now mandatory

Previously, by omitting the second argument of allowReconnection(), you were in control over when to cancel the possibility for reconnection.

To make this intent more explicit, it is now mandatory to provide the second argument either as "manual", or the number of seconds to wait for reconnection:

async onLeave (client: Client, consented: boolean) {
  // ...
  try {
    if (consented) { throw new Error("consented leave"); }

    //
    // Get reconnection token
    // NOTE: do not use `await` here yet!
    //
    const reconnection = this.allowReconnection(client, "manual");

    //
    // here is the custom logic for rejecting the reconnection.
    // for demonstration purposes of the API, an interval is created
    // rejecting the reconnection if the player has missed 2 rounds,
    // (assuming he's playing a turn-based game)
    //
    // in a real scenario, you would store the `reconnection` in
    // your Player instance, for example, and perform this check during your
    // game loop logic
    //
    const currentRound = this.state.currentRound;
    const interval = setInterval(() => {
      if ((this.state.currentRound - currentRound) > 2) {
        // manually reject the client reconnection
        reconnection.reject();
        clearInterval(interval);
      }
    }, 1000);

    // now it's time to `await` for the reconnection
    await reconnection;

    // client returned! let's re-activate it.
    // ...

  } catch (e) {

    // reconnection has been rejected. let's remove the client.
    // ...
  }
}
async onLeave (client: Client, consented: boolean) {
  try {
    if (consented) { throw new Error("consented leave"); }

    // allow disconnected client to reconnect into this room until 20 seconds
    await this.allowReconnection(client, 20);

  } catch (e) {

    // 20 seconds expired. reconnection not successful.
  }
}

@colyseus/loadtest has been reworked!

The loadtest tool has been reworked to allow for more complex scripting, so your loadtest scripts will need to be slightly rewritten, as the new format looks like this:

import { Client, Room } from "colyseus.js";
import { Options } from "@colyseus/loadtest";

export async function main(options: Options) {
    const client = new Client(options.endpoint);
    const room: Room = await client.joinOrCreate(options.roomName, {/*
        your join options here...
    */});

    console.log("joined successfully!");

    room.onMessage("message-type", (payload) => {
        // logic
    });

    room.onStateChange((state) => {
        console.log("state change:", state);
    });

    room.onLeave((code) => {
        console.log("left");
    });
}

@colyseus/command typings update

On latest version of @colyseus/command (0.2.0), instead of providing the state as a generic when extending the Command, you now provide your whole Room type:

import { Command } from "@colyseus/command";
export class MyRoom extends Room<State> {/* ... */}
-export class MyCommand extends Command<State> {/* ... */}
+export class MyCommand extends Command<MyRoom> {/* ... */}


Built-in client.auth is gone! @colyseus/social fully deprecated.

The documentation has been discouraging the use of @colyseus/social since version 0.14.

Now @colyseus/social has been officially deprecated. If you really rely on it please reach out to the devs on Discord on how to proceed if you still need to use it.

Back to top