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>
This commit is contained in:
@@ -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<Item | undefined> {
|
||||
|
||||
@@ -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<string, Base>()
|
||||
}),
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,6 @@ describe('CacheReader testing', () => {
|
||||
const base = await objPromise
|
||||
|
||||
expect(base).toMatchSnapshot()
|
||||
cacheReader.dispose()
|
||||
await cacheReader.disposeAsync()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -77,7 +77,7 @@ export class CacheReader {
|
||||
this.#logger('readBatch: left, time', items.length, performance.now() - start)
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.#readQueue?.dispose()
|
||||
disposeAsync(): Promise<void> {
|
||||
return this.#readQueue?.disposeAsync() || Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,8 @@ export class CacheWriter implements Queue<Item> {
|
||||
}
|
||||
|
||||
async disposeAsync(): Promise<void> {
|
||||
this.#writeQueue?.dispose()
|
||||
this.#disposed = true
|
||||
return Promise.resolve()
|
||||
await this.#writeQueue?.disposeAsync()
|
||||
}
|
||||
|
||||
get isDisposed(): boolean {
|
||||
|
||||
@@ -121,8 +121,7 @@ export default class IndexedDatabase implements Database {
|
||||
async disposeAsync(): Promise<void> {
|
||||
this.#cacheDB?.close()
|
||||
this.#cacheDB = undefined
|
||||
this.#writeQueue?.dispose()
|
||||
await this.#writeQueue?.disposeAsync()
|
||||
this.#writeQueue = undefined
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { ObjectLoader2 } from './core/objectLoader2.js'
|
||||
export { ObjectLoader2Factory } from './core/objectLoader2Factory.js'
|
||||
export { getQueryParameter } from './types/functions.js'
|
||||
|
||||
@@ -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>) => void = () => {}
|
||||
const processPromise = new Promise<void>((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<string>({
|
||||
batchSize: 5,
|
||||
maxWaitTime: 1000,
|
||||
processFunction
|
||||
})
|
||||
|
||||
await queue.disposeAsync()
|
||||
queue.add('key1', 'item1')
|
||||
expect(queue.count()).toBe(0)
|
||||
expect(processFunction).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -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<string>
|
||||
|
||||
beforeEach(() => {
|
||||
queue = new BatchingQueue({
|
||||
batchSize: 3,
|
||||
maxWaitTime: 100,
|
||||
processFunction: async (): Promise<void> => {
|
||||
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<void> => {
|
||||
@@ -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<void> => {
|
||||
@@ -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<void> => {
|
||||
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<void> => {
|
||||
@@ -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<string>({
|
||||
batchSize: 3,
|
||||
maxWaitTime: 100,
|
||||
processFunction: async (): Promise<void> => {
|
||||
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<string>({
|
||||
batchSize: 3,
|
||||
maxWaitTime: 100,
|
||||
processFunction: async (): Promise<void> => {
|
||||
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<void> => {
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<T> {
|
||||
#queue: KeyedQueue<string, T> = new KeyedQueue<string, T>()
|
||||
#batchSize: number
|
||||
@@ -43,24 +49,41 @@ export default class BatchingQueue<T> {
|
||||
this.#logger = params.logger || ((): void => {})
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
async disposeAsync(): Promise<void> {
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -49,3 +49,22 @@ export function take<T>(it: Iterator<T>, 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
|
||||
}
|
||||
|
||||
@@ -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`)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user