@smess/networking 1.0.7 → 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 CHANGED
@@ -1,6 +1,10 @@
1
1
  export * from './services/user.service';
2
2
  export * from './services/product.service';
3
3
  export * from './services/auth.service';
4
+ export * from './services/store.service';
5
+ export * from './services/customer.service';
6
+ export * from './services/order.service';
7
+ export * from './services/cache.service';
4
8
  export * from './services/smess.service';
5
9
  export * from './core/base.provider';
6
10
  export * from './core/base.service.wrapper';
package/dist/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  export * from './services/user.service';
2
2
  export * from './services/product.service';
3
3
  export * from './services/auth.service';
4
+ export * from './services/store.service';
5
+ export * from './services/customer.service';
6
+ export * from './services/order.service';
7
+ export * from './services/cache.service';
4
8
  export * from './services/smess.service';
5
9
  export * from './core/base.provider';
6
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.provider('store-get').request(null, undefined, id);
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.provider('store-list').request();
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.provider('store-product-category-list').request();
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
- return this.provider('store-product-collection').request(payload, undefined, id);
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.provider('store-featured-product-list').request(null, undefined, id);
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
- export {};
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
- it('get success', async () => {
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
- jest.spyOn(service, 'provider').mockReturnValue({
20
- request: (_payload, _mockResponse, id) => of(NetworkingUIEventWrapped.succeeded(response)),
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 error', async () => {
27
- jest.spyOn(service, 'provider').mockReturnValue({
28
- request: (_payload, _mockResponse, id) => of(NetworkingUIEventWrapped.failed(AlertModel.default())),
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 success', async () => {
60
+ it('should get store list from network when no cache', async () => {
35
61
  const response = { data: [{ id: '1', name: 'Store' }] };
36
- jest.spyOn(service, 'provider').mockReturnValue({
37
- request: () => of(NetworkingUIEventWrapped.succeeded(response)),
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 error', async () => {
44
- jest.spyOn(service, 'provider').mockReturnValue({
45
- request: () => of(NetworkingUIEventWrapped.failed(AlertModel.default())),
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?.hasFailed).toBe(true);
49
- expect(result?.alertModel).toBeInstanceOf(AlertModel);
84
+ expect(result?.hasSuccess).toBe(true);
85
+ expect(result?.response).toEqual(response);
50
86
  });
51
- it('productCategoryList success', async () => {
87
+ it('should get product category list from network when no cache', async () => {
52
88
  const response = { data: [{ id: 'cat1', name: 'Category' }] };
53
- jest.spyOn(service, 'provider').mockReturnValue({
54
- request: () => of(NetworkingUIEventWrapped.succeeded(response)),
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('productCategoryList error', async () => {
61
- jest.spyOn(service, 'provider').mockReturnValue({
62
- request: () => of(NetworkingUIEventWrapped.failed(AlertModel.default())),
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?.hasFailed).toBe(true);
66
- expect(result?.alertModel).toBeInstanceOf(AlertModel);
111
+ expect(result?.hasSuccess).toBe(true);
112
+ expect(result?.response).toEqual(response);
67
113
  });
68
- it('productCollection success', async () => {
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
- jest.spyOn(service, 'provider').mockReturnValue({
72
- request: (payload, _mockResponse, id) => of(NetworkingUIEventWrapped.succeeded(response)),
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('productCollection error', async () => {
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
- jest.spyOn(service, 'provider').mockReturnValue({
81
- request: (payload, _mockResponse, id) => of(NetworkingUIEventWrapped.failed(AlertModel.default())),
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?.hasFailed).toBe(true);
85
- expect(result?.alertModel).toBeInstanceOf(AlertModel);
140
+ expect(result?.hasSuccess).toBe(true);
141
+ expect(result?.response).toEqual(response);
86
142
  });
87
- it('featuredProductList success', async () => {
143
+ it('should get featured product list from network when no cache', async () => {
88
144
  const response = { data: [{ id: 'feat1', name: 'Featured' }] };
89
- jest.spyOn(service, 'provider').mockReturnValue({
90
- request: (_payload, _mockResponse, storeId) => of(NetworkingUIEventWrapped.succeeded(response)),
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('featuredProductList error', async () => {
97
- jest.spyOn(service, 'provider').mockReturnValue({
98
- request: (_payload, _mockResponse, storeId) => of(NetworkingUIEventWrapped.failed(AlertModel.default())),
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?.hasFailed).toBe(true);
102
- expect(result?.alertModel).toBeInstanceOf(AlertModel);
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
  });
@@ -0,0 +1,3 @@
1
+ import Dexie from 'dexie';
2
+ export { Dexie };
3
+ export type Table<T> = Dexie.Table<T, string>;
@@ -0,0 +1,2 @@
1
+ import Dexie from 'dexie';
2
+ export { Dexie };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smess/networking",
3
- "version": "1.0.7",
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
  }