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/dist/controller.js +30 -31
- package/dist/index.d.ts +2 -1
- package/dist/index.js +9 -5
- package/dist/items/index.js +6 -3
- package/dist/items/list.d.ts +13 -6
- package/dist/items/list.js +71 -20
- package/dist/items/object.d.ts +10 -4
- package/dist/items/object.js +57 -14
- package/dist/items/value.d.ts +1 -1
- package/dist/items/value.js +2 -1
- package/dist/message.d.ts +3 -15
- package/dist/message.js +4 -4
- package/multyx.js +1 -1
- package/package.json +1 -1
- package/src/controller.ts +67 -67
- package/src/index.ts +10 -7
- package/src/items/list.ts +87 -27
- package/src/items/object.ts +64 -16
- package/src/items/value.ts +3 -3
- package/src/message.ts +4 -4
- package/tsconfig.json +5 -0
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]))
|
|
163
|
+
next = this.get(parseInt(path[0]));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if(!next || next instanceof MultyxClientValue) {
|
|
167
|
+
return false;
|
|
159
168
|
}
|
|
160
|
-
|
|
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
|
-
|
|
171
|
-
if(
|
|
180
|
+
const incoming = (serverSet || IsMultyxClientItem(value)) ? value.value : value;
|
|
181
|
+
if(incoming === undefined) return this.delete(index, serverSet);
|
|
172
182
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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 ${
|
|
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(
|
|
202
|
+
this.list[index] = new (MultyxClientItemRouter(incoming))(
|
|
187
203
|
this.multyx,
|
|
188
|
-
serverSet ? new EditWrapper(
|
|
204
|
+
serverSet ? new EditWrapper(incoming) : incoming,
|
|
189
205
|
[...this.propertyPath, index.toString()],
|
|
190
206
|
this.editable
|
|
191
207
|
);
|
|
192
208
|
|
|
193
|
-
|
|
194
|
-
|
|
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 |
|
|
256
|
-
if(this.length === 0) return
|
|
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
|
}
|
package/src/items/object.ts
CHANGED
|
@@ -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
|
-
|
|
126
|
-
if(
|
|
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
|
|
130
|
-
return this.object[property].set(serverSet ? new EditWrapper(
|
|
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 ${
|
|
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(
|
|
151
|
+
this.object[property] = new (MultyxClientItemRouter(incoming))(
|
|
143
152
|
this.multyx,
|
|
144
|
-
serverSet ? new EditWrapper(
|
|
153
|
+
serverSet ? new EditWrapper(incoming) : incoming,
|
|
145
154
|
[...this.propertyPath, property],
|
|
146
155
|
this.editable
|
|
147
156
|
);
|
|
148
157
|
|
|
149
|
-
|
|
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
|
}
|
package/src/items/value.ts
CHANGED
|
@@ -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)
|
|
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,
|
|
19
|
-
if(instruction == '7') return { instruction: 'dcon',
|
|
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
|
}
|