@smess/networking 1.0.8 → 1.0.9
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.
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/services/cache.service.d.ts +31 -0
- package/dist/services/cache.service.js +77 -0
- package/dist/services/cache.service.test.d.ts +1 -0
- package/dist/services/cache.service.test.js +88 -0
- package/dist/services/store.service.d.ts +33 -0
- package/dist/services/store.service.js +136 -5
- package/dist/services/store.service.test.d.ts +1 -1
- package/dist/services/store.service.test.js +144 -48
- package/dist/utils/cache-import.d.ts +3 -0
- package/dist/utils/cache-import.js +2 -0
- package/package.json +3 -1
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ export * from './services/product.service';
|
|
|
3
3
|
export * from './services/auth.service';
|
|
4
4
|
export * from './services/store.service';
|
|
5
5
|
export * from './services/customer.service';
|
|
6
|
-
export * from './services/
|
|
6
|
+
export * from './services/order.service';
|
|
7
|
+
export * from './services/cache.service';
|
|
7
8
|
export * from './services/smess.service';
|
|
8
9
|
export * from './core/base.provider';
|
|
9
10
|
export * from './core/base.service.wrapper';
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,8 @@ export * from './services/product.service';
|
|
|
3
3
|
export * from './services/auth.service';
|
|
4
4
|
export * from './services/store.service';
|
|
5
5
|
export * from './services/customer.service';
|
|
6
|
-
export * from './services/
|
|
6
|
+
export * from './services/order.service';
|
|
7
|
+
export * from './services/cache.service';
|
|
7
8
|
export * from './services/smess.service';
|
|
8
9
|
export * from './core/base.provider';
|
|
9
10
|
export * from './core/base.service.wrapper';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Dexie, Table } from '../utils/cache-import';
|
|
2
|
+
export interface CacheMetadata {
|
|
3
|
+
key: string;
|
|
4
|
+
cachedAt: number;
|
|
5
|
+
expiresAt: number;
|
|
6
|
+
version: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class GenericDB<T> extends Dexie {
|
|
9
|
+
data: Table<T>;
|
|
10
|
+
cache: Table<CacheMetadata>;
|
|
11
|
+
constructor(name: string, dataStoreSchema: string);
|
|
12
|
+
}
|
|
13
|
+
export declare class GenericIndexedDBService<T, TCached> {
|
|
14
|
+
private toCached;
|
|
15
|
+
private fromCached;
|
|
16
|
+
private readonly CACHE_DURATION;
|
|
17
|
+
readonly db: GenericDB<TCached>;
|
|
18
|
+
constructor(dbName: string, storeSchema: string, toCached: (raw: T) => TCached, fromCached: (cached: TCached) => T);
|
|
19
|
+
saveAll(items: T[], cacheKey: string): Promise<void>;
|
|
20
|
+
getAll(cacheKey: string): Promise<T[] | null>;
|
|
21
|
+
saveOne(item: T): Promise<void>;
|
|
22
|
+
getOne(id: string): Promise<T | null>;
|
|
23
|
+
delete(id: string): Promise<void>;
|
|
24
|
+
clearAll(): Promise<void>;
|
|
25
|
+
getCacheInfo(key: string): Promise<{
|
|
26
|
+
count: number;
|
|
27
|
+
lastUpdated?: Date;
|
|
28
|
+
}>;
|
|
29
|
+
setStats(key: string, stats: unknown): Promise<void>;
|
|
30
|
+
getStats(key: string): Promise<unknown | null>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Dexie } from '../utils/cache-import';
|
|
2
|
+
export class GenericDB extends Dexie {
|
|
3
|
+
constructor(name, dataStoreSchema) {
|
|
4
|
+
super(name);
|
|
5
|
+
this.version(1).stores({
|
|
6
|
+
data: dataStoreSchema,
|
|
7
|
+
cache: 'key, cachedAt, expiresAt'
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class GenericIndexedDBService {
|
|
12
|
+
constructor(dbName, storeSchema, toCached, fromCached) {
|
|
13
|
+
this.toCached = toCached;
|
|
14
|
+
this.fromCached = fromCached;
|
|
15
|
+
this.CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
|
|
16
|
+
this.db = new GenericDB(dbName, storeSchema);
|
|
17
|
+
}
|
|
18
|
+
async saveAll(items, cacheKey) {
|
|
19
|
+
const cachedItems = items.map(this.toCached);
|
|
20
|
+
await this.db.transaction('rw', this.db.data, this.db.cache, async () => {
|
|
21
|
+
await this.db.data.clear();
|
|
22
|
+
await this.db.data.bulkAdd(cachedItems);
|
|
23
|
+
await this.db.cache.put({
|
|
24
|
+
key: cacheKey,
|
|
25
|
+
cachedAt: Date.now(),
|
|
26
|
+
expiresAt: Date.now() + this.CACHE_DURATION,
|
|
27
|
+
version: '1.0'
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async getAll(cacheKey) {
|
|
32
|
+
const cacheInfo = await this.db.cache.get(cacheKey);
|
|
33
|
+
if (!cacheInfo || Date.now() > cacheInfo.expiresAt)
|
|
34
|
+
return null;
|
|
35
|
+
const cachedItems = await this.db.data.toArray();
|
|
36
|
+
return cachedItems.map(this.fromCached);
|
|
37
|
+
}
|
|
38
|
+
async saveOne(item) {
|
|
39
|
+
const cached = this.toCached(item);
|
|
40
|
+
await this.db.data.put(cached);
|
|
41
|
+
}
|
|
42
|
+
async getOne(id) {
|
|
43
|
+
const cached = await this.db.data.get(id);
|
|
44
|
+
return cached ? this.fromCached(cached) : null;
|
|
45
|
+
}
|
|
46
|
+
async delete(id) {
|
|
47
|
+
await this.db.data.delete(id);
|
|
48
|
+
}
|
|
49
|
+
async clearAll() {
|
|
50
|
+
await this.db.transaction('rw', this.db.data, this.db.cache, async () => {
|
|
51
|
+
await this.db.data.clear();
|
|
52
|
+
await this.db.cache.clear();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async getCacheInfo(key) {
|
|
56
|
+
const count = await this.db.data.count();
|
|
57
|
+
const cacheInfo = await this.db.cache.get(key);
|
|
58
|
+
return {
|
|
59
|
+
count,
|
|
60
|
+
lastUpdated: cacheInfo ? new Date(cacheInfo.cachedAt) : undefined
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async setStats(key, stats) {
|
|
64
|
+
await this.db.cache.put({
|
|
65
|
+
key,
|
|
66
|
+
cachedAt: Date.now(),
|
|
67
|
+
expiresAt: Date.now() + this.CACHE_DURATION,
|
|
68
|
+
version: JSON.stringify(stats)
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async getStats(key) {
|
|
72
|
+
const cacheInfo = await this.db.cache.get(key);
|
|
73
|
+
if (!cacheInfo || Date.now() > cacheInfo.expiresAt)
|
|
74
|
+
return null;
|
|
75
|
+
return JSON.parse(cacheInfo.version);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import 'fake-indexeddb/auto';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import 'fake-indexeddb/auto';
|
|
2
|
+
import { GenericIndexedDBService } from './cache.service';
|
|
3
|
+
const toCached = (entity) => ({
|
|
4
|
+
id: entity.id,
|
|
5
|
+
name: entity.name,
|
|
6
|
+
createdAt: entity.createdAt.toISOString(),
|
|
7
|
+
cachedAt: Date.now()
|
|
8
|
+
});
|
|
9
|
+
const fromCached = (cached) => ({
|
|
10
|
+
id: cached.id,
|
|
11
|
+
name: cached.name,
|
|
12
|
+
createdAt: new Date(cached.createdAt)
|
|
13
|
+
});
|
|
14
|
+
describe('GenericIndexedDBService', () => {
|
|
15
|
+
let service;
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
service = new GenericIndexedDBService('TestDB', 'id, name', toCached, fromCached);
|
|
18
|
+
});
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await service.db.delete();
|
|
21
|
+
});
|
|
22
|
+
it('should save and retrieve one item', async () => {
|
|
23
|
+
const item = {
|
|
24
|
+
id: '1',
|
|
25
|
+
name: 'Test Item',
|
|
26
|
+
createdAt: new Date()
|
|
27
|
+
};
|
|
28
|
+
await service.saveOne(item);
|
|
29
|
+
const result = await service.getOne('1');
|
|
30
|
+
expect(result).not.toBeNull();
|
|
31
|
+
expect(result?.id).toBe('1');
|
|
32
|
+
});
|
|
33
|
+
it('should save and retrieve all items', async () => {
|
|
34
|
+
const now = new Date();
|
|
35
|
+
const items = [
|
|
36
|
+
{ id: '1', name: 'One', createdAt: now },
|
|
37
|
+
{ id: '2', name: 'Two', createdAt: now }
|
|
38
|
+
];
|
|
39
|
+
await service.saveAll(items, 'all');
|
|
40
|
+
const results = await service.getAll('all');
|
|
41
|
+
expect(results).not.toBeNull();
|
|
42
|
+
expect(results.length).toBe(2);
|
|
43
|
+
});
|
|
44
|
+
it('should return null when cache is expired', async () => {
|
|
45
|
+
const cacheTable = service.db.cache;
|
|
46
|
+
await cacheTable.put({
|
|
47
|
+
key: 'expired',
|
|
48
|
+
cachedAt: Date.now() - 10000,
|
|
49
|
+
expiresAt: Date.now() - 5000,
|
|
50
|
+
version: '1.0'
|
|
51
|
+
});
|
|
52
|
+
const result = await service.getAll('expired');
|
|
53
|
+
expect(result).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
it('should delete one item', async () => {
|
|
56
|
+
const item = {
|
|
57
|
+
id: '1',
|
|
58
|
+
name: 'Delete Me',
|
|
59
|
+
createdAt: new Date()
|
|
60
|
+
};
|
|
61
|
+
await service.saveOne(item);
|
|
62
|
+
await service.delete('1');
|
|
63
|
+
const result = await service.getOne('1');
|
|
64
|
+
expect(result).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
it('should set and retrieve stats', async () => {
|
|
67
|
+
const stats = { total: 99 };
|
|
68
|
+
await service.setStats('mystats', stats);
|
|
69
|
+
const result = await service.getStats('mystats');
|
|
70
|
+
expect(result).toEqual(stats);
|
|
71
|
+
});
|
|
72
|
+
it('should get cache info', async () => {
|
|
73
|
+
await service.saveOne({
|
|
74
|
+
id: '123',
|
|
75
|
+
name: 'Info Item',
|
|
76
|
+
createdAt: new Date()
|
|
77
|
+
});
|
|
78
|
+
await service.db.cache.put({
|
|
79
|
+
key: 'info',
|
|
80
|
+
cachedAt: Date.now(),
|
|
81
|
+
expiresAt: Date.now() + 10000,
|
|
82
|
+
version: '1.0'
|
|
83
|
+
});
|
|
84
|
+
const info = await service.getCacheInfo('info');
|
|
85
|
+
expect(info.count).toBeGreaterThanOrEqual(1);
|
|
86
|
+
expect(info.lastUpdated).toBeInstanceOf(Date);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -26,9 +26,38 @@ export declare class StoreProvider implements ProviderType {
|
|
|
26
26
|
get headers(): Record<string, string>;
|
|
27
27
|
getPath(...queryParams: unknown[]): string;
|
|
28
28
|
}
|
|
29
|
+
export interface StoreData {
|
|
30
|
+
id: string;
|
|
31
|
+
data: HttpResponse;
|
|
32
|
+
timestamp: number;
|
|
33
|
+
}
|
|
34
|
+
export interface StoreListData {
|
|
35
|
+
data: HttpResponse;
|
|
36
|
+
timestamp: number;
|
|
37
|
+
}
|
|
38
|
+
export interface ProductCollectionData {
|
|
39
|
+
storeId: string;
|
|
40
|
+
request: ProductCollectionRequest;
|
|
41
|
+
data: HttpResponse;
|
|
42
|
+
timestamp: number;
|
|
43
|
+
}
|
|
44
|
+
export interface ProductCategoryData {
|
|
45
|
+
data: HttpResponse;
|
|
46
|
+
timestamp: number;
|
|
47
|
+
}
|
|
48
|
+
export interface FeaturedProductData {
|
|
49
|
+
storeId: string;
|
|
50
|
+
data: HttpResponse;
|
|
51
|
+
timestamp: number;
|
|
52
|
+
}
|
|
29
53
|
export declare class StoreService {
|
|
30
54
|
private readonly config;
|
|
31
55
|
private readonly $authToken;
|
|
56
|
+
private readonly cacheService;
|
|
57
|
+
private readonly storeListCache;
|
|
58
|
+
private readonly productCollectionCache;
|
|
59
|
+
private readonly productCategoryCache;
|
|
60
|
+
private readonly featuredProductCache;
|
|
32
61
|
constructor(config: NetworkingConfig, $authToken: BehaviorSubject<string | null>);
|
|
33
62
|
provider(target: StoreTarget): SmessService<StoreProvider>;
|
|
34
63
|
get(id: string): Observable<NetworkingUIEventWrapped<HttpResponse>>;
|
|
@@ -36,4 +65,8 @@ export declare class StoreService {
|
|
|
36
65
|
productCategoryList(): Observable<NetworkingUIEventWrapped<HttpResponse>>;
|
|
37
66
|
productCollection(id: string, payload: ProductCollectionRequest): Observable<NetworkingUIEventWrapped<HttpResponse>>;
|
|
38
67
|
featuredProductList(id: string): Observable<NetworkingUIEventWrapped<HttpResponse>>;
|
|
68
|
+
clearCache(): Promise<void>;
|
|
69
|
+
clearStoreCache(id: string): Promise<void>;
|
|
70
|
+
clearProductCollectionCache(id: string): Promise<void>;
|
|
71
|
+
private isCacheValid;
|
|
39
72
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { NetworkingUIEventWrapped } from '../types/types';
|
|
1
2
|
import { Accept, SMESSHeader, SmessService } from './smess.service';
|
|
3
|
+
import { from, of } from 'rxjs';
|
|
4
|
+
import { map, switchMap, catchError } from 'rxjs/operators';
|
|
5
|
+
import { GenericIndexedDBService } from './cache.service';
|
|
2
6
|
export class StoreProvider {
|
|
3
7
|
constructor(config, target, $authToken) {
|
|
4
8
|
this.config = config;
|
|
@@ -50,24 +54,151 @@ export class StoreService {
|
|
|
50
54
|
constructor(config, $authToken) {
|
|
51
55
|
this.config = config;
|
|
52
56
|
this.$authToken = $authToken;
|
|
57
|
+
// Initialize cache services for different data types
|
|
58
|
+
this.cacheService = new GenericIndexedDBService('StoreCache', 'id, timestamp', (data) => data, (data) => data);
|
|
59
|
+
this.storeListCache = new GenericIndexedDBService('StoreListCache', 'timestamp', (data) => data, (data) => data);
|
|
60
|
+
this.productCollectionCache = new GenericIndexedDBService('ProductCollectionCache', 'storeId, timestamp', (data) => data, (data) => data);
|
|
61
|
+
this.productCategoryCache = new GenericIndexedDBService('ProductCategoryCache', 'timestamp', (data) => data, (data) => data);
|
|
62
|
+
this.featuredProductCache = new GenericIndexedDBService('FeaturedProductCache', 'storeId, timestamp', (data) => data, (data) => data);
|
|
53
63
|
}
|
|
54
64
|
provider(target) {
|
|
55
65
|
const targetWrapper = new StoreProvider(this.config, target, this.$authToken);
|
|
56
66
|
return new SmessService(targetWrapper);
|
|
57
67
|
}
|
|
58
68
|
get(id) {
|
|
59
|
-
return this.
|
|
69
|
+
return from(this.cacheService.getOne(id)).pipe(switchMap(cachedData => {
|
|
70
|
+
if (cachedData && this.isCacheValid(cachedData.timestamp)) {
|
|
71
|
+
// Return cached data if valid
|
|
72
|
+
return of(NetworkingUIEventWrapped.succeeded(cachedData.data));
|
|
73
|
+
}
|
|
74
|
+
// Fetch from network if no cache or expired
|
|
75
|
+
return this.provider('store-get').request(null, undefined, id).pipe(map(response => {
|
|
76
|
+
// Cache the successful response
|
|
77
|
+
if (response.response) {
|
|
78
|
+
this.cacheService.saveOne({
|
|
79
|
+
id,
|
|
80
|
+
data: response.response,
|
|
81
|
+
timestamp: Date.now()
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return response;
|
|
85
|
+
}), catchError(error => {
|
|
86
|
+
// If network fails and we have stale cache, return it
|
|
87
|
+
if (cachedData) {
|
|
88
|
+
return of(NetworkingUIEventWrapped.succeeded(cachedData.data));
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}));
|
|
92
|
+
}));
|
|
60
93
|
}
|
|
61
94
|
list() {
|
|
62
|
-
return this.
|
|
95
|
+
return from(this.storeListCache.getAll('store-list')).pipe(switchMap(cachedData => {
|
|
96
|
+
if (cachedData && cachedData.length > 0 && this.isCacheValid(cachedData[0].timestamp)) {
|
|
97
|
+
return of(NetworkingUIEventWrapped.succeeded(cachedData[0].data));
|
|
98
|
+
}
|
|
99
|
+
return this.provider('store-list').request().pipe(map(response => {
|
|
100
|
+
if (response.response) {
|
|
101
|
+
this.storeListCache.saveAll([{
|
|
102
|
+
data: response.response,
|
|
103
|
+
timestamp: Date.now()
|
|
104
|
+
}], 'store-list');
|
|
105
|
+
}
|
|
106
|
+
return response;
|
|
107
|
+
}), catchError(error => {
|
|
108
|
+
if (cachedData && cachedData.length > 0) {
|
|
109
|
+
return of(NetworkingUIEventWrapped.succeeded(cachedData[0].data));
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}));
|
|
113
|
+
}));
|
|
63
114
|
}
|
|
64
115
|
productCategoryList() {
|
|
65
|
-
return this.
|
|
116
|
+
return from(this.productCategoryCache.getAll('product-category-list')).pipe(switchMap(cachedData => {
|
|
117
|
+
if (cachedData && cachedData.length > 0 && this.isCacheValid(cachedData[0].timestamp)) {
|
|
118
|
+
return of(NetworkingUIEventWrapped.succeeded(cachedData[0].data));
|
|
119
|
+
}
|
|
120
|
+
return this.provider('store-product-category-list').request().pipe(map(response => {
|
|
121
|
+
if (response.response) {
|
|
122
|
+
this.productCategoryCache.saveAll([{
|
|
123
|
+
data: response.response,
|
|
124
|
+
timestamp: Date.now()
|
|
125
|
+
}], 'product-category-list');
|
|
126
|
+
}
|
|
127
|
+
return response;
|
|
128
|
+
}), catchError(error => {
|
|
129
|
+
if (cachedData && cachedData.length > 0) {
|
|
130
|
+
return of(NetworkingUIEventWrapped.succeeded(cachedData[0].data));
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}));
|
|
134
|
+
}));
|
|
66
135
|
}
|
|
67
136
|
productCollection(id, payload) {
|
|
68
|
-
|
|
137
|
+
const cacheKey = `${id}-${JSON.stringify(payload)}`;
|
|
138
|
+
return from(this.productCollectionCache.getOne(cacheKey)).pipe(switchMap(cachedData => {
|
|
139
|
+
if (cachedData && this.isCacheValid(cachedData.timestamp)) {
|
|
140
|
+
return of(NetworkingUIEventWrapped.succeeded(cachedData.data));
|
|
141
|
+
}
|
|
142
|
+
return this.provider('store-product-collection').request(payload, undefined, id).pipe(map(response => {
|
|
143
|
+
if (response.response) {
|
|
144
|
+
this.productCollectionCache.saveOne({
|
|
145
|
+
storeId: cacheKey,
|
|
146
|
+
request: payload,
|
|
147
|
+
data: response.response,
|
|
148
|
+
timestamp: Date.now()
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return response;
|
|
152
|
+
}), catchError(error => {
|
|
153
|
+
if (cachedData) {
|
|
154
|
+
return of(NetworkingUIEventWrapped.succeeded(cachedData.data));
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}));
|
|
158
|
+
}));
|
|
69
159
|
}
|
|
70
160
|
featuredProductList(id) {
|
|
71
|
-
return this.
|
|
161
|
+
return from(this.featuredProductCache.getOne(id)).pipe(switchMap(cachedData => {
|
|
162
|
+
if (cachedData && this.isCacheValid(cachedData.timestamp)) {
|
|
163
|
+
return of(NetworkingUIEventWrapped.succeeded(cachedData.data));
|
|
164
|
+
}
|
|
165
|
+
return this.provider('store-featured-product-list').request(null, undefined, id).pipe(map(response => {
|
|
166
|
+
if (response.response) {
|
|
167
|
+
this.featuredProductCache.saveOne({
|
|
168
|
+
storeId: id,
|
|
169
|
+
data: response.response,
|
|
170
|
+
timestamp: Date.now()
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return response;
|
|
174
|
+
}), catchError(error => {
|
|
175
|
+
if (cachedData) {
|
|
176
|
+
return of(NetworkingUIEventWrapped.succeeded(cachedData.data));
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}));
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
// Cache management methods
|
|
183
|
+
clearCache() {
|
|
184
|
+
return Promise.all([
|
|
185
|
+
this.cacheService.clearAll(),
|
|
186
|
+
this.storeListCache.clearAll(),
|
|
187
|
+
this.productCollectionCache.clearAll(),
|
|
188
|
+
this.productCategoryCache.clearAll(),
|
|
189
|
+
this.featuredProductCache.clearAll()
|
|
190
|
+
]).then(() => { });
|
|
191
|
+
}
|
|
192
|
+
clearStoreCache(id) {
|
|
193
|
+
return this.cacheService.delete(id);
|
|
194
|
+
}
|
|
195
|
+
clearProductCollectionCache(id) {
|
|
196
|
+
// This would need to be implemented with a more sophisticated approach
|
|
197
|
+
// to clear all product collection cache entries for a specific store
|
|
198
|
+
return Promise.resolve();
|
|
199
|
+
}
|
|
200
|
+
isCacheValid(timestamp) {
|
|
201
|
+
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours for testing
|
|
202
|
+
return Date.now() - timestamp < CACHE_DURATION;
|
|
72
203
|
}
|
|
73
204
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import 'fake-indexeddb/auto';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import 'fake-indexeddb/auto';
|
|
1
2
|
import { StoreService } from './store.service';
|
|
2
3
|
import { NetworkingUIEventWrapped, AlertModel } from '../types/types';
|
|
3
4
|
import { of, BehaviorSubject } from 'rxjs';
|
|
@@ -14,91 +15,186 @@ describe('StoreService', () => {
|
|
|
14
15
|
$authToken = new BehaviorSubject(null);
|
|
15
16
|
service = new StoreService(config, $authToken);
|
|
16
17
|
});
|
|
17
|
-
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await service.clearCache();
|
|
20
|
+
});
|
|
21
|
+
it('should create service instance', () => {
|
|
22
|
+
expect(service).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
it('should get store from network when no cache', async () => {
|
|
18
25
|
const response = { data: { id: '1', name: 'Store' } };
|
|
19
|
-
|
|
20
|
-
request: (
|
|
21
|
-
}
|
|
26
|
+
const mockProvider = {
|
|
27
|
+
request: jest.fn().mockReturnValue(of(NetworkingUIEventWrapped.succeeded(response)))
|
|
28
|
+
};
|
|
29
|
+
jest.spyOn(service, 'provider').mockReturnValue(mockProvider);
|
|
22
30
|
const result = await service.get('1').toPromise();
|
|
23
31
|
expect(result?.hasSuccess).toBe(true);
|
|
24
32
|
expect(result?.response).toEqual(response);
|
|
25
33
|
});
|
|
26
|
-
it('get
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
it('should get store from cache when available', async () => {
|
|
35
|
+
const response = { data: { id: '1', name: 'Store' } };
|
|
36
|
+
// First call to populate cache
|
|
37
|
+
const mockProvider1 = {
|
|
38
|
+
request: jest.fn().mockReturnValue(of(NetworkingUIEventWrapped.succeeded(response)))
|
|
39
|
+
};
|
|
40
|
+
jest.spyOn(service, 'provider').mockReturnValue(mockProvider1);
|
|
41
|
+
await service.get('1').toPromise();
|
|
42
|
+
// Second call should use cache (even if network fails)
|
|
43
|
+
const mockProvider2 = {
|
|
44
|
+
request: jest.fn().mockReturnValue(of(NetworkingUIEventWrapped.failed(AlertModel.default())))
|
|
45
|
+
};
|
|
46
|
+
jest.spyOn(service, 'provider').mockReturnValue(mockProvider2);
|
|
47
|
+
const result = await service.get('1').toPromise();
|
|
48
|
+
expect(result?.hasSuccess).toBe(true);
|
|
49
|
+
expect(result?.response).toEqual(response);
|
|
50
|
+
});
|
|
51
|
+
it('should handle network error when no cache', async () => {
|
|
52
|
+
const mockProvider = {
|
|
53
|
+
request: jest.fn().mockReturnValue(of(NetworkingUIEventWrapped.failed(AlertModel.default())))
|
|
54
|
+
};
|
|
55
|
+
jest.spyOn(service, 'provider').mockReturnValue(mockProvider);
|
|
30
56
|
const result = await service.get('1').toPromise();
|
|
31
57
|
expect(result?.hasFailed).toBe(true);
|
|
32
58
|
expect(result?.alertModel).toBeInstanceOf(AlertModel);
|
|
33
59
|
});
|
|
34
|
-
it('list
|
|
60
|
+
it('should get store list from network when no cache', async () => {
|
|
35
61
|
const response = { data: [{ id: '1', name: 'Store' }] };
|
|
36
|
-
|
|
37
|
-
request: ()
|
|
38
|
-
}
|
|
62
|
+
const mockProvider = {
|
|
63
|
+
request: jest.fn().mockReturnValue(of(NetworkingUIEventWrapped.succeeded(response)))
|
|
64
|
+
};
|
|
65
|
+
jest.spyOn(service, 'provider').mockReturnValue(mockProvider);
|
|
39
66
|
const result = await service.list().toPromise();
|
|
40
67
|
expect(result?.hasSuccess).toBe(true);
|
|
41
68
|
expect(result?.response).toEqual(response);
|
|
42
69
|
});
|
|
43
|
-
it('list
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
70
|
+
it('should get store list from cache when available', async () => {
|
|
71
|
+
const response = { data: [{ id: '1', name: 'Store' }] };
|
|
72
|
+
// First call to populate cache
|
|
73
|
+
const mockProvider1 = {
|
|
74
|
+
request: jest.fn().mockReturnValue(of(NetworkingUIEventWrapped.succeeded(response)))
|
|
75
|
+
};
|
|
76
|
+
jest.spyOn(service, 'provider').mockReturnValue(mockProvider1);
|
|
77
|
+
await service.list().toPromise();
|
|
78
|
+
// Second call should use cache
|
|
79
|
+
const mockProvider2 = {
|
|
80
|
+
request: jest.fn().mockReturnValue(of(NetworkingUIEventWrapped.failed(AlertModel.default())))
|
|
81
|
+
};
|
|
82
|
+
jest.spyOn(service, 'provider').mockReturnValue(mockProvider2);
|
|
47
83
|
const result = await service.list().toPromise();
|
|
48
|
-
expect(result?.
|
|
49
|
-
expect(result?.
|
|
84
|
+
expect(result?.hasSuccess).toBe(true);
|
|
85
|
+
expect(result?.response).toEqual(response);
|
|
50
86
|
});
|
|
51
|
-
it('
|
|
87
|
+
it('should get product category list from network when no cache', async () => {
|
|
52
88
|
const response = { data: [{ id: 'cat1', name: 'Category' }] };
|
|
53
|
-
|
|
54
|
-
request: ()
|
|
55
|
-
}
|
|
89
|
+
const mockProvider = {
|
|
90
|
+
request: jest.fn().mockReturnValue(of(NetworkingUIEventWrapped.succeeded(response)))
|
|
91
|
+
};
|
|
92
|
+
jest.spyOn(service, 'provider').mockReturnValue(mockProvider);
|
|
56
93
|
const result = await service.productCategoryList().toPromise();
|
|
57
94
|
expect(result?.hasSuccess).toBe(true);
|
|
58
95
|
expect(result?.response).toEqual(response);
|
|
59
96
|
});
|
|
60
|
-
it('
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
97
|
+
it('should get product category list from cache when available', async () => {
|
|
98
|
+
const response = { data: [{ id: 'cat1', name: 'Category' }] };
|
|
99
|
+
// First call to populate cache
|
|
100
|
+
const mockProvider1 = {
|
|
101
|
+
request: jest.fn().mockReturnValue(of(NetworkingUIEventWrapped.succeeded(response)))
|
|
102
|
+
};
|
|
103
|
+
jest.spyOn(service, 'provider').mockReturnValue(mockProvider1);
|
|
104
|
+
await service.productCategoryList().toPromise();
|
|
105
|
+
// Second call should use cache
|
|
106
|
+
const mockProvider2 = {
|
|
107
|
+
request: jest.fn().mockReturnValue(of(NetworkingUIEventWrapped.failed(AlertModel.default())))
|
|
108
|
+
};
|
|
109
|
+
jest.spyOn(service, 'provider').mockReturnValue(mockProvider2);
|
|
64
110
|
const result = await service.productCategoryList().toPromise();
|
|
65
|
-
expect(result?.
|
|
66
|
-
expect(result?.
|
|
111
|
+
expect(result?.hasSuccess).toBe(true);
|
|
112
|
+
expect(result?.response).toEqual(response);
|
|
67
113
|
});
|
|
68
|
-
it('
|
|
114
|
+
it('should get product collection from network when no cache', async () => {
|
|
69
115
|
const response = { data: [{ id: 'prod1', name: 'Product' }] };
|
|
70
116
|
const payload = { page: 1, per_page: 10 };
|
|
71
|
-
|
|
72
|
-
request: (
|
|
73
|
-
}
|
|
117
|
+
const mockProvider = {
|
|
118
|
+
request: jest.fn().mockReturnValue(of(NetworkingUIEventWrapped.succeeded(response)))
|
|
119
|
+
};
|
|
120
|
+
jest.spyOn(service, 'provider').mockReturnValue(mockProvider);
|
|
74
121
|
const result = await service.productCollection('1', payload).toPromise();
|
|
75
122
|
expect(result?.hasSuccess).toBe(true);
|
|
76
123
|
expect(result?.response).toEqual(response);
|
|
77
124
|
});
|
|
78
|
-
it('
|
|
125
|
+
it('should get product collection from cache when available', async () => {
|
|
126
|
+
const response = { data: [{ id: 'prod1', name: 'Product' }] };
|
|
79
127
|
const payload = { page: 1, per_page: 10 };
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
128
|
+
// First call to populate cache
|
|
129
|
+
const mockProvider1 = {
|
|
130
|
+
request: jest.fn().mockReturnValue(of(NetworkingUIEventWrapped.succeeded(response)))
|
|
131
|
+
};
|
|
132
|
+
jest.spyOn(service, 'provider').mockReturnValue(mockProvider1);
|
|
133
|
+
await service.productCollection('1', payload).toPromise();
|
|
134
|
+
// Second call should use cache
|
|
135
|
+
const mockProvider2 = {
|
|
136
|
+
request: jest.fn().mockReturnValue(of(NetworkingUIEventWrapped.failed(AlertModel.default())))
|
|
137
|
+
};
|
|
138
|
+
jest.spyOn(service, 'provider').mockReturnValue(mockProvider2);
|
|
83
139
|
const result = await service.productCollection('1', payload).toPromise();
|
|
84
|
-
expect(result?.
|
|
85
|
-
expect(result?.
|
|
140
|
+
expect(result?.hasSuccess).toBe(true);
|
|
141
|
+
expect(result?.response).toEqual(response);
|
|
86
142
|
});
|
|
87
|
-
it('
|
|
143
|
+
it('should get featured product list from network when no cache', async () => {
|
|
88
144
|
const response = { data: [{ id: 'feat1', name: 'Featured' }] };
|
|
89
|
-
|
|
90
|
-
request: (
|
|
91
|
-
}
|
|
145
|
+
const mockProvider = {
|
|
146
|
+
request: jest.fn().mockReturnValue(of(NetworkingUIEventWrapped.succeeded(response)))
|
|
147
|
+
};
|
|
148
|
+
jest.spyOn(service, 'provider').mockReturnValue(mockProvider);
|
|
92
149
|
const result = await service.featuredProductList('1').toPromise();
|
|
93
150
|
expect(result?.hasSuccess).toBe(true);
|
|
94
151
|
expect(result?.response).toEqual(response);
|
|
95
152
|
});
|
|
96
|
-
it('
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
153
|
+
it('should get featured product list from cache when available', async () => {
|
|
154
|
+
const response = { data: [{ id: 'feat1', name: 'Featured' }] };
|
|
155
|
+
// First call to populate cache
|
|
156
|
+
const mockProvider1 = {
|
|
157
|
+
request: jest.fn().mockReturnValue(of(NetworkingUIEventWrapped.succeeded(response)))
|
|
158
|
+
};
|
|
159
|
+
jest.spyOn(service, 'provider').mockReturnValue(mockProvider1);
|
|
160
|
+
await service.featuredProductList('1').toPromise();
|
|
161
|
+
// Second call should use cache
|
|
162
|
+
const mockProvider2 = {
|
|
163
|
+
request: jest.fn().mockReturnValue(of(NetworkingUIEventWrapped.failed(AlertModel.default())))
|
|
164
|
+
};
|
|
165
|
+
jest.spyOn(service, 'provider').mockReturnValue(mockProvider2);
|
|
100
166
|
const result = await service.featuredProductList('1').toPromise();
|
|
101
|
-
expect(result?.
|
|
102
|
-
expect(result?.
|
|
167
|
+
expect(result?.hasSuccess).toBe(true);
|
|
168
|
+
expect(result?.response).toEqual(response);
|
|
169
|
+
});
|
|
170
|
+
it('should clear cache successfully', async () => {
|
|
171
|
+
const response = { data: { id: '1', name: 'Store' } };
|
|
172
|
+
const mockProvider = {
|
|
173
|
+
request: jest.fn().mockReturnValue(of(NetworkingUIEventWrapped.succeeded(response)))
|
|
174
|
+
};
|
|
175
|
+
jest.spyOn(service, 'provider').mockReturnValue(mockProvider);
|
|
176
|
+
// Populate cache
|
|
177
|
+
await service.get('1').toPromise();
|
|
178
|
+
// Clear cache
|
|
179
|
+
await service.clearCache();
|
|
180
|
+
// Should call network again after cache clear
|
|
181
|
+
const result = await service.get('1').toPromise();
|
|
182
|
+
expect(result?.hasSuccess).toBe(true);
|
|
183
|
+
expect(mockProvider.request).toHaveBeenCalledTimes(2); // Once before clear, once after
|
|
184
|
+
});
|
|
185
|
+
it('should clear specific store cache', async () => {
|
|
186
|
+
const response = { data: { id: '1', name: 'Store' } };
|
|
187
|
+
const mockProvider = {
|
|
188
|
+
request: jest.fn().mockReturnValue(of(NetworkingUIEventWrapped.succeeded(response)))
|
|
189
|
+
};
|
|
190
|
+
jest.spyOn(service, 'provider').mockReturnValue(mockProvider);
|
|
191
|
+
// Populate cache for store '1'
|
|
192
|
+
await service.get('1').toPromise();
|
|
193
|
+
// Clear specific store cache
|
|
194
|
+
await service.clearStoreCache('1');
|
|
195
|
+
// Should call network again after cache clear
|
|
196
|
+
const result = await service.get('1').toPromise();
|
|
197
|
+
expect(result?.hasSuccess).toBe(true);
|
|
198
|
+
expect(mockProvider.request).toHaveBeenCalledTimes(2);
|
|
103
199
|
});
|
|
104
200
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smess/networking",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "Networking utilities and services for SMESS platform.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"axios": "^1.10.0",
|
|
37
|
+
"dexie": "^4.0.11",
|
|
38
|
+
"fake-indexeddb": "^6.0.1",
|
|
37
39
|
"rxjs": "^7.8.2"
|
|
38
40
|
}
|
|
39
41
|
}
|