diff --git a/.dev/appsettings.override.json b/.dev/appsettings.override.json index a639f8391..50deca4e1 100644 --- a/.dev/appsettings.override.json +++ b/.dev/appsettings.override.json @@ -55,6 +55,10 @@ "Providers": { "Dummy": { "Enabled": true + }, + "Sse": { + "Enabled": true, + "SseServerBaseAddress": "http://sse-server:8080" } } } @@ -126,7 +130,7 @@ } } }, - "Serilog": { + "Logging": { "MinimumLevel": { "Default": "Debug" }, @@ -143,15 +147,5 @@ "SwaggerUi": { "TokenUrl": "http://localhost:5000/connect/token", "Enabled": true - }, - "Logging": { - "WriteTo": { - "Console": { - "Name": "Console", - "Args": { - "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss}|{Level} => CorrelationID:{CorrelationID} => RequestId:{RequestId} => RequestPath:{RequestPath}{NewLine} {SourceContext}{NewLine} {Message}{NewLine}{Exception}" - } - } - } } } diff --git a/.dev/compose.backbone.yml b/.dev/compose.backbone.yml index e182e6a51..f8163f4aa 100644 --- a/.dev/compose.backbone.yml +++ b/.dev/compose.backbone.yml @@ -18,6 +18,8 @@ services: condition: service_started database-migrator: condition: service_completed_successfully + sse-server: + condition: service_started configs: - source: Config target: app/appsettings.override.json @@ -36,6 +38,19 @@ services: - source: Config target: app/appsettings.override.json + sse-server: + image: ghcr.io/nmshd/backbone-sse-server:${BACKBONE_VERSION} + container_name: sse-server + hostname: sse-server + ports: + - "8092:8080" + depends_on: + database: + condition: service_started + configs: + - source: Config + target: app/appsettings.override.json + admin-ui: image: ghcr.io/nmshd/backbone-admin-ui:${BACKBONE_VERSION} container_name: admin-ui diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9e4f18c2d..26fe880b4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,6 +32,7 @@ jobs: NMSHD_TEST_BASEURL: http://localhost:8090 NMSHD_TEST_CLIENTID: test NMSHD_TEST_CLIENTSECRET: test + NMSHD_TEST_BASEURL_SSE_SERVER: http://localhost:8092 - name: Upload coverage reports to Codecov with GitHub Action uses: codecov/codecov-action@v5 env: diff --git a/package-lock.json b/package-lock.json index 6552b227b..80dd87066 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1533,18 +1533,6 @@ "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", "license": "Apache-2.0" }, - "node_modules/@js-soft/web-logger": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@js-soft/web-logger/-/web-logger-1.0.4.tgz", - "integrity": "sha512-Gj+i0Ho9Yd3Z3rzpySpyeFTYkyHvgZ/ac97KxQrOEKAV8158D/Ewoims+MQKWUhoigvpLJj9ZIooKcBFW/ygsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@js-soft/logging-abstractions": "1.0.1", - "js-logger": "1.6.1", - "json-stringify-safe": "^5.0.1" - } - }, "node_modules/@mongodb-js/saslprep": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", @@ -2295,10 +2283,11 @@ "license": "MIT" }, "node_modules/@types/lodash": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.14.tgz", - "integrity": "sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==", - "dev": true + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/lokijs": { "version": "1.5.14", @@ -4903,6 +4892,27 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.5.tgz", + "integrity": "sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", + "integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -6714,13 +6724,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/js-logger": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/js-logger/-/js-logger-1.6.1.tgz", - "integrity": "sha512-yTgMCPXVjhmg28CuUH8CKjU+cIKL/G+zTu4Fn4lQxs8mRFH/03QTNvEFngcxfg/gRDiQAOoyCKmMTOm9ayOzXA==", - "dev": true, - "license": "MIT" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -11002,11 +11005,12 @@ "dependencies": { "@js-soft/docdb-access-loki": "^1.2.0", "@nmshd/runtime": "*", + "eventsource": "^3.0.5", "lodash": "^4.17.21" }, "devDependencies": { - "@js-soft/web-logger": "^1.0.4", - "@types/lodash": "^4.17.14", + "@js-soft/node-logger": "^1.2.0", + "@types/lodash": "^4.17.15", "@types/lokijs": "^1.5.14", "@types/luxon": "^3.4.2" } diff --git a/packages/app-runtime/package.json b/packages/app-runtime/package.json index db2272502..02cbc5c27 100644 --- a/packages/app-runtime/package.json +++ b/packages/app-runtime/package.json @@ -54,11 +54,12 @@ "dependencies": { "@js-soft/docdb-access-loki": "^1.2.0", "@nmshd/runtime": "*", + "eventsource": "^3.0.5", "lodash": "^4.17.21" }, "devDependencies": { - "@js-soft/web-logger": "^1.0.4", - "@types/lodash": "^4.17.14", + "@js-soft/node-logger": "^1.2.0", + "@types/lodash": "^4.17.15", "@types/lokijs": "^1.5.14", "@types/luxon": "^3.4.2" }, diff --git a/packages/app-runtime/src/AppConfig.ts b/packages/app-runtime/src/AppConfig.ts index 2cd3b6e15..b28beb5d4 100644 --- a/packages/app-runtime/src/AppConfig.ts +++ b/packages/app-runtime/src/AppConfig.ts @@ -76,6 +76,12 @@ export function createAppConfig(...configs: AppConfigOverwrite[]): AppConfig { location: "relationshipTemplateProcessed", enabled: true }, + sse: { + name: "SSEModule", + displayName: "SSE Module", + location: "sse", + enabled: false + }, decider: { displayName: "Decider Module", name: "DeciderModule", diff --git a/packages/app-runtime/src/AppRuntime.ts b/packages/app-runtime/src/AppRuntime.ts index 59ca6c217..d71ec32ac 100644 --- a/packages/app-runtime/src/AppRuntime.ts +++ b/packages/app-runtime/src/AppRuntime.ts @@ -22,7 +22,8 @@ import { OnboardingChangeReceivedModule, PushNotificationModule, RelationshipChangedModule, - RelationshipTemplateProcessedModule + RelationshipTemplateProcessedModule, + SSEModule } from "./modules"; import { AccountServices, LocalAccountMapper, LocalAccountSession, MultiAccountController } from "./multiAccount"; import { INativeBootstrapper, INativeEnvironment, INativeTranslationProvider } from "./natives"; @@ -260,13 +261,14 @@ export class AppRuntime extends Runtime { private static moduleRegistry: Record = { appLaunch: AppLaunchModule, appSync: AppSyncModule, - pushNotification: PushNotificationModule, - mailReceived: MailReceivedModule, - onboardingChangeReceived: OnboardingChangeReceivedModule, identityDeletionProcessStatusChanged: IdentityDeletionProcessStatusChangedModule, + mailReceived: MailReceivedModule, messageReceived: MessageReceivedModule, + onboardingChangeReceived: OnboardingChangeReceivedModule, + pushNotification: PushNotificationModule, relationshipChanged: RelationshipChangedModule, - relationshipTemplateProcessed: RelationshipTemplateProcessedModule + relationshipTemplateProcessed: RelationshipTemplateProcessedModule, + sse: SSEModule }; public static registerModule(moduleName: string, ctor: IAppRuntimeModuleConstructor): void { @@ -317,7 +319,7 @@ export class AppRuntime extends Runtime { await session.accountController.authenticator.getToken(); continue; } catch (error) { - this.logger.error(error); + this.logger.info(error); if (!(typeof error === "object" && error !== null && "code" in error)) { continue; diff --git a/packages/app-runtime/src/AppStringProcessor.ts b/packages/app-runtime/src/AppStringProcessor.ts index f12640663..78fd9f6cc 100644 --- a/packages/app-runtime/src/AppStringProcessor.ts +++ b/packages/app-runtime/src/AppStringProcessor.ts @@ -56,7 +56,7 @@ export class AppStringProcessor { if (truncatedReference.startsWith(Base64ForIdPrefix.File) || truncatedReference.startsWith(Base64ForIdPrefix.RelationshipTemplate)) { const result = await this.selectAccount(reference.forIdentityTruncated); if (result.isError) { - this.logger.error("Could not query account", result.error); + this.logger.info("Could not query account", result.error); return UserfriendlyResult.fail(result.error); } diff --git a/packages/app-runtime/src/modules/appSync/AppSyncModule.ts b/packages/app-runtime/src/modules/AppSyncModule.ts similarity index 98% rename from packages/app-runtime/src/modules/appSync/AppSyncModule.ts rename to packages/app-runtime/src/modules/AppSyncModule.ts index eaa6ed985..5425f6cd8 100644 --- a/packages/app-runtime/src/modules/appSync/AppSyncModule.ts +++ b/packages/app-runtime/src/modules/AppSyncModule.ts @@ -1,4 +1,4 @@ -import { AppRuntimeModule, AppRuntimeModuleConfiguration } from "../AppRuntimeModule"; +import { AppRuntimeModule, AppRuntimeModuleConfiguration } from "./AppRuntimeModule"; export interface AppSyncModuleConfiguration extends AppRuntimeModuleConfiguration { interval: number; diff --git a/packages/app-runtime/src/modules/pushNotifications/PushNotificationModule.ts b/packages/app-runtime/src/modules/PushNotificationModule.ts similarity index 89% rename from packages/app-runtime/src/modules/pushNotifications/PushNotificationModule.ts rename to packages/app-runtime/src/modules/PushNotificationModule.ts index 31f9141d2..b4a2b0747 100644 --- a/packages/app-runtime/src/modules/pushNotifications/PushNotificationModule.ts +++ b/packages/app-runtime/src/modules/PushNotificationModule.ts @@ -1,9 +1,20 @@ import { Result } from "@js-soft/ts-utils"; -import { AppRuntimeErrors } from "../../AppRuntimeErrors"; -import { AccountSelectedEvent, ExternalEventReceivedEvent } from "../../events"; -import { RemoteNotificationEvent, RemoteNotificationRegistrationEvent } from "../../natives"; -import { AppRuntimeModule, AppRuntimeModuleConfiguration } from "../AppRuntimeModule"; -import { BackboneEventName, IBackboneEventContent } from "./IBackboneEventContent"; +import { AppRuntimeErrors } from "../AppRuntimeErrors"; +import { AccountSelectedEvent, ExternalEventReceivedEvent } from "../events"; +import { RemoteNotificationEvent, RemoteNotificationRegistrationEvent } from "../natives"; +import { AppRuntimeModule, AppRuntimeModuleConfiguration } from "./AppRuntimeModule"; + +enum BackboneEventName { + DatawalletModificationsCreated = "DatawalletModificationsCreated", + ExternalEventCreated = "ExternalEventCreated" +} + +interface IBackboneEventContent { + devicePushIdentifier: string; + eventName: BackboneEventName; + sentAt: string; + payload: any; +} export interface PushNotificationModuleConfig extends AppRuntimeModuleConfiguration {} @@ -77,11 +88,10 @@ export class PushNotificationModule extends AppRuntimeModule { + private async registerPushTokenForLocalAccount(address: string, token: string): Promise { if (!token) { - throw AppRuntimeErrors.modules.pushNotificationModule - .tokenRegistrationNotPossible("The registered token was empty. This might be the case if you did not allow push notifications.") - .logWith(this.logger); + this.logger.info("The registered token was empty. This might be the case if you did not allow push notifications."); + return; } const services = await this.runtime.getServices(address); diff --git a/packages/app-runtime/src/modules/SSEModule.ts b/packages/app-runtime/src/modules/SSEModule.ts new file mode 100644 index 000000000..1f9fd2d75 --- /dev/null +++ b/packages/app-runtime/src/modules/SSEModule.ts @@ -0,0 +1,108 @@ +import { ILogger } from "@js-soft/logging-abstractions"; +import { ModuleConfiguration } from "@nmshd/runtime"; +import { EventSource } from "eventsource"; +import { AppRuntime } from "../AppRuntime"; +import { AccountSelectedEvent } from "../events"; +import { LocalAccountSession } from "../multiAccount"; +import { AppRuntimeModule } from "./AppRuntimeModule"; + +export interface SSEModuleConfiguration extends ModuleConfiguration { + baseUrlOverride?: string; +} + +export class SSEModule extends AppRuntimeModule { + private eventSource: Record = {}; + + public constructor(runtime: AppRuntime, configuration: SSEModuleConfiguration, logger: ILogger) { + super(runtime, configuration, logger); + } + + public init(): void { + // Nothing to do here + } + + public async start(): Promise { + for (const session of this.runtime.getSessions()) { + await this.runSync(session); + await this.recreateEventSource(session); + } + + this.subscribeToEvent(AccountSelectedEvent, this.handleAccountSelected.bind(this)); + } + + private async handleAccountSelected(event: AccountSelectedEvent) { + const session = await this.runtime.getOrCreateSession(event.eventTargetAddress); + + if (this.eventSource[session.account.id]) return; + + await this.runSync(session); + await this.recreateEventSource(session); + } + + private async recreateEventSource(session: LocalAccountSession): Promise { + const existingEventSource = this.eventSource[session.account.id]; + if (existingEventSource) { + try { + existingEventSource.close(); + } catch (error) { + this.logger.error("Failed to close event source", error); + } + } + + const baseUrl = this.configuration.baseUrlOverride ?? this.runtime["runtimeConfig"].transportLibrary.baseUrl; + const sseUrl = `${baseUrl}/api/v1/sse`; + + this.logger.info(`Connecting to SSE endpoint: ${sseUrl}`); + + const eventSource = new EventSource(sseUrl, { + fetch: async (url, options) => { + const token = await session.accountController.authenticator.getToken(); + + const result = await fetch(url, { + ...options, + headers: { ...options?.headers, authorization: `Bearer ${token}` } + }); + + this.logger.info(`SSE fetch result: ${result.status}`); + + return result; + } + }); + + this.eventSource[session.account.id] = eventSource; + + eventSource.addEventListener("ExternalEventCreated", async () => await this.runSync(session)); + + await new Promise((resolve, reject) => { + eventSource.onopen = () => { + this.logger.info("Connected to SSE endpoint"); + resolve(); + + eventSource.onopen = () => { + // noop + }; + }; + + eventSource.onerror = (error) => { + reject(error); + }; + }); + + eventSource.onerror = async (error) => { + if (error.code === 401) await this.recreateEventSource(session); + }; + } + + private async runSync(session: LocalAccountSession): Promise { + const syncResult = await session.transportServices.account.syncEverything(); + if (syncResult.isError) { + this.logger.error(syncResult); + } + } + + public stop(): void { + for (const eventsource of Object.values(this.eventSource).filter((eventsource) => typeof eventsource !== "undefined")) { + eventsource.close(); + } + } +} diff --git a/packages/app-runtime/src/modules/appSync/index.ts b/packages/app-runtime/src/modules/appSync/index.ts deleted file mode 100644 index 234c3787c..000000000 --- a/packages/app-runtime/src/modules/appSync/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./AppSyncModule"; diff --git a/packages/app-runtime/src/modules/index.ts b/packages/app-runtime/src/modules/index.ts index a2907791f..01ddda1b8 100644 --- a/packages/app-runtime/src/modules/index.ts +++ b/packages/app-runtime/src/modules/index.ts @@ -1,5 +1,6 @@ export * from "./appEvents"; export * from "./AppRuntimeModule"; -export * from "./appSync"; -export * from "./pushNotifications"; +export * from "./AppSyncModule"; +export * from "./PushNotificationModule"; export * from "./runtimeEvents"; +export * from "./SSEModule"; diff --git a/packages/app-runtime/src/modules/pushNotifications/IBackboneEventContent.ts b/packages/app-runtime/src/modules/pushNotifications/IBackboneEventContent.ts deleted file mode 100644 index fbf57dc1d..000000000 --- a/packages/app-runtime/src/modules/pushNotifications/IBackboneEventContent.ts +++ /dev/null @@ -1,11 +0,0 @@ -export enum BackboneEventName { - DatawalletModificationsCreated = "DatawalletModificationsCreated", - ExternalEventCreated = "ExternalEventCreated" -} - -export interface IBackboneEventContent { - devicePushIdentifier: string; - eventName: BackboneEventName; - sentAt: string; - payload: any; -} diff --git a/packages/app-runtime/src/modules/pushNotifications/index.ts b/packages/app-runtime/src/modules/pushNotifications/index.ts deleted file mode 100644 index c451564a3..000000000 --- a/packages/app-runtime/src/modules/pushNotifications/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./IBackboneEventContent"; -export * from "./PushNotificationModule"; diff --git a/packages/app-runtime/src/multiAccount/MultiAccountController.ts b/packages/app-runtime/src/multiAccount/MultiAccountController.ts index 54b6d17bc..4be81a11b 100644 --- a/packages/app-runtime/src/multiAccount/MultiAccountController.ts +++ b/packages/app-runtime/src/multiAccount/MultiAccountController.ts @@ -98,7 +98,7 @@ export class MultiAccountController { this._log.trace(`Selecting LocalAccount with id ${id}...`); const account = await this._localAccounts.read(id.toString()); if (!account) { - throw TransportCoreErrors.general.recordNotFound(LocalAccount, id.toString()).logWith(this._log); + throw TransportCoreErrors.general.recordNotFound(LocalAccount, id.toString()); } let localAccount: LocalAccount = LocalAccount.from(account); @@ -167,7 +167,7 @@ export class MultiAccountController { throw new CoreError( "error.app-runtime.onboardedAccountAlreadyExists", `An account with the address '${deviceSharedSecret.identity.address.toString()}' already exists in this app-runtime instance.` - ).logWith(this._log); + ); } this._log.trace(`Onboarding device ${deviceSharedSecret.id} for identity ${deviceSharedSecret.identity.address}...`); diff --git a/packages/app-runtime/test/lib/FakeUIBridge.ts b/packages/app-runtime/test/lib/FakeUIBridge.ts index 006f7a021..0346ebf37 100644 --- a/packages/app-runtime/test/lib/FakeUIBridge.ts +++ b/packages/app-runtime/test/lib/FakeUIBridge.ts @@ -3,34 +3,34 @@ import { IUIBridge, LocalAccountDTO } from "../../src"; export class FakeUIBridge implements IUIBridge { public showMessage(): Promise> { - throw new Error("Method not implemented."); + return Promise.resolve(Result.ok(undefined)); } public showRelationship(): Promise> { - throw new Error("Method not implemented."); + return Promise.resolve(Result.ok(undefined)); } public showFile(): Promise> { - throw new Error("Method not implemented."); + return Promise.resolve(Result.ok(undefined)); } public showDeviceOnboarding(): Promise> { - throw new Error("Method not implemented."); + return Promise.resolve(Result.ok(undefined)); } public showRequest(): Promise> { - throw new Error("Method not implemented."); + return Promise.resolve(Result.ok(undefined)); } public showError(): Promise> { - throw new Error("Method not implemented."); + return Promise.resolve(Result.ok(undefined)); } public requestAccountSelection(): Promise> { - throw new Error("Method not implemented."); + return Promise.resolve(Result.fail(new ApplicationError("not implemented", "not implemented"))); } public enterPassword(_passwordType: "pw" | "pin", _pinLength?: number, _attempt?: number): Promise> { - throw new Error("Method not implemented."); + return Promise.resolve(Result.fail(new ApplicationError("not implemented", "not implemented"))); } } diff --git a/packages/app-runtime/test/lib/MockEventBus.ts b/packages/app-runtime/test/lib/MockEventBus.ts index 685db5efb..e0c8b2d3c 100644 --- a/packages/app-runtime/test/lib/MockEventBus.ts +++ b/packages/app-runtime/test/lib/MockEventBus.ts @@ -28,7 +28,8 @@ export class MockEventBus extends EventEmitter2EventBus { public async waitForEvent( subscriptionTarget: SubscriptionTarget & { namespace: string }, - predicate?: (event: TEvent) => boolean + predicate?: (event: TEvent) => boolean, + timeout = 5000 ): Promise { const alreadyTriggeredEvents = this.publishedEvents.find( (e) => @@ -40,7 +41,7 @@ export class MockEventBus extends EventEmitter2EventBus { return alreadyTriggeredEvents; } - const event = await waitForEvent(this, subscriptionTarget, predicate); + const event = await waitForEvent(this, subscriptionTarget, predicate, timeout); return event; } diff --git a/packages/app-runtime/test/lib/TestUtil.ts b/packages/app-runtime/test/lib/TestUtil.ts index 39693b803..93b321d29 100644 --- a/packages/app-runtime/test/lib/TestUtil.ts +++ b/packages/app-runtime/test/lib/TestUtil.ts @@ -16,6 +16,7 @@ import { } from "@nmshd/runtime"; import { IConfigOverwrite, TransportLoggerFactory } from "@nmshd/transport"; import fs from "fs"; +import { defaultsDeep } from "lodash"; import path from "path"; import { GenericContainer, Wait } from "testcontainers"; import { LogLevel } from "typescript-logging"; @@ -25,6 +26,12 @@ import { FakeNativeBootstrapper } from "./natives/FakeNativeBootstrapper"; export class TestUtil { public static async createRuntime(configOverride?: any, uiBridge: IUIBridge = new FakeUIBridge(), eventBus?: EventBus): Promise { + configOverride = defaultsDeep(configOverride, { + modules: { + pushNotification: { enabled: false } + } + }); + const config = this.createAppConfig(configOverride); const nativeBootstrapper = new FakeNativeBootstrapper(eventBus); diff --git a/packages/app-runtime/test/lib/natives/FakeNativeBootstrapper.ts b/packages/app-runtime/test/lib/natives/FakeNativeBootstrapper.ts index 4b93b17d0..366207901 100644 --- a/packages/app-runtime/test/lib/natives/FakeNativeBootstrapper.ts +++ b/packages/app-runtime/test/lib/natives/FakeNativeBootstrapper.ts @@ -1,5 +1,5 @@ +import { NodeLoggerFactory } from "@js-soft/node-logger"; import { EventBus, EventEmitter2EventBus, Result } from "@js-soft/ts-utils"; -import { WebLoggerFactory } from "@js-soft/web-logger"; import { INativeBootstrapper, INativeEnvironment } from "../../../src"; import { FakeNativeConfigAccess } from "./FakeNativeConfigAccess"; import { FakeNativeDatabaseFactory } from "./FakeNativeDatabaseFactory"; @@ -19,7 +19,27 @@ export class FakeNativeBootstrapper implements INativeBootstrapper { } public async init(): Promise> { - const loggerFactory = new WebLoggerFactory(); + const loggerFactory = new NodeLoggerFactory({ + appenders: { + consoleAppender: { + type: "stdout", + layout: { type: "pattern", pattern: "%[[%p] %c - %m%]" } + }, + console: { + type: "logLevelFilter", + level: "Warn", + appender: "consoleAppender" + } + }, + + categories: { + default: { + appenders: ["console"], + level: "TRACE" + } + } + }); + const nativeLogger = loggerFactory.getLogger("FakeNatives"); this._nativeEnvironment = { diff --git a/packages/app-runtime/test/modules/PushNotification.test.ts b/packages/app-runtime/test/modules/PushNotification.test.ts index bde22218f..c6c9bacdc 100644 --- a/packages/app-runtime/test/modules/PushNotification.test.ts +++ b/packages/app-runtime/test/modules/PushNotification.test.ts @@ -10,7 +10,7 @@ describe("PushNotificationModuleTest", function () { let devicePushIdentifier = "dummy value"; beforeAll(async function () { - runtime = await TestUtil.createRuntime(undefined, undefined, eventBus); + runtime = await TestUtil.createRuntime({ modules: { pushNotification: { enabled: true } } }, undefined, eventBus); await runtime.start(); const accounts = await TestUtil.provideAccounts(runtime, 1); diff --git a/packages/app-runtime/test/modules/SSEModule.test.ts b/packages/app-runtime/test/modules/SSEModule.test.ts new file mode 100644 index 000000000..8cd71c2ef --- /dev/null +++ b/packages/app-runtime/test/modules/SSEModule.test.ts @@ -0,0 +1,37 @@ +import { MessageReceivedEvent } from "@nmshd/runtime"; +import { AppRuntime, LocalAccountSession } from "../../src"; +import { MockEventBus, TestUtil } from "../lib"; + +// eslint-disable-next-line jest/no-disabled-tests -- disabled because the backbone currently isn't performant enough in the CI +describe.skip("SSEModuleTest", function () { + const eventBus = new MockEventBus(); + + let runtime: AppRuntime; + let session1: LocalAccountSession; + let session2: LocalAccountSession; + + beforeAll(async function () { + runtime = await TestUtil.createRuntime({ modules: { sse: { enabled: true, baseUrlOverride: process.env.NMSHD_TEST_BASEURL_SSE_SERVER } } }, undefined, eventBus); + await runtime.start(); + + const accounts = await TestUtil.provideAccounts(runtime, 2); + session1 = await runtime.selectAccount(accounts[0].id); + session2 = await runtime.selectAccount(accounts[1].id); + + await TestUtil.addRelationship(session1, session2); + }); + + afterAll(async function () { + await runtime.stop(); + }); + + afterEach(() => eventBus.reset()); + + test("should auto sync when new external events arrive", async function () { + const message = await TestUtil.sendMessage(session1, session2); + + const received = await eventBus.waitForEvent(MessageReceivedEvent, (e) => e.eventTargetAddress === session2.account.address!, 20000); + + expect(received.data.id).toBe(message.id); + }); +}); diff --git a/packages/app-runtime/test/runtime/TranslationProvider.test.ts b/packages/app-runtime/test/runtime/TranslationProvider.test.ts index fb2c5fa70..13e311b39 100644 --- a/packages/app-runtime/test/runtime/TranslationProvider.test.ts +++ b/packages/app-runtime/test/runtime/TranslationProvider.test.ts @@ -10,6 +10,7 @@ describe("TranslationProvider", function () { beforeAll(async function () { runtime = await TestUtil.createRuntime(); + await runtime.start(); class SpecificTranslationProvider implements INativeTranslationProvider { public translate(key: string, ..._values: any[]): Promise> { diff --git a/packages/transport/src/core/backbone/ClientResult.ts b/packages/transport/src/core/backbone/ClientResult.ts index dcf191262..ec2f84d41 100644 --- a/packages/transport/src/core/backbone/ClientResult.ts +++ b/packages/transport/src/core/backbone/ClientResult.ts @@ -12,6 +12,7 @@ export class ClientResult { public readonly responseDuration?: number; public readonly responseTime?: CoreDate; public readonly traceId?: string; + public readonly correlationId?: string; protected constructor(isSuccess: boolean, value?: T, error?: ApplicationError, platformParameters?: PlatformParameters) { if (isSuccess && error) { @@ -37,6 +38,8 @@ export class ClientResult { this.responseTime = platformParameters.responseTime ? CoreDate.from(platformParameters.responseTime) : undefined; this.traceId = platformParameters.traceId; + + this.correlationId = platformParameters.correlationId; } } diff --git a/packages/transport/src/core/backbone/PlatformParameters.ts b/packages/transport/src/core/backbone/PlatformParameters.ts index a2e0d2b93..9fab60b0b 100644 --- a/packages/transport/src/core/backbone/PlatformParameters.ts +++ b/packages/transport/src/core/backbone/PlatformParameters.ts @@ -3,4 +3,5 @@ export interface PlatformParameters { responseDuration?: string; responseTime?: string; traceId?: string; + correlationId?: string; } diff --git a/packages/transport/src/core/backbone/RESTClient.ts b/packages/transport/src/core/backbone/RESTClient.ts index a614fcf8a..47a1a006a 100644 --- a/packages/transport/src/core/backbone/RESTClient.ts +++ b/packages/transport/src/core/backbone/RESTClient.ts @@ -494,7 +494,8 @@ export class RESTClient { requestTime: response.headers["x-request-time"], responseDuration: response.headers["x-response-duration-ms"], responseTime: response.headers["x-response-time"], - traceId: response.headers["x-trace-id"] + traceId: response.headers["x-trace-id"], + correlationId: response.headers["x-correlation-id"] }; } diff --git a/packages/transport/src/modules/messages/MessageController.ts b/packages/transport/src/modules/messages/MessageController.ts index 965a4d769..88f819234 100644 --- a/packages/transport/src/modules/messages/MessageController.ts +++ b/packages/transport/src/modules/messages/MessageController.ts @@ -377,13 +377,12 @@ export class MessageController extends TransportController { }; }); - const response = ( - await this.client.createMessage({ - attachments: fileIds, - body: cipher.toBase64(), - recipients: platformRecipients - }) - ).value; + const result = await this.client.createMessage({ + attachments: fileIds, + body: cipher.toBase64(), + recipients: platformRecipients + }); + const value = result.value; const recipients = envelopeRecipients.map((r) => CachedMessageRecipient.from({ @@ -397,7 +396,7 @@ export class MessageController extends TransportController { const cachedMessage = CachedMessage.from({ content: parsedParams.content, - createdAt: CoreDate.from(response.createdAt), + createdAt: CoreDate.from(value.createdAt), createdBy: this.parent.identity.identity.address, createdByDevice: this.parent.activeDevice.id, recipients, @@ -406,7 +405,7 @@ export class MessageController extends TransportController { }); const message = Message.from({ - id: CoreId.from(response.id), + id: CoreId.from(value.id), secretKey: secret, cache: cachedMessage, cachedAt: CoreDate.utc(),