From 477db6ef022bd9f684402ca737cec8da9a67f9fb Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Mon, 21 Jul 2025 12:01:27 +0100 Subject: [PATCH] All BatchedQueues should drain when disposed (also adds query string for output: "debug=true") (#5098) * ensure disposal is correct * add tests for disposal of batching queue * fixes for draining disposal * Update packages/objectloader2/src/queues/batchingQueue.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix comment * fix tests and build * add query string inspection of debug parameter * Update packages/objectloader2/src/queues/batchingQueue.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/objectloader2/src/core/objectLoader2Factory.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix test * fix AI * export getQueryParameter to avoid dup code. Sandbox uses it too * add tests for functions * prettier fix --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../objectloader2/src/core/objectLoader2.ts | 5 +- .../src/core/objectLoader2Factory.ts | 22 ++- .../src/core/stages/cacheReader.spec.ts | 2 +- .../src/core/stages/cacheReader.ts | 4 +- .../src/core/stages/cacheWriter.ts | 3 +- .../src/core/stages/indexedDatabase.ts | 3 +- packages/objectloader2/src/index.ts | 1 + .../src/queues/batchingQueue.dispose.test.ts | 74 ++++++++ .../src/queues/batchingQueue.test.ts | 159 +++++++++--------- .../objectloader2/src/queues/batchingQueue.ts | 25 ++- .../objectloader2/src/types/functions.spec.ts | 136 +++++++++++++++ packages/objectloader2/src/types/functions.ts | 19 +++ packages/viewer-sandbox/src/Sandbox.ts | 40 +++-- .../modules/loaders/Speckle/SpeckleLoader.ts | 32 +++- 14 files changed, 412 insertions(+), 113 deletions(-) create mode 100644 packages/objectloader2/src/queues/batchingQueue.dispose.test.ts create mode 100644 packages/objectloader2/src/types/functions.spec.ts diff --git a/packages/objectloader2/src/core/objectLoader2.ts b/packages/objectloader2/src/core/objectLoader2.ts index efccc3104..f2a817257 100644 --- a/packages/objectloader2/src/core/objectLoader2.ts +++ b/packages/objectloader2/src/core/objectLoader2.ts @@ -65,11 +65,10 @@ export class ObjectLoader2 { await Promise.all([ this.#gathered.disposeAsync(), this.#downloader.disposeAsync(), - this.#cacheWriter.disposeAsync() + this.#cacheWriter.disposeAsync(), + this.#cacheReader.disposeAsync() ]) this.#deferments.dispose() - this.#cacheReader.dispose() - this.#cache.dispose() } async getRootObject(): Promise { diff --git a/packages/objectloader2/src/core/objectLoader2Factory.ts b/packages/objectloader2/src/core/objectLoader2Factory.ts index 6f3fde824..0d5ab1ece 100644 --- a/packages/objectloader2/src/core/objectLoader2Factory.ts +++ b/packages/objectloader2/src/core/objectLoader2Factory.ts @@ -1,4 +1,4 @@ -import { CustomLogger } from '../types/functions.js' +import { CustomLogger, getQueryParameter } from '../types/functions.js' import { Base } from '../types/types.js' import { ObjectLoader2 } from './objectLoader2.js' import IndexedDatabase from './stages/indexedDatabase.js' @@ -11,7 +11,7 @@ export interface ObjectLoader2FactoryOptions { // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type keyRange?: { bound: Function; lowerBound: Function; upperBound: Function } indexedDB?: IDBFactory - logger?: CustomLogger + logger2?: CustomLogger } export class ObjectLoader2Factory { @@ -42,6 +42,7 @@ export class ObjectLoader2Factory { headers?: Headers options?: ObjectLoader2FactoryOptions }): ObjectLoader2 { + const log = ObjectLoader2Factory.getLogger(params.options?.logger2) let loader: ObjectLoader2 if (params.options?.useMemoryCache) { loader = new ObjectLoader2({ @@ -56,7 +57,7 @@ export class ObjectLoader2Factory { database: new MemoryDatabase({ items: new Map() }), - logger: params.options.logger + logger: log }) } else { loader = new ObjectLoader2({ @@ -69,13 +70,24 @@ export class ObjectLoader2Factory { headers: params.headers }), database: new IndexedDatabase({ - logger: params.options?.logger, + logger: log, indexedDB: params.options?.indexedDB, keyRange: params.options?.keyRange }), - logger: params.options?.logger + logger: log }) } return loader } + + static getLogger(providedLogger?: CustomLogger): CustomLogger | undefined { + if (getQueryParameter('debug', 'false') === 'true') { + return providedLogger || this.logger + } + return providedLogger + } + + static logger: CustomLogger = (m?: string, ...optionalParams: unknown[]) => { + console.log(`[debug] ${m}`, ...optionalParams) + } } diff --git a/packages/objectloader2/src/core/stages/cacheReader.spec.ts b/packages/objectloader2/src/core/stages/cacheReader.spec.ts index ec22a4351..2b1fbf9db 100644 --- a/packages/objectloader2/src/core/stages/cacheReader.spec.ts +++ b/packages/objectloader2/src/core/stages/cacheReader.spec.ts @@ -30,6 +30,6 @@ describe('CacheReader testing', () => { const base = await objPromise expect(base).toMatchSnapshot() - cacheReader.dispose() + await cacheReader.disposeAsync() }) }) diff --git a/packages/objectloader2/src/core/stages/cacheReader.ts b/packages/objectloader2/src/core/stages/cacheReader.ts index cf3c75448..1dfa892ef 100644 --- a/packages/objectloader2/src/core/stages/cacheReader.ts +++ b/packages/objectloader2/src/core/stages/cacheReader.ts @@ -77,7 +77,7 @@ export class CacheReader { this.#logger('readBatch: left, time', items.length, performance.now() - start) } - dispose(): void { - this.#readQueue?.dispose() + disposeAsync(): Promise { + return this.#readQueue?.disposeAsync() || Promise.resolve() } } diff --git a/packages/objectloader2/src/core/stages/cacheWriter.ts b/packages/objectloader2/src/core/stages/cacheWriter.ts index ed7277f65..445a829ec 100644 --- a/packages/objectloader2/src/core/stages/cacheWriter.ts +++ b/packages/objectloader2/src/core/stages/cacheWriter.ts @@ -38,9 +38,8 @@ export class CacheWriter implements Queue { } async disposeAsync(): Promise { - this.#writeQueue?.dispose() this.#disposed = true - return Promise.resolve() + await this.#writeQueue?.disposeAsync() } get isDisposed(): boolean { diff --git a/packages/objectloader2/src/core/stages/indexedDatabase.ts b/packages/objectloader2/src/core/stages/indexedDatabase.ts index ad437dd55..c10e60447 100644 --- a/packages/objectloader2/src/core/stages/indexedDatabase.ts +++ b/packages/objectloader2/src/core/stages/indexedDatabase.ts @@ -121,8 +121,7 @@ export default class IndexedDatabase implements Database { async disposeAsync(): Promise { this.#cacheDB?.close() this.#cacheDB = undefined - this.#writeQueue?.dispose() + await this.#writeQueue?.disposeAsync() this.#writeQueue = undefined - return Promise.resolve() } } diff --git a/packages/objectloader2/src/index.ts b/packages/objectloader2/src/index.ts index b6c0b2950..90da5df86 100644 --- a/packages/objectloader2/src/index.ts +++ b/packages/objectloader2/src/index.ts @@ -1,2 +1,3 @@ export { ObjectLoader2 } from './core/objectLoader2.js' export { ObjectLoader2Factory } from './core/objectLoader2Factory.js' +export { getQueryParameter } from './types/functions.js' diff --git a/packages/objectloader2/src/queues/batchingQueue.dispose.test.ts b/packages/objectloader2/src/queues/batchingQueue.dispose.test.ts new file mode 100644 index 000000000..f12baecae --- /dev/null +++ b/packages/objectloader2/src/queues/batchingQueue.dispose.test.ts @@ -0,0 +1,74 @@ +import { describe, test, expect, vi } from 'vitest' +import BatchingQueue from './batchingQueue.js' + +describe('BatchingQueue disposal', () => { + test('should drain the queue on dispose', async () => { + const processFunction = vi.fn().mockResolvedValue(undefined) + const queue = new BatchingQueue<{ id: string }>({ + batchSize: 5, + maxWaitTime: 1000, + processFunction + }) + + const items = Array.from({ length: 3 }, (_, i) => ({ id: `item-${i}` })) + items.forEach((item) => queue.add(item.id, item)) + + expect(queue.count()).toBe(3) + + await queue.disposeAsync() + + expect(processFunction).toHaveBeenCalledWith(items) + expect(queue.count()).toBe(0) + expect(queue.isDisposed()).toBe(true) + }) + + test('should wait for processing to finish before disposing', async () => { + let resolveProcess: (value: void | PromiseLike) => void = () => {} + const processPromise = new Promise((resolve) => { + resolveProcess = resolve + }) + + const processFunction = vi.fn().mockImplementation(() => processPromise) + + const queue = new BatchingQueue<{ id: string }>({ + batchSize: 2, + maxWaitTime: 100, + processFunction + }) + + const items1 = [{ id: 'item-1' }, { id: 'item-2' }] + items1.forEach((item) => queue.add(item.id, item)) + + // First batch is processing + expect(processFunction).toHaveBeenCalledWith(items1) + + const items2 = [{ id: 'item-3' }] + items2.forEach((item) => queue.add(item.id, item)) + + const disposePromise = queue.disposeAsync() + + // Queue should be disposed now, but processing is still ongoing + expect(queue.isDisposed()).toBe(true) + resolveProcess() + await disposePromise + + expect(processFunction).toHaveBeenCalledTimes(2) + expect(processFunction).toHaveBeenCalledWith(items2) + expect(queue.count()).toBe(0) + expect(queue.isDisposed()).toBe(true) + }) + + test('adding items after dispose should do nothing', async () => { + const processFunction = vi.fn().mockResolvedValue(undefined) + const queue = new BatchingQueue({ + batchSize: 5, + maxWaitTime: 1000, + processFunction + }) + + await queue.disposeAsync() + queue.add('key1', 'item1') + expect(queue.count()).toBe(0) + expect(processFunction).not.toHaveBeenCalled() + }) +}) diff --git a/packages/objectloader2/src/queues/batchingQueue.test.ts b/packages/objectloader2/src/queues/batchingQueue.test.ts index a1b39fef9..ff368ee93 100644 --- a/packages/objectloader2/src/queues/batchingQueue.test.ts +++ b/packages/objectloader2/src/queues/batchingQueue.test.ts @@ -1,26 +1,10 @@ -import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest' +import { describe, test, expect, vi } from 'vitest' import BatchingQueue from './batchingQueue.js' describe('BatchingQueue', () => { - let queue: BatchingQueue - - beforeEach(() => { - queue = new BatchingQueue({ - batchSize: 3, - maxWaitTime: 100, - processFunction: async (): Promise => { - await new Promise((resolve) => setTimeout(resolve, 0)) - } - }) - }) - - afterEach(() => { - queue.dispose() - }) - test('should add items and process them in batches', async () => { const processSpy = vi.fn() - queue = new BatchingQueue({ + const queue = new BatchingQueue({ batchSize: 2, maxWaitTime: 100, processFunction: async (batch: string[]): Promise => { @@ -29,18 +13,22 @@ describe('BatchingQueue', () => { } }) - queue.add('key1', 'item1') - queue.add('key2', 'item2') + try { + queue.add('key1', 'item1') + queue.add('key2', 'item2') - await new Promise((resolve) => setTimeout(resolve, 200)) + await new Promise((resolve) => setTimeout(resolve, 200)) - expect(processSpy).toHaveBeenCalledTimes(1) - expect(processSpy).toHaveBeenCalledWith(['item1', 'item2']) + expect(processSpy).toHaveBeenCalledTimes(1) + expect(processSpy).toHaveBeenCalledWith(['item1', 'item2']) + } finally { + await queue.disposeAsync() + } }) test('should process items after timeout if batch size is not reached', async () => { const processSpy = vi.fn() - queue = new BatchingQueue({ + const queue = new BatchingQueue({ batchSize: 5, maxWaitTime: 100, processFunction: async (batch: string[]): Promise => { @@ -49,37 +37,22 @@ describe('BatchingQueue', () => { } }) - queue.add('key1', 'item1') - queue.add('key2', 'item2') + try { + queue.add('key1', 'item1') + queue.add('key2', 'item2') - await new Promise((resolve) => setTimeout(resolve, 200)) + await new Promise((resolve) => setTimeout(resolve, 200)) - expect(processSpy).toHaveBeenCalledTimes(1) - expect(processSpy).toHaveBeenCalledWith(['item1', 'item2']) - }) - - test('should not process items if disposed', async () => { - const processSpy = vi.fn() - queue = new BatchingQueue({ - batchSize: 2, - maxWaitTime: 10000, - processFunction: async (batch: string[]): Promise => { - await new Promise((resolve) => setTimeout(resolve, 0)) - processSpy(batch) - } - }) - - queue.add('key1', 'item1') - queue.dispose() - - await new Promise((resolve) => setTimeout(resolve, 200)) - - expect(processSpy).not.toHaveBeenCalled() + expect(processSpy).toHaveBeenCalledTimes(1) + expect(processSpy).toHaveBeenCalledWith(['item1', 'item2']) + } finally { + await queue.disposeAsync() + } }) test('should handle multiple batches correctly', async () => { const processSpy = vi.fn() - queue = new BatchingQueue({ + const queue = new BatchingQueue({ batchSize: 2, maxWaitTime: 100, processFunction: async (batch: string[]): Promise => { @@ -88,39 +61,65 @@ describe('BatchingQueue', () => { } }) - queue.add('key1', 'item1') - queue.add('key2', 'item2') - queue.add('key3', 'item3') - queue.add('key4', 'item4') + try { + queue.add('key1', 'item1') + queue.add('key2', 'item2') + queue.add('key3', 'item3') + queue.add('key4', 'item4') - await new Promise((resolve) => setTimeout(resolve, 200)) + await new Promise((resolve) => setTimeout(resolve, 200)) - expect(processSpy).toHaveBeenCalledTimes(2) - expect(processSpy).toHaveBeenCalledWith(['item1', 'item2']) - expect(processSpy).toHaveBeenCalledWith(['item3', 'item4']) + expect(processSpy).toHaveBeenCalledTimes(2) + expect(processSpy).toHaveBeenCalledWith(['item1', 'item2']) + expect(processSpy).toHaveBeenCalledWith(['item3', 'item4']) + } finally { + await queue.disposeAsync() + } }) - test('should retrieve items by key', () => { - queue.add('key1', 'item1') - queue.add('key2', 'item2') + test('should retrieve items by key', async () => { + const queue = new BatchingQueue({ + batchSize: 3, + maxWaitTime: 100, + processFunction: async (): Promise => { + await new Promise((resolve) => setTimeout(resolve, 0)) + } + }) + try { + queue.add('key1', 'item1') + queue.add('key2', 'item2') - expect(queue.get('key1')).toBe('item1') - expect(queue.get('key2')).toBe('item2') - expect(queue.get('key3')).toBeUndefined() + expect(queue.get('key1')).toBe('item1') + expect(queue.get('key2')).toBe('item2') + expect(queue.get('key3')).toBeUndefined() + } finally { + await queue.disposeAsync() + } }) - test('should return correct count of items', () => { - expect(queue.count()).toBe(0) + test('should return correct count of items', async () => { + const queue = new BatchingQueue({ + batchSize: 3, + maxWaitTime: 100, + processFunction: async (): Promise => { + await new Promise((resolve) => setTimeout(resolve, 0)) + } + }) + try { + expect(queue.count()).toBe(0) - queue.add('key1', 'item1') - queue.add('key2', 'item2') + queue.add('key1', 'item1') + queue.add('key2', 'item2') - expect(queue.count()).toBe(2) + expect(queue.count()).toBe(2) + } finally { + await queue.disposeAsync() + } }) test('should not process items if already processing', async () => { const processSpy = vi.fn() - queue = new BatchingQueue({ + const queue = new BatchingQueue({ batchSize: 2, maxWaitTime: 100, processFunction: async (batch: string[]): Promise => { @@ -129,18 +128,22 @@ describe('BatchingQueue', () => { } }) - queue.add('key1', 'item1') - queue.add('key2', 'item2') - queue.add('key3', 'item3') + try { + queue.add('key1', 'item1') + queue.add('key2', 'item2') + queue.add('key3', 'item3') - await new Promise((resolve) => setTimeout(resolve, 200)) + await new Promise((resolve) => setTimeout(resolve, 200)) - expect(processSpy).toHaveBeenCalledTimes(1) - expect(processSpy).toHaveBeenCalledWith(['item1', 'item2']) + expect(processSpy).toHaveBeenCalledTimes(1) + expect(processSpy).toHaveBeenCalledWith(['item1', 'item2']) - await new Promise((resolve) => setTimeout(resolve, 200)) + await new Promise((resolve) => setTimeout(resolve, 200)) - expect(processSpy).toHaveBeenCalledTimes(2) - expect(processSpy).toHaveBeenCalledWith(['item3']) + expect(processSpy).toHaveBeenCalledTimes(2) + expect(processSpy).toHaveBeenCalledWith(['item3']) + } finally { + await queue.disposeAsync() + } }) }) diff --git a/packages/objectloader2/src/queues/batchingQueue.ts b/packages/objectloader2/src/queues/batchingQueue.ts index a28a96a45..98c938220 100644 --- a/packages/objectloader2/src/queues/batchingQueue.ts +++ b/packages/objectloader2/src/queues/batchingQueue.ts @@ -1,6 +1,12 @@ import { CustomLogger } from '../types/functions.js' import KeyedQueue from './keyedQueue.js' +/** + * Default wait time in milliseconds for processing ongoing tasks during disposal. + * This value was chosen to balance responsiveness and CPU usage in typical scenarios. + */ +const PROCESSING_WAIT_TIME_MS = 100 + export default class BatchingQueue { #queue: KeyedQueue = new KeyedQueue() #batchSize: number @@ -43,24 +49,41 @@ export default class BatchingQueue { this.#logger = params.logger || ((): void => {}) } - dispose(): void { + async disposeAsync(): Promise { this.#disposed = true if (this.#timeoutId) { this.#getClearTimeoutFn()(this.#timeoutId) + this.#timeoutId = null + } + + // Wait for any ongoing processing to finish + while (this.#isProcessing) { + await new Promise((resolve) => + this.#getSetTimeoutFn()(resolve, PROCESSING_WAIT_TIME_MS) + ) + } + + // After any ongoing flush is completed, there might be items in the queue. + // We should flush them. + if (this.#queue.size > 0) { + await this.#flush() } } add(key: string, item: T): void { + if (this.#disposed) return this.#queue.enqueue(key, item) this.#addCheck() } addAll(keys: string[], items: T[]): void { + if (this.#disposed) return this.#queue.enqueueAll(keys, items) this.#addCheck() } #addCheck(): void { + if (this.#disposed) return if (this.#queue.size >= this.#batchSize) { // Fire and forget, no need to await // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/packages/objectloader2/src/types/functions.spec.ts b/packages/objectloader2/src/types/functions.spec.ts new file mode 100644 index 000000000..3cd24eccb --- /dev/null +++ b/packages/objectloader2/src/types/functions.spec.ts @@ -0,0 +1,136 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { isBase, isReference, isScalar, take, getQueryParameter } from './functions.js' + +describe('isBase', () => { + it('should return true for valid Base objects', () => { + expect(isBase({ id: '123', speckle_type: 'Base' })).toBe(true) + }) + + it('should return false for objects without an id', () => { + expect(isBase({ speckle_type: 'Base' })).toBe(false) + }) + + it('should return false for objects with a non-string id', () => { + expect(isBase({ id: 123, speckle_type: 'Base' })).toBe(false) + }) + + it('should return false for null or undefined', () => { + expect(isBase(null)).toBe(false) + expect(isBase(undefined)).toBe(false) + }) + + it('should return false for non-objects', () => { + expect(isBase('a string')).toBe(false) + expect(isBase(123)).toBe(false) + }) +}) + +describe('isReference', () => { + it('should return true for valid Reference objects', () => { + expect(isReference({ referencedId: '456' })).toBe(true) + }) + + it('should return false for objects without a referencedId', () => { + expect(isReference({ id: '456' })).toBe(false) + }) + + it('should return false for objects with a non-string referencedId', () => { + expect(isReference({ referencedId: 456 })).toBe(false) + }) + + it('should return false for null or undefined', () => { + expect(isReference(null)).toBe(false) + expect(isReference(undefined)).toBe(false) + }) + + it('should return false for non-objects', () => { + expect(isReference('a string')).toBe(false) + expect(isReference(123)).toBe(false) + }) +}) + +describe('isScalar', () => { + it('should return true for scalar values', () => { + expect(isScalar('hello')).toBe(true) + expect(isScalar(123)).toBe(true) + expect(isScalar(true)).toBe(true) + expect(isScalar(BigInt(9007199254740991))).toBe(true) + expect(isScalar(Symbol('id'))).toBe(true) + expect(isScalar(undefined)).toBe(true) + expect(isScalar(null)).toBe(true) + }) + + it('should return false for non-scalar values', () => { + expect(isScalar({})).toBe(false) + expect(isScalar([])).toBe(false) + expect(isScalar(() => {})).toBe(false) + }) +}) + +describe('take', () => { + it('should take the specified number of items from an iterator', () => { + const arr = [1, 2, 3, 4, 5] + const iterator = arr[Symbol.iterator]() + expect(take(iterator, 3)).toEqual([1, 2, 3]) + }) + + it('should take all items if count is larger than the number of items', () => { + const arr = [1, 2] + const iterator = arr[Symbol.iterator]() + expect(take(iterator, 5)).toEqual([1, 2]) + }) + + it('should take no items if count is 0', () => { + const arr = [1, 2, 3] + const iterator = arr[Symbol.iterator]() + expect(take(iterator, 0)).toEqual([]) + }) + + it('should work with an empty iterator', () => { + const arr: number[] = [] + const iterator = arr[Symbol.iterator]() + expect(take(iterator, 3)).toEqual([]) + }) +}) + +describe('getQueryParameter', () => { + const defaultValue = 'default' + + describe('in a non-browser environment', () => { + it('should return the default value', () => { + expect(getQueryParameter('param', defaultValue)).toBe(defaultValue) + }) + }) + + describe('in a browser environment', () => { + const mockWindow = { + document: {}, + location: { + search: '' + } + } + + beforeEach(() => { + vi.stubGlobal('window', mockWindow) + }) + + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('should return the parameter value from the URL', () => { + mockWindow.location.search = '?param=value' + expect(getQueryParameter('param', defaultValue)).toBe('value') + }) + + it('should return the default value if the parameter is not in the URL', () => { + mockWindow.location.search = '?otherparam=value' + expect(getQueryParameter('param', defaultValue)).toBe(defaultValue) + }) + + it('should return the default value if the URL has no query string', () => { + mockWindow.location.search = '' + expect(getQueryParameter('param', defaultValue)).toBe(defaultValue) + }) + }) +}) diff --git a/packages/objectloader2/src/types/functions.ts b/packages/objectloader2/src/types/functions.ts index 4a44df982..1c09e0a3a 100644 --- a/packages/objectloader2/src/types/functions.ts +++ b/packages/objectloader2/src/types/functions.ts @@ -49,3 +49,22 @@ export function take(it: Iterator, count: number): T[] { } return result } + +export function getQueryParameter(paramName: string, defaultValue: string): string { + // Check if the code is running in a browser environment 🌐 + const isBrowser = + typeof window !== 'undefined' && typeof window.document !== 'undefined' + + if (!isBrowser) { + // If in Node.js or another server environment, return the default + return defaultValue + } + + // In a browser, parse the query string + const params = new URLSearchParams(window.location.search) + + // .get() returns the value, or null if it's not found. + // The nullish coalescing operator (??) provides the default value + // if the left-hand side is null or undefined. + return params.get(paramName) ?? defaultValue +} diff --git a/packages/viewer-sandbox/src/Sandbox.ts b/packages/viewer-sandbox/src/Sandbox.ts index 29cf561df..268b43b1b 100644 --- a/packages/viewer-sandbox/src/Sandbox.ts +++ b/packages/viewer-sandbox/src/Sandbox.ts @@ -54,7 +54,7 @@ import Bright from '../assets/hdri/Bright.png' import { Euler, Vector3, Box3, LinearFilter } from 'three' import { GeometryType } from '@speckle/viewer' import { MeshBatch } from '@speckle/viewer' -import { ObjectLoader2Factory } from '@speckle/objectloader2' +import { getQueryParameter, ObjectLoader2Factory } from '@speckle/objectloader2' export default class Sandbox { private viewer: Viewer @@ -1289,8 +1289,7 @@ export default class Sandbox { objUrl, authToken, true, - undefined, - (m?: string, ...args: unknown[]) => console.log(m, ...args) + undefined ) let dataProgress = 0 let renderedCount = 0 @@ -1302,25 +1301,30 @@ export default class Sandbox { if (colorImage) colorImage.style.clipPath = `inset(${(1 - arg.progress) * 100}% 0 0 0)` dataProgress = p - console.log(`Loading ${p}%`) - } - }) - loader.on(LoaderEvent.Traversed, (arg: { count: number }) => { - if (arg.count > traversedCount) { - traversedCount = arg.count - if (traversedCount % 500 === 0) { - console.log(`Traversed ${traversedCount}`) + + if (getQueryParameter('debug', 'false') !== 'true') { + console.log(`Loading ${p}%`) } } }) - loader.on(LoaderEvent.Converted, (arg: { count: number }) => { - if (arg.count > renderedCount) { - renderedCount = arg.count - if (renderedCount % 500 === 0) { - console.log(`Converting Data ${renderedCount}`) + if (getQueryParameter('debug', 'false') !== 'true') { + loader.on(LoaderEvent.Traversed, (arg: { count: number }) => { + if (arg.count > traversedCount) { + traversedCount = arg.count + if (traversedCount % 500 === 0) { + console.log(`Traversed ${traversedCount}`) + } } - } - }) + }) + loader.on(LoaderEvent.Converted, (arg: { count: number }) => { + if (arg.count > renderedCount) { + renderedCount = arg.count + if (renderedCount % 500 === 0) { + console.log(`Converting Data ${renderedCount}`) + } + } + }) + } loader.on(LoaderEvent.LoadCancelled, (resource: string) => { console.warn(`Resource ${resource} loading was canceled`) }) diff --git a/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts b/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts index e9d214d24..049811c20 100644 --- a/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts +++ b/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts @@ -3,7 +3,11 @@ import { Loader, LoaderEvent } from '../Loader.js' import { SpeckleGeometryConverter } from './SpeckleGeometryConverter.js' import { WorldTree, type SpeckleObject } from '../../../index.js' import Logger from '../../utils/Logger.js' -import { ObjectLoader2, ObjectLoader2Factory } from '@speckle/objectloader2' +import { + getQueryParameter, + ObjectLoader2, + ObjectLoader2Factory +} from '@speckle/objectloader2' import { TIME_MS } from '@speckle/shared' export class SpeckleLoader extends Loader { @@ -99,6 +103,7 @@ export class SpeckleLoader extends Loader { const total = await this.loader.getTotalObjectCount() let traversals = 0 let firstObjectPromise = null + this.progressListen() Logger.warn('Downloading object ', this.resource) @@ -186,6 +191,31 @@ export class SpeckleLoader extends Loader { return p } + private progressListen(): void { + if (getQueryParameter('debug', 'false') !== 'true') { + return + } + + let dataProgress = 0 + this.on(LoaderEvent.LoadProgress, (data) => { + const p = Math.floor(data.progress * 100) + if (p > dataProgress) { + Logger.log(`[debug] Loading ${p}%`) + dataProgress = p + } + }) + this.on(LoaderEvent.Traversed, (data) => { + if (data.count % 500 === 0) { + Logger.log(`[debug] Traversed ${data.count}`) + } + }) + this.on(LoaderEvent.Converted, (data) => { + if (data.count % 500 === 0) { + Logger.log(`[debug] Converted ${data.count}`) + } + }) + } + cancel() { this.isCancelled = true this.isFinished = false