@speckle/objectloader2 2.24.2 → 2.25.0
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.
- package/dist/commonjs/index.js +6 -7
- package/dist/esm/index.js +3 -3
- package/eslint.config.mjs +3 -1
- package/package.json +2 -2
- package/src/helpers/__snapshots__/cachePump.spec.ts.snap +31 -0
- package/src/helpers/__snapshots__/cacheReader.spec.ts.snap +8 -0
- package/src/helpers/aggregateQueue.ts +20 -0
- package/src/helpers/batchedPool.ts +5 -9
- package/src/helpers/batchingQueue.ts +21 -13
- package/src/helpers/cachePump.disposal.spec.ts +49 -0
- package/src/helpers/cachePump.spec.ts +103 -0
- package/src/helpers/cachePump.ts +99 -0
- package/src/helpers/cacheReader.spec.ts +35 -0
- package/src/helpers/cacheReader.ts +64 -0
- package/src/helpers/defermentManager.disposal.spec.ts +28 -0
- package/src/helpers/defermentManager.spec.ts +25 -1
- package/src/helpers/defermentManager.ts +128 -12
- package/src/helpers/deferredBase.ts +44 -6
- package/src/helpers/keyedQueue.ts +45 -0
- package/src/helpers/memoryPump.ts +40 -0
- package/src/helpers/pump.ts +8 -0
- package/src/index.ts +3 -4
- package/src/operations/__snapshots__/objectLoader2.spec.ts.snap +16 -16
- package/src/operations/{__snapshots__ → databases/__snapshots__}/indexedDatabase.spec.ts.snap +0 -21
- package/src/operations/{indexedDatabase.spec.ts → databases/indexedDatabase.spec.ts} +2 -28
- package/src/operations/databases/indexedDatabase.ts +150 -0
- package/src/operations/databases/memoryDatabase.ts +43 -0
- package/src/operations/{__snapshots__ → downloaders/__snapshots__}/serverDownloader.spec.ts.snap +34 -0
- package/src/operations/{memoryDownloader.ts → downloaders/memoryDownloader.ts} +15 -14
- package/src/operations/{serverDownloader.spec.ts → downloaders/serverDownloader.spec.ts} +68 -43
- package/src/operations/{serverDownloader.ts → downloaders/serverDownloader.ts} +92 -38
- package/src/operations/interfaces.ts +11 -12
- package/src/operations/objectLoader2.spec.ts +76 -144
- package/src/operations/objectLoader2.ts +57 -79
- package/src/operations/objectLoader2Factory.ts +56 -0
- package/src/operations/options.ts +18 -37
- package/src/operations/traverser.spec.ts +1 -1
- package/src/operations/traverser.ts +1 -1
- package/src/test/e2e.spec.ts +4 -4
- package/src/types/types.ts +11 -0
- package/src/operations/indexedDatabase.ts +0 -167
- package/src/operations/memoryDatabase.ts +0 -42
package/dist/commonjs/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
2
|
-
export
|
|
3
|
-
export {
|
|
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.
|
|
3
|
+
"version": "2.25.0",
|
|
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.
|
|
36
|
+
"@speckle/shared": "^2.25.0",
|
|
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,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
|
-
#
|
|
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.#
|
|
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
|
-
|
|
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.#
|
|
41
|
+
this.#disposed = true
|
|
46
42
|
await this.#processingLoop
|
|
47
43
|
}
|
|
48
44
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import KeyedQueue from './keyedQueue.js'
|
|
2
2
|
|
|
3
|
-
export default class BatchingQueue<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
|
-
#
|
|
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.#
|
|
29
|
+
this.#disposed = true
|
|
30
30
|
await this.#processingLoop
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
add(item: T): void {
|
|
34
|
-
this.#queue.
|
|
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.
|
|
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.
|
|
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.#
|
|
55
|
+
while (!this.#disposed || this.#queue.size > 0) {
|
|
48
56
|
const startTime = performance.now()
|
|
49
|
-
if (this.#queue.
|
|
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.
|
|
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 /
|
|
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
|
})
|