multyx-client 0.1.5 → 0.1.7

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/items/list.ts CHANGED
@@ -4,7 +4,10 @@ import { Add, Done, Edit, EditWrapper, Unpack } from '../utils';
4
4
  import MultyxClientItemRouter from './router';
5
5
  import { Message } from '../message';
6
6
 
7
+ const isPlainObject = (value: any) => value !== null && typeof value === 'object' && !Array.isArray(value);
8
+
7
9
  export default class MultyxClientList {
10
+ readonly type = 'list';
8
11
  protected list: MultyxClientItem[];
9
12
  private multyx: Multyx;
10
13
  propertyPath: string[];
@@ -82,6 +85,8 @@ export default class MultyxClientList {
82
85
  }
83
86
  }
84
87
 
88
+ [index: number]: MultyxClientItem | undefined;
89
+
85
90
  constructor(multyx: Multyx, list: any[] | EditWrapper<any[]>, propertyPath: string[] = [], editable: boolean){
86
91
  this.list = [];
87
92
  this.propertyPath = propertyPath;
@@ -127,7 +132,7 @@ export default class MultyxClientList {
127
132
  return index >= 0 && index < this.length;
128
133
  }
129
134
 
130
- get(index: number | string[]): MultyxClientItem {
135
+ get(index: number | string[]): MultyxClientItem | undefined{
131
136
  if(typeof index === 'number') return this.list[index];
132
137
  if(index.length == 0) return this;
133
138
  if(index.length == 1) return this.list[parseInt(index[0])];
@@ -155,9 +160,14 @@ export default class MultyxClientList {
155
160
  let next = this.get(parseInt(path[0]));
156
161
  if(next instanceof MultyxClientValue || next == undefined) {
157
162
  this.set(parseInt(path[0]), new EditWrapper({}));
158
- next = this.get(parseInt(path[0])) as MultyxClientObject;
163
+ next = this.get(parseInt(path[0]));
164
+ }
165
+
166
+ if(!next || next instanceof MultyxClientValue) {
167
+ return false;
159
168
  }
160
- return next.set(path.slice(1), value);
169
+
170
+ return (next as MultyxClientObject | MultyxClientList).set(path.slice(1), value);
161
171
  }
162
172
 
163
173
  set(index: number | string[], value: any): boolean {
@@ -167,41 +177,37 @@ export default class MultyxClientList {
167
177
 
168
178
  const serverSet = value instanceof EditWrapper;
169
179
  const allowed = serverSet || this.editable;
170
- if(serverSet || IsMultyxClientItem(value)) value = value.value;
171
- if(value === undefined) return this.delete(index, serverSet);
180
+ const incoming = (serverSet || IsMultyxClientItem(value)) ? value.value : value;
181
+ if(incoming === undefined) return this.delete(index, serverSet);
172
182
 
173
- // If value is a MultyxClientValue, set the value
174
- if(this.list[index] instanceof MultyxClientValue && typeof value != 'object') {
175
- return this.list[index].set(serverSet ? new EditWrapper(value) : value);
183
+ if(serverSet && this.tryApplyServerValue(index, incoming, oldValue)) {
184
+ return true;
185
+ }
186
+
187
+ // If value is a MultyxClientValue, set the value directly
188
+ if(this.list[index] instanceof MultyxClientValue && (typeof incoming !== 'object' || incoming === null)) {
189
+ const result = this.list[index].set(serverSet ? new EditWrapper(incoming) : incoming);
190
+ this.enqueueEditCallbacks(index, oldValue);
191
+ return result;
176
192
  }
177
193
 
178
194
  // Attempting to edit property not editable to client
179
195
  if(!allowed) {
180
196
  if(this.multyx.options.verbose) {
181
- console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join('.') + '.' + index}' to ${value}`);
197
+ console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join('.') + '.' + index}' to ${incoming}`);
182
198
  }
183
199
  return false;
184
200
  }
185
201
 
186
- this.list[index] = new (MultyxClientItemRouter(value))(
202
+ this.list[index] = new (MultyxClientItemRouter(incoming))(
187
203
  this.multyx,
188
- serverSet ? new EditWrapper(value) : value,
204
+ serverSet ? new EditWrapper(incoming) : incoming,
189
205
  [...this.propertyPath, index.toString()],
190
206
  this.editable
191
207
  );
192
208
 
193
- const propSymbol = Symbol.for("_" + this.propertyPath.join('.') + '.' + index);
194
- if(this.multyx.events.has(propSymbol)) {
195
- this.multyx[Done].push(...this.multyx.events.get(propSymbol).map(e =>
196
- () => e(this.list[index])
197
- ));
198
- }
199
-
200
- // We have to push into queue, since object may not be fully created
201
- // and there may still be more updates to parse
202
- for(const listener of this.editCallbacks) {
203
- this.multyx[Add](() => listener(index, this.get(index), oldValue));
204
- }
209
+ this.notifyIndexWaiters(index);
210
+ this.enqueueEditCallbacks(index, oldValue);
205
211
 
206
212
  return true;
207
213
  }
@@ -252,8 +258,8 @@ export default class MultyxClientList {
252
258
  return this.length;
253
259
  }
254
260
 
255
- pop(): MultyxClientItem | null {
256
- if(this.length === 0) return null;
261
+ pop(): MultyxClientItem | undefined {
262
+ if(this.length === 0) return undefined;
257
263
 
258
264
  const res = this.get(this.length);
259
265
  this.delete(this.length);
@@ -288,7 +294,7 @@ export default class MultyxClientList {
288
294
  }
289
295
 
290
296
  splice(start: number, deleteCount?: number, ...items: any[]) {
291
- return this.list.splice(start, deleteCount, ...items);
297
+ return this.list.splice(start, deleteCount ?? 0, ...items);
292
298
  }
293
299
 
294
300
  setSplice(start: number, deleteCount?: number, ...items: any[]) {
@@ -433,6 +439,8 @@ export default class MultyxClientList {
433
439
  return Array(this.length).fill(0).map((_, i) => i);
434
440
  }
435
441
 
442
+ [Edit](){}
443
+
436
444
  [Unpack](constraints: any[]) {
437
445
  for(let i=0; i<this.length; i++) {
438
446
  this.get(i)?.[Unpack](constraints[i]);
@@ -443,9 +451,61 @@ export default class MultyxClientList {
443
451
  [Symbol.iterator](): Iterator<MultyxClientItem> {
444
452
  const values = [];
445
453
  for(let i=0; i<this.length; i++) values[i] = this.get(i);
446
- return values[Symbol.iterator]();
454
+ return values[Symbol.iterator]() as Iterator<MultyxClientItem>;
447
455
  }
448
456
  toString = () => this.value.toString();
449
457
  valueOf = () => this.value;
450
458
  [Symbol.toPrimitive] = () => this.value;
459
+
460
+ hydrateFromServer(values: any[]) {
461
+ if(!Array.isArray(values)) return;
462
+ for(let i=0; i<values.length; i++) {
463
+ this.set(i, new EditWrapper(values[i]));
464
+ }
465
+ for(let i=values.length; i<this.length; i++) {
466
+ this.delete(i, true);
467
+ }
468
+ this.length = values.length;
469
+ }
470
+
471
+ private tryApplyServerValue(index: number, incoming: any, oldValue: MultyxClientItem | undefined) {
472
+ const current = this.list[index];
473
+ if(!current) return false;
474
+
475
+ if(current instanceof MultyxClientValue && (typeof incoming !== 'object' || incoming === null)) {
476
+ current.set(new EditWrapper(incoming));
477
+ this.enqueueEditCallbacks(index, oldValue);
478
+ return true;
479
+ }
480
+
481
+ const canHydrate = typeof (current as any)?.hydrateFromServer === 'function';
482
+ if(Array.isArray(incoming) && canHydrate && (current as any).type === 'list') {
483
+ (current as any).hydrateFromServer(incoming);
484
+ this.enqueueEditCallbacks(index, oldValue);
485
+ return true;
486
+ }
487
+
488
+ if(isPlainObject(incoming) && canHydrate && (current as any).type === 'object') {
489
+ (current as any).hydrateFromServer(incoming);
490
+ this.enqueueEditCallbacks(index, oldValue);
491
+ return true;
492
+ }
493
+
494
+ return false;
495
+ }
496
+
497
+ private notifyIndexWaiters(index: number) {
498
+ const propSymbol = Symbol.for("_" + this.propertyPath.join('.') + '.' + index);
499
+ if(this.multyx.events.has(propSymbol)) {
500
+ this.multyx[Done].push(...(this.multyx.events.get(propSymbol)?.map(e =>
501
+ () => e(this.list[index])
502
+ ) ?? []));
503
+ }
504
+ }
505
+
506
+ private enqueueEditCallbacks(index: number, oldValue: MultyxClientItem | undefined) {
507
+ for(const listener of this.editCallbacks) {
508
+ this.multyx[Add](() => listener(index, this.get(index), oldValue));
509
+ }
510
+ }
451
511
  }
@@ -7,7 +7,10 @@ import { IsMultyxClientItem, type MultyxClientList, type MultyxClientItem } from
7
7
  import MultyxClientItemRouter from "./router";
8
8
  import MultyxClientValue from "./value";
9
9
 
10
+ const isPlainObject = (value: any) => value !== null && typeof value === 'object' && !Array.isArray(value);
11
+
10
12
  export default class MultyxClientObject {
13
+ readonly type = 'object';
11
14
  protected object: RawObject<MultyxClientItem>;
12
15
  private multyx: Multyx;
13
16
  propertyPath: string[];
@@ -16,7 +19,7 @@ export default class MultyxClientObject {
16
19
  private editCallbacks: ((key: any, value: any) => void)[] = [];
17
20
 
18
21
  get value() {
19
- const parsed = {};
22
+ const parsed: RawObject<MultyxClientItem> = {};
20
23
  for(const prop in this.object) parsed[prop] = this.object[prop];
21
24
  return parsed;
22
25
  }
@@ -38,9 +41,11 @@ export default class MultyxClientObject {
38
41
  if(!this.has(updatePath[0])) {
39
42
  this.set(updatePath[0], new EditWrapper({}));
40
43
  }
41
- this.get(updatePath[0])[Edit](updatePath.slice(1), value);
44
+ this.get(updatePath[0])?.[Edit](updatePath.slice(1), value);
42
45
  }
43
46
 
47
+ [key: string]: any;
48
+
44
49
  constructor(multyx: Multyx, object: RawObject | EditWrapper<RawObject>, propertyPath: string[] = [], editable: boolean) {
45
50
  this.object = {};
46
51
  this.propertyPath = propertyPath;
@@ -85,7 +90,7 @@ export default class MultyxClientObject {
85
90
  return property in this.object;
86
91
  }
87
92
 
88
- get(property: string | string[]): MultyxClientItem {
93
+ get(property: string | string[]): MultyxClientItem | undefined {
89
94
  if(typeof property === 'string') return this.object[property];
90
95
  if(property.length == 0) return this;
91
96
  if(property.length == 1) return this.object[property[0]];
@@ -122,36 +127,35 @@ export default class MultyxClientObject {
122
127
 
123
128
  const serverSet = value instanceof EditWrapper;
124
129
  const allowed = serverSet || this.editable;
125
- if(serverSet || IsMultyxClientItem(value)) value = value.value;
126
- if(value === undefined) return this.delete(property, serverSet);
130
+ const incoming = (serverSet || IsMultyxClientItem(value)) ? value.value : value;
131
+ if(incoming === undefined) return this.delete(property, serverSet);
132
+
133
+ if(serverSet && this.applyServerValue(property, incoming)) {
134
+ return true;
135
+ }
127
136
 
128
137
  // 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);
138
+ if(this.object[property] instanceof MultyxClientValue && (typeof incoming !== 'object' || incoming === null)) {
139
+ return this.object[property].set(serverSet ? new EditWrapper(incoming) : incoming);
131
140
  }
132
141
 
133
142
  // Attempting to edit property not editable to client
134
143
  if(!allowed) {
135
144
  if(this.multyx.options.verbose) {
136
- console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join('.') + '.' + property}' to ${value}`);
145
+ console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join('.') + '.' + property}' to ${incoming}`);
137
146
  }
138
147
  return false;
139
148
  }
140
149
 
141
150
  // Creating a new value
142
- this.object[property] = new (MultyxClientItemRouter(value))(
151
+ this.object[property] = new (MultyxClientItemRouter(incoming))(
143
152
  this.multyx,
144
- serverSet ? new EditWrapper(value) : value,
153
+ serverSet ? new EditWrapper(incoming) : incoming,
145
154
  [...this.propertyPath, property],
146
155
  this.editable
147
156
  );
148
157
 
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
- ));
154
- }
158
+ this.notifyPropertyWaiters(property);
155
159
 
156
160
  return true;
157
161
  }
@@ -214,4 +218,48 @@ export default class MultyxClientObject {
214
218
  this.object[prop]?.[Unpack](constraints[prop]);
215
219
  }
216
220
  }
221
+
222
+ hydrateFromServer(value: RawObject) {
223
+ if(!isPlainObject(value)) return;
224
+ const remaining = new Set(Object.keys(this.object));
225
+ for(const [key, entry] of Object.entries(value)) {
226
+ remaining.delete(key);
227
+ this.set(key, new EditWrapper(entry));
228
+ }
229
+ for(const key of remaining) {
230
+ this.delete(key, true);
231
+ }
232
+ }
233
+
234
+ private applyServerValue(property: string, incoming: any) {
235
+ const current = this.object[property];
236
+ if(!current) return false;
237
+
238
+ if(current instanceof MultyxClientValue && (typeof incoming !== 'object' || incoming === null)) {
239
+ current.set(new EditWrapper(incoming));
240
+ return true;
241
+ }
242
+
243
+ const canHydrate = typeof (current as any)?.hydrateFromServer === 'function';
244
+ if(Array.isArray(incoming) && canHydrate && (current as any).type === 'list') {
245
+ (current as any).hydrateFromServer(incoming);
246
+ return true;
247
+ }
248
+
249
+ if(isPlainObject(incoming) && canHydrate && (current as any).type === 'object') {
250
+ (current as any).hydrateFromServer(incoming);
251
+ return true;
252
+ }
253
+
254
+ return false;
255
+ }
256
+
257
+ private notifyPropertyWaiters(property: string) {
258
+ const propSymbol = Symbol.for("_" + this.propertyPath.join('.') + '.' + property);
259
+ if(this.multyx.events.has(propSymbol)) {
260
+ this.multyx[Done].push(...(this.multyx.events.get(propSymbol)?.map(e =>
261
+ () => e(this.object[property])
262
+ ) ?? []));
263
+ }
264
+ }
217
265
  }
@@ -44,9 +44,9 @@ export default class MultyxClientValue {
44
44
 
45
45
  const propSymbol = Symbol.for("_" + this.propertyPath.join('.'));
46
46
  if(this.multyx.events.has(propSymbol)) {
47
- this.multyx[Done].push(...this.multyx.events.get(propSymbol).map(e =>
47
+ this.multyx[Done].push(...(this.multyx.events.get(propSymbol)?.map(e =>
48
48
  () => e(this.value)
49
- ));
49
+ ) ?? []));
50
50
  }
51
51
  }
52
52
 
@@ -66,7 +66,7 @@ export default class MultyxClientValue {
66
66
  return false;
67
67
  }
68
68
 
69
- let nv = value;
69
+ let nv: Value | null = value;
70
70
  for(const constraint in this.constraints) {
71
71
  const fn = this.constraints[constraint];
72
72
  nv = fn(nv);
package/src/message.ts CHANGED
@@ -15,8 +15,8 @@ export function UncompressUpdate(str: string) {
15
15
  if(instruction == '9') return { instruction: 'self', property: "space", data: JSON.parse(specifier) };
16
16
 
17
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 };
18
+ if(instruction == '6') return { instruction: 'conn', uuid: specifier, data: data[0] };
19
+ if(instruction == '7') return { instruction: 'dcon', client: specifier };
20
20
 
21
21
  if(instruction == '8') return {
22
22
  instruction: 'init',
@@ -45,8 +45,8 @@ export function CompressUpdate(update: Update) {
45
45
  pieces = [update.name, JSON.stringify(update.response)];
46
46
  }
47
47
 
48
- if(!pieces) return '';
49
- let compressed = code;
48
+ if(!pieces || code === undefined) return '';
49
+ let compressed = code.toString();
50
50
  for(let i = 0; i < pieces.length; i++) {
51
51
  compressed += pieces[i].replace(/;/g, ';_');
52
52
  if(i < pieces.length - 1) compressed += ';,';
package/tsconfig.json CHANGED
@@ -6,5 +6,10 @@
6
6
  "declaration": true,
7
7
  "outDir": "./dist",
8
8
  "rootDir": "./src", // Specify the root directory of your TypeScript source files.
9
+ "esModuleInterop": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "strict": true,
12
+ "strictPropertyInitialization": false,
13
+ "skipLibCheck": true
9
14
  }
10
15
  }