@speckle/objectloader 2.1.1 → 2.2.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/index.js +303 -45
- package/package.json +1 -1
- package/readme.md +1 -1
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: [ ] } } ) {
|
|
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,28 @@ 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
|
+
}
|
|
52
|
+
|
|
53
|
+
async asyncPause() {
|
|
54
|
+
// Don't freeze the UI
|
|
55
|
+
// while ( this.existingAsyncPause ) {
|
|
56
|
+
// await this.existingAsyncPause
|
|
57
|
+
// }
|
|
58
|
+
if ( Date.now() - this.lastAsyncPause >= 100 ) {
|
|
59
|
+
this.lastAsyncPause = Date.now()
|
|
60
|
+
this.existingAsyncPause = new Promise( resolve => setTimeout( resolve, 0 ) )
|
|
61
|
+
await this.existingAsyncPause
|
|
62
|
+
this.existingAsyncPause = null
|
|
63
|
+
if (Date.now() - this.lastAsyncPause > 500) console.log("Loader Event loop lag: ", Date.now() - this.lastAsyncPause)
|
|
64
|
+
}
|
|
65
|
+
|
|
39
66
|
}
|
|
40
67
|
|
|
41
68
|
dispose() {
|
|
@@ -45,25 +72,25 @@ export default class ObjectLoader {
|
|
|
45
72
|
|
|
46
73
|
/**
|
|
47
74
|
* 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
|
|
75
|
+
* @param {*} onProgress
|
|
76
|
+
* @returns
|
|
50
77
|
*/
|
|
51
78
|
async getAndConstructObject( onProgress ) {
|
|
52
|
-
|
|
79
|
+
|
|
53
80
|
;( await this.downloadObjectsInBuffer( onProgress ) ) // Fire and forget; PS: semicolon of doom
|
|
54
|
-
|
|
81
|
+
|
|
55
82
|
let rootObject = await this.getObject( this.objectId )
|
|
56
83
|
return this.traverseAndConstruct( rootObject, onProgress )
|
|
57
84
|
}
|
|
58
85
|
|
|
59
86
|
/**
|
|
60
87
|
* Internal function used to download all the objects in a local buffer.
|
|
61
|
-
* @param {*} onProgress
|
|
88
|
+
* @param {*} onProgress
|
|
62
89
|
*/
|
|
63
90
|
async downloadObjectsInBuffer( onProgress ) {
|
|
64
91
|
let first = true
|
|
65
92
|
let downloadNum = 0
|
|
66
|
-
|
|
93
|
+
|
|
67
94
|
for await ( let obj of this.getObjectIterator() ) {
|
|
68
95
|
if( first ) {
|
|
69
96
|
this.totalChildrenCount = obj.totalChildrenCount
|
|
@@ -78,9 +105,9 @@ export default class ObjectLoader {
|
|
|
78
105
|
|
|
79
106
|
/**
|
|
80
107
|
* Internal function used to recursively traverse an object and populate its references and dechunk any arrays.
|
|
81
|
-
* @param {*} obj
|
|
82
|
-
* @param {*} onProgress
|
|
83
|
-
* @returns
|
|
108
|
+
* @param {*} obj
|
|
109
|
+
* @param {*} onProgress
|
|
110
|
+
* @returns
|
|
84
111
|
*/
|
|
85
112
|
async traverseAndConstruct( obj, onProgress ) {
|
|
86
113
|
if( !obj ) return
|
|
@@ -91,20 +118,20 @@ export default class ObjectLoader {
|
|
|
91
118
|
let arr = []
|
|
92
119
|
for ( let element of obj ) {
|
|
93
120
|
if ( typeof element !== 'object' && ! this.options.fullyTraverseArrays ) return obj
|
|
94
|
-
|
|
121
|
+
|
|
95
122
|
// Dereference element if needed
|
|
96
123
|
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
|
-
|
|
124
|
+
if( element.referencedId && onProgress ) onProgress( { stage: 'construction', current: ++this.traversedReferencesCount > this.totalChildrenCount ? this.totalChildrenCount : this.traversedReferencesCount, total: this.totalChildrenCount } )
|
|
125
|
+
|
|
99
126
|
// Push the traversed object in the array
|
|
100
127
|
arr.push( await this.traverseAndConstruct( deRef, onProgress ) )
|
|
101
128
|
}
|
|
102
|
-
|
|
129
|
+
|
|
103
130
|
// De-chunk
|
|
104
|
-
if( arr[0]?.speckle_type?.toLowerCase().includes('datachunk') ) {
|
|
131
|
+
if( arr[0]?.speckle_type?.toLowerCase().includes('datachunk') ) {
|
|
105
132
|
return arr.reduce( ( prev, curr ) => prev.concat( curr.data ), [] )
|
|
106
133
|
}
|
|
107
|
-
|
|
134
|
+
|
|
108
135
|
return arr
|
|
109
136
|
}
|
|
110
137
|
|
|
@@ -113,11 +140,11 @@ export default class ObjectLoader {
|
|
|
113
140
|
for( let ignoredProp of this.options.excludeProps ) {
|
|
114
141
|
delete obj[ ignoredProp ]
|
|
115
142
|
}
|
|
116
|
-
|
|
143
|
+
|
|
117
144
|
// 2) Iterate through obj
|
|
118
|
-
for( let prop in obj ) {
|
|
145
|
+
for( let prop in obj ) {
|
|
119
146
|
if( typeof obj[prop] !== 'object' ) continue // leave alone primitive props
|
|
120
|
-
|
|
147
|
+
|
|
121
148
|
if( obj[prop].referencedId ) {
|
|
122
149
|
obj[prop] = await this.getObject( obj[prop].referencedId )
|
|
123
150
|
if( onProgress ) onProgress( { stage: 'construction', current: ++this.traversedReferencesCount > this.totalChildrenCount ? this.totalChildrenCount : this.traversedReferencesCount, total: this.totalChildrenCount } )
|
|
@@ -131,8 +158,8 @@ export default class ObjectLoader {
|
|
|
131
158
|
|
|
132
159
|
/**
|
|
133
160
|
* Internal function. Returns a promise that is resolved when the object id is loaded into the internal buffer.
|
|
134
|
-
* @param {*} id
|
|
135
|
-
* @returns
|
|
161
|
+
* @param {*} id
|
|
162
|
+
* @returns
|
|
136
163
|
*/
|
|
137
164
|
async getObject( id ){
|
|
138
165
|
if ( this.buffer[id] ) return this.buffer[id]
|
|
@@ -172,11 +199,15 @@ export default class ObjectLoader {
|
|
|
172
199
|
}
|
|
173
200
|
|
|
174
201
|
async * getObjectIterator( ) {
|
|
202
|
+
let t0 = Date.now()
|
|
203
|
+
let count = 0
|
|
175
204
|
for await ( let line of this.getRawObjectIterator() ) {
|
|
176
205
|
let { id, obj } = this.processLine( line )
|
|
177
206
|
this.buffer[ id ] = obj
|
|
207
|
+
count += 1
|
|
178
208
|
yield obj
|
|
179
209
|
}
|
|
210
|
+
console.log(`Loaded ${count} objects in: ${(Date.now() - t0) / 1000}`)
|
|
180
211
|
}
|
|
181
212
|
|
|
182
213
|
processLine( chunk ) {
|
|
@@ -185,31 +216,258 @@ export default class ObjectLoader {
|
|
|
185
216
|
}
|
|
186
217
|
|
|
187
218
|
async * getRawObjectIterator() {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
219
|
+
let tSTART = Date.now()
|
|
220
|
+
|
|
221
|
+
if ( this.options.enableCaching && window.indexedDB && this.cacheDB === null) {
|
|
222
|
+
await safariFix()
|
|
223
|
+
let idbOpenRequest = indexedDB.open('speckle-object-cache', 1)
|
|
224
|
+
idbOpenRequest.onupgradeneeded = () => idbOpenRequest.result.createObjectStore('objects');
|
|
225
|
+
this.cacheDB = await this.promisifyIdbRequest( idbOpenRequest )
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const rootObjJson = await this.getRawRootObject()
|
|
229
|
+
// console.log("Root in: ", Date.now() - tSTART)
|
|
230
|
+
|
|
231
|
+
yield `${this.objectId}\t${rootObjJson}`
|
|
232
|
+
|
|
233
|
+
const rootObj = JSON.parse(rootObjJson)
|
|
234
|
+
if ( !rootObj.__closure ) return
|
|
235
|
+
|
|
236
|
+
let childrenIds = Object.keys(rootObj.__closure).sort( (a, b) => rootObj.__closure[a] - rootObj.__closure[b] )
|
|
237
|
+
if ( childrenIds.length === 0 ) return
|
|
238
|
+
|
|
239
|
+
let splitHttpRequests = []
|
|
240
|
+
|
|
241
|
+
if ( childrenIds.length > 50 ) {
|
|
242
|
+
// split into 5%, 15%, 40%, 40% (5% for the high priority children: the ones with lower minDepth)
|
|
243
|
+
let splitBeforeCacheCheck = [ [], [], [], [] ]
|
|
244
|
+
let crtChildIndex = 0
|
|
245
|
+
|
|
246
|
+
for ( ; crtChildIndex < 0.05 * childrenIds.length; crtChildIndex++ ) {
|
|
247
|
+
splitBeforeCacheCheck[0].push( childrenIds[ crtChildIndex ] )
|
|
248
|
+
}
|
|
249
|
+
for ( ; crtChildIndex < 0.2 * childrenIds.length; crtChildIndex++ ) {
|
|
250
|
+
splitBeforeCacheCheck[1].push( childrenIds[ crtChildIndex ] )
|
|
251
|
+
}
|
|
252
|
+
for ( ; crtChildIndex < 0.6 * childrenIds.length; crtChildIndex++ ) {
|
|
253
|
+
splitBeforeCacheCheck[2].push( childrenIds[ crtChildIndex ] )
|
|
254
|
+
}
|
|
255
|
+
for ( ; crtChildIndex < childrenIds.length; crtChildIndex++ ) {
|
|
256
|
+
splitBeforeCacheCheck[3].push( childrenIds[ crtChildIndex ] )
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
console.log("Cache check for: ", splitBeforeCacheCheck)
|
|
261
|
+
|
|
262
|
+
let newChildren = []
|
|
263
|
+
let nextCachePromise = this.cacheGetObjects( splitBeforeCacheCheck[ 0 ] )
|
|
264
|
+
|
|
265
|
+
for ( let i = 0; i < 4; i++ ) {
|
|
266
|
+
let cachedObjects = await nextCachePromise
|
|
267
|
+
if ( i < 3 ) nextCachePromise = this.cacheGetObjects( splitBeforeCacheCheck[ i + 1 ] )
|
|
268
|
+
|
|
269
|
+
let sortedCachedKeys = Object.keys(cachedObjects).sort( (a, b) => rootObj.__closure[a] - rootObj.__closure[b] )
|
|
270
|
+
for ( let id of sortedCachedKeys ) {
|
|
271
|
+
yield `${id}\t${cachedObjects[ id ]}`
|
|
272
|
+
}
|
|
273
|
+
let newChildrenForBatch = splitBeforeCacheCheck[i].filter( id => !( id in cachedObjects ) )
|
|
274
|
+
newChildren.push( ...newChildrenForBatch )
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if ( newChildren.length === 0 ) return
|
|
193
278
|
|
|
194
|
-
|
|
195
|
-
|
|
279
|
+
if ( newChildren.length <= 50 ) {
|
|
280
|
+
// we have almost all of children in the cache. do only 1 requests for the remaining new children
|
|
281
|
+
splitHttpRequests.push( newChildren )
|
|
282
|
+
} else {
|
|
283
|
+
// we now set up the batches for 4 http requests, starting from `newChildren` (already sorted by priority)
|
|
284
|
+
splitHttpRequests = [ [], [], [], [] ]
|
|
285
|
+
crtChildIndex = 0
|
|
286
|
+
|
|
287
|
+
for ( ; crtChildIndex < 0.05 * newChildren.length; crtChildIndex++ ) {
|
|
288
|
+
splitHttpRequests[0].push( newChildren[ crtChildIndex ] )
|
|
289
|
+
}
|
|
290
|
+
for ( ; crtChildIndex < 0.2 * newChildren.length; crtChildIndex++ ) {
|
|
291
|
+
splitHttpRequests[1].push( newChildren[ crtChildIndex ] )
|
|
292
|
+
}
|
|
293
|
+
for ( ; crtChildIndex < 0.6 * newChildren.length; crtChildIndex++ ) {
|
|
294
|
+
splitHttpRequests[2].push( newChildren[ crtChildIndex ] )
|
|
295
|
+
}
|
|
296
|
+
for ( ; crtChildIndex < newChildren.length; crtChildIndex++ ) {
|
|
297
|
+
splitHttpRequests[3].push( newChildren[ crtChildIndex ] )
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
} else {
|
|
302
|
+
// small object with <= 50 children. check cache and make only 1 request
|
|
303
|
+
const cachedObjects = await this.cacheGetObjects( childrenIds )
|
|
304
|
+
let sortedCachedKeys = Object.keys(cachedObjects).sort( (a, b) => rootObj.__closure[a] - rootObj.__closure[b] )
|
|
305
|
+
for ( let id of sortedCachedKeys ) {
|
|
306
|
+
yield `${id}\t${cachedObjects[ id ]}`
|
|
307
|
+
}
|
|
308
|
+
childrenIds = childrenIds.filter(id => !( id in cachedObjects ) )
|
|
309
|
+
if ( childrenIds.length === 0 ) return
|
|
310
|
+
|
|
311
|
+
// only 1 http request with the remaining children ( <= 50 )
|
|
312
|
+
splitHttpRequests.push( childrenIds )
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Starting http requests for batches in `splitHttpRequests`
|
|
316
|
+
|
|
317
|
+
const decoders = []
|
|
318
|
+
const readers = []
|
|
319
|
+
const readPromisses = []
|
|
320
|
+
const startIndexes = []
|
|
321
|
+
const readBuffers = []
|
|
322
|
+
const finishedRequests = []
|
|
196
323
|
|
|
324
|
+
for (let i = 0; i < splitHttpRequests.length; i++) {
|
|
325
|
+
decoders.push(new TextDecoder())
|
|
326
|
+
readers.push( null )
|
|
327
|
+
readPromisses.push( null )
|
|
328
|
+
startIndexes.push( 0 )
|
|
329
|
+
readBuffers.push( '' )
|
|
330
|
+
finishedRequests.push( false )
|
|
331
|
+
|
|
332
|
+
fetch(
|
|
333
|
+
this.requestUrlChildren,
|
|
334
|
+
{
|
|
335
|
+
method: 'POST',
|
|
336
|
+
headers: { ...this.headers, 'Content-Type': 'application/json' },
|
|
337
|
+
body: JSON.stringify( { objects: JSON.stringify( splitHttpRequests[i] ) } )
|
|
338
|
+
}
|
|
339
|
+
).then( crtResponse => {
|
|
340
|
+
let crtReader = crtResponse.body.getReader()
|
|
341
|
+
readers[i] = crtReader
|
|
342
|
+
let crtReadPromise = crtReader.read().then(x => { x.reqId = i; return x })
|
|
343
|
+
readPromisses[i] = crtReadPromise
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
|
|
197
347
|
while ( true ) {
|
|
198
|
-
let
|
|
199
|
-
if (
|
|
200
|
-
if
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
348
|
+
let validReadPromises = readPromisses.filter(x => x != null)
|
|
349
|
+
if ( validReadPromises.length === 0 ) {
|
|
350
|
+
// Check if all requests finished
|
|
351
|
+
if ( finishedRequests.every(x => x) ) {
|
|
352
|
+
break
|
|
353
|
+
}
|
|
354
|
+
// Sleep 10 ms
|
|
355
|
+
await new Promise( ( resolve ) => {
|
|
356
|
+
setTimeout( resolve, 10 )
|
|
357
|
+
} )
|
|
205
358
|
continue
|
|
206
359
|
}
|
|
207
|
-
|
|
208
|
-
|
|
360
|
+
|
|
361
|
+
// Wait for data on any running request
|
|
362
|
+
let data = await Promise.any( validReadPromises )
|
|
363
|
+
let { value: crtDataChunk, done: readerDone, reqId } = data
|
|
364
|
+
finishedRequests[ reqId ] = readerDone
|
|
365
|
+
|
|
366
|
+
// Replace read promise on this request with a new `read` call
|
|
367
|
+
if ( !readerDone ) {
|
|
368
|
+
let crtReadPromise = readers[ reqId ].read().then(x => { x.reqId = reqId; return x })
|
|
369
|
+
readPromisses[ reqId ] = crtReadPromise
|
|
370
|
+
} else {
|
|
371
|
+
// This request finished. "Flush any non-newline-terminated text"
|
|
372
|
+
if ( readBuffers[ reqId ].length > 0 ) {
|
|
373
|
+
yield readBuffers[ reqId ]
|
|
374
|
+
readBuffers[ reqId ] = ''
|
|
375
|
+
}
|
|
376
|
+
// no other read calls for this request
|
|
377
|
+
readPromisses[ reqId ] = null
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if ( !crtDataChunk )
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
crtDataChunk = decoders[ reqId ].decode( crtDataChunk )
|
|
384
|
+
let unprocessedText = readBuffers[ reqId ] + crtDataChunk
|
|
385
|
+
let unprocessedLines = unprocessedText.split(/\r\n|\n|\r/)
|
|
386
|
+
let remainderText = unprocessedLines.pop()
|
|
387
|
+
readBuffers[ reqId ] = remainderText
|
|
388
|
+
|
|
389
|
+
for ( let line of unprocessedLines ) {
|
|
390
|
+
yield line
|
|
391
|
+
}
|
|
392
|
+
this.cacheStoreObjects(unprocessedLines)
|
|
209
393
|
}
|
|
394
|
+
}
|
|
210
395
|
|
|
211
|
-
|
|
212
|
-
|
|
396
|
+
async getRawRootObject() {
|
|
397
|
+
const cachedRootObject = await this.cacheGetObjects( [ this.objectId ] )
|
|
398
|
+
if ( cachedRootObject[ this.objectId ] )
|
|
399
|
+
return cachedRootObject[ this.objectId ]
|
|
400
|
+
const response = await fetch( this.requestUrlRootObj, { headers: this.headers } )
|
|
401
|
+
const responseText = await response.text()
|
|
402
|
+
this.cacheStoreObjects( [ `${this.objectId}\t${responseText}` ] )
|
|
403
|
+
return responseText
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
promisifyIdbRequest(request) {
|
|
407
|
+
return new Promise((resolve, reject) => {
|
|
408
|
+
request.oncomplete = request.onsuccess = () => resolve(request.result);
|
|
409
|
+
request.onabort = request.onerror = () => reject(request.error);
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async cacheGetObjects(ids) {
|
|
414
|
+
if ( !this.options.enableCaching || !window.indexedDB ) {
|
|
415
|
+
return {}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
let ret = {}
|
|
419
|
+
|
|
420
|
+
for (let i = 0; i < ids.length; i += 500) {
|
|
421
|
+
let idsChunk = ids.slice(i, i + 500)
|
|
422
|
+
let t0 = Date.now()
|
|
423
|
+
|
|
424
|
+
let store = this.cacheDB.transaction('objects', 'readonly').objectStore('objects')
|
|
425
|
+
let idbChildrenPromises = idsChunk.map( id => this.promisifyIdbRequest( store.get( id ) ).then( data => ( { id, data } ) ) )
|
|
426
|
+
let cachedData = await Promise.all(idbChildrenPromises)
|
|
427
|
+
|
|
428
|
+
// console.log("Cache check for : ", idsChunk.length, Date.now() - t0)
|
|
429
|
+
|
|
430
|
+
for ( let cachedObj of cachedData ) {
|
|
431
|
+
if ( !cachedObj.data ) // non-existent objects are retrieved with `undefined` data
|
|
432
|
+
continue
|
|
433
|
+
ret[ cachedObj.id ] = cachedObj.data
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return ret
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
cacheStoreObjects(objects) {
|
|
441
|
+
if ( !this.options.enableCaching || !window.indexedDB ) {
|
|
442
|
+
return {}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
let store = this.cacheDB.transaction('objects', 'readwrite').objectStore('objects')
|
|
446
|
+
for ( let obj of objects ) {
|
|
447
|
+
let idAndData = obj.split( '\t' )
|
|
448
|
+
store.put(idAndData[1], idAndData[0])
|
|
213
449
|
}
|
|
450
|
+
|
|
451
|
+
return this.promisifyIdbRequest( store.transaction )
|
|
214
452
|
}
|
|
215
453
|
}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
// Credits and more info: https://github.com/jakearchibald/safari-14-idb-fix
|
|
457
|
+
function safariFix() {
|
|
458
|
+
const isSafari =
|
|
459
|
+
!navigator.userAgentData &&
|
|
460
|
+
/Safari\//.test(navigator.userAgent) &&
|
|
461
|
+
!/Chrom(e|ium)\//.test(navigator.userAgent)
|
|
462
|
+
|
|
463
|
+
// No point putting other browsers or older versions of Safari through this mess.
|
|
464
|
+
if (!isSafari || !indexedDB.databases) return Promise.resolve()
|
|
465
|
+
|
|
466
|
+
let intervalId
|
|
467
|
+
|
|
468
|
+
return new Promise( ( resolve ) => {
|
|
469
|
+
const tryIdb = () => indexedDB.databases().finally(resolve)
|
|
470
|
+
intervalId = setInterval(tryIdb, 100)
|
|
471
|
+
tryIdb()
|
|
472
|
+
}).finally( () => clearInterval(intervalId) )
|
|
473
|
+
}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -10,7 +10,7 @@ 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
|
Here's a sample way on how to use it, pfilfered from the [3d viewer package](../viewer):
|
|
16
16
|
|