@speckle/objectloader2 2.24.2 → 2.25.1

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.
Files changed (42) hide show
  1. package/dist/commonjs/index.js +6 -7
  2. package/dist/esm/index.js +3 -3
  3. package/eslint.config.mjs +3 -1
  4. package/package.json +2 -2
  5. package/src/helpers/__snapshots__/cachePump.spec.ts.snap +31 -0
  6. package/src/helpers/__snapshots__/cacheReader.spec.ts.snap +8 -0
  7. package/src/helpers/aggregateQueue.ts +20 -0
  8. package/src/helpers/batchedPool.ts +5 -9
  9. package/src/helpers/batchingQueue.ts +21 -13
  10. package/src/helpers/cachePump.disposal.spec.ts +49 -0
  11. package/src/helpers/cachePump.spec.ts +103 -0
  12. package/src/helpers/cachePump.ts +99 -0
  13. package/src/helpers/cacheReader.spec.ts +35 -0
  14. package/src/helpers/cacheReader.ts +64 -0
  15. package/src/helpers/defermentManager.disposal.spec.ts +28 -0
  16. package/src/helpers/defermentManager.spec.ts +25 -1
  17. package/src/helpers/defermentManager.ts +128 -12
  18. package/src/helpers/deferredBase.ts +44 -6
  19. package/src/helpers/keyedQueue.ts +45 -0
  20. package/src/helpers/memoryPump.ts +40 -0
  21. package/src/helpers/pump.ts +8 -0
  22. package/src/index.ts +3 -4
  23. package/src/operations/__snapshots__/objectLoader2.spec.ts.snap +16 -16
  24. package/src/operations/{__snapshots__ → databases/__snapshots__}/indexedDatabase.spec.ts.snap +0 -21
  25. package/src/operations/{indexedDatabase.spec.ts → databases/indexedDatabase.spec.ts} +2 -28
  26. package/src/operations/databases/indexedDatabase.ts +150 -0
  27. package/src/operations/databases/memoryDatabase.ts +43 -0
  28. package/src/operations/{__snapshots__ → downloaders/__snapshots__}/serverDownloader.spec.ts.snap +34 -0
  29. package/src/operations/{memoryDownloader.ts → downloaders/memoryDownloader.ts} +15 -14
  30. package/src/operations/{serverDownloader.spec.ts → downloaders/serverDownloader.spec.ts} +68 -43
  31. package/src/operations/{serverDownloader.ts → downloaders/serverDownloader.ts} +92 -38
  32. package/src/operations/interfaces.ts +11 -12
  33. package/src/operations/objectLoader2.spec.ts +76 -144
  34. package/src/operations/objectLoader2.ts +57 -79
  35. package/src/operations/objectLoader2Factory.ts +56 -0
  36. package/src/operations/options.ts +18 -37
  37. package/src/operations/traverser.spec.ts +1 -1
  38. package/src/operations/traverser.ts +1 -1
  39. package/src/test/e2e.spec.ts +4 -4
  40. package/src/types/types.ts +11 -0
  41. package/src/operations/indexedDatabase.ts +0 -167
  42. package/src/operations/memoryDatabase.ts +0 -42
@@ -1,11 +1,10 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.MemoryDatabase = void 0;
7
- const objectLoader2_js_1 = __importDefault(require("./operations/objectLoader2.js"));
8
- exports.default = objectLoader2_js_1.default;
9
- var memoryDatabase_js_1 = require("./operations/memoryDatabase.js");
3
+ exports.ObjectLoader2Factory = exports.ObjectLoader2 = exports.MemoryDatabase = void 0;
4
+ var memoryDatabase_js_1 = require("./operations/databases/memoryDatabase.js");
10
5
  Object.defineProperty(exports, "MemoryDatabase", { enumerable: true, get: function () { return memoryDatabase_js_1.MemoryDatabase; } });
6
+ var objectLoader2_js_1 = require("./operations/objectLoader2.js");
7
+ Object.defineProperty(exports, "ObjectLoader2", { enumerable: true, get: function () { return objectLoader2_js_1.ObjectLoader2; } });
8
+ var objectLoader2Factory_js_1 = require("./operations/objectLoader2Factory.js");
9
+ Object.defineProperty(exports, "ObjectLoader2Factory", { enumerable: true, get: function () { return objectLoader2Factory_js_1.ObjectLoader2Factory; } });
11
10
  //# sourceMappingURL=index.js.map
package/dist/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import ObjectLoader2 from './operations/objectLoader2.js';
2
- export default ObjectLoader2;
3
- export { MemoryDatabase } from './operations/memoryDatabase.js';
1
+ export { MemoryDatabase } from './operations/databases/memoryDatabase.js';
2
+ export { ObjectLoader2 } from './operations/objectLoader2.js';
3
+ export { ObjectLoader2Factory } from './operations/objectLoader2Factory.js';
4
4
  //# sourceMappingURL=index.js.map
package/eslint.config.mjs CHANGED
@@ -29,7 +29,9 @@ const configs = [
29
29
  }
30
30
  },
31
31
  rules: {
32
- '@typescript-eslint/restrict-template-expressions': 'off'
32
+ '@typescript-eslint/restrict-template-expressions': 'off',
33
+ '@typescript-eslint/await-thenable': 'error',
34
+ '@typescript-eslint/explicit-function-return-type': 'error'
33
35
  }
34
36
  },
35
37
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@speckle/objectloader2",
3
- "version": "2.24.2",
3
+ "version": "2.25.1",
4
4
  "description": "This is an updated objectloader for the Speckle viewer written in typescript",
5
5
  "main": "./dist/commonjs/index.js",
6
6
  "module": "./dist/esm/index.js",
@@ -33,7 +33,7 @@
33
33
  "author": "AEC Systems",
34
34
  "license": "Apache-2.0",
35
35
  "dependencies": {
36
- "@speckle/shared": "^2.24.2",
36
+ "@speckle/shared": "^2.25.1",
37
37
  "dexie": "^4.0.11"
38
38
  },
39
39
  "devDependencies": {
@@ -0,0 +1,31 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`CachePump testing > write two items to queue use pumpItems that are NOT found 1`] = `[]`;
4
+
5
+ exports[`CachePump testing > write two items to queue use pumpItems that are NOT found 2`] = `
6
+ [
7
+ "id1",
8
+ "id2",
9
+ ]
10
+ `;
11
+
12
+ exports[`CachePump testing > write two items to queue use pumpItems that are found 1`] = `
13
+ [
14
+ {
15
+ "base": {
16
+ "id": "id",
17
+ "speckle_type": "type",
18
+ },
19
+ "baseId": "id1",
20
+ },
21
+ {
22
+ "base": {
23
+ "id": "id",
24
+ "speckle_type": "type",
25
+ },
26
+ "baseId": "id2",
27
+ },
28
+ ]
29
+ `;
30
+
31
+ exports[`CachePump testing > write two items to queue use pumpItems that are found 2`] = `[]`;
@@ -0,0 +1,8 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`CacheReader testing > deferred getObject 1`] = `
4
+ {
5
+ "id": "id",
6
+ "speckle_type": "type",
7
+ }
8
+ `;
@@ -0,0 +1,20 @@
1
+ import Queue from './queue.js'
2
+
3
+ export default class AggregateQueue<T> implements Queue<T> {
4
+ #queue1: Queue<T>
5
+ #queue2: Queue<T>
6
+
7
+ constructor(queue1: Queue<T>, queue2: Queue<T>) {
8
+ this.#queue1 = queue1
9
+ this.#queue2 = queue2
10
+ }
11
+
12
+ add(value: T): void {
13
+ this.#queue1.add(value)
14
+ this.#queue2.add(value)
15
+ }
16
+
17
+ values(): T[] {
18
+ throw new Error('Not implemented')
19
+ }
20
+ }
@@ -6,7 +6,7 @@ export default class BatchedPool<T> {
6
6
  #baseInterval: number
7
7
 
8
8
  #processingLoop: Promise<void>
9
- #finished = false
9
+ #disposed = false
10
10
 
11
11
  constructor(params: {
12
12
  concurrencyAndSizes: number[]
@@ -27,22 +27,18 @@ export default class BatchedPool<T> {
27
27
  return this.#queue.splice(0, Math.min(batchSize, this.#queue.length))
28
28
  }
29
29
 
30
- async #runWorker(batchSize: number) {
31
- while (!this.#finished || this.#queue.length > 0) {
30
+ async #runWorker(batchSize: number): Promise<void> {
31
+ while (!this.#disposed || this.#queue.length > 0) {
32
32
  if (this.#queue.length > 0) {
33
33
  const batch = this.getBatch(batchSize)
34
- try {
35
- await this.#processFunction(batch)
36
- } catch (e) {
37
- console.error(e)
38
- }
34
+ await this.#processFunction(batch)
39
35
  }
40
36
  await this.#delay(this.#baseInterval)
41
37
  }
42
38
  }
43
39
 
44
40
  async disposeAsync(): Promise<void> {
45
- this.#finished = true
41
+ this.#disposed = true
46
42
  await this.#processingLoop
47
43
  }
48
44
 
@@ -1,7 +1,7 @@
1
- import Queue from './queue.js'
1
+ import KeyedQueue from './keyedQueue.js'
2
2
 
3
- export default class BatchingQueue<T> implements Queue<T> {
4
- #queue: T[] = []
3
+ export default class BatchingQueue<T> {
4
+ #queue: KeyedQueue<string, T> = new KeyedQueue<string, T>()
5
5
  #batchSize: number
6
6
  #processFunction: (batch: T[]) => Promise<void>
7
7
 
@@ -10,7 +10,7 @@ export default class BatchingQueue<T> implements Queue<T> {
10
10
  #maxInterval: number
11
11
 
12
12
  #processingLoop: Promise<void>
13
- #finished = false
13
+ #disposed = false
14
14
 
15
15
  constructor(params: {
16
16
  batchSize: number
@@ -26,32 +26,40 @@ export default class BatchingQueue<T> implements Queue<T> {
26
26
  }
27
27
 
28
28
  async disposeAsync(): Promise<void> {
29
- this.#finished = true
29
+ this.#disposed = true
30
30
  await this.#processingLoop
31
31
  }
32
32
 
33
- add(item: T): void {
34
- this.#queue.push(item)
33
+ add(key: string, item: T): void {
34
+ this.#queue.enqueue(key, item)
35
+ }
36
+
37
+ get(id: string): T | undefined {
38
+ return this.#queue.get(id)
35
39
  }
36
40
 
37
41
  count(): number {
38
- return this.#queue.length
42
+ return this.#queue.size
43
+ }
44
+
45
+ isDisposed(): boolean {
46
+ return this.#disposed
39
47
  }
40
48
 
41
49
  #getBatch(batchSize: number): T[] {
42
- return this.#queue.splice(0, Math.min(batchSize, this.#queue.length))
50
+ return this.#queue.spliceValues(0, Math.min(batchSize, this.#queue.size))
43
51
  }
44
52
 
45
53
  async #loop(): Promise<void> {
46
54
  let interval = this.#baseInterval
47
- while (!this.#finished || this.#queue.length > 0) {
55
+ while (!this.#disposed || this.#queue.size > 0) {
48
56
  const startTime = performance.now()
49
- if (this.#queue.length > 0) {
57
+ if (this.#queue.size > 0) {
50
58
  const batch = this.#getBatch(this.#batchSize)
51
59
  //console.log('running with queue size of ' + this.#queue.length)
52
60
  await this.#processFunction(batch)
53
61
  }
54
- if (this.#queue.length < this.#batchSize / 2) {
62
+ if (this.#queue.size < this.#batchSize / 2) {
55
63
  //refigure interval
56
64
  const endTime = performance.now()
57
65
  const duration = endTime - startTime
@@ -62,7 +70,7 @@ export default class BatchingQueue<T> implements Queue<T> {
62
70
  }
63
71
  /*console.log(
64
72
  'queue is waiting ' +
65
- interval / TIME_MS.second +
73
+ interval / 1000 +
66
74
  ' with queue size of ' +
67
75
  this.#queue.length
68
76
  )*/
@@ -0,0 +1,49 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { CachePump } from './cachePump.js'
3
+ import { Database } from '../operations/interfaces.js'
4
+ import AsyncGeneratorQueue from './asyncGeneratorQueue.js'
5
+ import { Item } from '../types/types.js'
6
+ import { DefermentManager } from './defermentManager.js'
7
+
8
+ const makeDatabase = (): Database =>
9
+ ({
10
+ cacheSaveBatch: async (): Promise<void> => {},
11
+ getAll: async (): Promise<(Item | undefined)[]> => Promise.resolve([])
12
+ } as unknown as Database)
13
+ const makeGathered = (): AsyncGeneratorQueue<Item> =>
14
+ ({
15
+ add: () => {},
16
+ async *consume() {}
17
+ } as unknown as AsyncGeneratorQueue<Item>)
18
+ const makeDeferments = (): DefermentManager =>
19
+ ({
20
+ undefer: () => {}
21
+ } as unknown as DefermentManager)
22
+ describe('CachePump disposal', () => {
23
+ test('disposeAsync is idempotent and always resolves', async () => {
24
+ const pump = new CachePump(makeDatabase(), makeGathered(), makeDeferments(), {
25
+ maxCacheWriteSize: 2,
26
+ maxCacheBatchWriteWait: 100,
27
+ maxCacheBatchReadWait: 1,
28
+ maxWriteQueueSize: 2,
29
+ maxCacheReadSize: 2
30
+ })
31
+ await pump.disposeAsync()
32
+ await expect(pump.disposeAsync()).resolves.toBeUndefined()
33
+ })
34
+
35
+ test('should not throw on add after dispose if writeQueue was never created', async () => {
36
+ const pump = new CachePump(makeDatabase(), makeGathered(), makeDeferments(), {
37
+ maxCacheWriteSize: 2,
38
+ maxCacheBatchWriteWait: 100,
39
+ maxCacheBatchReadWait: 1,
40
+ maxWriteQueueSize: 2,
41
+ maxCacheReadSize: 2
42
+ })
43
+ await pump.disposeAsync()
44
+ // Should not throw, but will not add anything
45
+ expect(() =>
46
+ pump.add({ baseId: 'a', base: { id: 'b', speckle_type: 'type' } })
47
+ ).not.toThrow()
48
+ })
49
+ })
@@ -0,0 +1,103 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { CachePump } from './cachePump.js'
3
+ import { Base, Item } from '../types/types.js'
4
+ import BufferQueue from './bufferQueue.js'
5
+ import AsyncGeneratorQueue from './asyncGeneratorQueue.js'
6
+ import { DefermentManager } from './defermentManager.js'
7
+ import { MemoryDatabase } from '../operations/databases/memoryDatabase.js'
8
+ import { Database } from '../operations/interfaces.js'
9
+
10
+ describe('CachePump testing', () => {
11
+ test('write two items to queue use pumpItems that are NOT found', async () => {
12
+ const i1: Item = { baseId: 'id1', base: { id: 'id', speckle_type: 'type' } }
13
+ const i2: Item = { baseId: 'id2', base: { id: 'id', speckle_type: 'type' } }
14
+
15
+ const gathered = new AsyncGeneratorQueue<Item>()
16
+ const deferments = new DefermentManager({ maxSizeInMb: 1, ttlms: 1 })
17
+ const cachePump = new CachePump(new MemoryDatabase({}), gathered, deferments, {
18
+ maxCacheReadSize: 1,
19
+ maxCacheWriteSize: 1,
20
+ maxCacheBatchWriteWait: 1,
21
+ maxCacheBatchReadWait: 1,
22
+ maxWriteQueueSize: 1
23
+ })
24
+
25
+ const foundItems = new BufferQueue<Item>()
26
+ const notFoundItems = new BufferQueue<string>()
27
+
28
+ await cachePump.pumpItems({
29
+ ids: [i1.baseId, i2.baseId],
30
+ foundItems,
31
+ notFoundItems
32
+ })
33
+
34
+ expect(foundItems.values()).toMatchSnapshot()
35
+ expect(notFoundItems.values()).toMatchSnapshot()
36
+ await cachePump.disposeAsync()
37
+ })
38
+
39
+ test('write two items to queue use pumpItems that are found', async () => {
40
+ const i1: Item = { baseId: 'id1', base: { id: 'id', speckle_type: 'type' } }
41
+ const i2: Item = { baseId: 'id2', base: { id: 'id', speckle_type: 'type' } }
42
+
43
+ const db = new Map<string, Base>()
44
+ db.set(i1.baseId, i1.base)
45
+ db.set(i2.baseId, i2.base)
46
+
47
+ const gathered = new AsyncGeneratorQueue<Item>()
48
+ const deferments = new DefermentManager({ maxSizeInMb: 1, ttlms: 1 })
49
+ const cachePump = new CachePump(
50
+ new MemoryDatabase({ items: db }),
51
+ gathered,
52
+ deferments,
53
+ {
54
+ maxCacheReadSize: 1,
55
+ maxCacheWriteSize: 1,
56
+ maxCacheBatchWriteWait: 1,
57
+ maxCacheBatchReadWait: 1,
58
+ maxWriteQueueSize: 1
59
+ }
60
+ )
61
+
62
+ const foundItems = new BufferQueue<Item>()
63
+ const notFoundItems = new BufferQueue<string>()
64
+
65
+ await cachePump.pumpItems({
66
+ ids: [i1.baseId, i2.baseId],
67
+ foundItems,
68
+ notFoundItems
69
+ })
70
+
71
+ expect(foundItems.values()).toMatchSnapshot()
72
+ expect(notFoundItems.values()).toMatchSnapshot()
73
+ await cachePump.disposeAsync()
74
+ })
75
+
76
+ test('can dispose while waiting and not wait', async () => {
77
+ const i1: Item = { baseId: 'id1', base: { id: 'id', speckle_type: 'type' } }
78
+ const i2: Item = { baseId: 'id2', base: { id: 'id', speckle_type: 'type' } }
79
+
80
+ const db: Database = {
81
+ getAll: async () => Promise.resolve([])
82
+ } as unknown as Database
83
+ const gathered = new AsyncGeneratorQueue<Item>()
84
+ const deferments = new DefermentManager({ maxSizeInMb: 1, ttlms: 1 })
85
+ const cachePump = new CachePump(db, gathered, deferments, {
86
+ maxCacheReadSize: 1,
87
+ maxCacheWriteSize: 1,
88
+ maxCacheBatchWriteWait: 1,
89
+ maxCacheBatchReadWait: 1,
90
+ maxWriteQueueSize: 1
91
+ })
92
+
93
+ const foundItems = new BufferQueue<Item>()
94
+ const notFoundItems = new BufferQueue<string>()
95
+
96
+ await cachePump.disposeAsync()
97
+ await cachePump.pumpItems({
98
+ ids: [i1.baseId, i2.baseId],
99
+ foundItems,
100
+ notFoundItems
101
+ })
102
+ })
103
+ })
@@ -0,0 +1,99 @@
1
+ import { TIME } from '@speckle/shared'
2
+ import { Database } from '../operations/interfaces.js'
3
+ import { CacheOptions } from '../operations/options.js'
4
+ import { CustomLogger, Item } from '../types/types.js'
5
+ import BatchingQueue from './batchingQueue.js'
6
+ import Queue from './queue.js'
7
+ import { Downloader } from '../operations/interfaces.js'
8
+ import { DefermentManager } from './defermentManager.js'
9
+ import AsyncGeneratorQueue from './asyncGeneratorQueue.js'
10
+ import { Pump } from './pump.js'
11
+
12
+ export class CachePump implements Pump {
13
+ #writeQueue: BatchingQueue<Item> | undefined
14
+ #database: Database
15
+ #logger: CustomLogger
16
+ #deferments: DefermentManager
17
+
18
+ #gathered: AsyncGeneratorQueue<Item>
19
+
20
+ #options: CacheOptions
21
+
22
+ constructor(
23
+ database: Database,
24
+ gathered: AsyncGeneratorQueue<Item>,
25
+ deferments: DefermentManager,
26
+ options: CacheOptions
27
+ ) {
28
+ this.#database = database
29
+ this.#gathered = gathered
30
+ this.#deferments = deferments
31
+ this.#options = options
32
+ this.#logger = options.logger || ((): void => {})
33
+ }
34
+
35
+ add(item: Item): void {
36
+ if (!this.#writeQueue) {
37
+ this.#writeQueue = new BatchingQueue({
38
+ batchSize: this.#options.maxCacheWriteSize,
39
+ maxWaitTime: this.#options.maxCacheBatchWriteWait,
40
+ processFunction: (batch: Item[]): Promise<void> =>
41
+ this.#database.cacheSaveBatch({ batch })
42
+ })
43
+ }
44
+ this.#writeQueue.add(item.baseId, item)
45
+ }
46
+
47
+ async disposeAsync(): Promise<void> {
48
+ await this.#writeQueue?.disposeAsync()
49
+ }
50
+
51
+ async pumpItems(params: {
52
+ ids: string[]
53
+ foundItems: Queue<Item>
54
+ notFoundItems: Queue<string>
55
+ }): Promise<void> {
56
+ const { ids, foundItems, notFoundItems } = params
57
+ const maxCacheReadSize = this.#options.maxCacheReadSize
58
+
59
+ for (let i = 0; i < ids.length; ) {
60
+ if (this.#writeQueue?.isDisposed()) break
61
+ if ((this.#writeQueue?.count() ?? 0) > this.#options.maxWriteQueueSize) {
62
+ this.#logger(
63
+ 'pausing reads (# in write queue: ' + this.#writeQueue?.count() + ')'
64
+ )
65
+ await new Promise((resolve) => setTimeout(resolve, TIME.second)) // Pause for 1 second, protects against out of memory
66
+ continue
67
+ }
68
+ const batch = ids.slice(i, i + maxCacheReadSize)
69
+ const cachedData = await this.#database.getAll(batch)
70
+ for (let i = 0; i < cachedData.length; i++) {
71
+ if (cachedData[i]) {
72
+ foundItems.add(cachedData[i]!)
73
+ } else {
74
+ notFoundItems.add(batch[i])
75
+ }
76
+ }
77
+ i += maxCacheReadSize
78
+ }
79
+ }
80
+
81
+ async *gather(ids: string[], downloader: Downloader): AsyncGenerator<Item> {
82
+ const total = ids.length
83
+ const pumpPromise = this.pumpItems({
84
+ ids,
85
+ foundItems: this.#gathered,
86
+ notFoundItems: downloader
87
+ })
88
+ let count = 0
89
+ for await (const item of this.#gathered.consume()) {
90
+ this.#deferments.undefer(item)
91
+ yield item
92
+ count++
93
+ if (count >= total) {
94
+ this.#gathered.dispose()
95
+ }
96
+ }
97
+ await pumpPromise
98
+ }
99
+ }
@@ -0,0 +1,35 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { Base, Item } from '../types/types.js'
3
+ import { DefermentManager } from './defermentManager.js'
4
+ import { CacheReader } from './cacheReader.js'
5
+ import { MemoryDatabase } from '../operations/databases/memoryDatabase.js'
6
+
7
+ describe('CacheReader testing', () => {
8
+ test('deferred getObject', async () => {
9
+ const i1: Item = { baseId: 'id1', base: { id: 'id', speckle_type: 'type' } }
10
+
11
+ const deferments = new DefermentManager({ maxSizeInMb: 1, ttlms: 1 })
12
+ const cacheReader = new CacheReader(
13
+ new MemoryDatabase({
14
+ items: new Map<string, Base>([[i1.baseId, i1.base]])
15
+ }),
16
+ deferments,
17
+ {
18
+ maxCacheReadSize: 1,
19
+ maxCacheWriteSize: 1,
20
+ maxCacheBatchWriteWait: 1,
21
+ maxCacheBatchReadWait: 1,
22
+ maxWriteQueueSize: 1
23
+ }
24
+ )
25
+
26
+ const objPromise = cacheReader.getObject({
27
+ id: i1.baseId
28
+ })
29
+ deferments.undefer(i1)
30
+ const base = await objPromise
31
+
32
+ expect(base).toMatchSnapshot()
33
+ await cacheReader.disposeAsync()
34
+ })
35
+ })
@@ -0,0 +1,64 @@
1
+ import { Database } from '../operations/interfaces.js'
2
+ import { CacheOptions } from '../operations/options.js'
3
+ import { Base, CustomLogger, Item } from '../types/types.js'
4
+ import BatchingQueue from './batchingQueue.js'
5
+ import { DefermentManager } from './defermentManager.js'
6
+
7
+ export class CacheReader {
8
+ #database: Database
9
+ #defermentManager: DefermentManager
10
+ #logger: CustomLogger
11
+ #options: CacheOptions
12
+ #readQueue: BatchingQueue<string> | undefined
13
+
14
+ constructor(
15
+ database: Database,
16
+ defermentManager: DefermentManager,
17
+ options: CacheOptions
18
+ ) {
19
+ this.#database = database
20
+ this.#defermentManager = defermentManager
21
+ this.#options = options
22
+ this.#logger = options.logger || ((): void => {})
23
+ }
24
+
25
+ async getObject(params: { id: string }): Promise<Base> {
26
+ if (!this.#defermentManager.isDeferred(params.id)) {
27
+ this.#getItem(params.id)
28
+ }
29
+ return await this.#defermentManager.defer({ id: params.id })
30
+ }
31
+
32
+ #getItem(id: string): void {
33
+ if (!this.#readQueue) {
34
+ this.#readQueue = new BatchingQueue({
35
+ batchSize: this.#options.maxCacheReadSize,
36
+ maxWaitTime: this.#options.maxCacheBatchReadWait,
37
+ processFunction: this.#processBatch
38
+ })
39
+ }
40
+ if (!this.#readQueue.get(id)) {
41
+ this.#readQueue.add(id, id)
42
+ }
43
+ }
44
+
45
+ async getAll(keys: string[]): Promise<(Item | undefined)[]> {
46
+ return this.#database.getAll(keys)
47
+ }
48
+
49
+ #processBatch = async (batch: string[]): Promise<void> => {
50
+ const items = await this.#database.getAll(batch)
51
+ for (let i = 0; i < items.length; i++) {
52
+ if (items[i]) {
53
+ this.#defermentManager.undefer(items[i]!)
54
+ } else {
55
+ //this is okay!
56
+ //this.#logger(`Item ${batch[i]} not found in cache`)
57
+ }
58
+ }
59
+ }
60
+
61
+ async disposeAsync(): Promise<void> {
62
+ await this.#readQueue?.disposeAsync()
63
+ }
64
+ }
@@ -0,0 +1,28 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { DefermentManager } from './defermentManager.js'
3
+ import { DefermentManagerOptions } from '../operations/options.js'
4
+ import { Item } from '../types/types.js'
5
+
6
+ describe('DefermentManager disposal', () => {
7
+ const options: DefermentManagerOptions = { ttlms: 10, maxSizeInMb: 1 }
8
+ const makeItem = (id: string): Item => ({
9
+ baseId: id,
10
+ base: { id, speckle_type: 'test' }
11
+ })
12
+
13
+ it('should throw on get/defer/undefer after dispose', async () => {
14
+ const manager = new DefermentManager(options)
15
+ manager.dispose()
16
+ expect(() => manager.get('a')).toThrow('DefermentManager is disposed')
17
+ expect(() => manager.undefer(makeItem('a'))).toThrow('DefermentManager is disposed')
18
+ await expect(manager.defer({ id: 'a' })).rejects.toThrow(
19
+ 'DefermentManager is disposed'
20
+ )
21
+ })
22
+
23
+ it('dispose is idempotent', () => {
24
+ const manager = new DefermentManager(options)
25
+ manager.dispose()
26
+ expect(() => manager.dispose()).not.toThrow()
27
+ })
28
+ })
@@ -1,13 +1,37 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1
3
  import { describe, expect, test } from 'vitest'
2
4
  import { DefermentManager } from './defermentManager.js'
3
5
 
4
6
  describe('deferments', () => {
5
7
  test('defer one', async () => {
6
- const deferments = new DefermentManager()
8
+ const deferments = new DefermentManager({ maxSizeInMb: 1, ttlms: 1 })
7
9
  const x = deferments.defer({ id: 'id' })
8
10
  expect(x).toBeInstanceOf(Promise)
9
11
  deferments.undefer({ baseId: 'id', base: { id: 'id', speckle_type: 'type' } })
10
12
  const b = await x
11
13
  expect(b).toMatchSnapshot()
12
14
  })
15
+
16
+ test('expireAt timeout', async () => {
17
+ const now = 1
18
+ const deferments = new DefermentManager({ maxSizeInMb: 1, ttlms: 1 })
19
+ deferments['now'] = (): number => now
20
+ const x = deferments.defer({ id: 'id' })
21
+ expect(x).toBeInstanceOf(Promise)
22
+ const d = deferments.get('id')
23
+ expect(d).toBeDefined()
24
+ expect(d?.getId()).toBe('id')
25
+ expect((d as any).expiresAt).toBe(2)
26
+ expect((d as any).ttl).toBe(1)
27
+ expect((d as any).item).toBeUndefined()
28
+ expect(d?.isExpired(1)).toBe(false)
29
+ deferments.undefer({ baseId: 'id', base: { id: 'id', speckle_type: 'type' } })
30
+ await x
31
+ expect((d as any).expiresAt).toBe(2)
32
+ expect((d as any).ttl).toBe(1)
33
+ expect((d as any).item).toBeDefined()
34
+ expect(d?.isExpired(1)).toBe(false)
35
+ expect(d?.isExpired(3)).toBe(true)
36
+ })
13
37
  })