@katlux/providers 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,312 @@
1
+ import { ACacheProvider } from './ACacheProvider'
2
+ import type { ICacheEntry } from '../types'
3
+
4
+ export class CIndexedDBCache extends ACacheProvider {
5
+ private static instance: CIndexedDBCache | null = null
6
+
7
+ public static getInstance(): CIndexedDBCache {
8
+ if (!this.instance) {
9
+ this.instance = new CIndexedDBCache()
10
+ }
11
+ return this.instance
12
+ }
13
+
14
+ private dbName = 'KatluxDB'
15
+ private storeName = 'cache'
16
+ private version = 2
17
+ private dbPromise: Promise<IDBDatabase> | null = null
18
+
19
+ public setStore(storeName: string = "cache") {
20
+ this.storeName = storeName || 'cache'
21
+ if (import.meta.client) {
22
+ this.dbPromise = this.openDB()
23
+ this._hydrateFromPayload()
24
+ }
25
+ }
26
+
27
+ private constructor() {
28
+ super()
29
+ this.setStore()
30
+ }
31
+
32
+ private _hydrateFromPayload() {
33
+ try {
34
+ const nuxtApp = typeof useNuxtApp === 'function' ? useNuxtApp() : null
35
+ const payloadData = nuxtApp?.payload?.data || (globalThis as any).__NUXT__?.payload?.data
36
+ if (!payloadData) return
37
+
38
+ const prefix = 'cache:'
39
+ let count = 0
40
+ for (const key in payloadData) {
41
+ if (key.startsWith(prefix)) {
42
+ const value = payloadData[key]
43
+ if (value && typeof value === 'object' && 'data' in value && 'timestamp' in value && 'lifetime' in value) {
44
+ this.dbPromise?.then(async (db) => {
45
+ try {
46
+ const tx = db.transaction(this.storeName, 'readwrite')
47
+ const store = tx.objectStore(this.storeName)
48
+ store.put(value, key)
49
+ tx.commit?.()
50
+ } catch (e) {
51
+ // Silent fail
52
+ }
53
+ })
54
+ count++
55
+ }
56
+ }
57
+ }
58
+ if (count > 0) console.log(`[CIndexedDBCache] Hydrated ${count} entries from payload`)
59
+ } catch (e) {
60
+ // Silent fail
61
+ }
62
+ }
63
+
64
+ private openDB(): Promise<IDBDatabase> {
65
+ return new Promise((resolve, reject) => {
66
+ const request = indexedDB.open(this.dbName, this.version)
67
+ request.onerror = () => reject(request.error)
68
+ request.onsuccess = () => resolve(request.result)
69
+ request.onupgradeneeded = (event) => {
70
+ const db = (event.target as IDBOpenDBRequest).result
71
+ if (!db.objectStoreNames.contains(this.storeName)) {
72
+ db.createObjectStore(this.storeName)
73
+ }
74
+ }
75
+ })
76
+ }
77
+
78
+ private async getStore(mode: IDBTransactionMode): Promise<IDBObjectStore> {
79
+ if (!this.dbPromise) return Promise.reject("IndexedDB not initialized")
80
+ const db = await this.dbPromise
81
+ return db.transaction(this.storeName, mode).objectStore(this.storeName)
82
+ }
83
+
84
+ async getKeyList(nuxtApp?: any): Promise<Array<string>> {
85
+ if (!import.meta.client || !this.dbPromise) return []
86
+ const db = await this.dbPromise
87
+ const store = db.transaction(this.storeName, 'readonly').objectStore(this.storeName)
88
+ const keys = await new Promise<string[]>((resolve, reject) => {
89
+ const req = store.getAllKeys()
90
+ req.onsuccess = () => resolve(req.result as string[])
91
+ req.onerror = () => reject(req.error)
92
+ })
93
+ const prefix = 'cache:'
94
+ return keys
95
+ .filter(key => key.startsWith(prefix))
96
+ .map(key => key.substring(prefix.length))
97
+ }
98
+
99
+ async get<T>(key: string, nuxtApp?: any): Promise<T | null> {
100
+ const prefixedKey = 'cache:' + key
101
+
102
+ if (import.meta.client) {
103
+ try {
104
+ const store = await this.getStore('readonly')
105
+ const data = await new Promise<ICacheEntry<T> | undefined>((resolve, reject) => {
106
+ const req = store.get(prefixedKey)
107
+ req.onsuccess = () => resolve(req.result)
108
+ req.onerror = () => reject(req.error)
109
+ })
110
+
111
+ if (data) {
112
+ if (Date.now() - data.timestamp <= data.lifetime) {
113
+ console.log(`[CIndexedDBCache] GET SUCCESS: ${key}`)
114
+ return data.data
115
+ } else {
116
+ console.log(`[CIndexedDBCache] EXPIRED: ${key}`)
117
+ const writeStore = await this.getStore('readwrite')
118
+ writeStore.delete(prefixedKey)
119
+ }
120
+ } else {
121
+ console.log(`[CIndexedDBCache] NOT IN DB: ${key}`)
122
+ }
123
+ } catch (e) {
124
+ // Silent fail
125
+ }
126
+ }
127
+
128
+ // Check SSR Payload
129
+ try {
130
+ let payloadData: any = null
131
+ if (import.meta.client) {
132
+ payloadData = (globalThis as any).__NUXT__?.payload?.data
133
+ } else if (nuxtApp) {
134
+ payloadData = (nuxtApp as any).payload?.data || (nuxtApp as any).ssrContext?.payload?.data
135
+ }
136
+
137
+ if (payloadData && payloadData[prefixedKey]) {
138
+ const entry = payloadData[prefixedKey] as ICacheEntry<T>
139
+ if (Date.now() - entry.timestamp <= entry.lifetime) {
140
+ if (import.meta.client) {
141
+ this.set(key, entry.data, entry.lifetime, nuxtApp)
142
+ }
143
+ return entry.data
144
+ }
145
+ }
146
+ } catch (e) {
147
+ // Silent fail
148
+ }
149
+
150
+ return null
151
+ }
152
+
153
+ async set<T>(key: string, data: T, lifetime: number, nuxtApp?: any): Promise<void> {
154
+ const prefixedKey = 'cache:' + key
155
+ const entry: ICacheEntry<T> = {
156
+ data,
157
+ timestamp: Date.now(),
158
+ lifetime
159
+ }
160
+
161
+ if (import.meta.server && nuxtApp) {
162
+ try {
163
+ const payload = (nuxtApp as any).payload?.data || (nuxtApp as any).ssrContext?.payload?.data
164
+ if (payload) {
165
+ payload[prefixedKey] = entry
166
+ }
167
+ } catch (e) {
168
+ // Silent fail
169
+ }
170
+ }
171
+
172
+ if (import.meta.client) {
173
+ try {
174
+ const store = await this.getStore('readwrite')
175
+ await new Promise<void>((resolve, reject) => {
176
+ const req = store.put(entry, prefixedKey)
177
+ req.onsuccess = () => resolve()
178
+ req.onerror = () => reject(req.error)
179
+ })
180
+ } catch (e) {
181
+ // Silent fail
182
+ }
183
+ }
184
+ }
185
+
186
+ async remove(key: string, nuxtApp?: any): Promise<void> {
187
+ const prefixedKey = 'cache:' + key
188
+ if (import.meta.client) {
189
+ try {
190
+ const store = await this.getStore('readwrite')
191
+ await new Promise<void>((resolve, reject) => {
192
+ const req = store.delete(prefixedKey)
193
+ req.onsuccess = () => resolve()
194
+ req.onerror = () => reject(req.error)
195
+ })
196
+ } catch (e) {
197
+ // Silent fail
198
+ }
199
+ }
200
+ if (import.meta.server && nuxtApp) {
201
+ try {
202
+ const payload = (nuxtApp as any).payload?.data || (nuxtApp as any).ssrContext?.payload?.data
203
+ if (payload && payload[prefixedKey]) {
204
+ delete payload[prefixedKey]
205
+ }
206
+ } catch (e) {
207
+ // Silent fail
208
+ }
209
+ }
210
+ }
211
+
212
+ async removeByPrefix(prefix: string, nuxtApp?: any): Promise<void> {
213
+ const fullPrefix = 'cache:' + prefix
214
+ if (import.meta.client) {
215
+ try {
216
+ const store = await this.getStore('readwrite')
217
+ const allKeys = await new Promise<string[]>((resolve, reject) => {
218
+ const req = store.getAllKeys()
219
+ req.onsuccess = () => resolve(req.result as string[])
220
+ req.onerror = () => reject(req.error)
221
+ })
222
+
223
+ const keysToDelete = allKeys.filter(k => k.startsWith(fullPrefix))
224
+
225
+ for (const key of keysToDelete) {
226
+ await new Promise<void>((resolve, reject) => {
227
+ const req = store.delete(key)
228
+ req.onsuccess = () => resolve()
229
+ req.onerror = () => reject(req.error)
230
+ })
231
+ }
232
+ } catch (e) {
233
+ // Silent fail
234
+ }
235
+ }
236
+ if (import.meta.server && nuxtApp) {
237
+ try {
238
+ const payload = (nuxtApp as any).payload?.data || (nuxtApp as any).ssrContext?.payload?.data
239
+ if (payload) {
240
+ for (const key in payload) {
241
+ if (key.startsWith(fullPrefix)) {
242
+ delete payload[key]
243
+ }
244
+ }
245
+ }
246
+ } catch (e) {
247
+ // Silent fail
248
+ }
249
+ }
250
+ }
251
+
252
+ async getRemainingTime(key: string, nuxtApp?: any): Promise<number | null> {
253
+ if (import.meta.client) {
254
+ try {
255
+ const prefixedKey = 'cache:' + key
256
+ const store = await this.getStore('readonly')
257
+ const entry = await new Promise<ICacheEntry<any> | undefined>((resolve, reject) => {
258
+ const req = store.get(prefixedKey)
259
+ req.onsuccess = () => resolve(req.result)
260
+ req.onerror = () => reject(req.error)
261
+ })
262
+
263
+ if (!entry) return null
264
+ return Math.max(0, (entry.timestamp + entry.lifetime) - Date.now())
265
+ } catch (e) {
266
+ return null
267
+ }
268
+ }
269
+ return null
270
+ }
271
+
272
+ async cleanupExpired(): Promise<void> {
273
+ if (import.meta.client && this.dbPromise) {
274
+ try {
275
+ const prefix = 'cache:'
276
+ const now = Date.now()
277
+ const keysToDelete: string[] = []
278
+
279
+ const store = await this.getStore('readwrite')
280
+ const allKeys = await new Promise<string[]>((resolve, reject) => {
281
+ const req = store.getAllKeys()
282
+ req.onsuccess = () => resolve(req.result as string[])
283
+ req.onerror = () => reject(req.error)
284
+ })
285
+
286
+ for (const key of allKeys) {
287
+ if (key.startsWith(prefix)) {
288
+ const entry = await new Promise<ICacheEntry<any> | undefined>((resolve, reject) => {
289
+ const req = store.get(key)
290
+ req.onsuccess = () => resolve(req.result)
291
+ req.onerror = () => reject(req.error)
292
+ })
293
+
294
+ if (entry && now - entry.timestamp > entry.lifetime) {
295
+ keysToDelete.push(key)
296
+ }
297
+ }
298
+ }
299
+
300
+ for (const key of keysToDelete) {
301
+ await new Promise<void>((resolve, reject) => {
302
+ const req = store.delete(key)
303
+ req.onsuccess = () => resolve()
304
+ req.onerror = () => reject(req.error)
305
+ })
306
+ }
307
+ } catch (e) {
308
+ // Silent fail
309
+ }
310
+ }
311
+ }
312
+ }
@@ -0,0 +1,232 @@
1
+ import { ACacheProvider } from './ACacheProvider'
2
+ import type { ICacheEntry } from '../types'
3
+
4
+ export class CLocalStorageCache extends ACacheProvider {
5
+ private static instance: CLocalStorageCache | null = null
6
+
7
+ public static getInstance(): CLocalStorageCache {
8
+ if (!this.instance) {
9
+ this.instance = new CLocalStorageCache()
10
+ }
11
+ return this.instance
12
+ }
13
+
14
+ public type = 'LocalStorage'
15
+
16
+ private constructor() {
17
+ super()
18
+ if (import.meta.client) {
19
+ this._hydrateFromPayload()
20
+ }
21
+ }
22
+
23
+ private _hydrateFromPayload() {
24
+ try {
25
+ const nuxtApp = typeof useNuxtApp === 'function' ? useNuxtApp() : null
26
+ const payloadData = nuxtApp?.payload?.data || (window as any).__NUXT__?.payload?.data
27
+ if (!payloadData) return
28
+
29
+ const prefix = 'cache:'
30
+ let count = 0
31
+ for (const key in payloadData) {
32
+ if (key.startsWith(prefix)) {
33
+ const value = payloadData[key]
34
+ if (value && typeof value === 'object' && 'data' in value && 'timestamp' in value && 'lifetime' in value) {
35
+ try {
36
+ localStorage.setItem(key, JSON.stringify(value))
37
+ count++
38
+ } catch (e) {
39
+ // Ignore quota errors
40
+ }
41
+ }
42
+ }
43
+ }
44
+ if (count > 0) console.log(`[CLocalStorageCache] Hydrated ${count} entries from payload`)
45
+ } catch (e) {
46
+ // Silent fail
47
+ }
48
+ }
49
+
50
+ async getKeyList(nuxtApp?: any): Promise<Array<string>> {
51
+ if (!import.meta.client) return []
52
+ const prefix = 'cache:'
53
+ return Object.keys(localStorage)
54
+ .filter(key => key.startsWith(prefix))
55
+ .map(key => key.substring(prefix.length))
56
+ }
57
+
58
+ async get<T>(key: string, nuxtApp?: any): Promise<T | null> {
59
+ const prefixedKey = 'cache:' + key
60
+
61
+ if (import.meta.client) {
62
+ try {
63
+ // 1. Check LocalStorage
64
+ const stored = localStorage.getItem(prefixedKey)
65
+ if (stored) {
66
+ const entry: ICacheEntry<T> = JSON.parse(stored)
67
+ if (Date.now() - entry.timestamp <= entry.lifetime) {
68
+ console.log(`[CLocalStorageCache] GET SUCCESS: ${key}`)
69
+ return entry.data
70
+ } else {
71
+ console.log(`[CLocalStorageCache] EXPIRED: ${key}`)
72
+ localStorage.removeItem(prefixedKey)
73
+ }
74
+ } else {
75
+ console.log(`[CLocalStorageCache] NOT IN STORAGE: ${key}`)
76
+ }
77
+ } catch (e) {
78
+ // Serialisation error
79
+ }
80
+ }
81
+
82
+ // 2. Check SSR Payload (Hydration/Request Scoped Reuse)
83
+ // On server, payload is in nuxtApp. On client, it's also in nuxtApp if passed, or window.__NUXT__
84
+ try {
85
+ let payloadData: any = null
86
+ if (import.meta.client) {
87
+ payloadData = (window as any).__NUXT__?.payload?.data
88
+ } else if (nuxtApp) {
89
+ payloadData = (nuxtApp as any).payload?.data || (nuxtApp as any).ssrContext?.payload?.data
90
+ }
91
+
92
+ if (payloadData && payloadData[prefixedKey]) {
93
+ const entry = payloadData[prefixedKey] as ICacheEntry<T>
94
+ if (Date.now() - entry.timestamp <= entry.lifetime) {
95
+ if (import.meta.client) {
96
+ localStorage.setItem(prefixedKey, JSON.stringify(entry))
97
+ }
98
+ return entry.data
99
+ }
100
+ }
101
+ } catch (e) {
102
+ // Silent fail
103
+ }
104
+
105
+ return null
106
+ }
107
+
108
+ async set<T>(key: string, data: T, lifetime: number, nuxtApp?: any): Promise<void> {
109
+ const prefixedKey = 'cache:' + key
110
+ const entry: ICacheEntry<T> = {
111
+ data,
112
+ timestamp: Date.now(),
113
+ lifetime
114
+ }
115
+
116
+ if (import.meta.server && nuxtApp) {
117
+ // Write to Payload for hydration
118
+ try {
119
+ const payload = (nuxtApp as any).payload?.data || (nuxtApp as any).ssrContext?.payload?.data
120
+ if (payload) {
121
+ payload[prefixedKey] = entry
122
+ }
123
+ } catch (e) {
124
+ // Serialisation error
125
+ }
126
+ }
127
+
128
+ if (import.meta.client) {
129
+ try {
130
+ localStorage.setItem(prefixedKey, JSON.stringify(entry))
131
+ } catch (e) {
132
+ // Quota exceeded
133
+ }
134
+ }
135
+ }
136
+
137
+ async remove(key: string, nuxtApp?: any): Promise<void> {
138
+ const prefixedKey = 'cache:' + key
139
+ if (import.meta.client) {
140
+ localStorage.removeItem(prefixedKey)
141
+ }
142
+ if (import.meta.server && nuxtApp) {
143
+ try {
144
+ const payload = (nuxtApp as any).payload?.data || (nuxtApp as any).ssrContext?.payload?.data
145
+ if (payload && payload[prefixedKey]) {
146
+ delete payload[prefixedKey]
147
+ }
148
+ } catch (e) {
149
+ // Silent fail
150
+ }
151
+ }
152
+ }
153
+
154
+ async removeByPrefix(prefix: string, nuxtApp?: any): Promise<void> {
155
+ const fullPrefix = 'cache:' + prefix
156
+ if (import.meta.client) {
157
+ const keysToDelete: string[] = []
158
+ for (let i = 0; i < localStorage.length; i++) {
159
+ const key = localStorage.key(i)
160
+ if (key && key.startsWith(fullPrefix)) {
161
+ keysToDelete.push(key)
162
+ }
163
+ }
164
+ for (const key of keysToDelete) {
165
+ localStorage.removeItem(key)
166
+ }
167
+ }
168
+ if (import.meta.server && nuxtApp) {
169
+ try {
170
+ const payload = (nuxtApp as any).payload?.data || (nuxtApp as any).ssrContext?.payload?.data
171
+ if (payload) {
172
+ for (const key in payload) {
173
+ if (key.startsWith(fullPrefix)) {
174
+ delete payload[key]
175
+ }
176
+ }
177
+ }
178
+ } catch (e) {
179
+ // Silent fail
180
+ }
181
+ }
182
+ }
183
+
184
+ async getRemainingTime(key: string, nuxtApp?: any): Promise<number | null> {
185
+ if (import.meta.client) {
186
+ const prefixedKey = 'cache:' + key
187
+ const entryStr = localStorage.getItem(prefixedKey)
188
+ if (!entryStr) return null
189
+
190
+ try {
191
+ const entry: ICacheEntry<any> = JSON.parse(entryStr)
192
+ return Math.max(0, (entry.timestamp + entry.lifetime) - Date.now())
193
+ } catch (e) {
194
+ return null
195
+ }
196
+ }
197
+ return null
198
+ }
199
+
200
+ async cleanupExpired(): Promise<void> {
201
+ if (import.meta.client) {
202
+ try {
203
+ const prefix = 'cache:'
204
+ const now = Date.now()
205
+ const keysToDelete: string[] = []
206
+
207
+ for (let i = 0; i < localStorage.length; i++) {
208
+ const key = localStorage.key(i)
209
+ if (key && key.startsWith(prefix)) {
210
+ const stored = localStorage.getItem(key)
211
+ if (stored) {
212
+ try {
213
+ const entry: ICacheEntry<any> = JSON.parse(stored)
214
+ if (now - entry.timestamp > entry.lifetime) {
215
+ keysToDelete.push(key)
216
+ }
217
+ } catch (e) {
218
+ keysToDelete.push(key)
219
+ }
220
+ }
221
+ }
222
+ }
223
+
224
+ for (const key of keysToDelete) {
225
+ localStorage.removeItem(key)
226
+ }
227
+ } catch (e) {
228
+ // Silent fail
229
+ }
230
+ }
231
+ }
232
+ }