@speckle/objectloader2 2.24.0 → 2.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/index.js +6 -7
- package/dist/esm/index.js +3 -3
- package/eslint.config.mjs +3 -1
- package/package.json +2 -2
- package/src/helpers/__snapshots__/cachePump.spec.ts.snap +31 -0
- package/src/helpers/__snapshots__/cacheReader.spec.ts.snap +8 -0
- package/src/helpers/aggregateQueue.ts +20 -0
- package/src/helpers/batchedPool.ts +5 -9
- package/src/helpers/batchingQueue.ts +21 -13
- package/src/helpers/cachePump.disposal.spec.ts +49 -0
- package/src/helpers/cachePump.spec.ts +103 -0
- package/src/helpers/cachePump.ts +99 -0
- package/src/helpers/cacheReader.spec.ts +35 -0
- package/src/helpers/cacheReader.ts +64 -0
- package/src/helpers/defermentManager.disposal.spec.ts +28 -0
- package/src/helpers/defermentManager.spec.ts +25 -1
- package/src/helpers/defermentManager.ts +128 -12
- package/src/helpers/deferredBase.ts +44 -6
- package/src/helpers/keyedQueue.ts +45 -0
- package/src/helpers/memoryPump.ts +40 -0
- package/src/helpers/pump.ts +8 -0
- package/src/index.ts +3 -4
- package/src/operations/__snapshots__/objectLoader2.spec.ts.snap +16 -16
- package/src/operations/{__snapshots__ → databases/__snapshots__}/indexedDatabase.spec.ts.snap +0 -21
- package/src/operations/{indexedDatabase.spec.ts → databases/indexedDatabase.spec.ts} +2 -28
- package/src/operations/databases/indexedDatabase.ts +150 -0
- package/src/operations/databases/memoryDatabase.ts +43 -0
- package/src/operations/{__snapshots__ → downloaders/__snapshots__}/serverDownloader.spec.ts.snap +34 -0
- package/src/operations/{memoryDownloader.ts → downloaders/memoryDownloader.ts} +15 -14
- package/src/operations/{serverDownloader.spec.ts → downloaders/serverDownloader.spec.ts} +68 -43
- package/src/operations/{serverDownloader.ts → downloaders/serverDownloader.ts} +92 -38
- package/src/operations/interfaces.ts +11 -12
- package/src/operations/objectLoader2.spec.ts +76 -144
- package/src/operations/objectLoader2.ts +57 -79
- package/src/operations/objectLoader2Factory.ts +56 -0
- package/src/operations/options.ts +18 -37
- package/src/operations/traverser.spec.ts +1 -1
- package/src/operations/traverser.ts +1 -1
- package/src/test/e2e.spec.ts +4 -4
- package/src/types/types.ts +11 -0
- package/src/operations/indexedDatabase.ts +0 -167
- package/src/operations/memoryDatabase.ts +0 -42
|
@@ -1,25 +1,141 @@
|
|
|
1
1
|
import { DeferredBase } from './deferredBase.js'
|
|
2
|
-
import { Base, Item } from '../types/types.js'
|
|
2
|
+
import { Base, CustomLogger, Item } from '../types/types.js'
|
|
3
|
+
import { DefermentManagerOptions } from '../operations/options.js'
|
|
3
4
|
|
|
4
5
|
export class DefermentManager {
|
|
5
|
-
|
|
6
|
+
private deferments: Map<string, DeferredBase> = new Map()
|
|
7
|
+
private timer?: ReturnType<typeof setTimeout>
|
|
8
|
+
private logger: CustomLogger
|
|
9
|
+
private currentSize = 0
|
|
10
|
+
private disposed = false
|
|
11
|
+
|
|
12
|
+
constructor(private options: DefermentManagerOptions) {
|
|
13
|
+
this.resetGlobalTimer()
|
|
14
|
+
this.logger = options.logger || ((): void => {})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private now(): number {
|
|
18
|
+
return Date.now()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
isDeferred(id: string): boolean {
|
|
22
|
+
return this.deferments.has(id)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get(id: string): DeferredBase | undefined {
|
|
26
|
+
if (this.disposed) throw new Error('DefermentManager is disposed')
|
|
27
|
+
return this.deferments.get(id)
|
|
28
|
+
}
|
|
6
29
|
|
|
7
30
|
async defer(params: { id: string }): Promise<Base> {
|
|
8
|
-
|
|
31
|
+
if (this.disposed) throw new Error('DefermentManager is disposed')
|
|
32
|
+
const now = this.now()
|
|
33
|
+
const deferredBase = this.deferments.get(params.id)
|
|
9
34
|
if (deferredBase) {
|
|
10
|
-
|
|
35
|
+
deferredBase.setAccess(now)
|
|
36
|
+
return deferredBase.getPromise()
|
|
11
37
|
}
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
38
|
+
const notYetFound = new DeferredBase(
|
|
39
|
+
this.options.ttlms,
|
|
40
|
+
params.id,
|
|
41
|
+
now + this.options.ttlms
|
|
42
|
+
)
|
|
43
|
+
this.deferments.set(params.id, notYetFound)
|
|
44
|
+
return notYetFound.getPromise()
|
|
15
45
|
}
|
|
16
46
|
|
|
17
47
|
undefer(item: Item): void {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
48
|
+
if (this.disposed) throw new Error('DefermentManager is disposed')
|
|
49
|
+
const now = this.now()
|
|
50
|
+
this.currentSize += item.size || 0
|
|
51
|
+
//order matters here with found before undefer
|
|
52
|
+
const deferredBase = this.deferments.get(item.baseId)
|
|
53
|
+
if (deferredBase) {
|
|
54
|
+
deferredBase.found(item)
|
|
55
|
+
deferredBase.setAccess(now)
|
|
56
|
+
} else {
|
|
57
|
+
const existing = new DeferredBase(this.options.ttlms, item.baseId, now)
|
|
58
|
+
existing.found(item)
|
|
59
|
+
this.deferments.set(item.baseId, existing)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private resetGlobalTimer(): void {
|
|
64
|
+
const run = (): void => {
|
|
65
|
+
this.cleanDeferments()
|
|
66
|
+
this.timer = setTimeout(run, this.options.ttlms)
|
|
67
|
+
}
|
|
68
|
+
this.timer = setTimeout(run, this.options.ttlms)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
dispose(): void {
|
|
72
|
+
if (this.disposed) return
|
|
73
|
+
this.disposed = true
|
|
74
|
+
if (this.timer) {
|
|
75
|
+
clearTimeout(this.timer)
|
|
76
|
+
this.timer = undefined
|
|
23
77
|
}
|
|
78
|
+
this.clearDeferments()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private clearDeferments(): void {
|
|
82
|
+
let waiting = 0
|
|
83
|
+
for (const deferredBase of this.deferments.values()) {
|
|
84
|
+
deferredBase.done(0)
|
|
85
|
+
if (deferredBase.getItem() === undefined) {
|
|
86
|
+
waiting++
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
this.currentSize = 0
|
|
90
|
+
this.deferments.clear()
|
|
91
|
+
this.logger('cleared deferments, left', waiting)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private cleanDeferments(): void {
|
|
95
|
+
const maxSizeBytes = this.options.maxSizeInMb * 1024 * 1024
|
|
96
|
+
if (this.currentSize < maxSizeBytes) {
|
|
97
|
+
this.logger(
|
|
98
|
+
'deferments size is ok, no need to clean',
|
|
99
|
+
this.currentSize,
|
|
100
|
+
maxSizeBytes
|
|
101
|
+
)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
const now = this.now()
|
|
105
|
+
let cleaned = 0
|
|
106
|
+
const start = performance.now()
|
|
107
|
+
for (const deferredBase of Array.from(this.deferments.values())
|
|
108
|
+
.filter((x) => x.isExpired(now))
|
|
109
|
+
.sort((a, b) => this.compareMaybeBasesBySize(a.getItem(), b.getItem()))) {
|
|
110
|
+
if (deferredBase.done(now)) {
|
|
111
|
+
this.currentSize -= deferredBase.getItem()?.size || 0
|
|
112
|
+
this.deferments.delete(deferredBase.getId())
|
|
113
|
+
cleaned++
|
|
114
|
+
if (this.currentSize < maxSizeBytes) {
|
|
115
|
+
break
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
this.logger(
|
|
120
|
+
'cleaned deferments, cleaned, left',
|
|
121
|
+
cleaned,
|
|
122
|
+
this.deferments.size,
|
|
123
|
+
performance.now() - start
|
|
124
|
+
)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
compareMaybeBasesBySize(a: Item | undefined, b: Item | undefined): number {
|
|
129
|
+
if (a === undefined && b === undefined) return 0
|
|
130
|
+
if (a === undefined) return -1
|
|
131
|
+
if (b === undefined) return 1
|
|
132
|
+
return this.compareMaybe(a.size, b.size)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
compareMaybe(a: number | undefined, b: number | undefined): number {
|
|
136
|
+
if (a === undefined && b === undefined) return 0
|
|
137
|
+
if (a === undefined) return -1
|
|
138
|
+
if (b === undefined) return 1
|
|
139
|
+
return a - b
|
|
24
140
|
}
|
|
25
141
|
}
|
|
@@ -1,17 +1,55 @@
|
|
|
1
|
-
import { Base } from '../types/types.js'
|
|
1
|
+
import { Base, Item } from '../types/types.js'
|
|
2
2
|
|
|
3
3
|
export class DeferredBase {
|
|
4
|
-
promise: Promise<Base>
|
|
5
|
-
resolve!: (value: Base) => void
|
|
6
|
-
reject!: (reason?: Error) => void
|
|
4
|
+
private promise: Promise<Base>
|
|
5
|
+
private resolve!: (value: Base) => void
|
|
6
|
+
private reject!: (reason?: Error) => void
|
|
7
|
+
private item?: Item
|
|
7
8
|
|
|
8
|
-
readonly id: string
|
|
9
|
+
private readonly id: string
|
|
10
|
+
private expiresAt: number // Timestamp in ms
|
|
11
|
+
private ttl: number // ttl in ms
|
|
9
12
|
|
|
10
|
-
constructor(id: string) {
|
|
13
|
+
constructor(ttl: number, id: string, expiresAt: number) {
|
|
14
|
+
this.expiresAt = expiresAt
|
|
15
|
+
this.ttl = ttl
|
|
11
16
|
this.id = id
|
|
12
17
|
this.promise = new Promise<Base>((resolve, reject) => {
|
|
13
18
|
this.resolve = resolve
|
|
14
19
|
this.reject = reject
|
|
15
20
|
})
|
|
16
21
|
}
|
|
22
|
+
|
|
23
|
+
getId(): string {
|
|
24
|
+
return this.id
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getItem(): Item | undefined {
|
|
28
|
+
return this.item
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getPromise(): Promise<Base> {
|
|
32
|
+
return this.promise
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
isExpired(now: number): boolean {
|
|
36
|
+
return this.item !== undefined && now > this.expiresAt
|
|
37
|
+
}
|
|
38
|
+
setAccess(now: number): void {
|
|
39
|
+
this.expiresAt = now + this.ttl
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
found(value: Item): void {
|
|
43
|
+
this.item = value
|
|
44
|
+
this.resolve(value.base)
|
|
45
|
+
}
|
|
46
|
+
done(now: number): boolean {
|
|
47
|
+
if (this.item) {
|
|
48
|
+
this.resolve(this.item.base)
|
|
49
|
+
}
|
|
50
|
+
if (this.isExpired(now)) {
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
17
55
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export default class KeyedQueue<K, V> {
|
|
2
|
+
private _map: Map<K, V>
|
|
3
|
+
private _order: K[]
|
|
4
|
+
|
|
5
|
+
constructor() {
|
|
6
|
+
this._map = new Map<K, V>()
|
|
7
|
+
this._order = []
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
enqueue(key: K, value: V): boolean {
|
|
11
|
+
if (this._map.has(key)) {
|
|
12
|
+
return false // Key already exists
|
|
13
|
+
}
|
|
14
|
+
this._map.set(key, value)
|
|
15
|
+
this._order.push(key)
|
|
16
|
+
return true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get(key: K): V | undefined {
|
|
20
|
+
return this._map.get(key)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
has(key: K): boolean {
|
|
24
|
+
return this._map.has(key)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get size(): number {
|
|
28
|
+
return this._order.length
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
spliceValues(start: number, deleteCount: number): V[] {
|
|
32
|
+
const splicedKeys = this._order.splice(start, deleteCount)
|
|
33
|
+
const result: V[] = []
|
|
34
|
+
|
|
35
|
+
for (const key of splicedKeys) {
|
|
36
|
+
const value = this._map.get(key)
|
|
37
|
+
if (value !== undefined) {
|
|
38
|
+
result.push(value)
|
|
39
|
+
this._map.delete(key)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Item } from '../types/types.js'
|
|
2
|
+
import { Pump } from './pump.js'
|
|
3
|
+
import Queue from './queue.js'
|
|
4
|
+
|
|
5
|
+
export class MemoryPump implements Pump {
|
|
6
|
+
#items: Map<string, Item> = new Map()
|
|
7
|
+
|
|
8
|
+
add(item: Item): void {
|
|
9
|
+
this.#items.set(item.baseId, item)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async pumpItems(params: {
|
|
13
|
+
ids: string[]
|
|
14
|
+
foundItems: Queue<Item>
|
|
15
|
+
notFoundItems: Queue<string>
|
|
16
|
+
}): Promise<void> {
|
|
17
|
+
const { ids, foundItems, notFoundItems } = params
|
|
18
|
+
for (const id of ids) {
|
|
19
|
+
const item = this.#items.get(id)
|
|
20
|
+
if (item) {
|
|
21
|
+
foundItems.add(item)
|
|
22
|
+
} else {
|
|
23
|
+
notFoundItems.add(id)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return Promise.resolve()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async *gather(ids: string[]): AsyncGenerator<Item> {
|
|
30
|
+
for (const id of ids) {
|
|
31
|
+
const item = this.#items.get(id)
|
|
32
|
+
if (item) {
|
|
33
|
+
yield item
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return Promise.resolve()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async disposeAsync(): Promise<void> {}
|
|
40
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Downloader } from '../operations/interfaces.js'
|
|
2
|
+
import { Item } from '../types/types.js'
|
|
3
|
+
import Queue from './queue.js'
|
|
4
|
+
|
|
5
|
+
export interface Pump extends Queue<Item> {
|
|
6
|
+
gather(ids: string[], downloader: Downloader): AsyncGenerator<Item>
|
|
7
|
+
disposeAsync(): Promise<void>
|
|
8
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
export { MemoryDatabase } from './operations/memoryDatabase.js'
|
|
1
|
+
export { MemoryDatabase } from './operations/databases/memoryDatabase.js'
|
|
2
|
+
export { ObjectLoader2 } from './operations/objectLoader2.js'
|
|
3
|
+
export { ObjectLoader2Factory } from './operations/objectLoader2Factory.js'
|
|
@@ -2,35 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
exports[`objectloader2 > add extra header 1`] = `
|
|
4
4
|
{
|
|
5
|
-
"
|
|
5
|
+
"base": {
|
|
6
|
+
"__closure": {
|
|
7
|
+
"child1Id": 100,
|
|
8
|
+
},
|
|
9
|
+
"id": "rootId",
|
|
10
|
+
"speckle_type": "type",
|
|
11
|
+
},
|
|
12
|
+
"baseId": "rootId",
|
|
6
13
|
}
|
|
7
14
|
`;
|
|
8
15
|
|
|
9
16
|
exports[`objectloader2 > can get a root object from cache 1`] = `
|
|
10
17
|
{
|
|
18
|
+
"base": {
|
|
19
|
+
"id": "baseId",
|
|
20
|
+
"speckle_type": "type",
|
|
21
|
+
},
|
|
11
22
|
"baseId": "baseId",
|
|
12
23
|
}
|
|
13
24
|
`;
|
|
14
25
|
|
|
15
26
|
exports[`objectloader2 > can get a root object from downloader 1`] = `
|
|
16
27
|
{
|
|
17
|
-
"
|
|
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
|
+
"base": {
|
|
29
|
+
"id": "baseId",
|
|
28
30
|
"speckle_type": "type",
|
|
29
31
|
},
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
},
|
|
33
|
-
]
|
|
32
|
+
"baseId": "baseId",
|
|
33
|
+
}
|
|
34
34
|
`;
|
|
35
35
|
|
|
36
36
|
exports[`objectloader2 > can get root/child object from memory cache using iterator and getObject 1`] = `
|
package/src/operations/{__snapshots__ → databases/__snapshots__}/indexedDatabase.spec.ts.snap
RENAMED
|
@@ -29,24 +29,3 @@ exports[`database cache > write two items to queue use getItem 2`] = `
|
|
|
29
29
|
"baseId": "id2",
|
|
30
30
|
}
|
|
31
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`] = `[]`;
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest'
|
|
2
2
|
import IndexedDatabase from './indexedDatabase.js'
|
|
3
3
|
import { IDBFactory, IDBKeyRange } from 'fake-indexeddb'
|
|
4
|
-
import { Item } from '
|
|
5
|
-
import BufferQueue from '../helpers/bufferQueue.js'
|
|
4
|
+
import { Item } from '../../types/types.js'
|
|
6
5
|
|
|
7
6
|
describe('database cache', () => {
|
|
8
7
|
test('write single item to queue use getItem', async () => {
|
|
9
8
|
const i: Item = { baseId: 'id', base: { id: 'id', speckle_type: 'type' } }
|
|
10
9
|
const database = new IndexedDatabase({
|
|
11
10
|
indexedDB: new IDBFactory(),
|
|
12
|
-
keyRange: IDBKeyRange
|
|
13
|
-
maxCacheBatchWriteWait: 200
|
|
11
|
+
keyRange: IDBKeyRange
|
|
14
12
|
})
|
|
15
13
|
await database.add(i)
|
|
16
14
|
await database.disposeAsync()
|
|
@@ -36,28 +34,4 @@ describe('database cache', () => {
|
|
|
36
34
|
const x2 = await database.getItem({ id: i2.baseId })
|
|
37
35
|
expect(x2).toMatchSnapshot()
|
|
38
36
|
})
|
|
39
|
-
|
|
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' } }
|
|
43
|
-
const database = new IndexedDatabase({
|
|
44
|
-
indexedDB: new IDBFactory(),
|
|
45
|
-
keyRange: IDBKeyRange
|
|
46
|
-
})
|
|
47
|
-
await database.add(i1)
|
|
48
|
-
await database.add(i2)
|
|
49
|
-
await database.disposeAsync()
|
|
50
|
-
|
|
51
|
-
const foundItems = new BufferQueue<Item>()
|
|
52
|
-
const notFoundItems = new BufferQueue<string>()
|
|
53
|
-
|
|
54
|
-
await database.processItems({
|
|
55
|
-
ids: [i1.baseId, i2.baseId],
|
|
56
|
-
foundItems,
|
|
57
|
-
notFoundItems
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
expect(foundItems.values()).toMatchSnapshot()
|
|
61
|
-
expect(notFoundItems.values()).toMatchSnapshot()
|
|
62
|
-
})
|
|
63
37
|
})
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
|
2
|
+
import BatchingQueue from '../../helpers/batchingQueue.js'
|
|
3
|
+
import { CustomLogger, Item } from '../../types/types.js'
|
|
4
|
+
import { isSafari } from '@speckle/shared'
|
|
5
|
+
import { Dexie, DexieOptions, Table } from 'dexie'
|
|
6
|
+
import { Database } from '../interfaces.js'
|
|
7
|
+
|
|
8
|
+
class ObjectStore extends Dexie {
|
|
9
|
+
static #databaseName: string = 'speckle-cache'
|
|
10
|
+
objects!: Table<Item, string> // Table type: <entity, primaryKey>
|
|
11
|
+
|
|
12
|
+
constructor(options: DexieOptions) {
|
|
13
|
+
super(ObjectStore.#databaseName, options)
|
|
14
|
+
|
|
15
|
+
this.version(1).stores({
|
|
16
|
+
objects: 'baseId, item' // baseId is primary key
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface IndexedDatabaseOptions {
|
|
22
|
+
logger?: CustomLogger
|
|
23
|
+
indexedDB?: IDBFactory
|
|
24
|
+
keyRange?: {
|
|
25
|
+
bound: Function
|
|
26
|
+
lowerBound: Function
|
|
27
|
+
upperBound: Function
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default class IndexedDatabase implements Database {
|
|
32
|
+
#options: IndexedDatabaseOptions
|
|
33
|
+
#logger: CustomLogger
|
|
34
|
+
|
|
35
|
+
#cacheDB?: ObjectStore
|
|
36
|
+
|
|
37
|
+
#writeQueue: BatchingQueue<Item> | undefined
|
|
38
|
+
|
|
39
|
+
// #count: number = 0
|
|
40
|
+
|
|
41
|
+
constructor(options: IndexedDatabaseOptions) {
|
|
42
|
+
this.#options = options
|
|
43
|
+
this.#logger = options.logger || ((): void => {})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async getAll(keys: string[]): Promise<(Item | undefined)[]> {
|
|
47
|
+
await this.#setupCacheDb()
|
|
48
|
+
let items: (Item | undefined)[] = []
|
|
49
|
+
// this.#count++
|
|
50
|
+
// const startTime = performance.now()
|
|
51
|
+
// this.#logger('Start read ' + x + ' ' + batch.length)
|
|
52
|
+
|
|
53
|
+
//faster than BulkGet with dexie
|
|
54
|
+
await this.#cacheDB!.transaction('r', this.#cacheDB!.objects, async () => {
|
|
55
|
+
const gets = keys.map((key) => this.#cacheDB!.objects.get(key))
|
|
56
|
+
const cachedData = await Promise.all(gets)
|
|
57
|
+
items = cachedData
|
|
58
|
+
})
|
|
59
|
+
// const endTime = performance.now()
|
|
60
|
+
// const duration = endTime - startTime
|
|
61
|
+
//this.#logger('Saved batch ' + x + ' ' + batch.length + ' ' + duration / TIME_MS.second)
|
|
62
|
+
|
|
63
|
+
return items
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async #openDatabase(): Promise<ObjectStore> {
|
|
67
|
+
const db = new ObjectStore({
|
|
68
|
+
indexedDB: this.#options.indexedDB ?? globalThis.indexedDB,
|
|
69
|
+
IDBKeyRange: this.#options.keyRange ?? IDBKeyRange,
|
|
70
|
+
chromeTransactionDurability: 'relaxed'
|
|
71
|
+
})
|
|
72
|
+
await db.open()
|
|
73
|
+
return db
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async #setupCacheDb(): Promise<void> {
|
|
77
|
+
if (this.#cacheDB !== undefined) {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Initialize
|
|
82
|
+
await this.#safariFix()
|
|
83
|
+
this.#cacheDB = await this.#openDatabase()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//this is for testing only - in the real world we will not use this
|
|
87
|
+
async add(item: Item): Promise<void> {
|
|
88
|
+
await this.#setupCacheDb()
|
|
89
|
+
await this.#cacheDB!.transaction('rw', this.#cacheDB!.objects, async () => {
|
|
90
|
+
return await this.#cacheDB?.objects.add(item)
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async getItem(params: { id: string }): Promise<Item | undefined> {
|
|
95
|
+
const { id } = params
|
|
96
|
+
await this.#setupCacheDb()
|
|
97
|
+
//might not be in the real DB yet, so check the write queue first
|
|
98
|
+
if (this.#writeQueue) {
|
|
99
|
+
const item = this.#writeQueue.get(id)
|
|
100
|
+
if (item) {
|
|
101
|
+
return item
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return this.#cacheDB!.transaction('r', this.#cacheDB!.objects, async () => {
|
|
106
|
+
return await this.#cacheDB?.objects.get(id)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async cacheSaveBatch(params: { batch: Item[] }): Promise<void> {
|
|
111
|
+
await this.#setupCacheDb()
|
|
112
|
+
const { batch } = params
|
|
113
|
+
//const x = this.#count
|
|
114
|
+
//this.#count++
|
|
115
|
+
|
|
116
|
+
// const startTime = performance.now()
|
|
117
|
+
// this.#logger('Start save ' + x + ' ' + batch.length)
|
|
118
|
+
await this.#cacheDB!.objects.bulkPut(batch)
|
|
119
|
+
// const endTime = performance.now()
|
|
120
|
+
// const duration = endTime - startTime
|
|
121
|
+
//this.#logger('Saved batch ' + x + ' ' + batch.length + ' ' + duration / TIME_MS.second)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Fixes a Safari bug where IndexedDB requests get lost and never resolve - invoke before you use IndexedDB
|
|
126
|
+
* @link Credits and more info: https://github.com/jakearchibald/safari-14-idb-fix
|
|
127
|
+
*/
|
|
128
|
+
async #safariFix(): Promise<void> {
|
|
129
|
+
// No point putting other browsers or older versions of Safari through this mess.
|
|
130
|
+
if (!isSafari() || !this.#options.indexedDB?.databases) return Promise.resolve()
|
|
131
|
+
|
|
132
|
+
let intervalId: ReturnType<typeof setInterval>
|
|
133
|
+
|
|
134
|
+
return new Promise<void>((resolve: () => void) => {
|
|
135
|
+
const tryIdb = (): Promise<IDBDatabaseInfo[]> | undefined =>
|
|
136
|
+
this.#options.indexedDB?.databases().finally(resolve)
|
|
137
|
+
intervalId = setInterval(() => {
|
|
138
|
+
void tryIdb()
|
|
139
|
+
}, 100)
|
|
140
|
+
void tryIdb()
|
|
141
|
+
}).finally(() => clearInterval(intervalId))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async disposeAsync(): Promise<void> {
|
|
145
|
+
this.#cacheDB?.close()
|
|
146
|
+
this.#cacheDB = undefined
|
|
147
|
+
await this.#writeQueue?.disposeAsync()
|
|
148
|
+
this.#writeQueue = undefined
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Base, Item } from '../../types/types.js'
|
|
2
|
+
import { Database } from '../interfaces.js'
|
|
3
|
+
import { MemoryDatabaseOptions } from '../options.js'
|
|
4
|
+
|
|
5
|
+
export class MemoryDatabase implements Database {
|
|
6
|
+
private items: Map<string, Base>
|
|
7
|
+
|
|
8
|
+
constructor(options?: MemoryDatabaseOptions) {
|
|
9
|
+
this.items = options?.items || new Map<string, Base>()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getAll(keys: string[]): Promise<(Item | undefined)[]> {
|
|
13
|
+
const found: (Item | undefined)[] = []
|
|
14
|
+
for (const key of keys) {
|
|
15
|
+
const item = this.items.get(key)
|
|
16
|
+
if (item) {
|
|
17
|
+
found.push({ baseId: key, base: item })
|
|
18
|
+
} else {
|
|
19
|
+
found.push(undefined)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return Promise.resolve(found)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
cacheSaveBatch({ batch }: { batch: Item[] }): Promise<void> {
|
|
26
|
+
for (const item of batch) {
|
|
27
|
+
this.items.set(item.baseId, item.base)
|
|
28
|
+
}
|
|
29
|
+
return Promise.resolve()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getItem(params: { id: string }): Promise<Item | undefined> {
|
|
33
|
+
const item = this.items.get(params.id)
|
|
34
|
+
if (item) {
|
|
35
|
+
return Promise.resolve({ baseId: params.id, base: item })
|
|
36
|
+
}
|
|
37
|
+
return Promise.resolve(undefined)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
disposeAsync(): Promise<void> {
|
|
41
|
+
return Promise.resolve()
|
|
42
|
+
}
|
|
43
|
+
}
|