@speckle/objectloader 2.1.1 → 2.4.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/examples/node/script.js +24 -1
- package/index.js +311 -46
- package/package.json +5 -2
- package/readme.md +17 -2
package/examples/node/script.js
CHANGED
|
@@ -1 +1,24 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Since Node v<18 does not provide fetch, we need to pass it in the options object. Note that fetch must return a WHATWG compliant stream, so cross-fetch won't work, but node/undici's implementation will.
|
|
2
|
+
|
|
3
|
+
import { fetch } from 'undici'
|
|
4
|
+
import ObjectLoader from '../../index.js'
|
|
5
|
+
|
|
6
|
+
let loader = new ObjectLoader({
|
|
7
|
+
serverUrl:"https://latest.speckle.dev",
|
|
8
|
+
streamId:"3ed8357f29",
|
|
9
|
+
objectId:"0408ab9caaa2ebefb2dd7f1f671e7555",
|
|
10
|
+
options:{ enableCaching: false, excludeProps:[], fetch }
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
let loadData = async function loadData() {
|
|
15
|
+
|
|
16
|
+
let obj = await loader.getAndConstructObject((e) =>{
|
|
17
|
+
console.log(e) // log progress!
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
console.log('Done!')
|
|
21
|
+
console.log( obj )
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
loadData()
|
package/index.js
CHANGED
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Simple client that streams object info from a Speckle Server.
|
|
2
|
+
* Simple client that streams object info from a Speckle Server.
|
|
3
3
|
* TODO: Object construction progress reporting is weird.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
export default class ObjectLoader {
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* Creates a new object loader instance.
|
|
12
|
-
* @param {*} param0
|
|
11
|
+
* @param {*} param0
|
|
13
12
|
*/
|
|
14
|
-
constructor( { serverUrl, streamId, token, objectId, options = { fullyTraverseArrays: false, excludeProps: [ ] } } ) {
|
|
13
|
+
constructor( { serverUrl, streamId, token, objectId, options = { enableCaching: true, fullyTraverseArrays: false, excludeProps: [ ], fetch:null } } ) {
|
|
15
14
|
this.INTERVAL_MS = 20
|
|
16
15
|
this.TIMEOUT_MS = 180000 // three mins
|
|
17
16
|
|
|
18
17
|
this.serverUrl = serverUrl || window.location.origin
|
|
19
18
|
this.streamId = streamId
|
|
20
19
|
this.objectId = objectId
|
|
21
|
-
|
|
20
|
+
console.log('Object loader constructor called!')
|
|
21
|
+
try {
|
|
22
|
+
this.token = token || localStorage.getItem( 'AuthToken' )
|
|
23
|
+
} catch (error) {
|
|
24
|
+
// Accessing localStorage may throw when executing on sandboxed document, ignore.
|
|
25
|
+
}
|
|
22
26
|
|
|
23
27
|
this.headers = {
|
|
24
28
|
'Accept': 'text/plain'
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
if( token ) {
|
|
31
|
+
if( this.token ) {
|
|
28
32
|
this.headers['Authorization'] = `Bearer ${this.token}`
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
this.
|
|
35
|
+
this.requestUrlRootObj = `${this.serverUrl}/objects/${this.streamId}/${this.objectId}/single`
|
|
36
|
+
this.requestUrlChildren = `${this.serverUrl}/api/getobjects/${this.streamId}`
|
|
32
37
|
this.promises = []
|
|
33
38
|
this.intervals = {}
|
|
34
39
|
this.buffer = []
|
|
@@ -36,6 +41,35 @@ export default class ObjectLoader {
|
|
|
36
41
|
this.totalChildrenCount = 0
|
|
37
42
|
this.traversedReferencesCount = 0
|
|
38
43
|
this.options = options
|
|
44
|
+
this.options.numConnections = this.options.numConnections || 4
|
|
45
|
+
|
|
46
|
+
this.cacheDB = null
|
|
47
|
+
|
|
48
|
+
this.lastAsyncPause = Date.now()
|
|
49
|
+
this.existingAsyncPause = null
|
|
50
|
+
|
|
51
|
+
// we can't simply bind fetch to this.fetch, so instead we have to do some acrobatics: https://stackoverflow.com/questions/69337187/uncaught-in-promise-typeerror-failed-to-execute-fetch-on-workerglobalscope#comment124731316_69337187
|
|
52
|
+
this.preferredFetch = options.fetch
|
|
53
|
+
this.fetch = function(...args) {
|
|
54
|
+
let currentFetch = this.preferredFetch || fetch
|
|
55
|
+
return currentFetch(...args)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async asyncPause() {
|
|
61
|
+
// Don't freeze the UI
|
|
62
|
+
// while ( this.existingAsyncPause ) {
|
|
63
|
+
// await this.existingAsyncPause
|
|
64
|
+
// }
|
|
65
|
+
if ( Date.now() - this.lastAsyncPause >= 100 ) {
|
|
66
|
+
this.lastAsyncPause = Date.now()
|
|
67
|
+
this.existingAsyncPause = new Promise( resolve => setTimeout( resolve, 0 ) )
|
|
68
|
+
await this.existingAsyncPause
|
|
69
|
+
this.existingAsyncPause = null
|
|
70
|
+
if (Date.now() - this.lastAsyncPause > 500) console.log("Loader Event loop lag: ", Date.now() - this.lastAsyncPause)
|
|
71
|
+
}
|
|
72
|
+
|
|
39
73
|
}
|
|
40
74
|
|
|
41
75
|
dispose() {
|
|
@@ -45,25 +79,25 @@ export default class ObjectLoader {
|
|
|
45
79
|
|
|
46
80
|
/**
|
|
47
81
|
* Use this method to receive and construct the object. It will return the full, de-referenced and de-chunked original object.
|
|
48
|
-
* @param {*} onProgress
|
|
49
|
-
* @returns
|
|
82
|
+
* @param {*} onProgress
|
|
83
|
+
* @returns
|
|
50
84
|
*/
|
|
51
85
|
async getAndConstructObject( onProgress ) {
|
|
52
|
-
|
|
86
|
+
|
|
53
87
|
;( await this.downloadObjectsInBuffer( onProgress ) ) // Fire and forget; PS: semicolon of doom
|
|
54
|
-
|
|
88
|
+
|
|
55
89
|
let rootObject = await this.getObject( this.objectId )
|
|
56
90
|
return this.traverseAndConstruct( rootObject, onProgress )
|
|
57
91
|
}
|
|
58
92
|
|
|
59
93
|
/**
|
|
60
94
|
* Internal function used to download all the objects in a local buffer.
|
|
61
|
-
* @param {*} onProgress
|
|
95
|
+
* @param {*} onProgress
|
|
62
96
|
*/
|
|
63
97
|
async downloadObjectsInBuffer( onProgress ) {
|
|
64
98
|
let first = true
|
|
65
99
|
let downloadNum = 0
|
|
66
|
-
|
|
100
|
+
|
|
67
101
|
for await ( let obj of this.getObjectIterator() ) {
|
|
68
102
|
if( first ) {
|
|
69
103
|
this.totalChildrenCount = obj.totalChildrenCount
|
|
@@ -78,9 +112,9 @@ export default class ObjectLoader {
|
|
|
78
112
|
|
|
79
113
|
/**
|
|
80
114
|
* Internal function used to recursively traverse an object and populate its references and dechunk any arrays.
|
|
81
|
-
* @param {*} obj
|
|
82
|
-
* @param {*} onProgress
|
|
83
|
-
* @returns
|
|
115
|
+
* @param {*} obj
|
|
116
|
+
* @param {*} onProgress
|
|
117
|
+
* @returns
|
|
84
118
|
*/
|
|
85
119
|
async traverseAndConstruct( obj, onProgress ) {
|
|
86
120
|
if( !obj ) return
|
|
@@ -91,20 +125,20 @@ export default class ObjectLoader {
|
|
|
91
125
|
let arr = []
|
|
92
126
|
for ( let element of obj ) {
|
|
93
127
|
if ( typeof element !== 'object' && ! this.options.fullyTraverseArrays ) return obj
|
|
94
|
-
|
|
128
|
+
|
|
95
129
|
// Dereference element if needed
|
|
96
130
|
let deRef = element.referencedId ? await this.getObject( element.referencedId ) : element
|
|
97
|
-
if( element.referencedId && onProgress ) onProgress( { stage: 'construction', current: ++this.traversedReferencesCount > this.totalChildrenCount ? this.totalChildrenCount : this.traversedReferencesCount, total: this.totalChildrenCount } )
|
|
98
|
-
|
|
131
|
+
if( element.referencedId && onProgress ) onProgress( { stage: 'construction', current: ++this.traversedReferencesCount > this.totalChildrenCount ? this.totalChildrenCount : this.traversedReferencesCount, total: this.totalChildrenCount } )
|
|
132
|
+
|
|
99
133
|
// Push the traversed object in the array
|
|
100
134
|
arr.push( await this.traverseAndConstruct( deRef, onProgress ) )
|
|
101
135
|
}
|
|
102
|
-
|
|
136
|
+
|
|
103
137
|
// De-chunk
|
|
104
|
-
if( arr[0]?.speckle_type?.toLowerCase().includes('datachunk') ) {
|
|
138
|
+
if( arr[0]?.speckle_type?.toLowerCase().includes('datachunk') ) {
|
|
105
139
|
return arr.reduce( ( prev, curr ) => prev.concat( curr.data ), [] )
|
|
106
140
|
}
|
|
107
|
-
|
|
141
|
+
|
|
108
142
|
return arr
|
|
109
143
|
}
|
|
110
144
|
|
|
@@ -113,11 +147,11 @@ export default class ObjectLoader {
|
|
|
113
147
|
for( let ignoredProp of this.options.excludeProps ) {
|
|
114
148
|
delete obj[ ignoredProp ]
|
|
115
149
|
}
|
|
116
|
-
|
|
150
|
+
|
|
117
151
|
// 2) Iterate through obj
|
|
118
|
-
for( let prop in obj ) {
|
|
119
|
-
if( typeof obj[prop] !== 'object' ) continue // leave alone primitive props
|
|
120
|
-
|
|
152
|
+
for( let prop in obj ) {
|
|
153
|
+
if( typeof obj[prop] !== 'object' || obj[prop] === null ) continue // leave alone primitive props
|
|
154
|
+
|
|
121
155
|
if( obj[prop].referencedId ) {
|
|
122
156
|
obj[prop] = await this.getObject( obj[prop].referencedId )
|
|
123
157
|
if( onProgress ) onProgress( { stage: 'construction', current: ++this.traversedReferencesCount > this.totalChildrenCount ? this.totalChildrenCount : this.traversedReferencesCount, total: this.totalChildrenCount } )
|
|
@@ -131,8 +165,8 @@ export default class ObjectLoader {
|
|
|
131
165
|
|
|
132
166
|
/**
|
|
133
167
|
* Internal function. Returns a promise that is resolved when the object id is loaded into the internal buffer.
|
|
134
|
-
* @param {*} id
|
|
135
|
-
* @returns
|
|
168
|
+
* @param {*} id
|
|
169
|
+
* @returns
|
|
136
170
|
*/
|
|
137
171
|
async getObject( id ){
|
|
138
172
|
if ( this.buffer[id] ) return this.buffer[id]
|
|
@@ -172,11 +206,15 @@ export default class ObjectLoader {
|
|
|
172
206
|
}
|
|
173
207
|
|
|
174
208
|
async * getObjectIterator( ) {
|
|
209
|
+
let t0 = Date.now()
|
|
210
|
+
let count = 0
|
|
175
211
|
for await ( let line of this.getRawObjectIterator() ) {
|
|
176
212
|
let { id, obj } = this.processLine( line )
|
|
177
213
|
this.buffer[ id ] = obj
|
|
214
|
+
count += 1
|
|
178
215
|
yield obj
|
|
179
216
|
}
|
|
217
|
+
console.log(`Loaded ${count} objects in: ${(Date.now() - t0) / 1000}`)
|
|
180
218
|
}
|
|
181
219
|
|
|
182
220
|
processLine( chunk ) {
|
|
@@ -185,31 +223,258 @@ export default class ObjectLoader {
|
|
|
185
223
|
}
|
|
186
224
|
|
|
187
225
|
async * getRawObjectIterator() {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
226
|
+
let tSTART = Date.now()
|
|
227
|
+
|
|
228
|
+
if ( this.options.enableCaching && window.indexedDB && this.cacheDB === null) {
|
|
229
|
+
await safariFix()
|
|
230
|
+
let idbOpenRequest = indexedDB.open('speckle-object-cache', 1)
|
|
231
|
+
idbOpenRequest.onupgradeneeded = () => idbOpenRequest.result.createObjectStore('objects');
|
|
232
|
+
this.cacheDB = await this.promisifyIdbRequest( idbOpenRequest )
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const rootObjJson = await this.getRawRootObject()
|
|
236
|
+
// console.log("Root in: ", Date.now() - tSTART)
|
|
237
|
+
|
|
238
|
+
yield `${this.objectId}\t${rootObjJson}`
|
|
239
|
+
|
|
240
|
+
const rootObj = JSON.parse(rootObjJson)
|
|
241
|
+
if ( !rootObj.__closure ) return
|
|
242
|
+
|
|
243
|
+
let childrenIds = Object.keys(rootObj.__closure).sort( (a, b) => rootObj.__closure[a] - rootObj.__closure[b] )
|
|
244
|
+
if ( childrenIds.length === 0 ) return
|
|
245
|
+
|
|
246
|
+
let splitHttpRequests = []
|
|
247
|
+
|
|
248
|
+
if ( childrenIds.length > 50 ) {
|
|
249
|
+
// split into 5%, 15%, 40%, 40% (5% for the high priority children: the ones with lower minDepth)
|
|
250
|
+
let splitBeforeCacheCheck = [ [], [], [], [] ]
|
|
251
|
+
let crtChildIndex = 0
|
|
252
|
+
|
|
253
|
+
for ( ; crtChildIndex < 0.05 * childrenIds.length; crtChildIndex++ ) {
|
|
254
|
+
splitBeforeCacheCheck[0].push( childrenIds[ crtChildIndex ] )
|
|
255
|
+
}
|
|
256
|
+
for ( ; crtChildIndex < 0.2 * childrenIds.length; crtChildIndex++ ) {
|
|
257
|
+
splitBeforeCacheCheck[1].push( childrenIds[ crtChildIndex ] )
|
|
258
|
+
}
|
|
259
|
+
for ( ; crtChildIndex < 0.6 * childrenIds.length; crtChildIndex++ ) {
|
|
260
|
+
splitBeforeCacheCheck[2].push( childrenIds[ crtChildIndex ] )
|
|
261
|
+
}
|
|
262
|
+
for ( ; crtChildIndex < childrenIds.length; crtChildIndex++ ) {
|
|
263
|
+
splitBeforeCacheCheck[3].push( childrenIds[ crtChildIndex ] )
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
console.log("Cache check for: ", splitBeforeCacheCheck)
|
|
268
|
+
|
|
269
|
+
let newChildren = []
|
|
270
|
+
let nextCachePromise = this.cacheGetObjects( splitBeforeCacheCheck[ 0 ] )
|
|
271
|
+
|
|
272
|
+
for ( let i = 0; i < 4; i++ ) {
|
|
273
|
+
let cachedObjects = await nextCachePromise
|
|
274
|
+
if ( i < 3 ) nextCachePromise = this.cacheGetObjects( splitBeforeCacheCheck[ i + 1 ] )
|
|
275
|
+
|
|
276
|
+
let sortedCachedKeys = Object.keys(cachedObjects).sort( (a, b) => rootObj.__closure[a] - rootObj.__closure[b] )
|
|
277
|
+
for ( let id of sortedCachedKeys ) {
|
|
278
|
+
yield `${id}\t${cachedObjects[ id ]}`
|
|
279
|
+
}
|
|
280
|
+
let newChildrenForBatch = splitBeforeCacheCheck[i].filter( id => !( id in cachedObjects ) )
|
|
281
|
+
newChildren.push( ...newChildrenForBatch )
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if ( newChildren.length === 0 ) return
|
|
285
|
+
|
|
286
|
+
if ( newChildren.length <= 50 ) {
|
|
287
|
+
// we have almost all of children in the cache. do only 1 requests for the remaining new children
|
|
288
|
+
splitHttpRequests.push( newChildren )
|
|
289
|
+
} else {
|
|
290
|
+
// we now set up the batches for 4 http requests, starting from `newChildren` (already sorted by priority)
|
|
291
|
+
splitHttpRequests = [ [], [], [], [] ]
|
|
292
|
+
crtChildIndex = 0
|
|
293
|
+
|
|
294
|
+
for ( ; crtChildIndex < 0.05 * newChildren.length; crtChildIndex++ ) {
|
|
295
|
+
splitHttpRequests[0].push( newChildren[ crtChildIndex ] )
|
|
296
|
+
}
|
|
297
|
+
for ( ; crtChildIndex < 0.2 * newChildren.length; crtChildIndex++ ) {
|
|
298
|
+
splitHttpRequests[1].push( newChildren[ crtChildIndex ] )
|
|
299
|
+
}
|
|
300
|
+
for ( ; crtChildIndex < 0.6 * newChildren.length; crtChildIndex++ ) {
|
|
301
|
+
splitHttpRequests[2].push( newChildren[ crtChildIndex ] )
|
|
302
|
+
}
|
|
303
|
+
for ( ; crtChildIndex < newChildren.length; crtChildIndex++ ) {
|
|
304
|
+
splitHttpRequests[3].push( newChildren[ crtChildIndex ] )
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
} else {
|
|
309
|
+
// small object with <= 50 children. check cache and make only 1 request
|
|
310
|
+
const cachedObjects = await this.cacheGetObjects( childrenIds )
|
|
311
|
+
let sortedCachedKeys = Object.keys(cachedObjects).sort( (a, b) => rootObj.__closure[a] - rootObj.__closure[b] )
|
|
312
|
+
for ( let id of sortedCachedKeys ) {
|
|
313
|
+
yield `${id}\t${cachedObjects[ id ]}`
|
|
314
|
+
}
|
|
315
|
+
childrenIds = childrenIds.filter(id => !( id in cachedObjects ) )
|
|
316
|
+
if ( childrenIds.length === 0 ) return
|
|
317
|
+
|
|
318
|
+
// only 1 http request with the remaining children ( <= 50 )
|
|
319
|
+
splitHttpRequests.push( childrenIds )
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Starting http requests for batches in `splitHttpRequests`
|
|
193
323
|
|
|
194
|
-
|
|
195
|
-
|
|
324
|
+
const decoders = []
|
|
325
|
+
const readers = []
|
|
326
|
+
const readPromisses = []
|
|
327
|
+
const startIndexes = []
|
|
328
|
+
const readBuffers = []
|
|
329
|
+
const finishedRequests = []
|
|
330
|
+
|
|
331
|
+
for (let i = 0; i < splitHttpRequests.length; i++) {
|
|
332
|
+
decoders.push(new TextDecoder())
|
|
333
|
+
readers.push( null )
|
|
334
|
+
readPromisses.push( null )
|
|
335
|
+
startIndexes.push( 0 )
|
|
336
|
+
readBuffers.push( '' )
|
|
337
|
+
finishedRequests.push( false )
|
|
338
|
+
|
|
339
|
+
this.fetch(
|
|
340
|
+
this.requestUrlChildren,
|
|
341
|
+
{
|
|
342
|
+
method: 'POST',
|
|
343
|
+
headers: { ...this.headers, 'Content-Type': 'application/json' },
|
|
344
|
+
body: JSON.stringify( { objects: JSON.stringify( splitHttpRequests[i] ) } )
|
|
345
|
+
}
|
|
346
|
+
).then( crtResponse => {
|
|
347
|
+
let crtReader = crtResponse.body.getReader()
|
|
348
|
+
readers[i] = crtReader
|
|
349
|
+
let crtReadPromise = crtReader.read().then(x => { x.reqId = i; return x })
|
|
350
|
+
readPromisses[i] = crtReadPromise
|
|
351
|
+
})
|
|
352
|
+
}
|
|
196
353
|
|
|
197
354
|
while ( true ) {
|
|
198
|
-
let
|
|
199
|
-
if (
|
|
200
|
-
if
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
355
|
+
let validReadPromises = readPromisses.filter(x => x != null)
|
|
356
|
+
if ( validReadPromises.length === 0 ) {
|
|
357
|
+
// Check if all requests finished
|
|
358
|
+
if ( finishedRequests.every(x => x) ) {
|
|
359
|
+
break
|
|
360
|
+
}
|
|
361
|
+
// Sleep 10 ms
|
|
362
|
+
await new Promise( ( resolve ) => {
|
|
363
|
+
setTimeout( resolve, 10 )
|
|
364
|
+
} )
|
|
365
|
+
continue
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Wait for data on any running request
|
|
369
|
+
let data = await Promise.any( validReadPromises )
|
|
370
|
+
let { value: crtDataChunk, done: readerDone, reqId } = data
|
|
371
|
+
finishedRequests[ reqId ] = readerDone
|
|
372
|
+
|
|
373
|
+
// Replace read promise on this request with a new `read` call
|
|
374
|
+
if ( !readerDone ) {
|
|
375
|
+
let crtReadPromise = readers[ reqId ].read().then(x => { x.reqId = reqId; return x })
|
|
376
|
+
readPromisses[ reqId ] = crtReadPromise
|
|
377
|
+
} else {
|
|
378
|
+
// This request finished. "Flush any non-newline-terminated text"
|
|
379
|
+
if ( readBuffers[ reqId ].length > 0 ) {
|
|
380
|
+
yield readBuffers[ reqId ]
|
|
381
|
+
readBuffers[ reqId ] = ''
|
|
382
|
+
}
|
|
383
|
+
// no other read calls for this request
|
|
384
|
+
readPromisses[ reqId ] = null
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if ( !crtDataChunk )
|
|
205
388
|
continue
|
|
389
|
+
|
|
390
|
+
crtDataChunk = decoders[ reqId ].decode( crtDataChunk )
|
|
391
|
+
let unprocessedText = readBuffers[ reqId ] + crtDataChunk
|
|
392
|
+
let unprocessedLines = unprocessedText.split(/\r\n|\n|\r/)
|
|
393
|
+
let remainderText = unprocessedLines.pop()
|
|
394
|
+
readBuffers[ reqId ] = remainderText
|
|
395
|
+
|
|
396
|
+
for ( let line of unprocessedLines ) {
|
|
397
|
+
yield line
|
|
206
398
|
}
|
|
207
|
-
|
|
208
|
-
startIndex = re.lastIndex
|
|
399
|
+
this.cacheStoreObjects(unprocessedLines)
|
|
209
400
|
}
|
|
401
|
+
}
|
|
210
402
|
|
|
211
|
-
|
|
212
|
-
|
|
403
|
+
async getRawRootObject() {
|
|
404
|
+
const cachedRootObject = await this.cacheGetObjects( [ this.objectId ] )
|
|
405
|
+
if ( cachedRootObject[ this.objectId ] )
|
|
406
|
+
return cachedRootObject[ this.objectId ]
|
|
407
|
+
const response = await this.fetch( this.requestUrlRootObj, { headers: this.headers } )
|
|
408
|
+
const responseText = await response.text()
|
|
409
|
+
this.cacheStoreObjects( [ `${this.objectId}\t${responseText}` ] )
|
|
410
|
+
return responseText
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
promisifyIdbRequest(request) {
|
|
414
|
+
return new Promise((resolve, reject) => {
|
|
415
|
+
request.oncomplete = request.onsuccess = () => resolve(request.result);
|
|
416
|
+
request.onabort = request.onerror = () => reject(request.error);
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async cacheGetObjects(ids) {
|
|
421
|
+
if ( !this.options.enableCaching || !window.indexedDB ) {
|
|
422
|
+
return {}
|
|
213
423
|
}
|
|
424
|
+
|
|
425
|
+
let ret = {}
|
|
426
|
+
|
|
427
|
+
for (let i = 0; i < ids.length; i += 500) {
|
|
428
|
+
let idsChunk = ids.slice(i, i + 500)
|
|
429
|
+
let t0 = Date.now()
|
|
430
|
+
|
|
431
|
+
let store = this.cacheDB.transaction('objects', 'readonly').objectStore('objects')
|
|
432
|
+
let idbChildrenPromises = idsChunk.map( id => this.promisifyIdbRequest( store.get( id ) ).then( data => ( { id, data } ) ) )
|
|
433
|
+
let cachedData = await Promise.all(idbChildrenPromises)
|
|
434
|
+
|
|
435
|
+
// console.log("Cache check for : ", idsChunk.length, Date.now() - t0)
|
|
436
|
+
|
|
437
|
+
for ( let cachedObj of cachedData ) {
|
|
438
|
+
if ( !cachedObj.data ) // non-existent objects are retrieved with `undefined` data
|
|
439
|
+
continue
|
|
440
|
+
ret[ cachedObj.id ] = cachedObj.data
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return ret
|
|
214
445
|
}
|
|
446
|
+
|
|
447
|
+
cacheStoreObjects(objects) {
|
|
448
|
+
if ( !this.options.enableCaching || !window.indexedDB ) {
|
|
449
|
+
return {}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
let store = this.cacheDB.transaction('objects', 'readwrite').objectStore('objects')
|
|
453
|
+
for ( let obj of objects ) {
|
|
454
|
+
let idAndData = obj.split( '\t' )
|
|
455
|
+
store.put(idAndData[1], idAndData[0])
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return this.promisifyIdbRequest( store.transaction )
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
// Credits and more info: https://github.com/jakearchibald/safari-14-idb-fix
|
|
464
|
+
function safariFix() {
|
|
465
|
+
const isSafari =
|
|
466
|
+
!navigator.userAgentData &&
|
|
467
|
+
/Safari\//.test(navigator.userAgent) &&
|
|
468
|
+
!/Chrom(e|ium)\//.test(navigator.userAgent)
|
|
469
|
+
|
|
470
|
+
// No point putting other browsers or older versions of Safari through this mess.
|
|
471
|
+
if (!isSafari || !indexedDB.databases) return Promise.resolve()
|
|
472
|
+
|
|
473
|
+
let intervalId
|
|
474
|
+
|
|
475
|
+
return new Promise( ( resolve ) => {
|
|
476
|
+
const tryIdb = () => indexedDB.databases().finally(resolve)
|
|
477
|
+
intervalId = setInterval(tryIdb, 100)
|
|
478
|
+
tryIdb()
|
|
479
|
+
}).finally( () => clearInterval(intervalId) )
|
|
215
480
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@speckle/objectloader",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Simple API helper to stream in objects from the Speckle Server.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"homepage": "https://speckle.systems",
|
|
@@ -15,5 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"keywords": ["speckle", "aec", "speckle api"],
|
|
17
17
|
"author": "AEC Systems",
|
|
18
|
-
"license": "Apache-2.0"
|
|
18
|
+
"license": "Apache-2.0",
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"undici": "^4.14.1"
|
|
21
|
+
}
|
|
19
22
|
}
|
package/readme.md
CHANGED
|
@@ -10,9 +10,11 @@ Comprehensive developer and user documentation can be found in our:
|
|
|
10
10
|
|
|
11
11
|
## Getting started
|
|
12
12
|
|
|
13
|
-
This is a small utility class that helps you stream an object and all its sub-components from the Speckle Server API. It is
|
|
13
|
+
This is a small utility class that helps you stream an object and all its sub-components from the Speckle Server API. It is intended to be used in contexts where you want to "download" the whole object, or iteratively traverse its whole tree.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
### In the browser
|
|
16
|
+
|
|
17
|
+
Here's a sample way on how to use it, pilfered from the [3d viewer package](../viewer):
|
|
16
18
|
|
|
17
19
|
```js
|
|
18
20
|
|
|
@@ -52,6 +54,19 @@ let loader = new ObjectLoader( {
|
|
|
52
54
|
|
|
53
55
|
let obj = await loader.getAndConstructObject( ( e ) => console.log( 'Progress', e ) )
|
|
54
56
|
|
|
57
|
+
### On the server
|
|
58
|
+
|
|
59
|
+
Since Node.js does not yet support the [`fetch API`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch), you'll need to provide your own `fetch` function in the options object. Note that `fetch` must return a [Web Stream](https://nodejs.org/api/webstreams.html), so [node-fetch](https://github.com/node-fetch/node-fetch) won't work, but [node/undici's](https://undici.nodejs.org/) implementation will.
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
import { fetch } from 'undici'
|
|
63
|
+
|
|
64
|
+
let loader = new ObjectLoader({
|
|
65
|
+
serverUrl: 'https://latest.speckle.dev',
|
|
66
|
+
streamId: '3ed8357f29',
|
|
67
|
+
objectId: '0408ab9caaa2ebefb2dd7f1f671e7555',
|
|
68
|
+
options: { enableCaching: false, excludeProps: [], fetch },
|
|
69
|
+
})
|
|
55
70
|
```
|
|
56
71
|
|
|
57
72
|
## Community
|