Skip to content

Commit a5d8170

Browse files
Copilothotlong
andcommitted
Add connection state monitoring, auto-reconnect, and batch progress to data-objectstack
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 1d72855 commit a5d8170

3 files changed

Lines changed: 219 additions & 11 deletions

File tree

apps/console/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ The standard runtime UI for ObjectStack applications. This package provides the
44

55
## Features
66

7-
- **Spec-Compliant**: Fully implements ObjectStack Spec v0.8.2
7+
- **Spec-Compliant**: Fully implements ObjectStack Spec v0.9.0
88
- **Dynamic UI**: Renders Dashboards, Grids, and Forms based on JSON schemas
99
- **Multi-App Support**: Switch between different apps defined in your stack
1010
- **Plugin Architecture**: Can be loaded as a static plugin in the ObjectStack Runtime
@@ -27,8 +27,8 @@ This console implements the following ObjectStack Spec features:
2727

2828
### Navigation Support
2929
-`object` - Navigate to object list views
30-
-`dashboard` - Navigate to dashboards (planned)
31-
-`page` - Navigate to custom pages (planned)
30+
-`dashboard` - Navigate to dashboards
31+
-`page` - Navigate to custom pages
3232
-`url` - External URL navigation with target support
3333
-`group` - Nested navigation groups
3434
- ✅ Navigation item visibility conditions

apps/console/src/__tests__/SpecCompliance.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import appConfig from '../../objectstack.config';
55
/**
66
* Spec Compliance Tests
77
*
8-
* These tests verify that the console properly implements the ObjectStack Spec v0.8.2
8+
* These tests verify that the console properly implements the ObjectStack Spec v0.9.0
99
* See: apps/console/SPEC_ALIGNMENT.md for full compliance details
1010
*/
1111

12-
describe('ObjectStack Spec v0.8.2 Compliance', () => {
12+
describe('ObjectStack Spec v0.9.0 Compliance', () => {
1313

1414
describe('AppSchema Validation', () => {
1515
it('should have at least one app defined', () => {

packages/data-objectstack/src/index.ts

Lines changed: 214 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,41 @@ import {
1818
createErrorFromResponse,
1919
} from './errors';
2020

21+
/**
22+
* Connection state for monitoring
23+
*/
24+
export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error';
25+
26+
/**
27+
* Connection state change event
28+
*/
29+
export interface ConnectionStateEvent {
30+
state: ConnectionState;
31+
timestamp: number;
32+
error?: Error;
33+
}
34+
35+
/**
36+
* Batch operation progress event
37+
*/
38+
export interface BatchProgressEvent {
39+
operation: 'create' | 'update' | 'delete';
40+
total: number;
41+
completed: number;
42+
failed: number;
43+
percentage: number;
44+
}
45+
46+
/**
47+
* Event listener type for connection state changes
48+
*/
49+
export type ConnectionStateListener = (event: ConnectionStateEvent) => void;
50+
51+
/**
52+
* Event listener type for batch operation progress
53+
*/
54+
export type BatchProgressListener = (event: BatchProgressEvent) => void;
55+
2156
/**
2257
* ObjectStack Data Source Adapter
2358
*
@@ -31,7 +66,14 @@ import {
3166
*
3267
* const dataSource = new ObjectStackAdapter({
3368
* baseUrl: 'https://api.example.com',
34-
* token: 'your-api-token'
69+
* token: 'your-api-token',
70+
* autoReconnect: true,
71+
* maxReconnectAttempts: 5
72+
* });
73+
*
74+
* // Monitor connection state
75+
* dataSource.onConnectionStateChange((event) => {
76+
* console.log('Connection state:', event.state);
3577
* });
3678
*
3779
* const users = await dataSource.find('users', {
@@ -44,6 +86,13 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
4486
private client: ObjectStackClient;
4587
private connected: boolean = false;
4688
private metadataCache: MetadataCache;
89+
private connectionState: ConnectionState = 'disconnected';
90+
private connectionStateListeners: ConnectionStateListener[] = [];
91+
private batchProgressListeners: BatchProgressListener[] = [];
92+
private autoReconnect: boolean;
93+
private maxReconnectAttempts: number;
94+
private reconnectDelay: number;
95+
private reconnectAttempts: number = 0;
4796

4897
constructor(config: {
4998
baseUrl: string;
@@ -53,9 +102,15 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
53102
maxSize?: number;
54103
ttl?: number;
55104
};
105+
autoReconnect?: boolean;
106+
maxReconnectAttempts?: number;
107+
reconnectDelay?: number;
56108
}) {
57109
this.client = new ObjectStackClient(config);
58110
this.metadataCache = new MetadataCache(config.cache);
111+
this.autoReconnect = config.autoReconnect ?? true;
112+
this.maxReconnectAttempts = config.maxReconnectAttempts ?? 3;
113+
this.reconnectDelay = config.reconnectDelay ?? 1000;
59114
}
60115

61116
/**
@@ -64,20 +119,127 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
64119
*/
65120
async connect(): Promise<void> {
66121
if (!this.connected) {
122+
this.setConnectionState('connecting');
123+
67124
try {
68125
await this.client.connect();
69126
this.connected = true;
127+
this.reconnectAttempts = 0;
128+
this.setConnectionState('connected');
70129
} catch (error: unknown) {
71130
const errorMessage = error instanceof Error ? error.message : 'Failed to connect to ObjectStack server';
72-
throw new ConnectionError(
131+
const connectionError = new ConnectionError(
73132
errorMessage,
74133
undefined,
75134
{ originalError: error }
76135
);
136+
137+
this.setConnectionState('error', connectionError);
138+
139+
// Attempt auto-reconnect if enabled
140+
if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
141+
await this.attemptReconnect();
142+
} else {
143+
throw connectionError;
144+
}
77145
}
78146
}
79147
}
80148

149+
/**
150+
* Attempt to reconnect to the server with exponential backoff
151+
*/
152+
private async attemptReconnect(): Promise<void> {
153+
this.reconnectAttempts++;
154+
this.setConnectionState('reconnecting');
155+
156+
// Exponential backoff: delay * 2^(attempts-1)
157+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
158+
159+
await new Promise(resolve => setTimeout(resolve, delay));
160+
161+
this.connected = false;
162+
await this.connect();
163+
}
164+
165+
/**
166+
* Get the current connection state
167+
*/
168+
getConnectionState(): ConnectionState {
169+
return this.connectionState;
170+
}
171+
172+
/**
173+
* Check if the adapter is currently connected
174+
*/
175+
isConnected(): boolean {
176+
return this.connected && this.connectionState === 'connected';
177+
}
178+
179+
/**
180+
* Register a listener for connection state changes
181+
*/
182+
onConnectionStateChange(listener: ConnectionStateListener): () => void {
183+
this.connectionStateListeners.push(listener);
184+
185+
// Return unsubscribe function
186+
return () => {
187+
const index = this.connectionStateListeners.indexOf(listener);
188+
if (index > -1) {
189+
this.connectionStateListeners.splice(index, 1);
190+
}
191+
};
192+
}
193+
194+
/**
195+
* Register a listener for batch operation progress
196+
*/
197+
onBatchProgress(listener: BatchProgressListener): () => void {
198+
this.batchProgressListeners.push(listener);
199+
200+
// Return unsubscribe function
201+
return () => {
202+
const index = this.batchProgressListeners.indexOf(listener);
203+
if (index > -1) {
204+
this.batchProgressListeners.splice(index, 1);
205+
}
206+
};
207+
}
208+
209+
/**
210+
* Set connection state and notify listeners
211+
*/
212+
private setConnectionState(state: ConnectionState, error?: Error): void {
213+
this.connectionState = state;
214+
215+
const event: ConnectionStateEvent = {
216+
state,
217+
timestamp: Date.now(),
218+
error,
219+
};
220+
221+
this.connectionStateListeners.forEach(listener => {
222+
try {
223+
listener(event);
224+
} catch (err) {
225+
console.error('Error in connection state listener:', err);
226+
}
227+
});
228+
}
229+
230+
/**
231+
* Emit batch progress event to listeners
232+
*/
233+
private emitBatchProgress(event: BatchProgressEvent): void {
234+
this.batchProgressListeners.forEach(listener => {
235+
try {
236+
listener(event);
237+
} catch (err) {
238+
console.error('Error in batch progress listener:', err);
239+
}
240+
});
241+
}
242+
81243
/**
82244
* Find multiple records with query parameters.
83245
* Converts OData-style params to ObjectStack query options.
@@ -155,6 +317,7 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
155317

156318
/**
157319
* Bulk operations with optimized batch processing and error handling.
320+
* Emits progress events for tracking operation status.
158321
*
159322
* @param resource - Resource name
160323
* @param operation - Operation type (create, update, delete)
@@ -168,10 +331,29 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
168331
return [];
169332
}
170333

334+
const total = data.length;
335+
let completed = 0;
336+
let failed = 0;
337+
338+
const emitProgress = () => {
339+
this.emitBatchProgress({
340+
operation,
341+
total,
342+
completed,
343+
failed,
344+
percentage: total > 0 ? (completed + failed) / total * 100 : 0,
345+
});
346+
};
347+
171348
try {
172349
switch (operation) {
173350
case 'create':
174-
return await this.client.data.createMany<T>(resource, data);
351+
emitProgress();
352+
const created = await this.client.data.createMany<T>(resource, data);
353+
completed = created.length;
354+
failed = total - completed;
355+
emitProgress();
356+
return created;
175357

176358
case 'delete': {
177359
const ids = data.map(item => (item as Record<string, unknown>).id).filter(Boolean) as string[];
@@ -183,10 +365,17 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
183365
error: `Missing ID for item at index ${index}`
184366
}));
185367

368+
failed = data.length;
369+
emitProgress();
370+
186371
throw new BulkOperationError('delete', 0, data.length, errors);
187372
}
188373

374+
emitProgress();
189375
await this.client.data.deleteMany(resource, ids);
376+
completed = ids.length;
377+
failed = total - completed;
378+
emitProgress();
190379
return [] as T[];
191380
}
192381

@@ -195,16 +384,21 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
195384
// eslint-disable-next-line @typescript-eslint/no-explicit-any
196385
if (typeof (this.client.data as any).updateMany === 'function') {
197386
try {
387+
emitProgress();
198388
// eslint-disable-next-line @typescript-eslint/no-explicit-any
199389
const updateMany = (this.client.data as any).updateMany;
200-
return await updateMany(resource, data) as T[];
390+
const updated = await updateMany(resource, data) as T[];
391+
completed = updated.length;
392+
failed = total - completed;
393+
emitProgress();
394+
return updated;
201395
} catch {
202396
// If updateMany is not supported, fall back to individual updates
203397
// Silently fallback without logging
204398
}
205399
}
206400

207-
// Fallback: Process updates individually with detailed error tracking
401+
// Fallback: Process updates individually with detailed error tracking and progress
208402
const results: T[] = [];
209403
const errors: Array<{ index: number; error: unknown }> = [];
210404

@@ -214,15 +408,21 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
214408

215409
if (!id) {
216410
errors.push({ index: i, error: 'Missing ID' });
411+
failed++;
412+
emitProgress();
217413
continue;
218414
}
219415

220416
try {
221417
const result = await this.client.data.update<T>(resource, String(id), item);
222418
results.push(result);
419+
completed++;
420+
emitProgress();
223421
} catch (error: unknown) {
224422
const errorMessage = error instanceof Error ? error.message : String(error);
225423
errors.push({ index: i, error: errorMessage });
424+
failed++;
425+
emitProgress();
226426
}
227427
}
228428

@@ -248,6 +448,9 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
248448
);
249449
}
250450
} catch (error: unknown) {
451+
// Emit final progress with failure
452+
emitProgress();
453+
251454
// If it's already a BulkOperationError, re-throw it
252455
if (error instanceof BulkOperationError) {
253456
throw error;
@@ -393,7 +596,9 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
393596
* const dataSource = createObjectStackAdapter({
394597
* baseUrl: process.env.API_URL,
395598
* token: process.env.API_TOKEN,
396-
* cache: { maxSize: 100, ttl: 300000 }
599+
* cache: { maxSize: 100, ttl: 300000 },
600+
* autoReconnect: true,
601+
* maxReconnectAttempts: 5
397602
* });
398603
* ```
399604
*/
@@ -405,6 +610,9 @@ export function createObjectStackAdapter<T = unknown>(config: {
405610
maxSize?: number;
406611
ttl?: number;
407612
};
613+
autoReconnect?: boolean;
614+
maxReconnectAttempts?: number;
615+
reconnectDelay?: number;
408616
}): DataSource<T> {
409617
return new ObjectStackAdapter<T>(config);
410618
}

0 commit comments

Comments
 (0)