Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions npm/ng-packs/packages/oauth/ng-package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
"lib": {
"entryFile": "src/public-api.ts"
},
"assets": [
{
"glob": "token-storage.worker.js",
"input": "src/lib/workers",
"output": "workers"
}
],
"allowedNonPeerDependencies": [
"@abp/utils",
"@abp/ng.core",
Expand Down
1 change: 1 addition & 0 deletions npm/ng-packs/packages/oauth/src/lib/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './oauth-error-filter.service';
export * from './remember-me.service';
export * from './browser-token-storage.service';
export * from './server-token-storage.service';
export * from './memory-token-storage.service';
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { DOCUMENT, inject, Injectable } from '@angular/core';
import { OAuthStorage } from 'angular-oauth2-oidc';
import { AbpLocalStorageService } from '@abp/ng.core';

@Injectable({
providedIn: 'root',
})
export class MemoryTokenStorageService implements OAuthStorage {
private keysShouldStoreInMemory = [
'access_token', 'id_token', 'expires_at',
'id_token_claims_obj', 'id_token_expires_at',
'id_token_stored_at', 'access_token_stored_at',
'abpOAuthClientId', 'granted_scopes'
];

private worker?: any;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private worker?: SharedWorker;
It would be better to add a proper type for worker instead of leaving it as an optional SharedWorker. This improves clarity and type safety.

private port?: MessagePort;
private cache = new Map<string, string>();
private localStorageService = inject(AbpLocalStorageService);
private _document = inject(DOCUMENT);
private useSharedWorker = false;

constructor() {
this.initializeStorage();
}

private initializeStorage(): void {
// @ts-ignore
if (typeof SharedWorker !== 'undefined') {
try {
// @ts-ignore
this.worker = new SharedWorker(
Comment on lines +28 to +32
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace @ts-ignore comments with proper TypeScript type declarations or type guards. The repeated use of @ts-ignore suppresses type checking and can hide potential runtime errors.

Copilot uses AI. Check for mistakes.
new URL(
'../workers/token-storage.worker.js',
import.meta.url
),
{ name: 'oauth-token-storage', type: "module" }
);

this.port = this.worker.port;
this.port.start();
this.useSharedWorker = true;

this.port.onmessage = (event) => {
const { action, key, value } = event.data;

switch (action) {
case 'set':
this.checkAuthStateChanges(key);
this.cache.set(key, value);
break;
case 'remove':
this.cache.delete(key);
this.refreshDocument();
break;
case 'clear':
this.cache.clear();
this.refreshDocument();
break;
}
};
} catch (error) {
this.useSharedWorker = false;
}
} else {
this.useSharedWorker = false;
}
}

getItem(key: string): string | null {
if (!this.keysShouldStoreInMemory.includes(key)) {
return this.localStorageService.getItem(key);
}
return this.cache.get(key) || null;
}

setItem(key: string, value: string): void {
if (!this.keysShouldStoreInMemory.includes(key)) {
this.localStorageService.setItem(key, value);
return;
}

if (this.useSharedWorker && this.port) {
this.cache.set(key, value);
this.port.postMessage({ action: 'set', key, value });
} else {
this.cache.set(key, value);
}
}

removeItem(key: string): void {
if (!this.keysShouldStoreInMemory.includes(key)) {
this.localStorageService.removeItem(key);
return;
}

if (this.useSharedWorker && this.port) {
this.cache.delete(key);
this.port.postMessage({ action: 'remove', key });
} else {
this.cache.delete(key);
}
}

clear(): void {
if (this.useSharedWorker && this.port) {
this.port.postMessage({ action: 'clear' });
}
this.cache.clear();
}

private checkAuthStateChanges = (key: string) => {
if (key === 'access_token' && !this.cache.get('access_token')) {
this.refreshDocument();
}
}

private refreshDocument(): void {
this._document.defaultView?.location.reload();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,12 @@ export class AbpOAuthService implements IAuthService {
this.document.defaultView?.location.replace('/authorize');
return Promise.resolve();
}
return this.oAuthService.refreshToken();
try {
return this.oAuthService.refreshToken();
} catch (error) {
console.log("Error while refreshing token: ", error);
return Promise.reject();
}
}

getAccessTokenExpiration(): number {
Expand Down
6 changes: 3 additions & 3 deletions npm/ng-packs/packages/oauth/src/lib/utils/storage.factory.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { ServerTokenStorageService } from '../services/server-token-storage.service';
import { BrowserTokenStorageService } from '../services';
import { BrowserTokenStorageService, MemoryTokenStorageService } from '../services';
import { OAuthStorage } from 'angular-oauth2-oidc';
import { AbpLocalStorageService, APP_STARTED_WITH_SSR } from '@abp/ng.core';
import { APP_STARTED_WITH_SSR } from '@abp/ng.core';

export class MockStorage implements Storage {
private data = new Map<string, string>();
Expand Down Expand Up @@ -35,5 +35,5 @@ export function oAuthStorageFactory(): OAuthStorage {
? inject(BrowserTokenStorageService)
: inject(ServerTokenStorageService);
}
return inject(AbpLocalStorageService);
return inject(MemoryTokenStorageService);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// ESM SharedWorker

const tokenStore = new Map();
const ports = new Set();

self.onconnect = (event) => {
const port = event.ports[0];
ports.add(port);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like there’s a potential memory leak issue here.
When closing a tab, the number of ports continues to increase, which indicates that the reference to the closed tab is not being properly released. This could negatively impact performance over time.

Steps to reproduce:

  1. Run the project and open it in 3 different tabs.
  2. Close 2 of the tabs.
  3. Open 2 new tabs again.

At this point, you’ll notice that the ports array contains 5 elements instead of 3.
The closed tab’s port should be removed from the ports array when the tab is closed.

image

To fix this issue, we probably need to introduce a new **disconnect** action type (or a similar mechanism) to properly handle tab disconnections.
When a tab is closed, we should dispatch this action to remove the corresponding port from the ports array, ensuring that stale connections don’t accumulate and cause memory leaks.

port.onmessage = (e) => {
const { action, key, value } = e.data;
switch (action) {
case 'set':
if (key && value !== undefined) {
tokenStore.set(key, value);
ports.forEach(p => {
if (p !== port) {
p.postMessage({ action: 'set', key, value });
}
});
}
break;
case 'remove':
if (key) {
tokenStore.delete(key);
ports.forEach(p => {
if (p !== port) {
p.postMessage({ action: 'remove', key });
}
});
}
break;
case 'clear':
tokenStore.clear();
ports.forEach(p => {
if (p !== port) {
p.postMessage({ action: 'clear' });
}
});
break;
case 'get':
if (key) {
const value = tokenStore.get(key) ?? null;
port.postMessage({ action: 'get', key, value });
}
break;
}
};

port.start();
};

export {};
Loading