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 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
- export default class Index {
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
- private token;
13
+ /**
14
+ * Socket.IO socket instance for real-time communication with the server.
15
+ *
16
+ * @type {Socket}
17
+ */
6
18
  socket: Socket;
7
- constructor(baseUrl: string, callback?: (event: I_EventString, data: I_WsMessage) => void);
8
- private request;
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
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const socket_io_client_1 = require("socket.io-client");
7
- const package_json_1 = __importDefault(require("../package.json"));
8
- class Index {
9
- constructor(baseUrl, callback) {
10
- this.token = null;
11
- this.baseUrl = baseUrl;
12
- this.socket = (0, socket_io_client_1.io)(baseUrl, { autoConnect: false });
13
- if (callback) {
14
- this.socket.connect();
15
- this.socket.onAny((event, data) => {
16
- if (event === "heartbeatPing") {
17
- console.log("Ping event received:", data);
18
- this.socket.emit("heartbeatPong", Date.now());
19
- }
20
- else {
21
- callback(event, data);
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
- async request(endpoint, method, body, requiresAuth = true) {
27
- const headers = {
28
- 'Content-Type': 'application/json'
29
- };
30
- if (requiresAuth && this.token) {
31
- headers['Authorization'] = `Bearer ${this.token}`;
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
- const options = {
34
- method,
35
- headers,
36
- body: body ? JSON.stringify(body) : undefined
37
- };
38
- const response = await fetch(`${this.baseUrl}${endpoint}`, options);
39
- if (!response.ok) {
40
- if (response.status === 401 || response.status === 403) {
41
- this.token = null;
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.token = response.token;
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
- setToken(token) {
98
- this.token = token;
142
+ // Data Type Endpoints
143
+ async createType(type) {
144
+ return await this.request('/types/write', 'PATCH', type);
99
145
  }
100
- getToken() {
101
- return this.token;
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 package_json_1.default.version;
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 type I_EventString = 'subscribe' | 'unsubscribe' | 'heartbeatPong' | "heartbeatPing" | "newDocument" | "newStructure" | "newUser" | "removedID" | "changedDocument" | "changedStructure" | "changedUser" | "confirmSubscription" | "confirmUnsubscription";
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
@@ -1,3 +1,2 @@
1
- "use strict";
2
1
  // Common type definitions for both frontend and backend
3
- Object.defineProperty(exports, "__esModule", { value: true });
2
+ export {};
package/package.json CHANGED
@@ -1,8 +1,15 @@
1
1
  {
2
2
  "name": "docpouch-client",
3
- "version": "0.8.3",
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/node": "^22.15.24",
19
- "socket.io-client": "^4.8.1",
20
- "jest": "^29.7.0",
21
- "ts-jest": "^29.3.4",
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 Index from '../index.js';
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: 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 Index(baseUrl);
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 Index(baseUrl, mockCallback);
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 Index(baseUrl);
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 Index(baseUrl, mockCallback);
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 Index(baseUrl, mockCallback);
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
- I_LoginResponse, I_DocumentQuery, I_StructureCreation, I_WsMessage, I_EventString
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
- export default class Index {
15
- baseUrl: string;
16
- private token: string | null = null;
17
- socket: Socket
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
- constructor(baseUrl: string, callback?: (event: I_EventString, data: I_WsMessage) => void) {
20
- this.baseUrl = baseUrl;
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
- this.socket = io(baseUrl, { autoConnect: false });
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
- private async request<T>(endpoint: string, method: string, body?: any, requiresAuth: boolean = true): Promise<T> {
38
- const headers: HeadersInit = {
39
- 'Content-Type': 'application/json'
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
- if (requiresAuth && this.token) {
43
- headers['Authorization'] = `Bearer ${this.token}`;
44
- }
86
+ this.callbackFunction = callback;
45
87
 
46
- const options: RequestInit = {
47
- method,
48
- headers,
49
- body: body ? JSON.stringify(body) : undefined
50
- };
88
+ this.setupPermanentSocketListeners();
89
+ }
51
90
 
52
- const response = await fetch(`${this.baseUrl}${endpoint}`, options);
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
- if (!response.ok) {
55
- if (response.status === 401 || response.status === 403) {
56
- this.token = null;
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
- // User Administration Endpoints
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
- async listUsers(): Promise<I_UserEntry[]> {
75
- return await this.request<I_UserEntry[]>('/users/list', 'GET');
76
- }
107
+ if (newRealTimeSync && this.authToken) {
108
+ console.log("Activating realtime updates");
77
109
 
78
- async updateUser(userID: string, userData: I_UserUpdate): Promise<void> {
79
- await this.request<void>(`/users/update/${userID}`, 'PATCH', userData);
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
- async createUser(userData: I_UserCreation): Promise<I_UserDisplay> {
83
- return await this.request<I_UserDisplay>('/users/create', 'POST', userData);
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
- async removeUser(userID: string): Promise<void> {
87
- await this.request<void>(`/users/remove/${userID}`, 'DELETE');
88
- }
132
+ // Reconnect websocket with new token if realtime sync is enabled
133
+ if (this.realTimeSync) {
134
+ this.initWebSocket();
135
+ }
89
136
 
90
- // Document Management Endpoints
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
- async listDocuments(): Promise<I_DocumentEntry[]> {
96
- return await this.request<I_DocumentEntry[]>('/docs/list', 'GET');
97
- }
142
+ async listUsers(): Promise<I_UserEntry[]> {
143
+ return await this.request<I_UserEntry[]>('/users/list', 'GET');
144
+ }
98
145
 
99
- async fetchDocument(queryObject: I_DocumentQuery): Promise<I_DocumentEntry[]> {
100
- return await this.request<I_DocumentEntry[]>(`/docs/fetch/`, 'POST', queryObject);
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
- async updateDocument(documentID: string, documentData: I_DocumentEntry): Promise<void> {
104
- await this.request<void>(`/docs/update/${documentID}`, 'PATCH', documentData);
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
- async removeDocument(documentID: string): Promise<void> {
108
- await this.request<void>(`/docs/remove/${documentID}`, 'DELETE');
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
- // Data Structure Endpoints
112
- async createStructure(structure: I_StructureCreation): Promise<I_DataStructure> {
113
- return await this.request<I_DataStructure>('/structures/create', 'POST', structure);
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
- async getStructures(): Promise<I_DataStructure[]> {
117
- return await this.request<I_DataStructure[]>('/structures/list', 'GET');
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
- async updateStructure(structureID: string, structureData: I_DataStructure): Promise<void> {
121
- await this.request<void>(`/structures/update/${structureID}`, 'PATCH', structureData);
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
- async removeStructure(structureID: string): Promise<void> {
125
- await this.request<void>(`/structures/remove/${structureID}`, 'DELETE');
311
+ if (this.connectionInProgress) {
312
+ console.log("Connection already in progress, skipping initialization");
313
+ return;
126
314
  }
127
315
 
128
- setToken(token: string | null): void {
129
- this.token = token;
316
+ if (this.socket.connected) {
317
+ console.log("Socket already connected with ID:", this.socket.id);
318
+ return;
130
319
  }
131
320
 
132
- getToken() {
133
- return this.token;
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
- getVersion() {
137
- return packetJson.version;
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
- module.exports = Index;
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/**/*"]