roboto-js 1.6.17 → 1.7.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/rbt_object.js CHANGED
@@ -19,15 +19,27 @@ export default class RbtObject{
19
19
  this._data = record.dataJson ? this._deepUnpackJson(record.dataJson) : {};
20
20
  }
21
21
 
22
- if (options.websocketClient && this.id) {
22
+ if (options.websocketClient && this.id && options.enableRealtime) {
23
23
  this._realtime = true;
24
24
  this._ws = options.websocketClient;
25
25
  this._subscribeToRealtime(this._ws);
26
+ } else if (options.websocketClient && this.id) {
27
+ // Store websocket client for potential later use, but don't auto-subscribe
28
+ this._ws = options.websocketClient;
29
+ }
30
+
31
+ // If enableRealtime is requested but no websocketClient provided, try to initialize lazily
32
+ if (options.enableRealtime && this.id && !this._realtime) {
33
+ this._initRealtime();
26
34
  }
27
35
  this._eventHandlers = {
28
36
  change: [],
29
37
  save: []
30
38
  };
39
+
40
+ // Debounce properties for WebSocket broadcasting
41
+ this._broadcastDebounceTimer = null;
42
+ this._pendingBroadcasts = new Map(); // path -> value
31
43
  }
32
44
 
33
45
  get(path) {
@@ -86,16 +98,22 @@ export default class RbtObject{
86
98
  if (!_.isEqual(currentValue, mergedValue)) {
87
99
  _.set(this._data, path, mergedValue); // Set the merged value at the deep path
88
100
  this._addChange(path);
101
+ // Trigger local change events for any registered handlers
102
+ this._trigger('change', { path, value: mergedValue, options });
89
103
  }
90
104
  } else {
91
105
  // If value is undefined and not merging, delete the key
92
106
  if (value === undefined) {
93
107
  _.unset(this._data, path);
94
108
  this._addChange(path);
109
+ // Trigger local change events for any registered handlers
110
+ this._trigger('change', { path, value: undefined, options });
95
111
  } else if (!_.isEqual(currentValue, value)) {
96
112
  _.set(this._data, path, value); // Set the value directly at the deep path
97
113
  this._addChange(path);
98
114
  this._broadcastChange(path, value);
115
+ // Trigger local change events for any registered handlers
116
+ this._trigger('change', { path, value, options });
99
117
  }
100
118
  }
101
119
  }
@@ -125,6 +143,8 @@ export default class RbtObject{
125
143
  if (!_.isEqual(currentValue, mergedValue)) {
126
144
  _.set(this._data, path, mergedValue);
127
145
  this._addChange(path);
146
+ // Trigger change event for this path
147
+ this._trigger('change', { path, value: mergedValue, options });
128
148
  }
129
149
  } else if (!_.isEqual(currentValue, newValue)) {
130
150
  _.setWith(this._data, path, newValue, (nsValue, key, nsObject, nsPath) => {
@@ -134,6 +154,8 @@ export default class RbtObject{
134
154
  return nsValue;
135
155
  });
136
156
  this._addChange(path);
157
+ // Trigger change event for this path
158
+ this._trigger('change', { path, value: newValue, options });
137
159
  }
138
160
  });
139
161
  }
@@ -341,11 +363,16 @@ export default class RbtObject{
341
363
  _initRealtime() {
342
364
  if (this._realtime || !this._axios) return;
343
365
 
344
- // Lazily pull WebSocket from parent API (injected via axios instance)
345
- const api = this._axios?.__rbtApiInstance;
346
- if (!api || typeof api.getWebSocketClient !== 'function') return;
347
-
348
- const ws = api.getWebSocketClient();
366
+ // Use stored websocket client if available
367
+ let ws = this._ws;
368
+
369
+ // Otherwise, lazily pull WebSocket from parent API (injected via axios instance)
370
+ if (!ws) {
371
+ const api = this._axios?.__rbtApiInstance;
372
+ if (!api || typeof api.getWebSocketClient !== 'function') return;
373
+ ws = api.getWebSocketClient();
374
+ }
375
+
349
376
  if (!ws || this._realtime) return;
350
377
 
351
378
  this._ws = ws;
@@ -354,6 +381,12 @@ export default class RbtObject{
354
381
  }
355
382
 
356
383
  _subscribeToRealtime(ws) {
384
+ // Track subscription for reconnection
385
+ const api = this._axios?.__rbtApiInstance;
386
+ if (api && typeof api._trackSubscription === 'function') {
387
+ api._trackSubscription(this.id);
388
+ }
389
+
357
390
  if (ws.readyState === 1) {
358
391
  ws.send(JSON.stringify({ type: 'subscribe', objectId: this.id }));
359
392
  } else {
@@ -363,42 +396,107 @@ export default class RbtObject{
363
396
  }
364
397
 
365
398
  ws.addEventListener('message', (event) => {
366
- const msg = JSON.parse(event.data);
367
- if (msg.objectId !== this.id) return;
368
-
369
- if (msg.type === 'update') {
370
- _.set(this._data, msg.delta.path, msg.delta.value);
371
- this._trigger('change', msg.delta);
372
- } else if (msg.type === 'save') {
373
- this._trigger('save', msg.revision || {});
399
+ try {
400
+ const msg = JSON.parse(event.data);
401
+ if (msg.objectId !== this.id) return;
402
+
403
+ if (msg.type === 'update') {
404
+ if (msg.delta && msg.delta.path !== undefined) {
405
+ _.set(this._data, msg.delta.path, msg.delta.value);
406
+
407
+ // Add metadata to indicate if this is state sync vs live update
408
+ const changeData = {
409
+ ...msg.delta,
410
+ isStateSync: msg.isStateSync || false,
411
+ source: msg.isStateSync ? 'state-sync' : 'realtime',
412
+ userId: msg.userId || msg.delta.userId || null
413
+ };
414
+
415
+ this._trigger('change', changeData);
416
+
417
+ if (msg.isStateSync) {
418
+ console.log('[RbtObject] Applied state sync:', msg.delta.path, '=', msg.delta.value);
419
+ }
420
+ } else {
421
+ console.warn('[RbtObject] Received update message without valid delta:', msg);
422
+ }
423
+ } else if (msg.type === 'save') {
424
+ this._trigger('save', msg.revision || {});
425
+ }
426
+ } catch (error) {
427
+ console.error('[RbtObject] Error processing WebSocket message:', error, event.data);
374
428
  }
375
429
  });
376
430
  }
377
431
 
378
- onRealtimeChange(cb) {
432
+ // General change handler for both local and remote changes
433
+ onChange(cb) {
379
434
  this._eventHandlers.change.push(cb);
380
- this._initRealtime(); // lazy connect
435
+ // Auto-initialize realtime if this object has WebSocket capability
436
+ if (this._ws && !this._realtime) {
437
+ this._initRealtime();
438
+ }
381
439
  }
382
440
 
383
- onRealtimeSave(cb) {
441
+ // General save handler for both local and remote saves
442
+ onSave(cb) {
384
443
  this._eventHandlers.save.push(cb);
385
- this._initRealtime(); // lazy connect
444
+ // Note: Does not initialize realtime connection
386
445
  }
387
446
 
388
447
  _trigger(type, data) {
448
+ console.log('[AgentProviderSync] _trigger called:', type, 'handlers:', this._eventHandlers[type]?.length, 'data:', data)
389
449
  for (const fn of this._eventHandlers[type] || []) {
390
450
  fn(data);
391
451
  }
392
452
  }
393
453
 
394
454
  _broadcastChange(path, value) {
395
- if (this._realtime && this._ws && this._ws.readyState === 1) {
455
+ if (!this._realtime || !this._ws || this._ws.readyState !== 1) {
456
+ return;
457
+ }
458
+
459
+ // Store the pending broadcast
460
+ this._pendingBroadcasts.set(path, value);
461
+
462
+ // Clear existing timer if any
463
+ if (this._broadcastDebounceTimer) {
464
+ clearTimeout(this._broadcastDebounceTimer);
465
+ }
466
+
467
+ // Set new debounced timer
468
+ this._broadcastDebounceTimer = setTimeout(() => {
469
+ this._flushPendingBroadcasts();
470
+ }, 300);
471
+ }
472
+
473
+ _flushPendingBroadcasts() {
474
+ if (!this._realtime || !this._ws || this._ws.readyState !== 1) {
475
+ this._pendingBroadcasts.clear();
476
+ return;
477
+ }
478
+
479
+ // Send all pending broadcasts
480
+ for (const [path, value] of this._pendingBroadcasts) {
396
481
  this._ws.send(JSON.stringify({
397
482
  type: 'update',
398
483
  objectId: this.id,
399
484
  delta: { path, value }
400
485
  }));
401
486
  }
487
+
488
+ // Clear pending broadcasts and timer
489
+ this._pendingBroadcasts.clear();
490
+ this._broadcastDebounceTimer = null;
491
+ }
492
+
493
+ // Cleanup method to clear any pending debounced broadcasts
494
+ _cleanup() {
495
+ if (this._broadcastDebounceTimer) {
496
+ clearTimeout(this._broadcastDebounceTimer);
497
+ this._broadcastDebounceTimer = null;
498
+ }
499
+ this._pendingBroadcasts.clear();
402
500
  }
403
501
 
404
502