Skip to content

Commit

Permalink
implementation of client push methods (#2382)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
foxriver76 authored Aug 15, 2023
1 parent 8694566 commit 25f1857
Show file tree
Hide file tree
Showing 8 changed files with 376 additions and 43 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
Placeholder for the next version (at the beginning of the line):
## __WORK IN PROGRESS__
-->
## 5.0.11 (2023-07-30) - Jana
## __WORK IN PROGRESS__ - Jana
**BREAKING CHANGES**
* Support for Node.js 12 and 14 is dropped! Supported are Node.js 16.4.0+ and 18.x
* Backups created with the new js-controller version cannot be restored on hosts with lower js-controller version!
* Update recommended npm version to 8
* Deprecate binary states, Adapters will change to use Files instead!

**Features**
* (foxriver76) added method `sendToUserInterfaceClient` to push messages to UI client
* (foxriver76) Show npm error message on failing adapter installations and update also without debug parameter
* (bluefox/Apollon77/foxriver76) Try to solve `ENOTEMPTY` errors automatically on adapter upgrades/installations
* (foxriver76) Introduce iobroker setting (dnsResolution) to choose between verbatim and ipv4 first dns resolution order
Expand Down
75 changes: 74 additions & 1 deletion packages/adapter/src/lib/_Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export interface AdapterOptions {
logTransporter?: boolean;
/** if true, the date format from system.config */
useFormatDate?: boolean;
/** if it is possible for other instances to retrive states of this adapter automatically */
/** if it is possible for other instances to retrieve states of this adapter automatically */
subscribable?: boolean;
/** compact group instance if running in compact mode */
compactInstance?: number;
Expand Down Expand Up @@ -32,6 +32,10 @@ export interface AdapterOptions {
stateChange?: ioBroker.StateChangeHandler;
/** callback function (id, file) that will be called if file changed */
fileChange?: ioBroker.FileChangeHandler;
/** callback function that will be called when a new UI client subscribes */
uiClientSubscribe?: UserInterfaceClientSubscribeHandler;
/** callback function that will be called when a new UI client unsubscribes */
uiClientUnsubscribe?: UserInterfaceClientUnsubscribeHandler;
/** callback to inform about new message the adapter */
message?: ioBroker.MessageHandler;
/** callback to stop the adapter */
Expand All @@ -46,6 +50,66 @@ export interface AdapterOptions {
error?: ioBroker.ErrorHandler;
}

type MessageUnsubscribeReason = 'client' | 'disconnect';
export type ClientUnsubscribeReason = MessageUnsubscribeReason | 'clientSubscribeError';
type UserInterfaceClientUnsubscribeReason = ClientUnsubscribeReason | 'timeout';

export interface UserInterfaceSubscribeInfo {
/** The client id, which can be used to send information to clients */
clientId: string;
/** The message used for subscription */
message: ioBroker.Message;
}

export type UserInterfaceClientSubscribeHandler = (
subscribeInfo: UserInterfaceSubscribeInfo
) => UserInterfaceClientSubscribeReturnType | Promise<UserInterfaceClientSubscribeReturnType>;

export interface UserInterfaceClientSubscribeReturnType {
/** If the adapter has accepted the client subscription */
accepted: boolean;
/** Optional heartbeat, if set, the client needs to re-subscribe every heartbeat interval */
heartbeat?: number;
}

type UserInterfaceUnsubscribeInfoBaseObject = {
/** The handler id, which can be used to send information to clients */
clientId: string;
};

export type UserInterfaceUnsubscribeInfo = UserInterfaceUnsubscribeInfoBaseObject &
(
| {
/** Reason for unsubscribe */
reason: Exclude<UserInterfaceClientUnsubscribeReason, ClientUnsubscribeReason>;
message?: undefined;
}
| {
/** Reason for unsubscribe */
reason: ClientUnsubscribeReason;
/** Message used for unsubscribe */
message: ioBroker.Message;
}
);

export type UserInterfaceClientUnsubscribeHandler = (
unsubscribeInfo: UserInterfaceUnsubscribeInfo
) => void | Promise<void>;

export type UserInterfaceClientRemoveMessage =
| (ioBroker.Message & {
command: 'clientUnsubscribe';
message: {
reason: MessageUnsubscribeReason;
};
})
| (ioBroker.Message & {
command: 'clientSubscribeError';
message: {
reason: undefined;
};
});

export type Pattern = string | string[];

export interface AdapterOptionsConfig {
Expand Down Expand Up @@ -162,6 +226,15 @@ export type CommandsPermissions = CommandsPermissionsObject | CommandsPermission

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

export interface SendToUserInterfaceClientOptions {
/** id of the UI client, if not given send to all active clients */
clientId?: string;
/** data to send to the client */
data: unknown;
}

export type AllPropsUnknown<T> = { [K in keyof T]: unknown };

export interface InternalCalculatePermissionsOptions {
user: string;
commandsPermissions: CommandsPermissions;
Expand Down
64 changes: 63 additions & 1 deletion packages/adapter/src/lib/adapter/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,12 @@ import type {
MessageCallbackObject,
SendToOptions,
GetCertificatesPromiseReturnType,
InternalAdapterConfig
InternalAdapterConfig,
UserInterfaceClientRemoveMessage,
SendToUserInterfaceClientOptions,
AllPropsUnknown
} from '../_Types';
import { UserInterfaceMessagingController } from './userInterfaceMessagingController';

tools.ensureDNSOrder();

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

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

constructor(options: AdapterOptions | string) {
super();
Expand Down Expand Up @@ -832,6 +848,12 @@ export class AdapterClass extends EventEmitter {
this.terminate(EXIT_CODES.CANNOT_FIND_ADAPTER_DIR);
}

this.uiMessagingController = new UserInterfaceMessagingController({
adapter: this,
subscribeCallback: this._options.uiClientSubscribe,
unsubscribeCallback: this._options.uiClientUnsubscribe
});

// Create dynamic methods
/**
* Promise-version of `Adapter.getPort`
Expand Down Expand Up @@ -7343,6 +7365,36 @@ export class AdapterClass extends EventEmitter {
}
}

sendToUI(options: SendToUserInterfaceClientOptions): Promise<void>;

/**
* Send a message to an active UI Client
*
* @param options clientId and data options
*/
sendToUI(options: AllPropsUnknown<SendToUserInterfaceClientOptions>): Promise<void> {
if (!adapterStates) {
throw new Error(tools.ERRORS.ERROR_DB_CLOSED);
}

const { clientId, data } = options;

if (clientId === undefined) {
return this.uiMessagingController.sendToAllClients({
data,
states: adapterStates
});
}

Validator.assertString(clientId, 'clientId');

return this.uiMessagingController.sendToClient({
clientId,
data,
states: adapterStates
});
}

registerNotification<Scope extends keyof ioBroker.NotificationScopes>(
scope: Scope,
category: ioBroker.NotificationScopes[Scope] | null,
Expand Down Expand Up @@ -11000,6 +11052,16 @@ export class AdapterClass extends EventEmitter {
}
}
} else if (!this._stopInProgress) {
if (obj.command === 'clientSubscribe') {
return this.uiMessagingController.registerClientSubscribeByMessage(obj);
}

if (obj.command === 'clientUnsubscribe' || obj.command === 'clientSubscribeError') {
return this.uiMessagingController.removeClientSubscribeByMessage(
obj as UserInterfaceClientRemoveMessage
);
}

if (this._options.message) {
// Else inform about new message the adapter
this._options.message(obj);
Expand Down
Loading

0 comments on commit 25f1857

Please sign in to comment.