@hcikn/colibri 1.3.2 → 2.0.0

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/dist/index.d.ts CHANGED
@@ -1,6 +1,51 @@
1
1
  import * as rxjs from 'rxjs';
2
2
  import { Subject, Observable } from 'rxjs';
3
+ import { CustomError } from 'ts-custom-error';
3
4
 
5
+ /** Base class of colibri-web's errors, such as an invalid address or port. */
6
+ declare class ColibriError extends CustomError {
7
+ }
8
+ /**
9
+ * Reported when this client and the server do not speak the same wire protocol. The fix is to run
10
+ * matching `colibri-web` and `colibri-server` versions.
11
+ *
12
+ * Check `fatal` first. Both kinds are reported on `Colibri.protocolMismatch`:
13
+ *
14
+ * - `fatal: true`: the server refused this client and disconnected it. Terminal. There is no
15
+ * negotiation, so reconnecting cannot succeed, and `Colibri` stops Socket.IO from retrying.
16
+ * - `fatal: false`: the server is suspected to predate colibri-server 2.0.0, because it never
17
+ * announced its protocol version. The connection stays up. Upgrade the server when you can.
18
+ */
19
+ declare class ProtocolMismatchError extends ColibriError {
20
+ /**
21
+ * Wire protocol version the server speaks, like `'2'`, not a release version. The server
22
+ * reports it on refusal, and it is `'unknown'` if the refusal did not say. When not `fatal`
23
+ * it is inferred: a server that never announced itself is taken to speak `'1'`.
24
+ */
25
+ readonly serverVersion: string;
26
+ /** Wire protocol version this client announced in its handshake. */
27
+ readonly clientVersion: string;
28
+ /** Whether the connection is gone. False when the server is only suspected to be old. */
29
+ readonly fatal: boolean;
30
+ constructor(message: string,
31
+ /**
32
+ * Wire protocol version the server speaks, like `'2'`, not a release version. The server
33
+ * reports it on refusal, and it is `'unknown'` if the refusal did not say. When not `fatal`
34
+ * it is inferred: a server that never announced itself is taken to speak `'1'`.
35
+ */
36
+ serverVersion: string,
37
+ /** Wire protocol version this client announced in its handshake. */
38
+ clientVersion: string,
39
+ /** Whether the connection is gone. False when the server is only suspected to be old. */
40
+ fatal?: boolean);
41
+ }
42
+
43
+ /**
44
+ * The wire protocol version this client speaks, sent in the handshake query. Must match the
45
+ * server's `PROTOCOL_VERSION` (`src/server/modules/networking/protocol.ts`); a server refuses a
46
+ * client that announces another version.
47
+ */
48
+ declare const PROTOCOL_VERSION = "2";
4
49
  interface Message {
5
50
  channel: string;
6
51
  command: string;
@@ -9,16 +54,54 @@ interface Message {
9
54
  declare class Colibri {
10
55
  readonly app: string;
11
56
  readonly server: string;
12
- readonly port: number;
13
57
  private static instance;
14
58
  private readonly socket;
15
59
  private readonly messageSubject;
16
60
  readonly messages: rxjs.Observable<Message>;
61
+ private readonly protocolMismatchSubject;
62
+ /**
63
+ * Reports that this client and the server do not speak the same wire protocol. Two kinds,
64
+ * told apart by {@link ProtocolMismatchError.fatal `fatal`}, each emitted at most once per
65
+ * instance:
66
+ *
67
+ * - `fatal: true`: the server refused this client. The connection is closed and not retried.
68
+ * - `fatal: false`: the server did not announce itself within 5s of connecting, so it is
69
+ * suspected to predate colibri-server 2.0.0. The connection stays up.
70
+ *
71
+ * A suspicion can be followed by a refusal, never the other way round. Nothing is replayed to
72
+ * a late subscriber, so subscribe right after construction. Both kinds are also logged.
73
+ */
74
+ readonly protocolMismatch: rxjs.Observable<ProtocolMismatchError>;
17
75
  readonly uri: string;
18
76
  readonly uriRestApi: string;
77
+ /** The port connected to: the one in the server address, else the one passed, else 9211. */
78
+ readonly port: number;
79
+ private oldServerTimer;
80
+ private hasReportedOldServer;
81
+ private hasReportedRefusal;
82
+ private hasConnected;
83
+ private hasReportedOutage;
84
+ /**
85
+ * Connects to a Colibri server. Only one instance may exist.
86
+ * @param app the application name; clients only see each other's messages within one app
87
+ * @param server the host, optionally after `ws://`, `wss://`, `http://` or `https://` (the
88
+ * secure ones mean `wss` for the socket and `https` for REST) and optionally with a port,
89
+ * as in `'http://example.com:9211'`; no path. Defaults to the host that served the page,
90
+ * and is required outside a browser.
91
+ * @param port the server port, if the address has none; 9211 if neither does. When both have
92
+ * one, they must agree.
93
+ * @throws ColibriError for an address or port this client cannot connect to, or when an
94
+ * instance already exists
95
+ */
19
96
  constructor(app: string, server?: string, port?: number);
20
97
  private onSocketConnect;
98
+ private onSocketDisconnect;
99
+ private onSocketConnectError;
100
+ private waitForServerHello;
101
+ private stopWaitingForServerHello;
102
+ private onServerHelloMissing;
21
103
  private onSocketAny;
104
+ private onProtocolRejected;
22
105
  /**
23
106
  * Returns the existing Colibri instance or null if there's none yet
24
107
  * @param warnIfNotInitialized true => print Log message if Colibri has not been initialized yet
@@ -51,24 +134,43 @@ declare class Colibri {
51
134
  */
52
135
  registerOnce(channel: string, handler: (payload: Message) => void): void;
53
136
  /**
54
- * Returns the REST API Endpoint for a given `key` or null of the key is empty.
137
+ * Returns the REST API Endpoint for a given `key`, or null if the key is empty, `.` or `..`.
138
+ * Any other key is stored under exactly that name (`#`, `?`, `/`, `%` and spaces included)
139
+ * after surrounding whitespace and leading slashes are stripped.
55
140
  * @param key REST API storage key
56
141
  * @returns
57
142
  */
58
143
  getRestUri(key: string): string | null;
59
144
  /**
60
145
  * Queries an object from the REST API identified by `key`.
146
+ *
147
+ * Rejects when the server cannot be reached (wrong address, server down, no network) or an
148
+ * answer with a success status is not JSON.
61
149
  * @param key REST API storage key
62
- * @returns JSON object with data or null if object does not exist or any other error occurrs
150
+ * @returns the stored value; or null if `key` is empty or the server answered with an error
151
+ * status, such as 404 for a key that was never stored. A stored `null` looks the same.
63
152
  */
64
- getRestObject(key: string): Promise<any | null>;
153
+ getRestObject(key: string): Promise<any>;
65
154
  /**
66
- * Sets or updatees an object in the REST API identified by `key`.
155
+ * Sets or updates an object in the REST API identified by `key`.
156
+ *
157
+ * Rejects when the server cannot be reached (wrong address, server down, no network) or
158
+ * `data` cannot be serialized to JSON (circular structure, BigInt).
67
159
  * @param key REST API storage key
68
160
  * @param data JSON data to write
69
- * @returns true if data was written to REST API
161
+ * @returns true if the server stored the data; false if `key` is empty or the server answered
162
+ * with any other status
70
163
  */
71
164
  setRestObject(key: string, data: unknown): Promise<boolean>;
165
+ /**
166
+ * Deletes the object identified by `key` from the REST API.
167
+ *
168
+ * Rejects when the server cannot be reached (wrong address, server down, no network).
169
+ * @param key REST API storage key
170
+ * @returns true if the server deleted the value; false if `key` is empty or the server answered
171
+ * with any other status, such as 404 for a key that holds no value
172
+ */
173
+ deleteRestObject(key: string): Promise<boolean>;
72
174
  }
73
175
  /**
74
176
  * @see {@link Colibri.sendMessage `Colibri.sendMessage()`}
@@ -77,19 +179,23 @@ declare class Colibri {
77
179
  declare const SendMessage: (channel: string, command: string, payload?: unknown) => void | undefined;
78
180
  /**
79
181
  * @see {@link Colibri.registerChannel `Colibri.registerChannel()`}
80
- * @returns undefined if {@link Colibri `Colibri`} has not been initialized yet
182
+ *
183
+ * Works before `new Colibri()` too: the handler is attached once the instance is constructed.
81
184
  */
82
- declare const RegisterChannel: (channel: string, handler: (payload: Message) => void) => void | undefined;
185
+ declare const RegisterChannel: (channel: string, handler: (payload: Message) => void) => void;
83
186
  /**
84
187
  * @see {@link Colibri.unregisterChannel `Colibri.unregisterChannel()`}
85
- * @returns undefined if {@link Colibri `Colibri`} has not been initialized yet
188
+ *
189
+ * Before `new Colibri()`, takes back a handler that {@link RegisterChannel} or
190
+ * {@link RegisterOnce} queued.
86
191
  */
87
- declare const UnregisterChannel: (channel: string, handler: (payload: Message) => void) => void | undefined;
192
+ declare const UnregisterChannel: (channel: string, handler: (payload: Message) => void) => void;
88
193
  /**
89
194
  * @see {@link Colibri.registerOnce `Colibri.registerOnce()`}
90
- * @returns undefined if {@link Colibri `Colibri`} has not been initialized yet
195
+ *
196
+ * Works before `new Colibri()` too, like {@link RegisterChannel}.
91
197
  */
92
- declare const RegisterOnce: (channel: string, handler: (payload: Message) => void) => void | undefined;
198
+ declare const RegisterOnce: (channel: string, handler: (payload: Message) => void) => void;
93
199
  /**
94
200
  * @see {@link Colibri.getRestObject `Colibri.getRestObject()`}
95
201
  * @returns undefined if {@link Colibri `Colibri`} has not been initialized yet
@@ -100,8 +206,24 @@ declare const GetRestApi: (key: string) => Promise<any> | undefined;
100
206
  * @returns undefined if {@link Colibri `Colibri`} has not been initialized yet
101
207
  */
102
208
  declare const PutRestApi: (key: string, data: unknown) => Promise<boolean> | undefined;
209
+ /**
210
+ * @see {@link Colibri.deleteRestObject `Colibri.deleteRestObject()`}
211
+ * @returns undefined if {@link Colibri `Colibri`} has not been initialized yet
212
+ */
213
+ declare const DeleteRestApi: (key: string) => Promise<boolean> | undefined;
103
214
 
215
+ /**
216
+ * A colour on the wire: `[r, g, b, a]` with components 0-1 from colibri-web, `"#RRGGBBAA"` from
217
+ * colibri-unity. Received colours can be either; normalize with {@link toHexColor} or
218
+ * {@link toRgbaColor}.
219
+ */
220
+ type ColorValue = string | [number, number, number, number];
104
221
  type genericCallback = (val: any) => void;
222
+ /** Converts a colour to `"#RRGGBBAA"`. An unparseable colour warns and gives opaque black. */
223
+ declare const toHexColor: (val: ColorValue) => string;
224
+ /** Converts a colour to `[r, g, b, a]` with components 0-1. An unparseable colour warns and gives opaque black. */
225
+ declare const toRgbaColor: (val: ColorValue) => [number, number, number, number];
226
+ /** Sends typed values on a channel and registers callbacks for the values received on it. */
105
227
  declare const Sync: {
106
228
  sendBool: (channel: string, val: boolean) => void;
107
229
  sendBoolArray: (channel: string, val: boolean[]) => void;
@@ -109,12 +231,14 @@ declare const Sync: {
109
231
  sendNumberArray: (channel: string, val: number[]) => void;
110
232
  sendString: (channel: string, val: string) => void;
111
233
  sendStringArray: (channel: string, val: string[]) => void;
234
+ sendVector2: (channel: string, val: [number, number]) => void;
235
+ sendVector2Array: (channel: string, val: [number, number][]) => void;
112
236
  sendVector3: (channel: string, val: [number, number, number]) => void;
113
237
  sendVector3Array: (channel: string, val: [number, number, number][]) => void;
114
238
  sendQuaternion: (channel: string, val: [number, number, number, number]) => void;
115
239
  sendQuaternionArray: (channel: string, val: [number, number, number, number][]) => void;
116
- sendColor: (channel: string, val: [number, number, number, number]) => void;
117
- sendColorArray: (channel: string, val: [number, number, number, number][]) => void;
240
+ sendColor: (channel: string, val: ColorValue) => void;
241
+ sendColorArray: (channel: string, val: ColorValue[]) => void;
118
242
  sendJson: (channel: string, val: {
119
243
  [key: string]: unknown;
120
244
  }) => void;
@@ -124,12 +248,14 @@ declare const Sync: {
124
248
  receiveNumberArray: (channel: string, callback: (val: number[]) => void) => void;
125
249
  receiveString: (channel: string, callback: (val: string) => void) => void;
126
250
  receiveStringArray: (channel: string, callback: (val: string[]) => void) => void;
251
+ receiveVector2: (channel: string, callback: (val: [number, number]) => void) => void;
252
+ receiveVector2Array: (channel: string, callback: (val: [number, number][]) => void) => void;
127
253
  receiveVector3: (channel: string, callback: (val: [number, number, number]) => void) => void;
128
254
  receiveVector3Array: (channel: string, callback: (val: [number, number, number][]) => void) => void;
129
255
  receiveQuaternion: (channel: string, callback: (val: [number, number, number, number]) => void) => void;
130
256
  receiveQuaternionArray: (channel: string, callback: (val: [number, number, number, number][]) => void) => void;
131
- receiveColor: (channel: string, callback: (val: string) => void) => void;
132
- receiveColorArray: (channel: string, callback: (val: string[]) => void) => void;
257
+ receiveColor: (channel: string, callback: (val: ColorValue) => void) => void;
258
+ receiveColorArray: (channel: string, callback: (val: ColorValue[]) => void) => void;
133
259
  receiveJson: (channel: string, callback: (val: {
134
260
  [key: string]: unknown;
135
261
  }) => void) => void;
@@ -142,37 +268,67 @@ declare const Sync: {
142
268
 
143
269
  declare class RemoteLogger {
144
270
  private enabled;
271
+ private static patched;
272
+ private readonly forwarder;
273
+ private hasWarnedOfAnotherInstance;
145
274
  private readonly consoleDebug;
146
275
  private readonly consoleLog;
147
276
  private readonly consoleInfo;
148
277
  private readonly consoleWarn;
149
278
  private readonly consoleError;
279
+ private forwarding;
280
+ private readonly earlyLines;
281
+ private droppedEarlyLines;
282
+ private waitingForColibri;
150
283
  /**
151
284
  * @deprecated use new class constructor directly
152
285
  */
153
286
  static init(enabled?: boolean): RemoteLogger;
287
+ /**
288
+ * Forwards everything logged through `console` to the server's log. Create one, at startup.
289
+ * A further instance warns once and does not patch again; its `enabled` argument, `enable()` and
290
+ * `disable()` act on the first one.
291
+ * @param enabled whether to forward from the start
292
+ */
154
293
  constructor(enabled?: boolean);
155
294
  private sendMessage;
295
+ private keepUntilColibriExists;
296
+ private sendEarlyLines;
297
+ /** Starts forwarding console output to the server's log. */
156
298
  enable(): void;
299
+ /** Stops forwarding to the server's log; the console still prints. */
157
300
  disable(): void;
158
301
  }
159
302
 
160
303
  declare abstract class SyncModel<T> {
304
+ #private;
305
+ private __syncedProperties?;
161
306
  readonly id: string;
162
- private syncedProperties;
163
- ignoreNextChange: boolean;
307
+ applyingRemoteUpdate: boolean;
164
308
  readonly modelChanges: Subject<string>;
165
309
  readonly modelChanges$: rxjs.Observable<string[]>;
310
+ readonly deletions: Subject<void>;
166
311
  constructor(id: string);
167
312
  private uniq;
168
313
  protected onModelChanges(prop: string): void;
314
+ /**
315
+ * Deletes the model for every client of the app (Unity destroys its GameObject) and removes it
316
+ * from this client's list. The model stops syncing.
317
+ */
169
318
  delete(): void;
319
+ /**
320
+ * Stops syncing the model on this client only; every other client keeps it. RegisterModelSync
321
+ * calls it on a remote delete and when registerModel replaces a listed copy.
322
+ */
323
+ detach(): void;
170
324
  registerSyncedProperty(name: string, prop: string | symbol): void;
325
+ private getSyncedProperties;
171
326
  update(updates: Partial<T>): void;
172
327
  toJson(attributes?: string[]): Partial<T>;
173
328
  }
174
329
 
175
330
  interface ModelSyncRegistration<T> {
331
+ /** The channel to sync on. Recommended: the default, the lowercased class name, changes when minified. */
176
332
  name?: string;
177
333
  type: {
178
334
  new (id: string): T;
@@ -182,10 +338,67 @@ type ModelSync<T> = [Observable<T[]>, (model: T) => void];
182
338
  declare const RegisterModelSync: <T extends SyncModel<T>>(registration: ModelSyncRegistration<T>) => ModelSync<T>;
183
339
 
184
340
  /**
185
- * This decorator can be attached to any property or accessor of a SyncModel.
341
+ * This decorator can be attached to any `accessor` member of a SyncModel.
186
342
  * It will cause the property to be synchronized with the server.
187
- * Refer to https://www.typescriptlang.org/docs/handbook/decorators.html
343
+ * Refer to https://github.com/tc39/proposal-decorators
344
+ */
345
+ declare function Synced<This extends SyncModel<unknown>, V>(syncedName?: string): (value: ClassAccessorDecoratorTarget<This, V>, context: ClassAccessorDecoratorContext<This, V>) => ClassAccessorDecoratorResult<This, V>;
346
+ /**
347
+ * Declares synchronized properties without a decorator, for plain JavaScript. Call it in the
348
+ * constructor of a SyncModel subclass, after `super(id)`, with each property and its start value:
349
+ *
350
+ * ```js
351
+ * class Cube extends SyncModel {
352
+ * constructor(id) {
353
+ * super(id);
354
+ * synced(this, { x: 0, y: 0, color: 'red' });
355
+ * }
356
+ * }
357
+ * ```
358
+ *
359
+ * Each property syncs as with `@Synced()`, under its lowercased name or under `names[property]`.
360
+ * Throws for a property the model already has as a member, and for a start value of `undefined`.
361
+ */
362
+ declare function synced<M extends SyncModel<unknown>, P extends Record<string, unknown>>(model: M, values: P, names?: Partial<Record<keyof P & string, string>>): asserts model is M & P;
363
+
364
+ /** `[x, y, z]`, as colibri-unity sends a Vector3. */
365
+ type Vector3 = [number, number, number];
366
+ /** `[x, y, z, w]`, as colibri-unity sends a Quaternion. */
367
+ type Quaternion = [number, number, number, number];
368
+ /**
369
+ * The web side of colibri-unity's `SyncTransform`: active state, position, rotation and scale in
370
+ * Unity's coordinates. Register it with {@link RegisterSyncTransforms}.
371
+ *
372
+ * Members mirror colibri-unity's `[Sync]` members. A Unity object with a member's Sync box unticked
373
+ * never sends it, so it keeps its default here, which equals Unity's placeholder.
374
+ *
375
+ * Assign a new array to change a vector: `transform.position = [1, 2, 3]` is sent,
376
+ * `transform.position[0] = 1` is not.
377
+ */
378
+ declare class SyncTransform extends SyncModel<SyncTransform> {
379
+ /** Unity's `activeSelf`. `false` hides the object on the Unity clients. */
380
+ accessor active: boolean;
381
+ /** World position, or local position with *Use Local Transform* ticked. */
382
+ accessor position: Vector3;
383
+ /** World rotation, or local rotation with *Use Local Transform* ticked. */
384
+ accessor rotation: Quaternion;
385
+ /** Local scale. */
386
+ accessor scale: Vector3;
387
+ /**
388
+ * Id of the Unity client that simulates the object's physics (`PhysicsAuthority`). Declared so a
389
+ * remote update carrying it is not warned about as an unknown property. Setting it takes the
390
+ * authority away from that client.
391
+ */
392
+ accessor physicsId: string;
393
+ }
394
+ /**
395
+ * {@link RegisterModelSync} for `SyncTransform`: returns an Observable of all of them, which emits
396
+ * when one is added, changed or deleted, and a function that registers a new one.
397
+ *
398
+ * Unity objects with a `ModelId` sync on a channel of their own, so pass the same `modelId`. Call
399
+ * this once per channel and share the result: two registrations on one channel in one page do not
400
+ * receive each other's updates.
188
401
  */
189
- declare const Synced: <T>(syncedName?: string) => PropertyDecorator;
402
+ declare const RegisterSyncTransforms: (modelId?: string) => [Observable<SyncTransform[]>, (transform: SyncTransform) => void];
190
403
 
191
- export { Colibri, GetRestApi, Message, PutRestApi, RegisterChannel, RegisterModelSync, RegisterOnce, RemoteLogger, SendMessage, Sync, SyncModel, Synced, UnregisterChannel };
404
+ export { Colibri, ColibriError, type ColorValue, DeleteRestApi, GetRestApi, type Message, PROTOCOL_VERSION, ProtocolMismatchError, PutRestApi, type Quaternion, RegisterChannel, RegisterModelSync, RegisterOnce, RegisterSyncTransforms, RemoteLogger, SendMessage, Sync, SyncModel, SyncTransform, Synced, UnregisterChannel, type Vector3, synced, toHexColor, toRgbaColor };