@speckle/objectloader2 2.23.22 → 2.24.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 +3 -0
- package/dist/esm/index.js +1 -0
- package/eslint.config.mjs +1 -0
- package/package.json +2 -2
- package/src/helpers/__snapshots__/defermentManager.spec.ts.snap +8 -0
- package/src/helpers/batchingQueue.ts +1 -1
- package/src/helpers/defermentManager.spec.ts +13 -0
- package/src/helpers/defermentManager.ts +25 -0
- package/src/index.ts +1 -0
- package/src/operations/__snapshots__/indexedDatabase.spec.ts.snap +52 -0
- package/src/operations/__snapshots__/objectLoader2.spec.ts.snap +149 -0
- package/src/operations/__snapshots__/serverDownloader.spec.ts.snap +58 -0
- package/src/operations/__snapshots__/traverser.spec.ts.snap +45 -0
- package/src/operations/indexedDatabase.spec.ts +11 -17
- package/src/operations/indexedDatabase.ts +4 -4
- package/src/operations/memoryDatabase.ts +42 -0
- package/src/operations/memoryDownloader.ts +38 -0
- package/src/operations/objectLoader2.spec.ts +145 -9
- package/src/operations/objectLoader2.ts +32 -19
- package/src/operations/options.ts +8 -1
- package/src/operations/serverDownloader.spec.ts +19 -16
- package/src/operations/traverser.spec.ts +58 -0
- package/src/operations/traverser.ts +112 -0
- package/src/test/e2e.spec.ts +31 -26
- package/src/types/types.ts +35 -0
package/dist/commonjs/index.js
CHANGED
|
@@ -3,6 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MemoryDatabase = void 0;
|
|
6
7
|
const objectLoader2_js_1 = __importDefault(require("./operations/objectLoader2.js"));
|
|
7
8
|
exports.default = objectLoader2_js_1.default;
|
|
9
|
+
var memoryDatabase_js_1 = require("./operations/memoryDatabase.js");
|
|
10
|
+
Object.defineProperty(exports, "MemoryDatabase", { enumerable: true, get: function () { return memoryDatabase_js_1.MemoryDatabase; } });
|
|
8
11
|
//# sourceMappingURL=index.js.map
|
package/dist/esm/index.js
CHANGED
package/eslint.config.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@speckle/objectloader2",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.24.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.24.0",
|
|
37
37
|
"dexie": "^4.0.11"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { DefermentManager } from './defermentManager.js'
|
|
3
|
+
|
|
4
|
+
describe('deferments', () => {
|
|
5
|
+
test('defer one', async () => {
|
|
6
|
+
const deferments = new DefermentManager()
|
|
7
|
+
const x = deferments.defer({ id: 'id' })
|
|
8
|
+
expect(x).toBeInstanceOf(Promise)
|
|
9
|
+
deferments.undefer({ baseId: 'id', base: { id: 'id', speckle_type: 'type' } })
|
|
10
|
+
const b = await x
|
|
11
|
+
expect(b).toMatchSnapshot()
|
|
12
|
+
})
|
|
13
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DeferredBase } from './deferredBase.js'
|
|
2
|
+
import { Base, Item } from '../types/types.js'
|
|
3
|
+
|
|
4
|
+
export class DefermentManager {
|
|
5
|
+
#deferments: DeferredBase[] = []
|
|
6
|
+
|
|
7
|
+
async defer(params: { id: string }): Promise<Base> {
|
|
8
|
+
const deferredBase = this.#deferments.find((x) => x.id === params.id)
|
|
9
|
+
if (deferredBase) {
|
|
10
|
+
return await deferredBase.promise
|
|
11
|
+
}
|
|
12
|
+
const d = new DeferredBase(params.id)
|
|
13
|
+
this.#deferments.push(d)
|
|
14
|
+
return d.promise
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
undefer(item: Item): void {
|
|
18
|
+
const deferredIndex = this.#deferments.findIndex((x) => x.id === item.baseId)
|
|
19
|
+
if (deferredIndex !== -1) {
|
|
20
|
+
const deferredBase = this.#deferments[deferredIndex]
|
|
21
|
+
deferredBase.resolve(item.base)
|
|
22
|
+
this.#deferments.splice(deferredIndex, 1)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`database cache > write single item to queue use getItem 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"base": {
|
|
6
|
+
"id": "id",
|
|
7
|
+
"speckle_type": "type",
|
|
8
|
+
},
|
|
9
|
+
"baseId": "id",
|
|
10
|
+
}
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
exports[`database cache > write two items to queue use getItem 1`] = `
|
|
14
|
+
{
|
|
15
|
+
"base": {
|
|
16
|
+
"id": "id",
|
|
17
|
+
"speckle_type": "type",
|
|
18
|
+
},
|
|
19
|
+
"baseId": "id1",
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
exports[`database cache > write two items to queue use getItem 2`] = `
|
|
24
|
+
{
|
|
25
|
+
"base": {
|
|
26
|
+
"id": "id",
|
|
27
|
+
"speckle_type": "type",
|
|
28
|
+
},
|
|
29
|
+
"baseId": "id2",
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
exports[`database cache > write two items to queue use processItems 1`] = `
|
|
34
|
+
[
|
|
35
|
+
{
|
|
36
|
+
"base": {
|
|
37
|
+
"id": "id",
|
|
38
|
+
"speckle_type": "type",
|
|
39
|
+
},
|
|
40
|
+
"baseId": "id1",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"base": {
|
|
44
|
+
"id": "id",
|
|
45
|
+
"speckle_type": "type",
|
|
46
|
+
},
|
|
47
|
+
"baseId": "id2",
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
exports[`database cache > write two items to queue use processItems 2`] = `[]`;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`objectloader2 > add extra header 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"baseId": "baseId",
|
|
6
|
+
}
|
|
7
|
+
`;
|
|
8
|
+
|
|
9
|
+
exports[`objectloader2 > can get a root object from cache 1`] = `
|
|
10
|
+
{
|
|
11
|
+
"baseId": "baseId",
|
|
12
|
+
}
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
exports[`objectloader2 > can get a root object from downloader 1`] = `
|
|
16
|
+
{
|
|
17
|
+
"baseId": "baseId",
|
|
18
|
+
}
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
exports[`objectloader2 > can get root/child object from cache using iterator 1`] = `
|
|
22
|
+
[
|
|
23
|
+
{
|
|
24
|
+
"__closure": {
|
|
25
|
+
"child1Id": 100,
|
|
26
|
+
},
|
|
27
|
+
"id": "rootId",
|
|
28
|
+
"speckle_type": "type",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "child1Id",
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
exports[`objectloader2 > can get root/child object from memory cache using iterator and getObject 1`] = `
|
|
37
|
+
[
|
|
38
|
+
{
|
|
39
|
+
"__closure": {
|
|
40
|
+
"child1Id": 100,
|
|
41
|
+
},
|
|
42
|
+
"id": "rootId",
|
|
43
|
+
"speckle_type": "type",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "child1Id",
|
|
47
|
+
"speckle_type": "type",
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
exports[`objectloader2 > can get root/child object from memory cache using iterator and getObject 2`] = `
|
|
53
|
+
{
|
|
54
|
+
"id": "child1Id",
|
|
55
|
+
"speckle_type": "type",
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
exports[`objectloader2 > can get root/child object from memory downloader using iterator and getObject 1`] = `
|
|
60
|
+
[
|
|
61
|
+
{
|
|
62
|
+
"__closure": {
|
|
63
|
+
"child1Id": 100,
|
|
64
|
+
},
|
|
65
|
+
"id": "rootId",
|
|
66
|
+
"speckle_type": "type",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "child1Id",
|
|
70
|
+
"speckle_type": "type",
|
|
71
|
+
},
|
|
72
|
+
]
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
exports[`objectloader2 > can get root/child object from memory downloader using iterator and getObject 2`] = `
|
|
76
|
+
{
|
|
77
|
+
"id": "child1Id",
|
|
78
|
+
"speckle_type": "type",
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
exports[`objectloader2 > can get single object from cache using iterator 1`] = `
|
|
83
|
+
[
|
|
84
|
+
{
|
|
85
|
+
"id": "baseId",
|
|
86
|
+
"speckle_type": "type",
|
|
87
|
+
},
|
|
88
|
+
]
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
exports[`objectloader2 > createFromJSON test 1`] = `
|
|
92
|
+
[
|
|
93
|
+
{
|
|
94
|
+
"__closure": {
|
|
95
|
+
"0e61e61edee00404ec6e0f9f594bce24": 100,
|
|
96
|
+
"f70738e3e3e593ac11099a6ed6b71154": 100,
|
|
97
|
+
},
|
|
98
|
+
"applicationId": "1",
|
|
99
|
+
"arr": null,
|
|
100
|
+
"attachedProp": null,
|
|
101
|
+
"crazyProp": null,
|
|
102
|
+
"detachedProp": null,
|
|
103
|
+
"detachedProp2": null,
|
|
104
|
+
"dynamicProp": 123,
|
|
105
|
+
"id": "efeadaca70a85ae6d3acfc93a8b380db",
|
|
106
|
+
"list": [
|
|
107
|
+
{
|
|
108
|
+
"__closure": null,
|
|
109
|
+
"referencedId": "0e61e61edee00404ec6e0f9f594bce24",
|
|
110
|
+
"speckle_type": "reference",
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
"list2": [
|
|
114
|
+
{
|
|
115
|
+
"__closure": null,
|
|
116
|
+
"referencedId": "f70738e3e3e593ac11099a6ed6b71154",
|
|
117
|
+
"speckle_type": "reference",
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
"speckle_type": "Speckle.Core.Tests.Unit.Models.BaseTests+SampleObjectBase2",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"applicationId": null,
|
|
124
|
+
"data": [
|
|
125
|
+
1,
|
|
126
|
+
2,
|
|
127
|
+
3,
|
|
128
|
+
4,
|
|
129
|
+
5,
|
|
130
|
+
6,
|
|
131
|
+
7,
|
|
132
|
+
8,
|
|
133
|
+
9,
|
|
134
|
+
10,
|
|
135
|
+
],
|
|
136
|
+
"id": "0e61e61edee00404ec6e0f9f594bce24",
|
|
137
|
+
"speckle_type": "Speckle.Core.Models.DataChunk",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"applicationId": null,
|
|
141
|
+
"data": [
|
|
142
|
+
1,
|
|
143
|
+
10,
|
|
144
|
+
],
|
|
145
|
+
"id": "f70738e3e3e593ac11099a6ed6b71154",
|
|
146
|
+
"speckle_type": "Speckle.Core.Models.DataChunk",
|
|
147
|
+
},
|
|
148
|
+
]
|
|
149
|
+
`;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`downloader > add extra header 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"base": {
|
|
6
|
+
"__closure": {
|
|
7
|
+
"childIds": 1,
|
|
8
|
+
},
|
|
9
|
+
"id": "id",
|
|
10
|
+
"speckle_type": "type",
|
|
11
|
+
},
|
|
12
|
+
"baseId": "id",
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
exports[`downloader > download batch of one 1`] = `
|
|
17
|
+
[
|
|
18
|
+
{
|
|
19
|
+
"base": {
|
|
20
|
+
"id": "id",
|
|
21
|
+
"speckle_type": "type",
|
|
22
|
+
},
|
|
23
|
+
"baseId": "id",
|
|
24
|
+
},
|
|
25
|
+
]
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
exports[`downloader > download batch of two 1`] = `
|
|
29
|
+
[
|
|
30
|
+
{
|
|
31
|
+
"base": {
|
|
32
|
+
"id": "id1",
|
|
33
|
+
"speckle_type": "type",
|
|
34
|
+
},
|
|
35
|
+
"baseId": "id1",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"base": {
|
|
39
|
+
"id": "id2",
|
|
40
|
+
"speckle_type": "type",
|
|
41
|
+
},
|
|
42
|
+
"baseId": "id2",
|
|
43
|
+
},
|
|
44
|
+
]
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
exports[`downloader > download single exists 1`] = `
|
|
48
|
+
{
|
|
49
|
+
"base": {
|
|
50
|
+
"__closure": {
|
|
51
|
+
"childIds": 1,
|
|
52
|
+
},
|
|
53
|
+
"id": "id",
|
|
54
|
+
"speckle_type": "type",
|
|
55
|
+
},
|
|
56
|
+
"baseId": "id",
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`Traverser > root and two children with referenceId 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"applicationId": "1",
|
|
6
|
+
"arr": null,
|
|
7
|
+
"attachedProp": null,
|
|
8
|
+
"crazyProp": null,
|
|
9
|
+
"detachedProp": null,
|
|
10
|
+
"detachedProp2": null,
|
|
11
|
+
"dynamicProp": 123,
|
|
12
|
+
"id": "efeadaca70a85ae6d3acfc93a8b380db",
|
|
13
|
+
"list": [
|
|
14
|
+
{
|
|
15
|
+
"applicationId": null,
|
|
16
|
+
"data": [
|
|
17
|
+
1,
|
|
18
|
+
2,
|
|
19
|
+
3,
|
|
20
|
+
4,
|
|
21
|
+
5,
|
|
22
|
+
6,
|
|
23
|
+
7,
|
|
24
|
+
8,
|
|
25
|
+
9,
|
|
26
|
+
10,
|
|
27
|
+
],
|
|
28
|
+
"id": "0e61e61edee00404ec6e0f9f594bce24",
|
|
29
|
+
"speckle_type": "Speckle.Core.Models.DataChunk",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
"list2": [
|
|
33
|
+
{
|
|
34
|
+
"applicationId": null,
|
|
35
|
+
"data": [
|
|
36
|
+
1,
|
|
37
|
+
10,
|
|
38
|
+
],
|
|
39
|
+
"id": "f70738e3e3e593ac11099a6ed6b71154",
|
|
40
|
+
"speckle_type": "Speckle.Core.Models.DataChunk",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
"speckle_type": "Speckle.Core.Tests.Unit.Models.BaseTests+SampleObjectBase2",
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
@@ -6,7 +6,7 @@ import BufferQueue from '../helpers/bufferQueue.js'
|
|
|
6
6
|
|
|
7
7
|
describe('database cache', () => {
|
|
8
8
|
test('write single item to queue use getItem', async () => {
|
|
9
|
-
const i: Item = { baseId: 'id', base: { id: 'id' } }
|
|
9
|
+
const i: Item = { baseId: 'id', base: { id: 'id', speckle_type: 'type' } }
|
|
10
10
|
const database = new IndexedDatabase({
|
|
11
11
|
indexedDB: new IDBFactory(),
|
|
12
12
|
keyRange: IDBKeyRange,
|
|
@@ -16,13 +16,12 @@ describe('database cache', () => {
|
|
|
16
16
|
await database.disposeAsync()
|
|
17
17
|
|
|
18
18
|
const x = await database.getItem({ id: 'id' })
|
|
19
|
-
expect(x).
|
|
20
|
-
expect(JSON.stringify(x)).toBe(JSON.stringify(i))
|
|
19
|
+
expect(x).toMatchSnapshot()
|
|
21
20
|
})
|
|
22
21
|
|
|
23
22
|
test('write two items to queue use getItem', async () => {
|
|
24
|
-
const i1: Item = { baseId: 'id1', base: { id: 'id' } }
|
|
25
|
-
const i2: Item = { baseId: 'id2', base: { id: 'id' } }
|
|
23
|
+
const i1: Item = { baseId: 'id1', base: { id: 'id', speckle_type: 'type' } }
|
|
24
|
+
const i2: Item = { baseId: 'id2', base: { id: 'id', speckle_type: 'type' } }
|
|
26
25
|
const database = new IndexedDatabase({
|
|
27
26
|
indexedDB: new IDBFactory(),
|
|
28
27
|
keyRange: IDBKeyRange
|
|
@@ -32,17 +31,15 @@ describe('database cache', () => {
|
|
|
32
31
|
await database.disposeAsync()
|
|
33
32
|
|
|
34
33
|
const x1 = await database.getItem({ id: i1.baseId })
|
|
35
|
-
expect(x1).
|
|
36
|
-
expect(JSON.stringify(x1)).toBe(JSON.stringify(i1))
|
|
34
|
+
expect(x1).toMatchSnapshot()
|
|
37
35
|
|
|
38
36
|
const x2 = await database.getItem({ id: i2.baseId })
|
|
39
|
-
expect(x2).
|
|
40
|
-
expect(JSON.stringify(x2)).toBe(JSON.stringify(i2))
|
|
37
|
+
expect(x2).toMatchSnapshot()
|
|
41
38
|
})
|
|
42
39
|
|
|
43
|
-
test('write two items to queue use
|
|
44
|
-
const i1: Item = { baseId: 'id1', base: { id: 'id' } }
|
|
45
|
-
const i2: Item = { baseId: 'id2', base: { id: 'id' } }
|
|
40
|
+
test('write two items to queue use processItems', async () => {
|
|
41
|
+
const i1: Item = { baseId: 'id1', base: { id: 'id', speckle_type: 'type' } }
|
|
42
|
+
const i2: Item = { baseId: 'id2', base: { id: 'id', speckle_type: 'type' } }
|
|
46
43
|
const database = new IndexedDatabase({
|
|
47
44
|
indexedDB: new IDBFactory(),
|
|
48
45
|
keyRange: IDBKeyRange
|
|
@@ -60,10 +57,7 @@ describe('database cache', () => {
|
|
|
60
57
|
notFoundItems
|
|
61
58
|
})
|
|
62
59
|
|
|
63
|
-
expect(foundItems.values()
|
|
64
|
-
expect(
|
|
65
|
-
expect(JSON.stringify(foundItems.values()[1])).toBe(JSON.stringify(i2))
|
|
66
|
-
|
|
67
|
-
expect(notFoundItems.values().length).toBe(0)
|
|
60
|
+
expect(foundItems.values()).toMatchSnapshot()
|
|
61
|
+
expect(notFoundItems.values()).toMatchSnapshot()
|
|
68
62
|
})
|
|
69
63
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import BatchingQueue from '../helpers/batchingQueue.js'
|
|
2
2
|
import Queue from '../helpers/queue.js'
|
|
3
3
|
import { CustomLogger, Item } from '../types/types.js'
|
|
4
|
-
import { isSafari } from '@speckle/shared'
|
|
4
|
+
import { isSafari, TIME } from '@speckle/shared'
|
|
5
5
|
import { BaseDatabaseOptions } from './options.js'
|
|
6
6
|
import { Cache } from './interfaces.js'
|
|
7
7
|
import { Dexie, DexieOptions, Table } from 'dexie'
|
|
@@ -91,7 +91,7 @@ export default class IndexedDatabase implements Cache {
|
|
|
91
91
|
this.#logger(
|
|
92
92
|
'pausing reads (# in write queue: ' + this.#writeQueue?.count() + ')'
|
|
93
93
|
)
|
|
94
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, TIME.second)) // Pause for 1 second, protects against out of memory
|
|
95
95
|
continue
|
|
96
96
|
}
|
|
97
97
|
const batch = ids.slice(i, i + maxCacheReadSize)
|
|
@@ -114,7 +114,7 @@ export default class IndexedDatabase implements Cache {
|
|
|
114
114
|
})
|
|
115
115
|
// const endTime = performance.now()
|
|
116
116
|
// const duration = endTime - startTime
|
|
117
|
-
// this.#logger('Read batch ' + x + ' ' + batch.length + ' ' + duration /
|
|
117
|
+
// this.#logger('Read batch ' + x + ' ' + batch.length + ' ' + duration / TIME_MS.second)
|
|
118
118
|
|
|
119
119
|
// interate down here to help with pausing
|
|
120
120
|
i += maxCacheReadSize
|
|
@@ -143,7 +143,7 @@ export default class IndexedDatabase implements Cache {
|
|
|
143
143
|
await cacheDB.objects.bulkPut(batch)
|
|
144
144
|
// const endTime = performance.now()
|
|
145
145
|
// const duration = endTime - startTime
|
|
146
|
-
//this.#logger('Saved batch ' + x + ' ' + batch.length + ' ' + duration /
|
|
146
|
+
//this.#logger('Saved batch ' + x + ' ' + batch.length + ' ' + duration / TIME_MS.second)
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/**
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import Queue from '../helpers/queue.js'
|
|
2
|
+
import { Base, Item } from '../types/types.js'
|
|
3
|
+
import { Cache } from './interfaces.js'
|
|
4
|
+
import { MemoryDatabaseOptions } from './options.js'
|
|
5
|
+
|
|
6
|
+
export class MemoryDatabase implements Cache {
|
|
7
|
+
#items: Record<string, Base>
|
|
8
|
+
constructor(options?: MemoryDatabaseOptions) {
|
|
9
|
+
this.#items = options?.items || {}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getItem(params: { id: string }): Promise<Item | undefined> {
|
|
13
|
+
const item = this.#items[params.id]
|
|
14
|
+
if (item) {
|
|
15
|
+
return Promise.resolve({ baseId: params.id, base: item })
|
|
16
|
+
}
|
|
17
|
+
return Promise.resolve(undefined)
|
|
18
|
+
}
|
|
19
|
+
processItems(params: {
|
|
20
|
+
ids: string[]
|
|
21
|
+
foundItems: Queue<Item>
|
|
22
|
+
notFoundItems: Queue<string>
|
|
23
|
+
}): Promise<void> {
|
|
24
|
+
const { ids, foundItems, notFoundItems } = params
|
|
25
|
+
for (const id of ids) {
|
|
26
|
+
const item = this.#items[id]
|
|
27
|
+
if (item) {
|
|
28
|
+
foundItems.add({ baseId: id, base: item })
|
|
29
|
+
} else {
|
|
30
|
+
notFoundItems.add(id)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return Promise.resolve()
|
|
34
|
+
}
|
|
35
|
+
add(item: Item): Promise<void> {
|
|
36
|
+
this.#items[item.baseId] = item.base
|
|
37
|
+
return Promise.resolve()
|
|
38
|
+
}
|
|
39
|
+
disposeAsync(): Promise<void> {
|
|
40
|
+
return Promise.resolve()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import AsyncGeneratorQueue from '../helpers/asyncGeneratorQueue.js'
|
|
2
|
+
import { Base, Item } from '../types/types.js'
|
|
3
|
+
import { Downloader } from './interfaces.js'
|
|
4
|
+
|
|
5
|
+
export class MemoryDownloader implements Downloader {
|
|
6
|
+
#items: Record<string, Base>
|
|
7
|
+
#rootId: string
|
|
8
|
+
#results?: AsyncGeneratorQueue<Item>
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
rootId: string,
|
|
12
|
+
items: Record<string, Base>,
|
|
13
|
+
results?: AsyncGeneratorQueue<Item>
|
|
14
|
+
) {
|
|
15
|
+
this.#rootId = rootId
|
|
16
|
+
this.#items = items
|
|
17
|
+
this.#results = results
|
|
18
|
+
}
|
|
19
|
+
initializePool(): void {}
|
|
20
|
+
downloadSingle(): Promise<Item> {
|
|
21
|
+
const root = this.#items[this.#rootId]
|
|
22
|
+
if (root) {
|
|
23
|
+
return Promise.resolve({ baseId: this.#rootId, base: root })
|
|
24
|
+
}
|
|
25
|
+
throw new Error('Method not implemented.')
|
|
26
|
+
}
|
|
27
|
+
disposeAsync(): Promise<void> {
|
|
28
|
+
return Promise.resolve()
|
|
29
|
+
}
|
|
30
|
+
add(id: string): void {
|
|
31
|
+
const base = this.#items[id]
|
|
32
|
+
if (base) {
|
|
33
|
+
this.#results?.add({ baseId: id, base })
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
throw new Error('Method not implemented.')
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -3,6 +3,9 @@ import ObjectLoader2 from './objectLoader2.js'
|
|
|
3
3
|
import { Base, Item } from '../types/types.js'
|
|
4
4
|
import { Cache, Downloader } from './interfaces.js'
|
|
5
5
|
import Queue from '../helpers/queue.js'
|
|
6
|
+
import { MemoryDatabase } from './memoryDatabase.js'
|
|
7
|
+
import { MemoryDownloader } from './memoryDownloader.js'
|
|
8
|
+
import AsyncGeneratorQueue from '../helpers/asyncGeneratorQueue.js'
|
|
6
9
|
|
|
7
10
|
describe('objectloader2', () => {
|
|
8
11
|
test('can get a root object from cache', async () => {
|
|
@@ -22,7 +25,7 @@ describe('objectloader2', () => {
|
|
|
22
25
|
downloader
|
|
23
26
|
})
|
|
24
27
|
const x = await loader.getRootObject()
|
|
25
|
-
expect(x).
|
|
28
|
+
expect(x).toMatchSnapshot()
|
|
26
29
|
})
|
|
27
30
|
|
|
28
31
|
test('can get a root object from downloader', async () => {
|
|
@@ -50,12 +53,12 @@ describe('objectloader2', () => {
|
|
|
50
53
|
downloader
|
|
51
54
|
})
|
|
52
55
|
const x = await loader.getRootObject()
|
|
53
|
-
expect(x).
|
|
56
|
+
expect(x).toMatchSnapshot()
|
|
54
57
|
})
|
|
55
58
|
|
|
56
59
|
test('can get single object from cache using iterator', async () => {
|
|
57
60
|
const rootId = 'baseId'
|
|
58
|
-
const rootBase: Base = { id: 'baseId' }
|
|
61
|
+
const rootBase: Base = { id: 'baseId', speckle_type: 'type' }
|
|
59
62
|
const root = { baseId: rootId, base: rootBase } as unknown as Item
|
|
60
63
|
const cache = {
|
|
61
64
|
getItem(params: { id: string }): Promise<Item | undefined> {
|
|
@@ -76,8 +79,7 @@ describe('objectloader2', () => {
|
|
|
76
79
|
r.push(x)
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
expect(r
|
|
80
|
-
expect(r[0]).toBe(rootBase)
|
|
82
|
+
expect(r).toMatchSnapshot()
|
|
81
83
|
})
|
|
82
84
|
|
|
83
85
|
test('can get root/child object from cache using iterator', async () => {
|
|
@@ -85,7 +87,11 @@ describe('objectloader2', () => {
|
|
|
85
87
|
const child1 = { baseId: 'child1Id', base: child1Base } as unknown as Item
|
|
86
88
|
|
|
87
89
|
const rootId = 'rootId'
|
|
88
|
-
const rootBase: Base = {
|
|
90
|
+
const rootBase: Base = {
|
|
91
|
+
id: 'rootId',
|
|
92
|
+
speckle_type: 'type',
|
|
93
|
+
__closure: { child1Id: 100 }
|
|
94
|
+
}
|
|
89
95
|
const root = {
|
|
90
96
|
baseId: rootId,
|
|
91
97
|
base: rootBase
|
|
@@ -130,10 +136,87 @@ describe('objectloader2', () => {
|
|
|
130
136
|
for await (const x of loader.getObjectIterator()) {
|
|
131
137
|
r.push(x)
|
|
132
138
|
}
|
|
139
|
+
expect(r).toMatchSnapshot()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('can get root/child object from memory cache using iterator and getObject', async () => {
|
|
143
|
+
const child1Base = { id: 'child1Id', speckle_type: 'type' } as Base
|
|
144
|
+
const child1 = { baseId: 'child1Id', base: child1Base } as unknown as Item
|
|
145
|
+
|
|
146
|
+
const rootId = 'rootId'
|
|
147
|
+
const rootBase: Base = {
|
|
148
|
+
id: 'rootId',
|
|
149
|
+
speckle_type: 'type',
|
|
150
|
+
__closure: { child1Id: 100 }
|
|
151
|
+
}
|
|
152
|
+
const root = {
|
|
153
|
+
baseId: rootId,
|
|
154
|
+
base: rootBase
|
|
155
|
+
} as unknown as Item
|
|
156
|
+
|
|
157
|
+
const records: Record<string, Base> = {}
|
|
158
|
+
records[root.baseId] = rootBase
|
|
159
|
+
records[child1.baseId] = child1Base
|
|
160
|
+
|
|
161
|
+
const loader = new ObjectLoader2({
|
|
162
|
+
serverUrl: 'a',
|
|
163
|
+
streamId: 'b',
|
|
164
|
+
objectId: root.baseId,
|
|
165
|
+
cache: new MemoryDatabase({ items: records }),
|
|
166
|
+
downloader: new MemoryDownloader(rootId, records)
|
|
167
|
+
})
|
|
168
|
+
const r = []
|
|
169
|
+
const obj = loader.getObject({ id: child1.baseId })
|
|
170
|
+
for await (const x of loader.getObjectIterator()) {
|
|
171
|
+
r.push(x)
|
|
172
|
+
}
|
|
133
173
|
|
|
134
|
-
expect(
|
|
135
|
-
expect(r
|
|
136
|
-
|
|
174
|
+
expect(obj).toBeDefined()
|
|
175
|
+
expect(r).toMatchSnapshot()
|
|
176
|
+
const obj2 = await obj
|
|
177
|
+
expect(obj2).toBe(child1Base)
|
|
178
|
+
expect(obj2).toMatchSnapshot()
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('can get root/child object from memory downloader using iterator and getObject', async () => {
|
|
182
|
+
const child1Base = { id: 'child1Id', speckle_type: 'type' } as Base
|
|
183
|
+
const child1 = { baseId: 'child1Id', base: child1Base } as unknown as Item
|
|
184
|
+
|
|
185
|
+
const rootId = 'rootId'
|
|
186
|
+
const rootBase: Base = {
|
|
187
|
+
id: 'rootId',
|
|
188
|
+
speckle_type: 'type',
|
|
189
|
+
__closure: { child1Id: 100 }
|
|
190
|
+
}
|
|
191
|
+
const root = {
|
|
192
|
+
baseId: rootId,
|
|
193
|
+
base: rootBase
|
|
194
|
+
} as unknown as Item
|
|
195
|
+
|
|
196
|
+
const records: Record<string, Base> = {}
|
|
197
|
+
records[root.baseId] = rootBase
|
|
198
|
+
records[child1.baseId] = child1Base
|
|
199
|
+
|
|
200
|
+
const results: AsyncGeneratorQueue<Item> = new AsyncGeneratorQueue<Item>()
|
|
201
|
+
const loader = new ObjectLoader2({
|
|
202
|
+
serverUrl: 'a',
|
|
203
|
+
streamId: 'b',
|
|
204
|
+
objectId: root.baseId,
|
|
205
|
+
results,
|
|
206
|
+
cache: new MemoryDatabase(),
|
|
207
|
+
downloader: new MemoryDownloader(rootId, records, results)
|
|
208
|
+
})
|
|
209
|
+
const r = []
|
|
210
|
+
const obj = loader.getObject({ id: child1.baseId })
|
|
211
|
+
for await (const x of loader.getObjectIterator()) {
|
|
212
|
+
r.push(x)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
expect(obj).toBeDefined()
|
|
216
|
+
expect(r).toMatchSnapshot()
|
|
217
|
+
const obj2 = await obj
|
|
218
|
+
expect(obj2).toBe(child1Base)
|
|
219
|
+
expect(obj2).toMatchSnapshot()
|
|
137
220
|
})
|
|
138
221
|
|
|
139
222
|
test('add extra header', async () => {
|
|
@@ -157,5 +240,58 @@ describe('objectloader2', () => {
|
|
|
157
240
|
})
|
|
158
241
|
const x = await loader.getRootObject()
|
|
159
242
|
expect(x).toBe(root)
|
|
243
|
+
expect(x).toMatchSnapshot()
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('createFromJSON test', async () => {
|
|
247
|
+
const root = `{
|
|
248
|
+
"list": [{
|
|
249
|
+
"speckle_type": "reference",
|
|
250
|
+
"referencedId": "0e61e61edee00404ec6e0f9f594bce24",
|
|
251
|
+
"__closure": null
|
|
252
|
+
}],
|
|
253
|
+
"list2": [{
|
|
254
|
+
"speckle_type": "reference",
|
|
255
|
+
"referencedId": "f70738e3e3e593ac11099a6ed6b71154",
|
|
256
|
+
"__closure": null
|
|
257
|
+
}],
|
|
258
|
+
"arr": null,
|
|
259
|
+
"detachedProp": null,
|
|
260
|
+
"detachedProp2": null,
|
|
261
|
+
"attachedProp": null,
|
|
262
|
+
"crazyProp": null,
|
|
263
|
+
"applicationId": "1",
|
|
264
|
+
"speckle_type": "Speckle.Core.Tests.Unit.Models.BaseTests+SampleObjectBase2",
|
|
265
|
+
"dynamicProp": 123,
|
|
266
|
+
"id": "efeadaca70a85ae6d3acfc93a8b380db",
|
|
267
|
+
"__closure": {
|
|
268
|
+
"0e61e61edee00404ec6e0f9f594bce24": 100,
|
|
269
|
+
"f70738e3e3e593ac11099a6ed6b71154": 100
|
|
270
|
+
}
|
|
271
|
+
}`
|
|
272
|
+
|
|
273
|
+
const list1 = `{
|
|
274
|
+
"data": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0],
|
|
275
|
+
"applicationId": null,
|
|
276
|
+
"speckle_type": "Speckle.Core.Models.DataChunk",
|
|
277
|
+
"id": "0e61e61edee00404ec6e0f9f594bce24"
|
|
278
|
+
}`
|
|
279
|
+
|
|
280
|
+
const list2 = `{
|
|
281
|
+
"data": [1.0, 10.0],
|
|
282
|
+
"applicationId": null,
|
|
283
|
+
"speckle_type": "Speckle.Core.Models.DataChunk",
|
|
284
|
+
"id": "f70738e3e3e593ac11099a6ed6b71154"
|
|
285
|
+
}`
|
|
286
|
+
const rootObj = JSON.parse(root) as Base
|
|
287
|
+
const list1Obj = JSON.parse(list1) as Base
|
|
288
|
+
const list2Obj = JSON.parse(list2) as Base
|
|
289
|
+
|
|
290
|
+
const loader = ObjectLoader2.createFromObjects([rootObj, list1Obj, list2Obj])
|
|
291
|
+
const r = []
|
|
292
|
+
for await (const x of loader.getObjectIterator()) {
|
|
293
|
+
r.push(x)
|
|
294
|
+
}
|
|
295
|
+
expect(r).toMatchSnapshot()
|
|
160
296
|
})
|
|
161
297
|
})
|
|
@@ -4,7 +4,9 @@ import IndexedDatabase from './indexedDatabase.js'
|
|
|
4
4
|
import ServerDownloader from './serverDownloader.js'
|
|
5
5
|
import { CustomLogger, Base, Item } from '../types/types.js'
|
|
6
6
|
import { ObjectLoader2Options } from './options.js'
|
|
7
|
-
import {
|
|
7
|
+
import { MemoryDownloader } from './memoryDownloader.js'
|
|
8
|
+
import { MemoryDatabase } from './memoryDatabase.js'
|
|
9
|
+
import { DefermentManager } from '../helpers/defermentManager.js'
|
|
8
10
|
|
|
9
11
|
export default class ObjectLoader2 {
|
|
10
12
|
#objectId: string
|
|
@@ -14,21 +16,22 @@ export default class ObjectLoader2 {
|
|
|
14
16
|
#database: Cache
|
|
15
17
|
#downloader: Downloader
|
|
16
18
|
|
|
17
|
-
#
|
|
19
|
+
#deferments: DefermentManager
|
|
18
20
|
|
|
19
|
-
#
|
|
21
|
+
#gathered: AsyncGeneratorQueue<Item>
|
|
20
22
|
|
|
21
23
|
constructor(options: ObjectLoader2Options) {
|
|
22
24
|
this.#objectId = options.objectId
|
|
23
25
|
|
|
24
26
|
this.#logger = options.logger || console.log
|
|
25
|
-
this.#gathered = new AsyncGeneratorQueue()
|
|
27
|
+
this.#gathered = options.results || new AsyncGeneratorQueue()
|
|
28
|
+
this.#deferments = new DefermentManager()
|
|
26
29
|
this.#database =
|
|
27
30
|
options.cache ||
|
|
28
31
|
new IndexedDatabase({
|
|
29
32
|
logger: this.#logger,
|
|
30
|
-
maxCacheReadSize:
|
|
31
|
-
maxCacheWriteSize:
|
|
33
|
+
maxCacheReadSize: 10_000,
|
|
34
|
+
maxCacheWriteSize: 5_000,
|
|
32
35
|
indexedDB: options.indexedDB,
|
|
33
36
|
keyRange: options.keyRange
|
|
34
37
|
})
|
|
@@ -69,13 +72,7 @@ export default class ObjectLoader2 {
|
|
|
69
72
|
if (item) {
|
|
70
73
|
return item.base
|
|
71
74
|
}
|
|
72
|
-
|
|
73
|
-
if (deferredBase) {
|
|
74
|
-
return await deferredBase.promise
|
|
75
|
-
}
|
|
76
|
-
const d = new DeferredBase(params.id)
|
|
77
|
-
this.#buffer.push(d)
|
|
78
|
-
return d
|
|
75
|
+
return await this.#deferments.defer({ id: params.id })
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
async getTotalObjectCount() {
|
|
@@ -103,12 +100,7 @@ export default class ObjectLoader2 {
|
|
|
103
100
|
})
|
|
104
101
|
let count = 0
|
|
105
102
|
for await (const item of this.#gathered.consume()) {
|
|
106
|
-
|
|
107
|
-
if (deferredIndex !== -1) {
|
|
108
|
-
const deferredBase = this.#buffer[deferredIndex]
|
|
109
|
-
deferredBase.resolve(item.base)
|
|
110
|
-
this.#buffer.splice(deferredIndex, 1)
|
|
111
|
-
}
|
|
103
|
+
this.#deferments.undefer(item)
|
|
112
104
|
yield item.base
|
|
113
105
|
count++
|
|
114
106
|
if (count >= total) {
|
|
@@ -117,4 +109,25 @@ export default class ObjectLoader2 {
|
|
|
117
109
|
}
|
|
118
110
|
await processPromise
|
|
119
111
|
}
|
|
112
|
+
|
|
113
|
+
static createFromObjects(objects: Base[]): ObjectLoader2 {
|
|
114
|
+
const root = objects[0]
|
|
115
|
+
const records: Record<string, Base> = {}
|
|
116
|
+
objects.forEach((element) => {
|
|
117
|
+
records[element.id] = element
|
|
118
|
+
})
|
|
119
|
+
const loader = new ObjectLoader2({
|
|
120
|
+
serverUrl: 'dummy',
|
|
121
|
+
streamId: 'dummy',
|
|
122
|
+
objectId: root.id,
|
|
123
|
+
cache: new MemoryDatabase({ items: records }),
|
|
124
|
+
downloader: new MemoryDownloader(root.id, records)
|
|
125
|
+
})
|
|
126
|
+
return loader
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
static createFromJSON(json: string): ObjectLoader2 {
|
|
130
|
+
const jsonObj = JSON.parse(json) as Base[]
|
|
131
|
+
return this.createFromObjects(jsonObj)
|
|
132
|
+
}
|
|
120
133
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
|
2
|
+
import AsyncGeneratorQueue from '../helpers/asyncGeneratorQueue.js'
|
|
2
3
|
import Queue from '../helpers/queue.js'
|
|
3
|
-
import { CustomLogger, Fetcher, Item } from '../types/types.js'
|
|
4
|
+
import { Base, CustomLogger, Fetcher, Item } from '../types/types.js'
|
|
4
5
|
import { Cache, Downloader } from './interfaces.js'
|
|
5
6
|
|
|
6
7
|
export interface ObjectLoader2Options {
|
|
@@ -12,6 +13,7 @@ export interface ObjectLoader2Options {
|
|
|
12
13
|
token?: string
|
|
13
14
|
logger?: CustomLogger
|
|
14
15
|
headers?: Headers
|
|
16
|
+
results?: AsyncGeneratorQueue<Item>
|
|
15
17
|
cache?: Cache
|
|
16
18
|
downloader?: Downloader
|
|
17
19
|
}
|
|
@@ -39,3 +41,8 @@ export interface BaseDownloadOptions {
|
|
|
39
41
|
database: Cache
|
|
40
42
|
results: Queue<Item>
|
|
41
43
|
}
|
|
44
|
+
|
|
45
|
+
export interface MemoryDatabaseOptions {
|
|
46
|
+
logger?: CustomLogger
|
|
47
|
+
items?: Record<string, Base>
|
|
48
|
+
}
|
|
@@ -9,9 +9,9 @@ import ServerDownloader from './serverDownloader.js'
|
|
|
9
9
|
describe('downloader', () => {
|
|
10
10
|
test('download batch of one', async () => {
|
|
11
11
|
const fetchMocker = createFetchMock(vi)
|
|
12
|
-
const i: Item = { baseId: 'id', base: { id: 'id' } }
|
|
12
|
+
const i: Item = { baseId: 'id', base: { id: 'id', speckle_type: 'type' } }
|
|
13
13
|
fetchMocker.mockResponseOnce('id\t' + JSON.stringify(i.base) + '\n')
|
|
14
|
-
const results = new AsyncGeneratorQueue
|
|
14
|
+
const results = new AsyncGeneratorQueue()
|
|
15
15
|
const db = {
|
|
16
16
|
async add(): Promise<void> {
|
|
17
17
|
return Promise.resolve()
|
|
@@ -36,18 +36,17 @@ describe('downloader', () => {
|
|
|
36
36
|
r.push(x)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
expect(r
|
|
40
|
-
expect(JSON.stringify(r[0])).toBe(JSON.stringify(i))
|
|
39
|
+
expect(r).toMatchSnapshot()
|
|
41
40
|
})
|
|
42
41
|
|
|
43
42
|
test('download batch of two', async () => {
|
|
44
43
|
const fetchMocker = createFetchMock(vi)
|
|
45
|
-
const i1: Item = { baseId: 'id1', base: { id: 'id1' } }
|
|
46
|
-
const i2: Item = { baseId: 'id2', base: { id: 'id2' } }
|
|
44
|
+
const i1: Item = { baseId: 'id1', base: { id: 'id1', speckle_type: 'type' } }
|
|
45
|
+
const i2: Item = { baseId: 'id2', base: { id: 'id2', speckle_type: 'type' } }
|
|
47
46
|
fetchMocker.mockResponseOnce(
|
|
48
47
|
'id1\t' + JSON.stringify(i1.base) + '\nid2\t' + JSON.stringify(i2.base) + '\n'
|
|
49
48
|
)
|
|
50
|
-
const results = new AsyncGeneratorQueue
|
|
49
|
+
const results = new AsyncGeneratorQueue()
|
|
51
50
|
const db = {
|
|
52
51
|
async add(): Promise<void> {
|
|
53
52
|
return Promise.resolve()
|
|
@@ -72,16 +71,17 @@ describe('downloader', () => {
|
|
|
72
71
|
r.push(x)
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
expect(r
|
|
76
|
-
expect(JSON.stringify(r[0])).toBe(JSON.stringify(i1))
|
|
77
|
-
expect(JSON.stringify(r[1])).toBe(JSON.stringify(i2))
|
|
74
|
+
expect(r).toMatchSnapshot()
|
|
78
75
|
})
|
|
79
76
|
|
|
80
77
|
test('download single exists', async () => {
|
|
81
78
|
const fetchMocker = createFetchMock(vi)
|
|
82
|
-
const i: Item = {
|
|
79
|
+
const i: Item = {
|
|
80
|
+
baseId: 'id',
|
|
81
|
+
base: { id: 'id', speckle_type: 'type', __closure: { childIds: 1 } }
|
|
82
|
+
}
|
|
83
83
|
fetchMocker.mockResponseOnce(JSON.stringify(i.base))
|
|
84
|
-
const results = new AsyncGeneratorQueue
|
|
84
|
+
const results = new AsyncGeneratorQueue()
|
|
85
85
|
const db = {
|
|
86
86
|
async add(): Promise<void> {
|
|
87
87
|
return Promise.resolve()
|
|
@@ -98,17 +98,20 @@ describe('downloader', () => {
|
|
|
98
98
|
fetch: fetchMocker
|
|
99
99
|
})
|
|
100
100
|
const x = await downloader.downloadSingle()
|
|
101
|
-
expect(
|
|
101
|
+
expect(x).toMatchSnapshot()
|
|
102
102
|
})
|
|
103
103
|
|
|
104
104
|
test('add extra header', async () => {
|
|
105
105
|
const fetchMocker = createFetchMock(vi)
|
|
106
|
-
const i: Item = {
|
|
106
|
+
const i: Item = {
|
|
107
|
+
baseId: 'id',
|
|
108
|
+
base: { id: 'id', speckle_type: 'type', __closure: { childIds: 1 } }
|
|
109
|
+
}
|
|
107
110
|
fetchMocker.mockResponseIf(
|
|
108
111
|
(req) => req.headers.get('x-test') === 'asdf',
|
|
109
112
|
JSON.stringify(i.base)
|
|
110
113
|
)
|
|
111
|
-
const results = new AsyncGeneratorQueue
|
|
114
|
+
const results = new AsyncGeneratorQueue()
|
|
112
115
|
const db = {
|
|
113
116
|
async add(): Promise<void> {
|
|
114
117
|
return Promise.resolve()
|
|
@@ -128,6 +131,6 @@ describe('downloader', () => {
|
|
|
128
131
|
fetch: fetchMocker
|
|
129
132
|
})
|
|
130
133
|
const x = await downloader.downloadSingle()
|
|
131
|
-
expect(
|
|
134
|
+
expect(x).toMatchSnapshot()
|
|
132
135
|
})
|
|
133
136
|
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { Base } from '../types/types.js'
|
|
3
|
+
import ObjectLoader2 from './objectLoader2.js'
|
|
4
|
+
import Traverser from './traverser.js'
|
|
5
|
+
|
|
6
|
+
describe('Traverser', () => {
|
|
7
|
+
test('root and two children with referenceId', async () => {
|
|
8
|
+
const root = `{
|
|
9
|
+
"list": [{
|
|
10
|
+
"speckle_type": "reference",
|
|
11
|
+
"referencedId": "0e61e61edee00404ec6e0f9f594bce24",
|
|
12
|
+
"__closure": null
|
|
13
|
+
}],
|
|
14
|
+
"list2": [{
|
|
15
|
+
"speckle_type": "reference",
|
|
16
|
+
"referencedId": "f70738e3e3e593ac11099a6ed6b71154",
|
|
17
|
+
"__closure": null
|
|
18
|
+
}],
|
|
19
|
+
"arr": null,
|
|
20
|
+
"detachedProp": null,
|
|
21
|
+
"detachedProp2": null,
|
|
22
|
+
"attachedProp": null,
|
|
23
|
+
"crazyProp": null,
|
|
24
|
+
"applicationId": "1",
|
|
25
|
+
"speckle_type": "Speckle.Core.Tests.Unit.Models.BaseTests+SampleObjectBase2",
|
|
26
|
+
"dynamicProp": 123,
|
|
27
|
+
"id": "efeadaca70a85ae6d3acfc93a8b380db",
|
|
28
|
+
"__closure": {
|
|
29
|
+
"0e61e61edee00404ec6e0f9f594bce24": 100,
|
|
30
|
+
"f70738e3e3e593ac11099a6ed6b71154": 100
|
|
31
|
+
}
|
|
32
|
+
}`
|
|
33
|
+
|
|
34
|
+
const list1 = `{
|
|
35
|
+
"data": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0],
|
|
36
|
+
"applicationId": null,
|
|
37
|
+
"speckle_type": "Speckle.Core.Models.DataChunk",
|
|
38
|
+
"id": "0e61e61edee00404ec6e0f9f594bce24"
|
|
39
|
+
}`
|
|
40
|
+
|
|
41
|
+
const list2 = `{
|
|
42
|
+
"data": [1.0, 10.0],
|
|
43
|
+
"applicationId": null,
|
|
44
|
+
"speckle_type": "Speckle.Core.Models.DataChunk",
|
|
45
|
+
"id": "f70738e3e3e593ac11099a6ed6b71154"
|
|
46
|
+
}`
|
|
47
|
+
|
|
48
|
+
const rootObj = JSON.parse(root) as Base
|
|
49
|
+
const list1Obj = JSON.parse(list1) as Base
|
|
50
|
+
const list2Obj = JSON.parse(list2) as Base
|
|
51
|
+
|
|
52
|
+
const loader = ObjectLoader2.createFromObjects([rootObj, list1Obj, list2Obj])
|
|
53
|
+
|
|
54
|
+
const traverser = new Traverser(loader)
|
|
55
|
+
const r = await traverser.traverse()
|
|
56
|
+
expect(r).toMatchSnapshot()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Base, DataChunk, isBase, isReference, isScalar } from '../types/types.js'
|
|
2
|
+
import ObjectLoader2 from './objectLoader2.js'
|
|
3
|
+
|
|
4
|
+
export type ProgressStage = 'download' | 'construction'
|
|
5
|
+
export type OnProgress = (e: {
|
|
6
|
+
stage: ProgressStage
|
|
7
|
+
current: number
|
|
8
|
+
total: number
|
|
9
|
+
}) => void
|
|
10
|
+
|
|
11
|
+
export interface TraverserOptions {
|
|
12
|
+
excludeProps?: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default class Traverser {
|
|
16
|
+
#loader: ObjectLoader2
|
|
17
|
+
#options: TraverserOptions
|
|
18
|
+
|
|
19
|
+
#totalChildrenCount = 0
|
|
20
|
+
#traversedReferencesCount = 0
|
|
21
|
+
|
|
22
|
+
constructor(loader: ObjectLoader2, options?: TraverserOptions) {
|
|
23
|
+
this.#options = options || {}
|
|
24
|
+
this.#loader = loader
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async traverse(onProgress?: OnProgress): Promise<Base> {
|
|
28
|
+
let firstObjectPromise: Promise<Base> | undefined = undefined
|
|
29
|
+
for await (const obj of this.#loader.getObjectIterator()) {
|
|
30
|
+
if (!firstObjectPromise) {
|
|
31
|
+
firstObjectPromise = this.traverseBase(obj, onProgress)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (firstObjectPromise) {
|
|
36
|
+
return await firstObjectPromise
|
|
37
|
+
} else {
|
|
38
|
+
throw new Error('No objects found')
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async traverseArray(array: Array<unknown>, onProgress?: OnProgress): Promise<void> {
|
|
43
|
+
for (let i = 0; i < 10; i++) {
|
|
44
|
+
const prop = array[i]
|
|
45
|
+
if (isScalar(prop)) continue
|
|
46
|
+
if (isBase(prop)) {
|
|
47
|
+
array[i] = await this.traverseBase(prop, onProgress)
|
|
48
|
+
} else if (isReference(prop)) {
|
|
49
|
+
array[i] = await this.traverseBase(
|
|
50
|
+
await this.#loader.getObject({ id: prop.referencedId }),
|
|
51
|
+
onProgress
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async traverseBase(base: Base, onProgress?: OnProgress): Promise<Base> {
|
|
58
|
+
for (const ignoredProp of this.#options.excludeProps || []) {
|
|
59
|
+
delete (base as never)[ignoredProp]
|
|
60
|
+
}
|
|
61
|
+
if (base.__closure) {
|
|
62
|
+
const ids = Object.keys(base.__closure)
|
|
63
|
+
const promises: Promise<Base>[] = []
|
|
64
|
+
for (const id of ids) {
|
|
65
|
+
promises.push(
|
|
66
|
+
this.traverseBase(await this.#loader.getObject({ id }), onProgress)
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
await Promise.all(promises)
|
|
70
|
+
}
|
|
71
|
+
delete (base as never)['__closure']
|
|
72
|
+
|
|
73
|
+
// De-chunk
|
|
74
|
+
if (base.speckle_type?.includes('DataChunk')) {
|
|
75
|
+
const chunk = base as DataChunk
|
|
76
|
+
if (chunk.data) {
|
|
77
|
+
await this.traverseArray(chunk.data, onProgress)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//other props
|
|
82
|
+
for (const prop in base) {
|
|
83
|
+
if (prop === '__closure') continue
|
|
84
|
+
if (prop === 'referenceId') continue
|
|
85
|
+
if (prop === 'speckle_type') continue
|
|
86
|
+
if (prop === 'data') continue
|
|
87
|
+
const baseProp = (base as unknown as Record<string, unknown>)[prop]
|
|
88
|
+
if (isScalar(baseProp)) continue
|
|
89
|
+
if (isBase(baseProp)) {
|
|
90
|
+
await this.traverseBase(baseProp, onProgress)
|
|
91
|
+
} else if (isReference(baseProp)) {
|
|
92
|
+
await this.traverseBase(
|
|
93
|
+
await this.#loader.getObject({ id: baseProp.referencedId }),
|
|
94
|
+
onProgress
|
|
95
|
+
)
|
|
96
|
+
} else if (Array.isArray(baseProp)) {
|
|
97
|
+
await this.traverseArray(baseProp, onProgress)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (onProgress) {
|
|
101
|
+
onProgress({
|
|
102
|
+
stage: 'construction',
|
|
103
|
+
current:
|
|
104
|
+
++this.#traversedReferencesCount > this.#totalChildrenCount
|
|
105
|
+
? this.#totalChildrenCount
|
|
106
|
+
: this.#traversedReferencesCount,
|
|
107
|
+
total: this.#totalChildrenCount
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
return base
|
|
111
|
+
}
|
|
112
|
+
}
|
package/src/test/e2e.spec.ts
CHANGED
|
@@ -2,35 +2,40 @@ import { describe, test, expect } from 'vitest'
|
|
|
2
2
|
import { IDBFactory, IDBKeyRange } from 'fake-indexeddb'
|
|
3
3
|
import ObjectLoader2 from '../operations/objectLoader2.js'
|
|
4
4
|
import { Base } from '../types/types.js'
|
|
5
|
+
import { TIME } from '@speckle/shared'
|
|
5
6
|
|
|
6
7
|
describe('e2e', () => {
|
|
7
|
-
test(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
test(
|
|
9
|
+
'download small model',
|
|
10
|
+
async () => {
|
|
11
|
+
// Revit sample house (good for bim-like stuff with many display meshes)
|
|
12
|
+
//const resource = 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
|
|
13
|
+
const loader = new ObjectLoader2({
|
|
14
|
+
serverUrl: 'https://app.speckle.systems',
|
|
15
|
+
streamId: 'da9e320dad',
|
|
16
|
+
objectId: '31d10c0cea569a1e26809658ed27e281',
|
|
17
|
+
indexedDB: new IDBFactory(),
|
|
18
|
+
keyRange: IDBKeyRange
|
|
19
|
+
})
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
const getObjectPromise = loader.getObject({
|
|
22
|
+
id: '1708a78e057e8115f924c620ba686db6'
|
|
23
|
+
})
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
const bases: Base[] = []
|
|
26
|
+
for await (const obj of loader.getObjectIterator()) {
|
|
27
|
+
bases.push(obj)
|
|
28
|
+
}
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
expect(await loader.getTotalObjectCount()).toBe(1328)
|
|
31
|
+
expect(bases.length).toBe(1328)
|
|
32
|
+
const base = await getObjectPromise
|
|
33
|
+
expect(base).toBeDefined()
|
|
34
|
+
expect(base.id).toBe('1708a78e057e8115f924c620ba686db6')
|
|
35
|
+
const base2 = await loader.getObject({ id: '3841e3cbc45d52c47bc2f1b7b0ad4eb9' })
|
|
36
|
+
expect(base2).toBeDefined()
|
|
37
|
+
expect(base2.id).toBe('3841e3cbc45d52c47bc2f1b7b0ad4eb9')
|
|
38
|
+
},
|
|
39
|
+
10 * TIME.second
|
|
40
|
+
)
|
|
36
41
|
})
|
package/src/types/types.ts
CHANGED
|
@@ -12,9 +12,20 @@ export interface Item {
|
|
|
12
12
|
|
|
13
13
|
export interface Base {
|
|
14
14
|
id: string
|
|
15
|
+
speckle_type: string
|
|
15
16
|
__closure?: Record<string, number>
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
export interface Reference {
|
|
20
|
+
speckle_type: string
|
|
21
|
+
referencedId: string
|
|
22
|
+
__closure?: Record<string, number>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DataChunk extends Base {
|
|
26
|
+
data?: Base[]
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
export function isBase(maybeBase?: unknown): maybeBase is Base {
|
|
19
30
|
return (
|
|
20
31
|
maybeBase !== null &&
|
|
@@ -23,3 +34,27 @@ export function isBase(maybeBase?: unknown): maybeBase is Base {
|
|
|
23
34
|
typeof maybeBase.id === 'string'
|
|
24
35
|
)
|
|
25
36
|
}
|
|
37
|
+
|
|
38
|
+
export function isReference(maybeRef?: unknown): maybeRef is Reference {
|
|
39
|
+
return (
|
|
40
|
+
maybeRef !== null &&
|
|
41
|
+
typeof maybeRef === 'object' &&
|
|
42
|
+
'referencedId' in maybeRef &&
|
|
43
|
+
typeof maybeRef.referencedId === 'string'
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isScalar(
|
|
48
|
+
value: unknown
|
|
49
|
+
): value is string | number | boolean | bigint | symbol | undefined {
|
|
50
|
+
const type = typeof value
|
|
51
|
+
return (
|
|
52
|
+
value === null ||
|
|
53
|
+
type === 'string' ||
|
|
54
|
+
type === 'number' ||
|
|
55
|
+
type === 'boolean' ||
|
|
56
|
+
type === 'bigint' ||
|
|
57
|
+
type === 'symbol' ||
|
|
58
|
+
type === 'undefined'
|
|
59
|
+
)
|
|
60
|
+
}
|