@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/index.js CHANGED
@@ -326,6 +326,7 @@ const API_BASE_URL = "https://www.renderapi.buildcores.com";
326
326
  // API Endpoints
327
327
  const API_ENDPOINTS = {
328
328
  RENDER_BUILD_EXPERIMENTAL: "/render-build-experimental",
329
+ RENDER_BUILD: "/render-build",
329
330
  AVAILABLE_PARTS: "/available-parts",
330
331
  };
331
332
  // API URL helpers
@@ -348,6 +349,7 @@ const buildHeaders = (config) => {
348
349
  }
349
350
  return headers;
350
351
  };
352
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
351
353
  // API Implementation
352
354
  const renderBuildExperimental = async (request, config) => {
353
355
  const requestWithFormat = {
@@ -371,6 +373,68 @@ const renderBuildExperimental = async (request, config) => {
371
373
  },
372
374
  };
373
375
  };
376
+ // New async endpoints implementation
377
+ const createRenderBuildJob = async (request, config) => {
378
+ const body = {
379
+ parts: request.parts,
380
+ // If provided, forward format; default handled server-side but we keep explicit default
381
+ ...(request.format ? { format: request.format } : {}),
382
+ };
383
+ const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD, config), {
384
+ method: "POST",
385
+ headers: buildHeaders(config),
386
+ body: JSON.stringify(body),
387
+ });
388
+ if (!response.ok) {
389
+ throw new Error(`Create render job failed: ${response.status} ${response.statusText}`);
390
+ }
391
+ const data = (await response.json());
392
+ if (!data?.job_id) {
393
+ throw new Error("Create render job failed: missing job_id in response");
394
+ }
395
+ return data;
396
+ };
397
+ const getRenderBuildStatus = async (jobId, config) => {
398
+ const url = buildApiUrl(`${API_ENDPOINTS.RENDER_BUILD}/${encodeURIComponent(jobId)}`, config);
399
+ const response = await fetch(url, {
400
+ method: "GET",
401
+ headers: buildHeaders(config),
402
+ });
403
+ if (response.status === 404) {
404
+ throw new Error("Render job not found");
405
+ }
406
+ if (!response.ok) {
407
+ throw new Error(`Get render job status failed: ${response.status} ${response.statusText}`);
408
+ }
409
+ return (await response.json());
410
+ };
411
+ const renderBuild = async (request, config, options) => {
412
+ const pollIntervalMs = 1500;
413
+ const timeoutMs = 120000; // 2 minutes default
414
+ const { job_id } = await createRenderBuildJob(request, config);
415
+ const start = Date.now();
416
+ // Poll until completed or error or timeout
417
+ for (;;) {
418
+ const status = await getRenderBuildStatus(job_id, config);
419
+ if (status.status === "completed") {
420
+ const requestedFormat = request.format ?? "video";
421
+ const finalUrl = (requestedFormat === "sprite"
422
+ ? status.sprite_url || status.url || undefined
423
+ : status.video_url || status.url || undefined);
424
+ if (!finalUrl) {
425
+ throw new Error("Render job completed but no URL returned");
426
+ }
427
+ return { videoUrl: finalUrl };
428
+ }
429
+ if (status.status === "error") {
430
+ throw new Error(status.error || "Render job failed");
431
+ }
432
+ if (Date.now() - start > timeoutMs) {
433
+ throw new Error("Timed out waiting for render job to complete");
434
+ }
435
+ await sleep(pollIntervalMs);
436
+ }
437
+ };
374
438
  const renderSpriteExperimental = async (request, config) => {
375
439
  const requestWithFormat = {
376
440
  ...request,
@@ -475,7 +539,7 @@ const arePartsEqual = (parts1, parts2) => {
475
539
  }
476
540
  return true;
477
541
  };
478
- const useBuildRender = (parts, apiConfig, onLoadStart) => {
542
+ const useBuildRender = (parts, apiConfig, onLoadStart, options) => {
479
543
  const [videoSrc, setVideoSrc] = React.useState(null);
480
544
  const [isRenderingBuild, setIsRenderingBuild] = React.useState(false);
481
545
  const [renderError, setRenderError] = React.useState(null);
@@ -485,15 +549,27 @@ const useBuildRender = (parts, apiConfig, onLoadStart) => {
485
549
  setIsRenderingBuild(true);
486
550
  setRenderError(null);
487
551
  onLoadStart?.();
488
- const response = await renderBuildExperimental(currentParts, apiConfig);
489
- const objectUrl = URL.createObjectURL(response.video);
490
- // Clean up previous video URL before setting new one
491
- setVideoSrc((prevSrc) => {
492
- if (prevSrc) {
493
- URL.revokeObjectURL(prevSrc);
494
- }
495
- return objectUrl;
496
- });
552
+ const mode = options?.mode ?? "async";
553
+ if (mode === "experimental") {
554
+ const response = await renderBuildExperimental(currentParts, apiConfig);
555
+ const objectUrl = URL.createObjectURL(response.video);
556
+ setVideoSrc((prevSrc) => {
557
+ if (prevSrc && prevSrc.startsWith("blob:")) {
558
+ URL.revokeObjectURL(prevSrc);
559
+ }
560
+ return objectUrl;
561
+ });
562
+ }
563
+ else {
564
+ const { videoUrl } = await renderBuild(currentParts, apiConfig);
565
+ // Clean up previous object URL (if any) before setting new one
566
+ setVideoSrc((prevSrc) => {
567
+ if (prevSrc && prevSrc.startsWith("blob:")) {
568
+ URL.revokeObjectURL(prevSrc);
569
+ }
570
+ return videoUrl;
571
+ });
572
+ }
497
573
  }
498
574
  catch (error) {
499
575
  setRenderError(error instanceof Error ? error.message : "Failed to render build");
@@ -501,7 +577,7 @@ const useBuildRender = (parts, apiConfig, onLoadStart) => {
501
577
  finally {
502
578
  setIsRenderingBuild(false);
503
579
  }
504
- }, [apiConfig, onLoadStart]);
580
+ }, [apiConfig, onLoadStart, options?.mode]);
505
581
  // Effect to call API when parts content changes (using custom equality check)
506
582
  React.useEffect(() => {
507
583
  const shouldFetch = previousPartsRef.current === null ||
@@ -514,7 +590,7 @@ const useBuildRender = (parts, apiConfig, onLoadStart) => {
514
590
  // Cleanup effect for component unmount
515
591
  React.useEffect(() => {
516
592
  return () => {
517
- if (videoSrc) {
593
+ if (videoSrc && videoSrc.startsWith("blob:")) {
518
594
  URL.revokeObjectURL(videoSrc);
519
595
  }
520
596
  };
@@ -526,7 +602,7 @@ const useBuildRender = (parts, apiConfig, onLoadStart) => {
526
602
  };
527
603
  };
528
604
 
529
- const useSpriteRender = (parts, apiConfig, onLoadStart) => {
605
+ const useSpriteRender = (parts, apiConfig, onLoadStart, options) => {
530
606
  const [spriteSrc, setSpriteSrc] = React.useState(null);
531
607
  const [isRenderingSprite, setIsRenderingSprite] = React.useState(false);
532
608
  const [renderError, setRenderError] = React.useState(null);
@@ -537,21 +613,36 @@ const useSpriteRender = (parts, apiConfig, onLoadStart) => {
537
613
  setIsRenderingSprite(true);
538
614
  setRenderError(null);
539
615
  onLoadStart?.();
540
- const response = await renderSpriteExperimental(currentParts, apiConfig);
541
- const objectUrl = URL.createObjectURL(response.sprite);
542
- // Clean up previous sprite URL before setting new one
543
- setSpriteSrc((prevSrc) => {
544
- if (prevSrc) {
545
- URL.revokeObjectURL(prevSrc);
546
- }
547
- return objectUrl;
548
- });
549
- // Set sprite metadata
550
- setSpriteMetadata({
551
- cols: response.metadata?.cols || 12,
552
- rows: response.metadata?.rows || 6,
553
- totalFrames: response.metadata?.totalFrames || 72,
554
- });
616
+ const mode = options?.mode ?? "async";
617
+ if (mode === "experimental") {
618
+ const response = await renderSpriteExperimental(currentParts, apiConfig);
619
+ const objectUrl = URL.createObjectURL(response.sprite);
620
+ // Clean up previous sprite URL before setting new one
621
+ setSpriteSrc((prevSrc) => {
622
+ if (prevSrc && prevSrc.startsWith("blob:")) {
623
+ URL.revokeObjectURL(prevSrc);
624
+ }
625
+ return objectUrl;
626
+ });
627
+ // Set sprite metadata
628
+ setSpriteMetadata({
629
+ cols: response.metadata?.cols || 12,
630
+ rows: response.metadata?.rows || 6,
631
+ totalFrames: response.metadata?.totalFrames || 72,
632
+ });
633
+ }
634
+ else {
635
+ // Async job-based flow: request sprite format and use returned URL
636
+ const { videoUrl: spriteUrl } = await renderBuild({ ...currentParts, format: "sprite" }, apiConfig);
637
+ setSpriteSrc((prevSrc) => {
638
+ if (prevSrc && prevSrc.startsWith("blob:")) {
639
+ URL.revokeObjectURL(prevSrc);
640
+ }
641
+ return spriteUrl;
642
+ });
643
+ // No metadata from async endpoint; keep defaults
644
+ setSpriteMetadata({ cols: 12, rows: 6, totalFrames: 72 });
645
+ }
555
646
  }
556
647
  catch (error) {
557
648
  setRenderError(error instanceof Error ? error.message : "Failed to render sprite");
@@ -559,7 +650,7 @@ const useSpriteRender = (parts, apiConfig, onLoadStart) => {
559
650
  finally {
560
651
  setIsRenderingSprite(false);
561
652
  }
562
- }, [apiConfig, onLoadStart]);
653
+ }, [apiConfig, onLoadStart, options?.mode]);
563
654
  // Effect to call API when parts content changes (using custom equality check)
564
655
  React.useEffect(() => {
565
656
  const shouldFetch = previousPartsRef.current === null ||
@@ -572,7 +663,7 @@ const useSpriteRender = (parts, apiConfig, onLoadStart) => {
572
663
  // Cleanup effect for component unmount
573
664
  React.useEffect(() => {
574
665
  return () => {
575
- if (spriteSrc) {
666
+ if (spriteSrc && spriteSrc.startsWith("blob:")) {
576
667
  URL.revokeObjectURL(spriteSrc);
577
668
  }
578
669
  };