@spatialwalk/avatarkit 1.0.0-beta.47 → 1.0.0-beta.49
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 +13 -0
- package/dist/{StreamingAudioPlayer-dNR3u0Z_.js → StreamingAudioPlayer-CeiNxmCv.js} +1 -1
- package/dist/core/AvatarController.d.ts +4 -0
- package/dist/{index-DBmQDc73.js → index-BoQe49oM.js} +325 -12
- package/dist/index.js +1 -1
- package/dist/utils/pwa-cache-manager.d.ts +16 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ 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.49] - 2025-01-05
|
|
9
|
+
|
|
10
|
+
### 🐛 Bugfix
|
|
11
|
+
- **Template Cache Version** - Fixed issue where template cache would be incorrectly cleared when switching between different SDK versions. Now uses independent template resource version (1.0.0) instead of SDK version, allowing different SDK versions to share the same template cache when template resources are identical.
|
|
12
|
+
|
|
13
|
+
## [1.0.0-beta.48] - 2025-01-05
|
|
14
|
+
|
|
15
|
+
### ✨ New Features
|
|
16
|
+
- **PWA Cache Management** - Added automatic PWA cache management for character and template resources to improve loading performance
|
|
17
|
+
|
|
18
|
+
### 🔧 Performance Improvements
|
|
19
|
+
- **Cache Hit Rate Metrics** - Resource downloads now report cache status for analytics
|
|
20
|
+
|
|
8
21
|
## [1.0.0-beta.47] - 2025-12-29
|
|
9
22
|
|
|
10
23
|
### 🐛 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-
|
|
4
|
+
import { A as APP_CONFIG, e as errorToMessage, l as logEvent, a as logger } from "./index-BoQe49oM.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-
|
|
7579
|
+
const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-CeiNxmCv.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.
|
|
8969
|
+
__publicField(AvatarSDK, "_version", "1.0.0-beta.49");
|
|
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,202 @@ function errorToMessage(err) {
|
|
|
11558
11638
|
}
|
|
11559
11639
|
return String(err);
|
|
11560
11640
|
}
|
|
11561
|
-
|
|
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() {
|
|
11729
|
+
if (!_PwaCacheManager.isSupported()) {
|
|
11730
|
+
return false;
|
|
11731
|
+
}
|
|
11732
|
+
try {
|
|
11733
|
+
const currentTemplateVersion = _PwaCacheManager.TEMPLATE_RESOURCE_VERSION;
|
|
11734
|
+
const storedVersion = localStorage.getItem(_PwaCacheManager.TEMPLATE_VERSION_STORAGE_KEY);
|
|
11735
|
+
if (storedVersion !== currentTemplateVersion) {
|
|
11736
|
+
if (storedVersion) {
|
|
11737
|
+
const oldCacheName = `spatialwalk-sdk-template-cache-${storedVersion}`;
|
|
11738
|
+
await caches.delete(oldCacheName).catch(() => {
|
|
11739
|
+
});
|
|
11740
|
+
}
|
|
11741
|
+
localStorage.setItem(_PwaCacheManager.TEMPLATE_VERSION_STORAGE_KEY, currentTemplateVersion);
|
|
11742
|
+
logger.log(`[PwaCacheManager] Template cache version changed: ${storedVersion} -> ${currentTemplateVersion}, old cache cleared`);
|
|
11743
|
+
return true;
|
|
11744
|
+
}
|
|
11745
|
+
return false;
|
|
11746
|
+
} catch (error) {
|
|
11747
|
+
logger.warn(`[PwaCacheManager] Failed to check template cache version:`, error);
|
|
11748
|
+
return false;
|
|
11749
|
+
}
|
|
11750
|
+
}
|
|
11751
|
+
};
|
|
11752
|
+
__publicField(_PwaCacheManager, "TEMPLATE_RESOURCE_VERSION", "1.0.0");
|
|
11753
|
+
__publicField(_PwaCacheManager, "TEMPLATE_CACHE_NAME", `spatialwalk-sdk-template-cache-${_PwaCacheManager.TEMPLATE_RESOURCE_VERSION}`);
|
|
11754
|
+
__publicField(_PwaCacheManager, "TEMPLATE_VERSION_STORAGE_KEY", "spatialwalk-sdk-template-cache-version");
|
|
11755
|
+
__publicField(_PwaCacheManager, "CHARACTER_CACHE_PREFIX", "spatialwalk-sdk-character-");
|
|
11756
|
+
__publicField(_PwaCacheManager, "CHARACTER_CACHE_SUFFIX", "-cache");
|
|
11757
|
+
__publicField(_PwaCacheManager, "MAX_CHARACTER_CACHE_ENTRIES", 1e3);
|
|
11758
|
+
let PwaCacheManager = _PwaCacheManager;
|
|
11759
|
+
const pwaCacheManager = Object.freeze(Object.defineProperty({
|
|
11760
|
+
__proto__: null,
|
|
11761
|
+
PwaCacheManager
|
|
11762
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
11763
|
+
function getCacheInfo(url, response) {
|
|
11764
|
+
var _a;
|
|
11765
|
+
const resourceTiming = performance.getEntriesByName(url, "resource")[0];
|
|
11766
|
+
const transferSize = (resourceTiming == null ? void 0 : resourceTiming.transferSize) ?? 0;
|
|
11767
|
+
const deliveryType = resourceTiming == null ? void 0 : resourceTiming.deliveryType;
|
|
11768
|
+
const hasServiceWorker = typeof navigator !== "undefined" && ((_a = navigator.serviceWorker) == null ? void 0 : _a.controller) !== null;
|
|
11769
|
+
const isPwaCache = transferSize === 0 && hasServiceWorker && (deliveryType === "cache" || deliveryType === "serviceworker") && (resourceTiming == null ? void 0 : resourceTiming.duration) !== void 0 && resourceTiming.duration > 0;
|
|
11770
|
+
const isBrowserCache = transferSize === 0 && !isPwaCache && (!hasServiceWorker || deliveryType === "cache") && (resourceTiming == null ? void 0 : resourceTiming.duration) !== void 0 && resourceTiming.duration > 0;
|
|
11771
|
+
const cfCacheStatus = response.headers.get("cf-cache-status");
|
|
11772
|
+
const xCache = response.headers.get("x-cache");
|
|
11773
|
+
const xSwiftCacheTime = response.headers.get("x-swift-cache-time");
|
|
11774
|
+
const age = response.headers.get("age");
|
|
11775
|
+
const cdnCacheStatus = cfCacheStatus || xCache || xSwiftCacheTime;
|
|
11776
|
+
const isCdnCache = cdnCacheStatus && (cdnCacheStatus.toLowerCase() === "hit" || cdnCacheStatus.toLowerCase() === "hits" || cdnCacheStatus.toLowerCase().includes("hit")) || age && parseInt(age, 10) > 0;
|
|
11777
|
+
let cacheHit = false;
|
|
11778
|
+
let cacheType = "none";
|
|
11779
|
+
if (isPwaCache) {
|
|
11780
|
+
cacheHit = true;
|
|
11781
|
+
cacheType = "pwa";
|
|
11782
|
+
} else if (isBrowserCache) {
|
|
11783
|
+
cacheHit = true;
|
|
11784
|
+
cacheType = "browser";
|
|
11785
|
+
} else if (isCdnCache) {
|
|
11786
|
+
cacheHit = true;
|
|
11787
|
+
cacheType = "cdn";
|
|
11788
|
+
}
|
|
11789
|
+
return {
|
|
11790
|
+
cacheHit,
|
|
11791
|
+
cacheType,
|
|
11792
|
+
transferSize,
|
|
11793
|
+
cdnCacheStatus: cdnCacheStatus || void 0
|
|
11794
|
+
};
|
|
11795
|
+
}
|
|
11796
|
+
async function downloadResource(url, options) {
|
|
11562
11797
|
try {
|
|
11798
|
+
let cached = null;
|
|
11799
|
+
let pwaCacheSubtype = void 0;
|
|
11800
|
+
if (options == null ? void 0 : options.characterId) {
|
|
11801
|
+
cached = await PwaCacheManager.getCharacterResource(options.characterId, url);
|
|
11802
|
+
if (cached) {
|
|
11803
|
+
pwaCacheSubtype = "character";
|
|
11804
|
+
}
|
|
11805
|
+
} else if ((options == null ? void 0 : options.resourceType) === "template") {
|
|
11806
|
+
cached = await PwaCacheManager.getTemplateResource(url);
|
|
11807
|
+
if (cached) {
|
|
11808
|
+
pwaCacheSubtype = "template";
|
|
11809
|
+
}
|
|
11810
|
+
}
|
|
11811
|
+
if (cached) {
|
|
11812
|
+
const response2 = new Response(cached);
|
|
11813
|
+
await new Promise((resolve2) => setTimeout(resolve2, 0));
|
|
11814
|
+
const cacheInfo2 = getCacheInfo(url, response2);
|
|
11815
|
+
cacheInfo2.cacheHit = true;
|
|
11816
|
+
cacheInfo2.cacheType = "pwa";
|
|
11817
|
+
cacheInfo2.pwaCacheSubtype = pwaCacheSubtype;
|
|
11818
|
+
return { data: cached, cacheInfo: cacheInfo2 };
|
|
11819
|
+
}
|
|
11563
11820
|
const response = await fetch(url);
|
|
11564
11821
|
if (!response.ok) {
|
|
11565
11822
|
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
11566
11823
|
}
|
|
11567
|
-
|
|
11824
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
11825
|
+
if (options == null ? void 0 : options.characterId) {
|
|
11826
|
+
PwaCacheManager.putCharacterResource(options.characterId, url, arrayBuffer).catch((err) => {
|
|
11827
|
+
logger.warn(`[downloadResource] Failed to cache character resource:`, err);
|
|
11828
|
+
});
|
|
11829
|
+
} else if ((options == null ? void 0 : options.resourceType) === "template") {
|
|
11830
|
+
PwaCacheManager.putTemplateResource(url, arrayBuffer).catch((err) => {
|
|
11831
|
+
logger.warn(`[downloadResource] Failed to cache template resource:`, err);
|
|
11832
|
+
});
|
|
11833
|
+
}
|
|
11834
|
+
await new Promise((resolve2) => setTimeout(resolve2, 0));
|
|
11835
|
+
const cacheInfo = getCacheInfo(url, response);
|
|
11836
|
+
return { data: arrayBuffer, cacheInfo };
|
|
11568
11837
|
} catch (err) {
|
|
11569
11838
|
const msg = errorToMessage(err);
|
|
11570
11839
|
throw new Error(`[downloadResource] ${url} → ${msg}`);
|
|
@@ -11577,6 +11846,7 @@ class AvatarDownloader {
|
|
|
11577
11846
|
}
|
|
11578
11847
|
async loadTemplateResources(flameResources, progressCallback = null) {
|
|
11579
11848
|
var _a, _b, _c, _d;
|
|
11849
|
+
await PwaCacheManager.checkTemplateCacheVersion();
|
|
11580
11850
|
const useApiResources = flameResources && Object.keys(flameResources).length > 0;
|
|
11581
11851
|
if (!useApiResources) {
|
|
11582
11852
|
logger.log("Template resources not provided in CharacterMeta, using global CDN config");
|
|
@@ -11623,15 +11893,25 @@ class AvatarDownloader {
|
|
|
11623
11893
|
}
|
|
11624
11894
|
updateProgress(resourceName, false);
|
|
11625
11895
|
logger.log(`📥 Loading ${key} from API CDN: ${url}`);
|
|
11626
|
-
const buffer = await downloadResource(url);
|
|
11896
|
+
const { data: buffer, cacheInfo } = await downloadResource(url, { resourceType: "template" });
|
|
11627
11897
|
logger.log(`✅ ${key} loaded: ${buffer.byteLength} bytes`);
|
|
11628
11898
|
templateResources[key] = buffer;
|
|
11629
11899
|
updateProgress(resourceName, true);
|
|
11900
|
+
logEvent("resource_download", "info", {
|
|
11901
|
+
resource_type: "template",
|
|
11902
|
+
resource_name: resourceName,
|
|
11903
|
+
cache_hit: cacheInfo.cacheHit,
|
|
11904
|
+
cache_type: cacheInfo.cacheType,
|
|
11905
|
+
pwa_cache_subtype: cacheInfo.pwaCacheSubtype,
|
|
11906
|
+
transfer_size: cacheInfo.transferSize,
|
|
11907
|
+
cdn_cache_status: cacheInfo.cdnCacheStatus
|
|
11908
|
+
});
|
|
11630
11909
|
});
|
|
11631
11910
|
await Promise.all(promises);
|
|
11632
11911
|
return templateResources;
|
|
11633
11912
|
}
|
|
11634
11913
|
async loadGlobalFlameResources(progressCallback = null) {
|
|
11914
|
+
await PwaCacheManager.checkTemplateCacheVersion();
|
|
11635
11915
|
const startTime = Date.now();
|
|
11636
11916
|
const { cdnBaseUrl, resources } = APP_CONFIG.flame;
|
|
11637
11917
|
const templateFiles = {
|
|
@@ -11672,10 +11952,19 @@ class AvatarDownloader {
|
|
|
11672
11952
|
const promises = Object.entries(templateFiles).map(async ([key, { url, resourceName }]) => {
|
|
11673
11953
|
updateProgress(resourceName, false);
|
|
11674
11954
|
logger.log(`📥 Loading ${key} from global CDN: ${url}`);
|
|
11675
|
-
const buffer = await downloadResource(url);
|
|
11955
|
+
const { data: buffer, cacheInfo } = await downloadResource(url, { resourceType: "template" });
|
|
11676
11956
|
logger.log(`✅ ${key} loaded: ${buffer.byteLength} bytes`);
|
|
11677
11957
|
templateResources[key] = buffer;
|
|
11678
11958
|
updateProgress(resourceName, true);
|
|
11959
|
+
logEvent("resource_download", "info", {
|
|
11960
|
+
resource_type: "template",
|
|
11961
|
+
resource_name: resourceName,
|
|
11962
|
+
cache_hit: cacheInfo.cacheHit,
|
|
11963
|
+
cache_type: cacheInfo.cacheType,
|
|
11964
|
+
pwa_cache_subtype: cacheInfo.pwaCacheSubtype,
|
|
11965
|
+
transfer_size: cacheInfo.transferSize,
|
|
11966
|
+
cdn_cache_status: cacheInfo.cdnCacheStatus
|
|
11967
|
+
});
|
|
11679
11968
|
});
|
|
11680
11969
|
await Promise.all(promises);
|
|
11681
11970
|
const duration = Date.now() - startTime;
|
|
@@ -11697,11 +11986,21 @@ class AvatarDownloader {
|
|
|
11697
11986
|
}
|
|
11698
11987
|
try {
|
|
11699
11988
|
logger.log(`📥 Loading camera info from: ${cameraUrl}`);
|
|
11700
|
-
const
|
|
11701
|
-
|
|
11702
|
-
|
|
11703
|
-
}
|
|
11704
|
-
const
|
|
11989
|
+
const { data: arrayBuffer, cacheInfo } = await downloadResource(cameraUrl, {
|
|
11990
|
+
characterId: characterMeta.characterId ?? void 0,
|
|
11991
|
+
resourceType: "character"
|
|
11992
|
+
});
|
|
11993
|
+
const text = new TextDecoder().decode(arrayBuffer);
|
|
11994
|
+
const cameraSettings = JSON.parse(text);
|
|
11995
|
+
logEvent("resource_download", "info", {
|
|
11996
|
+
resource_type: "camera_settings",
|
|
11997
|
+
resource_name: "camera.json",
|
|
11998
|
+
cache_hit: cacheInfo.cacheHit,
|
|
11999
|
+
cache_type: cacheInfo.cacheType,
|
|
12000
|
+
pwa_cache_subtype: cacheInfo.pwaCacheSubtype,
|
|
12001
|
+
transfer_size: cacheInfo.transferSize,
|
|
12002
|
+
cdn_cache_status: cacheInfo.cdnCacheStatus
|
|
12003
|
+
});
|
|
11705
12004
|
logger.log("✅ Camera info loaded:", cameraSettings);
|
|
11706
12005
|
return cameraSettings;
|
|
11707
12006
|
} catch (error) {
|
|
@@ -11751,7 +12050,10 @@ class AvatarDownloader {
|
|
|
11751
12050
|
const downloadPromises = filesToLoad.map(async ({ key, url, filename, optional }) => {
|
|
11752
12051
|
updateProgress(filename, false);
|
|
11753
12052
|
try {
|
|
11754
|
-
const arrayBuffer = await downloadResource(url
|
|
12053
|
+
const { data: arrayBuffer, cacheInfo } = await downloadResource(url, {
|
|
12054
|
+
characterId: characterMeta.characterId ?? void 0,
|
|
12055
|
+
resourceType: "character"
|
|
12056
|
+
});
|
|
11755
12057
|
if (key === "shape") {
|
|
11756
12058
|
characterData.shape = arrayBuffer;
|
|
11757
12059
|
} else if (key === "pointCloud") {
|
|
@@ -11760,6 +12062,15 @@ class AvatarDownloader {
|
|
|
11760
12062
|
characterData.idleAnimation = arrayBuffer;
|
|
11761
12063
|
}
|
|
11762
12064
|
updateProgress(filename, true);
|
|
12065
|
+
logEvent("resource_download", "info", {
|
|
12066
|
+
resource_type: "character_asset",
|
|
12067
|
+
resource_name: filename,
|
|
12068
|
+
cache_hit: cacheInfo.cacheHit,
|
|
12069
|
+
cache_type: cacheInfo.cacheType,
|
|
12070
|
+
pwa_cache_subtype: cacheInfo.pwaCacheSubtype,
|
|
12071
|
+
transfer_size: cacheInfo.transferSize,
|
|
12072
|
+
cdn_cache_status: cacheInfo.cdnCacheStatus
|
|
12073
|
+
});
|
|
11763
12074
|
return { key, success: true, size: arrayBuffer.byteLength };
|
|
11764
12075
|
} catch (error) {
|
|
11765
12076
|
if (!optional) {
|
|
@@ -11955,6 +12266,8 @@ const _AvatarManager = class _AvatarManager {
|
|
|
11955
12266
|
} else {
|
|
11956
12267
|
logger.log(`[AvatarManager] Avatar ${id} version mismatch: cached=${cachedVersion}, new=${newVersion}, reloading...`);
|
|
11957
12268
|
this.avatarCache.delete(id);
|
|
12269
|
+
const { PwaCacheManager: PwaCacheManager2 } = await Promise.resolve().then(() => pwaCacheManager);
|
|
12270
|
+
await PwaCacheManager2.clearCharacterCache(id);
|
|
11958
12271
|
}
|
|
11959
12272
|
}
|
|
11960
12273
|
logger.log(`[AvatarManager] Queuing avatar download for id: ${id}`);
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare class PwaCacheManager {
|
|
2
|
+
private static readonly TEMPLATE_RESOURCE_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(): Promise<boolean>;
|
|
16
|
+
}
|
package/package.json
CHANGED