Ver código fonte

feat: devtools communication strategy (#1067)

* feat: integrate Tres DevTools messaging system

- Added a new `DevtoolsMessenger` class for handling communication with Tres DevTools, allowing for asset loading events and performance metrics to be sent to the devtools.
- Updated the `useLoader` composable to send asset loading progress and completion messages to the devtools.
- Implemented a subscription mechanism in the `gltf-loader` component to log asset load messages from the devtools.
- Enhanced the `setupTresDevtools` function to initialize the devtools messenger if it doesn't already exist on the window object, ensuring seamless integration with the Tres ecosystem.

* refactor: remove unused onMounted subscription in gltf-loader component playground

* fix: enhance asset loading progress reporting in useLoader composable

- Updated the `useLoader` function to improve the reporting of asset loading progress to Tres DevTools.
- Consolidated progress calculation and messaging logic, ensuring accurate tracking of loading status.
- Removed redundant progress event handling code for cleaner implementation.

* fix: simplify DevtoolsMessenger type reference in env.d.ts

- Updated the type reference for `__TRES__DEVTOOLS__` in the Window interface to directly use `DevtoolsMessenger`, improving clarity and reducing import complexity.

* chore: remove development type exports from package.json

- Eliminated the development type exports for `types`, `import`, and `default` in the package.json file, streamlining the export structure and focusing on the production-ready output.

* feat: add devtools export for enhanced integration

- Introduced export for `DevtoolsMessenger` in the devtools module, allowing for improved communication with Tres DevTools.
- Updated the main index file to include the new devtools export, streamlining the overall export structure.

* feat: enhance DevtoolsMessenger with message queuing and flushing

- Implemented message queuing in the `DevtoolsMessenger` class to handle scenarios where no subscribers are available, preventing message loss.
- Added a `flushQueue` method to deliver queued messages to new subscribers immediately upon subscription.
- Introduced a maximum queue size to prevent memory leaks, ensuring only the most recent messages are retained.
- Updated the `useLoader` composable to improve asset loading progress reporting, consolidating progress calculation and messaging logic for better clarity and accuracy.
- Added unit tests for `DevtoolsMessenger` to verify queuing behavior and message delivery.

* feat: refine message queuing in DevtoolsMessenger

- Updated the `DevtoolsMessenger` to queue only specific message types ('asset-load') when no subscribers are available, preventing unnecessary message accumulation.
- Enhanced unit tests to verify that non-queueable messages ('performance' and 'context') are not queued when no subscribers exist.
- Improved documentation within the code to clarify the queuing behavior and message handling logic.

* feat: enhance useLoader composable with total size tracking

- Added a `totalSize` variable to track the total size of assets being loaded, improving the accuracy of asset loading progress reporting.
- Updated the progress reporting logic to utilize the tracked `totalSize` instead of the previous method, ensuring more reliable completion event data.
- Enhanced inline comments for better understanding of the changes made to the loading progress logic.

* fix: update size reporting in useLoader composable

- Changed the `sizeKB` property to `size` in the `useLoader` composable to directly reflect the total size of the loaded asset, improving clarity in asset loading data.
- This adjustment enhances the accuracy of the asset loading progress reporting by providing the actual size instead of a converted value.

* feat: enhance asset load messaging in useLoader and DevtoolsMessenger

- Introduced a new `AssetLoadData` interface in `DevtoolsMessenger` to standardize asset loading messages sent to Tres DevTools.
- Updated the `useLoader` composable to send the actual loader constructor and improved the asset load message structure, enhancing clarity and consistency in reporting.
- Refined the `setupTresDevtools` function to utilize type annotations for context and performance messages, ensuring type safety and better integration with the devtools system.

* refactor: remove unused imports from setupDevtools.ts

* refactor: removed useLoader devtools communication since will be performed in a different way

* fix: add TODO comment for future implementation in AssetLoadData interface
Alvaro Saburido 2 semanas atrás
pai
commit
d8752bbdb3

+ 3 - 1
src/composables/useLoader/index.ts

@@ -82,7 +82,9 @@ export function useLoader<T, Shallow extends boolean = false>(
   const initialPath = toValue(path)
   const result = useAsyncState(
     (path?: string) => new Promise((resolve, reject) => {
-      proto.load(path || initialPath || '', (result: T) => {
+      const assetPath = path || initialPath || ''
+
+      proto.load(assetPath, (result: T) => {
         resolve(result as unknown as TresObject)
       }, (event: ProgressEvent<EventTarget>) => {
         progress.loaded = event.loaded

+ 94 - 0
src/devtools/DevtoolsMessenger.test.ts

@@ -0,0 +1,94 @@
+import { beforeEach, describe, expect, it } from 'vitest'
+import { DevtoolsMessenger } from './DevtoolsMessenger'
+
+describe('devtoolsMessenger', () => {
+  let messenger: DevtoolsMessenger
+
+  beforeEach(() => {
+    messenger = new DevtoolsMessenger()
+  })
+
+  it('should queue only queueable messages when no subscribers are available', () => {
+    // Send different types of messages without any subscribers
+    messenger.send('asset-load', { url: 'test.gltf', progress: 50 })
+    messenger.send('performance', { fps: 60 })
+    messenger.send('context', { scene: {} })
+    messenger.send('asset-load', { url: 'test.gltf', progress: 100 })
+
+    // Only asset-load messages should be queued
+    expect(messenger.queueSize).toBe(2)
+    expect(messenger.hasSubscribers).toBe(false)
+  })
+
+  it('should deliver queued messages when subscriber is added', () => {
+    const receivedMessages: any[] = []
+
+    // Send messages without subscribers
+    messenger.send('asset-load', { url: 'test.gltf', progress: 50 })
+    messenger.send('asset-load', { url: 'test.gltf', progress: 100 })
+
+    // Add subscriber - should receive queued messages immediately
+    const unsubscribe = messenger.subscribe((message) => {
+      receivedMessages.push(message)
+    })
+
+    // Should have received both queued messages
+    expect(receivedMessages).toHaveLength(2)
+    expect(receivedMessages[0].data.progress).toBe(50)
+    expect(receivedMessages[1].data.progress).toBe(100)
+    expect(messenger.queueSize).toBe(0) // Queue should be cleared
+
+    unsubscribe()
+  })
+
+  it('should send messages immediately when subscribers are available', () => {
+    const receivedMessages: any[] = []
+
+    // Add subscriber first
+    const unsubscribe = messenger.subscribe((message) => {
+      receivedMessages.push(message)
+    })
+
+    // Send messages - should be delivered immediately
+    messenger.send('asset-load', { url: 'test.gltf', progress: 50 })
+    messenger.send('asset-load', { url: 'test.gltf', progress: 100 })
+
+    expect(receivedMessages).toHaveLength(2)
+    expect(messenger.queueSize).toBe(0) // No queuing when subscribers exist
+
+    unsubscribe()
+  })
+
+  it('should limit queue size to prevent memory leaks', () => {
+    // Send more messages than the max queue size (100)
+    for (let i = 0; i < 105; i++) {
+      messenger.send('asset-load', { url: `test${i}.gltf`, progress: i })
+    }
+
+    // Should only keep the last 100 messages
+    expect(messenger.queueSize).toBe(100)
+    expect(messenger.hasSubscribers).toBe(false)
+  })
+
+  it('should not queue performance and context messages', () => {
+    // Send performance and context messages without subscribers
+    messenger.send('performance', { fps: 60 })
+    messenger.send('context', { scene: {} })
+    messenger.send('performance', { fps: 55 })
+    messenger.send('context', { scene: {} })
+
+    // These should not be queued
+    expect(messenger.queueSize).toBe(0)
+    expect(messenger.hasSubscribers).toBe(false)
+  })
+
+  it('should clear queue when requested', () => {
+    messenger.send('asset-load', { url: 'test.gltf', progress: 50 })
+    messenger.send('asset-load', { url: 'test.gltf', progress: 100 })
+
+    expect(messenger.queueSize).toBe(2)
+
+    messenger.clearQueue()
+    expect(messenger.queueSize).toBe(0)
+  })
+})

+ 122 - 0
src/devtools/DevtoolsMessenger.ts

@@ -0,0 +1,122 @@
+export type DevtoolsMessageType = 'performance' | 'context' | 'asset-load'
+
+// TODO: To be used in https://github.com/Tresjs/tres/issues/1080
+export interface AssetLoadData {
+  url: string
+  type: string
+  loaded: boolean
+  size: number
+  asset: HTMLImageElement | HTMLScriptElement | HTMLLinkElement | Blob | ArrayBuffer | null
+}
+
+// Message types that should be queued when no subscribers are available
+const QUEUEABLE_MESSAGE_TYPES: DevtoolsMessageType[] = ['asset-load']
+
+export interface DevtoolsMessage<T = any> {
+  type: DevtoolsMessageType
+  data: T
+  timestamp?: number
+}
+
+export type DevtoolsSubscriber = (message: DevtoolsMessage) => void
+
+/**
+ * Messenger class for communicating with Tres DevTools
+ * This class will be attached to window.__TRES__DEVTOOLS__
+ */
+export class DevtoolsMessenger {
+  private subscribers: Set<DevtoolsSubscriber> = new Set()
+  private messageQueue: DevtoolsMessage[] = []
+  private maxQueueSize = 100 // Prevent memory leaks by limiting queue size
+
+  /**
+   * Send a message to devtools subscribers
+   * If no subscribers are available, only queueable message types are queued
+   */
+  send<T>(type: DevtoolsMessageType, data: T): void {
+    const message: DevtoolsMessage<T> = {
+      type,
+      data,
+      timestamp: Date.now(),
+    }
+
+    // If there are subscribers, send immediately
+    if (this.subscribers.size > 0) {
+      this.subscribers.forEach(subscriber => subscriber(message))
+    }
+    else {
+      // Only queue specific message types (like asset-load)
+      // Performance and context messages are dropped when no subscribers
+      if (QUEUEABLE_MESSAGE_TYPES.includes(type)) {
+        this.queueMessage(message)
+      }
+    }
+  }
+
+  /**
+   * Queue a message for later delivery
+   */
+  private queueMessage(message: DevtoolsMessage): void {
+    // Add to queue
+    this.messageQueue.push(message)
+
+    // Prevent memory leaks by removing oldest messages if queue gets too large
+    if (this.messageQueue.length > this.maxQueueSize) {
+      this.messageQueue.shift()
+    }
+  }
+
+  /**
+   * Flush all queued messages to current subscribers
+   */
+  private flushQueue(): void {
+    if (this.messageQueue.length === 0 || this.subscribers.size === 0) {
+      return
+    }
+
+    // Send all queued messages to current subscribers
+    this.messageQueue.forEach((message) => {
+      this.subscribers.forEach(subscriber => subscriber(message))
+    })
+
+    // Clear the queue after sending
+    this.messageQueue = []
+  }
+
+  /**
+   * Subscribe to devtools messages
+   * When a new subscriber is added, all queued messages (asset-load events) are immediately delivered
+   */
+  subscribe(subscriber: DevtoolsSubscriber): () => void {
+    this.subscribers.add(subscriber)
+
+    // Flush any queued messages to the new subscriber
+    this.flushQueue()
+
+    // Return unsubscribe function
+    return () => {
+      this.subscribers.delete(subscriber)
+    }
+  }
+
+  /**
+   * Check if there are any subscribers
+   */
+  get hasSubscribers(): boolean {
+    return this.subscribers.size > 0
+  }
+
+  /**
+   * Get the current queue size
+   */
+  get queueSize(): number {
+    return this.messageQueue.length
+  }
+
+  /**
+   * Clear all queued messages
+   */
+  clearQueue(): void {
+    this.messageQueue = []
+  }
+}

+ 1 - 0
src/devtools/index.ts

@@ -1 +1,2 @@
+export * from './DevtoolsMessenger'
 export { registerTresDevtools } from './plugin'

+ 8 - 4
src/devtools/setupDevtools.ts

@@ -3,6 +3,7 @@ import { boundedPush, calculateMemoryUsage } from '../utils/perf'
 import type { TresContext } from '../composables'
 import type { TresObject } from '../types'
 import { onUnmounted } from 'vue'
+import { DevtoolsMessenger } from './DevtoolsMessenger'
 
 export interface PerformanceState {
   maxFrames: number
@@ -20,6 +21,11 @@ export interface PerformanceState {
 export function setupTresDevtools(ctx: TresContext) {
   if (!ctx) { return }
 
+  // Initialize devtools messenger
+  if (typeof window !== 'undefined' && !window.__TRES__DEVTOOLS__) {
+    window.__TRES__DEVTOOLS__ = new DevtoolsMessenger()
+  }
+
   const performanceState: PerformanceState = {
     maxFrames: 160,
     fps: {
@@ -82,10 +88,8 @@ export function setupTresDevtools(ctx: TresContext) {
 
     // Check if the accumulated time is greater than or equal to the interval
     if (accumulatedTime >= interval) {
-      window.__TRES__DEVTOOLS__.cb({
-        context: ctx,
-        performance: performanceState,
-      })
+      window.__TRES__DEVTOOLS__.send<TresContext>('context', ctx)
+      window.__TRES__DEVTOOLS__.send<PerformanceState>('performance', performanceState)
 
       // Reset the accumulated time
       accumulatedTime = 0

+ 1 - 7
src/env.d.ts

@@ -21,13 +21,7 @@ interface PerformanceState {
 }
 
 interface Window {
-  __TRES__DEVTOOLS__?: {
-    cb: (
-      { context, performance }:
-      { context: TresContext, performance: PerformanceState } // TODO https://github.com/Tresjs/nuxt/issues/163 this type should come from the devtools package
-    ) => void
-    // You can add other properties of __TRES__DEVTOOLS__ here if needed
-  }
+  __TRES__DEVTOOLS__?: DevtoolsMessenger
 }
 
 declare module '*.glsl' {}

+ 1 - 0
src/index.ts

@@ -7,6 +7,7 @@ import templateCompilerOptions from './utils/template-compiler-options'
 export * from './components'
 export * from './composables'
 export * from './core/catalogue'
+export * from './devtools'
 export * from './directives'
 export * from './types'
 export * from './utils/graph'