@spatialwalk/avatarkit 1.0.0-beta.47 → 1.0.0-beta.48

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/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.0-beta.48] - 2025-01-05
9
+
10
+ ### ✨ New Features
11
+ - **PWA Cache Management** - Added automatic PWA cache management for character and template resources to improve loading performance
12
+
13
+ ### 🔧 Performance Improvements
14
+ - **Cache Hit Rate Metrics** - Resource downloads now report cache status for analytics
15
+
8
16
  ## [1.0.0-beta.47] - 2025-12-29
9
17
 
10
18
  ### 🐛 Bugfix
@@ -1,7 +1,7 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
- import { A as APP_CONFIG, e as errorToMessage, l as logEvent, a as logger } from "./index-DBmQDc73.js";
4
+ import { A as APP_CONFIG, e as errorToMessage, l as logEvent, a as logger } from "./index-C0EU_1kt.js";
5
5
  class StreamingAudioPlayer {
6
6
  constructor(options) {
7
7
  __publicField(this, "audioContext", null);
@@ -32,6 +32,10 @@ export declare class AvatarController {
32
32
  private lastSyncLogTime;
33
33
  private lastOutOfBoundsState;
34
34
  private isAudioOnlyMode;
35
+ private playbackStuckCheckState;
36
+ private readonly MAX_AUDIO_TIME_ZERO_COUNT;
37
+ private readonly MAX_AUDIO_TIME_STUCK_COUNT;
38
+ private readonly AUDIO_TIME_STUCK_THRESHOLD;
35
39
  private hostModeMetrics;
36
40
  private readonly audioBytesPerSecond;
37
41
  constructor(avatar: Avatar, options?: {
@@ -7576,7 +7576,7 @@ const _AnimationPlayer = class _AnimationPlayer {
7576
7576
  if (this.streamingPlayer) {
7577
7577
  return;
7578
7578
  }
7579
- const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-dNR3u0Z_.js");
7579
+ const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-BCUbWwGa.js");
7580
7580
  const { AvatarSDK: AvatarSDK2 } = await Promise.resolve().then(() => AvatarSDK$1);
7581
7581
  const audioFormat = AvatarSDK2.getAudioFormat();
7582
7582
  this.streamingPlayer = new StreamingAudioPlayer({
@@ -8966,7 +8966,7 @@ class AvatarSDK {
8966
8966
  }
8967
8967
  __publicField(AvatarSDK, "_isInitialized", false);
8968
8968
  __publicField(AvatarSDK, "_configuration", null);
8969
- __publicField(AvatarSDK, "_version", "1.0.0-beta.47");
8969
+ __publicField(AvatarSDK, "_version", "1.0.0-beta.48");
8970
8970
  __publicField(AvatarSDK, "_avatarCore", null);
8971
8971
  __publicField(AvatarSDK, "_dynamicSdkConfig", null);
8972
8972
  const AvatarSDK$1 = Object.freeze(Object.defineProperty({
@@ -10635,6 +10635,15 @@ class AvatarController {
10635
10635
  __publicField(this, "lastSyncLogTime", 0);
10636
10636
  __publicField(this, "lastOutOfBoundsState", false);
10637
10637
  __publicField(this, "isAudioOnlyMode", false);
10638
+ __publicField(this, "playbackStuckCheckState", {
10639
+ audioTimeZeroCount: 0,
10640
+ lastAudioTime: 0,
10641
+ audioTimeStuckCount: 0,
10642
+ reported: false
10643
+ });
10644
+ __publicField(this, "MAX_AUDIO_TIME_ZERO_COUNT", 60);
10645
+ __publicField(this, "MAX_AUDIO_TIME_STUCK_COUNT", 60);
10646
+ __publicField(this, "AUDIO_TIME_STUCK_THRESHOLD", 1e-3);
10638
10647
  __publicField(this, "hostModeMetrics", {
10639
10648
  accumulatedBytes: 0,
10640
10649
  startTimestamp: 0,
@@ -11240,6 +11249,68 @@ class AvatarController {
11240
11249
  this.isPlaying = false;
11241
11250
  }
11242
11251
  }
11252
+ checkPlaybackStuck(audioTime) {
11253
+ var _a, _b, _c, _d, _e2, _f;
11254
+ const streamingPlayer = (_a = this.animationPlayer) == null ? void 0 : _a.getStreamingPlayer();
11255
+ if (!streamingPlayer) {
11256
+ return false;
11257
+ }
11258
+ const state = this.playbackStuckCheckState;
11259
+ const hasAnimationData = this.currentKeyframes.length > 0;
11260
+ const hasAudioData = ((_b = streamingPlayer.audioChunks) == null ? void 0 : _b.length) > 0;
11261
+ const isNotPaused = this.currentState !== AvatarState.paused;
11262
+ if (!hasAnimationData || !hasAudioData || this.currentState === AvatarState.paused) {
11263
+ state.audioTimeZeroCount = 0;
11264
+ state.audioTimeStuckCount = 0;
11265
+ state.lastAudioTime = 0;
11266
+ state.reported = false;
11267
+ return false;
11268
+ }
11269
+ if (state.reported) {
11270
+ return true;
11271
+ }
11272
+ const audioContextState = ((_c = streamingPlayer.audioContext) == null ? void 0 : _c.state) || "unknown";
11273
+ const isAudioContextSuspended = audioContextState === "suspended";
11274
+ const activeSourcesCount = ((_d = streamingPlayer.activeSources) == null ? void 0 : _d.size) || 0;
11275
+ const scheduledChunks = streamingPlayer.scheduledChunks || 0;
11276
+ const hasScheduledButNoActive = scheduledChunks > 0 && activeSourcesCount === 0;
11277
+ if (audioTime === 0) {
11278
+ state.audioTimeZeroCount++;
11279
+ } else {
11280
+ state.audioTimeZeroCount = 0;
11281
+ }
11282
+ if (Math.abs(audioTime - state.lastAudioTime) < this.AUDIO_TIME_STUCK_THRESHOLD) {
11283
+ state.audioTimeStuckCount++;
11284
+ } else {
11285
+ state.audioTimeStuckCount = 0;
11286
+ }
11287
+ state.lastAudioTime = audioTime;
11288
+ const shouldReport = state.audioTimeZeroCount > this.MAX_AUDIO_TIME_ZERO_COUNT || state.audioTimeStuckCount > this.MAX_AUDIO_TIME_STUCK_COUNT || isAudioContextSuspended && this.isPlaying || hasScheduledButNoActive && this.isPlaying;
11289
+ if (shouldReport && isNotPaused) {
11290
+ state.reported = true;
11291
+ logEvent("character_player", "error", {
11292
+ event: "playback_stuck_after_transition",
11293
+ avatar_id: this.avatar.id,
11294
+ conversationId: ((_e2 = this.networkLayer) == null ? void 0 : _e2.getCurrentConversationId()) || void 0,
11295
+ audioContextState,
11296
+ isAudioContextSuspended,
11297
+ audioTime,
11298
+ audioTimeZero: audioTime === 0,
11299
+ audioTimeZeroCount: state.audioTimeZeroCount,
11300
+ audioTimeStuck: state.audioTimeStuckCount > this.MAX_AUDIO_TIME_STUCK_COUNT,
11301
+ audioTimeStuckCount: state.audioTimeStuckCount,
11302
+ hasScheduledButNoActive,
11303
+ isPlaying: this.isPlaying,
11304
+ currentState: this.currentState,
11305
+ animationFramesCount: this.currentKeyframes.length,
11306
+ audioChunksCount: ((_f = streamingPlayer.audioChunks) == null ? void 0 : _f.length) || 0,
11307
+ scheduledChunks,
11308
+ activeSourcesCount
11309
+ });
11310
+ return true;
11311
+ }
11312
+ return false;
11313
+ }
11243
11314
  startPlaybackLoop() {
11244
11315
  if (this.playbackLoopId) {
11245
11316
  return;
@@ -11248,14 +11319,23 @@ class AvatarController {
11248
11319
  const playLoop = async () => {
11249
11320
  if (!this.isPlaying || this.currentState === AvatarState.paused || !this.animationPlayer) {
11250
11321
  this.playbackLoopId = null;
11322
+ this.playbackStuckCheckState.audioTimeZeroCount = 0;
11323
+ this.playbackStuckCheckState.audioTimeStuckCount = 0;
11324
+ this.playbackStuckCheckState.lastAudioTime = 0;
11325
+ this.playbackStuckCheckState.reported = false;
11251
11326
  return;
11252
11327
  }
11253
11328
  try {
11254
11329
  const audioTime = this.animationPlayer.getCurrentTime();
11330
+ this.checkPlaybackStuck(audioTime);
11255
11331
  if (audioTime === 0) {
11256
11332
  this.playbackLoopId = requestAnimationFrame(playLoop);
11257
11333
  return;
11258
11334
  }
11335
+ if (audioTime > 0 && Math.abs(audioTime - this.playbackStuckCheckState.lastAudioTime) >= this.AUDIO_TIME_STUCK_THRESHOLD) {
11336
+ this.playbackStuckCheckState.audioTimeZeroCount = 0;
11337
+ this.playbackStuckCheckState.audioTimeStuckCount = 0;
11338
+ }
11259
11339
  let frameIndex = Math.floor(audioTime * fps);
11260
11340
  if (frameIndex < 0) frameIndex = 0;
11261
11341
  let arrayIndex = frameIndex - this.keyframesOffset;
@@ -11558,13 +11638,197 @@ function errorToMessage(err) {
11558
11638
  }
11559
11639
  return String(err);
11560
11640
  }
11561
- async function downloadResource(url) {
11641
+ const _PwaCacheManager = class _PwaCacheManager {
11642
+ static isSupported() {
11643
+ return typeof caches !== "undefined";
11644
+ }
11645
+ static getCharacterCacheName(characterId) {
11646
+ return `${_PwaCacheManager.CHARACTER_CACHE_PREFIX}${characterId}${_PwaCacheManager.CHARACTER_CACHE_SUFFIX}`;
11647
+ }
11648
+ static async getCharacterResource(characterId, url) {
11649
+ if (!_PwaCacheManager.isSupported()) {
11650
+ return null;
11651
+ }
11652
+ try {
11653
+ const cacheName = _PwaCacheManager.getCharacterCacheName(characterId);
11654
+ const cache = await caches.open(cacheName);
11655
+ const response = await cache.match(url);
11656
+ if (response) {
11657
+ const arrayBuffer = await response.arrayBuffer();
11658
+ logger.log(`[PwaCacheManager] Character resource cache hit: ${url}`);
11659
+ return arrayBuffer;
11660
+ }
11661
+ return null;
11662
+ } catch (error) {
11663
+ logger.warn(`[PwaCacheManager] Failed to get character resource from cache:`, error);
11664
+ return null;
11665
+ }
11666
+ }
11667
+ static async putCharacterResource(characterId, url, data) {
11668
+ if (!_PwaCacheManager.isSupported()) {
11669
+ return;
11670
+ }
11671
+ try {
11672
+ const cacheName = _PwaCacheManager.getCharacterCacheName(characterId);
11673
+ const cache = await caches.open(cacheName);
11674
+ const keys = await cache.keys();
11675
+ if (keys.length >= _PwaCacheManager.MAX_CHARACTER_CACHE_ENTRIES) {
11676
+ const oldestKey = keys[0];
11677
+ await cache.delete(oldestKey);
11678
+ logger.log(`[PwaCacheManager] Character cache full, deleted oldest entry: ${oldestKey.url}`);
11679
+ }
11680
+ await cache.put(url, new Response(data));
11681
+ logger.log(`[PwaCacheManager] Character resource cached: ${url}`);
11682
+ } catch (error) {
11683
+ logger.warn(`[PwaCacheManager] Failed to put character resource to cache:`, error);
11684
+ }
11685
+ }
11686
+ static async getTemplateResource(url) {
11687
+ if (!_PwaCacheManager.isSupported()) {
11688
+ return null;
11689
+ }
11690
+ try {
11691
+ const cache = await caches.open(_PwaCacheManager.TEMPLATE_CACHE_NAME);
11692
+ const response = await cache.match(url);
11693
+ if (response) {
11694
+ const arrayBuffer = await response.arrayBuffer();
11695
+ logger.log(`[PwaCacheManager] Template resource cache hit: ${url}`);
11696
+ return arrayBuffer;
11697
+ }
11698
+ return null;
11699
+ } catch (error) {
11700
+ logger.warn(`[PwaCacheManager] Failed to get template resource from cache:`, error);
11701
+ return null;
11702
+ }
11703
+ }
11704
+ static async putTemplateResource(url, data) {
11705
+ if (!_PwaCacheManager.isSupported()) {
11706
+ return;
11707
+ }
11708
+ try {
11709
+ const cache = await caches.open(_PwaCacheManager.TEMPLATE_CACHE_NAME);
11710
+ await cache.put(url, new Response(data));
11711
+ logger.log(`[PwaCacheManager] Template resource cached: ${url}`);
11712
+ } catch (error) {
11713
+ logger.warn(`[PwaCacheManager] Failed to put template resource to cache:`, error);
11714
+ }
11715
+ }
11716
+ static async clearCharacterCache(characterId) {
11717
+ if (!_PwaCacheManager.isSupported()) {
11718
+ return;
11719
+ }
11720
+ try {
11721
+ const cacheName = _PwaCacheManager.getCharacterCacheName(characterId);
11722
+ await caches.delete(cacheName);
11723
+ logger.log(`[PwaCacheManager] Character cache cleared: ${characterId}`);
11724
+ } catch (error) {
11725
+ logger.warn(`[PwaCacheManager] Failed to clear character cache:`, error);
11726
+ }
11727
+ }
11728
+ static async checkTemplateCacheVersion(newVersion) {
11729
+ if (!_PwaCacheManager.isSupported()) {
11730
+ return false;
11731
+ }
11732
+ try {
11733
+ const storedVersion = localStorage.getItem(_PwaCacheManager.TEMPLATE_VERSION_STORAGE_KEY);
11734
+ if (storedVersion !== newVersion) {
11735
+ await caches.delete(_PwaCacheManager.TEMPLATE_CACHE_NAME);
11736
+ localStorage.setItem(_PwaCacheManager.TEMPLATE_VERSION_STORAGE_KEY, newVersion);
11737
+ logger.log(`[PwaCacheManager] Template cache version changed: ${storedVersion} -> ${newVersion}, cache cleared`);
11738
+ return true;
11739
+ }
11740
+ return false;
11741
+ } catch (error) {
11742
+ logger.warn(`[PwaCacheManager] Failed to check template cache version:`, error);
11743
+ return false;
11744
+ }
11745
+ }
11746
+ };
11747
+ __publicField(_PwaCacheManager, "TEMPLATE_CACHE_VERSION", "v1");
11748
+ __publicField(_PwaCacheManager, "TEMPLATE_CACHE_NAME", `spatialwalk-sdk-template-cache-${_PwaCacheManager.TEMPLATE_CACHE_VERSION}`);
11749
+ __publicField(_PwaCacheManager, "TEMPLATE_VERSION_STORAGE_KEY", "spatialwalk-sdk-template-cache-version");
11750
+ __publicField(_PwaCacheManager, "CHARACTER_CACHE_PREFIX", "spatialwalk-sdk-character-");
11751
+ __publicField(_PwaCacheManager, "CHARACTER_CACHE_SUFFIX", "-cache");
11752
+ __publicField(_PwaCacheManager, "MAX_CHARACTER_CACHE_ENTRIES", 1e3);
11753
+ let PwaCacheManager = _PwaCacheManager;
11754
+ const pwaCacheManager = Object.freeze(Object.defineProperty({
11755
+ __proto__: null,
11756
+ PwaCacheManager
11757
+ }, Symbol.toStringTag, { value: "Module" }));
11758
+ function getCacheInfo(url, response) {
11759
+ var _a;
11760
+ const resourceTiming = performance.getEntriesByName(url, "resource")[0];
11761
+ const transferSize = (resourceTiming == null ? void 0 : resourceTiming.transferSize) ?? 0;
11762
+ const deliveryType = resourceTiming == null ? void 0 : resourceTiming.deliveryType;
11763
+ const hasServiceWorker = typeof navigator !== "undefined" && ((_a = navigator.serviceWorker) == null ? void 0 : _a.controller) !== null;
11764
+ const isPwaCache = transferSize === 0 && hasServiceWorker && (deliveryType === "cache" || deliveryType === "serviceworker") && (resourceTiming == null ? void 0 : resourceTiming.duration) !== void 0 && resourceTiming.duration > 0;
11765
+ const isBrowserCache = transferSize === 0 && !isPwaCache && (!hasServiceWorker || deliveryType === "cache") && (resourceTiming == null ? void 0 : resourceTiming.duration) !== void 0 && resourceTiming.duration > 0;
11766
+ const cfCacheStatus = response.headers.get("cf-cache-status");
11767
+ const xCache = response.headers.get("x-cache");
11768
+ const xSwiftCacheTime = response.headers.get("x-swift-cache-time");
11769
+ const age = response.headers.get("age");
11770
+ const cdnCacheStatus = cfCacheStatus || xCache || xSwiftCacheTime;
11771
+ const isCdnCache = cdnCacheStatus && (cdnCacheStatus.toLowerCase() === "hit" || cdnCacheStatus.toLowerCase() === "hits" || cdnCacheStatus.toLowerCase().includes("hit")) || age && parseInt(age, 10) > 0;
11772
+ let cacheHit = false;
11773
+ let cacheType = "none";
11774
+ if (isPwaCache) {
11775
+ cacheHit = true;
11776
+ cacheType = "pwa";
11777
+ } else if (isBrowserCache) {
11778
+ cacheHit = true;
11779
+ cacheType = "browser";
11780
+ } else if (isCdnCache) {
11781
+ cacheHit = true;
11782
+ cacheType = "cdn";
11783
+ }
11784
+ return {
11785
+ cacheHit,
11786
+ cacheType,
11787
+ transferSize,
11788
+ cdnCacheStatus: cdnCacheStatus || void 0
11789
+ };
11790
+ }
11791
+ async function downloadResource(url, options) {
11562
11792
  try {
11793
+ let cached = null;
11794
+ let pwaCacheSubtype = void 0;
11795
+ if (options == null ? void 0 : options.characterId) {
11796
+ cached = await PwaCacheManager.getCharacterResource(options.characterId, url);
11797
+ if (cached) {
11798
+ pwaCacheSubtype = "character";
11799
+ }
11800
+ } else if ((options == null ? void 0 : options.resourceType) === "template") {
11801
+ cached = await PwaCacheManager.getTemplateResource(url);
11802
+ if (cached) {
11803
+ pwaCacheSubtype = "template";
11804
+ }
11805
+ }
11806
+ if (cached) {
11807
+ const response2 = new Response(cached);
11808
+ await new Promise((resolve2) => setTimeout(resolve2, 0));
11809
+ const cacheInfo2 = getCacheInfo(url, response2);
11810
+ cacheInfo2.cacheHit = true;
11811
+ cacheInfo2.cacheType = "pwa";
11812
+ cacheInfo2.pwaCacheSubtype = pwaCacheSubtype;
11813
+ return { data: cached, cacheInfo: cacheInfo2 };
11814
+ }
11563
11815
  const response = await fetch(url);
11564
11816
  if (!response.ok) {
11565
11817
  throw new Error(`HTTP ${response.status} ${response.statusText}`);
11566
11818
  }
11567
- return await response.arrayBuffer();
11819
+ const arrayBuffer = await response.arrayBuffer();
11820
+ if (options == null ? void 0 : options.characterId) {
11821
+ PwaCacheManager.putCharacterResource(options.characterId, url, arrayBuffer).catch((err) => {
11822
+ logger.warn(`[downloadResource] Failed to cache character resource:`, err);
11823
+ });
11824
+ } else if ((options == null ? void 0 : options.resourceType) === "template") {
11825
+ PwaCacheManager.putTemplateResource(url, arrayBuffer).catch((err) => {
11826
+ logger.warn(`[downloadResource] Failed to cache template resource:`, err);
11827
+ });
11828
+ }
11829
+ await new Promise((resolve2) => setTimeout(resolve2, 0));
11830
+ const cacheInfo = getCacheInfo(url, response);
11831
+ return { data: arrayBuffer, cacheInfo };
11568
11832
  } catch (err) {
11569
11833
  const msg = errorToMessage(err);
11570
11834
  throw new Error(`[downloadResource] ${url} → ${msg}`);
@@ -11577,6 +11841,8 @@ class AvatarDownloader {
11577
11841
  }
11578
11842
  async loadTemplateResources(flameResources, progressCallback = null) {
11579
11843
  var _a, _b, _c, _d;
11844
+ const sdkVersion2 = AvatarSDK.version;
11845
+ await PwaCacheManager.checkTemplateCacheVersion(sdkVersion2);
11580
11846
  const useApiResources = flameResources && Object.keys(flameResources).length > 0;
11581
11847
  if (!useApiResources) {
11582
11848
  logger.log("Template resources not provided in CharacterMeta, using global CDN config");
@@ -11623,15 +11889,26 @@ class AvatarDownloader {
11623
11889
  }
11624
11890
  updateProgress(resourceName, false);
11625
11891
  logger.log(`📥 Loading ${key} from API CDN: ${url}`);
11626
- const buffer = await downloadResource(url);
11892
+ const { data: buffer, cacheInfo } = await downloadResource(url, { resourceType: "template" });
11627
11893
  logger.log(`✅ ${key} loaded: ${buffer.byteLength} bytes`);
11628
11894
  templateResources[key] = buffer;
11629
11895
  updateProgress(resourceName, true);
11896
+ logEvent("resource_download", "info", {
11897
+ resource_type: "template",
11898
+ resource_name: resourceName,
11899
+ cache_hit: cacheInfo.cacheHit,
11900
+ cache_type: cacheInfo.cacheType,
11901
+ pwa_cache_subtype: cacheInfo.pwaCacheSubtype,
11902
+ transfer_size: cacheInfo.transferSize,
11903
+ cdn_cache_status: cacheInfo.cdnCacheStatus
11904
+ });
11630
11905
  });
11631
11906
  await Promise.all(promises);
11632
11907
  return templateResources;
11633
11908
  }
11634
11909
  async loadGlobalFlameResources(progressCallback = null) {
11910
+ const sdkVersion2 = AvatarSDK.version;
11911
+ await PwaCacheManager.checkTemplateCacheVersion(sdkVersion2);
11635
11912
  const startTime = Date.now();
11636
11913
  const { cdnBaseUrl, resources } = APP_CONFIG.flame;
11637
11914
  const templateFiles = {
@@ -11672,10 +11949,19 @@ class AvatarDownloader {
11672
11949
  const promises = Object.entries(templateFiles).map(async ([key, { url, resourceName }]) => {
11673
11950
  updateProgress(resourceName, false);
11674
11951
  logger.log(`📥 Loading ${key} from global CDN: ${url}`);
11675
- const buffer = await downloadResource(url);
11952
+ const { data: buffer, cacheInfo } = await downloadResource(url, { resourceType: "template" });
11676
11953
  logger.log(`✅ ${key} loaded: ${buffer.byteLength} bytes`);
11677
11954
  templateResources[key] = buffer;
11678
11955
  updateProgress(resourceName, true);
11956
+ logEvent("resource_download", "info", {
11957
+ resource_type: "template",
11958
+ resource_name: resourceName,
11959
+ cache_hit: cacheInfo.cacheHit,
11960
+ cache_type: cacheInfo.cacheType,
11961
+ pwa_cache_subtype: cacheInfo.pwaCacheSubtype,
11962
+ transfer_size: cacheInfo.transferSize,
11963
+ cdn_cache_status: cacheInfo.cdnCacheStatus
11964
+ });
11679
11965
  });
11680
11966
  await Promise.all(promises);
11681
11967
  const duration = Date.now() - startTime;
@@ -11697,11 +11983,21 @@ class AvatarDownloader {
11697
11983
  }
11698
11984
  try {
11699
11985
  logger.log(`📥 Loading camera info from: ${cameraUrl}`);
11700
- const response = await fetch(cameraUrl);
11701
- if (!response.ok) {
11702
- throw new Error(`Failed to fetch camera info: ${response.statusText}`);
11703
- }
11704
- const cameraSettings = await response.json();
11986
+ const { data: arrayBuffer, cacheInfo } = await downloadResource(cameraUrl, {
11987
+ characterId: characterMeta.characterId ?? void 0,
11988
+ resourceType: "character"
11989
+ });
11990
+ const text = new TextDecoder().decode(arrayBuffer);
11991
+ const cameraSettings = JSON.parse(text);
11992
+ logEvent("resource_download", "info", {
11993
+ resource_type: "camera_settings",
11994
+ resource_name: "camera.json",
11995
+ cache_hit: cacheInfo.cacheHit,
11996
+ cache_type: cacheInfo.cacheType,
11997
+ pwa_cache_subtype: cacheInfo.pwaCacheSubtype,
11998
+ transfer_size: cacheInfo.transferSize,
11999
+ cdn_cache_status: cacheInfo.cdnCacheStatus
12000
+ });
11705
12001
  logger.log("✅ Camera info loaded:", cameraSettings);
11706
12002
  return cameraSettings;
11707
12003
  } catch (error) {
@@ -11751,7 +12047,10 @@ class AvatarDownloader {
11751
12047
  const downloadPromises = filesToLoad.map(async ({ key, url, filename, optional }) => {
11752
12048
  updateProgress(filename, false);
11753
12049
  try {
11754
- const arrayBuffer = await downloadResource(url);
12050
+ const { data: arrayBuffer, cacheInfo } = await downloadResource(url, {
12051
+ characterId: characterMeta.characterId ?? void 0,
12052
+ resourceType: "character"
12053
+ });
11755
12054
  if (key === "shape") {
11756
12055
  characterData.shape = arrayBuffer;
11757
12056
  } else if (key === "pointCloud") {
@@ -11760,6 +12059,15 @@ class AvatarDownloader {
11760
12059
  characterData.idleAnimation = arrayBuffer;
11761
12060
  }
11762
12061
  updateProgress(filename, true);
12062
+ logEvent("resource_download", "info", {
12063
+ resource_type: "character_asset",
12064
+ resource_name: filename,
12065
+ cache_hit: cacheInfo.cacheHit,
12066
+ cache_type: cacheInfo.cacheType,
12067
+ pwa_cache_subtype: cacheInfo.pwaCacheSubtype,
12068
+ transfer_size: cacheInfo.transferSize,
12069
+ cdn_cache_status: cacheInfo.cdnCacheStatus
12070
+ });
11763
12071
  return { key, success: true, size: arrayBuffer.byteLength };
11764
12072
  } catch (error) {
11765
12073
  if (!optional) {
@@ -11955,6 +12263,8 @@ const _AvatarManager = class _AvatarManager {
11955
12263
  } else {
11956
12264
  logger.log(`[AvatarManager] Avatar ${id} version mismatch: cached=${cachedVersion}, new=${newVersion}, reloading...`);
11957
12265
  this.avatarCache.delete(id);
12266
+ const { PwaCacheManager: PwaCacheManager2 } = await Promise.resolve().then(() => pwaCacheManager);
12267
+ await PwaCacheManager2.clearCharacterCache(id);
11958
12268
  }
11959
12269
  }
11960
12270
  logger.log(`[AvatarManager] Queuing avatar download for id: ${id}`);
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { b, c, f, d, j, g, C, i, D, E, k, h, L, R, S, m } from "./index-DBmQDc73.js";
1
+ import { b, c, f, d, j, g, C, i, D, E, k, h, L, R, S, m } from "./index-C0EU_1kt.js";
2
2
  export {
3
3
  b as Avatar,
4
4
  c as AvatarController,
@@ -0,0 +1,16 @@
1
+ export declare class PwaCacheManager {
2
+ private static readonly TEMPLATE_CACHE_VERSION;
3
+ private static readonly TEMPLATE_CACHE_NAME;
4
+ private static readonly TEMPLATE_VERSION_STORAGE_KEY;
5
+ private static readonly CHARACTER_CACHE_PREFIX;
6
+ private static readonly CHARACTER_CACHE_SUFFIX;
7
+ private static readonly MAX_CHARACTER_CACHE_ENTRIES;
8
+ static isSupported(): boolean;
9
+ private static getCharacterCacheName;
10
+ static getCharacterResource(characterId: string, url: string): Promise<ArrayBuffer | null>;
11
+ static putCharacterResource(characterId: string, url: string, data: ArrayBuffer): Promise<void>;
12
+ static getTemplateResource(url: string): Promise<ArrayBuffer | null>;
13
+ static putTemplateResource(url: string, data: ArrayBuffer): Promise<void>;
14
+ static clearCharacterCache(characterId: string): Promise<void>;
15
+ static checkTemplateCacheVersion(newVersion: string): Promise<boolean>;
16
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@spatialwalk/avatarkit",
3
3
  "type": "module",
4
- "version": "1.0.0-beta.47",
4
+ "version": "1.0.0-beta.48",
5
5
  "packageManager": "pnpm@10.18.2",
6
6
  "description": "SPAvatar SDK - 3D Gaussian Splatting Avatar Rendering SDK",
7
7
  "author": "SPAvatar Team",