@codenotch/codenotch.react 2.0.0 → 2.0.2

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/README.md DELETED
@@ -1,183 +0,0 @@
1
- # codenotch-react
2
-
3
- React bindings for [Codenotch](https://codenotch.com) applications.
4
-
5
- Codenotch is a full-stack development platform: a project combines server-side **BPMN processes**, **SQL tables** / **NoSQL documents**, **i18n** translations and **React** apps. This package is the client-side bridge between those React apps and the Codenotch runtime: it lets a component start BPMN processes, run CNQL queries, translate i18n keys, listen to real-time signals, and manage theme/language.
6
-
7
- > **Requirements** — Codenotch apps run on **React 19** (`react@^19`, `react-dom@^19`). The package uses the automatic JSX runtime and `createRoot`; all modern React APIs (hooks, `useId`, `use`, Actions…) are available.
8
-
9
- ## Quick start
10
-
11
- ```tsx
12
- import { useCodenotch } from 'codenotch-react';
13
-
14
- const MyApp = () => {
15
- const cn = useCodenotch();
16
-
17
- return <div className="p-4">
18
- <h1 className="text-xl font-bold">{cn.i18n('welcome')}</h1>
19
- </div>;
20
- };
21
-
22
- export default MyApp;
23
- ```
24
-
25
- `useCodenotch()` is a real React hook: the returned object is stable across renders and is replaced — re-rendering the component — whenever the environment changes (`setLanguage`, `setTheme`, `init`). Regular hook rules apply. The same API is available everywhere else through two non-hook entry points:
26
-
27
- | Where | Use |
28
- |-------|-----|
29
- | Function component / custom hook | `const cn = useCodenotch();` — re-renders on language/theme change. |
30
- | Class component | `export default withCodenotch(MyClass);` — injects `this.props.cn`, re-renders on change, forwards `ref`. |
31
- | Event handler, plain module, service, anywhere | `getCodenotch()` — plain function, never stale, no re-render. |
32
- | Manual subscription (rare) | `onCodenotchChange(listener)` → `IDisposable`. |
33
-
34
- ```tsx
35
- import React from 'react';
36
- import { withCodenotch, WithCodenotchProps } from 'codenotch-react';
37
-
38
- interface Props extends WithCodenotchProps {
39
- userId: string;
40
- }
41
-
42
- class TodoList extends React.Component<Props> {
43
- render() {
44
- return <h1>{this.props.cn.i18n('todos.title')}</h1>;
45
- }
46
- }
47
-
48
- export default withCodenotch(TodoList); // <TodoList userId="42" /> — `cn` is injected
49
- ```
50
-
51
- ```ts
52
- // services/todos.ts — outside React
53
- import { getCodenotch } from 'codenotch-react';
54
-
55
- export async function loadTodos(userId: string) {
56
- const result = await getCodenotch().startProcess('getTodos', 'start', { UserId: userId });
57
- return result.output.todos;
58
- }
59
- ```
60
-
61
- The environment (cluster URL, service name, language, translations…) is set up by `init(envVariables)`, which the Codenotch runtime calls automatically from the page hosting the application. You only call `init` yourself in custom hosting scenarios.
62
-
63
- ## API overview
64
-
65
- | Member | Description |
66
- |--------|-------------|
67
- | `cn.env` | Readonly environment: `clusterUrl`, `serviceName`, `tenantName`, `accessToken`, `language`, `theme`, `i18n`, `appManifest`, `projectManifest`. |
68
- | `cn.i18n(key, ...args)` | Translate a key for the current language, filling `{0}`, `{1}`, … placeholders. |
69
- | `cn.startProcess(name, startNodeId, inputs)` | Start a server-side BPMN process and await its result. |
70
- | `cn.requestCnql(cnql, verbose?)` | Run a CNQL query (XML, SELECT-only) against the project's tables. |
71
- | `cn.listenSignal(signalId, callback)` | Subscribe to real-time signals emitted by BPMN processes. |
72
- | `cn.showDialog(node)` | Render a React node in a fullscreen modal dialog (own React root, unmounted on `close()`). |
73
- | `cn.setTheme(t)` / `cn.getTheme()` | `'light' \| 'dark'`; `setTheme` also toggles the class on `<html>` (Tailwind `dark:`). |
74
- | `cn.setLanguage(l)` / `cn.getLanguage()` / `cn.getLanguages()` | Current language and the languages declared in `manifest.json`. |
75
- | `cn.getProjectFile(path)` / `cn.getProjectFileUrl(path)` | Read/link a file of the deployed project. |
76
- | `cn.getUrlParams()` | Query-string parameters of the current URL as a plain object. |
77
- | `cn.uuid()` | Random UUID v4. |
78
- | `cn.getProjectManifest()` / `cn.getAppManifest()` | Project / application manifests. |
79
-
80
- Full signatures and JSDoc live in `dist/index.d.ts` (sources in `src/`).
81
-
82
- ## Calling a BPMN process
83
-
84
- `processName` is the `.bpmn` file name without extension; `startNodeId` is the id of the start event to trigger (conventionally `'start'`); `inputs` are the input parameters declared by that start event. The result's `output` contains the parameters of the end event reached.
85
-
86
- ```tsx
87
- const result = await cn.startProcess('getTodos', 'start', { UserId: userId });
88
-
89
- if (!result.isError) {
90
- setTodos(result.output.todos);
91
- } else {
92
- console.error(result.errorMessage);
93
- }
94
- ```
95
-
96
- ## Querying tables with CNQL
97
-
98
- CNQL is Codenotch's XML query language over the project's SQL tables (SELECT only — writes go through BPMN processes). The root `xmlns` must be the project's `serviceName`; each queried table's `Ref` attribute names its result set:
99
-
100
- ```tsx
101
- const data = await cn.requestCnql(`
102
- <CNQL xmlns="myproject" PageSize="10" PageIndex="0">
103
- <Users Ref="results">
104
- <Id />
105
- <Email />
106
- <IsAdmin Equal="true" />
107
- </Users>
108
- </CNQL>`);
109
-
110
- console.log(data.results); // [{ Id: '...', Email: '...' }, ...]
111
- ```
112
-
113
- ## Translations (i18n)
114
-
115
- Translations come from the project's `.i18n.csv` files and are injected into `cn.env.i18n` at startup. `cn.i18n(key, ...args)` translates for the current language and replaces `{0}`, `{1}`, … placeholders; it never throws — an unknown key is returned as-is (with a console warning).
116
-
117
- ```tsx
118
- cn.i18n('welcome'); // "Bienvenue"
119
- cn.i18n('greeting', 'Ada', 3); // "Bonjour Ada, 3 messages" (from "Bonjour {0}, {1} messages")
120
- cn.setLanguage('en'); // switch language at runtime — every component using
121
- // useCodenotch() / withCodenotch() re-renders
122
- ```
123
-
124
- ## Real-time signals
125
-
126
- BPMN processes can broadcast signals; subscribe from the UI with `listenSignal`:
127
-
128
- ```tsx
129
- useEffect(() => {
130
- let sub: IDisposable | undefined;
131
- let cancelled = false;
132
- getCodenotch().listenSignal('todosChanged', (signal) => refresh(signal.data))
133
- .then(s => { if (cancelled) s.dispose(); else sub = s; });
134
- return () => { cancelled = true; sub?.dispose(); };
135
- }, []);
136
- ```
137
-
138
- Two details: the `cancelled` flag matters under React 19's StrictMode, which mounts / unmounts / remounts effects in development — without it the first subscription would leak when the cleanup runs before the promise resolves. And the effect uses `getCodenotch()` rather than the `cn` from `useCodenotch()`: the hook's object changes on every language/theme change, so listing it in the dependency array would re-subscribe on each change, while omitting it trips `react-hooks/exhaustive-deps`. `getCodenotch()` sidesteps both.
139
-
140
- ## Dialogs
141
-
142
- ```tsx
143
- const dialog = cn.showDialog(
144
- <div className="bg-white dark:bg-gray-800 p-6 rounded shadow">
145
- <p>{cn.i18n('confirm.message')}</p>
146
- <button onClick={() => dialog.close()}>{cn.i18n('close')}</button>
147
- </div>
148
- );
149
- ```
150
-
151
- The dialog stays open until you call `dialog.close()`, which unmounts the React tree and removes the `<dialog>` element. The content is rendered in its own React root, so it does not share context (providers) with the calling component.
152
-
153
- ## Type-safe processes and translation keys
154
-
155
- `startProcess` and `i18n` are typed through two registries, `ProcessRegistry` and `TranslationRegistry`, that the **Codenotch IDE fills by declaration merging**: it generates `typings/process.d.ts` and `typings/i18n.d.ts` in each project from the `.bpmn` and `.i18n.csv` files (never edit those files — refresh the project instead). The generated files look like:
156
-
157
- ```ts
158
- import 'codenotch-react';
159
-
160
- declare module 'codenotch-react' {
161
- interface TranslationRegistry {
162
- 'welcome': [];
163
- 'greeting': [arg1: any, arg2: any];
164
- }
165
-
166
- interface ProcessRegistry {
167
- 'getTodos': {
168
- nodes: { 'start': { UserId: string } };
169
- output: { todos: any[] };
170
- };
171
- }
172
- }
173
- ```
174
-
175
- With these in the compilation, keys, inputs and outputs are strictly checked and autocompleted. **Without them** (project compiled outside the IDE), the registries are empty and both methods gracefully fall back to plain `string` keys and untyped arguments — the code still compiles.
176
-
177
- ## Notes for AI assistants
178
-
179
- - The complete typed API surface is in `dist/index.d.ts`; the readable implementation ships in `src/` (entry point `src/index.ts`).
180
- - `useCodenotch()` is a real hook (hook rules apply; re-renders on language/theme change). In class components use `withCodenotch(Component)` and read `this.props.cn`; in event handlers, services, plain modules or effects use `getCodenotch()`. Never call `useCodenotch()` outside a function component / custom hook.
181
- - The object returned by `useCodenotch()` changes when the environment changes: fine in `useMemo` deps, but in `useEffect` prefer `getCodenotch()` unless the effect should re-run on language/theme change.
182
- - Never hand-edit a project's `typings/i18n.d.ts` / `typings/process.d.ts`: they are regenerated by the Codenotch IDE.
183
- - Codenotch apps are React 19 + Tailwind CSS (automatic JSX runtime — no `import React` needed for JSX); CNQL is SELECT-only (writes go through BPMN processes via `startProcess`).
@@ -1,86 +0,0 @@
1
- import { v4 } from "uuid";
2
- import { IProcessCallbacks } from "../models/Misc";
3
- import { IProcessResult } from "../models/Codenotch";
4
-
5
- /**
6
- * static class mainly containing a method to start processes
7
- */
8
- export default class ProcessUtils {
9
-
10
- public static async startProcess(subscribeFunc: (processInstanceId: string, callbacks: IProcessCallbacks) => Promise<void>, clusterUrl: string, tenantName: string, projectName: string, processId: string, processInput: any, processInstanceId?: string, startNodeId?: string, token?: string): Promise<IProcessResult>
11
- {
12
- let instanceId = processInstanceId ? processInstanceId : v4();
13
-
14
- return new Promise(async (resolve, reject) => {
15
-
16
- try
17
- {
18
- // Another class/component is responsible for listening to processes' states and calling back when they finish
19
- await subscribeFunc(instanceId, {
20
- onCallback: (callback) => {
21
- //TODO check redirect ?
22
- },
23
- onOver: (processResult) => {
24
- // The process has finished
25
- resolve(processResult);
26
- },
27
- })
28
-
29
- // Create a new process instance
30
- await ProcessUtils.launchMainProcess(clusterUrl, tenantName, projectName, processId, processInput, instanceId, startNodeId, token);
31
- }
32
- catch(err: any)
33
- {
34
- console.error(err);
35
-
36
- let processResult: IProcessResult = {
37
- isError: true,
38
- processInstanceId: instanceId,
39
- output: {},
40
- errorMessage: err.message
41
- }
42
-
43
- resolve(processResult);
44
- }
45
- });
46
- }
47
-
48
- private static async launchMainProcess(clusterUrl: string, tenantName: string, projectName: string, processId: string, processInput: any, processInstanceId: string, startNodeId?: string, token?: string) {
49
-
50
- console.log("launching main process");
51
-
52
- let input = processInput ? processInput : {};
53
-
54
- // We launch the process with an http request
55
-
56
- let url = `${clusterUrl}/${projectName.toLowerCase()}/processes/${processId}/instances`;
57
-
58
- let body = {
59
- id: processInstanceId,
60
- input: input,
61
- startNode: startNodeId
62
- }
63
-
64
- let requestHeaders = new Headers();
65
- requestHeaders.set('Content-Type', 'application/json');
66
-
67
- if(token && token !== "")
68
- {
69
- requestHeaders.set(`${tenantName}AccessToken`, token);
70
- }
71
-
72
- const response = await fetch(url, {
73
- method: 'POST',
74
- credentials: 'include', // forward auth cookiesbody
75
- body: JSON.stringify(body),
76
- headers: requestHeaders
77
- })
78
-
79
- if(!response.ok)
80
- {
81
- // Something went wrong
82
- let errorMessage = await response.text();
83
- throw new Error(errorMessage)
84
- }
85
- }
86
- }
@@ -1,375 +0,0 @@
1
- import { HttpTransportType, HubConnectionBuilder, HubConnectionState, LogLevel } from "@microsoft/signalr";
2
- import { v4 } from "uuid";
3
- import { IProcessCallbacks, ICodenotchSignal } from "../models/Misc";
4
- import { IProcessResult } from "../models/Codenotch";
5
-
6
-
7
-
8
- export class SignalR {
9
-
10
- private _connection: signalR.HubConnection | null;
11
- private _hubUrl: string;
12
- private _accessToken: string | undefined;
13
-
14
- // The callbacks coming from components when a signal is received
15
- // Also used on the root component to keep track of which signals we listen to
16
- private _signalCallbacks: {[signalId: string]: {[subscriptionId: string]: (signal: ICodenotchSignal) => void} };
17
-
18
- // Store callbacks of ongoing processes
19
- private _processCallbacks: {[processInstanceId: string]: IProcessCallbacks};
20
-
21
- private _inactivityTimer: NodeJS.Timeout | null;
22
- private _unsubscribeTimers: {[signalId: string]: NodeJS.Timeout} = {};
23
-
24
- private _isConnecting: boolean = false;
25
- private _pendingConnectionPromises: Array<{ resolve: () => void; reject: (error: Error) => void }> = [];
26
-
27
- // Determines if connection should be kept alive even if there are no subscriptions
28
- private _keepAlive: boolean;
29
-
30
- public constructor(clusterUrl: string, serviceName: string, keepAlive: boolean, accessToken?: string)
31
- {
32
- this._hubUrl = `${clusterUrl}/${serviceName}/exec`;
33
- this._accessToken = accessToken;
34
- this._connection = null;
35
- this._signalCallbacks = {};
36
- this._processCallbacks = {};
37
-
38
- this._inactivityTimer = null;
39
- this._keepAlive = keepAlive;
40
- }
41
-
42
- async connect() {
43
-
44
- console.log(`SignalR establishing connection...`)
45
-
46
- this._isConnecting = true;
47
-
48
- // Here we inject the access token if defined
49
- let options: signalR.IHttpConnectionOptions = this._accessToken ? { accessTokenFactory: () => this._accessToken! } : {};
50
-
51
- // Skip negotiation and only use websockets
52
- // This allows us to have multiple SignalR server without sticky sessions
53
- // But it won't work in the rare cases where websockets are not supported
54
- options.skipNegotiation = true;
55
- options.transport = HttpTransportType.WebSockets;
56
- //TODO if the server has multi instances, and the one we are connected fails, do sticky sessions prevent us from connecting to a healthy instance ?
57
-
58
- this._connection = new HubConnectionBuilder()
59
- .withUrl(this._hubUrl, options)
60
- .withAutomaticReconnect([0, 1000, 3000, 5000, 10000, 30000, 60000, 90000]) // If the server is a single instance and decide to change node, it might take a little while
61
- .configureLogging(LogLevel.Error)
62
- .build();
63
-
64
- this._connection.off("ReceiveSignal");
65
- this._connection.off("ReceiveLog");
66
- this._connection.off("ReceiveCallback");
67
- this._connection.off("ReceiveProcessOver");
68
-
69
- // On Signal
70
- this._connection.on("ReceiveSignal", (signal: ICodenotchSignal) => this.onSignalReceived(signal));
71
-
72
- // On log
73
- this._connection.on("ReceiveLog", (log: string) => {
74
- console.log(log);
75
- });
76
-
77
- // On callback received from process
78
- //TODO only used to set wec content and redirect, do we still need those ?
79
- this._connection.on("ReceiveCallback", (callback) => this.onProcessCallbackReceived(callback));
80
-
81
- // On process completed
82
- this._connection.on("ReceiveProcessOver", (processResult: IProcessResult) => this.onProcessOverReceived(processResult));
83
-
84
- this._connection.onclose(
85
- (error) => console.log(`disconnected: ${error ? error.message : "no error"}`)
86
- );
87
-
88
- this._connection.onreconnected(
89
- () => this.resolvePendingRequests()
90
- );
91
-
92
- try
93
- {
94
- await this._connection.start();
95
- console.log(`SignalR connection established`)
96
-
97
- this.resolvePendingRequests();
98
- this._isConnecting = false;
99
- }
100
- catch(err: any)
101
- {
102
- console.log(`SignalR connection error: ${err}`)
103
-
104
- this.rejectPendingRequests(err);
105
- this._isConnecting = false;
106
- }
107
- }
108
-
109
- private resolvePendingRequests()
110
- {
111
- if(this._pendingConnectionPromises.length > 0)
112
- {
113
- console.log(`Resolving ${this._pendingConnectionPromises.length} pending connection promises`);
114
- }
115
-
116
- for(let p of this._pendingConnectionPromises)
117
- {
118
- p?.resolve();
119
- }
120
- }
121
-
122
- private rejectPendingRequests(error: Error): void {
123
-
124
- if(this._pendingConnectionPromises.length > 0)
125
- {
126
- console.log(`Rejecting ${this._pendingConnectionPromises.length} pending connection promises`);
127
- }
128
-
129
- for(let p of this._pendingConnectionPromises)
130
- {
131
- p?.reject(error);
132
- }
133
- }
134
-
135
- private async ensureConnected() {
136
-
137
- if(this._inactivityTimer)
138
- {
139
- // cancel disconnection
140
- clearTimeout(this._inactivityTimer);
141
- this._inactivityTimer = null;
142
- }
143
-
144
- if(this._connection && this._connection.state === HubConnectionState.Connected)
145
- {
146
- return Promise.resolve();
147
- }
148
-
149
- console.log(`SignalR not currently connected, waiting on connection...`)
150
-
151
- const connectionPromise = new Promise<void>((resolve, reject) => {
152
- this._pendingConnectionPromises.push({ resolve, reject });
153
- });
154
-
155
- if (!this._isConnecting) {
156
- this.connect();
157
- }
158
-
159
- // Return a promise that will resolve when the connection is established
160
- return connectionPromise;
161
- }
162
-
163
-
164
- private onSignalReceived(signal: ICodenotchSignal) {
165
- try {
166
- console.log(`Received signal '${signal.signalId}'`)
167
- console.log(signal);
168
-
169
- // parse the eventual data in the signal
170
- if(signal.data)
171
- {
172
- try
173
- {
174
- signal.data = JSON.parse(signal.data);
175
- }
176
- catch {
177
- // Data is not json but might still be valid
178
- }
179
- }
180
-
181
- // If signal id contains multiple paths, trigger each one
182
- var signalPaths = this.getSignalPaths(signal.signalId);
183
-
184
- for(let p of signalPaths)
185
- {
186
- if(this._signalCallbacks[p])
187
- {
188
- for(let callback of Object.values(this._signalCallbacks[p]))
189
- {
190
- callback(signal);
191
- }
192
- }
193
- }
194
- }
195
- catch (ex) {
196
- console.log(`An exception occured during 'ReceiveSignal' callback:`);
197
- console.log(ex);
198
- }
199
- }
200
-
201
- private getSignalPaths(signalId: string): string[]
202
- {
203
- // Signals can be segmented using '.', each segment corresponds to a more specific subject
204
- // Subscribers can subscribe to a very specific signal or to a more general topic
205
- // eg. Signal ref : invoice.update.0000-1111-2222-3333, will be received by subscribers on:
206
- // -> 'invoice'
207
- // -> 'invoice.update'
208
- // -> 'invoice.update.0000-1111-2222-3333'
209
- var signalPaths:string[] = [];
210
-
211
- if(!signalId || signalId === '')
212
- return signalPaths;
213
-
214
- var segments = signalId.split('.');
215
- for (let i = 0; i < segments.length; i++)
216
- {
217
- signalPaths.push(segments.slice(0, i + 1).join('.')); // "path" from index 0 to i
218
- }
219
-
220
- return signalPaths;
221
- }
222
-
223
- private onProcessCallbackReceived(callback: {processInstanceId: string }) {
224
-
225
- // Trigger the corresponding callback (set in ProcessUtils before launching the process)
226
- if(this._processCallbacks[callback.processInstanceId])
227
- {
228
- this._processCallbacks[callback.processInstanceId].onCallback(callback)
229
- }
230
- }
231
-
232
- private onProcessOverReceived(processResult: IProcessResult) {
233
-
234
- // Trigger the corresponding callback (set in ProcessUtils before launching the process)
235
- if(this._processCallbacks[processResult.processInstanceId])
236
- {
237
- this._processCallbacks[processResult.processInstanceId].onOver(processResult)
238
-
239
- // Clean it, now that the process is over we should not receive anymore callbacks
240
- delete this._processCallbacks[processResult.processInstanceId];
241
-
242
- this.disconnectIfInactive();
243
- }
244
- }
245
-
246
- async subscribeToProcessInstance(processInstanceId: string, callbacks: IProcessCallbacks) {
247
-
248
- if(this._processCallbacks[processInstanceId])
249
- {
250
- // Already subscribed
251
- return;
252
- }
253
-
254
- await this.ensureConnected();
255
-
256
- try
257
- {
258
- await this._connection!.invoke("SubscribeToInstanceEvents", processInstanceId);
259
-
260
- // Setup callbacks
261
- this._processCallbacks[processInstanceId] = callbacks;
262
- }
263
- catch(err)
264
- {
265
- console.error(`Couldn't subscribe to process '${processInstanceId}', : ${err}`)
266
- }
267
- }
268
-
269
- async subscribeToSignal(signalId: string, callback: (signal: ICodenotchSignal) => void): Promise<() => Promise<void>> {
270
-
271
- console.log(`subscribeToSignal: signalId:${signalId}`);
272
-
273
- if (!signalId || signalId === "")
274
- return () => Promise.resolve();
275
-
276
- await this.ensureConnected();
277
-
278
- if(this._unsubscribeTimers[signalId])
279
- {
280
- // cancel eventual unsubscription
281
- clearTimeout(this._unsubscribeTimers[signalId]);
282
- delete this._unsubscribeTimers[signalId];
283
- }
284
-
285
- if(!this._signalCallbacks[signalId])
286
- {
287
- // Create the subscription
288
- try
289
- {
290
- await this._connection!.invoke("SubscribeToSignal", signalId);
291
-
292
- // Remember which signals we are subscribed so we don't subscribe twice
293
- if(!this._signalCallbacks[signalId])
294
- {
295
- this._signalCallbacks[signalId] = {};
296
- }
297
-
298
- }
299
- catch(err)
300
- {
301
- throw `Couldn't subscribe to signal '${signalId}', : ${err}`;
302
- }
303
- }
304
-
305
- // Generate a new id to keep track of each subscription
306
- let subscriptionId = v4();
307
- this._signalCallbacks[signalId][subscriptionId] = callback;
308
-
309
- // Return the unsubscribe function
310
- return async () => await this.unsubscribeFromSignal(signalId, subscriptionId);
311
- }
312
-
313
- async unsubscribeFromSignal(signalId: string, subscriptionId: string)
314
- {
315
- console.log(`unsubscribeFromSignal: signalId:${signalId}, subscriptionId:${subscriptionId}`);
316
-
317
- if(!this._signalCallbacks[signalId])
318
- {
319
- return;
320
- }
321
-
322
- delete this._signalCallbacks[signalId][subscriptionId];
323
-
324
- // From this point we won't send events to the component who just unsubscribed, but we are still subscribed to the signal on the server so we'll recieve new signals
325
- this.unsubscribeFromServerIfNoMoreSubscriptions(signalId);
326
-
327
- // If no more subscriptions, we can disconnect from the server
328
- this.disconnectIfInactive();
329
- }
330
-
331
- private unsubscribeFromServerIfNoMoreSubscriptions(signalId: string)
332
- {
333
- if(Object.keys(this._signalCallbacks[signalId]).length === 0)
334
- {
335
- delete this._signalCallbacks[signalId];
336
-
337
- // No more subscriptions for this signal, we can unsubscribe from the server
338
- // but in order to not send too many requests, we will wait a little bit before actually unsubscribing, the client might come back to the tab that need this signal in a few seconds
339
- this._unsubscribeTimers[signalId] = setTimeout(() => this.unsubscribeSignalServer(signalId), 1 * 60 * 1000);
340
- }
341
- }
342
-
343
- private async unsubscribeSignalServer(signalId: string)
344
- {
345
- console.log(`unsubscribeSignalServer: signalId:${signalId}`);
346
-
347
- await this._connection!.invoke("UnsubscribeFromSignal", signalId);
348
- }
349
-
350
- private disconnectIfInactive()
351
- {
352
- if(this._keepAlive)
353
- return;
354
-
355
- if(Object.keys(this._signalCallbacks).length === 0 && Object.keys(this._processCallbacks).length === 0)
356
- {
357
- // No more subscriptions, disconnect from the server after a little while (5 minutes) to save resources
358
- this._inactivityTimer = setTimeout(() => this.disconnect(), 5 * 60 * 1000);
359
- console.log("SignalR has no more active subscriptions, will disconnect in 5 minutes...");
360
- }
361
- }
362
-
363
- public disconnect(): void {
364
-
365
- if (this._connection) {
366
- this._connection.off("ReceiveCallback");
367
- this._connection.off("ReceiveProcessOver");
368
- this._connection.off("ReceiveSignal");
369
- this._connection.off("ReceiveLog");
370
-
371
- this._connection.stop();
372
- this._connection = null;
373
- }
374
- }
375
- }