docpouch-client 0.8.14 → 0.8.15

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/src/index.ts CHANGED
@@ -1,532 +1,665 @@
1
- import {io, Socket} from "socket.io-client";
2
- import packetJson from '../package.json'
3
-
4
- /**
5
- * Client for interacting with docPouch API.
6
- */
7
- export default class docPouchClient {
8
- /**
9
- * The base URL of the server.
10
- *
11
- * @type {string}
12
- */
13
- baseUrl: string;
14
- /**
15
- * Socket.IO socket instance for real-time communication with the server.
16
- *
17
- * @type {Socket}
18
- */
19
- socket: Socket;
20
- /**
21
- * Callback function to handle socket events.
22
- *
23
- * @type {(event: I_EventString, data: I_WsMessage) => void}
24
- */
25
- callbackFunction;
26
- /**
27
- * Flag indicating whether real-time synchronization is enabled.
28
- *
29
- * @type {boolean}
30
- */
31
- realTimeSync: boolean = false;
32
- /**
33
- * Authentication token used to authorize requests.
34
- *
35
- * @private
36
- * @type {string | null}
37
- */
38
- private authToken: string | null = null;
39
- /**
40
- * Flag indicating whether a connection attempt is in progress.
41
- *
42
- * @private
43
- * @type {boolean}
44
- */
45
- private connectionInProgress = false;
46
-
47
- /**
48
- * Creates an instance of docPouchClient.
49
- *
50
- * @param {string} host - The base URL for the server.
51
- * @param {number} [port=80] - The port number to connect to (default is 80).
52
- * @param {(event: I_EventString, data: I_WsMessage) => void} [callback] - Optional callback function for socket events.
53
- */
54
- constructor(host: string, port: number = 80, callback?: (event: I_EventString, data: I_WsMessage) => void) {
55
- this.baseUrl = host;
56
- const socketUrl = host.includes('://') ? host : `https://${host}`;
57
- const socketUrlWithPort = socketUrl.includes(':') && !socketUrl.endsWith(':')
58
- ? socketUrl
59
- : `${socketUrl}:${port}`;
60
-
61
- console.log(`Initializing Socket.IO with URL: ${socketUrlWithPort}, path: /socket.io`);
62
-
63
- this.socket = io(`${socketUrlWithPort}`, {
64
- autoConnect: false,
65
- transports: ['websocket'], // Try websocket only first
66
- reconnection: true,
67
- reconnectionAttempts: 5,
68
- reconnectionDelay: 1000,
69
- forceNew: true, // Force a new connection
70
- auth: {
71
- token: null // Will be set later
72
- },
73
- path: '/socket.io'
74
- });
75
-
76
- this.callbackFunction = callback;
77
-
78
- this.setupPermanentSocketListeners();
79
- }
80
-
81
- /**
82
- * Sets the real-time synchronization status.
83
- *
84
- * @param {boolean} newRealTimeSync - The new real-time sync setting (true/false).
85
- */
86
- setRealTimeSync(newRealTimeSync: boolean) {
87
- console.log(`Setting realtime sync to: ${newRealTimeSync}. Current setting: ${this.realTimeSync}`);
88
-
89
- // Skip if the setting isn't changing
90
- if (newRealTimeSync === this.realTimeSync) {
91
- console.log("Realtime sync setting unchanged, skipping");
92
- return;
93
- }
94
-
95
- this.realTimeSync = newRealTimeSync;
96
-
97
- if (newRealTimeSync && this.authToken) {
98
- console.log("Activating realtime updates");
99
-
100
- // Ensure we're not in the middle of another connection attempt
101
- if (this.connectionInProgress) {
102
- console.log("Connection already in progress, waiting before initializing");
103
- setTimeout(() => this.initWebSocket(), 500);
104
- } else {
105
- this.initWebSocket();
106
- }
107
- } else if (!newRealTimeSync) {
108
- console.log("Deactivating realtime updates");
109
- if (this.socket.connected) {
110
- console.log("Disconnecting socket");
111
- this.socket.disconnect();
112
- }
113
- }
114
- }
115
-
116
- // User Administration Endpoints
117
- async login(credentials: I_UserLogin): Promise<I_LoginResponse | null> {
118
- const response = await this.request<I_LoginResponse>('/users/login', 'POST', credentials, false);
119
- if (response.token) {
120
- this.authToken = response.token;
121
-
122
- // Reconnect websocket with new token if realtime sync is enabled
123
- if (this.realTimeSync) {
124
- this.initWebSocket();
125
- }
126
-
127
- return {token: response.token, isAdmin: response.isAdmin, userName: response.userName};
128
- }
129
- return null;
130
- }
131
-
132
- async listUsers(): Promise<I_UserEntry[]> {
133
- return await this.request<I_UserEntry[]>('/users/list', 'GET');
134
- }
135
-
136
- async updateUser(userID: string, userData: I_UserUpdate): Promise<void> {
137
- await this.request<void>(`/users/update/${userID}`, 'PATCH', userData);
138
- }
139
-
140
- async createUser(userData: I_UserCreation): Promise<I_UserDisplay> {
141
- return await this.request<I_UserDisplay>('/users/create', 'POST', userData);
142
- }
143
-
144
- async removeUser(userID: string): Promise<void> {
145
- await this.request<void>(`/users/remove/${userID}`, 'DELETE');
146
- }
147
-
148
- // Document Management Endpoints
149
- async createDocument(document: I_DocumentEntry): Promise<I_DocumentEntry> {
150
- return await this.request<I_DocumentEntry>('/docs/create', 'POST', document);
151
- }
152
-
153
- async listDocuments(): Promise<I_DocumentEntry[]> {
154
- return await this.request<I_DocumentEntry[]>('/docs/list', 'GET');
155
- }
156
-
157
- async fetchDocuments(queryObject: I_DocumentQuery): Promise<I_DocumentEntry[]> {
158
- return await this.request<I_DocumentEntry[]>(`/docs/fetch/`, 'POST', queryObject);
159
- }
160
-
161
- async updateDocument(documentID: string, documentData: I_DocumentEntry): Promise<void> {
162
- await this.request<void>(`/docs/update/${documentID}`, 'PATCH', documentData);
163
- }
164
-
165
- async removeDocument(documentID: string): Promise<void> {
166
- await this.request<void>(`/docs/remove/${documentID}`, 'DELETE');
167
- }
168
-
169
- // Data Structure Endpoints
170
- async createStructure(structure: I_StructureCreation): Promise<I_DataStructure> {
171
- return await this.request<I_DataStructure>('/structures/create', 'POST', structure);
172
- }
173
-
174
- async getStructures(): Promise<I_DataStructure[]> {
175
- return await this.request<I_DataStructure[]>('/structures/list', 'GET');
176
- }
177
-
178
- async updateStructure(structureID: string, structureData: I_DataStructure): Promise<void> {
179
- await this.request<void>(`/structures/update/${structureID}`, 'PATCH', structureData);
180
- }
181
-
182
- async removeStructure(structureID: string): Promise<void> {
183
- await this.request<void>(`/structures/remove/${structureID}`, 'DELETE');
184
- }
185
-
186
- // Data Type Endpoints
187
- async createType(type: I_DocumentType): Promise<I_DocumentType> {
188
- return await this.request<I_DocumentType>('/types/write', 'POST', type);
189
- }
190
-
191
- async removeType(typeID: string) {
192
- return await this.request<void>(`/types/remove/${typeID}`, 'DELETE');
193
- }
194
-
195
- async getTypes(): Promise<I_DocumentType[]> {
196
- return await this.request<I_DocumentType[]>('/types/list', 'GET');
197
- }
198
-
199
- async updateType(updatedType: I_DocumentType): Promise<void> {
200
- await this.request<void>(`/types/write`, 'POST', updatedType);
201
- }
202
-
203
- setToken(token: string | null): void {
204
- console.log("Setting token to:", token ? "***token***" : "null");
205
-
206
- const tokenChanged = this.authToken !== token;
207
- this.authToken = token;
208
-
209
- if (!tokenChanged) {
210
- console.log("Token unchanged, no need to reconnect");
211
- return;
212
- }
213
-
214
- // If we have a new token and realtime sync is enabled
215
- if (token && this.realTimeSync) {
216
- console.log("New token set, will initialize WebSocket");
217
-
218
- // Ensure any existing connection is closed first
219
- if (this.socket.connected) {
220
- console.log("Disconnecting existing socket before reconnecting with new token");
221
- this.socket.disconnect();
222
- }
223
-
224
- // Wait a moment for the disconnect to complete
225
- setTimeout(() => {
226
- console.log("Initializing WebSocket with new token");
227
- this.initWebSocket();
228
- }, 300);
229
- }
230
- // If token was cleared or realtime sync is disabled
231
- else if (this.socket.connected) {
232
- console.log("Token cleared or realtime sync disabled, disconnecting");
233
- this.socket.disconnect();
234
- }
235
- }
236
-
237
- getVersion() {
238
- return packetJson.version;
239
- }
240
-
241
- debugSocketConnection(): void {
242
- console.log("Socket connection debug info:");
243
- console.log("- Connected:", this.socket.connected);
244
- console.log("- Socket ID:", this.socket.id);
245
- console.log("- Auth token present:", !!this.authToken);
246
- console.log("- Connection in progress:", this.connectionInProgress);
247
- console.log("- Realtime sync enabled:", this.realTimeSync);
248
- console.log("- Socket options:", this.socket.io.opts);
249
-
250
- // Try to force reconnection
251
- if (!this.socket.connected && this.authToken && this.realTimeSync) {
252
- console.log("Attempting to force reconnection...");
253
- this.socket.auth = {token: this.authToken};
254
- this.socket.connect();
255
- }
256
- }
257
-
258
- /**
259
- * Sets up permanent socket listeners for the client.
260
- *
261
- * @private
262
- */
263
- private setupPermanentSocketListeners() {
264
- // These are permanent listeners that won't be removed
265
- this.socket.on('connect_error', (error) => {
266
- console.error('Socket connection error:', error.message);
267
- this.connectionInProgress = false;
268
- });
269
-
270
- this.socket.on('connect', () => {
271
- console.log('Socket connected successfully with ID:', this.socket.id);
272
- this.connectionInProgress = false;
273
- });
274
-
275
- this.socket.on('disconnect', (reason) => {
276
- console.log('Socket disconnected. Reason:', reason);
277
- this.connectionInProgress = false;
278
- });
279
-
280
- this.socket.on('error', (error) => {
281
- console.error('Socket error:', error);
282
- this.connectionInProgress = false;
283
- });
284
- }
285
-
286
- /**
287
- * Initializes the WebSocket connection with the server.
288
- *
289
- * @private
290
- */
291
- private initWebSocket() {
292
- console.log("initWebSocket called. Auth token present:", !!this.authToken,
293
- "Connection in progress:", this.connectionInProgress,
294
- "Socket connected:", this.socket.connected);
295
-
296
- if (!this.authToken) {
297
- console.log("Skipping WebSocket initialization: No auth token");
298
- return;
299
- }
300
-
301
- if (this.connectionInProgress) {
302
- console.log("Connection already in progress, skipping initialization");
303
- return;
304
- }
305
-
306
- if (this.socket.connected) {
307
- console.log("Socket already connected with ID:", this.socket.id);
308
- return;
309
- }
310
-
311
- this.connectionInProgress = true;
312
-
313
- try {
314
- console.log("Setting up WebSocket connection with token");
315
-
316
- // Update the auth token
317
- this.socket.auth = {token: this.authToken};
318
-
319
- // Remove any dynamic event listeners that might have been added
320
- this.socket.offAny();
321
-
322
- // Set up event handler for application events
323
- this.socket.onAny((event: I_EventString, data: I_WsMessage) => {
324
- if (event === "heartbeatPing") {
325
- console.log("Ping event received:", data);
326
- this.socket.emit("heartbeatPong", Date.now());
327
- } else if (this.callbackFunction) {
328
- this.callbackFunction(event, data);
329
- }
330
- });
331
-
332
- // Connect to the server
333
- console.log("Connecting socket with auth token");
334
- this.socket.connect();
335
-
336
- // Add a timeout to detect if connection is taking too long
337
- setTimeout(() => {
338
- if (this.connectionInProgress) {
339
- console.warn("Socket connection attempt timed out after 5 seconds");
340
- this.connectionInProgress = false;
341
-
342
- // If we're still not connected after the timeout, try again with polling
343
- if (!this.socket.connected) {
344
- console.log("Retrying connection with polling transport");
345
- this.socket.io.opts.transports = ['polling', 'websocket'];
346
- this.socket.connect();
347
- }
348
- }
349
- }, 5000);
350
- } catch (error) {
351
- console.error('Error in initWebSocket:', error);
352
- this.connectionInProgress = false;
353
- }
354
- }
355
-
356
- private async request<T>(endpoint: string, method: string, body?: any, requiresAuth: boolean = true): Promise<T> {
357
- const headers: HeadersInit = {
358
- 'Content-Type': 'application/json',
359
- };
360
-
361
- if (requiresAuth && this.authToken)
362
- headers['Authorization'] = `Bearer ${this.authToken}`;
363
- if (this.socket.id)
364
- headers['X-Socket-ID'] = this.socket.id;
365
-
366
- const options: RequestInit = {
367
- method,
368
- headers,
369
- body: body ? JSON.stringify(body) : undefined
370
- };
371
-
372
- const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
373
- const normalizedBaseUrl = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
374
- const url = `${normalizedBaseUrl}${normalizedEndpoint}`;
375
- const response = await fetch(url, options);
376
-
377
- if (!response.ok) {
378
- if (response.status === 401 || response.status === 403) {
379
- this.authToken = null;
380
- }
381
- throw new Error(`API error: ${response.status} ${response.statusText}`);
382
- }
383
-
384
- return await response.json() as T;
385
- }
386
- }
387
-
388
- // Common type definitions for both frontend and backend
389
-
390
- // User related types
391
- export interface I_UserEntry extends I_UserCreation {
392
- _id: string;
393
- }
394
-
395
- export interface I_UserLogin {
396
- name: string;
397
- password: string;
398
- }
399
-
400
- export interface I_UserCreation {
401
- name: string;
402
- password: string;
403
- email?: string;
404
- department: string;
405
- group: string;
406
- isAdmin: boolean;
407
- }
408
-
409
- export interface I_UserUpdate {
410
- _id?: string;
411
- name?: string;
412
- password?: string;
413
- email?: string;
414
- department?: string;
415
- group?: string;
416
- isAdmin?: boolean;
417
- }
418
-
419
- export interface I_UserDisplay {
420
- _id: string;
421
- username: string;
422
- department: string;
423
- group: string;
424
- email?: string;
425
- }
426
-
427
- export interface I_LoginResponse {
428
- token: string;
429
- isAdmin: boolean;
430
- userName: string;
431
- }
432
-
433
- // Document related types
434
- export interface I_DocumentEntry extends I_DocumentCreationOwned {
435
- _id: string;
436
- }
437
-
438
- export interface I_DocumentCreation {
439
- title: string;
440
- description?: string;
441
- type: number;
442
- subType: number;
443
- content: any;
444
- shareWithGroup: boolean;
445
- shareWithDepartment: boolean;
446
- }
447
-
448
-
449
- export interface I_DocumentCreationOwned extends I_DocumentCreation {
450
- owner: string;
451
- }
452
-
453
- export interface I_DocumentUpdate extends I_DocumentQuery {
454
- content?: any;
455
- description?: string;
456
- }
457
-
458
- export interface I_DocumentQuery {
459
- _id?: string;
460
- owner?: string;
461
- title?: string;
462
- type?: number;
463
- subType?: number;
464
- shareWithGroup?: boolean;
465
- shareWithDepartment?: boolean;
466
- }
467
-
468
-
469
- // Structure related types
470
- export interface I_DataStructure {
471
- _id?: string | undefined;
472
- name: string;
473
- description: string;
474
- fields: I_StructureField[];
475
- }
476
-
477
- export interface I_StructureField {
478
- name: string;
479
- type: string;
480
- items?: string;
481
- }
482
-
483
- export interface I_StructureEntry {
484
- _id?: string;
485
- name: string;
486
- description: string;
487
- fields: I_StructureField[];
488
- }
489
-
490
-
491
- export interface I_StructureCreation {
492
- name: string;
493
- description?: string;
494
- fields: I_StructureField[];
495
- }
496
-
497
- export interface I_StructureUpdate {
498
- _id?: string
499
- name?: string;
500
- description?: string;
501
- fields?: I_StructureField[];
502
- }
503
-
504
- // Document type related types
505
- export interface I_DocumentType {
506
- _id?: string;
507
- type: number;
508
- subType: number;
509
- name: string;
510
- description?: string;
511
- defaultStructureID?: string;
512
- }
513
-
514
- // WebSocket-related types
515
- export type I_EventString = 'heartbeatPong' | "heartbeatPing" | "newDocument" | "newStructure" |
516
- "newUser" | "newType" | "removedID" | "changedDocument" | "changedStructure" | "changedUser" | "changedType" |
517
- "removedUser" | "removedStructure" | "removedDocument" | "removedType";
518
-
519
- export interface I_WsMessage {
520
- newDocument?: I_DocumentEntry;
521
- newStructure?: I_StructureEntry;
522
- newUser?: I_UserEntry;
523
- removedID?: string;
524
- changedDocument?: I_DocumentUpdate;
525
- changedStructure?: I_StructureUpdate;
526
- changedUser?: I_UserUpdate;
527
- confirmSubscription?: boolean;
528
- confirmUnsubscription?: boolean;
529
- heartbeatPing?: number;
530
- heartbeatPong?: number;
531
- newType?: I_DocumentType;
532
- }
1
+ import {io, Socket} from "socket.io-client";
2
+ import packetJson from '../package.json'
3
+
4
+ /**
5
+ * Client for interacting with docPouch API.
6
+ */
7
+ export default class docPouchClient {
8
+ /**
9
+ * The base URL of the server.
10
+ *
11
+ * @type {string}
12
+ */
13
+ baseUrl: string;
14
+ /**
15
+ * Socket.IO socket instance for real-time communication with the server.
16
+ *
17
+ * @type {Socket}
18
+ */
19
+ socket: Socket;
20
+ /**
21
+ * Callback function to handle socket events.
22
+ *
23
+ * @type {(event: I_EventString, data: I_WsMessage) => void}
24
+ */
25
+ callbackFunction;
26
+ /**
27
+ * Flag indicating whether real-time synchronization is enabled.
28
+ *
29
+ * @type {boolean}
30
+ */
31
+ realTimeSync: boolean = false;
32
+ /**
33
+ * Authentication token used to authorize requests.
34
+ *
35
+ * @private
36
+ * @type {string | null}
37
+ */
38
+ private authToken: string | null = null;
39
+ /**
40
+ * Flag indicating whether a connection attempt is in progress.
41
+ *
42
+ * @private
43
+ * @type {boolean}
44
+ */
45
+ private connectionInProgress = false;
46
+
47
+ /**
48
+ * Creates an instance of docPouchClient.
49
+ *
50
+ * @param {string} host - The base URL for the server.
51
+ * @param {number} [port=80] - The port number to connect to (default is 80).
52
+ * @param {(event: I_EventString, data: I_WsMessage) => void} [callback] - Optional callback function for socket events.
53
+ */
54
+ constructor(host: string, port: number = 80, callback?: (event: I_EventString, data: I_WsMessage) => void) {
55
+ this.baseUrl = host;
56
+ const socketUrl = host.includes('://') ? host : `https://${host}`;
57
+ const socketUrlWithPort = socketUrl.includes(':') && !socketUrl.endsWith(':')
58
+ ? socketUrl
59
+ : `${socketUrl}:${port}`;
60
+
61
+ console.log(`Initializing Socket.IO with URL: ${socketUrlWithPort}, path: /socket.io`);
62
+
63
+ this.socket = io(`${socketUrlWithPort}`, {
64
+ autoConnect: false,
65
+ transports: ['websocket'], // Try websocket only first
66
+ reconnection: true,
67
+ reconnectionAttempts: 5,
68
+ reconnectionDelay: 1000,
69
+ forceNew: true, // Force a new connection
70
+ auth: {
71
+ token: null // Will be set later
72
+ },
73
+ path: '/socket.io'
74
+ });
75
+
76
+ this.callbackFunction = callback;
77
+
78
+ this.setupPermanentSocketListeners();
79
+ }
80
+
81
+ /**
82
+ * Sets the real-time synchronization status.
83
+ *
84
+ * @param {boolean} newRealTimeSync - The new real-time sync setting (true/false).
85
+ */
86
+ setRealTimeSync(newRealTimeSync: boolean) {
87
+ console.log(`Setting realtime sync to: ${newRealTimeSync}. Current setting: ${this.realTimeSync}`);
88
+
89
+ // Skip if the setting isn't changing
90
+ if (newRealTimeSync === this.realTimeSync) {
91
+ console.log("Realtime sync setting unchanged, skipping");
92
+ return;
93
+ }
94
+
95
+ this.realTimeSync = newRealTimeSync;
96
+
97
+ if (newRealTimeSync && this.authToken) {
98
+ console.log("Activating realtime updates");
99
+
100
+ // Ensure we're not in the middle of another connection attempt
101
+ if (this.connectionInProgress) {
102
+ console.log("Connection already in progress, waiting before initializing");
103
+ setTimeout(() => this.initWebSocket(), 500);
104
+ } else {
105
+ this.initWebSocket();
106
+ }
107
+ } else if (!newRealTimeSync) {
108
+ console.log("Deactivating realtime updates");
109
+ if (this.socket.connected) {
110
+ console.log("Disconnecting socket");
111
+ this.socket.disconnect();
112
+ }
113
+ }
114
+ }
115
+
116
+ // User Administration Endpoints
117
+ /**
118
+ * Authenticates a user and stores the returned token for subsequent requests.
119
+ *
120
+ * @param {I_UserLogin} credentials - Username and password credentials.
121
+ * @returns {Promise<I_LoginResponse | null>} Login payload when successful, otherwise null.
122
+ */
123
+ async login(credentials: I_UserLogin): Promise<I_LoginResponse | null> {
124
+ const response = await this.request<I_LoginResponse>('/users/login', 'POST', credentials, false);
125
+ if (response.token) {
126
+ this.authToken = response.token;
127
+
128
+ // Reconnect websocket with new token if realtime sync is enabled
129
+ if (this.realTimeSync) {
130
+ this.initWebSocket();
131
+ }
132
+
133
+ return {token: response.token, isAdmin: response.isAdmin, userName: response.userName};
134
+ }
135
+ return null;
136
+ }
137
+
138
+ /**
139
+ * Retrieves all users visible to the authenticated user.
140
+ *
141
+ * @returns {Promise<I_UserEntry[]>} A list of user entries.
142
+ */
143
+ async listUsers(): Promise<I_UserEntry[]> {
144
+ return await this.request<I_UserEntry[]>('/users/list', 'GET');
145
+ }
146
+
147
+ /**
148
+ * Updates a user by ID.
149
+ *
150
+ * @param {string} userID - The ID of the user to update.
151
+ * @param {I_UserUpdate} userData - Partial user fields to update.
152
+ * @returns {Promise<void>}
153
+ */
154
+ async updateUser(userID: string, userData: I_UserUpdate): Promise<void> {
155
+ await this.request<void>(`/users/update/${userID}`, 'PATCH', userData);
156
+ }
157
+
158
+ /**
159
+ * Creates a new user.
160
+ *
161
+ * @param {I_UserCreation} userData - Data used to create the user.
162
+ * @returns {Promise<I_UserDisplay>} The created user payload returned by the API.
163
+ */
164
+ async createUser(userData: I_UserCreation): Promise<I_UserDisplay> {
165
+ return await this.request<I_UserDisplay>('/users/create', 'POST', userData);
166
+ }
167
+
168
+ /**
169
+ * Removes a user by ID.
170
+ *
171
+ * @param {string} userID - The ID of the user to remove.
172
+ * @returns {Promise<void>}
173
+ */
174
+ async removeUser(userID: string): Promise<void> {
175
+ await this.request<void>(`/users/remove/${userID}`, 'DELETE');
176
+ }
177
+
178
+ // Document Management Endpoints
179
+ /**
180
+ * Creates a new document.
181
+ *
182
+ * @param {I_DocumentEntry} document - The document payload to create.
183
+ * @returns {Promise<I_DocumentEntry>} The created document.
184
+ */
185
+ async createDocument(document: I_DocumentEntry): Promise<I_DocumentEntry> {
186
+ return await this.request<I_DocumentEntry>('/docs/create', 'POST', document);
187
+ }
188
+
189
+ /**
190
+ * Retrieves all documents visible to the authenticated user.
191
+ *
192
+ * @returns {Promise<I_DocumentEntry[]>} A list of document entries.
193
+ */
194
+ async listDocuments(): Promise<I_DocumentEntry[]> {
195
+ return await this.request<I_DocumentEntry[]>('/docs/list', 'GET');
196
+ }
197
+
198
+ /**
199
+ * Fetches documents matching a query object.
200
+ *
201
+ * @param {I_DocumentQuery} queryObject - Query fields used for filtering.
202
+ * @returns {Promise<I_DocumentEntry[]>} Matching documents.
203
+ */
204
+ async fetchDocuments(queryObject: I_DocumentQuery): Promise<I_DocumentEntry[]> {
205
+ return await this.request<I_DocumentEntry[]>(`/docs/fetch/`, 'POST', queryObject);
206
+ }
207
+
208
+ /**
209
+ * Updates a document by ID.
210
+ *
211
+ * @param {string} documentID - The ID of the document to update.
212
+ * @param {I_DocumentEntry} documentData - Updated document payload.
213
+ * @returns {Promise<void>}
214
+ */
215
+ async updateDocument(documentID: string, documentData: I_DocumentEntry): Promise<void> {
216
+ await this.request<void>(`/docs/update/${documentID}`, 'PATCH', documentData);
217
+ }
218
+
219
+ /**
220
+ * Removes a document by ID.
221
+ *
222
+ * @param {string} documentID - The ID of the document to remove.
223
+ * @returns {Promise<void>}
224
+ */
225
+ async removeDocument(documentID: string): Promise<void> {
226
+ await this.request<void>(`/docs/remove/${documentID}`, 'DELETE');
227
+ }
228
+
229
+ // Data Structure Endpoints
230
+ /**
231
+ * Creates a new data structure.
232
+ *
233
+ * @param {I_StructureCreation} structure - Data structure payload to create.
234
+ * @returns {Promise<I_DataStructure>} The created data structure.
235
+ */
236
+ async createStructure(structure: I_StructureCreation): Promise<I_DataStructure> {
237
+ return await this.request<I_DataStructure>('/structures/create', 'POST', structure);
238
+ }
239
+
240
+ /**
241
+ * Retrieves all data structures.
242
+ *
243
+ * @returns {Promise<I_DataStructure[]>} A list of data structures.
244
+ */
245
+ async getStructures(): Promise<I_DataStructure[]> {
246
+ return await this.request<I_DataStructure[]>('/structures/list', 'GET');
247
+ }
248
+
249
+ /**
250
+ * Updates a data structure by ID.
251
+ *
252
+ * @param {string} structureID - The ID of the structure to update.
253
+ * @param {I_DataStructure} structureData - Updated structure payload.
254
+ * @returns {Promise<void>}
255
+ */
256
+ async updateStructure(structureID: string, structureData: I_DataStructure): Promise<void> {
257
+ await this.request<void>(`/structures/update/${structureID}`, 'PATCH', structureData);
258
+ }
259
+
260
+ /**
261
+ * Removes a data structure by ID.
262
+ *
263
+ * @param {string} structureID - The ID of the structure to remove.
264
+ * @returns {Promise<void>}
265
+ */
266
+ async removeStructure(structureID: string): Promise<void> {
267
+ await this.request<void>(`/structures/remove/${structureID}`, 'DELETE');
268
+ }
269
+
270
+ // Data Type Endpoints
271
+ /**
272
+ * Creates or writes a document type.
273
+ *
274
+ * @param {I_DocumentType} type - The type payload.
275
+ * @returns {Promise<I_DocumentType>} The created or updated type.
276
+ */
277
+ async createType(type: I_DocumentType): Promise<I_DocumentType> {
278
+ return await this.request<I_DocumentType>('/types/write', 'POST', type);
279
+ }
280
+
281
+ /**
282
+ * Removes a document type by ID.
283
+ *
284
+ * @param {string} typeID - The ID of the type to remove.
285
+ * @returns {Promise<void>}
286
+ */
287
+ async removeType(typeID: string) {
288
+ return await this.request<void>(`/types/remove/${typeID}`, 'DELETE');
289
+ }
290
+
291
+ /**
292
+ * Retrieves all document types.
293
+ *
294
+ * @returns {Promise<I_DocumentType[]>} A list of document types.
295
+ */
296
+ async getTypes(): Promise<I_DocumentType[]> {
297
+ return await this.request<I_DocumentType[]>('/types/list', 'GET');
298
+ }
299
+
300
+ /**
301
+ * Updates a document type.
302
+ *
303
+ * @param {I_DocumentType} updatedType - The full type payload to persist.
304
+ * @returns {Promise<void>}
305
+ */
306
+ async updateType(updatedType: I_DocumentType): Promise<void> {
307
+ await this.request<void>(`/types/write`, 'POST', updatedType);
308
+ }
309
+
310
+ /**
311
+ * Sets or clears the authentication token used for API and WebSocket auth.
312
+ *
313
+ * @param {string | null} token - Bearer token to use, or null to clear it.
314
+ */
315
+ setToken(token: string | null): void {
316
+ console.log("Setting token to:", token ? "***token***" : "null");
317
+
318
+ const tokenChanged = this.authToken !== token;
319
+ this.authToken = token;
320
+
321
+ if (!tokenChanged) {
322
+ console.log("Token unchanged, no need to reconnect");
323
+ return;
324
+ }
325
+
326
+ // If we have a new token and realtime sync is enabled
327
+ if (token && this.realTimeSync) {
328
+ console.log("New token set, will initialize WebSocket");
329
+
330
+ // Ensure any existing connection is closed first
331
+ if (this.socket.connected) {
332
+ console.log("Disconnecting existing socket before reconnecting with new token");
333
+ this.socket.disconnect();
334
+ }
335
+
336
+ // Wait a moment for the disconnect to complete
337
+ setTimeout(() => {
338
+ console.log("Initializing WebSocket with new token");
339
+ this.initWebSocket();
340
+ }, 300);
341
+ }
342
+ // If token was cleared or realtime sync is disabled
343
+ else if (this.socket.connected) {
344
+ console.log("Token cleared or realtime sync disabled, disconnecting");
345
+ this.socket.disconnect();
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Returns the package version of this client.
351
+ *
352
+ * @returns {string} The semantic version string.
353
+ */
354
+ getVersion() {
355
+ return packetJson.version;
356
+ }
357
+
358
+ /**
359
+ * Logs socket diagnostics and attempts a reconnect when possible.
360
+ *
361
+ * @returns {void}
362
+ */
363
+ debugSocketConnection(): void {
364
+ console.log("Socket connection debug info:");
365
+ console.log("- Connected:", this.socket.connected);
366
+ console.log("- Socket ID:", this.socket.id);
367
+ console.log("- Auth token present:", !!this.authToken);
368
+ console.log("- Connection in progress:", this.connectionInProgress);
369
+ console.log("- Realtime sync enabled:", this.realTimeSync);
370
+ console.log("- Socket options:", this.socket.io.opts);
371
+
372
+ // Try to force reconnection
373
+ if (!this.socket.connected && this.authToken && this.realTimeSync) {
374
+ console.log("Attempting to force reconnection...");
375
+ this.socket.auth = {token: this.authToken};
376
+ this.socket.connect();
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Sets up permanent socket listeners for the client.
382
+ *
383
+ * @private
384
+ */
385
+ private setupPermanentSocketListeners() {
386
+ // These are permanent listeners that won't be removed
387
+ this.socket.on('connect_error', (error) => {
388
+ console.error('Socket connection error:', error.message);
389
+ this.connectionInProgress = false;
390
+ });
391
+
392
+ this.socket.on('connect', () => {
393
+ console.log('Socket connected successfully with ID:', this.socket.id);
394
+ this.connectionInProgress = false;
395
+ });
396
+
397
+ this.socket.on('disconnect', (reason) => {
398
+ console.log('Socket disconnected. Reason:', reason);
399
+ this.connectionInProgress = false;
400
+ });
401
+
402
+ this.socket.on('error', (error) => {
403
+ console.error('Socket error:', error);
404
+ this.connectionInProgress = false;
405
+ });
406
+ }
407
+
408
+ /**
409
+ * Initializes the WebSocket connection with the server.
410
+ *
411
+ * @private
412
+ */
413
+ private initWebSocket() {
414
+ console.log("initWebSocket called. Auth token present:", !!this.authToken,
415
+ "Connection in progress:", this.connectionInProgress,
416
+ "Socket connected:", this.socket.connected);
417
+
418
+ if (!this.authToken) {
419
+ console.log("Skipping WebSocket initialization: No auth token");
420
+ return;
421
+ }
422
+
423
+ if (this.connectionInProgress) {
424
+ console.log("Connection already in progress, skipping initialization");
425
+ return;
426
+ }
427
+
428
+ if (this.socket.connected) {
429
+ console.log("Socket already connected with ID:", this.socket.id);
430
+ return;
431
+ }
432
+
433
+ this.connectionInProgress = true;
434
+
435
+ try {
436
+ console.log("Setting up WebSocket connection with token");
437
+
438
+ // Update the auth token
439
+ this.socket.auth = {token: this.authToken};
440
+
441
+ // Remove any dynamic event listeners that might have been added
442
+ this.socket.offAny();
443
+
444
+ // Set up event handler for application events
445
+ this.socket.onAny((event: I_EventString, data: I_WsMessage) => {
446
+ if (event === "heartbeatPing") {
447
+ console.log("Ping event received:", data);
448
+ this.socket.emit("heartbeatPong", Date.now());
449
+ } else if (this.callbackFunction) {
450
+ this.callbackFunction(event, data);
451
+ }
452
+ });
453
+
454
+ // Connect to the server
455
+ console.log("Connecting socket with auth token");
456
+ this.socket.connect();
457
+
458
+ // Add a timeout to detect if connection is taking too long
459
+ setTimeout(() => {
460
+ if (this.connectionInProgress) {
461
+ console.warn("Socket connection attempt timed out after 5 seconds");
462
+ this.connectionInProgress = false;
463
+
464
+ // If we're still not connected after the timeout, try again with polling
465
+ if (!this.socket.connected) {
466
+ console.log("Retrying connection with polling transport");
467
+ this.socket.io.opts.transports = ['polling', 'websocket'];
468
+ this.socket.connect();
469
+ }
470
+ }
471
+ }, 5000);
472
+ } catch (error) {
473
+ console.error('Error in initWebSocket:', error);
474
+ this.connectionInProgress = false;
475
+ }
476
+ }
477
+
478
+ /**
479
+ * Sends an HTTP request to the configured docPouch backend.
480
+ *
481
+ * @template T
482
+ * @param {string} endpoint - Relative API endpoint (with or without leading slash).
483
+ * @param {string} method - HTTP method.
484
+ * @param {any} [body] - Optional JSON body.
485
+ * @param {boolean} [requiresAuth=true] - Whether the Authorization header should be attached.
486
+ * @returns {Promise<T>} Parsed JSON response body.
487
+ * @private
488
+ */
489
+ private async request<T>(endpoint: string, method: string, body?: any, requiresAuth: boolean = true): Promise<T> {
490
+ const headers: HeadersInit = {
491
+ 'Content-Type': 'application/json',
492
+ };
493
+
494
+ if (requiresAuth && this.authToken)
495
+ headers['Authorization'] = `Bearer ${this.authToken}`;
496
+ if (this.socket.id)
497
+ headers['X-Socket-ID'] = this.socket.id;
498
+
499
+ const options: RequestInit = {
500
+ method,
501
+ headers,
502
+ body: body ? JSON.stringify(body) : undefined
503
+ };
504
+
505
+ const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
506
+ const normalizedBaseUrl = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
507
+ const url = `${normalizedBaseUrl}${normalizedEndpoint}`;
508
+ const response = await fetch(url, options);
509
+
510
+ if (!response.ok) {
511
+ if (response.status === 401 || response.status === 403) {
512
+ this.authToken = null;
513
+ }
514
+ throw new Error(`API error: ${response.status} ${response.statusText}`);
515
+ }
516
+
517
+ return await response.json() as T;
518
+ }
519
+ }
520
+
521
+ // Common type definitions for both frontend and backend
522
+
523
+ // User related types
524
+ export interface I_UserEntry extends I_UserCreation {
525
+ _id: string;
526
+ }
527
+
528
+ export interface I_UserLogin {
529
+ name: string;
530
+ password: string;
531
+ }
532
+
533
+ export interface I_UserCreation {
534
+ name: string;
535
+ password: string;
536
+ email?: string;
537
+ department: string;
538
+ group: string;
539
+ isAdmin: boolean;
540
+ }
541
+
542
+ export interface I_UserUpdate {
543
+ _id?: string;
544
+ name?: string;
545
+ password?: string;
546
+ email?: string;
547
+ department?: string;
548
+ group?: string;
549
+ isAdmin?: boolean;
550
+ }
551
+
552
+ export interface I_UserDisplay {
553
+ _id: string;
554
+ username: string;
555
+ department: string;
556
+ group: string;
557
+ email?: string;
558
+ }
559
+
560
+ export interface I_LoginResponse {
561
+ token: string;
562
+ isAdmin: boolean;
563
+ userName: string;
564
+ }
565
+
566
+ // Document related types
567
+ export interface I_DocumentEntry extends I_DocumentCreationOwned {
568
+ _id: string;
569
+ }
570
+
571
+ export interface I_DocumentCreation {
572
+ title: string;
573
+ description?: string;
574
+ type: number;
575
+ subType: number;
576
+ content: any;
577
+ shareWithGroup: boolean;
578
+ shareWithDepartment: boolean;
579
+ }
580
+
581
+
582
+ export interface I_DocumentCreationOwned extends I_DocumentCreation {
583
+ owner: string;
584
+ }
585
+
586
+ export interface I_DocumentUpdate extends I_DocumentQuery {
587
+ content?: any;
588
+ description?: string;
589
+ }
590
+
591
+ export interface I_DocumentQuery {
592
+ _id?: string;
593
+ owner?: string;
594
+ title?: string;
595
+ type?: number;
596
+ subType?: number;
597
+ shareWithGroup?: boolean;
598
+ shareWithDepartment?: boolean;
599
+ }
600
+
601
+
602
+ // Structure related types
603
+ export interface I_DataStructure {
604
+ _id?: string | undefined;
605
+ name: string;
606
+ description: string;
607
+ fields: I_StructureField[];
608
+ }
609
+
610
+ export interface I_StructureField {
611
+ name: string;
612
+ type: string;
613
+ items?: string;
614
+ }
615
+
616
+ export interface I_StructureEntry {
617
+ _id?: string;
618
+ name: string;
619
+ description: string;
620
+ fields: I_StructureField[];
621
+ }
622
+
623
+
624
+ export interface I_StructureCreation {
625
+ name: string;
626
+ description?: string;
627
+ fields: I_StructureField[];
628
+ }
629
+
630
+ export interface I_StructureUpdate {
631
+ _id?: string
632
+ name?: string;
633
+ description?: string;
634
+ fields?: I_StructureField[];
635
+ }
636
+
637
+ // Document type related types
638
+ export interface I_DocumentType {
639
+ _id?: string;
640
+ type: number;
641
+ subType: number;
642
+ name: string;
643
+ description?: string;
644
+ defaultStructureID?: string;
645
+ }
646
+
647
+ // WebSocket-related types
648
+ export type I_EventString = 'heartbeatPong' | "heartbeatPing" | "newDocument" | "newStructure" |
649
+ "newUser" | "newType" | "removedID" | "changedDocument" | "changedStructure" | "changedUser" | "changedType" |
650
+ "removedUser" | "removedStructure" | "removedDocument" | "removedType";
651
+
652
+ export interface I_WsMessage {
653
+ newDocument?: I_DocumentEntry;
654
+ newStructure?: I_StructureEntry;
655
+ newUser?: I_UserEntry;
656
+ removedID?: string;
657
+ changedDocument?: I_DocumentUpdate;
658
+ changedStructure?: I_StructureUpdate;
659
+ changedUser?: I_UserUpdate;
660
+ confirmSubscription?: boolean;
661
+ confirmUnsubscription?: boolean;
662
+ heartbeatPing?: number;
663
+ heartbeatPong?: number;
664
+ newType?: I_DocumentType;
665
+ }