daiso 1.2.7 → 1.2.8

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.
@@ -32,8 +32,9 @@ export async function handleOliveyoungSearchProducts(c) {
32
32
  const size = parseInt(c.req.query('size') || '20');
33
33
  const sort = c.req.query('sort') || '01';
34
34
  const includeSoldOut = c.req.query('includeSoldOut') === 'true';
35
- const parsedTimeoutMs = parseInt(c.req.query('timeoutMs') || '15000', 10);
36
- const timeoutMs = Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0 ? parsedTimeoutMs : 15000;
35
+ const defaultTimeoutMs = c.env?.OY_RELAY_URL ? 60000 : 15000;
36
+ const parsedTimeoutMs = parseInt(c.req.query('timeoutMs') || '', 10);
37
+ const timeoutMs = Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0 ? parsedTimeoutMs : defaultTimeoutMs;
37
38
  if (keyword.trim().length === 0) {
38
39
  return errorResponse(c, 'MISSING_QUERY', '검색어(keyword)를 입력해주세요.');
39
40
  }
@@ -76,8 +77,9 @@ export async function handleOliveyoungFindStores(c) {
76
77
  const lng = parseFloat(c.req.query('lng') || '126.978');
77
78
  const pageIdx = parseInt(c.req.query('pageIdx') || '1');
78
79
  const limit = parseInt(c.req.query('limit') || '20');
79
- const parsedTimeoutMs = parseInt(c.req.query('timeoutMs') || '15000', 10);
80
- const timeoutMs = Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0 ? parsedTimeoutMs : 15000;
80
+ const defaultTimeoutMs = c.env?.OY_RELAY_URL ? 60000 : 15000;
81
+ const parsedTimeoutMs = parseInt(c.req.query('timeoutMs') || '', 10);
82
+ const timeoutMs = Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0 ? parsedTimeoutMs : defaultTimeoutMs;
81
83
  try {
82
84
  const result = await fetchOliveyoungStores({
83
85
  latitude: lat,
@@ -116,8 +118,9 @@ export async function handleOliveyoungCheckInventory(c) {
116
118
  const sort = c.req.query('sort') || '01';
117
119
  const includeSoldOut = c.req.query('includeSoldOut') === 'true';
118
120
  const storeLimit = parseInt(c.req.query('storeLimit') || '10');
119
- const parsedTimeoutMs = parseInt(c.req.query('timeoutMs') || '15000', 10);
120
- const timeoutMs = Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0 ? parsedTimeoutMs : 15000;
121
+ const defaultTimeoutMs = c.env?.OY_RELAY_URL ? 60000 : 15000;
122
+ const parsedTimeoutMs = parseInt(c.req.query('timeoutMs') || '', 10);
123
+ const timeoutMs = Number.isFinite(parsedTimeoutMs) && parsedTimeoutMs > 0 ? parsedTimeoutMs : defaultTimeoutMs;
121
124
  const parsedStockCheckLimit = parseInt(c.req.query('stockCheckLimit') || '5', 10);
122
125
  const stockCheckLimit = Number.isFinite(parsedStockCheckLimit) && parsedStockCheckLimit >= 0 ? parsedStockCheckLimit : 5;
123
126
  if (!keyword || keyword.trim().length === 0) {
@@ -4,7 +4,7 @@
4
4
  import * as z from 'zod';
5
5
  import { enrichOliveyoungProductsWithNearbyStoreInventory, fetchOliveyoungProducts, fetchOliveyoungStores, } from '../client.js';
6
6
  async function checkInventory(args, transport) {
7
- const { keyword, latitude = 37.5665, longitude = 126.978, storeKeyword = '', page = 1, size = 20, sort = '01', includeSoldOut = false, storeLimit = 10, stockCheckLimit = 5, timeoutMs = 15000, zyteApiKey, } = args;
7
+ const { keyword, latitude = 37.5665, longitude = 126.978, storeKeyword = '', page = 1, size = 20, sort = '01', includeSoldOut = false, storeLimit = 10, stockCheckLimit = 5, timeoutMs = transport.timeout ?? (transport.relayUrl ? 60000 : 15000), zyteApiKey, } = args;
8
8
  if (!keyword || keyword.trim().length === 0) {
9
9
  throw new Error('상품 검색어(keyword)를 입력해주세요.');
10
10
  }
@@ -15,9 +15,9 @@ async function checkInventory(args, transport) {
15
15
  pageIdx: 1,
16
16
  searchWords: storeKeyword,
17
17
  }, {
18
- timeout: timeoutMs,
19
18
  apiKey: zyteApiKey,
20
19
  ...transport,
20
+ timeout: timeoutMs,
21
21
  }),
22
22
  fetchOliveyoungProducts({
23
23
  keyword,
@@ -26,9 +26,9 @@ async function checkInventory(args, transport) {
26
26
  sort,
27
27
  includeSoldOut,
28
28
  }, {
29
- timeout: timeoutMs,
30
29
  apiKey: zyteApiKey,
31
30
  ...transport,
31
+ timeout: timeoutMs,
32
32
  }),
33
33
  ]);
34
34
  const enrichedInventory = await enrichOliveyoungProductsWithNearbyStoreInventory(productResult.products, {
@@ -37,9 +37,9 @@ async function checkInventory(args, transport) {
37
37
  storeKeyword,
38
38
  maxProducts: Math.min(productResult.products.length, Math.max(0, stockCheckLimit)),
39
39
  }, {
40
- timeout: timeoutMs,
41
40
  apiKey: zyteApiKey,
42
41
  ...transport,
42
+ timeout: timeoutMs,
43
43
  });
44
44
  const inStockProducts = enrichedInventory.products.filter((product) => product.inStock);
45
45
  const result = {
@@ -98,7 +98,7 @@ export function createCheckInventoryTool(apiKey, transport = {}) {
98
98
  .optional()
99
99
  .default(5)
100
100
  .describe('주변 매장별 재고를 보강할 상품 수 (기본값: 5, 최대: 5, 낮출수록 빠름)'),
101
- timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
101
+ timeoutMs: z.number().optional().default(transport.timeout ?? (transport.relayUrl ? 60000 : 15000)).describe('개별 조회 제한 시간(ms, 기본값: 무료 릴레이 60000 / 직접 15000)'),
102
102
  },
103
103
  },
104
104
  handler: ((args) => checkInventory({ ...args, zyteApiKey: apiKey }, transport)),
@@ -4,16 +4,16 @@
4
4
  import * as z from 'zod';
5
5
  import { fetchOliveyoungStores } from '../client.js';
6
6
  async function findNearbyStores(args, transport) {
7
- const { latitude = 37.5665, longitude = 126.978, keyword = '', pageIdx = 1, limit = 20, timeoutMs = 15000, zyteApiKey, } = args;
7
+ const { latitude = 37.5665, longitude = 126.978, keyword = '', pageIdx = 1, limit = 20, timeoutMs = transport.timeout ?? (transport.relayUrl ? 60000 : 15000), zyteApiKey, } = args;
8
8
  const { totalCount, stores } = await fetchOliveyoungStores({
9
9
  latitude,
10
10
  longitude,
11
11
  pageIdx,
12
12
  searchWords: keyword,
13
13
  }, {
14
- timeout: timeoutMs,
15
14
  apiKey: zyteApiKey,
16
15
  ...transport,
16
+ timeout: timeoutMs,
17
17
  });
18
18
  const limitedStores = stores.slice(0, limit);
19
19
  const result = {
@@ -46,7 +46,7 @@ export function createFindNearbyStoresTool(apiKey, transport = {}) {
46
46
  .describe('매장 검색어 (예: 강남, 명동, 신촌). 비우면 주변 매장 전체 조회'),
47
47
  pageIdx: z.number().optional().default(1).describe('매장 결과 페이지 번호 (기본값: 1)'),
48
48
  limit: z.number().optional().default(20).describe('반환할 최대 매장 수 (기본값: 20)'),
49
- timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
49
+ timeoutMs: z.number().optional().default(transport.timeout ?? (transport.relayUrl ? 60000 : 15000)).describe('개별 조회 제한 시간(ms, 기본값: 무료 릴레이 60000 / 직접 15000)'),
50
50
  },
51
51
  },
52
52
  handler: ((args) => findNearbyStores({ ...args, zyteApiKey: apiKey }, transport)),
@@ -4,7 +4,7 @@
4
4
  import * as z from 'zod';
5
5
  import { fetchOliveyoungProducts } from '../client.js';
6
6
  async function searchProducts(args, transport) {
7
- const { keyword, page = 1, size = 20, sort = '01', includeSoldOut = false, timeoutMs = 15000, zyteApiKey, } = args;
7
+ const { keyword, page = 1, size = 20, sort = '01', includeSoldOut = false, timeoutMs = transport.timeout ?? (transport.relayUrl ? 60000 : 15000), zyteApiKey, } = args;
8
8
  if (!keyword || keyword.trim().length === 0) {
9
9
  throw new Error('상품 검색어(keyword)를 입력해주세요.');
10
10
  }
@@ -15,9 +15,9 @@ async function searchProducts(args, transport) {
15
15
  sort,
16
16
  includeSoldOut,
17
17
  }, {
18
- timeout: timeoutMs,
19
18
  apiKey: zyteApiKey,
20
19
  ...transport,
20
+ timeout: timeoutMs,
21
21
  });
22
22
  return {
23
23
  content: [
@@ -50,7 +50,7 @@ export function createSearchProductsTool(apiKey, transport = {}) {
50
50
  size: z.number().optional().default(20).describe('페이지당 상품 수 (기본값: 20)'),
51
51
  sort: z.string().optional().default('01').describe('정렬 코드 (기본값: 01)'),
52
52
  includeSoldOut: z.boolean().optional().default(false).describe('품절 상품 포함 여부 (기본값: false)'),
53
- timeoutMs: z.number().optional().default(15000).describe('요청 제한 시간(ms, 기본값: 15000)'),
53
+ timeoutMs: z.number().optional().default(transport.timeout ?? (transport.relayUrl ? 60000 : 15000)).describe('개별 조회 제한 시간(ms, 기본값: 무료 릴레이 60000 / 직접 15000)'),
54
54
  },
55
55
  },
56
56
  handler: ((args) => searchProducts({ ...args, zyteApiKey: apiKey }, transport)),
@@ -24,7 +24,8 @@ export function isValidOliveyoungRelayUrl(value) {
24
24
  !(relay.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(relay.hostname))));
25
25
  }
26
26
  export async function requestOliveyoung(path, body, options = {}) {
27
- const { timeout = 15000, relayUrl, relayToken } = options;
27
+ const { relayUrl, relayToken, timeout = relayUrl ? 60000 : 15000 } = options;
28
+ const deadline = Date.now() + timeout;
28
29
  const headers = {
29
30
  'Content-Type': 'application/json',
30
31
  Accept: 'application/json',
@@ -64,40 +65,55 @@ export async function requestOliveyoung(path, body, options = {}) {
64
65
  }
65
66
  if (relayUrl) {
66
67
  scope = await relayCredentialScope([relayUrl.replace(/\/$/, ''), relayToken, options.accessClientId, options.accessClientSecret]);
67
- const quota = cooldown.get(scope, consumer);
68
- if (quota)
69
- throw toOliveyoungRelayError(new HttpError(429, '', '', new Headers({
70
- 'x-relay-quota-reason': quota.quotaReason, 'retry-after': String(quota.retryAfter),
71
- })));
72
68
  }
73
- let result;
74
- try {
75
- result = await fetchJson(url, {
76
- method: 'POST',
77
- // Workers는 error 모드를 지원하지 않으므로 따라가지 않고 아래 200 검사로 거절합니다.
78
- redirect: 'manual',
79
- headers,
80
- body: JSON.stringify(body),
81
- timeout,
82
- retries: 0,
83
- expectedStatus: 200,
84
- });
85
- }
86
- catch (error) {
87
- if (relayUrl) {
88
- if (error instanceof HttpError && error.quota)
89
- cooldown.set(scope, consumer, error.quota);
90
- throw toOliveyoungRelayError(error);
69
+ let retried = false;
70
+ while (true) {
71
+ let result;
72
+ const cachedQuota = relayUrl ? cooldown.get(scope, consumer) : undefined;
73
+ try {
74
+ const remaining = deadline - Date.now();
75
+ if (remaining <= 0)
76
+ throw new DOMException('Request deadline exceeded', 'AbortError');
77
+ if (cachedQuota)
78
+ throw new HttpError(429, '', '', new Headers({
79
+ 'x-relay-quota-reason': cachedQuota.quotaReason,
80
+ 'retry-after': String(cachedQuota.retryAfter),
81
+ }));
82
+ result = await fetchJson(url, {
83
+ method: 'POST',
84
+ // Workers는 error 모드를 지원하지 않으므로 따라가지 않고 아래 200 검사로 거절합니다.
85
+ redirect: 'manual',
86
+ headers,
87
+ body: JSON.stringify(body),
88
+ timeout: remaining,
89
+ retries: 0,
90
+ expectedStatus: 200,
91
+ });
92
+ }
93
+ catch (error) {
94
+ if (relayUrl) {
95
+ if (error instanceof HttpError && error.quota) {
96
+ if (!cachedQuota)
97
+ cooldown.set(scope, consumer, error.quota);
98
+ if (!retried && error.quota.quotaReason === 'consumer-busy' &&
99
+ deadline - Date.now() > 1000) {
100
+ retried = true;
101
+ await new Promise(resolve => setTimeout(resolve, 1000));
102
+ continue;
103
+ }
104
+ }
105
+ throw toOliveyoungRelayError(error);
106
+ }
107
+ if (error instanceof Error && error.name === 'AbortError') {
108
+ throw new Error('올리브영 API 요청 시간 초과');
109
+ }
110
+ throw new Error('올리브영 직접 요청 실패. 운영자는 OY_RELAY_URL과 OY_RELAY_TOKEN으로 브라우저 릴레이를 설정해주세요.', { cause: error });
91
111
  }
92
- if (error instanceof Error && error.name === 'AbortError') {
93
- throw new Error('올리브영 API 요청 시간 초과');
112
+ if (result?.status !== 'SUCCESS') {
113
+ if (relayUrl)
114
+ throw toOliveyoungRelayError(new SyntaxError());
115
+ throw new Error(`올리브영 API 상태 오류: ${result?.status || 'UNKNOWN'}`);
94
116
  }
95
- throw new Error('올리브영 직접 요청 실패. 운영자는 OY_RELAY_URL과 OY_RELAY_TOKEN으로 브라우저 릴레이를 설정해주세요.', { cause: error });
96
- }
97
- if (result?.status !== 'SUCCESS') {
98
- if (relayUrl)
99
- throw toOliveyoungRelayError(new SyntaxError());
100
- throw new Error(`올리브영 API 상태 오류: ${result?.status || 'UNKNOWN'}`);
117
+ return result;
101
118
  }
102
- return result;
103
119
  }
@@ -3,7 +3,9 @@ export function parseRelayQuota(status, headers) {
3
3
  return undefined;
4
4
  const reason = headers.get('x-relay-quota-reason');
5
5
  const retry = headers.get('retry-after') || '';
6
- if (reason !== 'minute' && reason !== 'daily' && reason !== 'consumer')
6
+ if (reason !== 'minute' && reason !== 'daily' && reason !== 'consumer' && reason !== 'consumer-busy')
7
+ return undefined;
8
+ if (reason === 'consumer-busy' && retry !== '1')
7
9
  return undefined;
8
10
  if (!/^[1-9][0-9]{0,4}$/.test(retry) || Number(retry) > 86400)
9
11
  return undefined;
@@ -27,7 +29,7 @@ export function createRelayCooldown(now = () => Date.now(), maxEntries = 1024) {
27
29
  },
28
30
  set(scope, consumer, quota) {
29
31
  prune();
30
- const key = quota.quotaReason === 'consumer' ? `${scope}:${consumer}` : scope;
32
+ const key = (quota.quotaReason === 'consumer' || quota.quotaReason === 'consumer-busy') ? `${scope}:${consumer}` : scope;
31
33
  if (!entries.has(key) && entries.size >= maxEntries)
32
34
  return;
33
35
  const until = now() + quota.retryAfter * 1000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "daiso",
3
- "version": "1.2.7",
3
+ "version": "1.2.8",
4
4
  "description": "Korean retail and cinema MCP server, CLI, and Codex Skill for Daiso, convenience stores, marts, Olive Young, and theaters",
5
5
  "main": "dist/index.js",
6
6
  "bin": {