alchemymvc 1.3.9 → 1.3.11

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.
@@ -0,0 +1,965 @@
1
+ const SESSION_KEY = 'Syncables',
2
+ UPDATE_ID = Symbol('update_id'),
3
+ CLIENT_MAP = new Classes.WeakValueMap(),
4
+ QUEUE_CALLBACKS = Symbol('queue_callbacks'),
5
+ QUEUE_CALLBACK_ID = Symbol('queue_callback_id');
6
+
7
+ /**
8
+ * The Syncable class
9
+ *
10
+ * @constructor
11
+ *
12
+ * @author Jelle De Loecker <jelle@elevenways.be>
13
+ * @since 1.3.10
14
+ * @version 1.3.10
15
+ *
16
+ * @param {String} type
17
+ */
18
+ const Syncable = Function.inherits('Alchemy.Base', function Syncable(type) {
19
+
20
+ if (!type) {
21
+ throw new Error('Each Syncable must have a type');
22
+ }
23
+
24
+ this.root = this;
25
+ this.type = type;
26
+ this.log = [];
27
+ this.counter = 0;
28
+
29
+ this.queues = new Map();
30
+
31
+ if (Blast.isNode) {
32
+ // Only used on the server
33
+ this.s2c_links = new Map();
34
+ } else {
35
+ // Only used on the client
36
+ this.c2s_link = null;
37
+ }
38
+ });
39
+
40
+ if (Blast.isNode) {
41
+
42
+ /**
43
+ * Handle an incoming linkup
44
+ *
45
+ * @author Jelle De Loecker <jelle@elevenways.be>
46
+ * @since 1.3.10
47
+ * @version 1.3.10
48
+ *
49
+ * @param {Conduit}
50
+ * @param {Linkup}
51
+ * @param {Object}
52
+ */
53
+ Syncable.setStatic(function handleLink(conduit, linkup, config) {
54
+
55
+ let syncables = conduit.session(SESSION_KEY),
56
+ syncable,
57
+ error_msg;
58
+
59
+ if (!syncables) {
60
+ error_msg = 'No syncables found';
61
+ } else {
62
+
63
+ let type_map = syncables.get(config.type);
64
+
65
+ if (!type_map) {
66
+ error_msg = 'No syncables found for type: ' + config.type;
67
+ } else {
68
+ syncable = type_map.get(config.id);
69
+
70
+ if (!syncable) {
71
+ error_msg = 'No syncable found for id: ' + config.id;
72
+ }
73
+ }
74
+ }
75
+
76
+ if (error_msg) {
77
+ let err = new Error(error_msg);
78
+ console.log('ERROR:', err);
79
+ linkup.emit('error', err);
80
+ linkup.destroy();
81
+ return;
82
+ }
83
+
84
+ syncable.attachClient(conduit, linkup, config);
85
+ }, false);
86
+
87
+ /**
88
+ * Attach a client
89
+ *
90
+ * @author Jelle De Loecker <jelle@elevenways.be>
91
+ * @since 1.3.10
92
+ * @version 1.3.10
93
+ */
94
+ Syncable.setMethod(function attachClient(scene_id, linkup, config) {
95
+
96
+ if (!scene_id) {
97
+ return;
98
+ }
99
+
100
+ if (!this.s2c_links) {
101
+ this.s2c_links = new Map();
102
+ }
103
+
104
+ if (typeof scene_id == 'object') {
105
+ scene_id = scene_id.scene_id;
106
+ }
107
+
108
+ if (!scene_id) {
109
+ return;
110
+ }
111
+
112
+ this.s2c_links.set(scene_id, linkup);
113
+
114
+ linkup.syncable_version = config.version || 0;
115
+
116
+ linkup.on('destroyed', () => {
117
+ this.s2c_links.delete(scene_id);
118
+ });
119
+
120
+ linkup.on('upstream-method', async (args, responder) => {
121
+
122
+ try {
123
+ // Get the value
124
+ let result = await this.handleUpstreamMethodRequest(args[0], args[1]);
125
+
126
+ // Make sure it's ready for the client-side
127
+ result = JSON.clone(result, 'toHawkejs');
128
+
129
+ // Send it to the client
130
+ responder(null, result);
131
+ } catch (err) {
132
+ responder(err);
133
+ }
134
+ });
135
+
136
+ // Send any updates that happened before the linkup was created
137
+ this.sendUpdateToLink(linkup);
138
+
139
+ this.emit('ready');
140
+ });
141
+
142
+ /**
143
+ * Handle an upstream method request
144
+ *
145
+ * @author Jelle De Loecker <jelle@elevenways.be>
146
+ * @since 1.3.10
147
+ * @version 1.3.10
148
+ *
149
+ * @param {String} name
150
+ * @param {Array} args
151
+ */
152
+ Syncable.setMethod(function handleUpstreamMethodRequest(name, args) {
153
+
154
+ let method = this[name];
155
+
156
+ if (!method) {
157
+ throw new Error('No method found with name: ' + name);
158
+ }
159
+
160
+ if (!method.is_syncable_upstream) {
161
+ throw new Error('Method is not syncable upstream: ' + name);
162
+ }
163
+
164
+ return method.apply(this, args);
165
+ });
166
+
167
+ /**
168
+ * Register a client by one of their conduits.
169
+ * Only clients that are registered can be synced.
170
+ *
171
+ * @author Jelle De Loecker <jelle@elevenways.be>
172
+ * @since 1.3.10
173
+ * @version 1.3.10
174
+ *
175
+ * @type {Conduit}
176
+ */
177
+ Syncable.setMethod(function registerClient(conduit) {
178
+
179
+ if (!conduit) {
180
+ return;
181
+ }
182
+
183
+ let syncables = conduit.session(SESSION_KEY);
184
+
185
+ if (!syncables) {
186
+ syncables = new Map();
187
+ conduit.session(SESSION_KEY, syncables);
188
+ }
189
+
190
+ let type_map = syncables.get(this.type);
191
+
192
+ if (!type_map) {
193
+ type_map = new Map();
194
+ syncables.set(this.type, type_map);
195
+ }
196
+
197
+ type_map.set(this.id, this);
198
+ });
199
+ }
200
+
201
+ if (Blast.isBrowser) {
202
+
203
+ /**
204
+ * Start the sync link from the browser to the server
205
+ *
206
+ * @author Jelle De Loecker <jelle@elevenways.be>
207
+ * @since 1.3.10
208
+ * @version 1.3.10
209
+ */
210
+ Syncable.setMethod(function startSyncLink() {
211
+
212
+ if (typeof hawkejs == 'undefined' || !hawkejs.scene) {
213
+ Blast.setImmediate(this.startSyncLink.bind(this));
214
+ return;
215
+ }
216
+
217
+ alchemy.enableWebsockets();
218
+
219
+ let data = {
220
+ version : this.version,
221
+ type : this.type,
222
+ id : this.id,
223
+ };
224
+
225
+ let link = this.c2s_link = alchemy.linkup('syncablelink', data, () => {
226
+ this.emit('ready');
227
+ });
228
+
229
+ link.on('process_updates', (data) => {
230
+
231
+ for (let update of data.updates) {
232
+ this.processUpdate(update);
233
+ }
234
+
235
+ this.version = data.version;
236
+ });
237
+ });
238
+ }
239
+
240
+ /**
241
+ * Add a syncable method.
242
+ * The method itself should probably NOT trigger changes
243
+ *
244
+ * @author Jelle De Loecker <jelle@elevenways.be>
245
+ * @since 1.3.10
246
+ * @version 1.3.10
247
+ *
248
+ * @param {Object} data
249
+ *
250
+ * @return {Syncable}
251
+ */
252
+ Syncable.setStatic(function setSyncMethod(types, method) {
253
+ return this.setHandledMethod(types, method, function handler(method, args) {
254
+
255
+ let result = method.apply(this, args);
256
+
257
+ if (this.is_server) {
258
+ this.addLog('call', [method.name, args]);
259
+ }
260
+
261
+ return result;
262
+ });
263
+ });
264
+
265
+ /**
266
+ * Add a method that fetches info from the server.
267
+ * The response is always a promise.
268
+ *
269
+ * @author Jelle De Loecker <jelle@elevenways.be>
270
+ * @since 1.3.10
271
+ * @version 1.3.10
272
+ *
273
+ * @param {Object} data
274
+ *
275
+ * @return {Syncable}
276
+ */
277
+ Syncable.setStatic(function setUpstreamMethod(types, method) {
278
+ let result = this.setHandledMethod(types, method, function handler(method, args) {
279
+
280
+ let result;
281
+
282
+ if (this.is_server) {
283
+ result = method.apply(this, args);
284
+ } else {
285
+
286
+ let pledge = new Pledge();
287
+
288
+ let bomb = Function.timebomb(30*1000, (err) => {
289
+ pledge.reject(err);
290
+ });
291
+
292
+ Pledge.done(this.c2s_link.demand('upstream-method', [method.name, args]), (err, result) => {
293
+
294
+ bomb.defuse();
295
+
296
+ if (err) {
297
+ pledge.reject(err);
298
+ } else {
299
+ pledge.resolve(result);
300
+ }
301
+ });
302
+
303
+ result = pledge;
304
+ }
305
+
306
+ return result;
307
+ });
308
+
309
+ result.is_syncable_upstream = true;
310
+
311
+ return result;
312
+ });
313
+
314
+ /**
315
+ * Add a method that may or may not use types
316
+ *
317
+ * @author Jelle De Loecker <jelle@elevenways.be>
318
+ * @since 1.3.10
319
+ * @version 1.3.10
320
+ *
321
+ * @param {Array} types The optional types of the method
322
+ * @param {Function} method The main method implementation
323
+ * @param {Function} handler The handler
324
+ *
325
+ * @return {Syncable}
326
+ */
327
+ Syncable.setStatic(function setHandledMethod(types, method, handler) {
328
+
329
+ let result;
330
+
331
+ if (typeof types == 'function') {
332
+ method = types;
333
+ types = null;
334
+ }
335
+
336
+ function director(...args) {
337
+ return handler.call(this, method, args);
338
+ }
339
+
340
+ if (types) {
341
+ result = this.setTypedMethod(types, method.name, director);
342
+ } else {
343
+ result = this.setMethod(method.name, director);
344
+ }
345
+
346
+ return result;
347
+ });
348
+
349
+ /**
350
+ * Add a property
351
+ *
352
+ * @author Jelle De Loecker <jelle@elevenways.be>
353
+ * @since 1.3.10
354
+ * @version 1.3.10
355
+ *
356
+ * @param {String} name
357
+ * @param {Object} options
358
+ */
359
+ Syncable.setStatic(function setStateProperty(name, options) {
360
+
361
+ if (!options) {
362
+ options = {};
363
+ }
364
+
365
+ let has_default = !!options.default,
366
+ allow_set;
367
+
368
+ if (Blast.isNode) {
369
+ allow_set = options.allow_server_set ?? true;
370
+ } else {
371
+ allow_set = options.allow_client_set ?? false;
372
+ }
373
+
374
+ let getter;
375
+
376
+ if (has_default) {
377
+ let has_default_function = typeof options.default == 'function';
378
+
379
+ if (has_default_function) {
380
+ let default_function = options.default;
381
+
382
+ getter = function getter() {
383
+ let result = this.state[name];
384
+
385
+ if (result == null) {
386
+ this.state[name] = result = default_function.call(this);
387
+ }
388
+
389
+ return result;
390
+ };
391
+
392
+ } else {
393
+ let default_value = options.default;
394
+
395
+ getter = function getter() {
396
+
397
+ let result = this.state[name];
398
+
399
+ if (result == null) {
400
+ this.state[name] = result = default_value;
401
+ }
402
+
403
+ return result;
404
+ };
405
+ }
406
+
407
+ } else {
408
+ getter = function getter() {
409
+ return this.state[name];
410
+ }
411
+ }
412
+
413
+ if (allow_set) {
414
+ this.setProperty(name, getter, function setValue(value) {
415
+ this.setProperty(name, value);
416
+ });
417
+ } else {
418
+ this.setProperty(name, getter);
419
+ }
420
+ });
421
+
422
+ /**
423
+ * Undry this value
424
+ *
425
+ * @author Jelle De Loecker <jelle@elevenways.be>
426
+ * @since 1.3.10
427
+ * @version 1.3.10
428
+ *
429
+ * @param {Object} data
430
+ *
431
+ * @return {Syncable}
432
+ */
433
+ Syncable.setStatic(function unDry(data) {
434
+
435
+ let result;
436
+
437
+ // Try to reuse the same instance on the client side
438
+ if (Blast.isBrowser) {
439
+ result = CLIENT_MAP.get(data.id);
440
+
441
+ if (result) {
442
+ return result;
443
+ }
444
+ }
445
+
446
+ let clone = JSON.clone(data);
447
+
448
+ result = new this(clone.type);
449
+
450
+ result.id = clone.id;
451
+ result.state = clone.state;
452
+ result.version = clone.version;
453
+ result.queues = clone.queues;
454
+
455
+ if (Blast.isBrowser) {
456
+ result.startSyncLink();
457
+ }
458
+
459
+ return result;
460
+ });
461
+
462
+ /**
463
+ * Is this the server instance?
464
+ *
465
+ * @author Jelle De Loecker <jelle@elevenways.be>
466
+ * @since 1.3.10
467
+ * @version 1.3.10
468
+ *
469
+ * @type {Conduit}
470
+ */
471
+ Syncable.setProperty(function is_server() {
472
+ return Blast.isNode;
473
+ });
474
+
475
+ /**
476
+ * Enforce the ID property
477
+ *
478
+ * @author Jelle De Loecker <jelle@elevenways.be>
479
+ * @since 1.3.10
480
+ * @version 1.3.10
481
+ *
482
+ * @type {String}
483
+ */
484
+ Syncable.enforceProperty(function id(new_value) {
485
+
486
+ if (!new_value) {
487
+ new_value = Crypto.randomHex(16);
488
+ }
489
+
490
+ // Remember this instance for later
491
+ if (Blast.isBrowser) {
492
+ CLIENT_MAP.set(new_value, this);
493
+ }
494
+
495
+ return new_value;
496
+ });
497
+
498
+ /**
499
+ * Enforce the queues property
500
+ *
501
+ * @author Jelle De Loecker <jelle@elevenways.be>
502
+ * @since 1.3.10
503
+ * @version 1.3.10
504
+ *
505
+ * @type {Map}
506
+ */
507
+ Syncable.enforceProperty(function queues(new_value) {
508
+
509
+ if (!new_value) {
510
+ new_value = new Map();
511
+ } else {
512
+
513
+ // Loop over every queue entry
514
+ for (let [name, queue] of new_value) {
515
+
516
+ let listeners = [];
517
+
518
+ if (queue.listeners) {
519
+ for (let listener of queue.listeners) {
520
+ if (listener) {
521
+ listeners.push(listener);
522
+ }
523
+ }
524
+ }
525
+
526
+ queue.listeners = listeners;
527
+ }
528
+ }
529
+
530
+ return new_value;
531
+ });
532
+
533
+ /**
534
+ * Enforce the state property
535
+ *
536
+ * @author Jelle De Loecker <jelle@elevenways.be>
537
+ * @since 1.3.10
538
+ * @version 1.3.10
539
+ *
540
+ * @type {String}
541
+ */
542
+ Syncable.enforceProperty(function state(new_value) {
543
+
544
+ if (!new_value) {
545
+ new_value = {};
546
+ }
547
+
548
+ return new_value;
549
+ });
550
+
551
+ /**
552
+ * Enforce the version property
553
+ *
554
+ * @author Jelle De Loecker <jelle@elevenways.be>
555
+ * @since 1.3.10
556
+ * @version 1.3.10
557
+ *
558
+ * @type {String}
559
+ */
560
+ Syncable.enforceProperty(function version(new_value) {
561
+
562
+ if (!new_value) {
563
+ new_value = 0;
564
+ }
565
+
566
+ return new_value;
567
+ });
568
+
569
+ /**
570
+ * Clone for hawkejs
571
+ *
572
+ * @author Jelle De Loecker <jelle@elevenways.be>
573
+ * @since 1.3.10
574
+ * @version 1.3.10
575
+ */
576
+ Syncable.setMethod(function toHawkejs() {
577
+ return this.constructor.unDry(this.toDry().value);
578
+ });
579
+
580
+ /**
581
+ * Serialize this syncable
582
+ *
583
+ * @author Jelle De Loecker <jelle@elevenways.be>
584
+ * @since 1.3.10
585
+ * @version 1.3.10
586
+ */
587
+ Syncable.setMethod(function toDry() {
588
+
589
+ let queues = new Map();
590
+
591
+ for (let [name, queue] of this.queues) {
592
+
593
+ // Keep everything except the `listeners`
594
+ let entry = {
595
+ name : name,
596
+ listeners : [],
597
+ messages : queue.messages,
598
+ };
599
+
600
+ queues.set(name, entry);
601
+ }
602
+
603
+ let result = {
604
+ version : this.version,
605
+ queues : queues,
606
+ state : this.state,
607
+ type : this.type,
608
+ id : this.id,
609
+ };
610
+
611
+ return {value: result};
612
+ });
613
+
614
+ /**
615
+ * Process an update
616
+ *
617
+ * @author Jelle De Loecker <jelle@elevenways.be>
618
+ * @since 1.3.10
619
+ * @version 1.3.10
620
+ */
621
+ Syncable.setMethod(function processUpdate(update) {
622
+
623
+ let type = update.type,
624
+ args = update.args;
625
+
626
+ if (type == 'set') {
627
+ this.setProperty(...args);
628
+ } else if (type == 'call') {
629
+ let name = args[0],
630
+ method_args = args[1];
631
+
632
+ this[name](...method_args);
633
+ } else if (type == 'push_queue') {
634
+ let name = args[0],
635
+ method_args = args[1];
636
+
637
+ this.pushQueue(name, ...method_args);
638
+ } else if (type == 'clear_queue') {
639
+ let name = args[0];
640
+ this.clearQueue(name);
641
+ } else {
642
+ throw new Error('Unknown update type: ' + type);
643
+ }
644
+ });
645
+
646
+ /**
647
+ * Send the actual update
648
+ *
649
+ * @author Jelle De Loecker <jelle@elevenways.be>
650
+ * @since 1.3.10
651
+ * @version 1.3.10
652
+ */
653
+ Syncable.setMethod(function sendUpdates() {
654
+
655
+ if (this[UPDATE_ID]) {
656
+ clearTimeout(this[UPDATE_ID]);
657
+ this[UPDATE_ID] = null;
658
+ }
659
+
660
+ for (let linkup of this.s2c_links.values()) {
661
+ this.sendUpdateToLink(linkup);
662
+ }
663
+ });
664
+
665
+ /**
666
+ * Emit a change event for a certain property
667
+ *
668
+ * @author Jelle De Loecker <jelle@elevenways.be>
669
+ * @since 1.3.10
670
+ * @version 1.3.10
671
+ *
672
+ * @param {String} property
673
+ */
674
+ Syncable.setMethod(function emitPropertyChange(property) {
675
+ let value = this[property];
676
+ this.emit('property_change_' + property, value, null);
677
+ });
678
+
679
+ /**
680
+ * Listen for a change event for a certain property
681
+ *
682
+ * @author Jelle De Loecker <jelle@elevenways.be>
683
+ * @since 1.3.10
684
+ * @version 1.3.10
685
+ *
686
+ * @param {String} property
687
+ * @param {Function} callback
688
+ */
689
+ Syncable.setMethod(function watchProperty(property, callback) {
690
+ this.on('property_change_' + property, callback);
691
+ let value = this[property];
692
+ callback(value);
693
+ });
694
+
695
+ /**
696
+ * Watch a queue for changes
697
+ *
698
+ * @author Jelle De Loecker <jelle@elevenways.be>
699
+ * @since 1.3.10
700
+ * @version 1.3.10
701
+ *
702
+ * @param {String} name
703
+ * @param {Function} callback
704
+ */
705
+ Syncable.setMethod(function watchQueue(name, callback) {
706
+
707
+ let queue = this.queues.get(name);
708
+
709
+ if (!queue) {
710
+ queue = {
711
+ name,
712
+ listeners: [],
713
+ messages : [],
714
+ };
715
+
716
+ this.queues.set(name, queue);
717
+ }
718
+
719
+ queue.listeners.push(callback);
720
+
721
+ // Drain all the messages
722
+ while (queue.messages.length) {
723
+ this.scheduleQueueCallback(queue.messages.shift(), callback);
724
+ }
725
+ });
726
+
727
+ /**
728
+ * Clear all the entries in a queue
729
+ *
730
+ * @author Jelle De Loecker <jelle@elevenways.be>
731
+ * @since 1.3.10
732
+ * @version 1.3.10
733
+ *
734
+ * @param {String} name
735
+ */
736
+ Syncable.setAfterMethod('ready', function clearQueue(name) {
737
+
738
+ let queue = this.queues.get(name);
739
+
740
+ if (queue) {
741
+ queue.messages = [];
742
+ }
743
+
744
+ if (Blast.isNode) {
745
+ this.addLog('clear_queue', [name]);
746
+ }
747
+ });
748
+
749
+ /**
750
+ * Schedule a queue callback
751
+ * (This tries to keep events in different queues still use the same order)
752
+ *
753
+ * @author Jelle De Loecker <jelle@elevenways.be>
754
+ * @since 1.3.10
755
+ * @version 1.3.10
756
+ */
757
+ Syncable.setAfterMethod('ready', function scheduleQueueCallback(config, callback) {
758
+
759
+ let args = config.args,
760
+ counter = config.counter;
761
+
762
+ if (!this[QUEUE_CALLBACKS]) {
763
+ this[QUEUE_CALLBACKS] = [];
764
+ }
765
+
766
+ this[QUEUE_CALLBACKS].push({args, counter, callback});
767
+
768
+ if (this[QUEUE_CALLBACK_ID]) {
769
+ clearTimeout(this[QUEUE_CALLBACK_ID]);
770
+ }
771
+
772
+ this[QUEUE_CALLBACK_ID] = setTimeout(() => {
773
+ this[QUEUE_CALLBACK_ID] = null;
774
+ this.processQueueCallbacks();
775
+ }, 10);
776
+ });
777
+
778
+ /**
779
+ * Actually do the queued callbacks
780
+ *
781
+ * @author Jelle De Loecker <jelle@elevenways.be>
782
+ * @since 1.3.10
783
+ * @version 1.3.10
784
+ */
785
+ Syncable.setAfterMethod('ready', function processQueueCallbacks() {
786
+
787
+ let callbacks = this[QUEUE_CALLBACKS];
788
+
789
+ if (!callbacks) {
790
+ return;
791
+ }
792
+
793
+ this[QUEUE_CALLBACKS] = [];
794
+
795
+ callbacks.sort((a, b) => {
796
+ return a.counter - b.counter;
797
+ });
798
+
799
+ for (let item of callbacks) {
800
+ item.callback(...item.args);
801
+ }
802
+ });
803
+
804
+ /**
805
+ * Push something to a queue.
806
+ * If there are listeners, they will be called immediately.
807
+ *
808
+ * @author Jelle De Loecker <jelle@elevenways.be>
809
+ * @since 1.3.10
810
+ * @version 1.3.10
811
+ *
812
+ * @param {String} name
813
+ */
814
+ Syncable.setAfterMethod('ready', function pushQueue(name, ...args) {
815
+
816
+ let queue = this.queues.get(name);
817
+
818
+ if (!queue) {
819
+ queue = {
820
+ name,
821
+ listeners: [],
822
+ messages : [],
823
+ };
824
+
825
+ this.queues.set(name, queue);
826
+ }
827
+
828
+ if (queue.listeners.length) {
829
+ for (let listener of queue.listeners) {
830
+ this.scheduleQueueCallback({args, counter: this.counter++}, listener);
831
+ }
832
+ } else if (!Blast.isNode) {
833
+ queue.messages.push({
834
+ counter : this.counter++,
835
+ args
836
+ });
837
+ }
838
+
839
+ if (Blast.isNode) {
840
+ this.addLog('push_queue', [name, args]);
841
+ }
842
+ });
843
+
844
+ /**
845
+ * Send an update to the given link
846
+ *
847
+ * @author Jelle De Loecker <jelle@elevenways.be>
848
+ * @since 1.3.10
849
+ * @version 1.3.10
850
+ */
851
+ Syncable.setMethod(function sendUpdateToLink(link) {
852
+
853
+ let link_version = link.syncable_version;
854
+
855
+ if (link_version >= this.version) {
856
+ return;
857
+ }
858
+
859
+ let updates = [];
860
+
861
+ for (let i = link_version; i < this.version; i++) {
862
+ let entry = this.log[i];
863
+
864
+ if (entry) {
865
+ updates.push(entry);
866
+ }
867
+ }
868
+
869
+ if (updates.length) {
870
+ link.submit('process_updates', {
871
+ updates,
872
+ version: this.version,
873
+ });
874
+
875
+ link.syncable_version = this.version;
876
+ }
877
+ });
878
+
879
+ /**
880
+ * Queue an update to all the listeners
881
+ *
882
+ * @author Jelle De Loecker <jelle@elevenways.be>
883
+ * @since 1.3.10
884
+ * @version 1.3.10
885
+ */
886
+ Syncable.setMethod(function queueUpdate() {
887
+
888
+ let update_id = this[UPDATE_ID];
889
+
890
+ if (update_id) {
891
+ clearTimeout(update_id);
892
+ }
893
+
894
+ this[UPDATE_ID] = setTimeout(this.sendUpdates.bind(this), 30);
895
+ });
896
+
897
+ /**
898
+ * Add something to the log
899
+ *
900
+ * @author Jelle De Loecker <jelle@elevenways.be>
901
+ * @since 1.3.10
902
+ * @version 1.3.10
903
+ */
904
+ Syncable.setMethod(function _addLog(type, args) {
905
+
906
+ let entry = {
907
+ version : this.version,
908
+ type : type,
909
+ args : args,
910
+ };
911
+
912
+ this.log.push(entry);
913
+ this.queueUpdate();
914
+ });
915
+
916
+ /**
917
+ * Add something to the log and increase the version
918
+ *
919
+ * @author Jelle De Loecker <jelle@elevenways.be>
920
+ * @since 1.3.10
921
+ * @version 1.3.10
922
+ */
923
+ Syncable.setMethod(function addLog(type, args) {
924
+ this.version++;
925
+ this._addLog(type, args);
926
+ });
927
+
928
+ /**
929
+ * Set a property to a specific value
930
+ *
931
+ * @author Jelle De Loecker <jelle@elevenways.be>
932
+ * @since 1.3.10
933
+ * @version 1.3.10
934
+ */
935
+ Syncable.setMethod(function setProperty(key, value) {
936
+ if (this.state[key] !== value) {
937
+ this.state[key] = value;
938
+
939
+ if (this.is_server) {
940
+ this.addLog('set', [key, value]);
941
+ }
942
+
943
+ this.emitPropertyChange(key);
944
+ }
945
+ });
946
+
947
+ /**
948
+ * Release the syncable
949
+ * (On your own side)
950
+ *
951
+ * @author Jelle De Loecker <jelle@elevenways.be>
952
+ * @since 1.3.10
953
+ * @version 1.3.10
954
+ */
955
+ Syncable.setMethod(function release() {
956
+
957
+ if (this.c2s_link) {
958
+ this.c2s_link.destroy();
959
+ this.c2s_link = null;
960
+ }
961
+
962
+ if (Blast.isBrowser) {
963
+ CLIENT_MAP.delete(this.id);
964
+ }
965
+ });