@speckle/objectloader2 2.23.23 → 2.24.2

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.
@@ -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
@@ -1,3 +1,4 @@
1
1
  import ObjectLoader2 from './operations/objectLoader2.js';
2
2
  export default ObjectLoader2;
3
+ export { MemoryDatabase } from './operations/memoryDatabase.js';
3
4
  //# sourceMappingURL=index.js.map
package/eslint.config.mjs CHANGED
@@ -49,6 +49,7 @@ const configs = [
49
49
  {
50
50
  files: ['**/*.spec.ts'],
51
51
  rules: {
52
+ camelcase: 'off',
52
53
  '@typescript-eslint/no-unused-expressions': 'off'
53
54
  }
54
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@speckle/objectloader2",
3
- "version": "2.23.23",
3
+ "version": "2.24.2",
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.23.23",
36
+ "@speckle/shared": "^2.24.2",
37
37
  "dexie": "^4.0.11"
38
38
  },
39
39
  "devDependencies": {
@@ -0,0 +1,8 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`deferments > defer one 1`] = `
4
+ {
5
+ "id": "id",
6
+ "speckle_type": "type",
7
+ }
8
+ `;
@@ -62,7 +62,7 @@ export default class BatchingQueue<T> implements Queue<T> {
62
62
  }
63
63
  /*console.log(
64
64
  'queue is waiting ' +
65
- interval / 1000 +
65
+ interval / TIME_MS.second +
66
66
  ' with queue size of ' +
67
67
  this.#queue.length
68
68
  )*/
@@ -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
@@ -1,3 +1,4 @@
1
1
  import ObjectLoader2 from './operations/objectLoader2.js'
2
2
 
3
3
  export default ObjectLoader2
4
+ export { MemoryDatabase } from './operations/memoryDatabase.js'
@@ -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).toBeDefined()
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).toBeDefined()
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).toBeDefined()
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 getItem', async () => {
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().length).toBe(2)
64
- expect(JSON.stringify(foundItems.values()[0])).toBe(JSON.stringify(i1))
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, 1000)) // Pause for 1 second, protects against out of memory
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 / 1000)
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 / 1000)
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).toBe(root)
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).toBe(root)
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.length).toBe(1)
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 = { id: 'rootId', __closure: { child1Id: 100 } }
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(r.length).toBe(2)
135
- expect(r[0]).toBe(rootBase)
136
- expect(r[1]).toBe(child1Base)
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 { DeferredBase } from '../helpers/deferredBase.js'
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
- #gathered: AsyncGeneratorQueue<Item>
19
+ #deferments: DefermentManager
18
20
 
19
- #buffer: DeferredBase[] = []
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: 10000,
31
- maxCacheWriteSize: 5000,
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
- const deferredBase = this.#buffer.find((x) => x.id === params.id)
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
- const deferredIndex = this.#buffer.findIndex((x) => x.id === item.baseId)
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<Item>()
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.length).toBe(1)
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<Item>()
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.length).toBe(2)
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 = { baseId: 'id', base: { id: 'id', __closure: { childIds: 1 } } }
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<Item>()
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(JSON.stringify(x)).toBe(JSON.stringify(i))
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 = { baseId: 'id', base: { id: 'id', __closure: { childIds: 1 } } }
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<Item>()
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(JSON.stringify(x)).toBe(JSON.stringify(i))
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
+ }
@@ -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('download small model', async () => {
8
- // Revit sample house (good for bim-like stuff with many display meshes)
9
- //const resource = 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
10
- const loader = new ObjectLoader2({
11
- serverUrl: 'https://app.speckle.systems',
12
- streamId: 'da9e320dad',
13
- objectId: '31d10c0cea569a1e26809658ed27e281',
14
- indexedDB: new IDBFactory(),
15
- keyRange: IDBKeyRange
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
- const getObjectPromise = loader.getObject({
19
- id: '1708a78e057e8115f924c620ba686db6'
20
- })
21
+ const getObjectPromise = loader.getObject({
22
+ id: '1708a78e057e8115f924c620ba686db6'
23
+ })
21
24
 
22
- const bases: Base[] = []
23
- for await (const obj of loader.getObjectIterator()) {
24
- bases.push(obj)
25
- }
25
+ const bases: Base[] = []
26
+ for await (const obj of loader.getObjectIterator()) {
27
+ bases.push(obj)
28
+ }
26
29
 
27
- expect(await loader.getTotalObjectCount()).toBe(1328)
28
- expect(bases.length).toBe(1328)
29
- const base = await getObjectPromise
30
- expect(base).toBeDefined()
31
- expect(base.id).toBe('1708a78e057e8115f924c620ba686db6')
32
- const base2 = await loader.getObject({ id: '3841e3cbc45d52c47bc2f1b7b0ad4eb9' })
33
- expect(base2).toBeDefined()
34
- expect(base2.id).toBe('3841e3cbc45d52c47bc2f1b7b0ad4eb9')
35
- }, 10000)
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
  })
@@ -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
+ }