multyx-client 0.1.0 → 0.1.2

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.
@@ -1,9 +1,9 @@
1
1
  import { Message } from "../message";
2
2
  import { RawObject } from '../types';
3
- import { Add, EditWrapper, Unpack } from "../utils";
3
+ import { Done, Edit, EditWrapper, Unpack } from "../utils";
4
4
 
5
5
  import type Multyx from '../index';
6
- import type { MultyxClientItem } from ".";
6
+ import { IsMultyxClientItem, type MultyxClientList, type MultyxClientItem } from ".";
7
7
  import MultyxClientItemRouter from "./router";
8
8
  import MultyxClientValue from "./value";
9
9
 
@@ -13,7 +13,7 @@ export default class MultyxClientObject {
13
13
  propertyPath: string[];
14
14
  editable: boolean;
15
15
 
16
- private setterListeners: ((key: any, value: any) => void)[]
16
+ private editCallbacks: ((key: any, value: any) => void)[] = [];
17
17
 
18
18
  get value() {
19
19
  const parsed = {};
@@ -21,19 +21,39 @@ export default class MultyxClientObject {
21
21
  return parsed;
22
22
  }
23
23
 
24
+ addEditCallback(callback: (key: any, value: any) => void) {
25
+ this.editCallbacks.push(callback);
26
+ }
27
+
28
+ [Edit](updatePath: string[], value: any) {
29
+ if(updatePath.length == 1) {
30
+ this.set(updatePath[0], new EditWrapper(value));
31
+ return;
32
+ }
33
+
34
+ if(updatePath.length == 0 && this.multyx.options.verbose) {
35
+ console.error("Update path is empty. Attempting to edit MultyxClientObject with no path.");
36
+ }
37
+
38
+ if(!this.has(updatePath[0])) {
39
+ this.set(updatePath[0], new EditWrapper({}));
40
+ }
41
+ this.get(updatePath[0])[Edit](updatePath.slice(1), value);
42
+ }
43
+
24
44
  constructor(multyx: Multyx, object: RawObject | EditWrapper<RawObject>, propertyPath: string[] = [], editable: boolean) {
25
45
  this.object = {};
26
46
  this.propertyPath = propertyPath;
27
47
  this.multyx = multyx;
28
48
  this.editable = editable;
29
49
 
30
- this.setterListeners = [];
31
-
50
+ const isEditWrapper = object instanceof EditWrapper;
32
51
  if(object instanceof MultyxClientObject) object = object.value;
52
+ if(object instanceof EditWrapper) object = object.value;
33
53
 
34
- for(const prop in (object instanceof EditWrapper ? object.value : object)) {
35
- this.set(prop, object instanceof EditWrapper
36
- ? new EditWrapper(object.value[prop])
54
+ for(const prop in object) {
55
+ this.set(prop, isEditWrapper
56
+ ? new EditWrapper(object[prop])
37
57
  : object[prop]
38
58
  );
39
59
  }
@@ -44,11 +64,11 @@ export default class MultyxClientObject {
44
64
  has: (o, p) => {
45
65
  return o.has(p);
46
66
  },
47
- get: (o, p) => {
67
+ get: (o, p: string) => {
48
68
  if(p in o) return o[p];
49
69
  return o.get(p);
50
70
  },
51
- set: (o, p, v) => {
71
+ set: (o, p: string, v) => {
52
72
  if(p in o) {
53
73
  o[p] = v;
54
74
  return true;
@@ -65,47 +85,74 @@ export default class MultyxClientObject {
65
85
  return property in this.object;
66
86
  }
67
87
 
68
- get(property: any): MultyxClientItem {
69
- return this.object[property];
88
+ get(property: string | string[]): MultyxClientItem {
89
+ if(typeof property === 'string') return this.object[property];
90
+ if(property.length == 0) return this;
91
+ if(property.length == 1) return this.object[property[0]];
92
+
93
+ const next = this.object[property[0]];
94
+ if(!next || (next instanceof MultyxClientValue)) return undefined;
95
+ return next.get(property.slice(1));
96
+ }
97
+
98
+ private recursiveSet(path: string[], value: any): boolean {
99
+ if(path.length == 0) {
100
+ if(this.multyx.options.verbose) {
101
+ console.error(`Attempting to edit MultyxClientObject with no path. Setting '${this.propertyPath.join('.')}' to ${value}`);
102
+ }
103
+ return false;
104
+ }
105
+ if(path.length == 1) return this.set(path[0], value);
106
+
107
+ let next = this.get(path[0]);
108
+ if(next instanceof MultyxClientValue || next == undefined) {
109
+ if(isNaN(parseInt(path[1]))) {
110
+ this.set(path[0], new EditWrapper({}));
111
+ next = this.get(path[0]) as MultyxClientObject;
112
+ } else {
113
+ this.set(path[0], new EditWrapper([]));
114
+ next = this.get(path[0]) as MultyxClientList;
115
+ }
116
+ }
117
+ return next.set(path.slice(1), value);
70
118
  }
71
119
 
72
- set(property: any, value: any): boolean {
73
- if(value === undefined) return this.delete(property);
120
+ set(property: string | string[], value: any): boolean {
121
+ if(Array.isArray(property)) return this.recursiveSet(property, value);
74
122
 
75
- // Only create new MultyxClientItem when needed
76
- if(this.object[property] instanceof MultyxClientValue) return this.object[property].set(value);
123
+ const serverSet = value instanceof EditWrapper;
124
+ const allowed = serverSet || this.editable;
125
+ if(serverSet || IsMultyxClientItem(value)) value = value.value;
126
+ if(value === undefined) return this.delete(property, serverSet);
77
127
 
78
- // If value was deleted by the server
79
- if(value instanceof EditWrapper && value.value === undefined) return this.delete(property, true);
128
+ // Only create new MultyxClientItem when needed
129
+ if(this.object[property] instanceof MultyxClientValue && typeof value != 'object') {
130
+ return this.object[property].set(serverSet ? new EditWrapper(value) : value);
131
+ }
80
132
 
81
133
  // Attempting to edit property not editable to client
82
- if(!(value instanceof EditWrapper) && !this.editable) {
134
+ if(!allowed) {
83
135
  if(this.multyx.options.verbose) {
84
136
  console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join('.') + '.' + property}' to ${value}`);
85
137
  }
86
138
  return false;
87
139
  }
88
-
140
+
89
141
  // Creating a new value
90
- this.object[property] = new (MultyxClientItemRouter(
91
- value instanceof EditWrapper ? value.value : value
92
- ))(this.multyx, value, [...this.propertyPath, property], this.editable);
93
-
94
- // We have to push into queue, since object may not be fully created
95
- // and there may still be more updates to parse
96
- for(const listener of this.setterListeners) {
97
- this.multyx[Add](() => {
98
- if(this.has(property)) listener(property, this.get(property));
99
- });
142
+ this.object[property] = new (MultyxClientItemRouter(value))(
143
+ this.multyx,
144
+ serverSet ? new EditWrapper(value) : value,
145
+ [...this.propertyPath, property],
146
+ this.editable
147
+ );
148
+
149
+ const propSymbol = Symbol.for("_" + this.propertyPath.join('.') + '.' + property);
150
+ if(this.multyx.events.has(propSymbol)) {
151
+ this.multyx[Done].push(...this.multyx.events.get(propSymbol).map(e =>
152
+ () => e(this.object[property])
153
+ ));
100
154
  }
101
155
 
102
- // Relay change to server if not edit wrapped
103
- if(!(value instanceof EditWrapper)) this.multyx.ws.send(Message.Native({
104
- instruction: 'edit',
105
- path: this.propertyPath,
106
- value: this.object[property].value
107
- }));
108
-
109
156
  return true;
110
157
  }
111
158
 
@@ -131,17 +178,6 @@ export default class MultyxClientObject {
131
178
  return true;
132
179
  }
133
180
 
134
- /**
135
- * Create a callback function that gets called for any current or future property in object
136
- * @param callbackfn Function to call for every property
137
- */
138
- forAll(callbackfn: (key: any, value: any) => void) {
139
- for(let prop in this.object) {
140
- callbackfn(prop, this.get(prop));
141
- }
142
- this.setterListeners.push(callbackfn);
143
- }
144
-
145
181
  keys(): any[] {
146
182
  return Object.keys(this.object);
147
183
  }
@@ -158,13 +194,24 @@ export default class MultyxClientObject {
158
194
  return entryList;
159
195
  }
160
196
 
197
+ /**
198
+ * Wait for a specific property to be set
199
+ * @param property Property to wait for
200
+ * @returns Promise that resolves when the value is set
201
+ */
202
+ await(property: string) {
203
+ if(this.has(property)) return Promise.resolve(this.get(property));
204
+ const propSymbol = Symbol.for("_" + this.propertyPath.join('.') + '.' + property);
205
+ return new Promise(res => this.multyx.on(propSymbol, res));
206
+ }
207
+
161
208
  /**
162
209
  * Unpack constraints from server
163
210
  * @param constraints Packed constraints object mirroring MultyxClientObject shape
164
211
  */
165
212
  [Unpack](constraints: RawObject) {
166
213
  for(const prop in constraints) {
167
- this.object[prop][Unpack](constraints[prop]);
214
+ this.object[prop]?.[Unpack](constraints[prop]);
168
215
  }
169
216
  }
170
217
  }
@@ -1,7 +1,7 @@
1
1
  import type Multyx from '../';
2
2
  import { Message } from "../message";
3
3
  import { Constraint, RawObject, Value } from "../types";
4
- import { BuildConstraint, EditWrapper, Unpack } from '../utils';
4
+ import { BuildConstraint, Done, Edit, EditWrapper, Unpack } from '../utils';
5
5
 
6
6
  export default class MultyxClientValue {
7
7
  private _value: Value;
@@ -10,20 +10,29 @@ export default class MultyxClientValue {
10
10
  editable: boolean;
11
11
  constraints: { [key: string]: Constraint };
12
12
 
13
- private interpolator: undefined | {
14
- get: () => Value,
15
- set: () => void,
16
- history: { time: number, value: Value }[]
17
- };
18
-
13
+ private readModifiers: ((value: Value) => Value)[] = [];
14
+ private editCallbacks: ((value: Value, previousValue: Value) => void)[] = [];
15
+
19
16
  get value() {
20
- if(this.interpolator) return this.interpolator.get();
21
- return this._value;
17
+ return this.readModifiers.reduce((value, modifier) => modifier(value), this._value);
22
18
  }
23
19
 
24
20
  set value(v) {
25
21
  this._value = v;
26
- if(this.interpolator) this.interpolator.set();
22
+ }
23
+
24
+ addReadModifier(modifier: (value: Value) => Value) {
25
+ this.readModifiers.push(modifier);
26
+ }
27
+
28
+ addEditCallback(callback: (value: Value, previousValue: Value) => void) {
29
+ this.editCallbacks.push(callback);
30
+ }
31
+
32
+ [Edit](updatePath: string[], value: any) {
33
+ if(updatePath.length != 0) return;
34
+
35
+ this.set(new EditWrapper(value));
27
36
  }
28
37
 
29
38
  constructor(multyx: Multyx, value: Value | EditWrapper<Value>, propertyPath: string[] = [], editable: boolean) {
@@ -32,11 +41,20 @@ export default class MultyxClientValue {
32
41
  this.multyx = multyx;
33
42
  this.constraints = {};
34
43
  this.set(value);
44
+
45
+ const propSymbol = Symbol.for("_" + this.propertyPath.join('.'));
46
+ if(this.multyx.events.has(propSymbol)) {
47
+ this.multyx[Done].push(...this.multyx.events.get(propSymbol).map(e =>
48
+ () => e(this.value)
49
+ ));
50
+ }
35
51
  }
36
52
 
37
53
  set(value: Value | EditWrapper<Value>) {
38
54
  if(value instanceof EditWrapper) {
55
+ const oldValue = this.value;
39
56
  this.value = value.value;
57
+ this.editCallbacks.forEach(fn => fn(value.value, oldValue));
40
58
  return true;
41
59
  }
42
60
 
@@ -53,7 +71,7 @@ export default class MultyxClientValue {
53
71
  const fn = this.constraints[constraint];
54
72
  nv = fn(nv);
55
73
 
56
- if(nv === null) {
74
+ if(nv === null) {
57
75
  if(this.multyx.options.verbose) {
58
76
  console.error(`Attempting to set property that failed on constraint. Setting '${this.propertyPath.join('.')}' to ${value}, stopped by constraint '${constraint}'`);
59
77
  }
@@ -76,6 +94,14 @@ export default class MultyxClientValue {
76
94
  return true;
77
95
  }
78
96
 
97
+ bindElement(element: HTMLElement) {
98
+ this.addEditCallback((value, previousValue) => {
99
+ if(value !== previousValue) {
100
+ element.innerText = value.toString();
101
+ }
102
+ });
103
+ }
104
+
79
105
  /**
80
106
  * Unpack constraints sent from server and store
81
107
  * @param constraints Packed constraints from server
@@ -88,61 +114,6 @@ export default class MultyxClientValue {
88
114
  }
89
115
  }
90
116
 
91
- /**
92
- * Linearly interpolate value across frames
93
- * Will run 1 frame behind on average
94
- */
95
- Lerp() {
96
- this.interpolator = {
97
- history: [
98
- { value: this._value, time: Date.now() },
99
- { value: this._value, time: Date.now() }
100
- ],
101
- get: () => {
102
- const [e, s] = this.interpolator.history;
103
- const ratio = Math.min(1, (Date.now() - e.time) / Math.min(250, e.time - s.time));
104
- if(Number.isNaN(ratio) || typeof e.value != 'number' || typeof s.value != 'number') return e.value;
105
- return e.value * ratio + s.value * (1 - ratio);
106
- },
107
- set: () => {
108
- this.interpolator.history.pop();
109
- this.interpolator.history.unshift({
110
- value: this._value,
111
- time: Date.now()
112
- });
113
- }
114
- }
115
- }
116
-
117
- PredictiveLerp() {
118
- this.interpolator = {
119
- history: [
120
- { value: this._value, time: Date.now() },
121
- { value: this._value, time: Date.now() },
122
- { value: this._value, time: Date.now() }
123
- ],
124
- get: () => {
125
- const [e, s, p] = this.interpolator.history;
126
- const ratio = Math.min(1, (Date.now() - e.time) / (e.time - s.time));
127
-
128
- if(Number.isNaN(ratio) || typeof p.value != 'number') return e.value;
129
- if(typeof e.value != 'number' || typeof s.value != 'number') return e.value;
130
-
131
- // Speed changed too fast, don't interpolate, return new value
132
- if(Math.abs((e.value - s.value) / (s.value - p.value) - 1) > 0.2) return e.value;
133
-
134
- return e.value * (1 + ratio) - s.value * ratio;
135
- },
136
- set: () => {
137
- this.interpolator.history.pop();
138
- this.interpolator.history.unshift({
139
- value: this._value,
140
- time: Date.now()
141
- });
142
- }
143
- }
144
- }
145
-
146
117
  /* Native methods to allow MultyxValue to be treated as primitive */
147
118
  toString = () => this.value.toString();
148
119
  valueOf = () => this.value;
package/src/message.ts CHANGED
@@ -1,5 +1,58 @@
1
1
  import { Update } from "./types";
2
2
 
3
+ export function UncompressUpdate(str: string) {
4
+ const [target, ...escapedData] = str.split(/;,/);
5
+ const instruction = target[0];
6
+ const specifier = target.slice(1).replace(/;_/g, ';');
7
+ const data = escapedData.map(d => d.replace(/;_/g, ';')).map(d => d == "undefined" ? undefined : JSON.parse(d));
8
+
9
+ if(instruction == '0') return { instruction: 'edit', team: false, path: specifier.split('.'), value: data[0] };
10
+ if(instruction == '1') return { instruction: 'edit', team: true, path: specifier.split('.'), value: data[0] };
11
+
12
+ if(instruction == '2') return { instruction: 'self', property: "controller", data: JSON.parse(specifier) };
13
+ if(instruction == '3') return { instruction: 'self', property: "uuid", data: JSON.parse(specifier) };
14
+ if(instruction == '4') return { instruction: 'self', property: "constraint", data: JSON.parse(specifier) };
15
+ if(instruction == '9') return { instruction: 'self', property: "space", data: JSON.parse(specifier) };
16
+
17
+ if(instruction == '5') return { instruction: 'resp', name: specifier, response: data[0] };
18
+ if(instruction == '6') return { instruction: 'conn', uuid: specifier, publicData: data[0] };
19
+ if(instruction == '7') return { instruction: 'dcon', clientUUID: specifier };
20
+
21
+ if(instruction == '8') return {
22
+ instruction: 'init',
23
+ client: JSON.parse(specifier),
24
+ tps: data[0],
25
+ constraintTable: data[1],
26
+ clients: data[2],
27
+ teams: data[3]
28
+ };
29
+ }
30
+
31
+ export function CompressUpdate(update: Update) {
32
+ let code, pieces;
33
+ if(update.instruction == 'edit') {
34
+ code = 0;
35
+ pieces = [
36
+ update.path.join('.'),
37
+ JSON.stringify(update.value)
38
+ ];
39
+ } else if(update.instruction == 'input') {
40
+ code = 1;
41
+ pieces = [update.input];
42
+ } else if(update.instruction == 'resp') {
43
+ code = 2;
44
+ pieces = [update.name, JSON.stringify(update.response)];
45
+ }
46
+
47
+ if(!pieces) return '';
48
+ let compressed = code;
49
+ for(let i = 0; i < pieces.length; i++) {
50
+ compressed += pieces[i].replace(/;/g, ';_');
51
+ if(i < pieces.length - 1) compressed += ';,';
52
+ }
53
+ return JSON.stringify([compressed]);
54
+ }
55
+
3
56
  export class Message {
4
57
  name: string;
5
58
  data: any;
@@ -13,20 +66,23 @@ export class Message {
13
66
  this.native = native;
14
67
  }
15
68
 
16
- static BundleOperations(deltaTime, operations) {
69
+ static BundleOperations(deltaTime: number, operations: Update[]) {
17
70
  if(!Array.isArray(operations)) operations = [operations];
18
71
  return JSON.stringify(new Message('_', { operations, deltaTime }));
19
72
  }
20
73
 
21
74
  static Native(update: Update) {
22
- return JSON.stringify(new Message('_', update, true));
75
+ return CompressUpdate(update);
23
76
  }
24
77
 
25
78
  static Parse(str: string) {
26
79
  const parsed = JSON.parse(str);
27
- if(parsed.name[0] == '_') parsed.name = parsed.name.slice(1);
28
80
 
29
- return new Message(parsed.name, parsed.data, parsed.name == '');
81
+ if(Array.isArray(parsed)) {
82
+ return new Message('_', parsed, true);
83
+ }
84
+
85
+ return new Message(parsed.name ?? '', parsed.data ?? '', false);
30
86
  }
31
87
 
32
88
  static Create(name: string, data: any) {
package/src/options.ts CHANGED
@@ -7,7 +7,7 @@ export type Options = {
7
7
  };
8
8
 
9
9
  export const DefaultOptions: Options = {
10
- port: 443,
10
+ port: 8443,
11
11
  secure: false,
12
12
  uri: 'localhost',
13
13
  verbose: false,
package/src/types.ts CHANGED
@@ -14,4 +14,10 @@ export type InputUpdate = {
14
14
  data?: RawObject<Value>
15
15
  };
16
16
 
17
- export type Update = EditUpdate | InputUpdate;
17
+ export type ResponseUpdate = {
18
+ instruction: 'resp',
19
+ name: string,
20
+ response: any
21
+ };
22
+
23
+ export type Update = EditUpdate | InputUpdate | ResponseUpdate;
package/src/utils.ts CHANGED
@@ -3,6 +3,7 @@ import { Constraint, RawObject, Value } from "./types";
3
3
  export const Unpack = Symbol("unpack");
4
4
  export const Done = Symbol("done");
5
5
  export const Add = Symbol("add");
6
+ export const Edit = Symbol("edit");
6
7
 
7
8
  export class EditWrapper<T> {
8
9
  value: T;
package/tsconfig.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "target": "es6", // Specify the target ECMAScript version.
4
4
  "lib": ["es2017", "dom"],
5
5
  "module": "commonjs", // Do not use any module system (for browser use).
6
+ "declaration": true,
6
7
  "outDir": "./dist",
7
8
  "rootDir": "./src", // Specify the root directory of your TypeScript source files.
8
9
  }