roboto-js 1.6.18 → 1.7.1

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.
@@ -15,15 +15,27 @@ export default class RbtObject {
15
15
  } else {
16
16
  this._data = record.dataJson ? this._deepUnpackJson(record.dataJson) : {};
17
17
  }
18
- if (options.websocketClient && this.id) {
18
+ if (options.websocketClient && this.id && options.enableRealtime) {
19
19
  this._realtime = true;
20
20
  this._ws = options.websocketClient;
21
21
  this._subscribeToRealtime(this._ws);
22
+ } else if (options.websocketClient && this.id) {
23
+ // Store websocket client for potential later use, but don't auto-subscribe
24
+ this._ws = options.websocketClient;
25
+ }
26
+
27
+ // If enableRealtime is requested but no websocketClient provided, try to initialize lazily
28
+ if (options.enableRealtime && this.id && !this._realtime) {
29
+ this._initRealtime();
22
30
  }
23
31
  this._eventHandlers = {
24
32
  change: [],
25
33
  save: []
26
34
  };
35
+
36
+ // Debounce properties for WebSocket broadcasting
37
+ this._broadcastDebounceTimer = null;
38
+ this._pendingBroadcasts = new Map(); // path -> value
27
39
  }
28
40
  get(path) {
29
41
  return _.get(this._data, path);
@@ -80,16 +92,34 @@ export default class RbtObject {
80
92
  if (!_.isEqual(currentValue, mergedValue)) {
81
93
  _.set(this._data, path, mergedValue); // Set the merged value at the deep path
82
94
  this._addChange(path);
95
+ // Trigger local change events for any registered handlers
96
+ this._trigger('change', {
97
+ path,
98
+ value: mergedValue,
99
+ options
100
+ });
83
101
  }
84
102
  } else {
85
103
  // If value is undefined and not merging, delete the key
86
104
  if (value === undefined) {
87
105
  _.unset(this._data, path);
88
106
  this._addChange(path);
107
+ // Trigger local change events for any registered handlers
108
+ this._trigger('change', {
109
+ path,
110
+ value: undefined,
111
+ options
112
+ });
89
113
  } else if (!_.isEqual(currentValue, value)) {
90
114
  _.set(this._data, path, value); // Set the value directly at the deep path
91
115
  this._addChange(path);
92
116
  this._broadcastChange(path, value);
117
+ // Trigger local change events for any registered handlers
118
+ this._trigger('change', {
119
+ path,
120
+ value,
121
+ options
122
+ });
93
123
  }
94
124
  }
95
125
  }
@@ -116,6 +146,12 @@ export default class RbtObject {
116
146
  if (!_.isEqual(currentValue, mergedValue)) {
117
147
  _.set(this._data, path, mergedValue);
118
148
  this._addChange(path);
149
+ // Trigger change event for this path
150
+ this._trigger('change', {
151
+ path,
152
+ value: mergedValue,
153
+ options
154
+ });
119
155
  }
120
156
  } else if (!_.isEqual(currentValue, newValue)) {
121
157
  _.setWith(this._data, path, newValue, (nsValue, key, nsObject, nsPath) => {
@@ -125,6 +161,12 @@ export default class RbtObject {
125
161
  return nsValue;
126
162
  });
127
163
  this._addChange(path);
164
+ // Trigger change event for this path
165
+ this._trigger('change', {
166
+ path,
167
+ value: newValue,
168
+ options
169
+ });
128
170
  }
129
171
  });
130
172
  }
@@ -301,16 +343,26 @@ export default class RbtObject {
301
343
  _initRealtime() {
302
344
  if (this._realtime || !this._axios) return;
303
345
 
304
- // Lazily pull WebSocket from parent API (injected via axios instance)
305
- const api = this._axios?.__rbtApiInstance;
306
- if (!api || typeof api.getWebSocketClient !== 'function') return;
307
- const ws = api.getWebSocketClient();
346
+ // Use stored websocket client if available
347
+ let ws = this._ws;
348
+
349
+ // Otherwise, lazily pull WebSocket from parent API (injected via axios instance)
350
+ if (!ws) {
351
+ const api = this._axios?.__rbtApiInstance;
352
+ if (!api || typeof api.getWebSocketClient !== 'function') return;
353
+ ws = api.getWebSocketClient();
354
+ }
308
355
  if (!ws || this._realtime) return;
309
356
  this._ws = ws;
310
357
  this._realtime = true;
311
358
  this._subscribeToRealtime(ws);
312
359
  }
313
360
  _subscribeToRealtime(ws) {
361
+ // Track subscription for reconnection
362
+ const api = this._axios?.__rbtApiInstance;
363
+ if (api && typeof api._trackSubscription === 'function') {
364
+ api._trackSubscription(this.id);
365
+ }
314
366
  if (ws.readyState === 1) {
315
367
  ws.send(JSON.stringify({
316
368
  type: 'subscribe',
@@ -325,31 +377,82 @@ export default class RbtObject {
325
377
  });
326
378
  }
327
379
  ws.addEventListener('message', event => {
328
- const msg = JSON.parse(event.data);
329
- if (msg.objectId !== this.id) return;
330
- if (msg.type === 'update') {
331
- _.set(this._data, msg.delta.path, msg.delta.value);
332
- this._trigger('change', msg.delta);
333
- } else if (msg.type === 'save') {
334
- this._trigger('save', msg.revision || {});
380
+ try {
381
+ const msg = JSON.parse(event.data);
382
+ if (msg.objectId !== this.id) return;
383
+ if (msg.type === 'update') {
384
+ if (msg.delta && msg.delta.path !== undefined) {
385
+ _.set(this._data, msg.delta.path, msg.delta.value);
386
+
387
+ // Add metadata to indicate if this is state sync vs live update
388
+ const changeData = {
389
+ ...msg.delta,
390
+ isStateSync: msg.isStateSync || false,
391
+ source: msg.isStateSync ? 'state-sync' : 'realtime',
392
+ userId: msg.userId || msg.delta.userId || null
393
+ };
394
+ this._trigger('change', changeData);
395
+ if (msg.isStateSync) {
396
+ console.log('[RbtObject] Applied state sync:', msg.delta.path, '=', msg.delta.value);
397
+ }
398
+ } else {
399
+ console.warn('[RbtObject] Received update message without valid delta:', msg);
400
+ }
401
+ } else if (msg.type === 'save') {
402
+ this._trigger('save', msg.revision || {});
403
+ }
404
+ } catch (error) {
405
+ console.error('[RbtObject] Error processing WebSocket message:', error, event.data);
335
406
  }
336
407
  });
337
408
  }
338
- onRealtimeChange(cb) {
409
+
410
+ // General change handler for both local and remote changes
411
+ onChange(cb) {
339
412
  this._eventHandlers.change.push(cb);
340
- this._initRealtime(); // lazy connect
413
+ // Auto-initialize realtime if this object has WebSocket capability
414
+ if (this._ws && !this._realtime) {
415
+ this._initRealtime();
416
+ }
341
417
  }
342
- onRealtimeSave(cb) {
418
+
419
+ // General save handler for both local and remote saves
420
+ onSave(cb) {
343
421
  this._eventHandlers.save.push(cb);
344
- this._initRealtime(); // lazy connect
422
+ // Note: Does not initialize realtime connection
345
423
  }
346
424
  _trigger(type, data) {
425
+ console.log('[AgentProviderSync] _trigger called:', type, 'handlers:', this._eventHandlers[type]?.length, 'data:', data);
347
426
  for (const fn of this._eventHandlers[type] || []) {
348
427
  fn(data);
349
428
  }
350
429
  }
351
430
  _broadcastChange(path, value) {
352
- if (this._realtime && this._ws && this._ws.readyState === 1) {
431
+ if (!this._realtime || !this._ws || this._ws.readyState !== 1) {
432
+ return;
433
+ }
434
+
435
+ // Store the pending broadcast
436
+ this._pendingBroadcasts.set(path, value);
437
+
438
+ // Clear existing timer if any
439
+ if (this._broadcastDebounceTimer) {
440
+ clearTimeout(this._broadcastDebounceTimer);
441
+ }
442
+
443
+ // Set new debounced timer
444
+ this._broadcastDebounceTimer = setTimeout(() => {
445
+ this._flushPendingBroadcasts();
446
+ }, 300);
447
+ }
448
+ _flushPendingBroadcasts() {
449
+ if (!this._realtime || !this._ws || this._ws.readyState !== 1) {
450
+ this._pendingBroadcasts.clear();
451
+ return;
452
+ }
453
+
454
+ // Send all pending broadcasts
455
+ for (const [path, value] of this._pendingBroadcasts) {
353
456
  this._ws.send(JSON.stringify({
354
457
  type: 'update',
355
458
  objectId: this.id,
@@ -359,5 +462,18 @@ export default class RbtObject {
359
462
  }
360
463
  }));
361
464
  }
465
+
466
+ // Clear pending broadcasts and timer
467
+ this._pendingBroadcasts.clear();
468
+ this._broadcastDebounceTimer = null;
469
+ }
470
+
471
+ // Cleanup method to clear any pending debounced broadcasts
472
+ _cleanup() {
473
+ if (this._broadcastDebounceTimer) {
474
+ clearTimeout(this._broadcastDebounceTimer);
475
+ this._broadcastDebounceTimer = null;
476
+ }
477
+ this._pendingBroadcasts.clear();
362
478
  }
363
479
  }