Skip to content

Commit d4078aa

Browse files
committed
add IndexedDbSessionDatabase fallback
1 parent b5c8420 commit d4078aa

3 files changed

Lines changed: 168 additions & 3 deletions

File tree

jest.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export default {
1313
},
1414
moduleNameMapper: {
1515
'^@uvdsl/solid-oidc-client-browser$': '<rootDir>/test/mocks/solid-oidc-client-browser.ts',
16+
'^@uvdsl/solid-oidc-client-browser/core$': '<rootDir>/test/mocks/solid-oidc-client-browser.ts',
1617
},
1718
setupFilesAfterEnv: ['./test/helpers/setup.ts'],
1819
testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],

src/authSession/authSession.ts

Lines changed: 135 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import {
2-
Session,
2+
Session as WebSession,
33
} from '@uvdsl/solid-oidc-client-browser'
4+
import {
5+
SessionCore,
6+
} from '@uvdsl/solid-oidc-client-browser/core'
7+
import type { Session as OidcSession, SessionDatabase } from '@uvdsl/solid-oidc-client-browser/core'
48

59
type LegacyEventName = 'login' | 'logout' | 'sessionRestore'
610
type LegacyEventHandler = (...args: unknown[]) => void
@@ -30,9 +34,137 @@ export class SessionEvents {
3034
}
3135
}
3236

33-
export type SessionWithLegacyEvents = Session & { events: SessionEvents }
37+
type SessionCompatibilityShape = {
38+
webId?: string
39+
isActive?: boolean
40+
info?: {
41+
webId?: string
42+
isLoggedIn?: boolean
43+
}
44+
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
45+
authFetch?: (input: string | URL | Request, init?: RequestInit, dpopPayload?: any) => Promise<Response>
46+
}
47+
48+
export type SessionWithLegacyEvents = OidcSession & SessionCompatibilityShape & { events: SessionEvents }
49+
50+
class MemorySessionDatabase implements SessionDatabase {
51+
private readonly map = new Map<string, any>()
52+
53+
async init (): Promise<SessionDatabase> {
54+
return this
55+
}
56+
57+
async setItem (id: string, value: any): Promise<void> {
58+
this.map.set(id, value)
59+
}
60+
61+
async getItem (id: string): Promise<any> {
62+
return this.map.has(id) ? this.map.get(id) : null
63+
}
64+
65+
async deleteItem (id: string): Promise<void> {
66+
this.map.delete(id)
67+
}
68+
69+
async clear (): Promise<void> {
70+
this.map.clear()
71+
}
72+
73+
close (): void {
74+
// No-op for in-memory database
75+
}
76+
}
77+
78+
class IndexedDbSessionDatabase implements SessionDatabase {
79+
private db: IDBDatabase | null = null
80+
private readonly dbName = 'soidc'
81+
private readonly storeName = 'session'
82+
private readonly dbVersion = 1
83+
84+
async init (): Promise<SessionDatabase> {
85+
if (this.db) return this
86+
87+
await new Promise<void>((resolve, reject) => {
88+
const request = indexedDB.open(this.dbName, this.dbVersion)
89+
90+
request.onerror = () => reject(request.error)
91+
request.onsuccess = () => {
92+
this.db = request.result
93+
resolve()
94+
}
95+
request.onupgradeneeded = () => {
96+
const db = request.result
97+
if (!db.objectStoreNames.contains(this.storeName)) {
98+
db.createObjectStore(this.storeName)
99+
}
100+
}
101+
})
102+
103+
return this
104+
}
105+
106+
async setItem (id: string, value: any): Promise<void> {
107+
await this.init()
108+
await this.withStore('readwrite', store => store.put(value, id))
109+
}
110+
111+
async getItem (id: string): Promise<any> {
112+
await this.init()
113+
return this.withStore('readonly', store => store.get(id))
114+
}
115+
116+
async deleteItem (id: string): Promise<void> {
117+
await this.init()
118+
await this.withStore('readwrite', store => store.delete(id))
119+
}
120+
121+
async clear (): Promise<void> {
122+
await this.init()
123+
await this.withStore('readwrite', store => store.clear())
124+
}
125+
126+
close (): void {
127+
if (this.db) {
128+
this.db.close()
129+
this.db = null
130+
}
131+
}
132+
133+
private withStore(mode: IDBTransactionMode, op: (store: IDBObjectStore) => IDBRequest<any>): Promise<any> {
134+
return new Promise<any>((resolve, reject) => {
135+
if (!this.db) {
136+
reject(new Error('Session database not initialized'))
137+
return
138+
}
139+
140+
const tx = this.db.transaction(this.storeName, mode)
141+
const store = tx.objectStore(this.storeName)
142+
const request = op(store)
143+
144+
request.onerror = () => reject(request.error)
145+
request.onsuccess = () => resolve(request.result ?? null)
146+
})
147+
}
148+
}
149+
150+
function createSession (): OidcSession {
151+
try {
152+
return new WebSession()
153+
} catch (error) {
154+
// In some deployments, worker URL resolution can become file:// and fail cross-origin.
155+
// Fall back to SessionCore so auth still works without background refresh worker.
156+
// Use IndexedDB to keep refresh-token persistence across page reloads.
157+
console.warn('solid-logic: falling back to non-worker auth session:', error)
158+
try {
159+
return new SessionCore(undefined, { database: new IndexedDbSessionDatabase() })
160+
} catch (dbError) {
161+
console.warn('solid-logic: IndexedDB unavailable, using in-memory session database:', dbError)
162+
return new SessionCore(undefined, { database: new MemorySessionDatabase() })
163+
}
164+
}
165+
}
34166

35-
const _session = new Session()
167+
const _session = createSession()
36168
const events = new SessionEvents()
37169

38170
// Emit the legacy 'logout' event when the session transitions from active to inactive.

test/mocks/solid-oidc-client-browser.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,35 @@ export class Session {
5151
return globalThis.fetch(input, init)
5252
}
5353
}
54+
55+
export class SessionCore extends Session {
56+
constructor(_clientDetails?: unknown, _sessionOptions?: unknown) {
57+
super()
58+
}
59+
}
60+
61+
export class SessionIDB {
62+
async init(): Promise<SessionIDB> {
63+
return this
64+
}
65+
66+
async setItem(_id: string, _value: any): Promise<void> {
67+
return
68+
}
69+
70+
async getItem(_id: string): Promise<any> {
71+
return null
72+
}
73+
74+
async deleteItem(_id: string): Promise<void> {
75+
return
76+
}
77+
78+
async clear(): Promise<void> {
79+
return
80+
}
81+
82+
close(): void {
83+
return
84+
}
85+
}

0 commit comments

Comments
 (0)