(OL2) move files around to make more sense (#4950)

* Rename to saveBatch

* forgot a file

* first pass of cacheReader

* OL2 tests have infinite timeout

* OL2 refactor works

* fix for tests

* moved/removed types to make a more logical structure

* fixed imports

* rework loop to be in async generator for the expected count

* get rid of pumps and fix test

* lint fix

* redo mermaid diagrams

* add readme section on deferment

* always return root first

* fix linting

* revert the counting

* merge fixes

* remove unused var
This commit is contained in:
Adam Hathcock
2025-06-26 13:28:50 +01:00
committed by GitHub
parent c047ac7be1
commit c5967a9616
42 changed files with 215 additions and 142 deletions
@@ -1,4 +1,4 @@
import Queue from '../helpers/queue.js'
import Queue from '../queues/queue.js'
import { Item } from '../types/types.js'
export interface Downloader extends Queue<string> {
@@ -1,10 +1,10 @@
import { describe, expect, test } from 'vitest'
import { ObjectLoader2 } from './objectLoader2.js'
import { describe, test, expect } from 'vitest'
import { Base, Item } from '../types/types.js'
import { MemoryDownloader } from './downloaders/memoryDownloader.js'
import { ObjectLoader2 } from './objectLoader2.js'
import IndexedDatabase from './stages/indexedDatabase.js'
import { IDBFactory, IDBKeyRange } from 'fake-indexeddb'
import { MemoryDatabase } from './databases/memoryDatabase.js'
import IndexedDatabase from './databases/indexedDatabase.js'
import { MemoryDatabase } from './stages/memory/memoryDatabase.js'
import { MemoryDownloader } from './stages/memory/memoryDownloader.js'
describe('objectloader2', () => {
test('can get a root object from cache', async () => {
@@ -66,6 +66,9 @@ describe('objectloader2', () => {
const r = []
for await (const x of loader.getObjectIterator()) {
r.push(x)
if (r.length >= 1) {
break
}
}
await loader.disposeAsync()
@@ -101,6 +104,9 @@ describe('objectloader2', () => {
const obj = loader.getObject({ id: child1.baseId })
for await (const x of loader.getObjectIterator()) {
r.push(x)
if (r.length >= 2) {
break
}
}
await loader.disposeAsync()
@@ -139,6 +145,9 @@ describe('objectloader2', () => {
const obj = loader.getObject({ id: child1.baseId })
for await (const x of loader.getObjectIterator()) {
r.push(x)
if (r.length >= 2) {
break
}
}
await loader.disposeAsync()
@@ -1,12 +1,13 @@
import AsyncGeneratorQueue from '../helpers/asyncGeneratorQueue.js'
import { Downloader, Database } from './interfaces.js'
import { CustomLogger, Base, Item } from '../types/types.js'
import { CacheOptions, ObjectLoader2Options } from './options.js'
import { DefermentManager } from '../helpers/defermentManager.js'
import { CacheReader } from '../helpers/cacheReader.js'
import AggregateQueue from '../helpers/aggregateQueue.js'
import { DefermentManager } from '../deferment/defermentManager.js'
import AggregateQueue from '../queues/aggregateQueue.js'
import AsyncGeneratorQueue from '../queues/asyncGeneratorQueue.js'
import { CustomLogger } from '../types/functions.js'
import { Item, Base } from '../types/types.js'
import { Database, Downloader } from './interfaces.js'
import { ObjectLoader2Factory } from './objectLoader2Factory.js'
import { CacheWriter } from '../helpers/cacheWriter.js'
import { ObjectLoader2Options, CacheOptions } from './options.js'
import { CacheReader } from './stages/cacheReader.js'
import { CacheWriter } from './stages/cacheWriter.js'
export class ObjectLoader2 {
#rootId: string
@@ -52,8 +53,8 @@ export class ObjectLoader2 {
}
async disposeAsync(): Promise<void> {
this.#gathered.dispose()
await Promise.all([
this.#gathered.disposeAsync(),
this.#downloader.disposeAsync(),
this.#cacheReader.disposeAsync(),
this.#cacheWriter.disposeAsync()
@@ -93,7 +94,7 @@ export class ObjectLoader2 {
}
//sort the closures by their values descending
const sortedClosures = Object.entries(rootItem.base.__closure).sort(
const sortedClosures = Object.entries(rootItem.base.__closure ?? []).sort(
(a, b) => b[1] - a[1]
)
const children = sortedClosures.map((x) => x[0])
@@ -1,9 +1,10 @@
import { Base, CustomLogger } from '../types/types.js'
import IndexedDatabase from './databases/indexedDatabase.js'
import { MemoryDatabase } from './databases/memoryDatabase.js'
import { MemoryDownloader } from './downloaders/memoryDownloader.js'
import ServerDownloader from './downloaders/serverDownloader.js'
import { CustomLogger } from '../types/functions.js'
import { Base } from '../types/types.js'
import { ObjectLoader2 } from './objectLoader2.js'
import IndexedDatabase from './stages/indexedDatabase.js'
import { MemoryDatabase } from './stages/memory/memoryDatabase.js'
import { MemoryDownloader } from './stages/memory/memoryDownloader.js'
import ServerDownloader from './stages/serverDownloader.js'
export interface ObjectLoader2FactoryOptions {
useMemoryCache?: boolean
@@ -1,4 +1,5 @@
import { Base, CustomLogger } from '../types/types.js'
import { CustomLogger } from '../types/functions.js'
import { Base } from '../types/types.js'
import { Downloader, Database } from './interfaces.js'
export interface ObjectLoader2Options {
@@ -1,8 +1,8 @@
import { describe, expect, test } from 'vitest'
import { Base, Item } from '../types/types.js'
import { DefermentManager } from './defermentManager.js'
import { describe, test, expect } from 'vitest'
import { DefermentManager } from '../../deferment/defermentManager.js'
import { Item, Base } from '../../types/types.js'
import { CacheReader } from './cacheReader.js'
import { MemoryDatabase } from '../operations/databases/memoryDatabase.js'
import { MemoryDatabase } from './memory/memoryDatabase.js'
describe('CacheReader testing', () => {
test('deferred getObject', async () => {
@@ -1,9 +1,10 @@
import { Database } from '../operations/interfaces.js'
import { CacheOptions } from '../operations/options.js'
import { Base, CustomLogger, Item } from '../types/types.js'
import BatchingQueue from './batchingQueue.js'
import { DefermentManager } from './defermentManager.js'
import Queue from './queue.js'
import { DefermentManager } from '../../deferment/defermentManager.js'
import BatchingQueue from '../../queues/batchingQueue.js'
import Queue from '../../queues/queue.js'
import { CustomLogger } from '../../types/functions.js'
import { Item, Base } from '../../types/types.js'
import { Database } from '../interfaces.js'
import { CacheOptions } from '../options.js'
export class CacheReader {
#database: Database
@@ -0,0 +1,49 @@
import { DefermentManager } from '../../deferment/defermentManager.js'
import BatchingQueue from '../../queues/batchingQueue.js'
import Queue from '../../queues/queue.js'
import { CustomLogger } from '../../types/functions.js'
import { Item } from '../../types/types.js'
import { Database } from '../interfaces.js'
import { CacheOptions } from '../options.js'
export class CacheWriter implements Queue<Item> {
#writeQueue: BatchingQueue<Item> | undefined
#database: Database
#defermentManager: DefermentManager
#logger: CustomLogger
#options: CacheOptions
#disposed = false
constructor(
database: Database,
defermentManager: DefermentManager,
options: CacheOptions
) {
this.#database = database
this.#defermentManager = defermentManager
this.#options = options
this.#logger = options.logger || ((): void => {})
}
add(item: Item): void {
if (!this.#writeQueue) {
this.#writeQueue = new BatchingQueue({
batchSize: this.#options.maxCacheWriteSize,
maxWaitTime: this.#options.maxCacheBatchWriteWait,
processFunction: (batch: Item[]): Promise<void> =>
this.#database.saveBatch({ batch })
})
}
this.#defermentManager.undefer(item)
this.#writeQueue.add(item.baseId, item)
}
async disposeAsync(): Promise<void> {
await this.#writeQueue?.disposeAsync()
this.#disposed = true
}
get isDisposed(): boolean {
return this.#disposed
}
}
@@ -1,8 +1,8 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { IDBFactory, IDBKeyRange } from 'fake-indexeddb'
import IndexedDatabase, { IndexedDatabaseOptions } from './indexedDatabase.js'
import { Item, Base } from '../../types/types.js'
import { Base, Item } from '../../types/types.js'
import IndexedDatabase, { IndexedDatabaseOptions } from './indexedDatabase.js'
// Mock Item
const defaultItem = (id: string): Item => ({
@@ -1,9 +1,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
import BatchingQueue from '../../helpers/batchingQueue.js'
import { CustomLogger, Item } from '../../types/types.js'
import { CustomLogger } from '../../types/functions.js'
import { Item } from '../../types/types.js'
import { isSafari } from '@speckle/shared'
import { Dexie, DexieOptions, Table } from 'dexie'
import { Database } from '../interfaces.js'
import BatchingQueue from '../../queues/batchingQueue.js'
export class ObjectStore extends Dexie {
static #databaseName: string = 'speckle-cache'
@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { Item, Base } from '../../../types/types.js'
import { MemoryDatabase } from './memoryDatabase.js'
import { Base, Item } from '../../types/types.js'
const makeItem = (id: string, foo = 'bar'): Item => ({
baseId: id,
@@ -1,6 +1,6 @@
import { Base, Item } from '../../types/types.js'
import { Database } from '../interfaces.js'
import { MemoryDatabaseOptions } from '../options.js'
import { Base, Item } from '../../../types/types.js'
import { Database } from '../../interfaces.js'
import { MemoryDatabaseOptions } from '../../options.js'
export class MemoryDatabase implements Database {
private items: Map<string, Base>
@@ -1,8 +1,8 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { MemoryDownloader } from './memoryDownloader.js'
import { Base, Item } from '../../types/types.js'
import Queue from '../../helpers/queue.js'
import BufferQueue from '../../helpers/bufferQueue.js'
import BufferQueue from '../../../queues/bufferQueue.js'
import Queue from '../../../queues/queue.js'
import { Base, Item } from '../../../types/types.js'
const makeBase = (foo: string): Base => ({ foo } as unknown as Base)
@@ -1,6 +1,6 @@
import Queue from '../../helpers/queue.js'
import { Base, Item } from '../../types/types.js'
import { Downloader } from '../interfaces.js'
import Queue from '../../../queues/queue.js'
import { Base, Item } from '../../../types/types.js'
import { Downloader } from '../../interfaces.js'
export class MemoryDownloader implements Downloader {
#items: Map<string, Base>
@@ -3,7 +3,7 @@ import createFetchMock from 'vitest-fetch-mock'
import { vi } from 'vitest'
import { Item } from '../../types/types.js'
import ServerDownloader from './serverDownloader.js'
import AsyncGeneratorQueue from '../../helpers/asyncGeneratorQueue.js'
import AsyncGeneratorQueue from '../../queues/asyncGeneratorQueue.js'
describe('downloader', () => {
test('download batch of one', async () => {
@@ -24,13 +24,10 @@ describe('downloader', () => {
maxDownloadBatchWait: 200
})
downloader.add('id')
await downloader.disposeAsync()
const r = []
let count = 0
for await (const x of gathered.consume()) {
r.push(x)
count++
if (count >= 1) {
if (r.length >= 1) {
break
}
}
@@ -65,11 +62,9 @@ describe('downloader', () => {
downloader.add('id2')
await downloader.disposeAsync()
const r = []
let count = 0
for await (const x of gathered.consume()) {
r.push(x)
count++
if (count >= 2) {
if (r.length >= 2) {
break
}
}
@@ -112,11 +107,9 @@ describe('downloader', () => {
downloader.add('id3')
await downloader.disposeAsync()
const r = []
let count = 0
for await (const x of gathered.consume()) {
r.push(x)
count++
if (count >= 3) {
if (r.length >= 3) {
break
}
}
@@ -1,7 +1,8 @@
import BatchedPool from '../../helpers/batchedPool.js'
import Queue from '../../helpers/queue.js'
import BatchedPool from '../../queues/batchedPool.js'
import Queue from '../../queues/queue.js'
import { ObjectLoaderRuntimeError } from '../../types/errors.js'
import { Fetcher, isBase, Item, take } from '../../types/types.js'
import { Fetcher, isBase, take } from '../../types/functions.js'
import { Item } from '../../types/types.js'
import { Downloader } from '../interfaces.js'
export interface ServerDownloaderOptions {
@@ -20,6 +21,7 @@ export default class ServerDownloader implements Downloader {
#options: ServerDownloaderOptions
#fetch: Fetcher
#results?: Queue<Item>
#total?: number
#downloadQueue?: BatchedPool<string>
#decoder = new TextDecoder()
@@ -63,6 +65,7 @@ export default class ServerDownloader implements Downloader {
}): void {
const { results, total } = params
this.#results = results
this.#total = total
this.#downloadQueue = new BatchedPool<string>({
concurrencyAndSizes: this.#getDownloadCountAndSizes(total),
maxWaitTime: params.maxDownloadBatchWait,
@@ -90,20 +93,6 @@ export default class ServerDownloader implements Downloader {
await this.#downloadQueue?.disposeAsync()
}
#processJson(baseId: string, unparsedBase: string): Item {
let base: unknown
try {
base = JSON.parse(unparsedBase)
} catch (e: unknown) {
throw new Error(`Error parsing object ${baseId}: ${(e as Error).message}`)
}
if (isBase(base)) {
return { baseId, base }
} else {
throw new ObjectLoaderRuntimeError(`${baseId} is not a base`)
}
}
async downloadBatch(params: {
batch: string[]
url: string
@@ -142,6 +131,10 @@ export default class ServerDownloader implements Downloader {
'Items requested were not downloaded: ' + take(keys.values(), 10).join(',')
)
}
count += keys.size // count the leftovers
if (count >= this.#total!) {
await this.#results?.disposeAsync() // mark the queue as done
}
}
async processArray(
@@ -186,6 +179,20 @@ export default class ServerDownloader implements Downloader {
)
}
#processJson(baseId: string, unparsedBase: string): Item {
let base: unknown
try {
base = JSON.parse(unparsedBase)
} catch (e: unknown) {
throw new Error(`Error parsing object ${baseId}: ${(e as Error).message}`)
}
if (isBase(base)) {
return { baseId, base }
} else {
throw new ObjectLoaderRuntimeError(`${baseId} is not a base`)
}
}
concatUint8Arrays(a: Uint8Array, b: Uint8Array): Uint8Array {
const c = new Uint8Array(a.length + b.length)
c.set(a, 0)
@@ -1,7 +1,7 @@
import { describe, expect, test } from 'vitest'
import { Base } from '../types/types.js'
import { ObjectLoader2 } from './objectLoader2.js'
import Traverser from './traverser.js'
import { Base } from '../types/types.js'
describe('Traverser', () => {
test('root and two children with referenceId', async () => {
@@ -1,4 +1,5 @@
import { Base, DataChunk, isBase, isReference, isScalar } from '../types/types.js'
import { isScalar, isBase, isReference } from '../types/functions.js'
import { Base, DataChunk } from '../types/types.js'
import { ObjectLoader2 } from './objectLoader2.js'
export type ProgressStage = 'download' | 'construction'
@@ -1,6 +1,6 @@
import { describe, test, expect, beforeEach } from 'vitest'
import { DefermentManager } from './defermentManager.js'
import { DefermentManagerOptions } from '../operations/options.js'
import { DefermentManagerOptions } from '../core/options.js'
import { Base, Item } from '../types/types.js'
const makeItem = (id: string, size = 1): Item => ({
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest'
import { DefermentManager } from './defermentManager.js'
import { DefermentManagerOptions } from '../operations/options.js'
import { DefermentManagerOptions } from '../core/options.js'
import { Item } from '../types/types.js'
describe('DefermentManager disposal', () => {
@@ -1,6 +1,7 @@
import { DeferredBase } from './deferredBase.js'
import { Base, CustomLogger, Item } from '../types/types.js'
import { DefermentManagerOptions } from '../operations/options.js'
import { CustomLogger } from '../types/functions.js'
import { Item, Base } from '../types/types.js'
import { DefermentManagerOptions } from '../core/options.js'
export class DefermentManager {
private deferments: Map<string, DeferredBase> = new Map()
+2 -2
View File
@@ -1,2 +1,2 @@
export { ObjectLoader2 } from './operations/objectLoader2.js'
export { ObjectLoader2Factory } from './operations/objectLoader2Factory.js'
export { ObjectLoader2 } from './core/objectLoader2.js'
export { ObjectLoader2Factory } from './core/objectLoader2Factory.js'
@@ -8,13 +8,17 @@ export default class AggregateQueue<T> implements Queue<T> {
this.#queue1 = queue1
this.#queue2 = queue2
}
async disposeAsync(): Promise<void> {
await this.#queue1.disposeAsync()
await this.#queue2.disposeAsync()
}
add(value: T): void {
this.#queue1.add(value)
this.#queue2.add(value)
}
values(): T[] {
values(): never {
throw new Error('Not implemented')
}
}
@@ -29,7 +29,8 @@ export default class AsyncGeneratorQueue<T> implements Queue<T> {
}
}
}
dispose(): void {
disposeAsync(): Promise<void> {
this.#finished = true
return Promise.resolve()
}
}
@@ -9,4 +9,7 @@ export default class BufferQueue<T> implements Queue<T> {
values(): T[] {
return this.#buffer
}
disposeAsync(): Promise<void> {
return Promise.resolve()
}
}
@@ -1,15 +1,14 @@
import { Database } from '../operations/interfaces.js'
import { CacheOptions } from '../operations/options.js'
import { CustomLogger, Item } from '../types/types.js'
import { Database } from '../core/interfaces.js'
import { CacheOptions } from '../core/options.js'
import { DefermentManager } from '../deferment/defermentManager.js'
import { Item } from '../types/types.js'
import BatchingQueue from './batchingQueue.js'
import { DefermentManager } from './defermentManager.js'
import Queue from './queue.js'
export class CacheWriter implements Queue<Item> {
#writeQueue: BatchingQueue<Item> | undefined
#database: Database
#defermentManager: DefermentManager
#logger: CustomLogger
#options: CacheOptions
#disposed = false
@@ -21,7 +20,6 @@ export class CacheWriter implements Queue<Item> {
this.#database = database
this.#defermentManager = defermentManager
this.#options = options
this.#logger = options.logger || ((): void => {})
}
add(item: Item): void {
@@ -1,3 +1,4 @@
export default interface Queue<T> {
add(value: T): void
disposeAsync(): Promise<void>
}
+2 -2
View File
@@ -1,8 +1,8 @@
import { describe, test, expect } from 'vitest'
import { IDBFactory, IDBKeyRange } from 'fake-indexeddb'
import { Base } from '../types/types.js'
import { TIME_MS } from '@speckle/shared'
import { ObjectLoader2Factory } from '../operations/objectLoader2Factory.js'
import { ObjectLoader2Factory } from '../core/objectLoader2Factory.js'
import { Base } from '../types/types.js'
describe('e2e', () => {
test(
@@ -0,0 +1,51 @@
import { Base, Reference } from './types.js'
export type CustomLogger = (message?: string, ...optionalParams: unknown[]) => void
export type Fetcher = (
input: string | URL | Request,
init?: RequestInit
) => Promise<Response>
export function isBase(maybeBase?: unknown): maybeBase is Base {
return (
maybeBase !== null &&
typeof maybeBase === 'object' &&
'id' in maybeBase &&
typeof maybeBase.id === 'string'
)
}
export function isReference(maybeRef?: unknown): maybeRef is Reference {
return (
maybeRef !== null &&
typeof maybeRef === 'object' &&
'referencedId' in maybeRef &&
typeof maybeRef.referencedId === 'string'
)
}
export function isScalar(
value: unknown
): value is string | number | boolean | bigint | symbol | undefined {
const type = typeof value
return (
value === null ||
type === 'string' ||
type === 'number' ||
type === 'boolean' ||
type === 'bigint' ||
type === 'symbol' ||
type === 'undefined'
)
}
export function take<T>(it: Iterator<T>, count: number): T[] {
const result: T[] = []
for (let i = 0; i < count; i++) {
const itr = it.next()
if (itr.done) break
result.push(itr.value)
}
return result
}
-50
View File
@@ -1,10 +1,3 @@
export type CustomLogger = (message?: string, ...optionalParams: unknown[]) => void
export type Fetcher = (
input: string | URL | Request,
init?: RequestInit
) => Promise<Response>
export interface Item {
baseId: string
base?: Base
@@ -26,46 +19,3 @@ export interface Reference {
export interface DataChunk extends Base {
data?: Base[]
}
export function isBase(maybeBase?: unknown): maybeBase is Base {
return (
maybeBase !== null &&
typeof maybeBase === 'object' &&
'id' in maybeBase &&
typeof maybeBase.id === 'string'
)
}
export function isReference(maybeRef?: unknown): maybeRef is Reference {
return (
maybeRef !== null &&
typeof maybeRef === 'object' &&
'referencedId' in maybeRef &&
typeof maybeRef.referencedId === 'string'
)
}
export function isScalar(
value: unknown
): value is string | number | boolean | bigint | symbol | undefined {
const type = typeof value
return (
value === null ||
type === 'string' ||
type === 'number' ||
type === 'boolean' ||
type === 'bigint' ||
type === 'symbol' ||
type === 'undefined'
)
}
export function take<T>(it: Iterator<T>, count: number): T[] {
const result: T[] = []
for (let i = 0; i < count; i++) {
const itr = it.next()
if (itr.done) break
result.push(itr.value)
}
return result
}