@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/dist/index.d.ts +2 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +88 -112
- package/dist/models/Codenotch.d.ts +8 -129
- package/dist/models/Codenotch.d.ts.map +1 -1
- package/package.json +3 -4
- package/src/index.ts +93 -128
- package/src/models/Codenotch.ts +8 -146
- package/README.md +0 -183
- package/src/core/ProcessUtils.ts +0 -86
- package/src/core/SignalR.ts +0 -375
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`).
|
package/src/core/ProcessUtils.ts
DELETED
|
@@ -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
|
-
}
|
package/src/core/SignalR.ts
DELETED
|
@@ -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
|
-
}
|