@stackcraftmx/calcita 0.52.0 → 0.54.0

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.
Files changed (211) hide show
  1. package/README.md +109 -4
  2. package/dist/datePickers/DateInput.js +5 -3
  3. package/dist/datePickers/DateInput.js.map +1 -1
  4. package/dist/datePickers/DateTimeInput.js +6 -4
  5. package/dist/datePickers/DateTimeInput.js.map +1 -1
  6. package/dist/datePickers/FormDateField.js +1 -1
  7. package/dist/datePickers/FormDateField.js.map +1 -1
  8. package/dist/datePickers/FormDateTimeField.js +1 -1
  9. package/dist/datePickers/FormDateTimeField.js.map +1 -1
  10. package/dist/datePickers/FormMonthField.js +1 -1
  11. package/dist/datePickers/FormMonthField.js.map +1 -1
  12. package/dist/datePickers/FormTimeField.js +1 -1
  13. package/dist/datePickers/FormTimeField.js.map +1 -1
  14. package/dist/datePickers/MonthInput.js +5 -3
  15. package/dist/datePickers/MonthInput.js.map +1 -1
  16. package/dist/datePickers/PickerFrame.js +2 -2
  17. package/dist/datePickers/PickerFrame.js.map +1 -1
  18. package/dist/datePickers/TimeInput.js +6 -4
  19. package/dist/datePickers/TimeInput.js.map +1 -1
  20. package/dist/feedback/Toaster.js +11 -2
  21. package/dist/feedback/Toaster.js.map +1 -1
  22. package/dist/filepond/FileDropzone.js +60 -19
  23. package/dist/filepond/FileDropzone.js.map +1 -1
  24. package/dist/filepond/pondServer.js +86 -27
  25. package/dist/filepond/pondServer.js.map +1 -1
  26. package/dist/forms/CalcitaFormik.js +11 -0
  27. package/dist/forms/CalcitaFormik.js.map +1 -0
  28. package/dist/forms/FormAttachmentsField.js +1 -1
  29. package/dist/forms/FormAttachmentsField.js.map +1 -1
  30. package/dist/forms/FormCatalogField.js +1 -1
  31. package/dist/forms/FormCatalogField.js.map +1 -1
  32. package/dist/forms/FormCheckboxGroupField.js +1 -1
  33. package/dist/forms/FormCheckboxGroupField.js.map +1 -1
  34. package/dist/forms/FormColorField.js +1 -1
  35. package/dist/forms/FormColorField.js.map +1 -1
  36. package/dist/forms/FormFileField.js +1 -1
  37. package/dist/forms/FormFileField.js.map +1 -1
  38. package/dist/forms/FormFileUploadField.js +1 -1
  39. package/dist/forms/FormFileUploadField.js.map +1 -1
  40. package/dist/forms/FormImageField.js +1 -1
  41. package/dist/forms/FormImageField.js.map +1 -1
  42. package/dist/forms/FormMaskedField.js +1 -1
  43. package/dist/forms/FormMaskedField.js.map +1 -1
  44. package/dist/forms/FormNumberField.js +1 -1
  45. package/dist/forms/FormNumberField.js.map +1 -1
  46. package/dist/forms/FormSchemaProvider.js +17 -0
  47. package/dist/forms/FormSchemaProvider.js.map +1 -0
  48. package/dist/forms/FormSelectField.js +1 -1
  49. package/dist/forms/FormSelectField.js.map +1 -1
  50. package/dist/forms/FormTextField.js +1 -1
  51. package/dist/forms/FormTextField.js.map +1 -1
  52. package/dist/forms/FormTransferListField.js +1 -1
  53. package/dist/forms/FormTransferListField.js.map +1 -1
  54. package/dist/forms/FormWizard.js +2 -0
  55. package/dist/forms/FormWizard.js.map +1 -1
  56. package/dist/forms/schemaRequired.js +33 -0
  57. package/dist/forms/schemaRequired.js.map +1 -0
  58. package/dist/forms/useFormikField.js +18 -5
  59. package/dist/forms/useFormikField.js.map +1 -1
  60. package/dist/forms.js +52 -46
  61. package/dist/forms.js.map +1 -1
  62. package/dist/hookForm/FormAttachmentsField.js +1 -1
  63. package/dist/hookForm/FormAttachmentsField.js.map +1 -1
  64. package/dist/hookForm/FormCatalogField.js +1 -1
  65. package/dist/hookForm/FormCatalogField.js.map +1 -1
  66. package/dist/hookForm/FormCheckboxGroupField.js +1 -1
  67. package/dist/hookForm/FormCheckboxGroupField.js.map +1 -1
  68. package/dist/hookForm/FormColorField.js +1 -1
  69. package/dist/hookForm/FormColorField.js.map +1 -1
  70. package/dist/hookForm/FormFileField.js +1 -1
  71. package/dist/hookForm/FormFileField.js.map +1 -1
  72. package/dist/hookForm/FormFileUploadField.js +1 -1
  73. package/dist/hookForm/FormFileUploadField.js.map +1 -1
  74. package/dist/hookForm/FormImageField.js +1 -1
  75. package/dist/hookForm/FormImageField.js.map +1 -1
  76. package/dist/hookForm/FormMaskedField.js +1 -1
  77. package/dist/hookForm/FormMaskedField.js.map +1 -1
  78. package/dist/hookForm/FormNumberField.js +1 -1
  79. package/dist/hookForm/FormNumberField.js.map +1 -1
  80. package/dist/hookForm/FormSelectField.js +1 -1
  81. package/dist/hookForm/FormSelectField.js.map +1 -1
  82. package/dist/hookForm/FormTextField.js +1 -1
  83. package/dist/hookForm/FormTextField.js.map +1 -1
  84. package/dist/hookForm/FormTransferListField.js +1 -1
  85. package/dist/hookForm/FormTransferListField.js.map +1 -1
  86. package/dist/hookForm/FormWizard.js +2 -0
  87. package/dist/hookForm/FormWizard.js.map +1 -1
  88. package/dist/hookForm/useHookFormField.js +9 -3
  89. package/dist/hookForm/useHookFormField.js.map +1 -1
  90. package/dist/index.js +6 -2
  91. package/dist/index.js.map +1 -1
  92. package/dist/inputs/CatalogInput.js +3 -2
  93. package/dist/inputs/CatalogInput.js.map +1 -1
  94. package/dist/inputs/CheckboxGroupInput.js +2 -1
  95. package/dist/inputs/CheckboxGroupInput.js.map +1 -1
  96. package/dist/inputs/ColorInput.js +3 -1
  97. package/dist/inputs/ColorInput.js.map +1 -1
  98. package/dist/inputs/FieldFrame.js +18 -3
  99. package/dist/inputs/FieldFrame.js.map +1 -1
  100. package/dist/inputs/FormSection.js +42 -11
  101. package/dist/inputs/FormSection.js.map +1 -1
  102. package/dist/inputs/NumberInput.js +3 -1
  103. package/dist/inputs/NumberInput.js.map +1 -1
  104. package/dist/inputs/SelectInput.js +3 -1
  105. package/dist/inputs/SelectInput.js.map +1 -1
  106. package/dist/inputs/TextInput.js +1 -1
  107. package/dist/inputs/TextInput.js.map +1 -1
  108. package/dist/inputs/TransferListInput.js +2 -1
  109. package/dist/inputs/TransferListInput.js.map +1 -1
  110. package/dist/inputs/WizardLayout.js +12 -6
  111. package/dist/inputs/WizardLayout.js.map +1 -1
  112. package/dist/inputs/formLayoutContext.js +7 -2
  113. package/dist/inputs/formLayoutContext.js.map +1 -1
  114. package/dist/inputs/requiredMark.js +17 -0
  115. package/dist/inputs/requiredMark.js.map +1 -0
  116. package/dist/locales/en.js +2 -0
  117. package/dist/locales/en.js.map +1 -1
  118. package/dist/locales/es.js +2 -0
  119. package/dist/locales/es.js.map +1 -1
  120. package/dist/richText/FormRichTextField.js +1 -1
  121. package/dist/richText/FormRichTextField.js.map +1 -1
  122. package/dist/richText/RichTextEditor.js +4 -3
  123. package/dist/richText/RichTextEditor.js.map +1 -1
  124. package/dist/toolkit/ToolkitProvider.js +3 -0
  125. package/dist/toolkit/ToolkitProvider.js.map +1 -1
  126. package/dist/toolkit/toolkitContext.js.map +1 -1
  127. package/dist/types/datePickers/DateInput.d.ts +2 -0
  128. package/dist/types/datePickers/DateTimeInput.d.ts +2 -0
  129. package/dist/types/datePickers/FormDateField.d.ts +3 -3
  130. package/dist/types/datePickers/FormDateTimeField.d.ts +3 -3
  131. package/dist/types/datePickers/FormMonthField.d.ts +3 -3
  132. package/dist/types/datePickers/FormTimeField.d.ts +3 -3
  133. package/dist/types/datePickers/MonthInput.d.ts +2 -0
  134. package/dist/types/datePickers/PickerFrame.d.ts +3 -1
  135. package/dist/types/datePickers/TimeInput.d.ts +2 -0
  136. package/dist/types/feedback/Toaster.d.ts +15 -1
  137. package/dist/types/feedback/index.d.ts +1 -1
  138. package/dist/types/filepond/FileDropzone.d.ts +5 -0
  139. package/dist/types/filepond/chunkedUpload.d.ts +122 -0
  140. package/dist/types/filepond/pondServer.d.ts +25 -3
  141. package/dist/types/forms/CalcitaFormik.d.ts +6 -0
  142. package/dist/types/forms/FormAttachmentsField.d.ts +3 -3
  143. package/dist/types/forms/FormCatalogField.d.ts +2 -2
  144. package/dist/types/forms/FormCheckboxGroupField.d.ts +3 -3
  145. package/dist/types/forms/FormColorField.d.ts +2 -2
  146. package/dist/types/forms/FormFileField.d.ts +3 -3
  147. package/dist/types/forms/FormFileUploadField.d.ts +3 -3
  148. package/dist/types/forms/FormImageField.d.ts +3 -3
  149. package/dist/types/forms/FormMaskedField.d.ts +2 -2
  150. package/dist/types/forms/FormNumberField.d.ts +2 -2
  151. package/dist/types/forms/FormSchemaProvider.d.ts +13 -0
  152. package/dist/types/forms/FormSelectField.d.ts +2 -2
  153. package/dist/types/forms/FormTextField.d.ts +2 -2
  154. package/dist/types/forms/FormTransferListField.d.ts +3 -3
  155. package/dist/types/forms/FormWizard.d.ts +3 -1
  156. package/dist/types/forms/index.d.ts +1 -0
  157. package/dist/types/forms/schemaRequired.d.ts +14 -0
  158. package/dist/types/forms/useFormikField.d.ts +10 -4
  159. package/dist/types/forms.d.ts +4 -2
  160. package/dist/types/hookForm/FormAttachmentsField.d.ts +3 -3
  161. package/dist/types/hookForm/FormCatalogField.d.ts +3 -3
  162. package/dist/types/hookForm/FormCheckboxGroupField.d.ts +3 -3
  163. package/dist/types/hookForm/FormColorField.d.ts +3 -3
  164. package/dist/types/hookForm/FormFileField.d.ts +3 -3
  165. package/dist/types/hookForm/FormFileUploadField.d.ts +3 -3
  166. package/dist/types/hookForm/FormImageField.d.ts +3 -3
  167. package/dist/types/hookForm/FormMaskedField.d.ts +3 -3
  168. package/dist/types/hookForm/FormNumberField.d.ts +3 -3
  169. package/dist/types/hookForm/FormSelectField.d.ts +3 -3
  170. package/dist/types/hookForm/FormTextField.d.ts +3 -3
  171. package/dist/types/hookForm/FormTransferListField.d.ts +3 -3
  172. package/dist/types/hookForm/FormWizard.d.ts +3 -1
  173. package/dist/types/hookForm/useHookFormField.d.ts +8 -4
  174. package/dist/types/index.d.ts +3 -2
  175. package/dist/types/inputs/CatalogInput.d.ts +1 -0
  176. package/dist/types/inputs/CheckboxGroupInput.d.ts +1 -0
  177. package/dist/types/inputs/ColorInput.d.ts +1 -0
  178. package/dist/types/inputs/FieldFrame.d.ts +1 -0
  179. package/dist/types/inputs/FormSection.d.ts +13 -0
  180. package/dist/types/inputs/NumberInput.d.ts +1 -0
  181. package/dist/types/inputs/SelectInput.d.ts +1 -0
  182. package/dist/types/inputs/TextInput.d.ts +1 -1
  183. package/dist/types/inputs/TransferListInput.d.ts +1 -0
  184. package/dist/types/inputs/WizardLayout.d.ts +1 -0
  185. package/dist/types/inputs/formLayoutContext.d.ts +8 -1
  186. package/dist/types/inputs/requiredMark.d.ts +9 -0
  187. package/dist/types/locales/en.d.ts +2 -0
  188. package/dist/types/locales/es.d.ts +2 -0
  189. package/dist/types/richText/FormRichTextField.d.ts +3 -3
  190. package/dist/types/richText/RichTextEditor.d.ts +1 -0
  191. package/dist/types/toolkit/ToolkitProvider.d.ts +5 -1
  192. package/dist/types/toolkit/toolkitContext.d.ts +2 -1
  193. package/dist/types/upload/AttachmentsInput.d.ts +2 -0
  194. package/dist/types/upload/FileInput.d.ts +1 -0
  195. package/dist/types/upload/FileUploadInput.d.ts +1 -0
  196. package/dist/types/upload/ImageInput.d.ts +1 -0
  197. package/dist/types/upload/chunkedUpload.d.ts +189 -0
  198. package/dist/types/upload/uploadWithProgress.d.ts +4 -2
  199. package/dist/upload/AttachmentsInput.js +2 -1
  200. package/dist/upload/AttachmentsInput.js.map +1 -1
  201. package/dist/upload/FileInput.js +2 -1
  202. package/dist/upload/FileInput.js.map +1 -1
  203. package/dist/upload/FileUploadInput.js +2 -1
  204. package/dist/upload/FileUploadInput.js.map +1 -1
  205. package/dist/upload/ImageInput.js +2 -1
  206. package/dist/upload/ImageInput.js.map +1 -1
  207. package/dist/upload/chunkedUpload.js +261 -0
  208. package/dist/upload/chunkedUpload.js.map +1 -0
  209. package/dist/upload/uploadWithProgress.js +8 -3
  210. package/dist/upload/uploadWithProgress.js.map +1 -1
  211. package/package.json +1 -1
@@ -0,0 +1,261 @@
1
+ import { ApiError } from "../api/ApiError.js";
2
+ import { createLogger } from "../observability/logger.js";
3
+ import { toolkitSettings, translateToolkit } from "../toolkit/toolkitSettings.js";
4
+ import { parseFileSize } from "./fileRules.js";
5
+ import { uploadWithProgress } from "./uploadWithProgress.js";
6
+ const log = createLogger("upload");
7
+ const CHUNK_HEADER = Object.freeze({
8
+ length: "Upload-Length",
9
+ offset: "Upload-Offset",
10
+ name: "Upload-Name",
11
+ contentType: "Content-Type"
12
+ });
13
+ const UPLOAD_FAILURE = Object.freeze({ rejected: "rejected", noResponse: "noResponse", noTransferId: "noTransferId" });
14
+ const CHUNK_CONTENT_TYPE = "application/offset+octet-stream";
15
+ const HONEST_CEILING = 0.99;
16
+ const SERVER_ERROR_STATUS = 500;
17
+ const OFFLINE_STATUS = 0;
18
+ const DEFAULT_CHUNK_SIZE = 5e6;
19
+ const DEFAULT_CHUNK_RETRY_DELAYS = Object.freeze([500, 1e3, 3e3]);
20
+ class UploadFailure extends Error {
21
+ /**
22
+ * @param {string} reason de UPLOAD_FAILURE
23
+ * @param {Response|null} [response]
24
+ */
25
+ constructor(reason, response = null) {
26
+ super(`La subida no salió: ${reason}`);
27
+ this.name = "UploadFailure";
28
+ this.reason = reason;
29
+ this.response = response;
30
+ }
31
+ }
32
+ function chunkPiecesOf(fileSize, chunkSize) {
33
+ const count = Math.max(1, Math.ceil(fileSize / chunkSize));
34
+ return Array.from({ length: count }, (_, index) => {
35
+ const offset = index * chunkSize;
36
+ return { index, offset, end: Math.min(offset + chunkSize, fileSize) };
37
+ });
38
+ }
39
+ function honestLoadedOf({ confirmedBytes, sendingBytes, totalBytes }) {
40
+ return Math.min(confirmedBytes + sendingBytes, Math.floor(totalBytes * HONEST_CEILING));
41
+ }
42
+ function storedBytesOf(pieces, serverOffset) {
43
+ return pieces.filter((piece) => piece.end > piece.offset && piece.end <= serverOffset).reduce((last, piece) => Math.max(last, piece.end), 0);
44
+ }
45
+ function headerTextOf(value) {
46
+ return String.fromCharCode(...new TextEncoder().encode(String(value)));
47
+ }
48
+ function headerTextsOf(headers) {
49
+ return Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, headerTextOf(value)]));
50
+ }
51
+ function waitBeforeRetry(milliseconds, signal) {
52
+ return new Promise((resolve, reject) => {
53
+ const timer = setTimeout(resolve, milliseconds);
54
+ signal.addEventListener("abort", () => {
55
+ clearTimeout(timer);
56
+ reject(new DOMException("La subida se canceló", "AbortError"));
57
+ }, { once: true });
58
+ });
59
+ }
60
+ async function sendWithRetries(sendOnce, retryDelays, signal) {
61
+ const delays = [...retryDelays ?? []];
62
+ for (; ; ) {
63
+ let response = null;
64
+ try {
65
+ response = await sendOnce();
66
+ } catch (failure) {
67
+ if (failure?.name === "AbortError") {
68
+ throw failure;
69
+ }
70
+ }
71
+ if (response?.ok) {
72
+ return response;
73
+ }
74
+ const canRetry = response === null || response.status >= SERVER_ERROR_STATUS;
75
+ if (!canRetry || delays.length === 0) {
76
+ throw new UploadFailure(response ? UPLOAD_FAILURE.rejected : UPLOAD_FAILURE.noResponse, response);
77
+ }
78
+ await waitBeforeRetry(delays.shift(), signal);
79
+ }
80
+ }
81
+ function defaultPatchUrlOf(url) {
82
+ return `${url}${url.includes("?") ? "&" : "?"}patch=`;
83
+ }
84
+ async function problemOfResponse(response, toProblem) {
85
+ let body = null;
86
+ try {
87
+ body = JSON.parse(await response.text());
88
+ } catch {
89
+ }
90
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
91
+ return null;
92
+ }
93
+ if (!toProblem) {
94
+ return body;
95
+ }
96
+ try {
97
+ const problem = toProblem(body, response);
98
+ return problem && typeof problem === "object" && !Array.isArray(problem) ? problem : null;
99
+ } catch (failure) {
100
+ log.error("toProblem tronó al leer el error de una subida", failure);
101
+ return null;
102
+ }
103
+ }
104
+ function startChunkedUpload(options) {
105
+ const controller = new AbortController();
106
+ const done = runChunkedUpload(options, controller.signal);
107
+ return { done, abort: () => controller.abort() };
108
+ }
109
+ async function runChunkedUpload(options, signal) {
110
+ const {
111
+ file,
112
+ fileName,
113
+ fieldName,
114
+ metadata = null,
115
+ url,
116
+ patchUrl,
117
+ headers = {},
118
+ withCredentials = false,
119
+ chunkSize,
120
+ retryDelays,
121
+ transferId = null,
122
+ onTransferId,
123
+ onProgress,
124
+ holdBeforeFull = true,
125
+ sendRequest
126
+ } = options;
127
+ const totalBytes = file.size;
128
+ const pieces = chunkPiecesOf(totalBytes, chunkSize);
129
+ const send = (requestUrl, body, method, protocolHeaders, onUploadProgress) => sendRequest(requestUrl, body, {
130
+ method,
131
+ headers: headerTextsOf({ ...headers, ...protocolHeaders }),
132
+ withCredentials,
133
+ signal,
134
+ onProgress: onUploadProgress
135
+ });
136
+ let shownBytes = 0;
137
+ let confirmedBytes = 0;
138
+ const showProgress = (sendingBytes) => {
139
+ if (signal.aborted) {
140
+ return;
141
+ }
142
+ const loadedBytes = holdBeforeFull ? honestLoadedOf({ confirmedBytes, sendingBytes, totalBytes }) : Math.min(confirmedBytes + sendingBytes, totalBytes);
143
+ shownBytes = Math.max(shownBytes, loadedBytes);
144
+ if (totalBytes > 0) {
145
+ onProgress?.(shownBytes, totalBytes);
146
+ }
147
+ };
148
+ let folio = transferId;
149
+ if (folio) {
150
+ const offsetResponse = await sendWithRetries(() => send(`${patchUrl}${encodeURIComponent(folio)}`, null, "HEAD", {}), retryDelays, signal);
151
+ const serverOffset = Number.parseInt(offsetResponse.headers.get(CHUNK_HEADER.offset) ?? "", 10);
152
+ confirmedBytes = storedBytesOf(pieces, Number.isNaN(serverOffset) ? 0 : serverOffset);
153
+ } else {
154
+ const firstRequest = new FormData();
155
+ if (metadata && typeof metadata === "object") {
156
+ firstRequest.append(fieldName, JSON.stringify(metadata));
157
+ }
158
+ const transferResponse = await sendWithRetries(() => send(url, firstRequest, "POST", { [CHUNK_HEADER.length]: totalBytes }), retryDelays, signal);
159
+ folio = (await transferResponse.text()).trim();
160
+ if (!folio) {
161
+ throw new UploadFailure(UPLOAD_FAILURE.noTransferId, transferResponse);
162
+ }
163
+ onTransferId?.(folio);
164
+ }
165
+ showProgress(0);
166
+ for (const piece of pieces) {
167
+ const isAlreadyStored = piece.end > piece.offset && piece.end <= confirmedBytes;
168
+ if (isAlreadyStored) {
169
+ continue;
170
+ }
171
+ const pieceHeaders = {
172
+ [CHUNK_HEADER.contentType]: CHUNK_CONTENT_TYPE,
173
+ [CHUNK_HEADER.offset]: piece.offset,
174
+ [CHUNK_HEADER.length]: totalBytes,
175
+ [CHUNK_HEADER.name]: fileName
176
+ };
177
+ await sendWithRetries(
178
+ () => send(
179
+ `${patchUrl}${encodeURIComponent(folio)}`,
180
+ file.slice(piece.offset, piece.end, CHUNK_CONTENT_TYPE),
181
+ "PATCH",
182
+ pieceHeaders,
183
+ (percent, transfer) => showProgress(transfer.loaded)
184
+ ),
185
+ retryDelays,
186
+ signal
187
+ );
188
+ confirmedBytes = piece.end;
189
+ showProgress(0);
190
+ }
191
+ return folio;
192
+ }
193
+ async function apiErrorOfFailure(failure, toProblem) {
194
+ if (failure?.reason === UPLOAD_FAILURE.noTransferId) {
195
+ const message = translateToolkit("common", "upload.pond.noTransferId");
196
+ return new ApiError({ status: failure.response?.status ?? OFFLINE_STATUS, problem: { title: message, detail: message } });
197
+ }
198
+ if (failure?.reason === UPLOAD_FAILURE.rejected && failure.response) {
199
+ return new ApiError({ status: failure.response.status, problem: await problemOfResponse(failure.response, toProblem) });
200
+ }
201
+ if (failure?.reason !== UPLOAD_FAILURE.noResponse) {
202
+ log.error("La subida por trozos falló sin respuesta del servidor", failure);
203
+ }
204
+ return new ApiError({ status: OFFLINE_STATUS });
205
+ }
206
+ function createChunkedUpload({
207
+ url,
208
+ patchUrl,
209
+ chunkSize = DEFAULT_CHUNK_SIZE,
210
+ chunkRetryDelays = DEFAULT_CHUNK_RETRY_DELAYS,
211
+ headers = {},
212
+ withCredentials = false,
213
+ toProblem,
214
+ sendRequest = uploadWithProgress,
215
+ fieldName = "file",
216
+ metadata = null
217
+ }) {
218
+ const chunkBytes = parseFileSize(chunkSize);
219
+ const piecesUrl = patchUrl ?? defaultPatchUrlOf(url);
220
+ return (file, { signal, onUploadProgress } = {}) => new Promise((resolve, reject) => {
221
+ const running = startChunkedUpload({
222
+ file,
223
+ fileName: file.name,
224
+ fieldName,
225
+ metadata,
226
+ url,
227
+ patchUrl: piecesUrl,
228
+ withCredentials,
229
+ chunkSize: chunkBytes,
230
+ headers: { "Accept-Language": toolkitSettings().language, ...headers },
231
+ retryDelays: chunkRetryDelays,
232
+ sendRequest,
233
+ holdBeforeFull: false,
234
+ onProgress: (loaded, total) => onUploadProgress?.({ loaded, total })
235
+ });
236
+ signal?.addEventListener("abort", () => running.abort(), { once: true });
237
+ if (signal?.aborted) {
238
+ running.abort();
239
+ }
240
+ running.done.then(
241
+ (folio) => resolve({ id: folio }),
242
+ async (failure) => reject(failure?.name === "AbortError" ? failure : await apiErrorOfFailure(failure, toProblem))
243
+ );
244
+ });
245
+ }
246
+ export {
247
+ CHUNK_HEADER,
248
+ DEFAULT_CHUNK_RETRY_DELAYS,
249
+ DEFAULT_CHUNK_SIZE,
250
+ UPLOAD_FAILURE,
251
+ UploadFailure,
252
+ chunkPiecesOf,
253
+ createChunkedUpload,
254
+ defaultPatchUrlOf,
255
+ headerTextOf,
256
+ honestLoadedOf,
257
+ problemOfResponse,
258
+ startChunkedUpload,
259
+ storedBytesOf
260
+ };
261
+ //# sourceMappingURL=chunkedUpload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunkedUpload.js","sources":["../../src/upload/chunkedUpload.js"],"sourcesContent":["// Subir UN archivo por trozos, con el protocolo de trozos de FilePond (el que ya esperan los backends que lo usan):\n// 1. POST a url con «Upload-Length» (lo que pesa el archivo): el servidor aparta un lugar y contesta su folio (texto)\n// 2. cada trozo en su PATCH a patchUrl + folio, uno detrás de otro, con Upload-Offset (dónde empieza), Upload-Length y\n// Upload-Name; el cuerpo es el pedazo del archivo tal cual (application/offset+octet-stream)\n// 3. al reintentar después de un error, HEAD a patchUrl + folio: «Upload-Offset» dice desde dónde seguir\n//\n// Lo usan la zona de FilePond (createPondServer, con chunkUploads) y el cargador mini (createChunkedUpload, la función\n// upload de FileUploadInput y FormFileUploadField).\n//\n// Por qué lo hace Calcita y no FilePond: FilePond cuenta lo que ya SALIÓ del navegador y pinta «100 %» mientras el servidor\n// todavía guarda el último trozo (hay servidores que esperan medio segundo por trozo, a propósito). Aquí la barra es honesta:\n// - suma lo que el servidor ya confirmó más lo que va en camino del trozo de ahora;\n// - nunca regresa (un trozo que se reintenta no baja la barra);\n// - el final depende de la vista (holdBeforeFull): la tarjeta de FilePond no tiene «Procesando», así que se queda en 99 %\n// hasta que el servidor confirma el último trozo; el cargador mini sí lo tiene: llega a 100 % cuando salió el último\n// byte y su «Procesando» dice que el servidor está guardando.\n// Los trozos van uno tras otro, nunca juntos: el que tarda es el servidor y mandar dos a la vez no gana nada.\n//\n// Reintentos: solo cuando no hubo respuesta o el servidor falló (5xx), con las esperas de retryDelays. Un 4xx es el servidor\n// diciendo que no: se enseña en seguida, sin hacer esperar a la persona.\n//\n// OJO, distinto de FilePond: con un archivo que mide justo N trozos, FilePond manda al final un PATCH vacío de más (el\n// servidor ya había armado el archivo); aquí no.\n//\n// Cada petición sale por sendRequest, con la misma forma que uploadWithProgress de la casa (url, cuerpo, {method, headers,\n// onProgress, signal, withCredentials}) → Response: así el proyecto la pasa por su propio cliente, o se prueba sin servidor.\n//\n// Funciones del navegador, sin React: function (regla de la casa). Se prueba con node --test (chunkedUpload.test.js) con un\n// servidor de mentira.\nimport {ApiError} from '@/api/ApiError.js';\nimport {createLogger} from '@/observability/logger.js';\nimport {toolkitSettings, translateToolkit} from '@/toolkit/toolkitSettings.js';\nimport {parseFileSize} from '@/upload/fileRules.js';\nimport {uploadWithProgress} from '@/upload/uploadWithProgress.js';\n\nconst log = createLogger('upload');\n\n/** Los headers del protocolo de trozos de FilePond. */\nexport const CHUNK_HEADER = Object.freeze({\n length: 'Upload-Length',\n offset: 'Upload-Offset',\n name: 'Upload-Name',\n contentType: 'Content-Type',\n});\n\n/** Por qué no se pudo subir: el servidor dijo que no, no contestó, o no dio el folio de la subida. */\nexport const UPLOAD_FAILURE = Object.freeze({rejected: 'rejected', noResponse: 'noResponse', noTransferId: 'noTransferId'});\n\n// el cuerpo de cada trozo: los bytes del archivo tal cual, desde Upload-Offset\nconst CHUNK_CONTENT_TYPE = 'application/offset+octet-stream';\n// la barra se detiene aquí hasta que el servidor confirma el último trozo (FilePond redondea: 99.6 % ya diría «100 %»)\nconst HONEST_CEILING = 0.99;\nconst SERVER_ERROR_STATUS = 500;\n// status de «no hubo respuesta» (los textos del paquete dicen «sin conexión»)\nconst OFFLINE_STATUS = 0;\n/** Los de FilePond: trozos de 5 MB y tres reintentos por trozo. */\nexport const DEFAULT_CHUNK_SIZE = 5_000_000;\nexport const DEFAULT_CHUNK_RETRY_DELAYS = Object.freeze([500, 1000, 3000]);\n\n/** Una subida que no salió: el motivo (UPLOAD_FAILURE) y la respuesta del servidor, si la hubo. */\nexport class UploadFailure extends Error {\n /**\n * @param {string} reason de UPLOAD_FAILURE\n * @param {Response|null} [response]\n */\n constructor(reason, response = null) {\n super(`La subida no salió: ${reason}`);\n this.name = 'UploadFailure';\n this.reason = reason;\n this.response = response;\n }\n}\n\n/**\n * Los trozos de un archivo: dónde empieza y dónde acaba cada uno. Un archivo vacío es un trozo vacío: el servidor igual\n * necesita saber que el archivo llegó completo.\n *\n * @param {number} fileSize en bytes\n * @param {number} chunkSize en bytes\n * @returns {{index: number, offset: number, end: number}[]}\n */\nexport function chunkPiecesOf(fileSize, chunkSize) {\n const count = Math.max(1, Math.ceil(fileSize / chunkSize));\n return Array.from({length: count}, (_, index) => {\n const offset = index * chunkSize;\n return {index, offset, end: Math.min(offset + chunkSize, fileSize)};\n });\n}\n\n/**\n * Lo que la barra puede decir que ya subió: lo que el servidor confirmó más lo que va en camino del trozo de ahora, sin\n * llegar al final hasta que el servidor confirma el último trozo.\n *\n * @param {{confirmedBytes: number, sendingBytes: number, totalBytes: number}} transfer\n * @returns {number} bytes\n */\nexport function honestLoadedOf({confirmedBytes, sendingBytes, totalBytes}) {\n return Math.min(confirmedBytes + sendingBytes, Math.floor(totalBytes * HONEST_CEILING));\n}\n\n/**\n * Hasta dónde ya está guardado el archivo cuando el servidor dice su Upload-Offset: el final del último trozo COMPLETO antes\n * de ese punto. Un trozo que quedó a medias se vuelve a mandar entero.\n *\n * @param {{offset: number, end: number}[]} pieces\n * @param {number} serverOffset\n * @returns {number}\n */\nexport function storedBytesOf(pieces, serverOffset) {\n return pieces.filter((piece) => piece.end > piece.offset && piece.end <= serverOffset).reduce((last, piece) => Math.max(last, piece.end), 0);\n}\n\n/**\n * Un texto listo para viajar en un header. Los headers solo llevan bytes: el texto va en UTF-8 byte por byte (lo mismo que\n * FilePond hace con los suyos). Sin esto, un nombre con «–» o con un emoji tumba la petición y uno con acentos llega roto.\n *\n * @param {unknown} value\n * @returns {string}\n */\nexport function headerTextOf(value) {\n return String.fromCharCode(...new TextEncoder().encode(String(value)));\n}\n\n/**\n * @param {Record<string, unknown>} headers\n * @returns {Record<string, string>}\n */\nfunction headerTextsOf(headers) {\n return Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, headerTextOf(value)]));\n}\n\n/**\n * Espera antes de reintentar; si la persona cancela mientras tanto, termina en AbortError como lo demás.\n *\n * @param {number} milliseconds\n * @param {AbortSignal} signal\n * @returns {Promise<void>}\n */\nfunction waitBeforeRetry(milliseconds, signal) {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(resolve, milliseconds);\n signal.addEventListener('abort', () => {\n clearTimeout(timer);\n reject(new DOMException('La subida se canceló', 'AbortError'));\n }, {once: true});\n });\n}\n\n/**\n * Manda una petición y la repite si no hubo respuesta o el servidor falló (5xx), con las esperas de retryDelays. Regresa la\n * respuesta buena; si no la hubo, termina en UploadFailure. Cancelar termina en AbortError.\n *\n * @param {() => Promise<Response>} sendOnce\n * @param {number[]|null|undefined} retryDelays\n * @param {AbortSignal} signal\n * @returns {Promise<Response>}\n */\nasync function sendWithRetries(sendOnce, retryDelays, signal) {\n const delays = [...(retryDelays ?? [])];\n for (;;) {\n let response = null;\n try {\n response = await sendOnce();\n } catch (failure) {\n // cancelar no es un error de la subida: sube tal cual\n if (failure?.name === 'AbortError') {\n throw failure;\n }\n // sin respuesta (sin red, el servidor se cayó): se puede reintentar\n }\n if (response?.ok) {\n return response;\n }\n const canRetry = response === null || response.status >= SERVER_ERROR_STATUS;\n if (!canRetry || delays.length === 0) {\n throw new UploadFailure(response ? UPLOAD_FAILURE.rejected : UPLOAD_FAILURE.noResponse, response);\n }\n await waitBeforeRetry(delays.shift(), signal);\n }\n}\n\n/**\n * @typedef {(url: string, body: FormData|Blob|null, options: {method: string, headers: Record<string, string>, withCredentials: boolean, signal: AbortSignal, onProgress?: (percent: number, transfer: {loaded: number, total: number}) => void}) => Promise<Response>} UploadSendRequest\n * cómo viaja cada petición: la misma forma que uploadWithProgress de la casa\n */\n\n/**\n * A dónde van los trozos si no se dice: lo que usa FilePond, «?patch=» pegado a url (o «&patch=» si url ya trae «?»).\n *\n * @param {string} url\n * @returns {string}\n */\nexport function defaultPatchUrlOf(url) {\n return `${url}${url.includes('?') ? '&' : '?'}patch=`;\n}\n\n/**\n * El problema del cuerpo de error: el JSON del servidor, pasado por el adaptador del proyecto (toProblem). Lo que no sea un\n * objeto cuenta como sin problema (se usa el texto genérico del paquete). Un adaptador que truena no tumba el mensaje.\n *\n * @param {Response} response\n * @param {((body: any, response: Response) => any)|undefined} toProblem\n * @returns {Promise<object|null>}\n */\nexport async function problemOfResponse(response, toProblem) {\n let body = null;\n try {\n body = JSON.parse(await response.text());\n } catch {\n // la respuesta no era JSON (un 502 del proxy en HTML): va el título genérico de su status\n }\n if (!body || typeof body !== 'object' || Array.isArray(body)) {\n return null;\n }\n if (!toProblem) {\n return body;\n }\n try {\n const problem = toProblem(body, response);\n return problem && typeof problem === 'object' && !Array.isArray(problem) ? problem : null;\n } catch (failure) {\n log.error('toProblem tronó al leer el error de una subida', failure);\n return null;\n }\n}\n\n/**\n * Sube un archivo por trozos (ver el encabezado). Regresa la subida en curso: done termina con el folio que dio el servidor\n * y abort la cancela de verdad (la petición de ahora y los reintentos que esperaban).\n *\n * @param {object} options\n * @param {Blob} options.file\n * @param {string} options.fileName el que viaja en Upload-Name\n * @param {string} options.fieldName el campo del primer POST: lleva los metadatos en JSON, como FilePond\n * @param {object|null} [options.metadata]\n * @param {string} options.url a dónde va el POST que pide el folio\n * @param {string} options.patchUrl a dónde va cada trozo: el folio va pegado al final ('/subidas?patch=')\n * @param {Record<string, unknown>} [options.headers] van en las tres peticiones (Authorization, los datos de la pantalla)\n * @param {boolean} [options.withCredentials]\n * @param {number} options.chunkSize en bytes\n * @param {number[]|null} [options.retryDelays] las esperas entre reintentos, en milisegundos; null o [] = sin reintentos\n * @param {string|null} [options.transferId] el folio de un intento anterior: se sigue desde donde se quedó\n * @param {(transferId: string) => void} [options.onTransferId] el folio nuevo, para retomar si algo falla\n * @param {(loaded: number, total: number) => void} [options.onProgress] lo honesto (ver honestLoadedOf), en bytes\n * @param {boolean} [options.holdBeforeFull] true (por omisión): la barra se queda en 99 % hasta que el servidor confirma el\n * último trozo (una vista sin «Procesando»); false: llega a 100 % cuando salió el último byte\n * @param {UploadSendRequest} options.sendRequest\n * @returns {{done: Promise<string>, abort: () => void}}\n */\nexport function startChunkedUpload(options) {\n const controller = new AbortController();\n const done = runChunkedUpload(options, controller.signal);\n return {done, abort: () => controller.abort()};\n}\n\n/**\n * @param {Parameters<typeof startChunkedUpload>[0]} options\n * @param {AbortSignal} signal\n * @returns {Promise<string>} el folio\n */\nasync function runChunkedUpload(options, signal) {\n const {file, fileName, fieldName, metadata = null, url, patchUrl, headers = {}, withCredentials = false, chunkSize, retryDelays,\n transferId = null, onTransferId, onProgress, holdBeforeFull = true, sendRequest} = options;\n const totalBytes = file.size;\n const pieces = chunkPiecesOf(totalBytes, chunkSize);\n const send = (requestUrl, body, method, protocolHeaders, onUploadProgress) => sendRequest(requestUrl, body, {\n method, headers: headerTextsOf({...headers, ...protocolHeaders}), withCredentials, signal, onProgress: onUploadProgress,\n });\n\n // lo que la barra ya enseñó: nunca regresa\n let shownBytes = 0;\n let confirmedBytes = 0;\n const showProgress = (sendingBytes) => {\n // cancelada, ya no hay avance que contar: la tarjeta se queda en «cancelada»\n if (signal.aborted) {\n return;\n }\n const loadedBytes = holdBeforeFull\n ? honestLoadedOf({confirmedBytes, sendingBytes, totalBytes})\n : Math.min(confirmedBytes + sendingBytes, totalBytes);\n shownBytes = Math.max(shownBytes, loadedBytes);\n // un archivo vacío no tiene avance que contar\n if (totalBytes > 0) {\n onProgress?.(shownBytes, totalBytes);\n }\n };\n\n // 1. el folio: uno nuevo, o el del intento anterior y hasta dónde llegó\n let folio = transferId;\n if (folio) {\n const offsetResponse = await sendWithRetries(() => send(`${patchUrl}${encodeURIComponent(folio)}`, null, 'HEAD', {}), retryDelays, signal);\n const serverOffset = Number.parseInt(offsetResponse.headers.get(CHUNK_HEADER.offset) ?? '', 10);\n confirmedBytes = storedBytesOf(pieces, Number.isNaN(serverOffset) ? 0 : serverOffset);\n } else {\n const firstRequest = new FormData();\n // los metadatos de FilePond van en el campo del archivo, como los manda FilePond\n if (metadata && typeof metadata === 'object') {\n firstRequest.append(fieldName, JSON.stringify(metadata));\n }\n const transferResponse = await sendWithRetries(() => send(url, firstRequest, 'POST', {[CHUNK_HEADER.length]: totalBytes}), retryDelays, signal);\n folio = (await transferResponse.text()).trim();\n if (!folio) {\n throw new UploadFailure(UPLOAD_FAILURE.noTransferId, transferResponse);\n }\n onTransferId?.(folio);\n }\n showProgress(0);\n\n // 2. los trozos que faltan, uno tras otro: el siguiente sale cuando el servidor confirmó el anterior\n for (const piece of pieces) {\n const isAlreadyStored = piece.end > piece.offset && piece.end <= confirmedBytes;\n if (isAlreadyStored) {\n continue;\n }\n const pieceHeaders = {\n [CHUNK_HEADER.contentType]: CHUNK_CONTENT_TYPE,\n [CHUNK_HEADER.offset]: piece.offset,\n [CHUNK_HEADER.length]: totalBytes,\n [CHUNK_HEADER.name]: fileName,\n };\n await sendWithRetries(\n () => send(`${patchUrl}${encodeURIComponent(folio)}`, file.slice(piece.offset, piece.end, CHUNK_CONTENT_TYPE), 'PATCH', pieceHeaders,\n (percent, transfer) => showProgress(transfer.loaded)),\n retryDelays,\n signal,\n );\n confirmedBytes = piece.end;\n showProgress(0);\n }\n return folio;\n}\n\n/**\n * El ApiError de una subida que no salió, para las vistas que leen el error de la casa (el cargador mini enseña el motivo del\n * servidor: el primer error por campo o su detail).\n *\n * @param {unknown} failure\n * @param {((body: any, response: Response) => any)|undefined} toProblem\n * @returns {Promise<ApiError>}\n */\nasync function apiErrorOfFailure(failure, toProblem) {\n if (failure?.reason === UPLOAD_FAILURE.noTransferId) {\n const message = translateToolkit('common', 'upload.pond.noTransferId');\n return new ApiError({status: failure.response?.status ?? OFFLINE_STATUS, problem: {title: message, detail: message}});\n }\n if (failure?.reason === UPLOAD_FAILURE.rejected && failure.response) {\n return new ApiError({status: failure.response.status, problem: await problemOfResponse(failure.response, toProblem)});\n }\n if (failure?.reason !== UPLOAD_FAILURE.noResponse) {\n // no es de la red ni del servidor: un error de programación, que se anota para encontrarlo\n log.error('La subida por trozos falló sin respuesta del servidor', failure);\n }\n return new ApiError({status: OFFLINE_STATUS});\n}\n\n/**\n * La subida por trozos para el cargador mini (upload de FileUploadInput y FormFileUploadField) o para una pantalla propia:\n * regresa esa función upload. Todo va por trozos (un archivo chico va en un solo trozo); si solo los grandes deben ir por\n * trozos, la pantalla elige: upload={(file, options) => (file.size > LIMITE ? porTrozos : subidaNormal)(file, options)}.\n *\n * const uploadBackup = createChunkedUpload({url: '/upload', patchUrl: '/upload?patch=', chunkSize: '1 MB'});\n * <FileUploadInput label=\"Respaldo\" value={fileId} onChange={setFileId} upload={uploadBackup} />\n *\n * Al terminar regresa {id: folio} (idOf lo lee por omisión). Con todo enviado la barra llega a 100 % y el renglón dice\n * «Procesando» hasta que el servidor guarda el último trozo. Los errores son ApiError: el renglón enseña el motivo del\n * servidor (con toProblem, también el de un backend que no contesta RFC 9457). Cancelar termina en AbortError.\n *\n * @param {object} options\n * @param {string} options.url a dónde va el POST que pide el folio\n * @param {string} [options.patchUrl] a dónde va cada trozo, con el folio pegado al final; por omisión url + '?patch='\n * @param {number|string} [options.chunkSize] en bytes o con unidades ('1 MB'); por omisión 5 MB, el de FilePond\n * @param {number[]|null} [options.chunkRetryDelays] esperas entre reintentos de un trozo (ms); null o [] = sin reintentos\n * @param {Record<string, string>} [options.headers] van en todas las peticiones, como texto normal (viajan en UTF-8)\n * @param {boolean} [options.withCredentials]\n * @param {(body: any, response: Response) => any} [options.toProblem] el error del backend a la forma RFC 9457\n * @param {UploadSendRequest} [options.sendRequest] cómo viaja cada petición; por omisión uploadWithProgress de la casa\n * @param {string} [options.fieldName] el campo del primer POST; por omisión 'file'\n * @param {object|null} [options.metadata] lo que viaja en ese campo en JSON, como los metadatos de FilePond\n * @returns {(file: File, options?: {signal?: AbortSignal, onUploadProgress?: (transfer: {loaded: number, total: number}) => void}) => Promise<{id: string}>}\n */\nexport function createChunkedUpload({url, patchUrl, chunkSize = DEFAULT_CHUNK_SIZE, chunkRetryDelays = DEFAULT_CHUNK_RETRY_DELAYS, headers = {},\n withCredentials = false, toProblem, sendRequest = uploadWithProgress, fieldName = 'file', metadata = null}) {\n const chunkBytes = parseFileSize(chunkSize);\n const piecesUrl = patchUrl ?? defaultPatchUrlOf(url);\n return (file, {signal, onUploadProgress} = {}) => new Promise((resolve, reject) => {\n const running = startChunkedUpload({\n file, fileName: file.name, fieldName, metadata, url, patchUrl: piecesUrl, withCredentials, chunkSize: chunkBytes,\n headers: {'Accept-Language': toolkitSettings().language, ...headers}, retryDelays: chunkRetryDelays, sendRequest,\n holdBeforeFull: false, onProgress: (loaded, total) => onUploadProgress?.({loaded, total}),\n });\n signal?.addEventListener('abort', () => running.abort(), {once: true});\n if (signal?.aborted) {\n running.abort();\n }\n running.done.then(\n (folio) => resolve({id: folio}),\n async (failure) => reject(failure?.name === 'AbortError' ? failure : await apiErrorOfFailure(failure, toProblem)),\n );\n });\n}\n"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { toolkitSettings } from "../toolkit/toolkitSettings.js";
2
2
  const PERCENT = 100;
3
+ const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 204, 205, 304]);
3
4
  function headersOf(rawHeaders) {
4
5
  const headers = {};
5
6
  for (const line of String(rawHeaders ?? "").trim().split(/[\r\n]+/)) {
@@ -10,14 +11,14 @@ function headersOf(rawHeaders) {
10
11
  }
11
12
  return headers;
12
13
  }
13
- function uploadWithProgress(url, formData, { headers = {}, onProgress, signal, withCredentials = false } = {}) {
14
+ function uploadWithProgress(url, formData, { method = "POST", headers = {}, onProgress, signal, withCredentials = false } = {}) {
14
15
  return new Promise((resolve, reject) => {
15
16
  if (signal?.aborted) {
16
17
  reject(new DOMException("La subida se canceló", "AbortError"));
17
18
  return;
18
19
  }
19
20
  const request = new XMLHttpRequest();
20
- request.open("POST", url);
21
+ request.open(method, url);
21
22
  request.responseType = "blob";
22
23
  request.withCredentials = withCredentials;
23
24
  signal?.addEventListener("abort", () => request.abort(), { once: true });
@@ -26,12 +27,16 @@ function uploadWithProgress(url, formData, { headers = {}, onProgress, signal, w
26
27
  request.setRequestHeader(name, value);
27
28
  }
28
29
  request.upload.onprogress = (event) => {
30
+ if (signal?.aborted) {
31
+ return;
32
+ }
29
33
  if (event.lengthComputable) {
30
34
  onProgress?.(Math.round(event.loaded / event.total * PERCENT), { loaded: event.loaded, total: event.total });
31
35
  }
32
36
  };
33
37
  request.onload = () => {
34
- resolve(new Response(request.response, { status: request.status, headers: headersOf(request.getAllResponseHeaders()) }));
38
+ const body = NULL_BODY_STATUSES.has(request.status) ? null : request.response;
39
+ resolve(new Response(body, { status: request.status, headers: headersOf(request.getAllResponseHeaders()) }));
35
40
  };
36
41
  request.onerror = () => reject(new TypeError("No hubo respuesta del servidor"));
37
42
  request.ontimeout = () => reject(new TypeError("El servidor tardó demasiado en responder"));
@@ -1 +1 @@
1
- {"version":3,"file":"uploadWithProgress.js","sources":["../../src/upload/uploadWithProgress.js"],"sourcesContent":["// Subir un formulario con archivos (multipart) con el AVANCE real de la subida.\n//\n// fetch no dice cuántos bytes del cuerpo ya viajaron; XMLHttpRequest sí (xhr.upload.onprogress). Es UNA petición normal,\n// el mismo POST de siempre, pero la pantalla puede pintar «Subiendo 47 %».\n//\n// El avance llega a 100 % cuando el CUERPO terminó de viajar; lo que sigue (el servidor procesando y la respuesta\n// bajando) ya no se puede medir aquí: la pantalla cambia a «procesando…» sin porcentaje (uploadMachine lo hace sola).\n//\n// Responde como fetch: un Response (res.ok, res.status, res.headers, res.json(), res.blob()), así quien llama trata el\n// error RFC 9457 igual que con cualquier otra petición. La cancelación también es la de fetch: AbortSignal → AbortError.\n//\n// Funciones del navegador, sin React: function (regla de la casa). Se prueba con node --test (uploadWithProgress.test.js)\n// con un XMLHttpRequest de mentira.\nimport {toolkitSettings} from '@/toolkit/toolkitSettings.js';\n\nconst PERCENT = 100;\n\n/**\n * Los headers de XMLHttpRequest ('k: v\\r\\nk2: v2') como objeto para Response.\n *\n * @param {string|null} rawHeaders\n * @returns {Record<string, string>}\n */\nfunction headersOf(rawHeaders) {\n const headers = {};\n for (const line of String(rawHeaders ?? '').trim().split(/[\\r\\n]+/)) {\n const separatorPosition = line.indexOf(': ');\n if (separatorPosition > 0) {\n headers[line.slice(0, separatorPosition)] = line.slice(separatorPosition + 2);\n }\n }\n return headers;\n}\n\n/**\n * @param {string} url\n * @param {FormData} formData los archivos y los campos que pida el backend\n * @param {object} [options]\n * @param {Record<string, string>} [options.headers] p. ej. Authorization; Accept-Language ya va con el idioma activo\n * @param {(percent: number, transfer: {loaded: number, total: number}) => void} [options.onProgress] 0 a 100 de lo que ya\n * viajó del cuerpo, y los bytes (para la velocidad y lo que falta)\n * @param {AbortSignal} [options.signal]\n * @param {boolean} [options.withCredentials] manda las cookies a otro dominio (sesión de Sanctum en otro subdominio)\n * @returns {Promise<Response>}\n */\nexport function uploadWithProgress(url, formData, {headers = {}, onProgress, signal, withCredentials = false} = {}) {\n return new Promise((resolve, reject) => {\n // guard clause: ya venía cancelada\n if (signal?.aborted) {\n reject(new DOMException('La subida se canceló', 'AbortError'));\n return;\n }\n\n const request = new XMLHttpRequest();\n request.open('POST', url);\n request.responseType = 'blob';\n request.withCredentials = withCredentials;\n signal?.addEventListener('abort', () => request.abort(), {once: true});\n\n // OJO: Content-Type NO se pone a mano: el navegador lo arma con el separador del multipart; puesto a mano, el\n // servidor no encuentra los archivos\n const finalHeaders = {'Accept-Language': toolkitSettings().language, ...headers};\n for (const [name, value] of Object.entries(finalHeaders)) {\n request.setRequestHeader(name, value);\n }\n\n request.upload.onprogress = (event) => {\n if (event.lengthComputable) {\n onProgress?.(Math.round((event.loaded / event.total) * PERCENT), {loaded: event.loaded, total: event.total});\n }\n };\n request.onload = () => {\n resolve(new Response(request.response, {status: request.status, headers: headersOf(request.getAllResponseHeaders())}));\n };\n request.onerror = () => reject(new TypeError('No hubo respuesta del servidor'));\n request.ontimeout = () => reject(new TypeError('El servidor tardó demasiado en responder'));\n request.onabort = () => reject(new DOMException('La subida se canceló', 'AbortError'));\n request.send(formData);\n });\n}\n"],"names":[],"mappings":";AAeA,MAAM,UAAU;AAQhB,SAAS,UAAU,YAAY;AAC3B,QAAM,UAAU,CAAA;AAChB,aAAW,QAAQ,OAAO,cAAc,EAAE,EAAE,KAAI,EAAG,MAAM,SAAS,GAAG;AACjE,UAAM,oBAAoB,KAAK,QAAQ,IAAI;AAC3C,QAAI,oBAAoB,GAAG;AACvB,cAAQ,KAAK,MAAM,GAAG,iBAAiB,CAAC,IAAI,KAAK,MAAM,oBAAoB,CAAC;AAAA,IAChF;AAAA,EACJ;AACA,SAAO;AACX;AAaO,SAAS,mBAAmB,KAAK,UAAU,EAAC,UAAU,CAAA,GAAI,YAAY,QAAQ,kBAAkB,MAAK,IAAI,CAAA,GAAI;AAChH,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AAEpC,QAAI,QAAQ,SAAS;AACjB,aAAO,IAAI,aAAa,wBAAwB,YAAY,CAAC;AAC7D;AAAA,IACJ;AAEA,UAAM,UAAU,IAAI,eAAc;AAClC,YAAQ,KAAK,QAAQ,GAAG;AACxB,YAAQ,eAAe;AACvB,YAAQ,kBAAkB;AAC1B,YAAQ,iBAAiB,SAAS,MAAM,QAAQ,SAAS,EAAC,MAAM,KAAI,CAAC;AAIrE,UAAM,eAAe,EAAC,mBAAmB,gBAAe,EAAG,UAAU,GAAG,QAAO;AAC/E,eAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,YAAY,GAAG;AACtD,cAAQ,iBAAiB,MAAM,KAAK;AAAA,IACxC;AAEA,YAAQ,OAAO,aAAa,CAAC,UAAU;AACnC,UAAI,MAAM,kBAAkB;AACxB,qBAAa,KAAK,MAAO,MAAM,SAAS,MAAM,QAAS,OAAO,GAAG,EAAC,QAAQ,MAAM,QAAQ,OAAO,MAAM,MAAK,CAAC;AAAA,MAC/G;AAAA,IACJ;AACA,YAAQ,SAAS,MAAM;AACnB,cAAQ,IAAI,SAAS,QAAQ,UAAU,EAAC,QAAQ,QAAQ,QAAQ,SAAS,UAAU,QAAQ,sBAAqB,CAAE,EAAC,CAAC,CAAC;AAAA,IACzH;AACA,YAAQ,UAAU,MAAM,OAAO,IAAI,UAAU,gCAAgC,CAAC;AAC9E,YAAQ,YAAY,MAAM,OAAO,IAAI,UAAU,0CAA0C,CAAC;AAC1F,YAAQ,UAAU,MAAM,OAAO,IAAI,aAAa,wBAAwB,YAAY,CAAC;AACrF,YAAQ,KAAK,QAAQ;AAAA,EACzB,CAAC;AACL;"}
1
+ {"version":3,"file":"uploadWithProgress.js","sources":["../../src/upload/uploadWithProgress.js"],"sourcesContent":["// Subir un formulario con archivos (multipart) con el AVANCE real de la subida.\n//\n// fetch no dice cuántos bytes del cuerpo ya viajaron; XMLHttpRequest sí (xhr.upload.onprogress). Es UNA petición normal,\n// el mismo POST de siempre, pero la pantalla puede pintar «Subiendo 47 %».\n//\n// El avance llega a 100 % cuando el CUERPO terminó de viajar; lo que sigue (el servidor procesando y la respuesta\n// bajando) ya no se puede medir aquí: la pantalla cambia a «procesando…» sin porcentaje (uploadMachine lo hace sola).\n//\n// Responde como fetch: un Response (res.ok, res.status, res.headers, res.json(), res.blob()), así quien llama trata el\n// error RFC 9457 igual que con cualquier otra petición. La cancelación también es la de fetch: AbortSignal → AbortError.\n//\n// Por omisión es un POST; method cambia el verbo para los protocolos que suben por partes (el PATCH de cada trozo y el HEAD\n// que pregunta hasta dónde llegó, en la subida por trozos de FilePond).\n//\n// Funciones del navegador, sin React: function (regla de la casa). Se prueba con node --test (uploadWithProgress.test.js)\n// con un XMLHttpRequest de mentira.\nimport {toolkitSettings} from '@/toolkit/toolkitSettings.js';\n\nconst PERCENT = 100;\n// las respuestas que por regla no llevan cuerpo: Response truena si se le da uno, aunque venga vacío (un 204 a un PATCH es\n// lo normal en una subida por trozos)\nconst NULL_BODY_STATUSES = new Set([101, 204, 205, 304]);\n\n/**\n * Los headers de XMLHttpRequest ('k: v\\r\\nk2: v2') como objeto para Response.\n *\n * @param {string|null} rawHeaders\n * @returns {Record<string, string>}\n */\nfunction headersOf(rawHeaders) {\n const headers = {};\n for (const line of String(rawHeaders ?? '').trim().split(/[\\r\\n]+/)) {\n const separatorPosition = line.indexOf(': ');\n if (separatorPosition > 0) {\n headers[line.slice(0, separatorPosition)] = line.slice(separatorPosition + 2);\n }\n }\n return headers;\n}\n\n/**\n * @param {string} url\n * @param {FormData|Blob|null} formData los archivos y los campos que pida el backend (un trozo del archivo, o nada en un HEAD)\n * @param {object} [options]\n * @param {string} [options.method] por omisión POST\n * @param {Record<string, string>} [options.headers] p. ej. Authorization; Accept-Language ya va con el idioma activo\n * @param {(percent: number, transfer: {loaded: number, total: number}) => void} [options.onProgress] 0 a 100 de lo que ya\n * viajó del cuerpo, y los bytes (para la velocidad y lo que falta)\n * @param {AbortSignal} [options.signal]\n * @param {boolean} [options.withCredentials] manda las cookies a otro dominio (sesión de Sanctum en otro subdominio)\n * @returns {Promise<Response>}\n */\nexport function uploadWithProgress(url, formData, {method = 'POST', headers = {}, onProgress, signal, withCredentials = false} = {}) {\n return new Promise((resolve, reject) => {\n // guard clause: ya venía cancelada\n if (signal?.aborted) {\n reject(new DOMException('La subida se canceló', 'AbortError'));\n return;\n }\n\n const request = new XMLHttpRequest();\n request.open(method, url);\n request.responseType = 'blob';\n request.withCredentials = withCredentials;\n signal?.addEventListener('abort', () => request.abort(), {once: true});\n\n // OJO: Content-Type NO se pone a mano: el navegador lo arma con el separador del multipart; puesto a mano, el\n // servidor no encuentra los archivos\n const finalHeaders = {'Accept-Language': toolkitSettings().language, ...headers};\n for (const [name, value] of Object.entries(finalHeaders)) {\n request.setRequestHeader(name, value);\n }\n\n request.upload.onprogress = (event) => {\n // al cancelar, el navegador todavía avisa un último avance: ya no se cuenta (la pantalla volvería a «Subiendo»)\n if (signal?.aborted) {\n return;\n }\n if (event.lengthComputable) {\n onProgress?.(Math.round((event.loaded / event.total) * PERCENT), {loaded: event.loaded, total: event.total});\n }\n };\n request.onload = () => {\n const body = NULL_BODY_STATUSES.has(request.status) ? null : request.response;\n resolve(new Response(body, {status: request.status, headers: headersOf(request.getAllResponseHeaders())}));\n };\n request.onerror = () => reject(new TypeError('No hubo respuesta del servidor'));\n request.ontimeout = () => reject(new TypeError('El servidor tardó demasiado en responder'));\n request.onabort = () => reject(new DOMException('La subida se canceló', 'AbortError'));\n request.send(formData);\n });\n}\n"],"names":[],"mappings":";AAkBA,MAAM,UAAU;AAGhB,MAAM,qBAAqB,oBAAI,IAAI,CAAC,KAAK,KAAK,KAAK,GAAG,CAAC;AAQvD,SAAS,UAAU,YAAY;AAC3B,QAAM,UAAU,CAAA;AAChB,aAAW,QAAQ,OAAO,cAAc,EAAE,EAAE,KAAI,EAAG,MAAM,SAAS,GAAG;AACjE,UAAM,oBAAoB,KAAK,QAAQ,IAAI;AAC3C,QAAI,oBAAoB,GAAG;AACvB,cAAQ,KAAK,MAAM,GAAG,iBAAiB,CAAC,IAAI,KAAK,MAAM,oBAAoB,CAAC;AAAA,IAChF;AAAA,EACJ;AACA,SAAO;AACX;AAcO,SAAS,mBAAmB,KAAK,UAAU,EAAC,SAAS,QAAQ,UAAU,CAAA,GAAI,YAAY,QAAQ,kBAAkB,MAAK,IAAI,CAAA,GAAI;AACjI,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AAEpC,QAAI,QAAQ,SAAS;AACjB,aAAO,IAAI,aAAa,wBAAwB,YAAY,CAAC;AAC7D;AAAA,IACJ;AAEA,UAAM,UAAU,IAAI,eAAc;AAClC,YAAQ,KAAK,QAAQ,GAAG;AACxB,YAAQ,eAAe;AACvB,YAAQ,kBAAkB;AAC1B,YAAQ,iBAAiB,SAAS,MAAM,QAAQ,SAAS,EAAC,MAAM,KAAI,CAAC;AAIrE,UAAM,eAAe,EAAC,mBAAmB,gBAAe,EAAG,UAAU,GAAG,QAAO;AAC/E,eAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,YAAY,GAAG;AACtD,cAAQ,iBAAiB,MAAM,KAAK;AAAA,IACxC;AAEA,YAAQ,OAAO,aAAa,CAAC,UAAU;AAEnC,UAAI,QAAQ,SAAS;AACjB;AAAA,MACJ;AACA,UAAI,MAAM,kBAAkB;AACxB,qBAAa,KAAK,MAAO,MAAM,SAAS,MAAM,QAAS,OAAO,GAAG,EAAC,QAAQ,MAAM,QAAQ,OAAO,MAAM,MAAK,CAAC;AAAA,MAC/G;AAAA,IACJ;AACA,YAAQ,SAAS,MAAM;AACnB,YAAM,OAAO,mBAAmB,IAAI,QAAQ,MAAM,IAAI,OAAO,QAAQ;AACrE,cAAQ,IAAI,SAAS,MAAM,EAAC,QAAQ,QAAQ,QAAQ,SAAS,UAAU,QAAQ,sBAAqB,CAAE,EAAC,CAAC,CAAC;AAAA,IAC7G;AACA,YAAQ,UAAU,MAAM,OAAO,IAAI,UAAU,gCAAgC,CAAC;AAC9E,YAAQ,YAAY,MAAM,OAAO,IAAI,UAAU,0CAA0C,CAAC;AAC1F,YAAQ,UAAU,MAAM,OAAO,IAAI,aAAa,wBAAwB,YAAY,CAAC;AACrF,YAAQ,KAAK,QAAQ;AAAA,EACzB,CAAC;AACL;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackcraftmx/calcita",
3
- "version": "0.52.0",
3
+ "version": "0.54.0",
4
4
  "description": "Componentes y utilidades de React con MUI para arrancar proyectos rápido: tabla de servidor, diálogos, avisos, descargas, formularios, formatos y reportes en Excel.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "author": "calcifux",