@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,185 @@
1
+ import { ACacheProvider } from './ACacheProvider'
2
+ import type { ICacheEntry } from '../types'
3
+
4
+ export class CMemoryCache extends ACacheProvider {
5
+ private static instance: CMemoryCache | null = null
6
+ private static _storage: Map<string, ICacheEntry<any>> | null = null
7
+
8
+ private static get storage(): Map<string, ICacheEntry<any>> {
9
+ if (!this._storage) this._storage = new Map()
10
+ return this._storage
11
+ }
12
+
13
+ public static getInstance(): CMemoryCache {
14
+ if (!this.instance) {
15
+ this.instance = new CMemoryCache()
16
+ }
17
+ return this.instance
18
+ }
19
+
20
+ private constructor() {
21
+ super()
22
+ if (import.meta.client && CMemoryCache.storage.size === 0) {
23
+ this._hydrateFromPayload()
24
+ }
25
+ }
26
+
27
+ private _hydrateFromPayload() {
28
+ try {
29
+ const nuxtApp = typeof useNuxtApp === 'function' ? useNuxtApp() : null
30
+ const payloadData = nuxtApp?.payload?.data || (globalThis as any).__NUXT__?.payload?.data
31
+ if (!payloadData) return
32
+
33
+ for (const key in payloadData) {
34
+ const value = payloadData[key]
35
+ if (value && typeof value === 'object' && 'data' in value && 'timestamp' in value && 'lifetime' in value) {
36
+ if (typeof (value as any).lifetime === 'number') {
37
+ CMemoryCache.storage.set(key, value as ICacheEntry<any>)
38
+ }
39
+ }
40
+ }
41
+ } catch (e) {
42
+ // Silent fail
43
+ }
44
+ }
45
+
46
+ async getKeyList(nuxtApp?: any): Promise<Array<string>> {
47
+ const prefix = 'cache:'
48
+ return Array.from(CMemoryCache.storage.keys())
49
+ .filter(key => key.startsWith(prefix))
50
+ .map(key => key.substring(prefix.length))
51
+ }
52
+
53
+ async get<T>(key: string, nuxtApp?: any): Promise<T | null> {
54
+ const prefixedKey = 'cache:' + key
55
+
56
+ if (import.meta.client) {
57
+ const entry = CMemoryCache.storage.get(prefixedKey)
58
+ if (entry) {
59
+ if (Date.now() - entry.timestamp <= entry.lifetime) {
60
+ return entry.data as T
61
+ } else {
62
+ CMemoryCache.storage.delete(prefixedKey)
63
+ }
64
+ }
65
+ }
66
+
67
+ // Check SSR Payload
68
+ try {
69
+ let payloadData: any = null
70
+ if (import.meta.client) {
71
+ payloadData = (globalThis as any).__NUXT__?.payload?.data
72
+ } else if (nuxtApp) {
73
+ payloadData = (nuxtApp as any).payload?.data || (nuxtApp as any).ssrContext?.payload?.data
74
+ }
75
+
76
+ if (payloadData && payloadData[prefixedKey]) {
77
+ const entry = payloadData[prefixedKey] as ICacheEntry<T>
78
+ if (Date.now() - entry.timestamp <= entry.lifetime) {
79
+ if (import.meta.client) {
80
+ CMemoryCache.storage.set(prefixedKey, entry)
81
+ }
82
+ return entry.data
83
+ }
84
+ }
85
+ } catch (e) {
86
+ // Silent fail
87
+ }
88
+
89
+ return null
90
+ }
91
+
92
+ async set<T>(key: string, data: T, lifetime: number, nuxtApp?: any): Promise<void> {
93
+ const prefixedKey = 'cache:' + key
94
+ const entry: ICacheEntry<T> = {
95
+ data,
96
+ timestamp: Date.now(),
97
+ lifetime
98
+ }
99
+
100
+ if (import.meta.server && nuxtApp) {
101
+ try {
102
+ const payload = (nuxtApp as any).payload?.data || (nuxtApp as any).ssrContext?.payload?.data
103
+ if (payload) {
104
+ payload[prefixedKey] = entry
105
+ }
106
+ } catch (e) {
107
+ // Silent fail
108
+ }
109
+ }
110
+
111
+ if (import.meta.client) {
112
+ CMemoryCache.storage.set(prefixedKey, entry)
113
+ }
114
+ }
115
+
116
+ async remove(key: string, nuxtApp?: any): Promise<void> {
117
+ const prefixedKey = 'cache:' + key
118
+ CMemoryCache.storage.delete(prefixedKey)
119
+
120
+ if (import.meta.server && nuxtApp) {
121
+ try {
122
+ const payload = (nuxtApp as any).payload?.data || (nuxtApp as any).ssrContext?.payload?.data
123
+ if (payload && payload[prefixedKey]) {
124
+ delete payload[prefixedKey]
125
+ }
126
+ } catch (e) {
127
+ // Silent fail
128
+ }
129
+ }
130
+ }
131
+
132
+ async removeByPrefix(prefix: string, nuxtApp?: any): Promise<void> {
133
+ const fullPrefix = 'cache:' + prefix
134
+
135
+ const keysToDelete: string[] = []
136
+ for (const key of CMemoryCache.storage.keys()) {
137
+ if (key.startsWith(fullPrefix)) {
138
+ keysToDelete.push(key)
139
+ }
140
+ }
141
+ for (const key of keysToDelete) {
142
+ CMemoryCache.storage.delete(key)
143
+ }
144
+
145
+ if (import.meta.server && nuxtApp) {
146
+ try {
147
+ const payload = (nuxtApp as any).payload?.data || (nuxtApp as any).ssrContext?.payload?.data
148
+ if (payload) {
149
+ for (const key in payload) {
150
+ if (key.startsWith(fullPrefix)) {
151
+ delete payload[key]
152
+ }
153
+ }
154
+ }
155
+ } catch (e) {
156
+ // Silent fail
157
+ }
158
+ }
159
+ }
160
+
161
+ async getRemainingTime(key: string, nuxtApp?: any): Promise<number | null> {
162
+ const prefixedKey = 'cache:' + key
163
+ const entry = CMemoryCache.storage.get(prefixedKey)
164
+ if (!entry) return null
165
+ return Math.max(0, (entry.timestamp + entry.lifetime) - Date.now())
166
+ }
167
+
168
+ async cleanupExpired(): Promise<void> {
169
+ const prefix = 'cache:'
170
+ const now = Date.now()
171
+ const keysToDelete: string[] = []
172
+
173
+ for (const [key, entry] of CMemoryCache.storage.entries()) {
174
+ if (key.startsWith(prefix)) {
175
+ if (now - entry.timestamp > entry.lifetime) {
176
+ keysToDelete.push(key)
177
+ }
178
+ }
179
+ }
180
+
181
+ for (const key of keysToDelete) {
182
+ CMemoryCache.storage.delete(key)
183
+ }
184
+ }
185
+ }
@@ -0,0 +1,378 @@
1
+ import { ACacheProvider } from './ACacheProvider'
2
+ import type { ICacheEntry } from '../types'
3
+ import { CCookieCache } from './CCookieCache'
4
+
5
+ export class CSessionCache extends ACacheProvider {
6
+ private static instance: CSessionCache | null = null
7
+
8
+ public static getInstance(): CSessionCache {
9
+ if (!this.instance) {
10
+ this.instance = new CSessionCache()
11
+ }
12
+ return this.instance
13
+ }
14
+
15
+ // Use global symbol to share storage across module instances (API vs App)
16
+ private static get storage(): Map<string, Map<string, ICacheEntry<any>>> {
17
+ const globalKey = Symbol.for('Katlux_CSessionCache_Storage')
18
+ const _global = globalThis as any
19
+ if (!_global[globalKey]) {
20
+ _global[globalKey] = new Map<string, Map<string, ICacheEntry<any>>>()
21
+ }
22
+ return _global[globalKey]
23
+ }
24
+
25
+ public static getSnapshot(): Map<string, Map<string, ICacheEntry<any>>> {
26
+ return this.storage
27
+ }
28
+
29
+ private sessionIdCache: ACacheProvider | null = null
30
+ private sessionId: string | null = null
31
+ private readonly SESSION_ID_KEY = '_session_id'
32
+
33
+ private constructor() {
34
+ super()
35
+ // Use cookie cache for storing sessionId on client via Singleton
36
+ this.sessionIdCache = CCookieCache.getInstance()
37
+ }
38
+
39
+ /**
40
+ * Generate a UUID v4 sessionId
41
+ */
42
+ private generateSessionId(): string {
43
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
44
+ const r = Math.random() * 16 | 0
45
+ const v = c === 'x' ? r : (r & 0x3 | 0x8)
46
+ return v.toString(16)
47
+ })
48
+ }
49
+
50
+ /**
51
+ * Get or create sessionId
52
+ */
53
+ private async getSessionId(nuxtApp?: any): Promise<string> {
54
+ if (this.sessionId) {
55
+ return this.sessionId
56
+ }
57
+
58
+ if (!this.sessionIdCache) {
59
+ return this.generateSessionId()
60
+ }
61
+
62
+ // Try to get existing sessionId from cookie
63
+ const existingId = await this.sessionIdCache.get<string>(this.SESSION_ID_KEY, nuxtApp)
64
+
65
+ if (existingId) {
66
+ this.sessionId = existingId
67
+ return existingId
68
+ }
69
+
70
+ // Generate new sessionId
71
+ const newId = this.generateSessionId()
72
+ this.sessionId = newId
73
+
74
+ // Store in cookie (1 year lifetime)
75
+ if (this.sessionIdCache) {
76
+ await this.sessionIdCache.set(this.SESSION_ID_KEY, newId, 365 * 24 * 60 * 60 * 1000, nuxtApp)
77
+ }
78
+
79
+ return newId
80
+ }
81
+
82
+ async getKeyList(nuxtApp?: any): Promise<Array<string>> {
83
+ const prefix = 'cache:'
84
+
85
+ // Client: Use browser sessionStorage
86
+ if (import.meta.client) {
87
+ return Object.keys(sessionStorage)
88
+ .filter(key => key.startsWith(prefix))
89
+ .map(key => key.substring(prefix.length))
90
+ }
91
+
92
+ // Server: Use session storage (Map)
93
+ const sessionId = await this.getSessionId(nuxtApp)
94
+ const sessionData = CSessionCache.storage.get(sessionId)
95
+ if (!sessionData) {
96
+ return []
97
+ }
98
+
99
+ return Array.from(sessionData.keys())
100
+ .filter(key => key.startsWith(prefix))
101
+ .map(key => key.substring(prefix.length))
102
+ }
103
+ async get<T>(key: string, nuxtApp?: any): Promise<T | null> {
104
+ const prefixedKey = 'cache:' + key
105
+
106
+ // Client: Use browser sessionStorage
107
+ if (import.meta.client) {
108
+ try {
109
+ const stored = sessionStorage.getItem(prefixedKey)
110
+ if (stored) {
111
+ const entry: ICacheEntry<T> = JSON.parse(stored)
112
+ if (Date.now() - entry.timestamp <= entry.lifetime) {
113
+ return entry.data as T
114
+ } else {
115
+ sessionStorage.removeItem(prefixedKey)
116
+ }
117
+ }
118
+ } catch (e) {
119
+ // Serialisation error
120
+ }
121
+
122
+ // Check SSR Payload as fallback/hydration
123
+ try {
124
+ const payloadData = (window as any).__NUXT__?.payload?.data
125
+ if (payloadData && payloadData[prefixedKey]) {
126
+ const entry = payloadData[prefixedKey] as ICacheEntry<T>
127
+ if (Date.now() - entry.timestamp <= entry.lifetime) {
128
+ // Hydrate into sessionStorage
129
+ window.sessionStorage.setItem(prefixedKey, JSON.stringify(entry))
130
+ return entry.data
131
+ }
132
+ }
133
+ } catch (e) {
134
+ // Silent fail
135
+ }
136
+
137
+ return null
138
+ }
139
+
140
+ // Server: Retrieve from session storage (Map)
141
+ if (import.meta.server) {
142
+ const sessionId = await this.getSessionId(nuxtApp)
143
+ const sessionData = CSessionCache.storage.get(sessionId)
144
+ if (!sessionData) {
145
+ return null
146
+ }
147
+
148
+ const entry = sessionData.get(prefixedKey)
149
+ if (!entry) {
150
+ return null
151
+ }
152
+
153
+ // Check expiration
154
+ if (Date.now() - entry.timestamp <= entry.lifetime) {
155
+ return entry.data as T
156
+ } else {
157
+ // Remove expired entry
158
+ sessionData.delete(prefixedKey)
159
+ return null
160
+ }
161
+ }
162
+
163
+ return null
164
+ }
165
+
166
+ async set<T>(key: string, data: T, lifetime: number, nuxtApp?: any): Promise<void> {
167
+ const prefixedKey = 'cache:' + key
168
+ const entry: ICacheEntry<T> = {
169
+ data,
170
+ timestamp: Date.now(),
171
+ lifetime
172
+ }
173
+
174
+ // Client: Store in browser sessionStorage
175
+ if (import.meta.client) {
176
+ try {
177
+ sessionStorage.setItem(prefixedKey, JSON.stringify(entry))
178
+ } catch (e) {
179
+ // Quota exceeded
180
+ }
181
+ }
182
+
183
+ // Server: Store in session storage (Map) AND Payload for hydration
184
+ if (import.meta.server) {
185
+ try {
186
+ const sessionId = await this.getSessionId(nuxtApp)
187
+
188
+ // 1. Session Storage
189
+ let sessionData = CSessionCache.storage.get(sessionId)
190
+ if (!sessionData) {
191
+ sessionData = new Map<string, ICacheEntry<any>>()
192
+ CSessionCache.storage.set(sessionId, sessionData)
193
+ }
194
+ sessionData.set(prefixedKey, entry)
195
+
196
+ // 2. Nuxt Payload (for hydration into client's sessionStorage)
197
+ if (nuxtApp) {
198
+ const payload = (nuxtApp as any).payload?.data || (nuxtApp as any).ssrContext?.payload?.data
199
+ if (payload) {
200
+ payload[prefixedKey] = entry
201
+ }
202
+ }
203
+ } catch (e) {
204
+ // Silent fail
205
+ }
206
+ }
207
+ }
208
+
209
+ async remove(key: string, nuxtApp?: any): Promise<void> {
210
+ const prefixedKey = 'cache:' + key
211
+
212
+ if (import.meta.client) {
213
+ sessionStorage.removeItem(prefixedKey)
214
+ }
215
+
216
+ if (import.meta.server) {
217
+ try {
218
+ // 1. Session storage
219
+ const sessionId = await this.getSessionId(nuxtApp)
220
+ const sessionData = CSessionCache.storage.get(sessionId)
221
+ if (sessionData) {
222
+ sessionData.delete(prefixedKey)
223
+ }
224
+
225
+ // 2. Payload cleanup
226
+ if (nuxtApp) {
227
+ const payload = (nuxtApp as any).payload?.data || (nuxtApp as any).ssrContext?.payload?.data
228
+ if (payload && payload[prefixedKey]) {
229
+ delete payload[prefixedKey]
230
+ }
231
+ }
232
+ } catch (e) {
233
+ // Silent fail
234
+ }
235
+ }
236
+ }
237
+
238
+ async removeByPrefix(prefix: string, nuxtApp?: any): Promise<void> {
239
+ const fullPrefix = 'cache:' + prefix
240
+
241
+ // Client: Iterate sessionStorage
242
+ if (import.meta.client) {
243
+ const keysToDelete: string[] = []
244
+ for (let i = 0; i < sessionStorage.length; i++) {
245
+ const key = sessionStorage.key(i)
246
+ if (key && key.startsWith(fullPrefix)) {
247
+ keysToDelete.push(key)
248
+ }
249
+ }
250
+ for (const key of keysToDelete) {
251
+ sessionStorage.removeItem(key)
252
+ }
253
+ }
254
+
255
+ // Server: Iterate session map
256
+ if (import.meta.server) {
257
+ try {
258
+ const sessionId = await this.getSessionId(nuxtApp)
259
+ const sessionData = CSessionCache.storage.get(sessionId)
260
+
261
+ if (sessionData) {
262
+ const keysToDelete: string[] = []
263
+ for (const key of sessionData.keys()) {
264
+ if (key.startsWith(fullPrefix)) {
265
+ keysToDelete.push(key)
266
+ }
267
+ }
268
+ for (const key of keysToDelete) {
269
+ sessionData.delete(key)
270
+ }
271
+ }
272
+
273
+ // Payload cleanup
274
+ if (nuxtApp) {
275
+ const payload = (nuxtApp as any).payload?.data || (nuxtApp as any).ssrContext?.payload?.data
276
+ if (payload) {
277
+ for (const key in payload) {
278
+ if (key.startsWith(fullPrefix)) {
279
+ delete payload[key]
280
+ }
281
+ }
282
+ }
283
+ }
284
+ } catch (e) {
285
+ // Silent fail
286
+ }
287
+ }
288
+ }
289
+
290
+ async getRemainingTime(key: string, nuxtApp?: any): Promise<number | null> {
291
+ const prefixedKey = 'cache:' + key
292
+
293
+ if (import.meta.client) {
294
+ const entryStr = sessionStorage.getItem(prefixedKey)
295
+ if (!entryStr) return null
296
+ try {
297
+ const entry: ICacheEntry<any> = JSON.parse(entryStr)
298
+ return Math.max(0, (entry.timestamp + entry.lifetime) - Date.now())
299
+ } catch (e) {
300
+ return null
301
+ }
302
+ }
303
+
304
+ if (import.meta.server) {
305
+ const sessionId = await this.getSessionId(nuxtApp)
306
+ const sessionData = CSessionCache.storage.get(sessionId)
307
+ if (!sessionData) return null
308
+
309
+ const entry = sessionData.get(prefixedKey)
310
+ if (!entry) return null
311
+
312
+ return Math.max(0, (entry.timestamp + entry.lifetime) - Date.now())
313
+ }
314
+ return null
315
+ }
316
+
317
+ async cleanupExpired(): Promise<void> {
318
+ const prefix = 'cache:'
319
+ const now = Date.now()
320
+
321
+ if (import.meta.client) {
322
+ const keysToDelete: string[] = []
323
+ for (let i = 0; i < sessionStorage.length; i++) {
324
+ const key = sessionStorage.key(i)
325
+ if (key && key.startsWith(prefix)) {
326
+ const stored = sessionStorage.getItem(key)
327
+ if (stored) {
328
+ try {
329
+ const entry: ICacheEntry<any> = JSON.parse(stored)
330
+ if (now - entry.timestamp > entry.lifetime) {
331
+ keysToDelete.push(key)
332
+ }
333
+ } catch (e) {
334
+ keysToDelete.push(key)
335
+ }
336
+ }
337
+ }
338
+ }
339
+ for (const key of keysToDelete) {
340
+ sessionStorage.removeItem(key)
341
+ }
342
+ }
343
+
344
+ if (import.meta.server) {
345
+ const sessionsToDelete: string[] = []
346
+
347
+ // Iterate through all sessions
348
+ for (const [sessionId, sessionData] of CSessionCache.storage.entries()) {
349
+ const keysToDelete: string[] = []
350
+
351
+ // Check each entry in the session
352
+ for (const [key, entry] of sessionData.entries()) {
353
+ // Only cleanup entries for this cache key
354
+ if (key.startsWith(prefix)) {
355
+ if (now - entry.timestamp > entry.lifetime) {
356
+ keysToDelete.push(key)
357
+ }
358
+ }
359
+ }
360
+
361
+ // Remove expired entries
362
+ for (const key of keysToDelete) {
363
+ sessionData.delete(key)
364
+ }
365
+
366
+ // If session is empty, mark for deletion
367
+ if (sessionData.size === 0) {
368
+ sessionsToDelete.push(sessionId)
369
+ }
370
+ }
371
+
372
+ // Remove empty sessions
373
+ for (const sessionId of sessionsToDelete) {
374
+ CSessionCache.storage.delete(sessionId)
375
+ }
376
+ }
377
+ }
378
+ }
@@ -0,0 +1,22 @@
1
+ import { ECacheStrategy } from '../types'
2
+ import { ACacheProvider } from './ACacheProvider'
3
+ import { CApplicationCache } from './CApplicationCache'
4
+ import { CMemoryCache } from './CMemoryCache'
5
+ import { CLocalStorageCache } from './CLocalStorageCache'
6
+ import { CIndexedDBCache } from './CIndexedDBCache'
7
+ import { CCookieCache } from './CCookieCache'
8
+ import { CSessionCache } from './CSessionCache'
9
+
10
+ export class CacheProviderFactory {
11
+ static getProvider(strategy: ECacheStrategy): ACacheProvider | null {
12
+ switch (strategy) {
13
+ case ECacheStrategy.Application: return CApplicationCache.getInstance()
14
+ case ECacheStrategy.Session: return CSessionCache.getInstance()
15
+ case ECacheStrategy.Memory: return CMemoryCache.getInstance()
16
+ case ECacheStrategy.LocalStorage: return CLocalStorageCache.getInstance()
17
+ case ECacheStrategy.IndexedDB: return CIndexedDBCache.getInstance()
18
+ case ECacheStrategy.Cookie: return CCookieCache.getInstance()
19
+ default: return null
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,24 @@
1
+ import { ref, type Ref } from 'vue'
2
+ import { ECacheStrategy } from '../types';
3
+ import type { IDataProviderOptions } from '../types';
4
+ import { RequestProvider } from '../RequestProvider/RequestProvider';
5
+
6
+ export abstract class AAPIDataProvider {
7
+
8
+ apiUrl: Ref<string> = ref("")
9
+ cacheStrategy: Ref<ECacheStrategy | null> = ref(null)
10
+ cacheLifetime: Ref<number> = ref(60 * 1000)
11
+ requestProvider: RequestProvider = new RequestProvider()
12
+ refreshOnMutation: Ref<boolean | undefined> = ref(undefined)
13
+
14
+ constructor(options?: IDataProviderOptions) {
15
+ if (options?.cacheStrategy) this.cacheStrategy.value = options.cacheStrategy
16
+ if (options?.cacheLifetime) this.cacheLifetime.value = options.cacheLifetime
17
+ if (options?.refreshOnMutation !== undefined) this.refreshOnMutation.value = options.refreshOnMutation
18
+ }
19
+ abstract setAPIUrl(url: string): void
20
+ abstract create(item: any): Promise<void>
21
+ abstract update(item: any): Promise<void>
22
+ abstract delete(items: any[]): Promise<void>
23
+ }
24
+