Skip to content

Commit 959b4e8

Browse files
authored
fix: data structure error in redis storage adapter (#768)
* fix: redis storage adpater data error fixed * ci: add changeset file * test: modify test files
1 parent 67ff47a commit 959b4e8

File tree

10 files changed

+145
-64
lines changed

10 files changed

+145
-64
lines changed

.changeset/nine-zebras-watch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@alova/storage-redis': patch
3+
---
4+
5+
redis storage adpater data error fixed

examples/vue/src/api/methods.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,6 @@ export const uploadFiles = ({ file, name }) => {
147147
formData.append(name, file);
148148
return alova.Post('/upload', formData);
149149
};
150+
151+
// sse
152+
export const sseTest = () => alova.Get('https://sse.dev/test');

examples/vue/src/routes.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,12 @@ export default [
220220
description: 'An easier way to complete serial request with useHook',
221221
doc: 'client/strategy/use-serial-request',
222222
component: () => import('./views/SerialRequest')
223+
},
224+
{
225+
title: 'Server sent event',
226+
description: 'Receive real-time data from server via SSE(Server Sent Event) protocol, with `fetch` in sse',
227+
doc: 'client/strategy/use-sse',
228+
component: () => import('./views/SSE')
223229
}
224230
]
225231
}

examples/vue/src/views/SSE.vue

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<template>
2+
<div>
3+
<div>
4+
{{ msgs }}
5+
</div>
6+
<div>{{ readyState }}</div>
7+
<nord-button @click="close">Close connection</nord-button>
8+
</div>
9+
</template>
10+
11+
<script setup>
12+
import { useSSE } from 'alova/client';
13+
import { ref } from 'vue';
14+
import { sseTest } from '../api/methods';
15+
16+
const msgs = ref([]);
17+
const { data, onMessage, error, readyState, close } = useSSE(sseTest, {
18+
immediate: true,
19+
interceptByGlobalResponded: false
20+
})
21+
.onMessage(({ data }) => {
22+
msgs.value.push(data);
23+
})
24+
.onError(err => {
25+
console.error('SSE error', err);
26+
});
27+
</script>

packages/client/src/hooks/sse/EventSourceFetch.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,8 @@ export default class EventSourceFetch implements EventTarget {
421421

422422
if (this.readyState !== EventSourceFetch.CLOSED) {
423423
this.readyState = EventSourceFetch.CONNECTING;
424-
// 如果 _reconnectTime 为 null,使用默认值 1000ms
424+
425+
// if _reconnectTime is null, use default value 1000ms
425426
const reconnectDelay = this._reconnectTime ?? 1000;
426427
setTimeoutFn(() => this._connect(), reconnectDelay);
427428
}

packages/server/src/hooks/captcha.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,13 @@ export interface CaptchaProviderOptions {
4848
codeSet?: CaptchaCodeSetType;
4949
}
5050

51-
interface CaptchaData {
52-
code: string;
53-
expireTime: number;
54-
resetTime: number;
55-
}
51+
type CaptchaData = [
52+
{
53+
code: string;
54+
resetTs: number;
55+
},
56+
number
57+
];
5658

5759
const defaultCodeChars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
5860
const defaultCodeLength = 4;
@@ -102,15 +104,15 @@ export const createCaptchaProvider = (options: CaptchaProviderOptions) => {
102104
const sendCaptcha = async (methodHandler: (code: string, key: string) => Method, { key }: { key: string }) => {
103105
const storeKey = getStoreKey(key);
104106
let now = getTime();
105-
const storedData = await store.get<CaptchaData>(storeKey);
107+
const [storedData, expireTs = 0] = (await store.get<CaptchaData>(storeKey)) || [];
106108

107109
// Check if can resend
108-
assert(!storedData || now >= storedData.resetTime, 'Cannot send captcha yet, please wait');
110+
assert(!storedData || now >= storedData.resetTs, 'Cannot send captcha yet, please wait');
109111

110112
// If resendFormStore is enabled and there's an unexpired captcha in storage,
111113
// use the stored captcha
112114
let code: string;
113-
if (resendFormStore && storedData && now < storedData.expireTime) {
115+
if (resendFormStore && storedData && now < expireTs) {
114116
code = storedData.code;
115117
} else {
116118
code = generateCode(codeSet);
@@ -121,12 +123,13 @@ export const createCaptchaProvider = (options: CaptchaProviderOptions) => {
121123

122124
// Store captcha information
123125
now = getTime();
124-
await store.set(storeKey, {
125-
code,
126-
expireTime: now + expireTime,
127-
resetTime: now + resetTime
128-
});
129-
126+
await store.set(storeKey, [
127+
{
128+
code,
129+
resetTs: now + resetTime
130+
},
131+
now + expireTime
132+
] as CaptchaData);
130133
return response;
131134
};
132135

@@ -137,9 +140,9 @@ export const createCaptchaProvider = (options: CaptchaProviderOptions) => {
137140
*/
138141
const verifyCaptcha = async (code: string, key: string) => {
139142
const storeKey = getStoreKey(key);
140-
const storedData = await store.get<CaptchaData>(storeKey);
143+
const [storedData, expireTs = 0] = (await store.get<CaptchaData>(storeKey)) || [];
141144

142-
if (!storedData || getTime() > storedData.expireTime) {
145+
if (!storedData || getTime() > expireTs) {
143146
await store.remove(storeKey);
144147
return false;
145148
}

packages/server/test/captcha.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ describe('captcha', () => {
5959

6060
await sendCaptcha((code, key) => alovaInst.Post('/unit-test', { code, key }), { key: 'test' });
6161

62-
const storedData = await mockStorage.get('alova-captcha:test');
62+
const [storedData, expireTs] = await mockStorage.get('alova-captcha:test');
6363
expect(storedData).toBeDefined();
6464
expect(storedData.code).toMatch(/^\d{4}$/);
65-
expect(storedData.expireTime).toBe(currentTime + 300000);
66-
expect(storedData.resetTime).toBe(currentTime + 60000);
65+
expect(expireTs).toBe(currentTime + 300000);
66+
expect(storedData.resetTs).toBe(currentTime + 60000);
6767
});
6868

6969
test('should prevent resending within resetTime', async () => {
@@ -104,7 +104,7 @@ describe('captcha', () => {
104104

105105
await sendCaptcha((code, key) => alovaInst.Post('/unit-test', { code, key }), { key: 'test' });
106106

107-
const storedData = await mockStorage.get('alova-captcha:test');
107+
const [storedData] = await mockStorage.get('alova-captcha:test');
108108
expect(storedData.code).toMatch(/^[ABC]{4}$/);
109109
});
110110

@@ -119,7 +119,7 @@ describe('captcha', () => {
119119

120120
await sendCaptcha((code, key) => alovaInst.Post('/unit-test', { code, key }), { key: 'test' });
121121

122-
const storedData = await mockStorage.get('alova-captcha:test');
122+
const [storedData] = await mockStorage.get('alova-captcha:test');
123123
expect(storedData.code).toMatch(/^[XYZ]{6}$/);
124124
});
125125

@@ -131,7 +131,7 @@ describe('captcha', () => {
131131

132132
await sendCaptcha((code, key) => alovaInst.Post('/unit-test', { code, key }), { key: 'test' });
133133

134-
const storedData = await mockStorage.get('alova-captcha:test');
134+
const [storedData] = await mockStorage.get('alova-captcha:test');
135135
expect(storedData.code).toBe('1234');
136136
});
137137

packages/storage-redis/src/RedisStorageAdapter.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isArray, isNumber } from '@alova/shared';
12
import { AlovaGlobalCacheAdapter } from 'alova';
23
import Redis, { RedisOptions } from 'ioredis';
34

@@ -36,14 +37,17 @@ class RedisStorageAdapter implements AlovaGlobalCacheAdapter {
3637
}
3738

3839
async set(key: string, value: any) {
39-
const [data, expireTs] = value;
4040
const now = Date.now();
41-
const dataToStore = JSON.stringify(data);
41+
const redisKey = this._getKey(key);
42+
let ttlArg: any[] | undefined;
43+
if (isArray(value) && isNumber(value[1])) {
44+
// if value is an array like [data, expireTimestamp], set the expire time according to the expireTimestamp
45+
const expireTs = value[1];
46+
ttlArg = ['PX', expireTs - now];
47+
}
4248

43-
// Calculate the TTL in milliseconds
44-
const ttl = expireTs - now;
45-
if (ttl > 0) {
46-
await this.client.set(this._getKey(key), dataToStore, 'PX', ttl);
49+
if (!isArray(ttlArg) || ttlArg[1] > 0) {
50+
await this.client.set(redisKey, JSON.stringify(value), ...(ttlArg || []));
4751
}
4852
}
4953

packages/storage-redis/test/redis.spec.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,32 @@
11
import RedisStorageAdapter from '@/RedisStorageAdapter';
2+
import { createAlova, queryCache } from 'alova';
3+
import adapterFetch from 'alova/fetch';
24
import Redis from 'ioredis';
35
import { Mock } from 'vitest';
46

57
vi.mock('ioredis', () => {
8+
const data: Record<string, string> = {};
9+
610
const RedisMock = vi.fn(() => ({
7-
set: vi.fn().mockResolvedValue('OK'),
8-
get: vi.fn().mockResolvedValue(null),
9-
del: vi.fn().mockResolvedValue(1)
11+
set: vi.fn((key: string, value: string) => {
12+
data[key] = value;
13+
return Promise.resolve('OK');
14+
}),
15+
get: vi.fn((key: string) => Promise.resolve(data[key] || null)),
16+
del: vi.fn((key: string) => {
17+
const count = key in data ? 1 : 0;
18+
delete data[key];
19+
return Promise.resolve(count);
20+
}),
21+
// 可选:添加一个清空所有数据的方法用于测试
22+
flushAll: vi.fn(() => {
23+
Object.keys(data).forEach(key => delete data[key]);
24+
return Promise.resolve('OK');
25+
}),
26+
// 可选:添加一个获取所有数据的方法用于测试验证
27+
getAll: vi.fn(() => Promise.resolve({ ...data }))
1028
}));
29+
1130
return { default: RedisMock };
1231
});
1332

@@ -44,7 +63,7 @@ describe('RedisStorageAdapter', () => {
4463

4564
await adapter.set('test', [data, expireTs]);
4665

47-
expect(adapter.client.set).toHaveBeenCalledWith('alova:test', JSON.stringify(data), 'PX', 2000);
66+
expect(adapter.client.set).toHaveBeenCalledWith('alova:test', JSON.stringify([data, expireTs]), 'PX', 2000);
4867
mockDate.mockRestore();
4968
});
5069

@@ -69,7 +88,7 @@ describe('RedisStorageAdapter', () => {
6988
});
7089

7190
test('get should return undefined when key does not exist', async () => {
72-
const result = await adapter.get('test');
91+
const result = await adapter.get('test_not_exist__');
7392
expect(result).toBeUndefined();
7493
});
7594

@@ -87,4 +106,17 @@ describe('RedisStorageAdapter', () => {
87106
expect(adapter.client.del).not.toHaveBeenCalled();
88107
consoleError.mockRestore();
89108
});
109+
110+
test('name should return the adapter name', async () => {
111+
const alova = createAlova({
112+
baseURL: process.env.NODE_BASE_URL,
113+
requestAdapter: adapterFetch(),
114+
responded: response => response.json(),
115+
l1Cache: new RedisStorageAdapter(mockOptions)
116+
});
117+
118+
const method = alova.Get('/unit-test');
119+
const res = await method;
120+
await expect(queryCache(method)).resolves.toEqual(res);
121+
});
90122
});

0 commit comments

Comments
 (0)