roboto-js 1.6.18 → 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/.cursorignore +2 -0
- package/dist/cjs/index.cjs +13 -1
- package/dist/cjs/rbt_api.cjs +891 -328
- package/dist/cjs/rbt_object.cjs +166 -29
- package/dist/esm/index.js +8 -0
- package/dist/esm/rbt_api.js +418 -44
- package/dist/esm/rbt_object.js +133 -17
- package/package.json +3 -2
- package/src/index.js +9 -0
- package/src/rbt_api.js +391 -38
- package/src/rbt_object.js +117 -19
package/src/rbt_api.js
CHANGED
|
@@ -12,6 +12,15 @@ export default class RbtApi {
|
|
|
12
12
|
|
|
13
13
|
this.websocketClient = null;
|
|
14
14
|
|
|
15
|
+
// Object cache for sharing instances across multiple load() calls
|
|
16
|
+
this._objectCache = new Map();
|
|
17
|
+
|
|
18
|
+
// Track pending requests to prevent duplicate loads
|
|
19
|
+
this._pendingLoads = new Map();
|
|
20
|
+
|
|
21
|
+
// Track what we've already logged to reduce console spam
|
|
22
|
+
this._loggedCacheEvents = new Set();
|
|
23
|
+
|
|
15
24
|
this.axios = axios.create({
|
|
16
25
|
baseURL: baseUrl,
|
|
17
26
|
headers: {
|
|
@@ -33,10 +42,33 @@ export default class RbtApi {
|
|
|
33
42
|
removeItem: (key) => Promise.resolve(localStorage.removeItem(key))
|
|
34
43
|
};
|
|
35
44
|
}
|
|
45
|
+
|
|
46
|
+
// Synchronous browser hydration: set auth header and in-memory user immediately
|
|
47
|
+
if (typeof localStorage !== 'undefined') {
|
|
48
|
+
try {
|
|
49
|
+
const token = localStorage.getItem('authtoken');
|
|
50
|
+
if (token) {
|
|
51
|
+
this.authtoken = token;
|
|
52
|
+
this.axios.defaults.headers.common['authtoken'] = token;
|
|
53
|
+
}
|
|
54
|
+
} catch {}
|
|
55
|
+
try {
|
|
56
|
+
const cachedUser = localStorage.getItem('rbtUser');
|
|
57
|
+
if (cachedUser) {
|
|
58
|
+
const parsed = JSON.parse(cachedUser);
|
|
59
|
+
if (parsed && parsed.id) {
|
|
60
|
+
this.currentUser = new RbtUser({ id: parsed.id }, this.axios);
|
|
61
|
+
this.currentUser.setData(parsed);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
36
66
|
this.localDb = null;
|
|
37
67
|
this.iac_session = null;
|
|
38
68
|
this.appServiceHost = baseUrl;
|
|
39
69
|
this.requestCache = {};
|
|
70
|
+
this._loadCurrentUserPromise = null;
|
|
71
|
+
this._loadCurrentUserExtendedPromise = null;
|
|
40
72
|
|
|
41
73
|
// Use the storageAdaptor to get the authToken, if available
|
|
42
74
|
this.initAuthToken(authtoken);
|
|
@@ -46,26 +78,93 @@ export default class RbtApi {
|
|
|
46
78
|
}
|
|
47
79
|
|
|
48
80
|
getWebSocketClient() {
|
|
49
|
-
if (
|
|
81
|
+
// Reuse existing WebSocket if it's OPEN or CONNECTING (to prevent race condition)
|
|
82
|
+
if (this.websocketClient &&
|
|
83
|
+
(this.websocketClient.readyState === WebSocket.OPEN ||
|
|
84
|
+
this.websocketClient.readyState === WebSocket.CONNECTING)) {
|
|
85
|
+
return this.websocketClient;
|
|
86
|
+
}
|
|
50
87
|
|
|
51
88
|
const baseUrl = this.axios.defaults.baseURL;
|
|
52
89
|
const wsProtocol = baseUrl.startsWith('https') ? 'wss://' : 'ws://';
|
|
53
90
|
const wsUrl = baseUrl.replace(/^https?:\/\//, wsProtocol);
|
|
91
|
+
|
|
92
|
+
console.log('[RbtApi] Creating new WebSocket connection to:', wsUrl + '/realtime');
|
|
54
93
|
this.websocketClient = new WebSocket(`${wsUrl}/realtime`);
|
|
94
|
+
this._setupWebSocketHandlers(this.websocketClient);
|
|
95
|
+
|
|
96
|
+
return this.websocketClient;
|
|
97
|
+
}
|
|
55
98
|
|
|
56
|
-
|
|
99
|
+
_setupWebSocketHandlers(ws) {
|
|
100
|
+
ws.onopen = () => {
|
|
57
101
|
console.log('[RbtApi] WebSocket connected.');
|
|
102
|
+
this._wsReconnectAttempts = 0;
|
|
103
|
+
this._wsConnected = true;
|
|
104
|
+
|
|
105
|
+
// Re-subscribe to all objects that were previously subscribed
|
|
106
|
+
if (this._wsSubscriptions) {
|
|
107
|
+
for (const objectId of this._wsSubscriptions) {
|
|
108
|
+
ws.send(JSON.stringify({ type: 'subscribe', objectId }));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
58
111
|
};
|
|
59
112
|
|
|
60
|
-
|
|
61
|
-
console.warn('[RbtApi] WebSocket closed.
|
|
113
|
+
ws.onclose = (event) => {
|
|
114
|
+
console.warn('[RbtApi] WebSocket closed:', event.code, event.reason);
|
|
115
|
+
this._wsConnected = false;
|
|
116
|
+
|
|
117
|
+
// Attempt reconnection with exponential backoff
|
|
118
|
+
if (!this._wsManualClose && this._wsReconnectAttempts < 5) {
|
|
119
|
+
const delay = Math.min(1000 * Math.pow(2, this._wsReconnectAttempts), 30000);
|
|
120
|
+
console.log(`[RbtApi] Attempting reconnection in ${delay}ms (attempt ${this._wsReconnectAttempts + 1}/5)`);
|
|
121
|
+
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
this._wsReconnectAttempts++;
|
|
124
|
+
this.websocketClient = null; // Clear the old connection
|
|
125
|
+
this.getWebSocketClient(); // Create new connection
|
|
126
|
+
}, delay);
|
|
127
|
+
}
|
|
62
128
|
};
|
|
63
129
|
|
|
64
|
-
|
|
65
|
-
console.error('[RbtApi] WebSocket error:', err
|
|
130
|
+
ws.onerror = (err) => {
|
|
131
|
+
console.error('[RbtApi] WebSocket error:', err);
|
|
132
|
+
this._wsConnected = false;
|
|
66
133
|
};
|
|
67
134
|
|
|
68
|
-
|
|
135
|
+
// Handle ping/pong for keep-alive
|
|
136
|
+
ws.addEventListener('ping', () => {
|
|
137
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
138
|
+
ws.pong();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Initialize connection tracking
|
|
143
|
+
this._wsReconnectAttempts = this._wsReconnectAttempts || 0;
|
|
144
|
+
this._wsConnected = false;
|
|
145
|
+
this._wsManualClose = false;
|
|
146
|
+
this._wsSubscriptions = this._wsSubscriptions || new Set();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Method to track subscriptions for reconnection
|
|
150
|
+
_trackSubscription(objectId) {
|
|
151
|
+
if (!this._wsSubscriptions) this._wsSubscriptions = new Set();
|
|
152
|
+
this._wsSubscriptions.add(objectId);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
_untrackSubscription(objectId) {
|
|
156
|
+
if (this._wsSubscriptions) {
|
|
157
|
+
this._wsSubscriptions.delete(objectId);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Method to gracefully close WebSocket
|
|
162
|
+
closeWebSocket() {
|
|
163
|
+
if (this.websocketClient) {
|
|
164
|
+
this._wsManualClose = true;
|
|
165
|
+
this.websocketClient.close();
|
|
166
|
+
this.websocketClient = null;
|
|
167
|
+
}
|
|
69
168
|
}
|
|
70
169
|
|
|
71
170
|
async initAuthToken(authtoken) {
|
|
@@ -175,6 +274,9 @@ export default class RbtApi {
|
|
|
175
274
|
|
|
176
275
|
if(this.localStorageAdaptor){
|
|
177
276
|
await this.localStorageAdaptor.setItem('authtoken', response.data.authToken);
|
|
277
|
+
if (this.iac_session?.user) {
|
|
278
|
+
await this.localStorageAdaptor.setItem('rbtUser', JSON.stringify(this.iac_session.user));
|
|
279
|
+
}
|
|
178
280
|
}
|
|
179
281
|
|
|
180
282
|
return response.data;
|
|
@@ -256,6 +358,9 @@ export default class RbtApi {
|
|
|
256
358
|
async loadCurrentUser(){
|
|
257
359
|
|
|
258
360
|
try {
|
|
361
|
+
if (this._loadCurrentUserPromise) {
|
|
362
|
+
return this._loadCurrentUserPromise;
|
|
363
|
+
}
|
|
259
364
|
|
|
260
365
|
if(this.currentUser){
|
|
261
366
|
return this.currentUser;
|
|
@@ -269,17 +374,24 @@ export default class RbtApi {
|
|
|
269
374
|
}
|
|
270
375
|
else if(this.authtoken){
|
|
271
376
|
|
|
272
|
-
|
|
273
|
-
|
|
377
|
+
this._loadCurrentUserPromise = (async () => {
|
|
378
|
+
let response = await this.refreshAuthToken(this.authtoken);
|
|
379
|
+
if(!response) return null;
|
|
274
380
|
|
|
275
|
-
|
|
381
|
+
if(response?.user){
|
|
276
382
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
383
|
+
this.currentUser = new RbtUser({ id: response?.user?.id }, this.axios);
|
|
384
|
+
this.currentUser.setData(response.user);
|
|
385
|
+
if (this.localStorageAdaptor) {
|
|
386
|
+
await this.localStorageAdaptor.setItem('rbtUser', JSON.stringify(response.user));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
282
389
|
return this.currentUser;
|
|
390
|
+
})();
|
|
391
|
+
|
|
392
|
+
const result = await this._loadCurrentUserPromise;
|
|
393
|
+
this._loadCurrentUserPromise = null;
|
|
394
|
+
return result;
|
|
283
395
|
//this.currentOrg = new RbtObject(response.organization, this.axios);
|
|
284
396
|
//this.currentOrg.type = '<@iac.organization>';
|
|
285
397
|
|
|
@@ -319,12 +431,24 @@ export default class RbtApi {
|
|
|
319
431
|
|
|
320
432
|
async loadCurrentUserExtended(){
|
|
321
433
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
return this.loadUser(currentUser.id);
|
|
434
|
+
if (this._loadCurrentUserExtendedPromise) {
|
|
435
|
+
return this._loadCurrentUserExtendedPromise;
|
|
325
436
|
}
|
|
326
|
-
|
|
327
|
-
|
|
437
|
+
|
|
438
|
+
this._loadCurrentUserExtendedPromise = (async () => {
|
|
439
|
+
let currentUser = await this.loadCurrentUser();
|
|
440
|
+
if(currentUser){
|
|
441
|
+
return this.loadUser(currentUser.id);
|
|
442
|
+
}
|
|
443
|
+
else{
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
})();
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
return await this._loadCurrentUserExtendedPromise;
|
|
450
|
+
} finally {
|
|
451
|
+
this._loadCurrentUserExtendedPromise = null;
|
|
328
452
|
}
|
|
329
453
|
|
|
330
454
|
}
|
|
@@ -334,15 +458,23 @@ export default class RbtApi {
|
|
|
334
458
|
|
|
335
459
|
let params = { id: userId };
|
|
336
460
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
User.setData(userData);
|
|
344
|
-
return User;
|
|
461
|
+
const cacheKey = `loadUser:${userId}`;
|
|
462
|
+
const now = Date.now();
|
|
463
|
+
const existing = this.requestCache[cacheKey];
|
|
464
|
+
if (existing && (now - existing.time) < 10000) { // 10s TTL
|
|
465
|
+
return existing.val;
|
|
466
|
+
}
|
|
345
467
|
|
|
468
|
+
try {
|
|
469
|
+
const p = (async () => {
|
|
470
|
+
const response = await this.axios.post('/user_service/loadUser', [params]);
|
|
471
|
+
let userData = response?.data?.user;
|
|
472
|
+
let User = new RbtUser({ id: userData.id }, this.axios);
|
|
473
|
+
User.setData(userData);
|
|
474
|
+
return User;
|
|
475
|
+
})();
|
|
476
|
+
this.requestCache[cacheKey] = { val: p, time: now };
|
|
477
|
+
return await p;
|
|
346
478
|
} catch (e) {
|
|
347
479
|
return this._handleError(e);
|
|
348
480
|
}
|
|
@@ -494,9 +626,10 @@ export default class RbtApi {
|
|
|
494
626
|
*
|
|
495
627
|
* @param {string} type - The type of object to create.
|
|
496
628
|
* @param {Object} dataHash - The data for the new object.
|
|
629
|
+
* @param {Object} options - Additional options including enableRealtime.
|
|
497
630
|
* @returns {Promise<RbtObject>} - The newly created object as an RbtObject.
|
|
498
631
|
*/
|
|
499
|
-
async create(type, dataHash={}) {
|
|
632
|
+
async create(type, dataHash={}, options={}) {
|
|
500
633
|
try {
|
|
501
634
|
const response = await this.axios.post('/object_service/createObject', [type, dataHash]);
|
|
502
635
|
const record = response.data;
|
|
@@ -504,7 +637,11 @@ export default class RbtApi {
|
|
|
504
637
|
if(dataHash){
|
|
505
638
|
record.data = dataHash;
|
|
506
639
|
}
|
|
507
|
-
return new RbtObject(record, this.axios, {
|
|
640
|
+
return new RbtObject(record, this.axios, {
|
|
641
|
+
isNew: true,
|
|
642
|
+
websocketClient: this.websocketClient,
|
|
643
|
+
enableRealtime: options.enableRealtime || false
|
|
644
|
+
});
|
|
508
645
|
} catch (e) {
|
|
509
646
|
return this._handleError(e);
|
|
510
647
|
}
|
|
@@ -566,7 +703,7 @@ export default class RbtApi {
|
|
|
566
703
|
const responsePromise = this.axios.post('/object_service/queryObjects', [mergedParams]);
|
|
567
704
|
|
|
568
705
|
// Cache the promise of processing data, not just the raw response
|
|
569
|
-
const processingPromise = responsePromise.then(response => this._processResponseData(response)).catch(e => {
|
|
706
|
+
const processingPromise = responsePromise.then(response => this._processResponseData(response, params)).catch(e => {
|
|
570
707
|
delete this.requestCache[paramsKey]; // Ensure cache cleanup on failure
|
|
571
708
|
//console.log('RBTAPI.query ERROR (Processing)', paramsKey, e);
|
|
572
709
|
return this._handleError(e);
|
|
@@ -585,15 +722,32 @@ export default class RbtApi {
|
|
|
585
722
|
}
|
|
586
723
|
}
|
|
587
724
|
|
|
588
|
-
_processResponseData(response) {
|
|
725
|
+
_processResponseData(response, options = {}) {
|
|
589
726
|
if (response.data.ok === false) {
|
|
590
727
|
return this._handleError(response);
|
|
591
728
|
}
|
|
592
729
|
|
|
593
730
|
if (Array.isArray(response.data.items)) {
|
|
594
731
|
//console.log('RBTAPI.query RESPONSE PRE', response.data.items);
|
|
732
|
+
|
|
733
|
+
// Ensure WebSocket client is available if realtime is requested
|
|
734
|
+
let websocketClient = this.websocketClient;
|
|
735
|
+
if (options.enableRealtime && !websocketClient) {
|
|
736
|
+
console.log('[AgentProviderSync] Creating WebSocket client for realtime objects');
|
|
737
|
+
websocketClient = this.getWebSocketClient();
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
console.log('[AgentProviderSync] _processResponseData creating objects with:', {
|
|
741
|
+
enableRealtime: options.enableRealtime || false,
|
|
742
|
+
hasWebSocketClient: !!websocketClient,
|
|
743
|
+
itemCount: response.data.items.length
|
|
744
|
+
});
|
|
745
|
+
|
|
595
746
|
response.data.items = response.data.items.map(record => {
|
|
596
|
-
return new RbtObject(record, this.axios, {
|
|
747
|
+
return new RbtObject(record, this.axios, {
|
|
748
|
+
websocketClient: websocketClient,
|
|
749
|
+
enableRealtime: options.enableRealtime || false
|
|
750
|
+
});
|
|
597
751
|
});
|
|
598
752
|
}
|
|
599
753
|
|
|
@@ -612,17 +766,149 @@ export default class RbtApi {
|
|
|
612
766
|
*/
|
|
613
767
|
async load(type, ids, params={}){
|
|
614
768
|
|
|
769
|
+
if(type=='<@iac.user>'){
|
|
770
|
+
|
|
771
|
+
debugger;
|
|
772
|
+
}
|
|
773
|
+
|
|
615
774
|
try{
|
|
616
775
|
let mergedParams;
|
|
617
776
|
|
|
618
777
|
if(Array.isArray(ids)){
|
|
619
|
-
|
|
620
|
-
|
|
778
|
+
// For array requests, check cache for each ID and only load missing ones
|
|
779
|
+
const cachedObjects = [];
|
|
780
|
+
const missingIds = [];
|
|
781
|
+
|
|
782
|
+
for (const id of ids) {
|
|
783
|
+
const cacheKey = `${type}:${id}`;
|
|
784
|
+
const cached = this._objectCache.get(cacheKey);
|
|
785
|
+
if (cached) {
|
|
786
|
+
// Only log cache hits once per object to reduce spam
|
|
787
|
+
const hitLogKey = `hit:${cacheKey}`;
|
|
788
|
+
if (!this._loggedCacheEvents.has(hitLogKey)) {
|
|
789
|
+
console.log('[AgentProviderSync] 🎯 roboto.load cache HIT:', { type, id, hasRealtime: !!cached._realtime, requestedRealtime: !!params.enableRealtime });
|
|
790
|
+
this._loggedCacheEvents.add(hitLogKey);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// If realtime is requested but cached object doesn't have it, upgrade it
|
|
794
|
+
if (params.enableRealtime && !cached._realtime) {
|
|
795
|
+
console.log('[AgentProviderSync] 🔄 Upgrading cached object to realtime:', { type, id });
|
|
796
|
+
cached._initRealtime();
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
cachedObjects.push(cached);
|
|
800
|
+
} else {
|
|
801
|
+
missingIds.push(id);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Load missing objects
|
|
806
|
+
let loadedObjects = [];
|
|
807
|
+
if (missingIds.length > 0) {
|
|
808
|
+
// Only log bulk cache miss once
|
|
809
|
+
const bulkMissLogKey = `bulk-miss:${type}:${missingIds.join(',')}`;
|
|
810
|
+
if (!this._loggedCacheEvents.has(bulkMissLogKey)) {
|
|
811
|
+
console.log('[AgentProviderSync] 📦 roboto.load cache MISS, loading:', { type, ids: missingIds });
|
|
812
|
+
this._loggedCacheEvents.add(bulkMissLogKey);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
mergedParams = { ...params, where: `id IN ("${missingIds.join(`","`)}")` };
|
|
816
|
+
loadedObjects = await this.query(type, mergedParams);
|
|
817
|
+
|
|
818
|
+
// Cache the newly loaded objects
|
|
819
|
+
for (const obj of loadedObjects) {
|
|
820
|
+
const cacheKey = `${type}:${obj.id}`;
|
|
821
|
+
this._objectCache.set(cacheKey, obj);
|
|
822
|
+
|
|
823
|
+
// Only log cache set once per object to reduce spam
|
|
824
|
+
const setLogKey = `set:${cacheKey}`;
|
|
825
|
+
if (!this._loggedCacheEvents.has(setLogKey)) {
|
|
826
|
+
console.log('[AgentProviderSync] 💾 roboto.load cached object:', { type, id: obj.id });
|
|
827
|
+
this._loggedCacheEvents.add(setLogKey);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Return combined results in original order
|
|
833
|
+
const result = [];
|
|
834
|
+
for (const id of ids) {
|
|
835
|
+
const cacheKey = `${type}:${id}`;
|
|
836
|
+
const obj = this._objectCache.get(cacheKey);
|
|
837
|
+
if (obj) {
|
|
838
|
+
// Ensure realtime is enabled if requested
|
|
839
|
+
if (params.enableRealtime && !obj._realtime) {
|
|
840
|
+
obj._initRealtime();
|
|
841
|
+
}
|
|
842
|
+
result.push(obj);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return result;
|
|
621
846
|
}
|
|
622
847
|
else{
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
848
|
+
// For single object requests, check cache first
|
|
849
|
+
const cacheKey = `${type}:${ids}`;
|
|
850
|
+
const cached = this._objectCache.get(cacheKey);
|
|
851
|
+
|
|
852
|
+
if (cached) {
|
|
853
|
+
// Only log cache hits once per object to reduce spam
|
|
854
|
+
const hitLogKey = `hit:${cacheKey}`;
|
|
855
|
+
if (!this._loggedCacheEvents.has(hitLogKey) || console.log('[AgentProviderSync] 🎯 roboto.load cache HIT:', { type, id: ids, hasRealtime: !!cached._realtime, requestedRealtime: !!params.enableRealtime })) {
|
|
856
|
+
this._loggedCacheEvents.add(hitLogKey);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// If realtime is requested but cached object doesn't have it, upgrade it
|
|
860
|
+
if (params.enableRealtime && !cached._realtime) {
|
|
861
|
+
console.log('[AgentProviderSync] 🔄 Upgrading cached object to realtime:', { type, id: ids });
|
|
862
|
+
cached._initRealtime();
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return cached;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Check if we're already loading this object
|
|
869
|
+
const pendingKey = `${type}:${ids}`;
|
|
870
|
+
if (this._pendingLoads.has(pendingKey)) {
|
|
871
|
+
// Wait for the existing request to complete
|
|
872
|
+
return await this._pendingLoads.get(pendingKey);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Only log cache miss once per object to reduce spam
|
|
876
|
+
const missLogKey = `miss:${cacheKey}`;
|
|
877
|
+
if (!this._loggedCacheEvents.has(missLogKey)) {
|
|
878
|
+
console.log('[AgentProviderSync] 📦 roboto.load cache MISS, loading:', { type, id: ids });
|
|
879
|
+
this._loggedCacheEvents.add(missLogKey);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Create the loading promise and store it to prevent duplicate requests
|
|
883
|
+
const loadPromise = (async () => {
|
|
884
|
+
try {
|
|
885
|
+
mergedParams = { ...params, where: `id="${ids}"` };
|
|
886
|
+
let res = await this.query(type, mergedParams);
|
|
887
|
+
const obj = res[0];
|
|
888
|
+
|
|
889
|
+
if (obj) {
|
|
890
|
+
// Cache the loaded object
|
|
891
|
+
this._objectCache.set(cacheKey, obj);
|
|
892
|
+
|
|
893
|
+
// Only log cache set once per object to reduce spam
|
|
894
|
+
const setLogKey = `set:${cacheKey}`;
|
|
895
|
+
if (!this._loggedCacheEvents.has(setLogKey)) {
|
|
896
|
+
console.log('[AgentProviderSync] 💾 roboto.load cached object:', { type, id: ids });
|
|
897
|
+
this._loggedCacheEvents.add(setLogKey);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return obj;
|
|
902
|
+
} finally {
|
|
903
|
+
// Remove from pending loads
|
|
904
|
+
this._pendingLoads.delete(pendingKey);
|
|
905
|
+
}
|
|
906
|
+
})();
|
|
907
|
+
|
|
908
|
+
// Store the promise so other concurrent requests can await it
|
|
909
|
+
this._pendingLoads.set(pendingKey, loadPromise);
|
|
910
|
+
|
|
911
|
+
return await loadPromise;
|
|
626
912
|
}
|
|
627
913
|
|
|
628
914
|
} catch (e) {
|
|
@@ -631,6 +917,73 @@ export default class RbtApi {
|
|
|
631
917
|
|
|
632
918
|
}
|
|
633
919
|
|
|
920
|
+
/**
|
|
921
|
+
* Clears the object cache. Useful for cache invalidation.
|
|
922
|
+
* @param {string} type - Optional object type to clear. If not provided, clears all.
|
|
923
|
+
* @param {string} id - Optional object ID to clear. If not provided with type, clears all objects of that type.
|
|
924
|
+
*/
|
|
925
|
+
clearCache(type = null, id = null) {
|
|
926
|
+
if (type && id) {
|
|
927
|
+
// Clear specific object
|
|
928
|
+
const cacheKey = `${type}:${id}`;
|
|
929
|
+
const removed = this._objectCache.delete(cacheKey);
|
|
930
|
+
|
|
931
|
+
// Clear related log tracking
|
|
932
|
+
this._loggedCacheEvents.delete(`hit:${cacheKey}`);
|
|
933
|
+
this._loggedCacheEvents.delete(`miss:${cacheKey}`);
|
|
934
|
+
this._loggedCacheEvents.delete(`set:${cacheKey}`);
|
|
935
|
+
|
|
936
|
+
console.log('[AgentProviderSync] 🗑️ roboto.clearCache specific object:', { type, id, removed });
|
|
937
|
+
} else if (type) {
|
|
938
|
+
// Clear all objects of a specific type
|
|
939
|
+
let removedCount = 0;
|
|
940
|
+
for (const [key] of this._objectCache) {
|
|
941
|
+
if (key.startsWith(`${type}:`)) {
|
|
942
|
+
this._objectCache.delete(key);
|
|
943
|
+
removedCount++;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Clear related log tracking for this type
|
|
948
|
+
for (const logKey of this._loggedCacheEvents) {
|
|
949
|
+
if (logKey.includes(`${type}:`)) {
|
|
950
|
+
this._loggedCacheEvents.delete(logKey);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
console.log('[AgentProviderSync] 🗑️ roboto.clearCache by type:', { type, removedCount });
|
|
955
|
+
} else {
|
|
956
|
+
// Clear all cached objects
|
|
957
|
+
const size = this._objectCache.size;
|
|
958
|
+
this._objectCache.clear();
|
|
959
|
+
this._loggedCacheEvents.clear();
|
|
960
|
+
this._pendingLoads.clear();
|
|
961
|
+
console.log('[AgentProviderSync] 🗑️ roboto.clearCache all objects:', { clearedCount: size });
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Gets cache status for debugging
|
|
967
|
+
* @returns {Object} Cache status information
|
|
968
|
+
*/
|
|
969
|
+
getCacheStatus() {
|
|
970
|
+
const cacheEntries = Array.from(this._objectCache.entries()).map(([key, obj]) => ({
|
|
971
|
+
key,
|
|
972
|
+
id: obj.id,
|
|
973
|
+
type: obj._internalData?.type || 'unknown'
|
|
974
|
+
}));
|
|
975
|
+
|
|
976
|
+
const pendingLoads = Array.from(this._pendingLoads.keys());
|
|
977
|
+
|
|
978
|
+
return {
|
|
979
|
+
cacheSize: this._objectCache.size,
|
|
980
|
+
pendingLoads: pendingLoads.length,
|
|
981
|
+
loggedEvents: this._loggedCacheEvents.size,
|
|
982
|
+
entries: cacheEntries,
|
|
983
|
+
pendingKeys: pendingLoads
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
634
987
|
/**
|
|
635
988
|
* Makes a POST request to a specific endpoint to run a task and handle progress updates.
|
|
636
989
|
*
|