Skip to content

Commit

Permalink
Make APIs reactive (#65)
Browse files Browse the repository at this point in the history
* use reacticity api for auth state

---------

Co-authored-by: Arne Vogt <[email protected]>
  • Loading branch information
mbeckem and arnevogt authored Oct 22, 2024
1 parent 3d81a76 commit 1ce5f11
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 140 deletions.
38 changes: 38 additions & 0 deletions .changeset/chilled-suits-tease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
"@open-pioneer/authentication-keycloak": minor
"@open-pioneer/authentication": minor
---

Replace change events for auth state wiht signals from Reactivity API

watch for updates of the auth state
```typescript
const myAuthService = ...
watch(
() => [myAuthService.getAuthState()],
([state]) => {
console.log(state);
},
{
immediate: true
}
);
```

The Auth Service forwards the auth state from the underlying AuthPlugin.
Therefore, the plugin implementation must use reactive signals when its auth state changes in order to signal changes to the service.
```typescript
class DummyPlugin implements AuthPlugin {
#state = reactive<AuthState>( {
kind: "not-authenticated"
});

getAuthState(): AuthState {
return this.#state.value;
}

$setAuthState(newState: AuthState) {
this.#state.value = newState;
}
}
```
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

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

26 changes: 4 additions & 22 deletions src/packages/authentication-keycloak/KeycloakAuthPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer)
// SPDX-License-Identifier: Apache-2.0
import { reactive, watch } from "@conterra/reactivity-core";
import {
AuthPlugin,
AuthPluginEvents,
AuthState,
LoginBehavior
} from "@open-pioneer/authentication";
import { EventEmitter, Resource, createLogger, destroyResource } from "@open-pioneer/core";
import { reactive } from "@conterra/reactivity-core";
import { AuthPlugin, AuthState, LoginBehavior } from "@open-pioneer/authentication";
import { Resource, createLogger, destroyResource } from "@open-pioneer/core";
import { NotificationService } from "@open-pioneer/notifier";
import {
PackageIntl,
Expand All @@ -29,10 +24,7 @@ interface References {
notifier: NotificationService;
}

export class KeycloakAuthPlugin
extends EventEmitter<AuthPluginEvents>
implements Service, AuthPlugin
{
export class KeycloakAuthPlugin implements Service, AuthPlugin {
declare [DECLARE_SERVICE_INTERFACE]: "authentication-keycloak.KeycloakAuthPlugin";

#notifier: NotificationService;
Expand All @@ -50,20 +42,11 @@ export class KeycloakAuthPlugin
});

constructor(options: ServiceOptions<References>) {
super();
this.#notifier = options.references.notifier;
this.#intl = options.intl;
this.#logoutOptions = { redirectUri: undefined };
this.#loginOptions = { redirectUri: undefined };

// Backwards compatibility: emit "changed" event when the state changes
this.#watcher = watch(
() => [this.#state.value],
() => {
this.emit("changed");
}
);

try {
this.#keycloakOptions = getKeycloakConfig(options.properties);
} catch (e) {
Expand Down Expand Up @@ -173,7 +156,6 @@ export class KeycloakAuthPlugin
this.#updateState({
kind: "not-authenticated"
});
this.emit("changed");
this.destroy();
});
}, interval);
Expand Down
29 changes: 17 additions & 12 deletions src/packages/authentication/AuthServiceImpl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
/**
* @vitest-environment node
*/
import { EventEmitter } from "@open-pioneer/core";
import { it, expect } from "vitest";
import { AuthPlugin, AuthPluginEvents, AuthState, LoginFallback } from "./api";
import { AuthPlugin, AuthState, LoginFallback } from "./api";
import { createElement } from "react";
import { createService } from "@open-pioneer/test-utils/services";
import { AuthServiceImpl } from "./AuthServiceImpl";
import { reactive, syncWatch } from "@conterra/reactivity-core";

it("forwards the authentication plugin's state changes", async () => {
const plugin = new TestPlugin();
Expand All @@ -18,10 +18,16 @@ it("forwards the authentication plugin's state changes", async () => {
}
});

const observedStates: AuthState[] = [authService.getAuthState()];
authService.on("changed", () => {
observedStates.push(authService.getAuthState());
});
const observedStates: AuthState[] = [];
syncWatch(
() => [authService.getAuthState()],
([state]) => {
observedStates.push(state);
},
{
immediate: true
}
);

plugin.$setAuthState({ kind: "pending" });
plugin.$setAuthState({
Expand Down Expand Up @@ -112,15 +118,15 @@ it("calls the plugin's logout method", async () => {
expect(plugin.$logoutCalled).toBe(1);
});

class TestPlugin extends EventEmitter<AuthPluginEvents> implements AuthPlugin {
#state: AuthState = {
class TestPlugin implements AuthPlugin {
#state = reactive<AuthState>({
kind: "not-authenticated"
};
});

$logoutCalled = 0;

getAuthState(): AuthState {
return this.#state;
return this.#state.value;
}

getLoginBehavior(): LoginFallback {
Expand All @@ -135,8 +141,7 @@ class TestPlugin extends EventEmitter<AuthPluginEvents> implements AuthPlugin {
}

$setAuthState(newState: AuthState) {
this.#state = newState;
this.emit("changed");
this.#state.value = newState;
}
}

Expand Down
48 changes: 21 additions & 27 deletions src/packages/authentication/AuthServiceImpl.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,55 @@
// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer)
// SPDX-License-Identifier: Apache-2.0
import {
EventEmitter,
ManualPromise,
Resource,
createAbortError,
createManualPromise,
destroyResource,
createLogger
} from "@open-pioneer/core";
import type {
AuthEvents,
AuthPlugin,
AuthService,
AuthState,
LoginBehavior,
SessionInfo
} from "./api";
import type { AuthPlugin, AuthService, AuthState, LoginBehavior, SessionInfo } from "./api";
import type { Service, ServiceOptions } from "@open-pioneer/runtime";
import { syncWatch } from "@conterra/reactivity-core";

const LOG = createLogger("authentication:AuthService");

export class AuthServiceImpl extends EventEmitter<AuthEvents> implements AuthService, Service {
export class AuthServiceImpl implements AuthService, Service {
#plugin: AuthPlugin;
#currentState: AuthState;
#whenUserInfo: ManualPromise<SessionInfo | undefined> | undefined;
#eventHandle: Resource | undefined;
#watchPluginStateHandle: Resource | undefined;

constructor(serviceOptions: ServiceOptions<{ plugin: AuthPlugin }>) {
super();
this.#plugin = serviceOptions.references.plugin;

// Init from plugin state and watch for changes.
this.#currentState = this.#plugin.getAuthState();
this.#eventHandle = this.#plugin.on?.("changed", () => this.#onPluginStateChanged());
this.#watchPluginStateHandle = syncWatch(
() => [this.#plugin.getAuthState()],
([state]) => {
this.#onPluginStateChanged(state);
},
{
immediate: false
}
);
LOG.debug(
`Constructed with initial auth state '${this.#currentState.kind}'`,
this.#currentState
`Constructed with initial auth state '${this.getAuthState().kind}'`,
this.getAuthState()
);
}

destroy(): void {
this.#whenUserInfo?.reject(createAbortError());
this.#whenUserInfo = undefined;
this.#eventHandle = destroyResource(this.#eventHandle);
this.#watchPluginStateHandle = destroyResource(this.#watchPluginStateHandle);
}

getAuthState(): AuthState {
return this.#currentState;
return this.#plugin.getAuthState();
}

getSessionInfo(): Promise<SessionInfo | undefined> {
if (this.#currentState.kind !== "pending") {
return Promise.resolve(getSessionInfo(this.#currentState));
if (this.getAuthState().kind !== "pending") {
return Promise.resolve(getSessionInfo(this.getAuthState()));
}

if (!this.#whenUserInfo) {
Expand All @@ -70,15 +67,12 @@ export class AuthServiceImpl extends EventEmitter<AuthEvents> implements AuthSer
this.#plugin.logout();
}

#onPluginStateChanged() {
const newState = this.#plugin.getAuthState();
this.#currentState = newState;
#onPluginStateChanged(newState: AuthState) {
if (newState.kind !== "pending" && this.#whenUserInfo) {
this.#whenUserInfo.resolve(getSessionInfo(newState));
this.#whenUserInfo = undefined;
}
LOG.debug(`Auth state changed to '${this.#currentState.kind}'`, this.#currentState);
this.emit("changed");
LOG.debug(`Auth state changed to '${newState.kind}'`, newState);
}
}

Expand Down
16 changes: 7 additions & 9 deletions src/packages/authentication/ForceAuth.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer)
// SPDX-License-Identifier: Apache-2.0
import { EventEmitter } from "@open-pioneer/core";
import { PackageContextProvider } from "@open-pioneer/test-utils/react";
import { act, render, screen, waitFor } from "@testing-library/react";
import { expect, it } from "vitest";
import { reactive, Reactive } from "@conterra/reactivity-core";
import { ErrorFallbackProps, ForceAuth } from "./ForceAuth";
import { AuthEvents, AuthService, AuthState, LoginBehavior, SessionInfo } from "./api";
import { AuthState, LoginBehavior, SessionInfo } from "./api";
import { Box } from "@open-pioneer/chakra-integration";

it("renders children if the user is authenticated", async () => {
Expand Down Expand Up @@ -283,12 +283,11 @@ it("should use renderErrorFallback property rather than errorFallback property i
expect(result.innerHTML).toEqual(renderErrorFallbackInner);
});

class TestAuthService extends EventEmitter<AuthEvents> implements AuthService {
#currentState: AuthState;
class TestAuthService {
#currentState: Reactive<AuthState>;
#behavior: LoginBehavior;
constructor(initState: AuthState, loginBehavior?: LoginBehavior) {
super();
this.#currentState = initState;
this.#currentState = reactive<AuthState>(initState);
this.#behavior = loginBehavior ?? {
kind: "fallback",
Fallback(props: Record<string, unknown>) {
Expand All @@ -297,7 +296,7 @@ class TestAuthService extends EventEmitter<AuthEvents> implements AuthService {
};
}
getAuthState(): AuthState {
return this.#currentState;
return this.#currentState.value;
}
getSessionInfo(): Promise<SessionInfo | undefined> {
throw new Error("Method not implemented.");
Expand All @@ -309,7 +308,6 @@ class TestAuthService extends EventEmitter<AuthEvents> implements AuthService {
throw new Error("Method not implemented.");
}
setAuthState(newState: AuthState) {
this.#currentState = newState;
this.emit("changed");
this.#currentState.value = newState;
}
}
Loading

0 comments on commit 1ce5f11

Please sign in to comment.