@buildcores/render-client 1.6.0 → 1.7.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/dist/index.js CHANGED
@@ -213,17 +213,73 @@ const buildApiUrl = (endpoint, config) => {
213
213
  }
214
214
  return baseUrl;
215
215
  };
216
+ const SESSION_TOKEN_REFRESH_WINDOW_MS = 15000;
217
+ const sessionTokenCache = new WeakMap();
218
+ const resolveAuthMode = (config) => config.authMode ?? (config.getRenderSessionToken ? "session" : "legacy");
219
+ const isSessionAuthMode = (config) => resolveAuthMode(config) === "session";
220
+ const parseExpiresAtMs = (expiresAt) => {
221
+ const parsed = Date.parse(expiresAt);
222
+ if (!Number.isFinite(parsed)) {
223
+ throw new Error("Invalid session token expiry (expiresAt)");
224
+ }
225
+ return parsed;
226
+ };
227
+ const resolveSessionToken = async (config, forceRefresh) => {
228
+ const supplier = config.getRenderSessionToken;
229
+ if (!supplier) {
230
+ throw new Error("authMode=session requires getRenderSessionToken");
231
+ }
232
+ const cached = sessionTokenCache.get(supplier);
233
+ const now = Date.now();
234
+ if (!forceRefresh &&
235
+ cached &&
236
+ cached.expiresAtMs - SESSION_TOKEN_REFRESH_WINDOW_MS > now) {
237
+ return cached.token;
238
+ }
239
+ const session = await supplier();
240
+ if (!session?.token || !session?.expiresAt) {
241
+ throw new Error("getRenderSessionToken must return { token, expiresAt }");
242
+ }
243
+ const expiresAtMs = parseExpiresAtMs(session.expiresAt);
244
+ sessionTokenCache.set(supplier, {
245
+ token: session.token,
246
+ expiresAtMs,
247
+ });
248
+ return session.token;
249
+ };
250
+ const resolveAuthToken = async (config, forceRefresh) => {
251
+ if (isSessionAuthMode(config)) {
252
+ return resolveSessionToken(config, forceRefresh);
253
+ }
254
+ return config.authToken;
255
+ };
216
256
  // Helper to build request headers with auth token
217
- const buildHeaders = (config) => {
257
+ const buildHeaders = (config, authTokenOverride) => {
218
258
  const headers = {
219
259
  "Content-Type": "application/json",
220
260
  accept: "application/json",
221
261
  };
222
- if (config.authToken) {
223
- headers["Authorization"] = `Bearer ${config.authToken}`;
262
+ const authToken = authTokenOverride ?? config.authToken;
263
+ if (authToken) {
264
+ headers["Authorization"] = `Bearer ${authToken}`;
224
265
  }
225
266
  return headers;
226
267
  };
268
+ const fetchWithApiAuth = async (url, init, config) => {
269
+ const firstToken = await resolveAuthToken(config, false);
270
+ const firstResponse = await fetch(url, {
271
+ ...init,
272
+ headers: buildHeaders(config, firstToken),
273
+ });
274
+ if (!isSessionAuthMode(config) || firstResponse.status !== 401) {
275
+ return firstResponse;
276
+ }
277
+ const refreshedToken = await resolveAuthToken(config, true);
278
+ return fetch(url, {
279
+ ...init,
280
+ headers: buildHeaders(config, refreshedToken),
281
+ });
282
+ };
227
283
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
228
284
  // API Implementation
229
285
  const renderBuildExperimental = async (request, config) => {
@@ -235,12 +291,16 @@ const renderBuildExperimental = async (request, config) => {
235
291
  ...(request.height !== undefined ? { height: request.height } : {}),
236
292
  // Include profile if provided
237
293
  ...(request.profile ? { profile: request.profile } : {}),
294
+ ...(request.scene ? { scene: request.scene } : {}),
295
+ ...(request.showBackground !== undefined ? { showBackground: request.showBackground } : {}),
296
+ ...(request.showGrid !== undefined ? { showGrid: request.showGrid } : {}),
297
+ ...(request.winterMode !== undefined ? { winterMode: request.winterMode } : {}),
298
+ ...(request.springMode !== undefined ? { springMode: request.springMode } : {}),
238
299
  };
239
- const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
300
+ const response = await fetchWithApiAuth(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
240
301
  method: "POST",
241
- headers: buildHeaders(config),
242
302
  body: JSON.stringify(requestWithFormat),
243
- });
303
+ }, config);
244
304
  if (!response.ok) {
245
305
  throw new Error(`Render build failed: ${response.status} ${response.statusText}`);
246
306
  }
@@ -264,8 +324,12 @@ const createRenderBuildJob = async (request, config) => {
264
324
  ...(request.height !== undefined ? { height: request.height } : {}),
265
325
  // Include profile if provided
266
326
  ...(request.profile ? { profile: request.profile } : {}),
327
+ ...(request.scene ? { scene: request.scene } : {}),
328
+ ...(request.showBackground !== undefined ? { showBackground: request.showBackground } : {}),
267
329
  // Include composition settings
268
330
  ...(request.showGrid !== undefined ? { showGrid: request.showGrid } : {}),
331
+ ...(request.winterMode !== undefined ? { winterMode: request.winterMode } : {}),
332
+ ...(request.springMode !== undefined ? { springMode: request.springMode } : {}),
269
333
  ...(request.cameraOffsetX !== undefined ? { cameraOffsetX: request.cameraOffsetX } : {}),
270
334
  ...(request.gridSettings ? { gridSettings: request.gridSettings } : {}),
271
335
  // Include frame quality for sprite rendering
@@ -273,11 +337,10 @@ const createRenderBuildJob = async (request, config) => {
273
337
  // Include camera zoom for render-time scaling
274
338
  ...(request.cameraZoom !== undefined ? { cameraZoom: request.cameraZoom } : {}),
275
339
  };
276
- const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD, config), {
340
+ const response = await fetchWithApiAuth(buildApiUrl(API_ENDPOINTS.RENDER_BUILD, config), {
277
341
  method: "POST",
278
- headers: buildHeaders(config),
279
342
  body: JSON.stringify(body),
280
- });
343
+ }, config);
281
344
  if (!response.ok) {
282
345
  throw new Error(`Create render job failed: ${response.status} ${response.statusText}`);
283
346
  }
@@ -289,10 +352,7 @@ const createRenderBuildJob = async (request, config) => {
289
352
  };
290
353
  const getRenderBuildStatus = async (jobId, config) => {
291
354
  const url = buildApiUrl(`${API_ENDPOINTS.RENDER_BUILD}/${encodeURIComponent(jobId)}`, config);
292
- const response = await fetch(url, {
293
- method: "GET",
294
- headers: buildHeaders(config),
295
- });
355
+ const response = await fetchWithApiAuth(url, { method: "GET" }, config);
296
356
  if (response.status === 404) {
297
357
  throw new Error("Render job not found");
298
358
  }
@@ -337,12 +397,16 @@ const renderSpriteExperimental = async (request, config) => {
337
397
  ...(request.height !== undefined ? { height: request.height } : {}),
338
398
  // Include profile if provided
339
399
  ...(request.profile ? { profile: request.profile } : {}),
400
+ ...(request.scene ? { scene: request.scene } : {}),
401
+ ...(request.showBackground !== undefined ? { showBackground: request.showBackground } : {}),
402
+ ...(request.showGrid !== undefined ? { showGrid: request.showGrid } : {}),
403
+ ...(request.winterMode !== undefined ? { winterMode: request.winterMode } : {}),
404
+ ...(request.springMode !== undefined ? { springMode: request.springMode } : {}),
340
405
  };
341
- const response = await fetch(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
406
+ const response = await fetchWithApiAuth(buildApiUrl(API_ENDPOINTS.RENDER_BUILD_EXPERIMENTAL, config), {
342
407
  method: "POST",
343
- headers: buildHeaders(config),
344
408
  body: JSON.stringify(requestWithFormat),
345
- });
409
+ }, config);
346
410
  if (!response.ok) {
347
411
  throw new Error(`Render sprite failed: ${response.status} ${response.statusText}`);
348
412
  }
@@ -368,10 +432,7 @@ const getAvailableParts = async (category, config, options) => {
368
432
  params.set("skip", String(options.skip));
369
433
  const separator = base.includes("?") ? "&" : "?";
370
434
  const url = `${base}${separator}${params.toString()}`;
371
- const response = await fetch(url, {
372
- method: "GET",
373
- headers: buildHeaders(config),
374
- });
435
+ const response = await fetchWithApiAuth(url, { method: "GET" }, config);
375
436
  if (!response.ok) {
376
437
  throw new Error(`Get available parts failed: ${response.status} ${response.statusText}`);
377
438
  }
@@ -404,10 +465,7 @@ const getAvailableParts = async (category, config, options) => {
404
465
  */
405
466
  const getBuildByShareCode = async (shareCode, config) => {
406
467
  const url = buildApiUrl(`${API_ENDPOINTS.BUILD}/${encodeURIComponent(shareCode)}`, config);
407
- const response = await fetch(url, {
408
- method: "GET",
409
- headers: buildHeaders(config),
410
- });
468
+ const response = await fetchWithApiAuth(url, { method: "GET" }, config);
411
469
  if (response.status === 404) {
412
470
  throw new Error("Build not found");
413
471
  }
@@ -437,11 +495,10 @@ const getBuildByShareCode = async (shareCode, config) => {
437
495
  */
438
496
  const getPartsByIds = async (partIds, config) => {
439
497
  const url = buildApiUrl(API_ENDPOINTS.PARTS, config);
440
- const response = await fetch(url, {
498
+ const response = await fetchWithApiAuth(url, {
441
499
  method: "POST",
442
- headers: buildHeaders(config),
443
500
  body: JSON.stringify({ ids: partIds }),
444
- });
501
+ }, config);
445
502
  if (!response.ok) {
446
503
  throw new Error(`Get parts by IDs failed: ${response.status} ${response.statusText}`);
447
504
  }
@@ -464,17 +521,20 @@ const createRenderByShareCodeJob = async (shareCode, config, options) => {
464
521
  ...(options?.width !== undefined ? { width: options.width } : {}),
465
522
  ...(options?.height !== undefined ? { height: options.height } : {}),
466
523
  ...(options?.profile ? { profile: options.profile } : {}),
524
+ ...(options?.scene ? { scene: options.scene } : {}),
525
+ ...(options?.showBackground !== undefined ? { showBackground: options.showBackground } : {}),
467
526
  ...(options?.showGrid !== undefined ? { showGrid: options.showGrid } : {}),
527
+ ...(options?.winterMode !== undefined ? { winterMode: options.winterMode } : {}),
528
+ ...(options?.springMode !== undefined ? { springMode: options.springMode } : {}),
468
529
  ...(options?.cameraOffsetX !== undefined ? { cameraOffsetX: options.cameraOffsetX } : {}),
469
530
  ...(options?.gridSettings ? { gridSettings: options.gridSettings } : {}),
470
531
  ...(options?.frameQuality ? { frameQuality: options.frameQuality } : {}),
471
532
  ...(options?.cameraZoom !== undefined ? { cameraZoom: options.cameraZoom } : {}),
472
533
  };
473
- const response = await fetch(url, {
534
+ const response = await fetchWithApiAuth(url, {
474
535
  method: "POST",
475
- headers: buildHeaders(config),
476
536
  body: JSON.stringify(body),
477
- });
537
+ }, config);
478
538
  if (response.status === 404) {
479
539
  throw new Error("Build not found");
480
540
  }
@@ -683,7 +743,21 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
683
743
  const [spriteMetadata, setSpriteMetadata] = react.useState(null);
684
744
  const previousInputRef = react.useRef(null);
685
745
  // Normalize input to SpriteRenderInput format
686
- const normalizedInput = 'type' in input ? input : { type: 'parts', parts: input };
746
+ const normalizedInput = 'type' in input
747
+ ? input
748
+ : {
749
+ type: 'parts',
750
+ parts: input,
751
+ showGrid: input.showGrid,
752
+ scene: input.scene,
753
+ showBackground: input.showBackground,
754
+ winterMode: input.winterMode,
755
+ springMode: input.springMode,
756
+ cameraOffsetX: input.cameraOffsetX,
757
+ cameraZoom: input.cameraZoom,
758
+ gridSettings: input.gridSettings,
759
+ frameQuality: input.frameQuality
760
+ };
687
761
  const fetchRenderSprite = react.useCallback(async (currentInput) => {
688
762
  try {
689
763
  setIsRenderingSprite(true);
@@ -695,6 +769,10 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
695
769
  format: 'sprite',
696
770
  profile: currentInput.profile,
697
771
  showGrid: currentInput.showGrid,
772
+ scene: currentInput.scene,
773
+ showBackground: currentInput.showBackground,
774
+ winterMode: currentInput.winterMode,
775
+ springMode: currentInput.springMode,
698
776
  cameraOffsetX: currentInput.cameraOffsetX,
699
777
  cameraZoom: currentInput.cameraZoom,
700
778
  gridSettings: currentInput.gridSettings,
@@ -721,6 +799,10 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
721
799
  const response = await renderSpriteExperimental({
722
800
  ...currentParts,
723
801
  showGrid: currentInput.showGrid,
802
+ scene: currentInput.scene,
803
+ showBackground: currentInput.showBackground,
804
+ winterMode: currentInput.winterMode,
805
+ springMode: currentInput.springMode,
724
806
  cameraOffsetX: currentInput.cameraOffsetX,
725
807
  cameraZoom: currentInput.cameraZoom,
726
808
  gridSettings: currentInput.gridSettings,
@@ -747,6 +829,10 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
747
829
  ...currentParts,
748
830
  format: "sprite",
749
831
  showGrid: currentInput.showGrid,
832
+ scene: currentInput.scene,
833
+ showBackground: currentInput.showBackground,
834
+ winterMode: currentInput.winterMode,
835
+ springMode: currentInput.springMode,
750
836
  cameraOffsetX: currentInput.cameraOffsetX,
751
837
  cameraZoom: currentInput.cameraZoom,
752
838
  gridSettings: currentInput.gridSettings,
@@ -781,6 +867,10 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
781
867
  return a.shareCode === b.shareCode &&
782
868
  a.profile === b.profile &&
783
869
  a.showGrid === b.showGrid &&
870
+ a.scene === b.scene &&
871
+ a.showBackground === b.showBackground &&
872
+ a.winterMode === b.winterMode &&
873
+ a.springMode === b.springMode &&
784
874
  a.cameraOffsetX === b.cameraOffsetX &&
785
875
  a.cameraZoom === b.cameraZoom &&
786
876
  a.frameQuality === b.frameQuality &&
@@ -791,6 +881,10 @@ const useSpriteRender = (input, apiConfig, onLoadStart, options) => {
791
881
  const gridSettingsEqual = JSON.stringify(a.gridSettings ?? {}) === JSON.stringify(b.gridSettings ?? {});
792
882
  return arePartsEqual(a.parts, b.parts) &&
793
883
  a.showGrid === b.showGrid &&
884
+ a.scene === b.scene &&
885
+ a.showBackground === b.showBackground &&
886
+ a.winterMode === b.winterMode &&
887
+ a.springMode === b.springMode &&
794
888
  a.cameraOffsetX === b.cameraOffsetX &&
795
889
  a.cameraZoom === b.cameraZoom &&
796
890
  a.frameQuality === b.frameQuality &&
@@ -969,7 +1063,7 @@ const useZoomPan = ({ displayWidth, displayHeight, minScale = 0.5, maxScale = 2.
969
1063
  };
970
1064
  };
971
1065
 
972
- const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpriteRenderOptions, mouseSensitivity = 0.2, touchSensitivity = 0.2, showGrid, cameraOffsetX, cameraZoom, gridSettings, animationMode = 'bounce', spinDuration = 10000, interactive = true, frameQuality, zoom = 1, }) => {
1066
+ const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpriteRenderOptions, mouseSensitivity = 0.2, touchSensitivity = 0.2, showGrid, scene, showBackground, winterMode, springMode, cameraOffsetX, cameraZoom, gridSettings, animationMode = 'bounce', spinDuration = 10000, interactive = true, frameQuality, zoom = 1, }) => {
973
1067
  const canvasRef = react.useRef(null);
974
1068
  const containerRef = react.useRef(null);
975
1069
  const [img, setImg] = react.useState(null);
@@ -979,28 +1073,45 @@ const BuildRender = ({ parts, shareCode, width, height, size, apiConfig, useSpri
979
1073
  const displayH = height ?? size ?? 300;
980
1074
  // Build the render input - prefer shareCode if provided (preserves interactive state like case fan slots)
981
1075
  const renderInput = react.useMemo(() => {
1076
+ const resolvedShowGrid = showGrid ?? parts?.showGrid;
1077
+ const resolvedScene = scene ?? parts?.scene;
1078
+ const resolvedShowBackground = showBackground ?? parts?.showBackground;
1079
+ const resolvedWinterMode = winterMode ?? parts?.winterMode;
1080
+ const resolvedSpringMode = springMode ?? parts?.springMode;
1081
+ const resolvedCameraOffsetX = cameraOffsetX ?? parts?.cameraOffsetX;
1082
+ const resolvedCameraZoom = cameraZoom ?? parts?.cameraZoom;
1083
+ const resolvedGridSettings = gridSettings ?? parts?.gridSettings;
1084
+ const resolvedFrameQuality = frameQuality ?? parts?.frameQuality;
982
1085
  if (shareCode) {
983
1086
  return {
984
1087
  type: 'shareCode',
985
1088
  shareCode,
986
1089
  profile: parts?.profile,
987
- showGrid,
988
- cameraOffsetX,
989
- cameraZoom,
990
- gridSettings,
991
- frameQuality,
1090
+ showGrid: resolvedShowGrid,
1091
+ scene: resolvedScene,
1092
+ showBackground: resolvedShowBackground,
1093
+ winterMode: resolvedWinterMode,
1094
+ springMode: resolvedSpringMode,
1095
+ cameraOffsetX: resolvedCameraOffsetX,
1096
+ cameraZoom: resolvedCameraZoom,
1097
+ gridSettings: resolvedGridSettings,
1098
+ frameQuality: resolvedFrameQuality,
992
1099
  };
993
1100
  }
994
1101
  return {
995
1102
  type: 'parts',
996
1103
  parts: parts,
997
- showGrid,
998
- cameraOffsetX,
999
- cameraZoom,
1000
- gridSettings,
1001
- frameQuality,
1104
+ showGrid: resolvedShowGrid,
1105
+ scene: resolvedScene,
1106
+ showBackground: resolvedShowBackground,
1107
+ winterMode: resolvedWinterMode,
1108
+ springMode: resolvedSpringMode,
1109
+ cameraOffsetX: resolvedCameraOffsetX,
1110
+ cameraZoom: resolvedCameraZoom,
1111
+ gridSettings: resolvedGridSettings,
1112
+ frameQuality: resolvedFrameQuality,
1002
1113
  };
1003
- }, [shareCode, parts, showGrid, cameraOffsetX, cameraZoom, gridSettings, frameQuality]);
1114
+ }, [shareCode, parts, showGrid, scene, showBackground, winterMode, springMode, cameraOffsetX, cameraZoom, gridSettings, frameQuality]);
1004
1115
  // Use custom hook for sprite rendering
1005
1116
  const { spriteSrc, isRenderingSprite, renderError, spriteMetadata } = useSpriteRender(renderInput, apiConfig, undefined, useSpriteRenderOptions);
1006
1117
  const total = spriteMetadata ? spriteMetadata.totalFrames : 72;