Skip to content
128 changes: 114 additions & 14 deletions packages/browser/src/__legacy__/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ const DefaultConfig: Partial<AuthClientConfig<Config>> = {

/**
* This class provides the necessary methods to implement authentication in a Single Page Application.
* Implements a Multiton pattern to support multi-tenancy scenarios where multiple authentication
* contexts need to coexist in the same application.
*
* @export
* @class AsgardeoSPAClient
Expand Down Expand Up @@ -170,15 +172,22 @@ export class AsgardeoSPAClient {
}

/**
* This method returns the instance of the singleton class.
* This method returns the instance of the client for the specified ID.
* Implements a Multiton pattern to support multiple authentication contexts.
* If an ID is provided, it will return the instance with the given ID.
* If no ID is provided, it will return the default instance value 0.
* If no ID is provided, it will return the default instance (ID: 0).
*
* @return {AsgardeoSPAClient} - Returns the instance of the singleton class.
* @param {number} id - Optional unique identifier for the instance.
* @return {AsgardeoSPAClient} - Returns the instance associated with the ID.
*
* @example
* ```
* // Single tenant application (default instance)
* const auth = AsgardeoSPAClient.getInstance();
*
* // Multi-instance application
* const instance1 = AsgardeoSPAClient.getInstance(1);
* const instance2 = AsgardeoSPAClient.getInstance(2);
* ```
*
* @link https://github.com/asgardeo/asgardeo-auth-spa-sdk/tree/master#getinstance
Expand All @@ -187,22 +196,113 @@ export class AsgardeoSPAClient {
*
* @preserve
*/
public static getInstance(id?: number): AsgardeoSPAClient | undefined {
if (id && this._instances?.get(id)) {
return this._instances.get(id);
} else if (!id && this._instances?.get(0)) {
return this._instances.get(0);
public static getInstance(id: number = 0): AsgardeoSPAClient {
if (!this._instances.has(id)) {
this._instances.set(id, new AsgardeoSPAClient(id));
}

if (id) {
this._instances.set(id, new AsgardeoSPAClient(id));
return this._instances.get(id)!;
}

return this._instances.get(id);
}
/**
* This method checks if an instance exists for the given ID.
*
* @param {number} id - Optional unique identifier for the instance.
* @return {boolean} - Returns true if an instance exists for the ID.
*
* @example
* ```
* if (AsgardeoSPAClient.hasInstance(1)) {
* const auth = AsgardeoSPAClient.getInstance(1);
* }
* ```
*
* @memberof AsgardeoSPAClient
*
* @preserve
*/
public static hasInstance(id: number = 0): boolean {
return this._instances.has(id);
}

this._instances.set(0, new AsgardeoSPAClient(0));
/**
* This method removes and cleans up a specific instance.
* Useful when an instance is no longer needed.
*
* @param {number} id - Optional unique identifier for the instance to destroy.
* @return {boolean} - Returns true if the instance was found and removed.
*
* @example
* ```
* // Remove a specific instance
* AsgardeoSPAClient.destroyInstance(1);
*
* // Remove the default instance
* AsgardeoSPAClient.destroyInstance();
* ```
*
* @memberof AsgardeoSPAClient
*
* @preserve
*/
public static destroyInstance(id: number = 0): boolean {
return this._instances.delete(id);
}

return this._instances.get(0);
/**
* This method returns all active instance IDs.
* Useful for debugging or managing multiple instances.
*
* @return {number[]} - Returns an array of all active instance IDs.
*
* @example
* ```
* const activeInstances = AsgardeoSPAClient.getInstanceKeys();
* console.log('Active instances:', activeInstances);
* ```
*
* @memberof AsgardeoSPAClient
*
* @preserve
*/
public static getInstanceKeys(): number[] {
return Array.from(this._instances.keys());
}

/**
* This method removes all instances.
* Useful for cleanup in testing scenarios or application teardown.
*
* @example
* ```
* AsgardeoSPAClient.destroyAllInstances();
* ```
*
* @memberof AsgardeoSPAClient
*
* @preserve
*/
public static destroyAllInstances(): void {
this._instances.clear();
}

/**
* This method returns the instance ID for this client instance.
*
* @return {number} - The instance ID.
*
* @example
* ```
* const auth = AsgardeoSPAClient.getInstance(1);
* console.log(auth.getInstanceId()); // 1
* ```
*
* @memberof AsgardeoSPAClient
*
* @preserve
*/
public getInstanceId(): number {
return this._instanceID;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export const MainThreadClient = async (

let _getSignOutURLFromSessionStorage: boolean = false;

const _httpClient: HttpClientInstance = HttpClient.getInstance();
const _httpClient: HttpClientInstance = HttpClient.getInstance(instanceID);
let _isHttpHandlerEnabled: boolean = true;
let _httpErrorCallback: (error: HttpError) => void | Promise<void>;
let _httpFinishCallback: () => void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ import {HttpClientInstance, HttpClientInterface, HttpClientStatic} from '../mode
*/
@staticDecorator<HttpClientStatic<HttpClientInstance>>()
export class HttpClient implements HttpClientInterface<HttpRequestConfig, HttpResponse, HttpError> {
private static axiosInstance: HttpClientInstance;
private static clientInstance: HttpClient;
private static isHandlerEnabled: boolean;
private static instances: Map<number, HttpClientInstance> = new Map();
private static clientInstances: Map<number, HttpClient> = new Map();
private isHandlerEnabled: boolean = true;
private attachToken: (request: HttpRequestConfig) => Promise<void> = () => Promise.resolve();
private requestStartCallback: (request: HttpRequestConfig) => void = () => null;
private requestSuccessCallback: (response: HttpResponse) => void = () => null;
Expand All @@ -66,48 +66,51 @@ export class HttpClient implements HttpClientInterface<HttpRequestConfig, HttpRe
}

/**
* Returns an aggregated instance of type `HttpInstance` of `HttpClient`.
* Returns an instance-specific HttpClient instance.
* Each instance ID gets its own axios instance and HttpClient to avoid state conflicts.
*
* @return {any}
* @param instanceId - The instance ID for multi-auth context support. Defaults to 0.
* @return {HttpClientInstance}
*/
public static getInstance(): HttpClientInstance {
if (this.axiosInstance) {
return this.axiosInstance;
public static getInstance(instanceId: number = 0): HttpClientInstance {
if (this.instances.has(instanceId)) {
return this.instances.get(instanceId)!;
}

this.axiosInstance = axios.create({
const axiosInstance = axios.create({
withCredentials: true,
});
}) as HttpClientInstance;

if (!this.clientInstance) {
this.clientInstance = new HttpClient();
}
const clientInstance = new HttpClient();
this.clientInstances.set(instanceId, clientInstance);

// Register request interceptor
this.axiosInstance.interceptors.request.use(async request => await this.clientInstance.requestHandler(request as HttpRequestConfig));
axiosInstance.interceptors.request.use(async request => await clientInstance.requestHandler(request as HttpRequestConfig));

// Register response interceptor
this.axiosInstance.interceptors.response.use(
response => this.clientInstance.successHandler(response),
error => this.clientInstance.errorHandler(error),
axiosInstance.interceptors.response.use(
response => clientInstance.successHandler(response),
error => clientInstance.errorHandler(error),
);

// Add the missing helper methods from axios
this.axiosInstance.all = axios.all;
this.axiosInstance.spread = axios.spread;
axiosInstance.all = axios.all;
axiosInstance.spread = axios.spread;

// Add the init method from the `HttpClient` instance.
this.axiosInstance.init = this.clientInstance.init;
axiosInstance.init = clientInstance.init;

// Add the handler enabling & disabling methods to the instance.
this.axiosInstance.enableHandler = this.clientInstance.enableHandler;
this.axiosInstance.disableHandler = this.clientInstance.disableHandler;
this.axiosInstance.disableHandlerWithTimeout = this.clientInstance.disableHandlerWithTimeout;
this.axiosInstance.setHttpRequestStartCallback = this.clientInstance.setHttpRequestStartCallback;
this.axiosInstance.setHttpRequestSuccessCallback = this.clientInstance.setHttpRequestSuccessCallback;
this.axiosInstance.setHttpRequestErrorCallback = this.clientInstance.setHttpRequestErrorCallback;
this.axiosInstance.setHttpRequestFinishCallback = this.clientInstance.setHttpRequestFinishCallback;
return this.axiosInstance;
axiosInstance.enableHandler = clientInstance.enableHandler;
axiosInstance.disableHandler = clientInstance.disableHandler;
axiosInstance.disableHandlerWithTimeout = clientInstance.disableHandlerWithTimeout;
axiosInstance.setHttpRequestStartCallback = clientInstance.setHttpRequestStartCallback;
axiosInstance.setHttpRequestSuccessCallback = clientInstance.setHttpRequestSuccessCallback;
axiosInstance.setHttpRequestErrorCallback = clientInstance.setHttpRequestErrorCallback;
axiosInstance.setHttpRequestFinishCallback = clientInstance.setHttpRequestFinishCallback;

this.instances.set(instanceId, axiosInstance);
return axiosInstance;
}

/**
Expand All @@ -134,7 +137,7 @@ export class HttpClient implements HttpClientInterface<HttpRequestConfig, HttpRe

request.startTimeInMs = new Date().getTime();

if (HttpClient.isHandlerEnabled) {
if (this.isHandlerEnabled) {
if (this.requestStartCallback && typeof this.requestStartCallback === 'function') {
this.requestStartCallback(request);
}
Expand All @@ -151,7 +154,7 @@ export class HttpClient implements HttpClientInterface<HttpRequestConfig, HttpRe
* @return {HttpError}
*/
public errorHandler(error: HttpError): HttpError {
if (HttpClient.isHandlerEnabled) {
if (this.isHandlerEnabled) {
if (this.requestErrorCallback && typeof this.requestErrorCallback === 'function') {
this.requestErrorCallback(error);
}
Expand All @@ -171,7 +174,7 @@ export class HttpClient implements HttpClientInterface<HttpRequestConfig, HttpRe
* @return {HttpResponse}
*/
public successHandler(response: HttpResponse): HttpResponse {
if (HttpClient.isHandlerEnabled) {
if (this.isHandlerEnabled) {
if (this.requestSuccessCallback && typeof this.requestSuccessCallback === 'function') {
this.requestSuccessCallback(response);
}
Expand All @@ -195,22 +198,22 @@ export class HttpClient implements HttpClientInterface<HttpRequestConfig, HttpRe
isHandlerEnabled = true,
attachToken: (request: HttpRequestConfig) => Promise<void>,
): Promise<void> {
HttpClient.isHandlerEnabled = isHandlerEnabled;
this.isHandlerEnabled = isHandlerEnabled;
this.attachToken = attachToken;
}

/**
* Enables the handler.
*/
public enableHandler(): void {
HttpClient.isHandlerEnabled = true;
this.isHandlerEnabled = true;
}

/**
* Disables the handler.
*/
public disableHandler(): void {
HttpClient.isHandlerEnabled = false;
this.isHandlerEnabled = false;
}

/**
Expand All @@ -219,10 +222,10 @@ export class HttpClient implements HttpClientInterface<HttpRequestConfig, HttpRe
* @param {number} timeout - Timeout in milliseconds.
*/
public disableHandlerWithTimeout(timeout: number = HttpClient.DEFAULT_HANDLER_DISABLE_TIMEOUT): void {
HttpClient.isHandlerEnabled = false;
this.isHandlerEnabled = false;

setTimeout(() => {
HttpClient.isHandlerEnabled = true;
this.isHandlerEnabled = true;
}, timeout);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { HttpError, HttpResponse } from "../../models";
* Http client interface with static functions.
*/
export interface HttpClientStatic<S> {
getInstance(): S;
getInstance(instanceId?: number): S;
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export * from './__legacy__/worker/worker-receiver';
export {AsgardeoBrowserConfig} from './models/config';

export {default as hasAuthParamsInUrl} from './utils/hasAuthParamsInUrl';
export {default as hasCalledForThisInstanceInUrl} from './utils/hasCalledForThisInstanceInUrl';
export {default as navigate} from './utils/navigate';

export {default as AsgardeoBrowserClient} from './AsgardeoBrowserClient';
Expand Down
32 changes: 32 additions & 0 deletions packages/browser/src/utils/hasCalledForThisInstanceInUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/**
* Utility to check if `state` is available in the URL as a search param and matches the provided instance.
*
* @param params - The URL search params to check. Defaults to `window.location.search`.
* @param instanceId - The instance ID to match against the `state` param.
* @return `true` if the URL contains a matching `state` search param, otherwise `false`.
*/
const hasCalledForThisInstanceInUrl = (instanceId: number, params: string = window.location.search): boolean => {
const MATCHER: RegExp = new RegExp(`[?&]state=instance_${instanceId}-[^&]+`);

return MATCHER.test(params);
};

export default hasCalledForThisInstanceInUrl;
Loading
Loading