@hcikn/colibri 1.3.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 HCI Group University Konstanz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,156 +1,298 @@
1
- # Colibri - Web Client
1
+ # Colibri Web
2
+
3
+ TypeScript client for Colibri supporting messages between clients, synchronized objects, data store and monitoring.
4
+ Voice chat is currently not supported on the web.
5
+
6
+ Documentation: [colibri.hci.uni-konstanz.de](https://colibri.hci.uni-konstanz.de/web/installation)
7
+
8
+ Upgrading from Colibri 1.x: [Migration Guide](../MIGRATION.md).
9
+
10
+ ## Requirements
11
+
12
+ - Node.js 18 or newer
13
+ - TypeScript 5.0 or newer with standard decorators. Leave `experimentalDecorators` unset or `false` in
14
+ `tsconfig.json`. Vite needs a plugin for them: see
15
+ [Vite](https://colibri.hci.uni-konstanz.de/web/installation#vite).
16
+ - [colibri-server](../colibri-server/README.md#installation) 2.0.0 or newer
2
17
 
3
18
  ## Installation
4
19
 
5
- - NPM: `npm install @hcikn/colibri`
6
- - Yarn: `yarn add @hcikn/colibri`
20
+ ### npm (recommended)
21
+
22
+ ```sh
23
+ npm install @hcikn/colibri@2
24
+ ```
25
+
26
+ Keep the `@2`. Without it, npm may install a 1.x release. The peer dependency `rxjs` is installed automatically.
27
+
28
+ ### Building from source
29
+
30
+ To build the package from the repository:
31
+
32
+ ```sh
33
+ cd colibri-web
34
+ npm ci
35
+ npm run build
36
+ npm pack
37
+ ```
38
+
39
+ Copy the resulting `hcikn-colibri-<version>.tgz` into your project and install it with
40
+ `npm install ./hcikn-colibri-<version>.tgz`.
7
41
 
8
42
  ## Configuration
9
43
 
10
- Initialize Colibri once with:
44
+ Create the Colibri client once, when your app starts:
11
45
 
12
46
  ```ts
13
47
  import { Colibri } from '@hcikn/colibri';
14
- new Colibri('app_name', 'server_address');
15
48
 
16
- // if using a custom port:
17
- new Colibri('app_name', 'server_address', 9011);
49
+ new Colibri('myAppName', 'colibri.hci.uni-konstanz.de');
18
50
  ```
19
51
 
20
- For server setup, refer to [colibri-server](../colibri-server/).
52
+ - The first argument is the **app name**. Set it to a unique name that is not used on the server. Clients exchange
53
+ data only if their app names match, web and Unity alike. Do not use `colibri`, which the server's web page uses.
54
+ - The second argument is the **server address**: a host name or IP address, optionally with `http://` or
55
+ `https://`. A public test server is available: `colibri.hci.uni-konstanz.de`. You can leave it out when colibri-server runs on the server that served your page.
56
+ - A second `new Colibri()` throws an error. Elsewhere in your app, use `Colibri.getInstance()`.
21
57
 
22
- ## Usage
58
+ ## Samples
23
59
 
24
- ### Web Interface for Logging
60
+ The [samples](samples/) are in the `colibri-web` folder of the repository. They run in a terminal with Node.js:
25
61
 
26
- Colibri provides a _web logger_ with web interface to send diagnostic data (currently: console logs) to the server. This may be useful for devices (e.g., VR devices, smartphones) where access to the console is not easily available.
62
+ ```sh
63
+ cd colibri-web
64
+ npm ci
65
+ npm run samples/broadcast
66
+ ```
27
67
 
28
- To setup, import the `RemoteLogger` and call its `init()` method. Any subsequent `console` calls should now also appear on your colibri server's web interface, which can be accessed via `http://<your-server-ip>:9011`.
68
+ Each sample asks for the server address and port. Press Enter twice for the public test server. To see two clients
69
+ exchange data, start the same sample in a second terminal, or run it next to a Unity client whose app name is
70
+ `myAppName`, which the samples use.
29
71
 
30
- ```ts
31
- import { RemoteLogger } from '@hcikn/colibri';
32
- const logger = new RemoteLogger();
72
+ | Sample | Shows |
73
+ | ------------------------------------------- | -------------------------------------------------- |
74
+ | [broadcast](samples/broadcast.ts) | Sending and receiving messages |
75
+ | [model-sync](samples/model-sync.ts) | Synchronized objects with `SyncModel` |
76
+ | [remote-logging](samples/remote-logging.ts) | Sending the console log to the server |
77
+ | [rest-api](samples/rest-api.ts) | Saving and loading values in the data store |
78
+ | [sync-transform](samples/sync-transform.ts) | Showing and moving Unity's `SyncTransform` objects |
79
+ | [webxr](samples/webxr/) | The same in WebXR with three.js, in the browser |
33
80
 
34
- // en-/disable RemoteLogger
35
- logger.enable();
36
- logger.disable();
37
- ```
81
+ `npm run samples/webxr` serves the WebXR sample at `https://localhost:5173`, also to headsets in the same network.
82
+ How to open it and choose the server: [SyncTransform in WebXR](https://colibri.hci.uni-konstanz.de/web/synctransform#in-webxr).
38
83
 
39
- See also [the remote-logging sample](samples/remote-logging.ts) (run sample with `npm run sample/remote-logging`).
84
+ ## Usage
40
85
 
41
- ### Sending Data between Clients
86
+ ### Send/Receive Messages
42
87
 
43
- Colibri supports simple data transmission via pub/sub communication. Data can be published from anywhere in
44
- the executed code, as illustrated with the following simple example of sending a boolean value on a "click" channel:
88
+ For sending simple messages of events and commands you can use `Sync.send*`. Each message has a channel name.
45
89
 
46
90
  ```ts
47
- import { Sync } from '@hcikn/colibri';
48
- Sync.sendBool('myChannel', true);
91
+ const myNumber = 5;
92
+ Sync.sendNumber('myChannel', myNumber);
49
93
  ```
50
94
 
51
- The sent data can then be received anywhere by registering a listener:
95
+ A client receives the messages of every channel it listens to:
52
96
 
53
97
  ```ts
54
- Sync.receiveBool('myChannel', (value) => {
55
- // Will be called whenever a bool on "click" channel is received
56
- });
98
+ function onMyChannel(myNumber: number) {
99
+ // This code is executed whenever
100
+ // a number is received on 'myChannel'
101
+ }
102
+
103
+ Sync.receiveNumber('myChannel', onMyChannel);
57
104
  ```
58
105
 
59
- The listener can be deregistered via:
106
+ A full example:
60
107
 
61
108
  ```ts
62
- Sync.unregister('myChannel', MyMethod);
63
- ```
109
+ import { Sync } from '@hcikn/colibri';
110
+
111
+ function onStartRound(round: number) {
112
+ console.log(`Round ${round} starts`);
113
+ // reset the scores, start the timer, ...
114
+ }
64
115
 
65
- The following built-in types are available for sync: `bool, int (as number), float (as number), string, Vector2, Vector3, Quaternion, Color` and arrays thereof. For arbitrary data, you can use JSON:
116
+ // Register before anything is sent. Messages without a listener are dropped.
117
+ Sync.receiveNumber('startRound', onStartRound);
66
118
 
67
- ```c#
68
- Sync.sendJson('myChannel', { foo: 'bar' });
69
- Sync.receiveJson('myChannel', (json) => { /* ... */ });
119
+ // Reaches all other clients with the same app name, not this one.
120
+ export function startRound(round: number) {
121
+ Sync.sendNumber('startRound', round);
122
+ onStartRound(round);
123
+ }
70
124
  ```
71
125
 
72
- See also [the broadcast sample](samples/broadcast.ts) (run sample with `npm run sample/broadcast`).
126
+ The script listens on the channel `startRound` for numbers. One player calls `startRound`, e.g. from a button,
127
+ which sends the round number to all other clients, web and Unity. On each of them, Colibri calls `onStartRound`,
128
+ which starts the round. A client does not receive its own messages, so `startRound` also calls `onStartRound`
129
+ directly to start the round on the sending client too.
73
130
 
74
- Limitations:
131
+ - Channel and type must match on both sides.
132
+ - Supported types: `sendBool`, `sendNumber`, `sendString`, `sendVector2`, `sendVector3`, `sendQuaternion`,
133
+ `sendColor`, their array forms such as `sendVector3Array`, and `sendJson` for anything else. Each has a matching
134
+ `receive*` ([details](https://colibri.hci.uni-konstanz.de/web/messages#types)).
135
+ - JavaScript has only one number type, so every number is sent as `float`. Unity must receive it with
136
+ `Sync.Receive<float>`, not `Sync.Receive<int>`.
137
+ - A listener stays registered until `Sync.unregister`.
75
138
 
76
- - You have to register the listener _before_ sending out data
77
- - Type and channel _must_ match between Listener and Sender (`number` will be converted to float for Unity clients, i.e., use `float` listener on Unity clients for sending numbers!)
78
- - Remember to unregister your listener where necessary!
139
+ ### SyncTransform
79
140
 
80
- ### SyncModel
141
+ `SyncTransform` gives the web the objects of Unity's `SyncTransform` component: their position, rotation, scale
142
+ and visibility, e.g. to show a shared 3D object on a web page or to move it from there.
81
143
 
82
- (Counterpart to [SyncBehaviour in Unity client](../colibri-unity#SyncBehaviour))
144
+ ```ts
145
+ import { RegisterSyncTransforms } from '@hcikn/colibri';
83
146
 
84
- For more complex scenarios, Colibri supports synchronization of data models (e.g., for use in model-view-controller architectures). For this, we need extend the _Model_ with `SyncModel`:
147
+ const [transforms$] = RegisterSyncTransforms();
85
148
 
86
- ```ts
87
- import { SyncModel, Synced } from '@hcikn/colibri';
149
+ transforms$.subscribe(transforms => {
150
+ for (const transform of transforms) {
151
+ console.log(transform.id, transform.position, transform.rotation, transform.scale, transform.active);
152
+ }
153
+ });
154
+ ```
88
155
 
89
- export class SampleClass extends SyncModel<SampleClass> {
90
- @Synced()
91
- get name() { return this._name; }
92
- set name(val: string) { this._name = val; }
93
- private _name = '';
156
+ `RegisterSyncTransforms` returns `transforms$`, an Observable of all `SyncTransform` objects of the app. It emits
157
+ the list whenever an object is added, moved, shown, hidden or deleted on any client.
94
158
 
95
- @Synced()
96
- private age = 0;
159
+ To move an object, assign a new position to it, e.g. when the user drags it in a 3D view. Colibri sends it to all
160
+ other clients, web and Unity:
97
161
 
98
- @Synced('billingAddress')
99
- private address = '';
100
- }
162
+ ```ts
163
+ transform.position = [1, 0, 2];
101
164
  ```
102
165
 
103
- and enable `experimentalDecorators` in the `tsconfig.json`:
166
+ - Positions and scales are `[x, y, z]`, rotations `[x, y, z, w]`, in Unity's coordinates: left-handed with Y up.
167
+ three.js and most other web 3D libraries are right-handed. To convert, negate `z` of positions, and `x` and `y` of
168
+ rotations. Scales need no conversion.
169
+ - Assign a new array to change a value. Changing an array in place, e.g. `position[0] = 1`, is not sent.
170
+ - Unity objects with a `ModelId` sync on a channel of their own. Pass it: `RegisterSyncTransforms('<ModelId>')`.
171
+ - Call `RegisterSyncTransforms` once per `ModelId` and share what it returns.
172
+ - Move an object from one client at a time. Concurrent updates make it jitter.
173
+
174
+ ### SyncModel
175
+
176
+ `SyncModel` synchronizes the properties of your own classes between clients, e.g. a score, a selection or the state
177
+ of an experiment. It is the web counterpart of Unity's `SyncBehaviour`, and both sync with each other.
178
+
179
+ ```ts
180
+ import { RegisterModelSync, SyncModel, Synced } from '@hcikn/colibri';
104
181
 
105
- ```json
106
- {
107
- "compilerOptions": {
108
- "experimentalDecorators": true
109
- }
182
+ class Player extends SyncModel<Player> {
183
+ @Synced()
184
+ accessor score = 0;
110
185
  }
111
186
 
187
+ const [players$, registerPlayer] = RegisterModelSync<Player>({ name: 'player', type: Player });
188
+
189
+ players$.subscribe(players => {
190
+ console.log(players.map(p => `${p.id}: ${p.score}`));
191
+ });
192
+
193
+ const me = new Player(`player-${Date.now()}`);
194
+ registerPlayer(me);
195
+ me.score = 10;
112
196
  ```
113
197
 
114
- Any property or field marked with `@Synced()` will be synchronized across all network clients. *Note: Synchronization of fields (e.g., `@Synced() private age = 0;` does not work on some frameworks such as React for some reason.*
198
+ `Player` inherits from `SyncModel<Player>`. `@Synced()` marks `score` for syncing: when one client changes it, it
199
+ changes on all other clients. `RegisterModelSync` returns `players$`, an [RxJS](https://rxjs.dev/guide/observable)
200
+ Observable of all players, and `registerPlayer`. `players$` emits the list whenever a player is added, changed or
201
+ deleted, also by another client. `registerPlayer` starts syncing a new player. Its id must be unique across all
202
+ clients.
115
203
 
116
- Lastly, we need to register the class with the Synchronization mechanism by calling `RegisterModelSync`:
204
+ - Declare each synced property with `accessor`. Without it, `@Synced()` does not compile.
205
+ - Always pass `name`. Without it, the channel is the class name in lower case, which a minified production build
206
+ changes.
207
+ - To sync with a Unity `SyncBehaviour`, set `name` to the Unity class name in lower case, e.g. `'player'` for the
208
+ `Player` in the [Unity README](../colibri-unity/README.md#syncbehaviour). Property names are compared in lower
209
+ case, so `score` matches Unity's `Score` ([details](https://colibri.hci.uni-konstanz.de/web/syncmodel)).
210
+ - `delete()` deletes an object for every client.
117
211
 
118
- ```ts
119
- import { RegisterModelSync } from '@hcikn/colibri';
120
- const [ SampleClasses$, registerExampleClass ] = RegisterModelSync<SampleClass>({ type: SampleClass });
121
- ```
212
+ ### Monitoring
122
213
 
123
- The first return value (e.g., `SampleClasses$`) is a [BehaviorSubject](https://www.learnrxjs.io/learn-rxjs/subjects/behaviorsubject) that will be updated whenever a new instance of SampleClass is added, updated, or deleted. The second return value (e.g., `registerExampleClass`) can be used to sync new instantiations:
214
+ The server's web page at `http://<server-address>:9211` shows the connected clients with their latency and
215
+ throughput, and the synchronized objects. To also see a client's console log there, create a `RemoteLogger` when
216
+ your app starts:
124
217
 
125
218
  ```ts
126
- const mySample = new SampleClass('myId'); // mySample is not synchronized across clients yet
127
- registerExampleClass(mySample); // mySample is sent out to all other clients and will be synchronized
219
+ import { RemoteLogger } from '@hcikn/colibri';
220
+
221
+ new RemoteLogger();
128
222
  ```
129
223
 
130
- See also [the model-sync sample](samples/model-sync.ts) (run sample with `npm run sample/model-sync`).
224
+ From then on, everything logged with `console` also appears on the server's web page. This is the easiest way to
225
+ read the log of a web app on a smartphone or head-mounted display ([details](https://colibri.hci.uni-konstanz.de/web/monitoring)).
131
226
 
132
- ### Remote Store
227
+ ### Data store
133
228
 
134
- Colibri offers persistent data storage on the server, so that data can be shared easily between connected clients. Each object is identified by it's individual `key`:
229
+ The data store keeps small amounts of data on the server, e.g. the color a player selected, the settings of a game
230
+ or the condition of a study. Unlike messages and synchronized objects, the data stays saved after all clients
231
+ disconnect.
135
232
 
136
233
  ```ts
137
- import { Colibri, GetRestApi, PutRestApi } from '@hcikn/colibri';
234
+ import { GetRestApi, PutRestApi, toHexColor, toRgbaColor } from '@hcikn/colibri';
138
235
 
139
- const colibri = Colibri.getInstance();
140
- const key = 'sampleKey';
141
- const data = {...};
236
+ const playerName = 'Alex';
237
+ const avatar = document.getElementById('avatar')!;
142
238
 
143
- // fetch data
144
- colibri.getRestObject(key); // or...
145
- GetRestApi(key);
239
+ async function loadColor() {
240
+ const color = await GetRestApi(`color-${playerName}`);
241
+ if (color) {
242
+ avatar.style.backgroundColor = toHexColor(color);
243
+ }
244
+ }
146
245
 
147
- // update data
148
- colibri.setRestObject(key, data); // or...
149
- PutRestApi(key, data);
246
+ // Call this when the player selects a color, e.g. '#ff0000ff'.
247
+ async function selectColor(color: string) {
248
+ avatar.style.backgroundColor = color;
249
+ await PutRestApi(`color-${playerName}`, toRgbaColor(color));
250
+ }
150
251
  ```
151
252
 
152
- Each operation can be achieved by either directly interaction with the `Colibri` object or using the corresponding wrapper method.
253
+ `selectColor` colors the player's avatar and saves the color on the server under the name `color-<player name>`.
254
+ `loadColor` loads the saved color, so the player gets their color back in the next session. `GetRestApi` returns
255
+ `null` if the player has not selected a color yet. The color is saved as `[r, g, b, a]`, the format Unity uses, so
256
+ the same value also works in the [Unity README's](../colibri-unity/README.md#data-store) `PlayerColor` sample. Other
257
+ clients see a new value the next time they call `GetRestApi`. It is not sent to them automatically.
153
258
 
154
- ## Samples
259
+ - Values are saved per app name. Each value can be up to 5 MiB.
260
+ - Any JSON value works, e.g. an object, an array, a number or a string.
261
+ - Create the `Colibri` instance first. Without it, both functions return `undefined`.
262
+
263
+ ### Voice chat
264
+
265
+ Voice chat is currently not supported on the web. It is available in [colibri-unity](../colibri-unity/README.md#voice-chat).
266
+
267
+ ## Advanced Configuration
268
+
269
+ `new Colibri(app, server, port)` takes these arguments:
270
+
271
+ | Argument | Default | Description |
272
+ | -------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
273
+ | `app` | required | App name. Must match on all clients that exchange data. |
274
+ | `server` | Host of the page. Required in Node.js. | Host name or IP address, optionally with `http://`, `https://`, `ws://` or `wss://` and `:port`. No path, query or fragment. |
275
+ | `port` | `9211` | The server's web port. Used when `server` has no port. If both have one, they must match. |
276
+
277
+ - For a server with [TLS](https://colibri.hci.uni-konstanz.de/web/configuration#tls), use `https://` or `wss://`. A page served over `https://` can only
278
+ connect this way, because browsers block `http://` and `ws://` there.
279
+
280
+ ## Troubleshooting
281
+
282
+ | Symptom | Cause | Fix |
283
+ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
284
+ | `new Colibri()` throws a `ColibriError` | Path such as `/log`, query or fragment in the address, another scheme, a port that conflicts with the third argument, or a second instance | Pass only scheme, host and port. Use `Colibri.getInstance()`. |
285
+ | Clients do not see each other | Different app names or servers, or the listener was not registered and connected at send time | Use the same app name and server. Register listeners first. |
286
+ | Nothing connects from an `https://` page | The browser blocks `ws://` and `http://` (mixed content) | Use `https://` or `wss://` with [TLS](https://colibri.hci.uni-konstanz.de/web/configuration#tls) or a TLS proxy |
287
+ | `Colibri: could not connect to ...` after upgrading from 1.x | The address or `port` names 9011, 1.x's default port. 2.x servers default to 9211. | Use the server's port |
288
+ | `colibri.protocolMismatch` emits with `fatal: true` | The server refused this client and closed the connection | Use 2.x on both sides ([Protocol version](https://colibri.hci.uni-konstanz.de/web/configuration#protocol-version)) |
289
+ | `colibri.protocolMismatch` emits with `fatal: false` | No version announcement within 5 s, typical of a pre-2.0.0 server. The connection stays up. | Upgrade the server to 2.x |
290
+ | `@Synced()` fails to compile or does not sync | `experimentalDecorators` is on, or a member lacks `accessor` | Remove `experimentalDecorators`. Declare synced members with `accessor`. |
291
+ | Models sync in development but not in a production build or with Unity | No `name` in `RegisterModelSync` | Pass `name` |
292
+ | Unity never receives numbers from the web | colibri-web sends every number as `float` | Receive with `Sync.Receive<float>` in Unity, not `Sync.Receive<int>` |
293
+ | Colors arrive as strings or arrays | Unity sends `"#RRGGBBAA"`, colibri-web `[r, g, b, a]` | Normalize with `toHexColor()` or `toRgbaColor()` |
294
+ | `Sync.send*` does nothing | Called before `new Colibri()`, dropped with a console warning | Create the `Colibri` instance first |
295
+
296
+ ## License
155
297
 
156
- See [Sample folder](samples/) for more examples on how to use the Colibri web client.
298
+ [MIT](LICENSE)