multyx-client 0.1.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/src/index.ts ADDED
@@ -0,0 +1,261 @@
1
+ import { Message } from "./message";
2
+ import { Unpack, EditWrapper, Add } from './utils';
3
+ import { RawObject } from "./types";
4
+ import { Controller } from "./controller";
5
+ import { MultyxClientObject } from "./items";
6
+ import { DefaultOptions, Options } from "./options";
7
+
8
+ export default class Multyx {
9
+ ws: WebSocket;
10
+ uuid: string;
11
+ joinTime: number;
12
+ ping: number;
13
+ events: Map<string | Symbol, ((data?: any) => void)[]>;
14
+ self: RawObject;
15
+ all: RawObject;
16
+ clients: RawObject;
17
+ teams: RawObject;
18
+ controller: Controller;
19
+
20
+ options: Options;
21
+
22
+ // Queue of functions to be called after each frame
23
+ private listenerQueue: ((...args: any[]) => void)[];
24
+
25
+ static Start = Symbol('start');
26
+ static Connection = Symbol('connection');
27
+ static Disconnect = Symbol('disconnect');
28
+ static Edit = Symbol('edit');
29
+ static Native = Symbol('native');
30
+ static Custom = Symbol('custom');
31
+ static Any = Symbol('any');
32
+
33
+ constructor(options: Options = {}, callback?: () => void) {
34
+ this.options = { ...DefaultOptions, ...options };
35
+
36
+ const url = `ws${this.options.secure ? 's' : ''}://${this.options.uri}:${this.options.port}/`;
37
+ this.ws = new WebSocket(url);
38
+ this.ping = 0;
39
+ this.events = new Map();
40
+ this.self = {};
41
+ this.all = {};
42
+ this.teams = {};
43
+ this.clients = {};
44
+ this.controller = new Controller(this.ws);
45
+ this.listenerQueue = [];
46
+
47
+ callback?.();
48
+
49
+ this.ws.onmessage = event => {
50
+ const msg = Message.Parse(event.data);
51
+ this.ping = 2 * (Date.now() - msg.time);
52
+
53
+ if(msg.native) {
54
+ this.parseNativeEvent(msg);
55
+ this.events.get(Multyx.Native)?.forEach(cb => cb(msg));
56
+ } else if(msg.name in this.events) {
57
+ this.events[msg.name](msg.data);
58
+ this.events.get(Multyx.Custom)?.forEach(cb => cb(msg));
59
+ }
60
+ this.events.get(Multyx.Any)?.forEach(cb => cb(msg));
61
+ }
62
+ }
63
+
64
+ on(name: string | Symbol, callback: (data: RawObject) => void) {
65
+ const events = this.events.get(name) ?? [];
66
+ events.push(callback);
67
+ this.events.set(name, events);
68
+ }
69
+
70
+ send(name: string, data: any, expectResponse: boolean = false) {
71
+ if(name[0] === '_') name = '_' + name;
72
+ this.ws.send(Message.Create(name, data));
73
+ if(!expectResponse) return;
74
+
75
+ return new Promise(res => this.events.set(Symbol.for("_" + name), [res]));
76
+ }
77
+
78
+ /**
79
+ * Loop over a function
80
+ * @param callback Function to call on a loop
81
+ * @param timesPerSecond Recommended to leave blank. Number of times to loop in each second, if undefined, use requestAnimationFrame
82
+ */
83
+ loop(callback: () => void, timesPerSecond?: number) {
84
+ if(timesPerSecond) {
85
+ this.on(Multyx.Start, () => setInterval(callback, Math.round(1000/timesPerSecond)));
86
+ } else {
87
+ const caller = () => {
88
+ callback();
89
+ requestAnimationFrame(caller);
90
+ }
91
+ this.on(Multyx.Start, () => requestAnimationFrame(caller));
92
+ }
93
+ }
94
+
95
+
96
+ /**
97
+ * Create a callback function that gets called for any current or future client
98
+ * @param callbackfn Function to call for every client
99
+ */
100
+ forAll(callback: (client: MultyxClientObject) => void) {
101
+ this.on(Multyx.Start, () => {
102
+ this.teams.all.clients.forAll((uuid) => callback(this.clients[uuid]));
103
+ });
104
+ this.on(Multyx.Connection, callback);
105
+ }
106
+
107
+ private parseNativeEvent(msg: Message) {
108
+ if(this.options.logUpdateFrame) console.log(msg);
109
+
110
+ for(const update of msg.data) {
111
+ switch(update.instruction) {
112
+ // Initialization
113
+ case 'init': {
114
+ this.initialize(update);
115
+
116
+ for(const listener of this.events.get(Multyx.Start) ?? []) {
117
+ this.listenerQueue.push(() => listener(update));
118
+ }
119
+
120
+ // Clear start event as it will never be called again
121
+ if(this.events.has(Multyx.Start)) this.events.get(Multyx.Start).length = 0;
122
+ break;
123
+ }
124
+
125
+ // Client or team data edit
126
+ case 'edit': {
127
+ this.parseEdit(update);
128
+
129
+ for(const listener of this.events.get(Multyx.Edit) ?? []) {
130
+ this.listenerQueue.push(() => listener(update));
131
+ }
132
+ break;
133
+ }
134
+
135
+ // Other data change
136
+ case 'self': {
137
+ this.parseSelf(update);
138
+ break;
139
+ }
140
+
141
+ // Connection
142
+ case 'conn': {
143
+ this.clients[update.uuid] = new MultyxClientObject(
144
+ this,
145
+ update.data,
146
+ [update.uuid],
147
+ false
148
+ );
149
+
150
+ for(const listener of this.events.get(Multyx.Connection) ?? []) {
151
+ this.listenerQueue.push(() => listener(this.clients[update.uuid]));
152
+ }
153
+ break;
154
+ }
155
+
156
+ // Disconnection
157
+ case 'dcon': {
158
+ for(const listener of this.events.get(Multyx.Disconnect) ?? []) {
159
+ const clientValue = this.clients[update.client].value;
160
+ this.listenerQueue.push(() => listener(clientValue));
161
+ }
162
+ delete this.clients[update.client];
163
+ break;
164
+ }
165
+
166
+ // Response to client
167
+ case 'resp': {
168
+ const promiseResolve = this.events.get(Symbol.for("_" + update.name))[0];
169
+ promiseResolve(update.response);
170
+ break;
171
+ }
172
+
173
+ default: {
174
+ if(this.options.verbose) {
175
+ console.error("Server error: Unknown native Multyx instruction");
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ this.listenerQueue.forEach(x => x());
182
+ this.listenerQueue.length = 0;
183
+ }
184
+
185
+ private initialize(update: RawObject) {
186
+ this.uuid = update.client.uuid;
187
+ this.joinTime = update.client.joinTime;
188
+ this.controller.listening = new Set(update.client.controller);
189
+
190
+ // Create MultyxClientObject for all teams
191
+ this.teams = new MultyxClientObject(this, {}, [], true);
192
+ for(const team of Object.keys(update.teams)) {
193
+ this.teams[team] = new EditWrapper(update.teams[team]);
194
+ }
195
+ this.all = this.teams['all'];
196
+
197
+ // Create MultyxClientObject for all clients
198
+ this.clients = {};
199
+ for(const [uuid, client] of Object.entries(update.clients)) {
200
+ if(uuid == this.uuid) continue;
201
+ this.clients[uuid] = new MultyxClientObject(
202
+ this,
203
+ new EditWrapper(client),
204
+ [uuid],
205
+ false
206
+ );
207
+ };
208
+
209
+ const client = new MultyxClientObject(
210
+ this,
211
+ new EditWrapper(update.client.self),
212
+ [this.uuid],
213
+ true
214
+ );
215
+ this.self = client;
216
+ this.clients[this.uuid] = client;
217
+
218
+ // Apply all constraints on self and teams
219
+ for(const [uuid, table] of Object.entries(update.constraintTable)) {
220
+ const obj = this.uuid == uuid ? this.self : this.teams[uuid];
221
+ obj[Unpack](table);
222
+ }
223
+ }
224
+
225
+ private parseEdit(update: RawObject) {
226
+ let route: any = update.team ? this.teams : this.clients;
227
+ if(!route) return;
228
+
229
+ // Loop through path to get to object being edited
230
+ for(const p of update.path.slice(0, -1)) {
231
+ // Create new object at path if non-existent
232
+ if(!(p in route)) route[p] = new EditWrapper({});
233
+ route = route[p];
234
+ }
235
+
236
+ const prop = update.path.slice(-1)[0];
237
+ route[prop] = new EditWrapper(update.value);
238
+ }
239
+
240
+ private parseSelf(update: RawObject) {
241
+ if(update.prop == 'controller') {
242
+ this.controller.listening = new Set(update.data);
243
+ } else if(update.prop == 'uuid') {
244
+ this.uuid = update.data;
245
+ } else if(update.prop == 'constraint') {
246
+ let route = this.uuid == update.data.path[0] ? this.self : this.teams[update.data.path[0]];
247
+ for(const prop of update.data.path.slice(1)) route = route?.[prop];
248
+ if(route === undefined) return;
249
+
250
+ route[Unpack]({ [update.data.name]: update.data.args });
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Add function to listener queue
256
+ * @param fn Function to call once frame is complete
257
+ */
258
+ [Add](fn: ((...args: any[]) => void)) {
259
+ this.listenerQueue.push(fn);
260
+ }
261
+ }
@@ -0,0 +1,14 @@
1
+ import MultyxClientList from "./list";
2
+ import MultyxClientObject from "./object";
3
+ import MultyxClientValue from "./value";
4
+
5
+ type MultyxClientItem<T = any> = T extends any[] ? MultyxClientList
6
+ : T extends object ? MultyxClientObject
7
+ : MultyxClientValue;
8
+
9
+ export {
10
+ MultyxClientList,
11
+ MultyxClientObject,
12
+ MultyxClientValue,
13
+ MultyxClientItem,
14
+ };
@@ -0,0 +1,270 @@
1
+ import Multyx from '../';
2
+ import { MultyxClientItem } from '.';
3
+ import { EditWrapper } from '../utils';
4
+ import MultyxClientObject from "./object";
5
+
6
+ export default class MultyxClientList extends MultyxClientObject {
7
+ length: number;
8
+
9
+ get value() {
10
+ const parsed: any[] = [];
11
+ for(let i=0; i<this.length; i++) parsed[i] = this.get(i).value;
12
+ return parsed;
13
+ }
14
+
15
+ constructor(multyx: Multyx, list: any[] | EditWrapper<any[]>, propertyPath: string[] = [], editable: boolean){
16
+ super(multyx, {}, propertyPath, editable);
17
+
18
+ this.length = 0;
19
+ this.push(...(list instanceof EditWrapper ? list.value.map(x => new EditWrapper(x)) : list));
20
+
21
+ return new Proxy(this, {
22
+ has: (o, p) => {
23
+ if(p in o) return true;
24
+ return o.has(p);
25
+ },
26
+ get: (o, p) => {
27
+ if(p in o) return o[p];
28
+ return o.get(p);
29
+ },
30
+ set: (o, p, v) => {
31
+ if(p in o) {
32
+ o[p] = v;
33
+ return true;
34
+ }
35
+ return o.set(p as string, v);
36
+ },
37
+ deleteProperty: (o, p) => {
38
+ return o.delete(p as string, false);
39
+ }
40
+ });
41
+ }
42
+
43
+ set(index: string | number, value: any) {
44
+ if(typeof index == 'string') index = parseInt(index);
45
+ if(value === undefined) return this.delete(index, false);
46
+ if(value instanceof EditWrapper && value.value === undefined) return this.delete(index, true);
47
+
48
+ const result = super.set(index, value);
49
+ if(result && index >= this.length) this.length = index+1;
50
+
51
+ return result;
52
+ }
53
+
54
+ delete(index: string | number, native: boolean = false) {
55
+ if(typeof index == 'string') index = parseInt(index);
56
+
57
+ const res = super.delete(index, native);
58
+ if(res) this.length = this.reduce((a, c, i) => c !== undefined ? i+1 : a, 0);
59
+ return res;
60
+ }
61
+
62
+ /**
63
+ * Create a callback function that gets called for any current or future element in list
64
+ * @param callbackfn Function to call for every element
65
+ */
66
+ forAll(callbackfn: (value: any, index: number) => void) {
67
+ for(let i=0; i<this.length; i++) {
68
+ callbackfn(this.get(i), i);
69
+ }
70
+ super.forAll((key, value) => callbackfn(value, key));
71
+ }
72
+
73
+
74
+ /* All general array methods */
75
+ push(...items: any) {
76
+ for(const item of items) this.set(this.length, item);
77
+ return this.length;
78
+ }
79
+
80
+ pop(): MultyxClientItem | null {
81
+ if(this.length === 0) return null;
82
+
83
+ const res = this.get(this.length);
84
+ this.delete(this.length);
85
+ return res;
86
+ }
87
+
88
+ unshift(...items: any[]) {
89
+ for(let i=this.length-1; i>=0; i--) {
90
+ if(i >= items.length) {
91
+ this.set(i, this.get(i-items.length));
92
+ } else {
93
+ this.set(i, items[i]);
94
+ }
95
+ }
96
+
97
+ return this.length;
98
+ }
99
+
100
+ shift() {
101
+ if(this.length == 0) return undefined;
102
+ this.length--;
103
+
104
+ const res = this.get(0);
105
+ for(let i=0; i<this.length; i++) {
106
+ this.set(i, this.get(i+1));
107
+ }
108
+ return res;
109
+ }
110
+
111
+ splice(start: number, deleteCount?: number, ...items: any[]) {
112
+ if(deleteCount === undefined) {
113
+ deleteCount = this.length - start;
114
+ }
115
+
116
+ // Move elements in front of splice forward or backward
117
+ let move = items.length - deleteCount;
118
+ if(move > 0) {
119
+ for(let i=this.length-1; i>=start + deleteCount; i--) {
120
+ this.set(i + move, this.get(i));
121
+ }
122
+ } else if(move < 0) {
123
+ for(let i=start+deleteCount; i<this.length; i++) {
124
+ this.set(i + move, this.get(i));
125
+ }
126
+
127
+ // Delete elements past end of new list
128
+ const originalLength = this.length;
129
+ for(let i=originalLength+move; i<originalLength; i++) {
130
+ this.set(i, undefined);
131
+ }
132
+ }
133
+
134
+ // Insert new elements starting at start
135
+ for(let i=start; i<items.length; i++) {
136
+ this.set(i, items[i]);
137
+ }
138
+ }
139
+
140
+
141
+ filter(predicate: (value: any, index: number, array: MultyxClientList) => boolean) {
142
+ const keep = [];
143
+ for(let i=0; i<this.length; i++) {
144
+ keep.push(predicate(this.get(i), i, this));
145
+ }
146
+
147
+ let negativeOffset = 0;
148
+ for(let i=0; i<keep.length; i++) {
149
+ if(keep[i] && negativeOffset) this.set(i - negativeOffset, this.get(i));
150
+ if(!keep[i]) negativeOffset--;
151
+ }
152
+ }
153
+
154
+ map(callbackfn: (value: any, index: number, array: MultyxClientList) => any) {
155
+ for(let i=0; i<this.length; i++) {
156
+ this.set(i, callbackfn(this.get(i), i, this));
157
+ }
158
+ }
159
+
160
+ flat() {
161
+ for(let i=0; i<this.length; i++) {
162
+ const item = this.get(i);
163
+
164
+ if(item instanceof MultyxClientList) {
165
+ for(let j=0; j<item.length; j++) {
166
+ i++;
167
+ this.set(i, item[j]);
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ reduce(callbackfn: (accumulator: any, currentValue: any, index: number, array: MultyxClientList) => any, startingAccumulator: any) {
174
+ for(let i=0; i<this.length; i++) {
175
+ startingAccumulator = callbackfn(startingAccumulator, this.get(i), i, this);
176
+ }
177
+ return startingAccumulator;
178
+ }
179
+
180
+ reduceRight(callbackfn: (accumulator: any, currentValue: any, index: number, array: MultyxClientList) => any, startingAccumulator: any) {
181
+ for(let i=this.length-1; i>=0; i--) {
182
+ startingAccumulator = callbackfn(startingAccumulator, this.get(i), i, this);
183
+ }
184
+ return startingAccumulator;
185
+ }
186
+
187
+ reverse() {
188
+ let right = this.length-1;
189
+ for(let left=0; left<right; left++) {
190
+ const a = this.get(left);
191
+ const b = this.get(right);
192
+ this.set(left, b);
193
+ this.set(right, a);
194
+ }
195
+ return this;
196
+ }
197
+
198
+ forEach(callbackfn: (value: any, index: number, array: MultyxClientList) => void) {
199
+ for(let i=0; i<this.length; i++) {
200
+ callbackfn(this.get(i), i, this);
201
+ }
202
+ }
203
+
204
+ every(predicate: (value: any, index: number, array: MultyxClientList) => boolean) {
205
+ for(let i=0; i<this.length; i++) {
206
+ if(!predicate(this.get(i), i, this)) return false;
207
+ }
208
+ return true;
209
+ }
210
+
211
+ some(predicate: (value: any, index: number, array: MultyxClientList) => boolean) {
212
+ for(let i=0; i<this.length; i++) {
213
+ if(predicate(this.get(i), i, this)) return true;
214
+ }
215
+ return false;
216
+ }
217
+
218
+ find(predicate: (value: any, index: number, array: MultyxClientList) => boolean) {
219
+ for(let i=0; i<this.length; i++) {
220
+ if(predicate(this.get(i), i, this)) return this.get(i);
221
+ }
222
+ return undefined;
223
+ }
224
+
225
+ findIndex(predicate: (value: any, index: number, array: MultyxClientList) => boolean) {
226
+ for(let i=0; i<this.length; i++) {
227
+ if(predicate(this.get(i), i, this)) return i;
228
+ }
229
+ return -1;
230
+ }
231
+
232
+ deorder(): MultyxClientItem[] {
233
+ const values = [];
234
+ for(const index in this.object) {
235
+ values.push(this.get(index));
236
+ }
237
+ return values;
238
+ }
239
+
240
+ deorderEntries(): [number, MultyxClientItem][] {
241
+ const values = [];
242
+ for(const index in this.object) {
243
+ values.push([parseInt(index), this.get(index)]);
244
+ }
245
+ return values;
246
+ }
247
+
248
+ entries(): [any, number][] {
249
+ const entryList: [any, number][] = [];
250
+ for(let i=0; i<this.length; i++) {
251
+ entryList.push([this.get(i), i]);
252
+ }
253
+ return entryList;
254
+ }
255
+
256
+ keys(): number[] {
257
+ return Array(this.length).fill(0).map((_, i) => i);
258
+ }
259
+
260
+
261
+ /* Native methods to allow MultyxClientList to be treated as array */
262
+ [Symbol.iterator](): Iterator<MultyxClientItem> {
263
+ const values = [];
264
+ for(let i=0; i<this.length; i++) values[i] = this.get(i);
265
+ return values[Symbol.iterator]();
266
+ }
267
+ toString = () => this.value.toString();
268
+ valueOf = () => this.value;
269
+ [Symbol.toPrimitive] = () => this.value;
270
+ }