docpouch-client 0.8.3 → 0.8.5
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/dist/index.d.ts +73 -6
- package/dist/index.js +249 -49
- package/dist/types.d.ts +9 -1
- package/dist/types.js +1 -2
- package/package.json +14 -7
- package/src/__tests__/index.test.ts +7 -7
- package/src/index.ts +349 -93
- package/src/types.ts +11 -1
- package/tsconfig.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,61 @@
|
|
|
1
|
-
import type { I_UserEntry, I_UserLogin, I_UserCreation, I_UserUpdate, I_UserDisplay, I_DocumentEntry, I_DataStructure, I_LoginResponse, I_DocumentQuery, I_StructureCreation, I_WsMessage, I_EventString } from "./types.js";
|
|
1
|
+
import type { I_UserEntry, I_UserLogin, I_UserCreation, I_UserUpdate, I_UserDisplay, I_DocumentEntry, I_DataStructure, I_LoginResponse, I_DocumentQuery, I_StructureCreation, I_WsMessage, I_EventString, I_DocumentType } from "./types.js";
|
|
2
2
|
import { Socket } from "socket.io-client";
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Client for interacting with docPouch API.
|
|
5
|
+
*/
|
|
6
|
+
export default class docPouchClient {
|
|
7
|
+
/**
|
|
8
|
+
* The base URL of the server.
|
|
9
|
+
*
|
|
10
|
+
* @type {string}
|
|
11
|
+
*/
|
|
4
12
|
baseUrl: string;
|
|
5
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Socket.IO socket instance for real-time communication with the server.
|
|
15
|
+
*
|
|
16
|
+
* @type {Socket}
|
|
17
|
+
*/
|
|
6
18
|
socket: Socket;
|
|
7
|
-
|
|
8
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Callback function to handle socket events.
|
|
21
|
+
*
|
|
22
|
+
* @type {(event: I_EventString, data: I_WsMessage) => void}
|
|
23
|
+
*/
|
|
24
|
+
callbackFunction: ((event: I_EventString, data: I_WsMessage) => void) | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Flag indicating whether real-time synchronization is enabled.
|
|
27
|
+
*
|
|
28
|
+
* @type {boolean}
|
|
29
|
+
*/
|
|
30
|
+
realTimeSync: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Authentication token used to authorize requests.
|
|
33
|
+
*
|
|
34
|
+
* @private
|
|
35
|
+
* @type {string | null}
|
|
36
|
+
*/
|
|
37
|
+
private authToken;
|
|
38
|
+
/**
|
|
39
|
+
* Flag indicating whether a connection attempt is in progress.
|
|
40
|
+
*
|
|
41
|
+
* @private
|
|
42
|
+
* @type {boolean}
|
|
43
|
+
*/
|
|
44
|
+
private connectionInProgress;
|
|
45
|
+
/**
|
|
46
|
+
* Creates an instance of docPouchClient.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} host - The base URL for the server.
|
|
49
|
+
* @param {number} [port=80] - The port number to connect to (default is 80).
|
|
50
|
+
* @param {(event: I_EventString, data: I_WsMessage) => void} [callback] - Optional callback function for socket events.
|
|
51
|
+
*/
|
|
52
|
+
constructor(host: string, port?: number, callback?: (event: I_EventString, data: I_WsMessage) => void);
|
|
53
|
+
/**
|
|
54
|
+
* Sets the real-time synchronization status.
|
|
55
|
+
*
|
|
56
|
+
* @param {boolean} newRealTimeSync - The new real-time sync setting (true/false).
|
|
57
|
+
*/
|
|
58
|
+
setRealTimeSync(newRealTimeSync: boolean): void;
|
|
9
59
|
login(credentials: I_UserLogin): Promise<I_LoginResponse | null>;
|
|
10
60
|
listUsers(): Promise<I_UserEntry[]>;
|
|
11
61
|
updateUser(userID: string, userData: I_UserUpdate): Promise<void>;
|
|
@@ -20,7 +70,24 @@ export default class Index {
|
|
|
20
70
|
getStructures(): Promise<I_DataStructure[]>;
|
|
21
71
|
updateStructure(structureID: string, structureData: I_DataStructure): Promise<void>;
|
|
22
72
|
removeStructure(structureID: string): Promise<void>;
|
|
73
|
+
createType(type: I_DocumentType): Promise<I_DocumentType>;
|
|
74
|
+
removeType(typeID: string): Promise<void>;
|
|
75
|
+
getTypes(): Promise<I_DocumentType[]>;
|
|
76
|
+
updateType(updatedType: I_DocumentType): Promise<void>;
|
|
23
77
|
setToken(token: string | null): void;
|
|
24
|
-
getToken(): string | null;
|
|
25
78
|
getVersion(): string;
|
|
79
|
+
debugSocketConnection(): void;
|
|
80
|
+
/**
|
|
81
|
+
* Sets up permanent socket listeners for the client.
|
|
82
|
+
*
|
|
83
|
+
* @private
|
|
84
|
+
*/
|
|
85
|
+
private setupPermanentSocketListeners;
|
|
86
|
+
/**
|
|
87
|
+
* Initializes the WebSocket connection with the server.
|
|
88
|
+
*
|
|
89
|
+
* @private
|
|
90
|
+
*/
|
|
91
|
+
private initWebSocket;
|
|
92
|
+
private request;
|
|
26
93
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,54 +1,99 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
import { io } from "socket.io-client";
|
|
2
|
+
import packetJson from '../package.json';
|
|
3
|
+
/**
|
|
4
|
+
* Client for interacting with docPouch API.
|
|
5
|
+
*/
|
|
6
|
+
export default class docPouchClient {
|
|
7
|
+
/**
|
|
8
|
+
* Creates an instance of docPouchClient.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} host - The base URL for the server.
|
|
11
|
+
* @param {number} [port=80] - The port number to connect to (default is 80).
|
|
12
|
+
* @param {(event: I_EventString, data: I_WsMessage) => void} [callback] - Optional callback function for socket events.
|
|
13
|
+
*/
|
|
14
|
+
constructor(host, port = 80, callback) {
|
|
15
|
+
/**
|
|
16
|
+
* Flag indicating whether real-time synchronization is enabled.
|
|
17
|
+
*
|
|
18
|
+
* @type {boolean}
|
|
19
|
+
*/
|
|
20
|
+
this.realTimeSync = false;
|
|
21
|
+
/**
|
|
22
|
+
* Authentication token used to authorize requests.
|
|
23
|
+
*
|
|
24
|
+
* @private
|
|
25
|
+
* @type {string | null}
|
|
26
|
+
*/
|
|
27
|
+
this.authToken = null;
|
|
28
|
+
/**
|
|
29
|
+
* Flag indicating whether a connection attempt is in progress.
|
|
30
|
+
*
|
|
31
|
+
* @private
|
|
32
|
+
* @type {boolean}
|
|
33
|
+
*/
|
|
34
|
+
this.connectionInProgress = false;
|
|
35
|
+
this.baseUrl = host;
|
|
36
|
+
const socketUrl = host.includes('://') ? host : `https://${host}`;
|
|
37
|
+
const socketUrlWithPort = socketUrl.includes(':') && !socketUrl.endsWith(':')
|
|
38
|
+
? socketUrl
|
|
39
|
+
: `${socketUrl}:${port}`;
|
|
40
|
+
console.log(`Initializing Socket.IO with URL: ${socketUrlWithPort}, path: /socket.io`);
|
|
41
|
+
this.socket = io(`${socketUrlWithPort}`, {
|
|
42
|
+
autoConnect: false,
|
|
43
|
+
transports: ['websocket'], // Try websocket only first
|
|
44
|
+
reconnection: true,
|
|
45
|
+
reconnectionAttempts: 5,
|
|
46
|
+
reconnectionDelay: 1000,
|
|
47
|
+
forceNew: true, // Force a new connection
|
|
48
|
+
auth: {
|
|
49
|
+
token: null // Will be set later
|
|
50
|
+
},
|
|
51
|
+
path: '/socket.io'
|
|
52
|
+
});
|
|
53
|
+
this.callbackFunction = callback;
|
|
54
|
+
this.setupPermanentSocketListeners();
|
|
25
55
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Sets the real-time synchronization status.
|
|
58
|
+
*
|
|
59
|
+
* @param {boolean} newRealTimeSync - The new real-time sync setting (true/false).
|
|
60
|
+
*/
|
|
61
|
+
setRealTimeSync(newRealTimeSync) {
|
|
62
|
+
console.log(`Setting realtime sync to: ${newRealTimeSync}. Current setting: ${this.realTimeSync}`);
|
|
63
|
+
// Skip if the setting isn't changing
|
|
64
|
+
if (newRealTimeSync === this.realTimeSync) {
|
|
65
|
+
console.log("Realtime sync setting unchanged, skipping");
|
|
66
|
+
return;
|
|
32
67
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
this.realTimeSync = newRealTimeSync;
|
|
69
|
+
if (newRealTimeSync && this.authToken) {
|
|
70
|
+
console.log("Activating realtime updates");
|
|
71
|
+
// Ensure we're not in the middle of another connection attempt
|
|
72
|
+
if (this.connectionInProgress) {
|
|
73
|
+
console.log("Connection already in progress, waiting before initializing");
|
|
74
|
+
setTimeout(() => this.initWebSocket(), 500);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
this.initWebSocket();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (!newRealTimeSync) {
|
|
81
|
+
console.log("Deactivating realtime updates");
|
|
82
|
+
if (this.socket.connected) {
|
|
83
|
+
console.log("Disconnecting socket");
|
|
84
|
+
this.socket.disconnect();
|
|
42
85
|
}
|
|
43
|
-
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
44
86
|
}
|
|
45
|
-
return await response.json();
|
|
46
87
|
}
|
|
47
88
|
// User Administration Endpoints
|
|
48
89
|
async login(credentials) {
|
|
49
90
|
const response = await this.request('/users/login', 'POST', credentials, false);
|
|
50
91
|
if (response.token) {
|
|
51
|
-
this.
|
|
92
|
+
this.authToken = response.token;
|
|
93
|
+
// Reconnect websocket with new token if realtime sync is enabled
|
|
94
|
+
if (this.realTimeSync) {
|
|
95
|
+
this.initWebSocket();
|
|
96
|
+
}
|
|
52
97
|
return { token: response.token, isAdmin: response.isAdmin };
|
|
53
98
|
}
|
|
54
99
|
return null;
|
|
@@ -94,15 +139,170 @@ class Index {
|
|
|
94
139
|
async removeStructure(structureID) {
|
|
95
140
|
await this.request(`/structures/remove/${structureID}`, 'DELETE');
|
|
96
141
|
}
|
|
97
|
-
|
|
98
|
-
|
|
142
|
+
// Data Type Endpoints
|
|
143
|
+
async createType(type) {
|
|
144
|
+
return await this.request('/types/write', 'PATCH', type);
|
|
99
145
|
}
|
|
100
|
-
|
|
101
|
-
return this.
|
|
146
|
+
async removeType(typeID) {
|
|
147
|
+
return await this.request(`/types/remove/${typeID}`, 'DELETE');
|
|
148
|
+
}
|
|
149
|
+
async getTypes() {
|
|
150
|
+
return await this.request('/types/list', 'GET');
|
|
151
|
+
}
|
|
152
|
+
async updateType(updatedType) {
|
|
153
|
+
await this.request(`/types/write`, 'PATCH', updatedType);
|
|
154
|
+
}
|
|
155
|
+
setToken(token) {
|
|
156
|
+
console.log("Setting token to:", token ? "***token***" : "null");
|
|
157
|
+
const tokenChanged = this.authToken !== token;
|
|
158
|
+
this.authToken = token;
|
|
159
|
+
if (!tokenChanged) {
|
|
160
|
+
console.log("Token unchanged, no need to reconnect");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// If we have a new token and realtime sync is enabled
|
|
164
|
+
if (token && this.realTimeSync) {
|
|
165
|
+
console.log("New token set, will initialize WebSocket");
|
|
166
|
+
// Ensure any existing connection is closed first
|
|
167
|
+
if (this.socket.connected) {
|
|
168
|
+
console.log("Disconnecting existing socket before reconnecting with new token");
|
|
169
|
+
this.socket.disconnect();
|
|
170
|
+
}
|
|
171
|
+
// Wait a moment for the disconnect to complete
|
|
172
|
+
setTimeout(() => {
|
|
173
|
+
console.log("Initializing WebSocket with new token");
|
|
174
|
+
this.initWebSocket();
|
|
175
|
+
}, 300);
|
|
176
|
+
}
|
|
177
|
+
// If token was cleared or realtime sync is disabled
|
|
178
|
+
else if (this.socket.connected) {
|
|
179
|
+
console.log("Token cleared or realtime sync disabled, disconnecting");
|
|
180
|
+
this.socket.disconnect();
|
|
181
|
+
}
|
|
102
182
|
}
|
|
103
183
|
getVersion() {
|
|
104
|
-
return
|
|
184
|
+
return packetJson.version;
|
|
185
|
+
}
|
|
186
|
+
debugSocketConnection() {
|
|
187
|
+
console.log("Socket connection debug info:");
|
|
188
|
+
console.log("- Connected:", this.socket.connected);
|
|
189
|
+
console.log("- Socket ID:", this.socket.id);
|
|
190
|
+
console.log("- Auth token present:", !!this.authToken);
|
|
191
|
+
console.log("- Connection in progress:", this.connectionInProgress);
|
|
192
|
+
console.log("- Realtime sync enabled:", this.realTimeSync);
|
|
193
|
+
console.log("- Socket options:", this.socket.io.opts);
|
|
194
|
+
// Try to force reconnection
|
|
195
|
+
if (!this.socket.connected && this.authToken && this.realTimeSync) {
|
|
196
|
+
console.log("Attempting to force reconnection...");
|
|
197
|
+
this.socket.auth = { token: this.authToken };
|
|
198
|
+
this.socket.connect();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Sets up permanent socket listeners for the client.
|
|
203
|
+
*
|
|
204
|
+
* @private
|
|
205
|
+
*/
|
|
206
|
+
setupPermanentSocketListeners() {
|
|
207
|
+
// These are permanent listeners that won't be removed
|
|
208
|
+
this.socket.on('connect_error', (error) => {
|
|
209
|
+
console.error('Socket connection error:', error.message);
|
|
210
|
+
this.connectionInProgress = false;
|
|
211
|
+
});
|
|
212
|
+
this.socket.on('connect', () => {
|
|
213
|
+
console.log('Socket connected successfully with ID:', this.socket.id);
|
|
214
|
+
this.connectionInProgress = false;
|
|
215
|
+
});
|
|
216
|
+
this.socket.on('disconnect', (reason) => {
|
|
217
|
+
console.log('Socket disconnected. Reason:', reason);
|
|
218
|
+
this.connectionInProgress = false;
|
|
219
|
+
});
|
|
220
|
+
this.socket.on('error', (error) => {
|
|
221
|
+
console.error('Socket error:', error);
|
|
222
|
+
this.connectionInProgress = false;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Initializes the WebSocket connection with the server.
|
|
227
|
+
*
|
|
228
|
+
* @private
|
|
229
|
+
*/
|
|
230
|
+
initWebSocket() {
|
|
231
|
+
console.log("initWebSocket called. Auth token present:", !!this.authToken, "Connection in progress:", this.connectionInProgress, "Socket connected:", this.socket.connected);
|
|
232
|
+
if (!this.authToken) {
|
|
233
|
+
console.log("Skipping WebSocket initialization: No auth token");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (this.connectionInProgress) {
|
|
237
|
+
console.log("Connection already in progress, skipping initialization");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (this.socket.connected) {
|
|
241
|
+
console.log("Socket already connected with ID:", this.socket.id);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
this.connectionInProgress = true;
|
|
245
|
+
try {
|
|
246
|
+
console.log("Setting up WebSocket connection with token");
|
|
247
|
+
// Update the auth token
|
|
248
|
+
this.socket.auth = { token: this.authToken };
|
|
249
|
+
// Remove any dynamic event listeners that might have been added
|
|
250
|
+
this.socket.offAny();
|
|
251
|
+
// Set up event handler for application events
|
|
252
|
+
this.socket.onAny((event, data) => {
|
|
253
|
+
if (event === "heartbeatPing") {
|
|
254
|
+
console.log("Ping event received:", data);
|
|
255
|
+
this.socket.emit("heartbeatPong", Date.now());
|
|
256
|
+
}
|
|
257
|
+
else if (this.callbackFunction) {
|
|
258
|
+
this.callbackFunction(event, data);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
// Connect to the server
|
|
262
|
+
console.log("Connecting socket with auth token");
|
|
263
|
+
this.socket.connect();
|
|
264
|
+
// Add a timeout to detect if connection is taking too long
|
|
265
|
+
setTimeout(() => {
|
|
266
|
+
if (this.connectionInProgress) {
|
|
267
|
+
console.warn("Socket connection attempt timed out after 5 seconds");
|
|
268
|
+
this.connectionInProgress = false;
|
|
269
|
+
// If we're still not connected after the timeout, try again with polling
|
|
270
|
+
if (!this.socket.connected) {
|
|
271
|
+
console.log("Retrying connection with polling transport");
|
|
272
|
+
this.socket.io.opts.transports = ['polling', 'websocket'];
|
|
273
|
+
this.socket.connect();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}, 5000);
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
console.error('Error in initWebSocket:', error);
|
|
280
|
+
this.connectionInProgress = false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async request(endpoint, method, body, requiresAuth = true) {
|
|
284
|
+
const headers = {
|
|
285
|
+
'Content-Type': 'application/json',
|
|
286
|
+
};
|
|
287
|
+
if (requiresAuth && this.authToken)
|
|
288
|
+
headers['Authorization'] = `Bearer ${this.authToken}`;
|
|
289
|
+
if (this.socket.id)
|
|
290
|
+
headers['X-Socket-ID'] = this.socket.id;
|
|
291
|
+
const options = {
|
|
292
|
+
method,
|
|
293
|
+
headers,
|
|
294
|
+
body: body ? JSON.stringify(body) : undefined
|
|
295
|
+
};
|
|
296
|
+
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
297
|
+
const normalizedBaseUrl = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
|
298
|
+
const url = `${normalizedBaseUrl}${normalizedEndpoint}`;
|
|
299
|
+
const response = await fetch(url, options);
|
|
300
|
+
if (!response.ok) {
|
|
301
|
+
if (response.status === 401 || response.status === 403) {
|
|
302
|
+
this.authToken = null;
|
|
303
|
+
}
|
|
304
|
+
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
305
|
+
}
|
|
306
|
+
return await response.json();
|
|
105
307
|
}
|
|
106
308
|
}
|
|
107
|
-
exports.default = Index;
|
|
108
|
-
module.exports = Index;
|
package/dist/types.d.ts
CHANGED
|
@@ -84,7 +84,15 @@ export interface I_StructureUpdate {
|
|
|
84
84
|
reference?: any;
|
|
85
85
|
fields?: any[];
|
|
86
86
|
}
|
|
87
|
-
export
|
|
87
|
+
export interface I_DocumentType {
|
|
88
|
+
_id?: string;
|
|
89
|
+
type: number;
|
|
90
|
+
subType: number;
|
|
91
|
+
name: string;
|
|
92
|
+
description?: string;
|
|
93
|
+
defaultStructureID?: string;
|
|
94
|
+
}
|
|
95
|
+
export type I_EventString = 'subscribe' | 'unsubscribe' | 'heartbeatPong' | "heartbeatPing" | "newDocument" | "newStructure" | "newUser" | "removedID" | "changedDocument" | "changedStructure" | "changedUser" | "confirmSubscription" | "confirmUnsubscription" | "removedUser" | "removedStructure" | "removedDocument";
|
|
88
96
|
export interface I_WsMessage {
|
|
89
97
|
newDocument?: I_DocumentEntry;
|
|
90
98
|
newStructure?: I_StructureEntry;
|
package/dist/types.js
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docpouch-client",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/index.js",
|
|
9
|
+
"require": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
6
13
|
"scripts": {
|
|
7
14
|
"build": "tsc",
|
|
8
15
|
"test": "jest"
|
|
@@ -15,13 +22,13 @@
|
|
|
15
22
|
"license": "MIT",
|
|
16
23
|
"description": "A small class to more easily access the docPouch API",
|
|
17
24
|
"devDependencies": {
|
|
18
|
-
"@types/
|
|
19
|
-
"
|
|
20
|
-
"jest": "^
|
|
21
|
-
"ts-jest": "^29.
|
|
22
|
-
"@types/jest": "^29.5.14"
|
|
25
|
+
"@types/jest": "^30.0.0",
|
|
26
|
+
"@types/node": "^24.0.3",
|
|
27
|
+
"jest": "^30.0.2",
|
|
28
|
+
"ts-jest": "^29.4.0"
|
|
23
29
|
},
|
|
24
30
|
"dependencies": {
|
|
31
|
+
"socket.io-client": "^4.8.1",
|
|
25
32
|
"typescript": "^5.8.3"
|
|
26
33
|
}
|
|
27
|
-
}
|
|
34
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import docPouchClient from '../index.js';
|
|
2
2
|
import packetJson from '../../package.json';
|
|
3
3
|
import {
|
|
4
4
|
I_UserLogin,
|
|
@@ -28,7 +28,7 @@ global.fetch = jest.fn();
|
|
|
28
28
|
const mockFetch = global.fetch as jest.Mock;
|
|
29
29
|
|
|
30
30
|
describe('Index Class', () => {
|
|
31
|
-
let index:
|
|
31
|
+
let index: docPouchClient;
|
|
32
32
|
const baseUrl = 'http://example.com';
|
|
33
33
|
|
|
34
34
|
beforeEach(() => {
|
|
@@ -37,7 +37,7 @@ describe('Index Class', () => {
|
|
|
37
37
|
mockFetch.mockClear();
|
|
38
38
|
|
|
39
39
|
// Create a new instance of Index for each test
|
|
40
|
-
index = new
|
|
40
|
+
index = new docPouchClient(baseUrl);
|
|
41
41
|
|
|
42
42
|
// Mock successful fetch response
|
|
43
43
|
mockFetch.mockImplementation(() =>
|
|
@@ -62,7 +62,7 @@ describe('Index Class', () => {
|
|
|
62
62
|
|
|
63
63
|
it('should connect socket and set up event handler when callback is provided', () => {
|
|
64
64
|
const mockCallback = jest.fn();
|
|
65
|
-
const indexWithCallback = new
|
|
65
|
+
const indexWithCallback = new docPouchClient(baseUrl, mockCallback);
|
|
66
66
|
|
|
67
67
|
expect(indexWithCallback.socket.connect).toHaveBeenCalled();
|
|
68
68
|
expect(indexWithCallback.socket.onAny).toHaveBeenCalled();
|
|
@@ -86,7 +86,7 @@ describe('Index Class', () => {
|
|
|
86
86
|
jest.mock('../../package.json', () => ({ version: packetJson.version }), { virtual: true });
|
|
87
87
|
|
|
88
88
|
// We need to re-create the index instance to pick up the mocked package.json
|
|
89
|
-
const newIndex = new
|
|
89
|
+
const newIndex = new docPouchClient(baseUrl);
|
|
90
90
|
expect(newIndex.getVersion()).toBe(packetJson.version);
|
|
91
91
|
});
|
|
92
92
|
});
|
|
@@ -472,7 +472,7 @@ describe('Index Class', () => {
|
|
|
472
472
|
describe('WebSocket functionality', () => {
|
|
473
473
|
it('should connect socket when callback is provided', () => {
|
|
474
474
|
const mockCallback = jest.fn();
|
|
475
|
-
const indexWithCallback = new
|
|
475
|
+
const indexWithCallback = new docPouchClient(baseUrl, mockCallback);
|
|
476
476
|
|
|
477
477
|
// Verify that connect was called
|
|
478
478
|
expect(indexWithCallback.socket.connect).toHaveBeenCalled();
|
|
@@ -480,7 +480,7 @@ describe('Index Class', () => {
|
|
|
480
480
|
|
|
481
481
|
it('should set up event handler when callback is provided', () => {
|
|
482
482
|
const mockCallback = jest.fn();
|
|
483
|
-
const indexWithCallback = new
|
|
483
|
+
const indexWithCallback = new docPouchClient(baseUrl, mockCallback);
|
|
484
484
|
|
|
485
485
|
// Verify that onAny was called
|
|
486
486
|
expect(indexWithCallback.socket.onAny).toHaveBeenCalled();
|
package/src/index.ts
CHANGED
|
@@ -6,136 +6,392 @@ import type {
|
|
|
6
6
|
I_UserDisplay,
|
|
7
7
|
I_DocumentEntry,
|
|
8
8
|
I_DataStructure,
|
|
9
|
-
|
|
9
|
+
I_LoginResponse, I_DocumentQuery, I_StructureCreation, I_WsMessage, I_EventString, I_DocumentType
|
|
10
10
|
} from "./types.js";
|
|
11
11
|
import {io, Socket} from "socket.io-client";
|
|
12
12
|
import packetJson from '../package.json'
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Client for interacting with docPouch API.
|
|
16
|
+
*/
|
|
17
|
+
export default class docPouchClient {
|
|
18
|
+
/**
|
|
19
|
+
* The base URL of the server.
|
|
20
|
+
*
|
|
21
|
+
* @type {string}
|
|
22
|
+
*/
|
|
23
|
+
baseUrl: string;
|
|
24
|
+
/**
|
|
25
|
+
* Socket.IO socket instance for real-time communication with the server.
|
|
26
|
+
*
|
|
27
|
+
* @type {Socket}
|
|
28
|
+
*/
|
|
29
|
+
socket: Socket;
|
|
30
|
+
/**
|
|
31
|
+
* Callback function to handle socket events.
|
|
32
|
+
*
|
|
33
|
+
* @type {(event: I_EventString, data: I_WsMessage) => void}
|
|
34
|
+
*/
|
|
35
|
+
callbackFunction;
|
|
36
|
+
/**
|
|
37
|
+
* Flag indicating whether real-time synchronization is enabled.
|
|
38
|
+
*
|
|
39
|
+
* @type {boolean}
|
|
40
|
+
*/
|
|
41
|
+
realTimeSync: boolean = false;
|
|
42
|
+
/**
|
|
43
|
+
* Authentication token used to authorize requests.
|
|
44
|
+
*
|
|
45
|
+
* @private
|
|
46
|
+
* @type {string | null}
|
|
47
|
+
*/
|
|
48
|
+
private authToken: string | null = null;
|
|
49
|
+
/**
|
|
50
|
+
* Flag indicating whether a connection attempt is in progress.
|
|
51
|
+
*
|
|
52
|
+
* @private
|
|
53
|
+
* @type {boolean}
|
|
54
|
+
*/
|
|
55
|
+
private connectionInProgress = false;
|
|
18
56
|
|
|
19
|
-
|
|
20
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Creates an instance of docPouchClient.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} host - The base URL for the server.
|
|
61
|
+
* @param {number} [port=80] - The port number to connect to (default is 80).
|
|
62
|
+
* @param {(event: I_EventString, data: I_WsMessage) => void} [callback] - Optional callback function for socket events.
|
|
63
|
+
*/
|
|
64
|
+
constructor(host: string, port: number = 80, callback?: (event: I_EventString, data: I_WsMessage) => void) {
|
|
65
|
+
this.baseUrl = host;
|
|
66
|
+
const socketUrl = host.includes('://') ? host : `https://${host}`;
|
|
67
|
+
const socketUrlWithPort = socketUrl.includes(':') && !socketUrl.endsWith(':')
|
|
68
|
+
? socketUrl
|
|
69
|
+
: `${socketUrl}:${port}`;
|
|
21
70
|
|
|
22
|
-
|
|
23
|
-
if (callback) {
|
|
24
|
-
this.socket.connect();
|
|
25
|
-
this.socket.onAny((event: I_EventString, data: I_WsMessage) => {
|
|
26
|
-
if (event === "heartbeatPing") {
|
|
27
|
-
console.log("Ping event received:", data);
|
|
28
|
-
this.socket.emit("heartbeatPong", Date.now());
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
callback(event, data);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
}
|
|
71
|
+
console.log(`Initializing Socket.IO with URL: ${socketUrlWithPort}, path: /socket.io`);
|
|
36
72
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
73
|
+
this.socket = io(`${socketUrlWithPort}`, {
|
|
74
|
+
autoConnect: false,
|
|
75
|
+
transports: ['websocket'], // Try websocket only first
|
|
76
|
+
reconnection: true,
|
|
77
|
+
reconnectionAttempts: 5,
|
|
78
|
+
reconnectionDelay: 1000,
|
|
79
|
+
forceNew: true, // Force a new connection
|
|
80
|
+
auth: {
|
|
81
|
+
token: null // Will be set later
|
|
82
|
+
},
|
|
83
|
+
path: '/socket.io'
|
|
84
|
+
});
|
|
41
85
|
|
|
42
|
-
|
|
43
|
-
headers['Authorization'] = `Bearer ${this.token}`;
|
|
44
|
-
}
|
|
86
|
+
this.callbackFunction = callback;
|
|
45
87
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
headers,
|
|
49
|
-
body: body ? JSON.stringify(body) : undefined
|
|
50
|
-
};
|
|
88
|
+
this.setupPermanentSocketListeners();
|
|
89
|
+
}
|
|
51
90
|
|
|
52
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Sets the real-time synchronization status.
|
|
93
|
+
*
|
|
94
|
+
* @param {boolean} newRealTimeSync - The new real-time sync setting (true/false).
|
|
95
|
+
*/
|
|
96
|
+
setRealTimeSync(newRealTimeSync: boolean) {
|
|
97
|
+
console.log(`Setting realtime sync to: ${newRealTimeSync}. Current setting: ${this.realTimeSync}`);
|
|
53
98
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return await response.json() as T;
|
|
99
|
+
// Skip if the setting isn't changing
|
|
100
|
+
if (newRealTimeSync === this.realTimeSync) {
|
|
101
|
+
console.log("Realtime sync setting unchanged, skipping");
|
|
102
|
+
return;
|
|
62
103
|
}
|
|
63
104
|
|
|
64
|
-
|
|
65
|
-
async login(credentials: I_UserLogin): Promise<I_LoginResponse | null> {
|
|
66
|
-
const response = await this.request<I_LoginResponse>('/users/login', 'POST', credentials, false);
|
|
67
|
-
if (response.token) {
|
|
68
|
-
this.token = response.token;
|
|
69
|
-
return {token: response.token, isAdmin: response.isAdmin};
|
|
70
|
-
}
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
105
|
+
this.realTimeSync = newRealTimeSync;
|
|
73
106
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
107
|
+
if (newRealTimeSync && this.authToken) {
|
|
108
|
+
console.log("Activating realtime updates");
|
|
77
109
|
|
|
78
|
-
|
|
79
|
-
|
|
110
|
+
// Ensure we're not in the middle of another connection attempt
|
|
111
|
+
if (this.connectionInProgress) {
|
|
112
|
+
console.log("Connection already in progress, waiting before initializing");
|
|
113
|
+
setTimeout(() => this.initWebSocket(), 500);
|
|
114
|
+
} else {
|
|
115
|
+
this.initWebSocket();
|
|
116
|
+
}
|
|
117
|
+
} else if (!newRealTimeSync) {
|
|
118
|
+
console.log("Deactivating realtime updates");
|
|
119
|
+
if (this.socket.connected) {
|
|
120
|
+
console.log("Disconnecting socket");
|
|
121
|
+
this.socket.disconnect();
|
|
122
|
+
}
|
|
80
123
|
}
|
|
124
|
+
}
|
|
81
125
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
126
|
+
// User Administration Endpoints
|
|
127
|
+
async login(credentials: I_UserLogin): Promise<I_LoginResponse | null> {
|
|
128
|
+
const response = await this.request<I_LoginResponse>('/users/login', 'POST', credentials, false);
|
|
129
|
+
if (response.token) {
|
|
130
|
+
this.authToken = response.token;
|
|
85
131
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
132
|
+
// Reconnect websocket with new token if realtime sync is enabled
|
|
133
|
+
if (this.realTimeSync) {
|
|
134
|
+
this.initWebSocket();
|
|
135
|
+
}
|
|
89
136
|
|
|
90
|
-
|
|
91
|
-
async createDocument(document: I_DocumentEntry): Promise<I_DocumentEntry> {
|
|
92
|
-
return await this.request<I_DocumentEntry>('/docs/create', 'POST', document);
|
|
137
|
+
return {token: response.token, isAdmin: response.isAdmin};
|
|
93
138
|
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
94
141
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
142
|
+
async listUsers(): Promise<I_UserEntry[]> {
|
|
143
|
+
return await this.request<I_UserEntry[]>('/users/list', 'GET');
|
|
144
|
+
}
|
|
98
145
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
146
|
+
async updateUser(userID: string, userData: I_UserUpdate): Promise<void> {
|
|
147
|
+
await this.request<void>(`/users/update/${userID}`, 'PATCH', userData);
|
|
148
|
+
}
|
|
102
149
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
150
|
+
async createUser(userData: I_UserCreation): Promise<I_UserDisplay> {
|
|
151
|
+
return await this.request<I_UserDisplay>('/users/create', 'POST', userData);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async removeUser(userID: string): Promise<void> {
|
|
155
|
+
await this.request<void>(`/users/remove/${userID}`, 'DELETE');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Document Management Endpoints
|
|
159
|
+
async createDocument(document: I_DocumentEntry): Promise<I_DocumentEntry> {
|
|
160
|
+
return await this.request<I_DocumentEntry>('/docs/create', 'POST', document);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async listDocuments(): Promise<I_DocumentEntry[]> {
|
|
164
|
+
return await this.request<I_DocumentEntry[]>('/docs/list', 'GET');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async fetchDocument(queryObject: I_DocumentQuery): Promise<I_DocumentEntry[]> {
|
|
168
|
+
return await this.request<I_DocumentEntry[]>(`/docs/fetch/`, 'POST', queryObject);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async updateDocument(documentID: string, documentData: I_DocumentEntry): Promise<void> {
|
|
172
|
+
await this.request<void>(`/docs/update/${documentID}`, 'PATCH', documentData);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async removeDocument(documentID: string): Promise<void> {
|
|
176
|
+
await this.request<void>(`/docs/remove/${documentID}`, 'DELETE');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Data Structure Endpoints
|
|
180
|
+
async createStructure(structure: I_StructureCreation): Promise<I_DataStructure> {
|
|
181
|
+
return await this.request<I_DataStructure>('/structures/create', 'POST', structure);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async getStructures(): Promise<I_DataStructure[]> {
|
|
185
|
+
return await this.request<I_DataStructure[]>('/structures/list', 'GET');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async updateStructure(structureID: string, structureData: I_DataStructure): Promise<void> {
|
|
189
|
+
await this.request<void>(`/structures/update/${structureID}`, 'PATCH', structureData);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async removeStructure(structureID: string): Promise<void> {
|
|
193
|
+
await this.request<void>(`/structures/remove/${structureID}`, 'DELETE');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Data Type Endpoints
|
|
197
|
+
async createType(type: I_DocumentType): Promise<I_DocumentType> {
|
|
198
|
+
return await this.request<I_DocumentType>('/types/write', 'PATCH', type);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async removeType(typeID: string) {
|
|
202
|
+
return await this.request<void>(`/types/remove/${typeID}`, 'DELETE');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async getTypes(): Promise<I_DocumentType[]> {
|
|
206
|
+
return await this.request<I_DocumentType[]>('/types/list', 'GET');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async updateType(updatedType: I_DocumentType): Promise<void> {
|
|
210
|
+
await this.request<void>(`/types/write`, 'PATCH', updatedType);
|
|
211
|
+
}
|
|
106
212
|
|
|
107
|
-
|
|
108
|
-
|
|
213
|
+
setToken(token: string | null): void {
|
|
214
|
+
console.log("Setting token to:", token ? "***token***" : "null");
|
|
215
|
+
|
|
216
|
+
const tokenChanged = this.authToken !== token;
|
|
217
|
+
this.authToken = token;
|
|
218
|
+
|
|
219
|
+
if (!tokenChanged) {
|
|
220
|
+
console.log("Token unchanged, no need to reconnect");
|
|
221
|
+
return;
|
|
109
222
|
}
|
|
110
223
|
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
224
|
+
// If we have a new token and realtime sync is enabled
|
|
225
|
+
if (token && this.realTimeSync) {
|
|
226
|
+
console.log("New token set, will initialize WebSocket");
|
|
227
|
+
|
|
228
|
+
// Ensure any existing connection is closed first
|
|
229
|
+
if (this.socket.connected) {
|
|
230
|
+
console.log("Disconnecting existing socket before reconnecting with new token");
|
|
231
|
+
this.socket.disconnect();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Wait a moment for the disconnect to complete
|
|
235
|
+
setTimeout(() => {
|
|
236
|
+
console.log("Initializing WebSocket with new token");
|
|
237
|
+
this.initWebSocket();
|
|
238
|
+
}, 300);
|
|
114
239
|
}
|
|
240
|
+
// If token was cleared or realtime sync is disabled
|
|
241
|
+
else if (this.socket.connected) {
|
|
242
|
+
console.log("Token cleared or realtime sync disabled, disconnecting");
|
|
243
|
+
this.socket.disconnect();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
getVersion() {
|
|
248
|
+
return packetJson.version;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
debugSocketConnection(): void {
|
|
252
|
+
console.log("Socket connection debug info:");
|
|
253
|
+
console.log("- Connected:", this.socket.connected);
|
|
254
|
+
console.log("- Socket ID:", this.socket.id);
|
|
255
|
+
console.log("- Auth token present:", !!this.authToken);
|
|
256
|
+
console.log("- Connection in progress:", this.connectionInProgress);
|
|
257
|
+
console.log("- Realtime sync enabled:", this.realTimeSync);
|
|
258
|
+
console.log("- Socket options:", this.socket.io.opts);
|
|
115
259
|
|
|
116
|
-
|
|
117
|
-
|
|
260
|
+
// Try to force reconnection
|
|
261
|
+
if (!this.socket.connected && this.authToken && this.realTimeSync) {
|
|
262
|
+
console.log("Attempting to force reconnection...");
|
|
263
|
+
this.socket.auth = {token: this.authToken};
|
|
264
|
+
this.socket.connect();
|
|
118
265
|
}
|
|
266
|
+
}
|
|
119
267
|
|
|
120
|
-
|
|
121
|
-
|
|
268
|
+
/**
|
|
269
|
+
* Sets up permanent socket listeners for the client.
|
|
270
|
+
*
|
|
271
|
+
* @private
|
|
272
|
+
*/
|
|
273
|
+
private setupPermanentSocketListeners() {
|
|
274
|
+
// These are permanent listeners that won't be removed
|
|
275
|
+
this.socket.on('connect_error', (error) => {
|
|
276
|
+
console.error('Socket connection error:', error.message);
|
|
277
|
+
this.connectionInProgress = false;
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
this.socket.on('connect', () => {
|
|
281
|
+
console.log('Socket connected successfully with ID:', this.socket.id);
|
|
282
|
+
this.connectionInProgress = false;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
this.socket.on('disconnect', (reason) => {
|
|
286
|
+
console.log('Socket disconnected. Reason:', reason);
|
|
287
|
+
this.connectionInProgress = false;
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
this.socket.on('error', (error) => {
|
|
291
|
+
console.error('Socket error:', error);
|
|
292
|
+
this.connectionInProgress = false;
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Initializes the WebSocket connection with the server.
|
|
298
|
+
*
|
|
299
|
+
* @private
|
|
300
|
+
*/
|
|
301
|
+
private initWebSocket() {
|
|
302
|
+
console.log("initWebSocket called. Auth token present:", !!this.authToken,
|
|
303
|
+
"Connection in progress:", this.connectionInProgress,
|
|
304
|
+
"Socket connected:", this.socket.connected);
|
|
305
|
+
|
|
306
|
+
if (!this.authToken) {
|
|
307
|
+
console.log("Skipping WebSocket initialization: No auth token");
|
|
308
|
+
return;
|
|
122
309
|
}
|
|
123
310
|
|
|
124
|
-
|
|
125
|
-
|
|
311
|
+
if (this.connectionInProgress) {
|
|
312
|
+
console.log("Connection already in progress, skipping initialization");
|
|
313
|
+
return;
|
|
126
314
|
}
|
|
127
315
|
|
|
128
|
-
|
|
129
|
-
|
|
316
|
+
if (this.socket.connected) {
|
|
317
|
+
console.log("Socket already connected with ID:", this.socket.id);
|
|
318
|
+
return;
|
|
130
319
|
}
|
|
131
320
|
|
|
132
|
-
|
|
133
|
-
|
|
321
|
+
this.connectionInProgress = true;
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
console.log("Setting up WebSocket connection with token");
|
|
325
|
+
|
|
326
|
+
// Update the auth token
|
|
327
|
+
this.socket.auth = {token: this.authToken};
|
|
328
|
+
|
|
329
|
+
// Remove any dynamic event listeners that might have been added
|
|
330
|
+
this.socket.offAny();
|
|
331
|
+
|
|
332
|
+
// Set up event handler for application events
|
|
333
|
+
this.socket.onAny((event: I_EventString, data: I_WsMessage) => {
|
|
334
|
+
if (event === "heartbeatPing") {
|
|
335
|
+
console.log("Ping event received:", data);
|
|
336
|
+
this.socket.emit("heartbeatPong", Date.now());
|
|
337
|
+
} else if (this.callbackFunction) {
|
|
338
|
+
this.callbackFunction(event, data);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Connect to the server
|
|
343
|
+
console.log("Connecting socket with auth token");
|
|
344
|
+
this.socket.connect();
|
|
345
|
+
|
|
346
|
+
// Add a timeout to detect if connection is taking too long
|
|
347
|
+
setTimeout(() => {
|
|
348
|
+
if (this.connectionInProgress) {
|
|
349
|
+
console.warn("Socket connection attempt timed out after 5 seconds");
|
|
350
|
+
this.connectionInProgress = false;
|
|
351
|
+
|
|
352
|
+
// If we're still not connected after the timeout, try again with polling
|
|
353
|
+
if (!this.socket.connected) {
|
|
354
|
+
console.log("Retrying connection with polling transport");
|
|
355
|
+
this.socket.io.opts.transports = ['polling', 'websocket'];
|
|
356
|
+
this.socket.connect();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}, 5000);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
console.error('Error in initWebSocket:', error);
|
|
362
|
+
this.connectionInProgress = false;
|
|
134
363
|
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private async request<T>(endpoint: string, method: string, body?: any, requiresAuth: boolean = true): Promise<T> {
|
|
367
|
+
const headers: HeadersInit = {
|
|
368
|
+
'Content-Type': 'application/json',
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
if (requiresAuth && this.authToken)
|
|
372
|
+
headers['Authorization'] = `Bearer ${this.authToken}`;
|
|
373
|
+
if (this.socket.id)
|
|
374
|
+
headers['X-Socket-ID'] = this.socket.id;
|
|
135
375
|
|
|
136
|
-
|
|
137
|
-
|
|
376
|
+
const options: RequestInit = {
|
|
377
|
+
method,
|
|
378
|
+
headers,
|
|
379
|
+
body: body ? JSON.stringify(body) : undefined
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
383
|
+
const normalizedBaseUrl = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
|
384
|
+
const url = `${normalizedBaseUrl}${normalizedEndpoint}`;
|
|
385
|
+
const response = await fetch(url, options);
|
|
386
|
+
|
|
387
|
+
if (!response.ok) {
|
|
388
|
+
if (response.status === 401 || response.status === 403) {
|
|
389
|
+
this.authToken = null;
|
|
390
|
+
}
|
|
391
|
+
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
138
392
|
}
|
|
139
|
-
}
|
|
140
393
|
|
|
141
|
-
|
|
394
|
+
return await response.json() as T;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -104,10 +104,20 @@ export interface I_StructureUpdate {
|
|
|
104
104
|
fields?: any[];
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// Document type related types
|
|
108
|
+
export interface I_DocumentType {
|
|
109
|
+
_id?: string;
|
|
110
|
+
type: number;
|
|
111
|
+
subType: number;
|
|
112
|
+
name: string;
|
|
113
|
+
description?: string;
|
|
114
|
+
defaultStructureID?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
107
117
|
// WebSocket-related types
|
|
108
118
|
export type I_EventString = 'subscribe' | 'unsubscribe' | 'heartbeatPong' | "heartbeatPing" | "newDocument" | "newStructure" |
|
|
109
119
|
"newUser" | "removedID" | "changedDocument" | "changedStructure" | "changedUser" | "confirmSubscription" |
|
|
110
|
-
"confirmUnsubscription";
|
|
120
|
+
"confirmUnsubscription" | "removedUser" | "removedStructure" | "removedDocument";
|
|
111
121
|
|
|
112
122
|
export interface I_WsMessage {
|
|
113
123
|
newDocument?: I_DocumentEntry;
|
package/tsconfig.json
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"declaration": true,
|
|
14
14
|
"emitDeclarationOnly": false,
|
|
15
15
|
"resolveJsonModule": true,
|
|
16
|
-
"isolatedModules": true
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"allowSyntheticDefaultImports": true
|
|
17
18
|
},
|
|
18
19
|
"include": ["**/*.ts", "**/*.tsx"],
|
|
19
20
|
"exclude": ["node_modules", "**/*.test.ts", "**/*.types.ts", "./dist/**/*"]
|