Skip to content

Commit

Permalink
AppRuntime SSE Module (#395)
Browse files Browse the repository at this point in the history
* chore: move modules out of folders

* chore: add eventsource

* feat: implement sse module

* feat: register for newly created accounts

* fix: downgrade eventsource

* chore: upgrade eventsource

* test: add sse server to local backbone

* test: add baseUrlOverride to the SSEModule config

* chore: use better logging solution

* chore: add logs

* test: sse tests

* chore: node-logger > simple-logger

* fix: get rid of all errors logged during testing

* chore: add test logs

* ci: archive backbone logs

* fix: higher timeout

* chore: bump

* chore: debug

* chore: remove old config

* chore: logs

* ci: only test sse

* ci: log correlation id

* fix: test name

* chore: remove logs

* chore: disable sse test

* ci: use 6.30.0 again

* chore: do not save backbone logs

* chore: undo printing correlation id

* chore: backbone loglevel

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
jkoenig134 and mergify[bot] authored Jan 29, 2025
1 parent 03fbd92 commit 4eb70ec
Show file tree
Hide file tree
Showing 27 changed files with 294 additions and 96 deletions.
16 changes: 5 additions & 11 deletions .dev/appsettings.override.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
"Providers": {
"Dummy": {
"Enabled": true
},
"Sse": {
"Enabled": true,
"SseServerBaseAddress": "http://sse-server:8080"
}
}
}
Expand Down Expand Up @@ -126,7 +130,7 @@
}
}
},
"Serilog": {
"Logging": {
"MinimumLevel": {
"Default": "Debug"
},
Expand All @@ -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}"
}
}
}
}
}
15 changes: 15 additions & 0 deletions .dev/compose.backbone.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
54 changes: 29 additions & 25 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions packages/app-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
6 changes: 6 additions & 0 deletions packages/app-runtime/src/AppConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 8 additions & 6 deletions packages/app-runtime/src/AppRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -260,13 +261,14 @@ export class AppRuntime extends Runtime<AppConfig> {
private static moduleRegistry: Record<string, IAppRuntimeModuleConstructor | undefined> = {
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 {
Expand Down Expand Up @@ -317,7 +319,7 @@ export class AppRuntime extends Runtime<AppConfig> {
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;
Expand Down
2 changes: 1 addition & 1 deletion packages/app-runtime/src/AppStringProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AppRuntimeModule, AppRuntimeModuleConfiguration } from "../AppRuntimeModule";
import { AppRuntimeModule, AppRuntimeModuleConfiguration } from "./AppRuntimeModule";

export interface AppSyncModuleConfiguration extends AppRuntimeModuleConfiguration {
interval: number;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {}

Expand Down Expand Up @@ -77,11 +88,10 @@ export class PushNotificationModule extends AppRuntimeModule<PushNotificationMod
await this.registerPushTokenForLocalAccount(event.data.address, tokenResult.value);
}

public async registerPushTokenForLocalAccount(address: string, token: string): Promise<void> {
private async registerPushTokenForLocalAccount(address: string, token: string): Promise<void> {
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);
Expand Down
108 changes: 108 additions & 0 deletions packages/app-runtime/src/modules/SSEModule.ts
Original file line number Diff line number Diff line change
@@ -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<SSEModuleConfiguration> {
private eventSource: Record<string, EventSource | undefined> = {};

public constructor(runtime: AppRuntime, configuration: SSEModuleConfiguration, logger: ILogger) {
super(runtime, configuration, logger);
}

public init(): void {
// Nothing to do here
}

public async start(): Promise<void> {
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<void> {
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<void>((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<void> {
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();
}
}
}
1 change: 0 additions & 1 deletion packages/app-runtime/src/modules/appSync/index.ts

This file was deleted.

Loading

0 comments on commit 4eb70ec

Please sign in to comment.