@open-mercato/ui 0.6.7-develop.6834.1.e76f4b8cbc → 0.6.7-develop.6851.1.c1680b00d8

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.
@@ -107,3 +107,64 @@ describe('readApiResultOrThrow', () => {
107
107
  expect(result).toBeNull()
108
108
  })
109
109
  })
110
+
111
+ describe('aborted requests', () => {
112
+ beforeEach(() => {
113
+ jest.resetAllMocks()
114
+ })
115
+
116
+ function createAbortedBodyResponse(): Response {
117
+ const abortError = new Error('The operation was aborted.')
118
+ abortError.name = 'AbortError'
119
+ const response = {
120
+ ok: true,
121
+ status: 200,
122
+ headers: new Map<string, string>(),
123
+ text: jest.fn(async () => { throw abortError }),
124
+ clone: () => response,
125
+ } as unknown as Response
126
+ return response
127
+ }
128
+
129
+ it('rejects with AbortError instead of reporting an empty payload', async () => {
130
+ ;(apiFetch as jest.Mock).mockResolvedValue(createAbortedBodyResponse())
131
+ const controller = new AbortController()
132
+ controller.abort()
133
+
134
+ await expect(
135
+ readApiResultOrThrow('/api/interactions', { signal: controller.signal }),
136
+ ).rejects.toMatchObject({ name: 'AbortError' })
137
+ })
138
+
139
+ it('does not mask a genuinely empty payload when the request was not aborted', async () => {
140
+ ;(apiFetch as jest.Mock).mockResolvedValue(createMockResponse('', { status: 200 }))
141
+ const controller = new AbortController()
142
+
143
+ await expect(
144
+ readApiResultOrThrow('/api/interactions', { signal: controller.signal }),
145
+ ).rejects.toThrow('Missing response payload (200)')
146
+ })
147
+
148
+ it('keeps the caller-provided fallback when one is configured', async () => {
149
+ ;(apiFetch as jest.Mock).mockResolvedValue(createAbortedBodyResponse())
150
+ const controller = new AbortController()
151
+ controller.abort()
152
+
153
+ const call = await apiCall<{ items: string[] }>(
154
+ '/api/interactions',
155
+ { signal: controller.signal },
156
+ { fallback: { items: [] } },
157
+ )
158
+ expect(call.result).toEqual({ items: [] })
159
+ })
160
+
161
+ it('propagates AbortError raised by a custom parser', async () => {
162
+ ;(apiFetch as jest.Mock).mockResolvedValue(createMockResponse('{}', { status: 200 }))
163
+ const abortError = new Error('The operation was aborted.')
164
+ abortError.name = 'AbortError'
165
+
166
+ await expect(
167
+ apiCall('/api/interactions', undefined, { parse: async () => { throw abortError } }),
168
+ ).rejects.toMatchObject({ name: 'AbortError' })
169
+ })
170
+ })
@@ -19,6 +19,27 @@ export type ApiCallResult<TReturn> = {
19
19
 
20
20
  const scopedRequestHeaders = createScopedHeaderStack()
21
21
 
22
+ function isAbortError(error: unknown): boolean {
23
+ return typeof error === 'object'
24
+ && error !== null
25
+ && (error as { name?: unknown }).name === 'AbortError'
26
+ }
27
+
28
+ function createAbortError(): Error {
29
+ if (typeof DOMException === 'function') {
30
+ return new DOMException('[internal] The operation was aborted.', 'AbortError')
31
+ }
32
+ const error = new Error('[internal] The operation was aborted.')
33
+ error.name = 'AbortError'
34
+ return error
35
+ }
36
+
37
+ function resolveAbortSignal(input: RequestInfo | URL, init?: RequestInit): AbortSignal | null {
38
+ if (init?.signal) return init.signal
39
+ if (typeof Request !== 'undefined' && input instanceof Request) return input.signal ?? null
40
+ return null
41
+ }
42
+
22
43
  function mergeHeaders(base: HeadersInit | undefined, extra: Record<string, string>): HeadersInit {
23
44
  if (!base) return extra
24
45
  if (typeof Headers !== 'undefined' && base instanceof Headers) {
@@ -63,9 +84,16 @@ export async function apiCall<TReturn = Record<string, unknown>>(
63
84
  : response
64
85
  if (parser) result = await parser(source)
65
86
  else result = await readJsonSafe<TReturn>(source, fallback)
66
- } catch {
87
+ } catch (err) {
88
+ if (isAbortError(err)) throw err
67
89
  result = fallback
68
90
  }
91
+ // `readJsonSafe` swallows the abort that cancels an in-flight body read, so a
92
+ // request aborted after its response headers arrived would otherwise look like
93
+ // a successful call that returned an empty payload.
94
+ if (result == null && resolveAbortSignal(input, init)?.aborted) {
95
+ throw createAbortError()
96
+ }
69
97
  return {
70
98
  ok: response.ok,
71
99
  status: response.status,
@@ -39,6 +39,7 @@ export type CustomFieldDefDto = {
39
39
  dictionaryId?: string
40
40
  dictionaryInlineCreate?: boolean
41
41
  defaultValue?: string | number | boolean | null
42
+ relatedEntityId?: string
42
43
  }
43
44
 
44
45
  export type CustomFieldsetGroupDto = {
@@ -24,6 +24,7 @@ export function LanguageSwitcher() {
24
24
  pl: t('common.languages.polish', 'Polski'),
25
25
  es: t('common.languages.spanish', 'Español'),
26
26
  de: t('common.languages.german', 'Deutsch'),
27
+ ko: t('common.languages.korean', '한국어'),
27
28
  }
28
29
 
29
30
  async function setLocale(locale: Locale) {