@liiift-studio/sanity-font-manager 2.3.19 → 2.5.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.
- package/README.md +437 -437
- package/dist/UploadModal-6LIX7XOK.js +6 -0
- package/dist/UploadModal-NME2W53V.mjs +6 -0
- package/dist/chunk-646WCBRR.mjs +7276 -0
- package/dist/chunk-FH4QKHOH.js +7276 -0
- package/dist/index.js +747 -1675
- package/dist/index.mjs +400 -1237
- package/package.json +85 -85
- package/src/components/BatchUploadFonts.jsx +653 -639
- package/src/components/BulkActions.jsx +99 -0
- package/src/components/ExistingDocumentResolver.jsx +152 -0
- package/src/components/FontReviewCard.jsx +415 -0
- package/src/components/FontScriptUploaderComponent.jsx +463 -463
- package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
- package/src/components/KeyValueInput.jsx +95 -95
- package/src/components/KeyValueReferenceInput.jsx +254 -254
- package/src/components/NestedObjectArraySelector.jsx +146 -146
- package/src/components/PriceInput.jsx +26 -26
- package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
- package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
- package/src/components/SetOTF.jsx +87 -87
- package/src/components/SingleUploaderTool.jsx +672 -673
- package/src/components/StatusDisplay.jsx +26 -26
- package/src/components/StyleCountInput.jsx +16 -16
- package/src/components/UpdateScriptsComponent.jsx +76 -76
- package/src/components/UploadButton.jsx +43 -43
- package/src/components/UploadModal.jsx +268 -0
- package/src/components/UploadScriptsComponent.jsx +539 -537
- package/src/components/UploadStep1Settings.jsx +272 -0
- package/src/components/UploadStep2Review.jsx +472 -0
- package/src/components/UploadStep3Execute.jsx +234 -0
- package/src/components/UploadSummary.jsx +196 -0
- package/src/components/VariableInstanceReferencesInput.jsx +190 -190
- package/src/hooks/useNestedObjects.js +92 -92
- package/src/hooks/useSanityClient.js +9 -9
- package/src/index.js +115 -70
- package/src/schema/openTypeField.js +1945 -1945
- package/src/schema/styleCountField.js +12 -12
- package/src/schema/stylesField.js +268 -268
- package/src/schema/stylisticSetField.js +301 -301
- package/src/utils/buildUploadPlan.js +325 -0
- package/src/utils/executeUploadPlan.js +437 -0
- package/src/utils/executionReducer.js +56 -0
- package/src/utils/fontHelpers.js +267 -0
- package/src/utils/generateCssFile.js +207 -205
- package/src/utils/generateFontData.js +98 -145
- package/src/utils/generateFontFile.js +38 -38
- package/src/utils/generateKeywords.js +185 -185
- package/src/utils/generateSubset.js +45 -45
- package/src/utils/getEmptyFontKit.js +101 -99
- package/src/utils/parseFont.js +55 -0
- package/src/utils/parseVariableFontInstances.js +211 -211
- package/src/utils/planReducer.js +517 -0
- package/src/utils/planTypes.js +183 -0
- package/src/utils/processFontFiles.js +529 -477
- package/src/utils/regenerateFontData.js +146 -146
- package/src/utils/resolveExistingFont.js +87 -0
- package/src/utils/sanitizeForSanityId.js +65 -65
- package/src/utils/updateFontPrices.js +94 -94
- package/src/utils/updateTypefaceDocument.js +149 -160
- package/src/utils/uploadFontFiles.js +405 -316
- package/src/utils/utils.js +24 -24
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
// Phase 2 executor — uploads assets, creates/updates font documents, patches the typeface document
|
|
2
|
+
|
|
3
|
+
import { nanoid } from 'nanoid';
|
|
4
|
+
import generateCssFile from './generateCssFile';
|
|
5
|
+
import generateFontData from './generateFontData';
|
|
6
|
+
import { parseVariableFontInstances } from './parseVariableFontInstances';
|
|
7
|
+
import { updateTypefaceDocument } from './updateTypefaceDocument';
|
|
8
|
+
import {
|
|
9
|
+
FONT_STATUS,
|
|
10
|
+
EXECUTION_STATUS,
|
|
11
|
+
RECOMMENDATION,
|
|
12
|
+
CONCURRENCY_LIMIT,
|
|
13
|
+
MAX_RETRIES,
|
|
14
|
+
backoffWithJitter,
|
|
15
|
+
} from './planTypes';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Phase 2: Executes a finalized plan — uploads assets, creates/updates
|
|
19
|
+
* font documents, patches the typeface document.
|
|
20
|
+
*
|
|
21
|
+
* Skips fonts with status 'error'. Caches asset references in progress
|
|
22
|
+
* for idempotent retry on partial failure.
|
|
23
|
+
*
|
|
24
|
+
* @param {object} params
|
|
25
|
+
* @param {object} params.plan - The reviewed and finalized UploadPlan
|
|
26
|
+
* @param {object} params.client - Sanity client
|
|
27
|
+
* @param {string} params.docId - Typeface document _id
|
|
28
|
+
* @param {object} params.stylesObject - Existing typeface styles
|
|
29
|
+
* @param {object} params.preferredStyleRef - Current preferredStyle reference
|
|
30
|
+
* @param {function} params.onProgress - Execution progress callback
|
|
31
|
+
* @returns {Promise<object>} ExecutionResult
|
|
32
|
+
*/
|
|
33
|
+
export async function executeUploadPlan({
|
|
34
|
+
plan,
|
|
35
|
+
client,
|
|
36
|
+
docId,
|
|
37
|
+
stylesObject = {},
|
|
38
|
+
preferredStyleRef = {},
|
|
39
|
+
onProgress,
|
|
40
|
+
}) {
|
|
41
|
+
const result = {
|
|
42
|
+
success: true,
|
|
43
|
+
created: 0,
|
|
44
|
+
updated: 0,
|
|
45
|
+
failed: 0,
|
|
46
|
+
skipped: 0,
|
|
47
|
+
failedFonts: [],
|
|
48
|
+
fontRefs: [],
|
|
49
|
+
variableRefs: [],
|
|
50
|
+
typefacePatchError: null,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Build execution queue — skip fonts with processing errors
|
|
54
|
+
const fontEntries = Object.values(plan.fonts);
|
|
55
|
+
const queue = fontEntries.filter(entry => entry.status !== FONT_STATUS.ERROR);
|
|
56
|
+
const skipped = fontEntries.filter(entry => entry.status === FONT_STATUS.ERROR);
|
|
57
|
+
result.skipped = skipped.length;
|
|
58
|
+
|
|
59
|
+
// Track per-font execution progress
|
|
60
|
+
const progress = {};
|
|
61
|
+
for (const entry of queue) {
|
|
62
|
+
progress[entry.tempId] = {
|
|
63
|
+
status: EXECUTION_STATUS.QUEUED,
|
|
64
|
+
currentFile: null,
|
|
65
|
+
filesComplete: 0,
|
|
66
|
+
filesTotal: entry.files.length,
|
|
67
|
+
assetRefs: {},
|
|
68
|
+
error: null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (onProgress) {
|
|
73
|
+
onProgress({ type: 'execution-start', totalFonts: queue.length, skippedFonts: result.skipped });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Process fonts with concurrency limit
|
|
77
|
+
const chunks = [];
|
|
78
|
+
for (let i = 0; i < queue.length; i += CONCURRENCY_LIMIT) {
|
|
79
|
+
chunks.push(queue.slice(i, i + CONCURRENCY_LIMIT));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let newPreferredStyle = { weight: -100, style: 'Italic', _ref: '' };
|
|
83
|
+
const subfamilies = {};
|
|
84
|
+
const uniqueSubfamilies = new Set();
|
|
85
|
+
|
|
86
|
+
for (const chunk of chunks) {
|
|
87
|
+
const chunkResults = await Promise.allSettled(
|
|
88
|
+
chunk.map(entry => executeSingleFont({
|
|
89
|
+
entry,
|
|
90
|
+
plan,
|
|
91
|
+
client,
|
|
92
|
+
progress,
|
|
93
|
+
onProgress,
|
|
94
|
+
}))
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < chunkResults.length; i++) {
|
|
98
|
+
const chunkResult = chunkResults[i];
|
|
99
|
+
const entry = chunk[i];
|
|
100
|
+
|
|
101
|
+
if (chunkResult.status === 'fulfilled' && chunkResult.value) {
|
|
102
|
+
const fontResult = chunkResult.value;
|
|
103
|
+
|
|
104
|
+
if (fontResult.isNew) result.created++;
|
|
105
|
+
else result.updated++;
|
|
106
|
+
|
|
107
|
+
// Track for typeface patch
|
|
108
|
+
if (entry.variableFont) {
|
|
109
|
+
result.variableRefs.push(fontResult.ref);
|
|
110
|
+
} else {
|
|
111
|
+
result.fontRefs.push(fontResult.ref);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
subfamilies[entry.documentId] = entry.subfamily;
|
|
115
|
+
if (entry.subfamily) uniqueSubfamilies.add(entry.subfamily);
|
|
116
|
+
|
|
117
|
+
// Track preferred style candidate
|
|
118
|
+
if (entry.weight > newPreferredStyle.weight) {
|
|
119
|
+
newPreferredStyle = {
|
|
120
|
+
weight: entry.weight,
|
|
121
|
+
style: entry.style,
|
|
122
|
+
_ref: fontResult.ref._ref,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
result.failed++;
|
|
127
|
+
result.success = false;
|
|
128
|
+
const errorMsg = chunkResult.reason?.message || chunkResult.value?.error || 'Unknown error';
|
|
129
|
+
result.failedFonts.push({
|
|
130
|
+
tempId: entry.tempId,
|
|
131
|
+
title: entry.title,
|
|
132
|
+
error: errorMsg,
|
|
133
|
+
failedAt: progress[entry.tempId]?.status || 'unknown',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Patch the typeface document with new font references
|
|
140
|
+
if (result.fontRefs.length > 0 || result.variableRefs.length > 0) {
|
|
141
|
+
try {
|
|
142
|
+
if (onProgress) {
|
|
143
|
+
onProgress({ type: 'typeface-patching' });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await updateTypefaceDocument(
|
|
147
|
+
docId,
|
|
148
|
+
result.fontRefs,
|
|
149
|
+
result.variableRefs,
|
|
150
|
+
subfamilies,
|
|
151
|
+
[...uniqueSubfamilies],
|
|
152
|
+
stylesObject?.subfamilies || [],
|
|
153
|
+
preferredStyleRef,
|
|
154
|
+
newPreferredStyle,
|
|
155
|
+
stylesObject,
|
|
156
|
+
client,
|
|
157
|
+
(msg) => { if (onProgress) onProgress({ type: 'typeface-status', message: msg }); },
|
|
158
|
+
(err) => { if (err) console.error('Typeface patch error flag set'); },
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (onProgress) {
|
|
162
|
+
onProgress({ type: 'typeface-patched' });
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
result.typefacePatchError = err.message;
|
|
166
|
+
result.success = false;
|
|
167
|
+
console.error('Typeface patch failed:', err.message);
|
|
168
|
+
|
|
169
|
+
if (onProgress) {
|
|
170
|
+
onProgress({ type: 'typeface-error', error: err.message });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (onProgress) {
|
|
176
|
+
onProgress({ type: 'execution-complete', result });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Executes upload for a single font entry.
|
|
184
|
+
* @returns {Promise<{ ref: object, isNew: boolean }>}
|
|
185
|
+
*/
|
|
186
|
+
async function executeSingleFont({ entry, plan, client, progress, onProgress }) {
|
|
187
|
+
const fontProgress = progress[entry.tempId];
|
|
188
|
+
fontProgress.status = EXECUTION_STATUS.UPLOADING_ASSETS;
|
|
189
|
+
|
|
190
|
+
if (onProgress) {
|
|
191
|
+
onProgress({ type: 'font-upload-start', tempId: entry.tempId, fontProgress: { ...fontProgress } });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Determine action based on resolution
|
|
195
|
+
const decision = entry.decisions.existingDocument;
|
|
196
|
+
const userChoice = decision.userChoice;
|
|
197
|
+
const recommendation = decision.recommendation;
|
|
198
|
+
const shouldUpdate = userChoice === 'update' ||
|
|
199
|
+
(!userChoice && (recommendation === RECOMMENDATION.USE_EXACT || recommendation === RECOMMENDATION.USE_CANDIDATE));
|
|
200
|
+
const existingDoc = shouldUpdate
|
|
201
|
+
? (decision.selectedCandidate || decision.exact || decision.candidates[0])
|
|
202
|
+
: null;
|
|
203
|
+
|
|
204
|
+
// Upload font files
|
|
205
|
+
const fileInput = {};
|
|
206
|
+
for (let j = 0; j < entry.files.length; j++) {
|
|
207
|
+
const file = entry.files[j];
|
|
208
|
+
const fileType = determineFileType(file);
|
|
209
|
+
if (!fileType) continue;
|
|
210
|
+
|
|
211
|
+
// Skip if already uploaded (idempotent retry)
|
|
212
|
+
if (fontProgress.assetRefs[fileType]) {
|
|
213
|
+
fileInput[fileType] = {
|
|
214
|
+
_type: 'file',
|
|
215
|
+
asset: { _type: 'reference', _ref: fontProgress.assetRefs[fileType] },
|
|
216
|
+
};
|
|
217
|
+
fontProgress.filesComplete++;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fontProgress.currentFile = fileType;
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const assetFilename = plan.settings.preserveFileNames && entry.originalFilename
|
|
225
|
+
? `${entry.originalFilename}.${fileType}`
|
|
226
|
+
: `${entry.documentId}.${fileType}`;
|
|
227
|
+
|
|
228
|
+
const baseAsset = await uploadWithRetry(
|
|
229
|
+
() => client.assets.upload('file', file, { filename: assetFilename }),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// Override Sanity SHA1-dedup originalFilename if needed
|
|
233
|
+
if (plan.settings.preserveFileNames && baseAsset.originalFilename !== assetFilename) {
|
|
234
|
+
try {
|
|
235
|
+
await client.patch(baseAsset._id).set({ originalFilename: assetFilename }).commit();
|
|
236
|
+
} catch (renameErr) {
|
|
237
|
+
console.warn('Could not rename asset:', renameErr.message);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
fileInput[fileType] = {
|
|
242
|
+
_type: 'file',
|
|
243
|
+
asset: { _type: 'reference', _ref: baseAsset._id },
|
|
244
|
+
};
|
|
245
|
+
fontProgress.assetRefs[fileType] = baseAsset._id;
|
|
246
|
+
fontProgress.filesComplete++;
|
|
247
|
+
|
|
248
|
+
if (onProgress) {
|
|
249
|
+
onProgress({ type: 'file-uploaded', tempId: entry.tempId, fileType, fontProgress: { ...fontProgress } });
|
|
250
|
+
}
|
|
251
|
+
} catch (err) {
|
|
252
|
+
fontProgress.status = EXECUTION_STATUS.ERROR;
|
|
253
|
+
fontProgress.error = err.message;
|
|
254
|
+
throw new Error(`Asset upload failed for ${fileType}: ${err.message}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Generate CSS from WOFF2 if available
|
|
259
|
+
if (fileInput.woff2 || fileInput.woff) {
|
|
260
|
+
fontProgress.status = EXECUTION_STATUS.GENERATING_CSS;
|
|
261
|
+
try {
|
|
262
|
+
const woff2File = entry.files.find(f => f.name.endsWith('.woff2') || f.name.endsWith('.woff'));
|
|
263
|
+
if (woff2File) {
|
|
264
|
+
const updatedFileInput = await generateCssFile({
|
|
265
|
+
woff2File,
|
|
266
|
+
fileInput,
|
|
267
|
+
fileName: entry.documentId,
|
|
268
|
+
fontName: entry.title,
|
|
269
|
+
variableFont: entry.variableFont,
|
|
270
|
+
weight: entry.weight,
|
|
271
|
+
style: entry.style,
|
|
272
|
+
client,
|
|
273
|
+
});
|
|
274
|
+
Object.assign(fileInput, updatedFileInput);
|
|
275
|
+
|
|
276
|
+
if (onProgress) {
|
|
277
|
+
onProgress({ type: 'css-generated', tempId: entry.tempId, fontProgress: { ...fontProgress } });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} catch (err) {
|
|
281
|
+
console.warn('CSS generation failed for', entry.title, '— document created without CSS:', err.message);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Generate font metadata
|
|
286
|
+
if (fileInput.ttf || fileInput.otf) {
|
|
287
|
+
fontProgress.status = EXECUTION_STATUS.GENERATING_METADATA;
|
|
288
|
+
try {
|
|
289
|
+
const ttfAssetRef = fileInput.ttf?.asset?._ref || fileInput.otf?.asset?._ref;
|
|
290
|
+
if (ttfAssetRef) {
|
|
291
|
+
const metadata = await generateFontData({
|
|
292
|
+
fileInput,
|
|
293
|
+
fontKit: null, // Will re-parse from URL
|
|
294
|
+
fontId: entry.documentId,
|
|
295
|
+
client,
|
|
296
|
+
commit: false, // Don't patch yet — we'll include in the document creation
|
|
297
|
+
});
|
|
298
|
+
Object.assign(entry, {
|
|
299
|
+
metaData: metadata.metaData,
|
|
300
|
+
metrics: metadata.metrics,
|
|
301
|
+
variableAxes: metadata.variableAxes,
|
|
302
|
+
variableInstances: metadata.variableInstances,
|
|
303
|
+
opentypeFeatures: metadata.opentypeFeatures,
|
|
304
|
+
characterSet: metadata.characterSet,
|
|
305
|
+
glyphCount: metadata.glyphCount,
|
|
306
|
+
variableFont: metadata.variableFont,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (onProgress) {
|
|
310
|
+
onProgress({ type: 'metadata-generated', tempId: entry.tempId, fontProgress: { ...fontProgress } });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} catch (err) {
|
|
314
|
+
console.warn('Metadata generation failed for', entry.title, ':', err.message);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Create or update font document
|
|
319
|
+
fontProgress.status = EXECUTION_STATUS.CREATING_DOCUMENT;
|
|
320
|
+
|
|
321
|
+
const fontDocId = shouldUpdate && existingDoc ? existingDoc._id : entry.documentId;
|
|
322
|
+
const isNew = !shouldUpdate;
|
|
323
|
+
|
|
324
|
+
const fontDoc = {
|
|
325
|
+
_id: fontDocId,
|
|
326
|
+
_type: 'font',
|
|
327
|
+
_key: nanoid(),
|
|
328
|
+
title: entry.title,
|
|
329
|
+
slug: { _type: 'slug', current: fontDocId },
|
|
330
|
+
typefaceName: plan.fonts[entry.tempId]?.decisions?.title?.original
|
|
331
|
+
? entry.title.split(' ').slice(0, -1).join(' ') || entry.title
|
|
332
|
+
: entry.title,
|
|
333
|
+
style: entry.style,
|
|
334
|
+
variableFont: entry.variableFont,
|
|
335
|
+
weightName: entry.weightName,
|
|
336
|
+
subfamily: entry.subfamily,
|
|
337
|
+
weight: entry.weight,
|
|
338
|
+
price: plan.settings.price,
|
|
339
|
+
sell: plan.settings.price > 0,
|
|
340
|
+
normalWeight: true,
|
|
341
|
+
fileInput,
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// Set typefaceName from the typeface title, not derived from font title
|
|
345
|
+
fontDoc.typefaceName = Object.values(plan.fonts)[0]?.decisions?.title?.original
|
|
346
|
+
? plan.settings.typefaceTitle || fontDoc.typefaceName
|
|
347
|
+
: fontDoc.typefaceName;
|
|
348
|
+
|
|
349
|
+
// Add metadata fields if available
|
|
350
|
+
if (entry.metaData) fontDoc.metaData = entry.metaData;
|
|
351
|
+
if (entry.metrics) fontDoc.metrics = entry.metrics;
|
|
352
|
+
if (entry.variableAxes) fontDoc.variableAxes = entry.variableAxes;
|
|
353
|
+
if (entry.variableInstances) fontDoc.variableInstances = entry.variableInstances;
|
|
354
|
+
if (entry.opentypeFeatures) fontDoc.opentypeFeatures = entry.opentypeFeatures;
|
|
355
|
+
if (entry.characterSet) fontDoc.characterSet = entry.characterSet;
|
|
356
|
+
if (entry.glyphCount) fontDoc.glyphCount = entry.glyphCount;
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
if (shouldUpdate && existingDoc) {
|
|
360
|
+
// Merge with existing data
|
|
361
|
+
if (existingDoc.fileInput) {
|
|
362
|
+
Object.keys(existingDoc.fileInput).forEach(key => {
|
|
363
|
+
if (!fontDoc.fileInput[key]) fontDoc.fileInput[key] = existingDoc.fileInput[key];
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
if (!fontDoc.metaData && existingDoc.metaData) fontDoc.metaData = existingDoc.metaData;
|
|
367
|
+
if (!fontDoc.metrics && existingDoc.metrics) fontDoc.metrics = existingDoc.metrics;
|
|
368
|
+
if (existingDoc.scriptFileInput) fontDoc.scriptFileInput = existingDoc.scriptFileInput;
|
|
369
|
+
if (existingDoc.variableInstanceReferences) {
|
|
370
|
+
fontDoc.variableInstanceReferences = existingDoc.variableInstanceReferences;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
await client.patch(fontDocId).set(fontDoc).commit();
|
|
374
|
+
console.log('Updated existing font:', fontDocId, entry.title);
|
|
375
|
+
} else {
|
|
376
|
+
await client.createOrReplace(fontDoc);
|
|
377
|
+
console.log('Created new font:', fontDocId, entry.title);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
fontProgress.status = EXECUTION_STATUS.COMPLETE;
|
|
381
|
+
|
|
382
|
+
if (onProgress) {
|
|
383
|
+
onProgress({ type: 'document-created', tempId: entry.tempId, isNew, fontProgress: { ...fontProgress } });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
ref: {
|
|
388
|
+
_key: nanoid(),
|
|
389
|
+
_type: 'reference',
|
|
390
|
+
_ref: fontDocId,
|
|
391
|
+
_weak: true,
|
|
392
|
+
},
|
|
393
|
+
isNew,
|
|
394
|
+
};
|
|
395
|
+
} catch (err) {
|
|
396
|
+
fontProgress.status = EXECUTION_STATUS.ERROR;
|
|
397
|
+
fontProgress.error = err.message;
|
|
398
|
+
throw new Error(`Document creation failed: ${err.message}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Determines the file type from a file's extension.
|
|
404
|
+
* @param {File} file
|
|
405
|
+
* @returns {string}
|
|
406
|
+
*/
|
|
407
|
+
function determineFileType(file) {
|
|
408
|
+
if (file.name.endsWith('.ttf')) return 'ttf';
|
|
409
|
+
if (file.name.endsWith('.otf')) return 'otf';
|
|
410
|
+
if (file.name.endsWith('.woff')) return 'woff';
|
|
411
|
+
if (file.name.endsWith('.woff2')) return 'woff2';
|
|
412
|
+
if (file.name.endsWith('.eot')) return 'eot';
|
|
413
|
+
if (file.name.endsWith('.svg')) return 'svg';
|
|
414
|
+
return '';
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Uploads with exponential backoff + jitter on 429 responses.
|
|
419
|
+
* @param {function} uploadFn - Async function to call
|
|
420
|
+
* @returns {Promise<object>}
|
|
421
|
+
*/
|
|
422
|
+
async function uploadWithRetry(uploadFn) {
|
|
423
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
424
|
+
try {
|
|
425
|
+
return await uploadFn();
|
|
426
|
+
} catch (err) {
|
|
427
|
+
const is429 = err.statusCode === 429 || err.status === 429;
|
|
428
|
+
if (is429 && attempt < MAX_RETRIES) {
|
|
429
|
+
const delay = backoffWithJitter(attempt);
|
|
430
|
+
console.warn(`Rate limited (429), retrying in ${delay}ms (attempt ${attempt + 1}/${MAX_RETRIES})`);
|
|
431
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
432
|
+
} else {
|
|
433
|
+
throw err;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Execution reducer — manages ExecutionState separately from the plan to prevent progress ticks from re-rendering the review UI
|
|
2
|
+
|
|
3
|
+
import { EXECUTION_STATUS } from './planTypes';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates the initial execution state.
|
|
7
|
+
* @returns {object}
|
|
8
|
+
*/
|
|
9
|
+
export function createInitialExecutionState() {
|
|
10
|
+
return {
|
|
11
|
+
status: 'idle',
|
|
12
|
+
progress: {},
|
|
13
|
+
error: null,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Execution reducer for useReducer. Manages per-font upload progress.
|
|
19
|
+
* Mounted in UploadStep3Execute — isolated from the plan/review UI.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} state - Current ExecutionState
|
|
22
|
+
* @param {object} action - Dispatched action
|
|
23
|
+
* @returns {object} New ExecutionState
|
|
24
|
+
*/
|
|
25
|
+
export function executionReducer(state, action) {
|
|
26
|
+
switch (action.type) {
|
|
27
|
+
case 'SET_EXECUTION_STATUS': {
|
|
28
|
+
return { ...state, status: action.status };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
case 'SET_FONT_EXECUTION_PROGRESS': {
|
|
32
|
+
const { tempId, progress } = action;
|
|
33
|
+
return {
|
|
34
|
+
...state,
|
|
35
|
+
progress: {
|
|
36
|
+
...state.progress,
|
|
37
|
+
[tempId]: {
|
|
38
|
+
...(state.progress[tempId] || {}),
|
|
39
|
+
...progress,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
case 'SET_EXECUTION_ERROR': {
|
|
46
|
+
return {
|
|
47
|
+
...state,
|
|
48
|
+
status: 'error',
|
|
49
|
+
error: action.error,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
default:
|
|
54
|
+
return state;
|
|
55
|
+
}
|
|
56
|
+
}
|