Skip to content

Commit 25f1857

Browse files
authored
implementation of client push methods (#2382)
* updated lerna and ran lerna repair * started working on client push methods * finished working on new send to client functrionality - except for disconnect detection * recator to just UI messaging controller * ensure by naming, that the client is a frontend client * pushmessage no longer has cb * handlerId is confusing, name it clientId * drop a line in changelog * extend subscription if subscribe is called as heartbeat * add signatures for emitted events * added missing emitted events * prepare changelog for next version * also handle subscribeerror and disconnect * typo * allow to send to all clients - added overload for external usage * add a failing type test too * rm auto import * address review
1 parent 8694566 commit 25f1857

File tree

8 files changed

+376
-43
lines changed

8 files changed

+376
-43
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
Placeholder for the next version (at the beginning of the line):
44
## __WORK IN PROGRESS__
55
-->
6-
## 5.0.11 (2023-07-30) - Jana
6+
## __WORK IN PROGRESS__ - Jana
77
**BREAKING CHANGES**
88
* Support for Node.js 12 and 14 is dropped! Supported are Node.js 16.4.0+ and 18.x
99
* Backups created with the new js-controller version cannot be restored on hosts with lower js-controller version!
1010
* Update recommended npm version to 8
1111
* Deprecate binary states, Adapters will change to use Files instead!
1212

1313
**Features**
14+
* (foxriver76) added method `sendToUserInterfaceClient` to push messages to UI client
1415
* (foxriver76) Show npm error message on failing adapter installations and update also without debug parameter
1516
* (bluefox/Apollon77/foxriver76) Try to solve `ENOTEMPTY` errors automatically on adapter upgrades/installations
1617
* (foxriver76) Introduce iobroker setting (dnsResolution) to choose between verbatim and ipv4 first dns resolution order

packages/adapter/src/lib/_Types.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export interface AdapterOptions {
44
logTransporter?: boolean;
55
/** if true, the date format from system.config */
66
useFormatDate?: boolean;
7-
/** if it is possible for other instances to retrive states of this adapter automatically */
7+
/** if it is possible for other instances to retrieve states of this adapter automatically */
88
subscribable?: boolean;
99
/** compact group instance if running in compact mode */
1010
compactInstance?: number;
@@ -32,6 +32,10 @@ export interface AdapterOptions {
3232
stateChange?: ioBroker.StateChangeHandler;
3333
/** callback function (id, file) that will be called if file changed */
3434
fileChange?: ioBroker.FileChangeHandler;
35+
/** callback function that will be called when a new UI client subscribes */
36+
uiClientSubscribe?: UserInterfaceClientSubscribeHandler;
37+
/** callback function that will be called when a new UI client unsubscribes */
38+
uiClientUnsubscribe?: UserInterfaceClientUnsubscribeHandler;
3539
/** callback to inform about new message the adapter */
3640
message?: ioBroker.MessageHandler;
3741
/** callback to stop the adapter */
@@ -46,6 +50,66 @@ export interface AdapterOptions {
4650
error?: ioBroker.ErrorHandler;
4751
}
4852

53+
type MessageUnsubscribeReason = 'client' | 'disconnect';
54+
export type ClientUnsubscribeReason = MessageUnsubscribeReason | 'clientSubscribeError';
55+
type UserInterfaceClientUnsubscribeReason = ClientUnsubscribeReason | 'timeout';
56+
57+
export interface UserInterfaceSubscribeInfo {
58+
/** The client id, which can be used to send information to clients */
59+
clientId: string;
60+
/** The message used for subscription */
61+
message: ioBroker.Message;
62+
}
63+
64+
export type UserInterfaceClientSubscribeHandler = (
65+
subscribeInfo: UserInterfaceSubscribeInfo
66+
) => UserInterfaceClientSubscribeReturnType | Promise<UserInterfaceClientSubscribeReturnType>;
67+
68+
export interface UserInterfaceClientSubscribeReturnType {
69+
/** If the adapter has accepted the client subscription */
70+
accepted: boolean;
71+
/** Optional heartbeat, if set, the client needs to re-subscribe every heartbeat interval */
72+
heartbeat?: number;
73+
}
74+
75+
type UserInterfaceUnsubscribeInfoBaseObject = {
76+
/** The handler id, which can be used to send information to clients */
77+
clientId: string;
78+
};
79+
80+
export type UserInterfaceUnsubscribeInfo = UserInterfaceUnsubscribeInfoBaseObject &
81+
(
82+
| {
83+
/** Reason for unsubscribe */
84+
reason: Exclude<UserInterfaceClientUnsubscribeReason, ClientUnsubscribeReason>;
85+
message?: undefined;
86+
}
87+
| {
88+
/** Reason for unsubscribe */
89+
reason: ClientUnsubscribeReason;
90+
/** Message used for unsubscribe */
91+
message: ioBroker.Message;
92+
}
93+
);
94+
95+
export type UserInterfaceClientUnsubscribeHandler = (
96+
unsubscribeInfo: UserInterfaceUnsubscribeInfo
97+
) => void | Promise<void>;
98+
99+
export type UserInterfaceClientRemoveMessage =
100+
| (ioBroker.Message & {
101+
command: 'clientUnsubscribe';
102+
message: {
103+
reason: MessageUnsubscribeReason;
104+
};
105+
})
106+
| (ioBroker.Message & {
107+
command: 'clientSubscribeError';
108+
message: {
109+
reason: undefined;
110+
};
111+
});
112+
49113
export type Pattern = string | string[];
50114

51115
export interface AdapterOptionsConfig {
@@ -162,6 +226,15 @@ export type CommandsPermissions = CommandsPermissionsObject | CommandsPermission
162226

163227
export type CalculatePermissionsCallback = (result: ioBroker.PermissionSet) => void;
164228

229+
export interface SendToUserInterfaceClientOptions {
230+
/** id of the UI client, if not given send to all active clients */
231+
clientId?: string;
232+
/** data to send to the client */
233+
data: unknown;
234+
}
235+
236+
export type AllPropsUnknown<T> = { [K in keyof T]: unknown };
237+
165238
export interface InternalCalculatePermissionsOptions {
166239
user: string;
167240
commandsPermissions: CommandsPermissions;

packages/adapter/src/lib/adapter/adapter.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,12 @@ import type {
106106
MessageCallbackObject,
107107
SendToOptions,
108108
GetCertificatesPromiseReturnType,
109-
InternalAdapterConfig
109+
InternalAdapterConfig,
110+
UserInterfaceClientRemoveMessage,
111+
SendToUserInterfaceClientOptions,
112+
AllPropsUnknown
110113
} from '../_Types';
114+
import { UserInterfaceMessagingController } from './userInterfaceMessagingController';
111115

112116
tools.ensureDNSOrder();
113117

@@ -121,6 +125,16 @@ let Objects: typeof ObjectsInRedisClient;
121125
* Here we define dynamically created methods
122126
*/
123127
export interface AdapterClass {
128+
on(event: 'stateChange', listener: ioBroker.StateChangeHandler): this;
129+
on(event: 'objectChange', listener: ioBroker.ObjectChangeHandler): this;
130+
on(event: 'fileChange', listener: ioBroker.FileChangeHandler): this;
131+
on(event: 'ready', listener: ioBroker.ReadyHandler): this;
132+
on(event: 'install', listener: ioBroker.ReadyHandler): this;
133+
on(event: 'unload', listener: ioBroker.UnloadHandler): this;
134+
on(event: 'message', listener: ioBroker.MessageHandler): this;
135+
/** Only emitted for compact instances */
136+
on(event: 'exit', listener: (exitCode: number, reason: string) => Promise<void> | void): this;
137+
on(event: 'log', listener: (info: any) => Promise<void> | void): this;
124138
/** Extend an object and create it if it might not exist */
125139
extendObjectAsync(
126140
id: string,
@@ -701,6 +715,8 @@ export class AdapterClass extends EventEmitter {
701715

702716
/** Features supported by the running instance */
703717
private readonly SUPPORTED_FEATURES = getSupportedFeatures();
718+
/** Controller for messaging related functionality */
719+
private readonly uiMessagingController: UserInterfaceMessagingController;
704720

705721
constructor(options: AdapterOptions | string) {
706722
super();
@@ -832,6 +848,12 @@ export class AdapterClass extends EventEmitter {
832848
this.terminate(EXIT_CODES.CANNOT_FIND_ADAPTER_DIR);
833849
}
834850

851+
this.uiMessagingController = new UserInterfaceMessagingController({
852+
adapter: this,
853+
subscribeCallback: this._options.uiClientSubscribe,
854+
unsubscribeCallback: this._options.uiClientUnsubscribe
855+
});
856+
835857
// Create dynamic methods
836858
/**
837859
* Promise-version of `Adapter.getPort`
@@ -7343,6 +7365,36 @@ export class AdapterClass extends EventEmitter {
73437365
}
73447366
}
73457367

7368+
sendToUI(options: SendToUserInterfaceClientOptions): Promise<void>;
7369+
7370+
/**
7371+
* Send a message to an active UI Client
7372+
*
7373+
* @param options clientId and data options
7374+
*/
7375+
sendToUI(options: AllPropsUnknown<SendToUserInterfaceClientOptions>): Promise<void> {
7376+
if (!adapterStates) {
7377+
throw new Error(tools.ERRORS.ERROR_DB_CLOSED);
7378+
}
7379+
7380+
const { clientId, data } = options;
7381+
7382+
if (clientId === undefined) {
7383+
return this.uiMessagingController.sendToAllClients({
7384+
data,
7385+
states: adapterStates
7386+
});
7387+
}
7388+
7389+
Validator.assertString(clientId, 'clientId');
7390+
7391+
return this.uiMessagingController.sendToClient({
7392+
clientId,
7393+
data,
7394+
states: adapterStates
7395+
});
7396+
}
7397+
73467398
registerNotification<Scope extends keyof ioBroker.NotificationScopes>(
73477399
scope: Scope,
73487400
category: ioBroker.NotificationScopes[Scope] | null,
@@ -11000,6 +11052,16 @@ export class AdapterClass extends EventEmitter {
1100011052
}
1100111053
}
1100211054
} else if (!this._stopInProgress) {
11055+
if (obj.command === 'clientSubscribe') {
11056+
return this.uiMessagingController.registerClientSubscribeByMessage(obj);
11057+
}
11058+
11059+
if (obj.command === 'clientUnsubscribe' || obj.command === 'clientSubscribeError') {
11060+
return this.uiMessagingController.removeClientSubscribeByMessage(
11061+
obj as UserInterfaceClientRemoveMessage
11062+
);
11063+
}
11064+
1100311065
if (this._options.message) {
1100411066
// Else inform about new message the adapter
1100511067
this._options.message(obj);

0 commit comments

Comments
 (0)