@buildcores/render-client 1.0.7 → 1.0.9
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/dist/api.d.ts +25 -5
- package/dist/hooks/useBuildRender.d.ts +9 -1
- package/dist/hooks/useSpriteRender.d.ts +9 -1
- package/dist/index.d.ts +23 -10
- package/dist/index.esm.js +121 -30
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +121 -30
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/SpriteRender.d.ts +0 -2
package/dist/api.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { RenderBuildRequest, AvailablePartsResponse } from "./types";
|
|
1
|
+
import { RenderBuildRequest, AvailablePartsResponse, ApiConfig } from "./types";
|
|
2
2
|
declare const API_BASE_URL = "https://www.renderapi.buildcores.com";
|
|
3
3
|
export declare const API_ENDPOINTS: {
|
|
4
4
|
readonly RENDER_BUILD_EXPERIMENTAL: "/render-build-experimental";
|
|
5
|
+
readonly RENDER_BUILD: "/render-build";
|
|
5
6
|
readonly AVAILABLE_PARTS: "/available-parts";
|
|
6
7
|
};
|
|
7
8
|
export interface RenderBuildResponse {
|
|
@@ -18,6 +19,23 @@ export interface RenderBuildResponse {
|
|
|
18
19
|
format?: string;
|
|
19
20
|
};
|
|
20
21
|
}
|
|
22
|
+
export interface RenderJobCreateResponse {
|
|
23
|
+
job_id: string;
|
|
24
|
+
status: "queued" | "processing" | "completed" | "error";
|
|
25
|
+
}
|
|
26
|
+
export interface RenderJobStatusResponse {
|
|
27
|
+
job_id: string;
|
|
28
|
+
status: "queued" | "processing" | "completed" | "error";
|
|
29
|
+
url?: string | null;
|
|
30
|
+
video_url?: string | null;
|
|
31
|
+
sprite_url?: string | null;
|
|
32
|
+
error?: string | null;
|
|
33
|
+
end_time?: string | null;
|
|
34
|
+
}
|
|
35
|
+
export interface RenderBuildAsyncResponse {
|
|
36
|
+
/** Final URL to the rendered MP4 (or sprite) asset */
|
|
37
|
+
videoUrl: string;
|
|
38
|
+
}
|
|
21
39
|
export interface RenderSpriteResponse {
|
|
22
40
|
/**
|
|
23
41
|
* The rendered sprite sheet as a Blob (when format is "sprite")
|
|
@@ -49,13 +67,15 @@ export interface RenderAPIService {
|
|
|
49
67
|
*/
|
|
50
68
|
getAvailableParts(config: ApiConfig): Promise<AvailablePartsResponse>;
|
|
51
69
|
}
|
|
52
|
-
export interface ApiConfig {
|
|
53
|
-
environment?: "staging" | "prod";
|
|
54
|
-
authToken?: string;
|
|
55
|
-
}
|
|
56
70
|
export declare const buildApiUrl: (endpoint: string, config: ApiConfig) => string;
|
|
57
71
|
export declare const buildHeaders: (config: ApiConfig) => Record<string, string>;
|
|
58
72
|
export declare const renderBuildExperimental: (request: RenderBuildRequest, config: ApiConfig) => Promise<RenderBuildResponse>;
|
|
73
|
+
export declare const createRenderBuildJob: (request: RenderBuildRequest, config: ApiConfig) => Promise<RenderJobCreateResponse>;
|
|
74
|
+
export declare const getRenderBuildStatus: (jobId: string, config: ApiConfig) => Promise<RenderJobStatusResponse>;
|
|
75
|
+
export declare const renderBuild: (request: RenderBuildRequest, config: ApiConfig, options?: {
|
|
76
|
+
pollIntervalMs?: number;
|
|
77
|
+
timeoutMs?: number;
|
|
78
|
+
}) => Promise<RenderBuildAsyncResponse>;
|
|
59
79
|
export declare const renderSpriteExperimental: (request: RenderBuildRequest, config: ApiConfig) => Promise<RenderSpriteResponse>;
|
|
60
80
|
export declare const getAvailableParts: (config: ApiConfig) => Promise<AvailablePartsResponse>;
|
|
61
81
|
export { API_BASE_URL };
|
|
@@ -9,4 +9,12 @@ export interface UseBuildRenderReturn {
|
|
|
9
9
|
isRenderingBuild: boolean;
|
|
10
10
|
renderError: string | null;
|
|
11
11
|
}
|
|
12
|
-
export
|
|
12
|
+
export interface UseBuildRenderOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Choose which backend flow to use
|
|
15
|
+
* - 'async' (default): uses /render-build and polls /render-build/{jobId}
|
|
16
|
+
* - 'experimental': uses /render-build-experimental and returns Blob
|
|
17
|
+
*/
|
|
18
|
+
mode?: "async" | "experimental";
|
|
19
|
+
}
|
|
20
|
+
export declare const useBuildRender: (parts: RenderBuildRequest, apiConfig: ApiConfig, onLoadStart?: () => void, options?: UseBuildRenderOptions) => UseBuildRenderReturn;
|
|
@@ -9,4 +9,12 @@ export interface UseSpriteRenderReturn {
|
|
|
9
9
|
totalFrames: number;
|
|
10
10
|
} | null;
|
|
11
11
|
}
|
|
12
|
-
export
|
|
12
|
+
export interface UseSpriteRenderOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Choose which backend flow to use
|
|
15
|
+
* - 'async' (default): uses /render-build and polls /render-build/{jobId} with format 'sprite'
|
|
16
|
+
* - 'experimental': uses /render-build-experimental and returns Blob
|
|
17
|
+
*/
|
|
18
|
+
mode?: "async" | "experimental";
|
|
19
|
+
}
|
|
20
|
+
export declare const useSpriteRender: (parts: RenderBuildRequest, apiConfig: ApiConfig, onLoadStart?: () => void, options?: UseSpriteRenderOptions) => UseSpriteRenderReturn;
|
package/dist/index.d.ts
CHANGED
|
@@ -79,7 +79,7 @@ interface BuildRenderVideoProps {
|
|
|
79
79
|
* />
|
|
80
80
|
* ```
|
|
81
81
|
*/
|
|
82
|
-
apiConfig: ApiConfig
|
|
82
|
+
apiConfig: ApiConfig;
|
|
83
83
|
/**
|
|
84
84
|
* Optional mouse sensitivity for dragging (default: 0.005).
|
|
85
85
|
*
|
|
@@ -188,7 +188,7 @@ interface BuildRenderProps {
|
|
|
188
188
|
* />
|
|
189
189
|
* ```
|
|
190
190
|
*/
|
|
191
|
-
apiConfig: ApiConfig
|
|
191
|
+
apiConfig: ApiConfig;
|
|
192
192
|
/**
|
|
193
193
|
* Optional mouse sensitivity for dragging (default: 0.05).
|
|
194
194
|
*
|
|
@@ -211,7 +211,7 @@ interface BuildRenderProps {
|
|
|
211
211
|
/**
|
|
212
212
|
* API configuration for environment and authentication
|
|
213
213
|
*/
|
|
214
|
-
interface ApiConfig
|
|
214
|
+
interface ApiConfig {
|
|
215
215
|
/**
|
|
216
216
|
* Environment to use for API requests
|
|
217
217
|
* - 'staging': Development/testing environment
|
|
@@ -455,7 +455,15 @@ interface UseBuildRenderReturn {
|
|
|
455
455
|
isRenderingBuild: boolean;
|
|
456
456
|
renderError: string | null;
|
|
457
457
|
}
|
|
458
|
-
|
|
458
|
+
interface UseBuildRenderOptions {
|
|
459
|
+
/**
|
|
460
|
+
* Choose which backend flow to use
|
|
461
|
+
* - 'async' (default): uses /render-build and polls /render-build/{jobId}
|
|
462
|
+
* - 'experimental': uses /render-build-experimental and returns Blob
|
|
463
|
+
*/
|
|
464
|
+
mode?: "async" | "experimental";
|
|
465
|
+
}
|
|
466
|
+
declare const useBuildRender: (parts: RenderBuildRequest, apiConfig: ApiConfig, onLoadStart?: () => void, options?: UseBuildRenderOptions) => UseBuildRenderReturn;
|
|
459
467
|
|
|
460
468
|
interface UseSpriteRenderReturn {
|
|
461
469
|
spriteSrc: string | null;
|
|
@@ -467,7 +475,15 @@ interface UseSpriteRenderReturn {
|
|
|
467
475
|
totalFrames: number;
|
|
468
476
|
} | null;
|
|
469
477
|
}
|
|
470
|
-
|
|
478
|
+
interface UseSpriteRenderOptions {
|
|
479
|
+
/**
|
|
480
|
+
* Choose which backend flow to use
|
|
481
|
+
* - 'async' (default): uses /render-build and polls /render-build/{jobId} with format 'sprite'
|
|
482
|
+
* - 'experimental': uses /render-build-experimental and returns Blob
|
|
483
|
+
*/
|
|
484
|
+
mode?: "async" | "experimental";
|
|
485
|
+
}
|
|
486
|
+
declare const useSpriteRender: (parts: RenderBuildRequest, apiConfig: ApiConfig, onLoadStart?: () => void, options?: UseSpriteRenderOptions) => UseSpriteRenderReturn;
|
|
471
487
|
|
|
472
488
|
interface DragIconProps {
|
|
473
489
|
width?: number;
|
|
@@ -494,6 +510,7 @@ declare const InstructionTooltip: React__default.FC<InstructionTooltipProps>;
|
|
|
494
510
|
declare const API_BASE_URL = "https://www.renderapi.buildcores.com";
|
|
495
511
|
declare const API_ENDPOINTS: {
|
|
496
512
|
readonly RENDER_BUILD_EXPERIMENTAL: "/render-build-experimental";
|
|
513
|
+
readonly RENDER_BUILD: "/render-build";
|
|
497
514
|
readonly AVAILABLE_PARTS: "/available-parts";
|
|
498
515
|
};
|
|
499
516
|
interface RenderBuildResponse {
|
|
@@ -541,10 +558,6 @@ interface RenderAPIService {
|
|
|
541
558
|
*/
|
|
542
559
|
getAvailableParts(config: ApiConfig): Promise<AvailablePartsResponse>;
|
|
543
560
|
}
|
|
544
|
-
interface ApiConfig {
|
|
545
|
-
environment?: "staging" | "prod";
|
|
546
|
-
authToken?: string;
|
|
547
|
-
}
|
|
548
561
|
declare const buildApiUrl: (endpoint: string, config: ApiConfig) => string;
|
|
549
562
|
declare const buildHeaders: (config: ApiConfig) => Record<string, string>;
|
|
550
563
|
declare const renderBuildExperimental: (request: RenderBuildRequest, config: ApiConfig) => Promise<RenderBuildResponse>;
|
|
@@ -552,4 +565,4 @@ declare const renderSpriteExperimental: (request: RenderBuildRequest, config: Ap
|
|
|
552
565
|
declare const getAvailableParts: (config: ApiConfig) => Promise<AvailablePartsResponse>;
|
|
553
566
|
|
|
554
567
|
export { API_BASE_URL, API_ENDPOINTS, BuildRender, BuildRenderVideo, DragIcon, InstructionTooltip, LoadingErrorOverlay, PartCategory, arePartsEqual, buildApiUrl, buildHeaders, calculateCircularFrame, calculateCircularTime, getAvailableParts, renderBuildExperimental, renderSpriteExperimental, useBouncePatternProgress, useBuildRender, useSpriteRender, useSpriteScrubbing, useVideoScrubbing };
|
|
555
|
-
export type { ApiConfig
|
|
568
|
+
export type { ApiConfig, AvailablePartsResponse, BuildRenderProps, BuildRenderVideoProps, PartDetails, RenderAPIService, RenderBuildRequest, RenderBuildResponse, RenderSpriteResponse, UseBuildRenderOptions, UseBuildRenderReturn, UseSpriteRenderOptions, UseSpriteRenderReturn };
|
package/dist/index.esm.js
CHANGED
|
@@ -324,6 +324,7 @@ const API_BASE_URL = "https://www.renderapi.buildcores.com";
|
|
|
324
324
|
// API Endpoints
|
|
325
325
|
const API_ENDPOINTS = {
|
|
326
326
|
RENDER_BUILD_EXPERIMENTAL: "/render-build-experimental",
|
|
327
|
+
RENDER_BUILD: "/render-build",
|
|
327
328
|
AVAILABLE_PARTS: "/available-parts",
|
|
328
329
|
};
|
|
329
330
|
// API URL helpers
|
|
@@ -346,6 +347,7 @@ const buildHeaders = (config) => {
|
|
|
346
347
|
}
|
|
347
348
|
return headers;
|
|
348
349
|
};
|
|
350
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
349
351
|
// API Implementation
|
|
350
352
|
const renderBuildExperimental = async (request, config) => {
|
|
351
353
|
const requestWithFormat = {
|
|
@@ -369,6 +371,68 @@ const renderBuildExperimental = async (request, config) => {
|
|
|
369
371
|
},
|
|
370
372
|
};
|
|
371
373
|
};
|
|
374
|
+
// New async endpoints implementation
|
|
375
|
+
const createRenderBuildJob = async (request, config) => {
|
|
376
|
+
const body = {
|
|
377
|
+
parts: request.parts,
|
|
378
|
+
// If provided, forward format; default handled server-side but we keep explicit default
|
|
379
|
+
...(request.format ? { format: request.format } : {}),
|
|
380
|
+
};
|
|
381
|
+
const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD, config), {
|
|
382
|
+
method: "POST",
|
|
383
|
+
headers: buildHeaders(config),
|
|
384
|
+
body: JSON.stringify(body),
|
|
385
|
+
});
|
|
386
|
+
if (!response.ok) {
|
|
387
|
+
throw new Error(`Create render job failed: ${response.status} ${response.statusText}`);
|
|
388
|
+
}
|
|
389
|
+
const data = (await response.json());
|
|
390
|
+
if (!data?.job_id) {
|
|
391
|
+
throw new Error("Create render job failed: missing job_id in response");
|
|
392
|
+
}
|
|
393
|
+
return data;
|
|
394
|
+
};
|
|
395
|
+
const getRenderBuildStatus = async (jobId, config) => {
|
|
396
|
+
const url = buildApiUrl(`${API_ENDPOINTS.RENDER_BUILD}/${encodeURIComponent(jobId)}`, config);
|
|
397
|
+
const response = await fetch(url, {
|
|
398
|
+
method: "GET",
|
|
399
|
+
headers: buildHeaders(config),
|
|
400
|
+
});
|
|
401
|
+
if (response.status === 404) {
|
|
402
|
+
throw new Error("Render job not found");
|
|
403
|
+
}
|
|
404
|
+
if (!response.ok) {
|
|
405
|
+
throw new Error(`Get render job status failed: ${response.status} ${response.statusText}`);
|
|
406
|
+
}
|
|
407
|
+
return (await response.json());
|
|
408
|
+
};
|
|
409
|
+
const renderBuild = async (request, config, options) => {
|
|
410
|
+
const pollIntervalMs = 1500;
|
|
411
|
+
const timeoutMs = 120000; // 2 minutes default
|
|
412
|
+
const { job_id } = await createRenderBuildJob(request, config);
|
|
413
|
+
const start = Date.now();
|
|
414
|
+
// Poll until completed or error or timeout
|
|
415
|
+
for (;;) {
|
|
416
|
+
const status = await getRenderBuildStatus(job_id, config);
|
|
417
|
+
if (status.status === "completed") {
|
|
418
|
+
const requestedFormat = request.format ?? "video";
|
|
419
|
+
const finalUrl = (requestedFormat === "sprite"
|
|
420
|
+
? status.sprite_url || status.url || undefined
|
|
421
|
+
: status.video_url || status.url || undefined);
|
|
422
|
+
if (!finalUrl) {
|
|
423
|
+
throw new Error("Render job completed but no URL returned");
|
|
424
|
+
}
|
|
425
|
+
return { videoUrl: finalUrl };
|
|
426
|
+
}
|
|
427
|
+
if (status.status === "error") {
|
|
428
|
+
throw new Error(status.error || "Render job failed");
|
|
429
|
+
}
|
|
430
|
+
if (Date.now() - start > timeoutMs) {
|
|
431
|
+
throw new Error("Timed out waiting for render job to complete");
|
|
432
|
+
}
|
|
433
|
+
await sleep(pollIntervalMs);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
372
436
|
const renderSpriteExperimental = async (request, config) => {
|
|
373
437
|
const requestWithFormat = {
|
|
374
438
|
...request,
|
|
@@ -473,7 +537,7 @@ const arePartsEqual = (parts1, parts2) => {
|
|
|
473
537
|
}
|
|
474
538
|
return true;
|
|
475
539
|
};
|
|
476
|
-
const useBuildRender = (parts, apiConfig, onLoadStart) => {
|
|
540
|
+
const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
|
|
477
541
|
const [videoSrc, setVideoSrc] = useState(null);
|
|
478
542
|
const [isRenderingBuild, setIsRenderingBuild] = useState(false);
|
|
479
543
|
const [renderError, setRenderError] = useState(null);
|
|
@@ -483,15 +547,27 @@ const useBuildRender = (parts, apiConfig, onLoadStart) => {
|
|
|
483
547
|
setIsRenderingBuild(true);
|
|
484
548
|
setRenderError(null);
|
|
485
549
|
onLoadStart?.();
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
550
|
+
const mode = options?.mode ?? "async";
|
|
551
|
+
if (mode === "experimental") {
|
|
552
|
+
const response = await renderBuildExperimental(currentParts, apiConfig);
|
|
553
|
+
const objectUrl = URL.createObjectURL(response.video);
|
|
554
|
+
setVideoSrc((prevSrc) => {
|
|
555
|
+
if (prevSrc && prevSrc.startsWith("blob:")) {
|
|
556
|
+
URL.revokeObjectURL(prevSrc);
|
|
557
|
+
}
|
|
558
|
+
return objectUrl;
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
const { videoUrl } = await renderBuild(currentParts, apiConfig);
|
|
563
|
+
// Clean up previous object URL (if any) before setting new one
|
|
564
|
+
setVideoSrc((prevSrc) => {
|
|
565
|
+
if (prevSrc && prevSrc.startsWith("blob:")) {
|
|
566
|
+
URL.revokeObjectURL(prevSrc);
|
|
567
|
+
}
|
|
568
|
+
return videoUrl;
|
|
569
|
+
});
|
|
570
|
+
}
|
|
495
571
|
}
|
|
496
572
|
catch (error) {
|
|
497
573
|
setRenderError(error instanceof Error ? error.message : "Failed to render build");
|
|
@@ -499,7 +575,7 @@ const useBuildRender = (parts, apiConfig, onLoadStart) => {
|
|
|
499
575
|
finally {
|
|
500
576
|
setIsRenderingBuild(false);
|
|
501
577
|
}
|
|
502
|
-
}, [apiConfig, onLoadStart]);
|
|
578
|
+
}, [apiConfig, onLoadStart, options?.mode]);
|
|
503
579
|
// Effect to call API when parts content changes (using custom equality check)
|
|
504
580
|
useEffect(() => {
|
|
505
581
|
const shouldFetch = previousPartsRef.current === null ||
|
|
@@ -512,7 +588,7 @@ const useBuildRender = (parts, apiConfig, onLoadStart) => {
|
|
|
512
588
|
// Cleanup effect for component unmount
|
|
513
589
|
useEffect(() => {
|
|
514
590
|
return () => {
|
|
515
|
-
if (videoSrc) {
|
|
591
|
+
if (videoSrc && videoSrc.startsWith("blob:")) {
|
|
516
592
|
URL.revokeObjectURL(videoSrc);
|
|
517
593
|
}
|
|
518
594
|
};
|
|
@@ -524,7 +600,7 @@ const useBuildRender = (parts, apiConfig, onLoadStart) => {
|
|
|
524
600
|
};
|
|
525
601
|
};
|
|
526
602
|
|
|
527
|
-
const useSpriteRender = (parts, apiConfig, onLoadStart) => {
|
|
603
|
+
const useSpriteRender = (parts, apiConfig, onLoadStart, options) => {
|
|
528
604
|
const [spriteSrc, setSpriteSrc] = useState(null);
|
|
529
605
|
const [isRenderingSprite, setIsRenderingSprite] = useState(false);
|
|
530
606
|
const [renderError, setRenderError] = useState(null);
|
|
@@ -535,21 +611,36 @@ const useSpriteRender = (parts, apiConfig, onLoadStart) => {
|
|
|
535
611
|
setIsRenderingSprite(true);
|
|
536
612
|
setRenderError(null);
|
|
537
613
|
onLoadStart?.();
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
614
|
+
const mode = options?.mode ?? "async";
|
|
615
|
+
if (mode === "experimental") {
|
|
616
|
+
const response = await renderSpriteExperimental(currentParts, apiConfig);
|
|
617
|
+
const objectUrl = URL.createObjectURL(response.sprite);
|
|
618
|
+
// Clean up previous sprite URL before setting new one
|
|
619
|
+
setSpriteSrc((prevSrc) => {
|
|
620
|
+
if (prevSrc && prevSrc.startsWith("blob:")) {
|
|
621
|
+
URL.revokeObjectURL(prevSrc);
|
|
622
|
+
}
|
|
623
|
+
return objectUrl;
|
|
624
|
+
});
|
|
625
|
+
// Set sprite metadata
|
|
626
|
+
setSpriteMetadata({
|
|
627
|
+
cols: response.metadata?.cols || 12,
|
|
628
|
+
rows: response.metadata?.rows || 6,
|
|
629
|
+
totalFrames: response.metadata?.totalFrames || 72,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
// Async job-based flow: request sprite format and use returned URL
|
|
634
|
+
const { videoUrl: spriteUrl } = await renderBuild({ ...currentParts, format: "sprite" }, apiConfig);
|
|
635
|
+
setSpriteSrc((prevSrc) => {
|
|
636
|
+
if (prevSrc && prevSrc.startsWith("blob:")) {
|
|
637
|
+
URL.revokeObjectURL(prevSrc);
|
|
638
|
+
}
|
|
639
|
+
return spriteUrl;
|
|
640
|
+
});
|
|
641
|
+
// No metadata from async endpoint; keep defaults
|
|
642
|
+
setSpriteMetadata({ cols: 12, rows: 6, totalFrames: 72 });
|
|
643
|
+
}
|
|
553
644
|
}
|
|
554
645
|
catch (error) {
|
|
555
646
|
setRenderError(error instanceof Error ? error.message : "Failed to render sprite");
|
|
@@ -557,7 +648,7 @@ const useSpriteRender = (parts, apiConfig, onLoadStart) => {
|
|
|
557
648
|
finally {
|
|
558
649
|
setIsRenderingSprite(false);
|
|
559
650
|
}
|
|
560
|
-
}, [apiConfig, onLoadStart]);
|
|
651
|
+
}, [apiConfig, onLoadStart, options?.mode]);
|
|
561
652
|
// Effect to call API when parts content changes (using custom equality check)
|
|
562
653
|
useEffect(() => {
|
|
563
654
|
const shouldFetch = previousPartsRef.current === null ||
|
|
@@ -570,7 +661,7 @@ const useSpriteRender = (parts, apiConfig, onLoadStart) => {
|
|
|
570
661
|
// Cleanup effect for component unmount
|
|
571
662
|
useEffect(() => {
|
|
572
663
|
return () => {
|
|
573
|
-
if (spriteSrc) {
|
|
664
|
+
if (spriteSrc && spriteSrc.startsWith("blob:")) {
|
|
574
665
|
URL.revokeObjectURL(spriteSrc);
|
|
575
666
|
}
|
|
576
667
|
};
|