@syncular/console 0.0.6-159 → 0.0.6-168

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.
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { useCallback, useEffect, useRef, useState } from 'react';
6
- import type { ConsoleRequestEvent, LiveEvent } from '../lib/types';
6
+ import type { LiveEvent } from '../lib/types';
7
7
  import { useConnection } from './ConnectionContext';
8
8
  import { useInstanceContext } from './useInstanceContext';
9
9
 
@@ -35,11 +35,6 @@ interface UseLiveEventsResult {
35
35
  clearEvents: () => void;
36
36
  }
37
37
 
38
- function isServiceWorkerServerMode(): boolean {
39
- if (typeof window === 'undefined') return false;
40
- return new URLSearchParams(window.location.search).get('swServer') === '1';
41
- }
42
-
43
38
  export function useLiveEvents(
44
39
  options: UseLiveEventsOptions = {}
45
40
  ): UseLiveEventsResult {
@@ -70,12 +65,10 @@ export function useLiveEvents(
70
65
  const reconnectAttemptsRef = useRef(0);
71
66
  const lastActivityAtRef = useRef(0);
72
67
  const lastEventTimestampRef = useRef<string | null>(null);
73
- const lastEventIdRef = useRef<number | null>(null);
74
68
 
75
69
  const clearEvents = useCallback(() => {
76
70
  setEvents([]);
77
71
  lastEventTimestampRef.current = null;
78
- lastEventIdRef.current = null;
79
72
  }, []);
80
73
 
81
74
  useEffect(() => {
@@ -87,113 +80,7 @@ export function useLiveEvents(
87
80
  const normalizedReplayLimit = Number.isFinite(replayLimit)
88
81
  ? Math.max(1, Math.min(500, Math.floor(replayLimit)))
89
82
  : 100;
90
- const usePollingFallback =
91
- isServiceWorkerServerMode() || typeof WebSocket === 'undefined';
92
-
93
- if (usePollingFallback) {
94
- setConnectionState('connecting');
95
- setIsConnected(false);
96
-
97
- let pollTimer: ReturnType<typeof setInterval> | null = null;
98
- let isPolling = false;
99
-
100
- const poll = async () => {
101
- if (isCleanedUp || isPolling) return;
102
- isPolling = true;
103
-
104
- try {
105
- const baseUrl = new URL(config.serverUrl, window.location.origin);
106
- const normalizedPath = baseUrl.pathname.endsWith('/')
107
- ? baseUrl.pathname.slice(0, -1)
108
- : baseUrl.pathname;
109
- baseUrl.pathname = `${normalizedPath}/console/events`;
110
- baseUrl.search = '';
111
- baseUrl.searchParams.set('limit', String(normalizedReplayLimit));
112
- baseUrl.searchParams.set('offset', '0');
113
- if (partitionId) {
114
- baseUrl.searchParams.set('partitionId', partitionId);
115
- }
116
-
117
- const response = await fetch(baseUrl.toString(), {
118
- headers: {
119
- Authorization: `Bearer ${config.token}`,
120
- },
121
- });
122
-
123
- if (!response.ok) {
124
- throw new Error(`Live event polling failed (${response.status})`);
125
- }
126
-
127
- const payload = (await response.json()) as {
128
- items?: ConsoleRequestEvent[];
129
- };
130
- const rows = Array.isArray(payload.items) ? payload.items : [];
131
- const filtered = rows
132
- .filter((row) =>
133
- effectiveInstanceId
134
- ? row.instanceId === effectiveInstanceId
135
- : true
136
- )
137
- .sort((a, b) => a.eventId - b.eventId);
138
-
139
- const previousLastId = lastEventIdRef.current ?? -1;
140
- const newRows = filtered.filter(
141
- (row) => row.eventId > previousLastId
142
- );
143
- if (newRows.length > 0) {
144
- const mapped: LiveEvent[] = newRows
145
- .map((row) => ({
146
- type: row.eventType,
147
- timestamp: row.createdAt,
148
- data: row as unknown as Record<string, unknown>,
149
- }))
150
- .reverse();
151
-
152
- setEvents((prev) => [...mapped, ...prev].slice(0, maxEvents));
153
- const newest = newRows[newRows.length - 1]!;
154
- lastEventIdRef.current = newest.eventId;
155
- lastEventTimestampRef.current = newest.createdAt;
156
- } else if (filtered.length > 0) {
157
- const newest = filtered[filtered.length - 1]!;
158
- lastEventIdRef.current = Math.max(
159
- lastEventIdRef.current ?? -1,
160
- newest.eventId
161
- );
162
- lastEventTimestampRef.current = newest.createdAt;
163
- }
164
-
165
- setError(null);
166
- setIsConnected(true);
167
- setConnectionState('connected');
168
- } catch (err) {
169
- if (!isCleanedUp) {
170
- setIsConnected(false);
171
- setConnectionState('disconnected');
172
- setError(
173
- err instanceof Error
174
- ? err
175
- : new Error('Live event polling failed')
176
- );
177
- }
178
- } finally {
179
- isPolling = false;
180
- }
181
- };
182
-
183
- void poll();
184
- pollTimer = setInterval(() => {
185
- void poll();
186
- }, 2_000);
187
-
188
- return () => {
189
- isCleanedUp = true;
190
- if (pollTimer) {
191
- clearInterval(pollTimer);
192
- }
193
- setIsConnected(false);
194
- setConnectionState('disconnected');
195
- };
196
- }
83
+ if (typeof WebSocket === 'undefined') return;
197
84
 
198
85
  const clearReconnectTimeout = () => {
199
86
  if (!reconnectTimeoutRef.current) return;
@@ -1,4 +1,4 @@
1
- import type { SyncClient } from '@syncular/ui';
1
+ import type { SyncClientNode } from '@syncular/ui';
2
2
  import type { ConsoleClient, SyncStats } from './types';
3
3
 
4
4
  interface TopologyAdapterOptions {
@@ -55,7 +55,7 @@ function inferLagCommitCount(
55
55
  function inferStatus(
56
56
  client: ConsoleClient,
57
57
  lagCommitCount: number
58
- ): SyncClient['status'] {
58
+ ): SyncClientNode['status'] {
59
59
  if (client.activityState === 'stale') {
60
60
  return 'offline';
61
61
  }
@@ -80,7 +80,7 @@ export function adaptConsoleClientsToTopology(
80
80
  clients: ConsoleClient[],
81
81
  stats?: SyncStats,
82
82
  options: TopologyAdapterOptions = {}
83
- ): SyncClient[] {
83
+ ): SyncClientNode[] {
84
84
  const maxNodes = options.maxNodes ?? 10;
85
85
 
86
86
  return clients.slice(0, maxNodes).map((client, index) => {
package/src/lib/types.ts CHANGED
@@ -1,180 +1,132 @@
1
1
  /**
2
- * Console API types - derived from OpenAPI spec
2
+ * Console API types derived from generated OpenAPI operations.
3
3
  */
4
4
 
5
- export type ApiKeyType = 'relay' | 'proxy' | 'admin';
6
-
7
- export interface ConsoleApiKey {
8
- keyId: string;
9
- keyPrefix: string;
10
- name: string;
11
- keyType: ApiKeyType;
12
- scopeKeys: string[];
13
- actorId: string | null;
14
- createdAt: string;
15
- expiresAt: string | null;
16
- lastUsedAt: string | null;
17
- revokedAt: string | null;
18
- }
5
+ import type { operations } from '@syncular/transport-http';
19
6
 
20
- export interface ConsoleApiKeyBulkRevokeResponse {
21
- requestedCount: number;
22
- revokedCount: number;
23
- alreadyRevokedCount: number;
24
- notFoundCount: number;
25
- revokedKeyIds: string[];
26
- alreadyRevokedKeyIds: string[];
27
- notFoundKeyIds: string[];
28
- }
7
+ type OperationName = keyof operations;
29
8
 
30
- export interface ConsoleCommitListItem {
31
- commitSeq: number;
32
- actorId: string;
33
- clientId: string;
34
- clientCommitId: string;
35
- createdAt: string;
36
- changeCount: number;
37
- affectedTables: string[];
38
- instanceId?: string;
39
- federatedCommitId?: string;
40
- localCommitSeq?: number;
9
+ type JsonResponse<
10
+ TOperation extends OperationName,
11
+ TStatus extends keyof operations[TOperation]['responses'],
12
+ > = operations[TOperation]['responses'][TStatus] extends {
13
+ content: { 'application/json': infer TJson };
41
14
  }
15
+ ? TJson
16
+ : never;
42
17
 
43
- export interface ConsoleChange {
44
- changeId: number;
45
- table: string;
46
- rowId: string;
47
- op: 'upsert' | 'delete';
48
- rowJson: unknown | null;
49
- rowVersion: number | null;
50
- scopes: Record<string, unknown>;
51
- }
18
+ type OperationWithResponse<TStatus extends PropertyKey> = {
19
+ [TName in OperationName]: TStatus extends keyof operations[TName]['responses']
20
+ ? TName
21
+ : never;
22
+ }[OperationName];
52
23
 
53
- export interface ConsoleCommitDetail extends ConsoleCommitListItem {
54
- changes: ConsoleChange[];
24
+ type JsonSuccessResponse<TOperation extends OperationWithResponse<200>> =
25
+ JsonResponse<TOperation, 200>;
26
+
27
+ type JsonRequestBody<TOperation extends OperationName> =
28
+ NonNullable<operations[TOperation]['requestBody']> extends {
29
+ content: { 'application/json': infer TJson };
30
+ }
31
+ ? TJson
32
+ : never;
33
+
34
+ type PaginatedItem<TOperation extends OperationWithResponse<200>> =
35
+ JsonSuccessResponse<TOperation> extends { items: Array<infer TItem> }
36
+ ? TItem
37
+ : never;
38
+
39
+ interface GatewayFailure {
40
+ instanceId: string;
41
+ reason: string;
42
+ status?: number;
55
43
  }
56
44
 
57
- export interface ConsoleClient {
58
- clientId: string;
59
- actorId: string;
60
- cursor: number;
61
- lagCommitCount: number;
62
- connectionPath: 'direct' | 'relay';
63
- connectionMode: 'polling' | 'realtime';
64
- realtimeConnectionCount: number;
65
- isRealtimeConnected: boolean;
66
- activityState: 'active' | 'idle' | 'stale';
67
- lastRequestAt: string | null;
68
- lastRequestType: 'push' | 'pull' | null;
69
- lastRequestOutcome: string | null;
70
- effectiveScopes: Record<string, unknown>;
71
- updatedAt: string;
72
- instanceId?: string;
73
- federatedClientId?: string;
45
+ interface GatewayAggregateMetadata {
46
+ partial?: boolean;
47
+ failedInstances?: GatewayFailure[];
74
48
  }
75
49
 
76
- export interface ConsoleHandler {
77
- table: string;
78
- dependsOn?: string[];
79
- snapshotChunkTtlMs?: number;
50
+ interface GatewayCommitFields {
51
+ instanceId?: string;
52
+ federatedCommitId?: string;
53
+ localCommitSeq?: number;
80
54
  }
81
55
 
82
- export interface ConsoleRequestEvent {
83
- eventId: number;
84
- partitionId: string;
85
- requestId: string;
86
- traceId: string | null;
87
- spanId: string | null;
88
- eventType: 'push' | 'pull';
89
- syncPath: 'http-combined' | 'ws-push';
90
- transportPath: 'direct' | 'relay';
91
- actorId: string;
92
- clientId: string;
93
- statusCode: number;
94
- outcome: string;
95
- responseStatus: string;
96
- errorCode: string | null;
97
- durationMs: number;
98
- commitSeq: number | null;
99
- operationCount: number | null;
100
- rowCount: number | null;
101
- subscriptionCount: number | null;
102
- scopesSummary: Record<string, string | string[]> | null;
103
- tables: string[];
104
- errorMessage: string | null;
105
- payloadRef: string | null;
106
- createdAt: string;
56
+ interface GatewayClientFields {
107
57
  instanceId?: string;
108
- federatedEventId?: string;
109
- localEventId?: number;
58
+ federatedClientId?: string;
110
59
  }
111
60
 
112
- export interface ConsoleRequestPayload {
113
- payloadRef: string;
114
- partitionId: string;
115
- requestPayload: unknown;
116
- responsePayload: unknown | null;
117
- createdAt: string;
61
+ interface GatewayEventFields {
118
62
  instanceId?: string;
119
63
  federatedEventId?: string;
120
64
  localEventId?: number;
121
65
  }
122
66
 
123
- export interface ConsoleTimelineItem {
124
- type: 'commit' | 'event';
125
- timestamp: string;
126
- commit: ConsoleCommitListItem | null;
127
- event: ConsoleRequestEvent | null;
67
+ interface GatewayTimelineFields {
128
68
  instanceId?: string;
129
69
  federatedTimelineId?: string;
130
70
  localCommitSeq?: number | null;
131
71
  localEventId?: number | null;
132
72
  }
133
73
 
134
- export type ConsoleOperationType =
135
- | 'prune'
136
- | 'compact'
137
- | 'notify_data_change'
138
- | 'evict_client';
139
-
140
- export interface ConsoleOperationEvent {
141
- operationId: number;
142
- operationType: ConsoleOperationType;
143
- consoleUserId: string | null;
144
- partitionId: string | null;
145
- targetClientId: string | null;
146
- requestPayload: unknown | null;
147
- resultPayload: unknown | null;
148
- createdAt: string;
74
+ interface GatewayOperationFields {
149
75
  instanceId?: string;
150
76
  federatedOperationId?: string;
151
77
  localOperationId?: number;
152
78
  }
153
79
 
154
- export interface ConsoleNotifyDataChangeResponse {
155
- commitSeq: number;
156
- tables: string[];
157
- deletedChunks: number;
158
- }
80
+ export type ApiKeyType = JsonRequestBody<'postConsoleApiKeys'>['keyType'];
159
81
 
160
- export interface SyncStats {
161
- commitCount: number;
162
- changeCount: number;
163
- minCommitSeq: number;
164
- maxCommitSeq: number;
165
- clientCount: number;
166
- activeClientCount: number;
167
- minActiveClientCursor: number | null;
168
- maxActiveClientCursor: number | null;
169
- partial?: boolean;
170
- failedInstances?: Array<{
171
- instanceId: string;
172
- reason: string;
173
- status?: number;
174
- }>;
175
- minCommitSeqByInstance?: Record<string, number>;
176
- maxCommitSeqByInstance?: Record<string, number>;
177
- }
82
+ export type ConsoleApiKey = JsonSuccessResponse<'getConsoleApiKeysById'>;
83
+
84
+ export type ConsoleApiKeyBulkRevokeResponse =
85
+ JsonSuccessResponse<'postConsoleApiKeysBulkRevoke'>;
86
+
87
+ export type ConsoleCommitListItem = PaginatedItem<'getConsoleCommits'> &
88
+ GatewayCommitFields;
89
+
90
+ export type ConsoleCommitDetail =
91
+ JsonSuccessResponse<'getConsoleCommitsBySeq'> & GatewayCommitFields;
92
+
93
+ export type ConsoleClient = PaginatedItem<'getConsoleClients'> &
94
+ GatewayClientFields;
95
+
96
+ export type ConsoleHandler =
97
+ JsonSuccessResponse<'getConsoleHandlers'>['items'][number];
98
+
99
+ export type ConsoleRequestEvent = PaginatedItem<'getConsoleEvents'> &
100
+ GatewayEventFields;
101
+
102
+ export type ConsoleRequestPayload =
103
+ JsonSuccessResponse<'getConsoleEventsByIdPayload'> & GatewayEventFields;
104
+
105
+ type BaseConsoleTimelineItem = PaginatedItem<'getConsoleTimeline'>;
106
+
107
+ export type ConsoleTimelineItem = Omit<
108
+ BaseConsoleTimelineItem,
109
+ 'commit' | 'event'
110
+ > &
111
+ GatewayTimelineFields & {
112
+ commit: ConsoleCommitListItem | null;
113
+ event: ConsoleRequestEvent | null;
114
+ };
115
+
116
+ export type ConsoleOperationType =
117
+ PaginatedItem<'getConsoleOperations'>['operationType'];
118
+
119
+ export type ConsoleOperationEvent = PaginatedItem<'getConsoleOperations'> &
120
+ GatewayOperationFields;
121
+
122
+ export type ConsoleNotifyDataChangeResponse =
123
+ JsonSuccessResponse<'postConsoleNotifyDataChange'>;
124
+
125
+ export type SyncStats = JsonSuccessResponse<'getConsoleStats'> &
126
+ GatewayAggregateMetadata & {
127
+ minCommitSeqByInstance?: Record<string, number>;
128
+ maxCommitSeqByInstance?: Record<string, number>;
129
+ };
178
130
 
179
131
  export interface PaginatedResponse<T> {
180
132
  items: T[];
@@ -183,38 +135,18 @@ export interface PaginatedResponse<T> {
183
135
  limit: number;
184
136
  }
185
137
 
186
- // Time-series types
187
- export type TimeseriesInterval = 'minute' | 'hour' | 'day';
188
- export type TimeseriesRange = '1h' | '6h' | '24h' | '7d' | '30d';
138
+ export type TimeseriesInterval =
139
+ JsonSuccessResponse<'getConsoleStatsTimeseries'>['interval'];
189
140
 
190
- export interface TimeseriesBucket {
191
- timestamp: string;
192
- pushCount: number;
193
- pullCount: number;
194
- errorCount: number;
195
- avgLatencyMs: number;
196
- }
141
+ export type TimeseriesRange =
142
+ JsonSuccessResponse<'getConsoleStatsTimeseries'>['range'];
197
143
 
198
- export interface TimeseriesStatsResponse {
199
- buckets: TimeseriesBucket[];
200
- interval: TimeseriesInterval;
201
- range: TimeseriesRange;
202
- }
144
+ export type TimeseriesStatsResponse =
145
+ JsonSuccessResponse<'getConsoleStatsTimeseries'> & GatewayAggregateMetadata;
203
146
 
204
- // Latency percentiles types
205
- export interface LatencyPercentiles {
206
- p50: number;
207
- p90: number;
208
- p99: number;
209
- }
147
+ export type LatencyStatsResponse =
148
+ JsonSuccessResponse<'getConsoleStatsLatency'> & GatewayAggregateMetadata;
210
149
 
211
- export interface LatencyStatsResponse {
212
- push: LatencyPercentiles;
213
- pull: LatencyPercentiles;
214
- range: TimeseriesRange;
215
- }
216
-
217
- // Live events types
218
150
  export interface LiveEvent {
219
151
  type:
220
152
  | 'push'
@@ -227,19 +159,4 @@ export interface LiveEvent {
227
159
  data: Record<string, unknown>;
228
160
  }
229
161
 
230
- // ---------------------------------------------------------------------------
231
- // Blob storage
232
- // ---------------------------------------------------------------------------
233
-
234
- export interface ConsoleBlob {
235
- key: string;
236
- size: number;
237
- uploaded: string;
238
- httpMetadata?: { contentType?: string };
239
- }
240
-
241
- export interface ConsoleBlobListResponse {
242
- items: ConsoleBlob[];
243
- truncated: boolean;
244
- cursor: string | null;
245
- }
162
+ export type ConsoleBlobListResponse = JsonSuccessResponse<'getConsoleStorage'>;
@@ -26,8 +26,8 @@ import {
26
26
  TableHead,
27
27
  TableHeader,
28
28
  TableRow,
29
+ Toggle,
29
30
  ToggleGroup,
30
- ToggleGroupItem,
31
31
  } from '@syncular/ui';
32
32
  import { useEffect, useMemo, useState } from 'react';
33
33
  import {
@@ -48,6 +48,8 @@ import type {
48
48
  ConsoleApiKeyBulkRevokeResponse,
49
49
  } from '../lib/types';
50
50
 
51
+ const ToggleGroupItem = Toggle;
52
+
51
53
  export function Config({ children }: { children?: import('react').ReactNode }) {
52
54
  return (
53
55
  <div className="space-y-4 px-5 py-5">