Move studio-related asset storage code out of core (#369)
* Move studio asset storage code out of core * Require blob-compare conditinally Co-authored-by: Aria <aria.minaei@gmail.com>
This commit is contained in:
parent
718beb4d7b
commit
ccabda65a3
4 changed files with 188 additions and 208 deletions
|
@ -1,191 +0,0 @@
|
||||||
import {createStore} from './IDBStorage'
|
|
||||||
import type Project from './Project'
|
|
||||||
import type {IAssetStorageConfig} from './Project'
|
|
||||||
// @ts-ignore
|
|
||||||
import blobCompare from 'blob-compare'
|
|
||||||
import {notify} from '@theatre/core/coreExports'
|
|
||||||
import {getAllPossibleAssetIDs} from '@theatre/shared/utils/assets'
|
|
||||||
|
|
||||||
export const createDefaultAssetStorageConfig = ({
|
|
||||||
project,
|
|
||||||
baseUrl = '',
|
|
||||||
}: {
|
|
||||||
project: Project
|
|
||||||
baseUrl?: string
|
|
||||||
}): IAssetStorageConfig => {
|
|
||||||
return {
|
|
||||||
coreAssetStorage: {
|
|
||||||
getAssetUrl: (assetId: string) => `${baseUrl}/${assetId}`,
|
|
||||||
},
|
|
||||||
createStudioAssetStorage: async () => {
|
|
||||||
// in SSR we bail out and return a dummy asset manager
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return {
|
|
||||||
getAssetUrl: () => '',
|
|
||||||
createAsset: () => Promise.resolve(null),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for support.
|
|
||||||
if (!('indexedDB' in window)) {
|
|
||||||
console.log("This browser doesn't support IndexedDB.")
|
|
||||||
|
|
||||||
return {
|
|
||||||
getAssetUrl: (assetId: string) => {
|
|
||||||
throw new Error(
|
|
||||||
`IndexedDB is required by the default asset manager, but it's not supported by this browser. To use assets, please provide your own asset manager to the project config.`,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
createAsset: (asset: Blob) => {
|
|
||||||
throw new Error(
|
|
||||||
`IndexedDB is required by the default asset manager, but it's not supported by this browser. To use assets, please provide your own asset manager to the project config.`,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const idb = createStore(`${project.address.projectId}-assets`)
|
|
||||||
|
|
||||||
// get all possible asset ids referenced by either static props or keyframes
|
|
||||||
const possibleAssetIDs = getAllPossibleAssetIDs(project)
|
|
||||||
|
|
||||||
// Clean up assets not referenced by the project. We can only do this at the start because otherwise
|
|
||||||
// we'd break undo/redo.
|
|
||||||
const idbKeys = await idb.keys<string>()
|
|
||||||
await Promise.all(
|
|
||||||
idbKeys.map(async (key) => {
|
|
||||||
if (!possibleAssetIDs.includes(key)) {
|
|
||||||
await idb.del(key)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Clean up idb entries exported to disk
|
|
||||||
await Promise.all(
|
|
||||||
idbKeys.map(async (key) => {
|
|
||||||
const assetUrl = `${baseUrl}/${key}`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(assetUrl, {method: 'HEAD'})
|
|
||||||
if (response.ok) {
|
|
||||||
await idb.del(key)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
notify.error(
|
|
||||||
'Failed to access assets',
|
|
||||||
`Failed to access assets at ${
|
|
||||||
project.config.assets?.baseUrl ?? '/'
|
|
||||||
}. This is likely due to a CORS issue.`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
// A map for caching the assets outside of the db. We also need this to be able to retrieve idb asset urls synchronously.
|
|
||||||
const assetsMap = new Map(await idb.entries<string, Blob>())
|
|
||||||
|
|
||||||
// A map for caching the object urls created from idb assets.
|
|
||||||
const urlCache = new Map<Blob, string>()
|
|
||||||
|
|
||||||
/** Gets idb aset url from asset blob */
|
|
||||||
const getUrlForAsset = (asset: Blob) => {
|
|
||||||
if (urlCache.has(asset)) {
|
|
||||||
return urlCache.get(asset)!
|
|
||||||
} else {
|
|
||||||
const url = URL.createObjectURL(asset)
|
|
||||||
urlCache.set(asset, url)
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gets idb asset url from id */
|
|
||||||
const getUrlForId = (assetId: string) => {
|
|
||||||
const asset = assetsMap.get(assetId)
|
|
||||||
if (!asset) {
|
|
||||||
throw new Error(`Asset with id ${assetId} not found`)
|
|
||||||
}
|
|
||||||
return getUrlForAsset(asset)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getAssetUrl: (assetId: string) => {
|
|
||||||
return assetsMap.has(assetId)
|
|
||||||
? getUrlForId(assetId)
|
|
||||||
: `${baseUrl}/${assetId}`
|
|
||||||
},
|
|
||||||
createAsset: async (asset: File) => {
|
|
||||||
const existingIDs = getAllPossibleAssetIDs(project)
|
|
||||||
|
|
||||||
let sameSame = false
|
|
||||||
|
|
||||||
if (existingIDs.includes(asset.name)) {
|
|
||||||
let existingAsset: Blob | undefined
|
|
||||||
try {
|
|
||||||
existingAsset =
|
|
||||||
assetsMap.get(asset.name) ??
|
|
||||||
(await fetch(`${baseUrl}/${asset.name}`).then((r) =>
|
|
||||||
r.ok ? r.blob() : undefined,
|
|
||||||
))
|
|
||||||
} catch (e) {
|
|
||||||
notify.error(
|
|
||||||
'Failed to access assets',
|
|
||||||
`Failed to access assets at ${
|
|
||||||
project.config.assets?.baseUrl ?? '/'
|
|
||||||
}. This is likely due to a CORS issue.`,
|
|
||||||
)
|
|
||||||
|
|
||||||
return Promise.resolve(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingAsset) {
|
|
||||||
// @ts-ignore
|
|
||||||
sameSame = await blobCompare.isEqual(asset, existingAsset)
|
|
||||||
|
|
||||||
// if same same, we do nothing
|
|
||||||
if (sameSame) {
|
|
||||||
return asset.name
|
|
||||||
// if different, we ask the user to pls rename
|
|
||||||
} else {
|
|
||||||
/** Initiates rename using a dialog. Returns a boolean indicating if the rename was succesful. */
|
|
||||||
const renameAsset = (text: string): boolean => {
|
|
||||||
const newAssetName = prompt(text, asset.name)
|
|
||||||
|
|
||||||
if (newAssetName === null) {
|
|
||||||
// asset creation canceled
|
|
||||||
return false
|
|
||||||
} else if (newAssetName === '') {
|
|
||||||
return renameAsset(
|
|
||||||
'Asset name cannot be empty. Please choose a different file name for this asset.',
|
|
||||||
)
|
|
||||||
} else if (existingIDs.includes(newAssetName)) {
|
|
||||||
console.log(existingIDs)
|
|
||||||
return renameAsset(
|
|
||||||
'An asset with this name already exists. Please choose a different file name for this asset.',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// rename asset
|
|
||||||
asset = new File([asset], newAssetName, {type: asset.type})
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// rename asset returns false if the user cancels the rename
|
|
||||||
const success = renameAsset(
|
|
||||||
'An asset with this name already exists. Please choose a different file name for this asset.',
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assetsMap.set(asset.name, asset)
|
|
||||||
await idb.set(asset.name, asset)
|
|
||||||
return asset.name
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -24,7 +24,6 @@ import type {
|
||||||
ITheatreLoggingConfig,
|
ITheatreLoggingConfig,
|
||||||
} from '@theatre/shared/logger'
|
} from '@theatre/shared/logger'
|
||||||
import {_coreLogger} from '@theatre/core/_coreLogger'
|
import {_coreLogger} from '@theatre/core/_coreLogger'
|
||||||
import {createDefaultAssetStorageConfig} from './DefaultAssetStorage'
|
|
||||||
|
|
||||||
type ICoreAssetStorage = {
|
type ICoreAssetStorage = {
|
||||||
/** Returns a URL for the provided asset ID */
|
/** Returns a URL for the provided asset ID */
|
||||||
|
@ -41,8 +40,6 @@ export type IAssetStorageConfig = {
|
||||||
* An object containing the core asset storage methods.
|
* An object containing the core asset storage methods.
|
||||||
*/
|
*/
|
||||||
coreAssetStorage: ICoreAssetStorage
|
coreAssetStorage: ICoreAssetStorage
|
||||||
/** A function that returns a promise to an object containing asset storage methods to be used by studio. */
|
|
||||||
createStudioAssetStorage: () => Promise<IStudioAssetStorage>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type IAssetConf = {
|
type IAssetConf = {
|
||||||
|
@ -77,7 +74,7 @@ export default class Project {
|
||||||
readonly address: ProjectAddress
|
readonly address: ProjectAddress
|
||||||
|
|
||||||
private readonly _studioReadyDeferred: Deferred<undefined>
|
private readonly _studioReadyDeferred: Deferred<undefined>
|
||||||
private readonly _defaultAssetStorageReadyDeferred: Deferred<undefined>
|
private readonly _assetStorageReadyDeferred: Deferred<undefined>
|
||||||
private readonly _readyPromise: Promise<void>
|
private readonly _readyPromise: Promise<void>
|
||||||
|
|
||||||
private _sheetTemplates = new Atom<{
|
private _sheetTemplates = new Atom<{
|
||||||
|
@ -85,7 +82,6 @@ export default class Project {
|
||||||
}>({})
|
}>({})
|
||||||
sheetTemplatesP = this._sheetTemplates.pointer
|
sheetTemplatesP = this._sheetTemplates.pointer
|
||||||
private _studio: Studio | undefined
|
private _studio: Studio | undefined
|
||||||
private _defaultAssetStorageConfig: IAssetStorageConfig
|
|
||||||
assetStorage: IStudioAssetStorage
|
assetStorage: IStudioAssetStorage
|
||||||
|
|
||||||
type: 'Theatre_Project' = 'Theatre_Project'
|
type: 'Theatre_Project' = 'Theatre_Project'
|
||||||
|
@ -117,13 +113,9 @@ export default class Project {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
this._defaultAssetStorageConfig = createDefaultAssetStorageConfig({
|
this._assetStorageReadyDeferred = defer()
|
||||||
project: this,
|
|
||||||
baseUrl: config.assets?.baseUrl,
|
|
||||||
})
|
|
||||||
this._defaultAssetStorageReadyDeferred = defer()
|
|
||||||
this.assetStorage = {
|
this.assetStorage = {
|
||||||
...this._defaultAssetStorageConfig.coreAssetStorage,
|
getAssetUrl: (assetId: string) => `${config.assets?.baseUrl}/${assetId}`,
|
||||||
|
|
||||||
// Until the asset storage is ready, we'll throw an error when the user tries to use it
|
// Until the asset storage is ready, we'll throw an error when the user tries to use it
|
||||||
createAsset: () => {
|
createAsset: () => {
|
||||||
|
@ -149,7 +141,7 @@ export default class Project {
|
||||||
|
|
||||||
this._readyPromise = Promise.all([
|
this._readyPromise = Promise.all([
|
||||||
this._studioReadyDeferred.promise,
|
this._studioReadyDeferred.promise,
|
||||||
this._defaultAssetStorageReadyDeferred.promise,
|
this._assetStorageReadyDeferred.promise,
|
||||||
// hide the array from the user, i.e. make it Promise<void> instead of Promise<[undefined, undefined]>
|
// hide the array from the user, i.e. make it Promise<void> instead of Promise<[undefined, undefined]>
|
||||||
]).then(() => {})
|
]).then(() => {})
|
||||||
|
|
||||||
|
@ -159,7 +151,7 @@ export default class Project {
|
||||||
// let's give it one tick to attach itself
|
// let's give it one tick to attach itself
|
||||||
if (!this._studio) {
|
if (!this._studio) {
|
||||||
this._studioReadyDeferred.resolve(undefined)
|
this._studioReadyDeferred.resolve(undefined)
|
||||||
this._defaultAssetStorageReadyDeferred.resolve(undefined)
|
this._assetStorageReadyDeferred.resolve(undefined)
|
||||||
this._logger._trace('ready deferred resolved with no state')
|
this._logger._trace('ready deferred resolved with no state')
|
||||||
}
|
}
|
||||||
}, 0)
|
}, 0)
|
||||||
|
@ -218,11 +210,11 @@ export default class Project {
|
||||||
)
|
)
|
||||||
|
|
||||||
// asset storage has to be initialized after the pointers are set
|
// asset storage has to be initialized after the pointers are set
|
||||||
this._defaultAssetStorageConfig
|
studio
|
||||||
.createStudioAssetStorage()
|
.createAssetStorage(this, this.config.assets?.baseUrl)
|
||||||
.then((assetStorage) => {
|
.then((assetStorage) => {
|
||||||
this.assetStorage = assetStorage
|
this.assetStorage = assetStorage
|
||||||
this._defaultAssetStorageReadyDeferred.resolve(undefined)
|
this._assetStorageReadyDeferred.resolve(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
this._studioReadyDeferred.resolve(undefined)
|
this._studioReadyDeferred.resolve(undefined)
|
||||||
|
@ -240,7 +232,7 @@ export default class Project {
|
||||||
isReady() {
|
isReady() {
|
||||||
return (
|
return (
|
||||||
this._studioReadyDeferred.status === 'resolved' &&
|
this._studioReadyDeferred.status === 'resolved' &&
|
||||||
this._defaultAssetStorageReadyDeferred.status === 'resolved'
|
this._assetStorageReadyDeferred.status === 'resolved'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,12 +23,19 @@ import {defer} from '@theatre/shared/utils/defer'
|
||||||
import type {ProjectId} from '@theatre/shared/utils/ids'
|
import type {ProjectId} from '@theatre/shared/utils/ids'
|
||||||
import checkForUpdates from './checkForUpdates'
|
import checkForUpdates from './checkForUpdates'
|
||||||
import shallowEqual from 'shallowequal'
|
import shallowEqual from 'shallowequal'
|
||||||
|
import {createStore} from './IDBStorage'
|
||||||
|
import {getAllPossibleAssetIDs} from '@theatre/shared/utils/assets'
|
||||||
|
import {notify} from './notify'
|
||||||
|
|
||||||
export type CoreExports = typeof _coreExports
|
export type CoreExports = typeof _coreExports
|
||||||
|
|
||||||
const UIConstructorModule =
|
const UIConstructorModule =
|
||||||
typeof window !== 'undefined' ? require('./UI') : null
|
typeof window !== 'undefined' ? require('./UI') : null
|
||||||
|
|
||||||
|
// this package has a reference to `window` that breaks SSR, so we require it conditionally
|
||||||
|
const blobCompare =
|
||||||
|
typeof window !== 'undefined' ? require('blob-compare') : null
|
||||||
|
|
||||||
const STUDIO_NOT_INITIALIZED_MESSAGE = `You seem to have imported '@theatre/studio' but haven't initialized it. You can initialize the studio by:
|
const STUDIO_NOT_INITIALIZED_MESSAGE = `You seem to have imported '@theatre/studio' but haven't initialized it. You can initialize the studio by:
|
||||||
\`\`\`
|
\`\`\`
|
||||||
import studio from '@theatre/studio'
|
import studio from '@theatre/studio'
|
||||||
|
@ -291,4 +298,176 @@ export class Studio {
|
||||||
createContentOfSaveFile(projectId: string): OnDiskState {
|
createContentOfSaveFile(projectId: string): OnDiskState {
|
||||||
return this._store.createContentOfSaveFile(projectId as ProjectId)
|
return this._store.createContentOfSaveFile(projectId as ProjectId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A function that returns a promise to an object containing asset storage methods for a project to be used by studio. */
|
||||||
|
async createAssetStorage(project: Project, baseUrl?: string) {
|
||||||
|
// in SSR we bail out and return a dummy asset manager
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return {
|
||||||
|
getAssetUrl: () => '',
|
||||||
|
createAsset: () => Promise.resolve(null),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for support.
|
||||||
|
if (!('indexedDB' in window)) {
|
||||||
|
console.log("This browser doesn't support IndexedDB.")
|
||||||
|
|
||||||
|
return {
|
||||||
|
getAssetUrl: (assetId: string) => {
|
||||||
|
throw new Error(
|
||||||
|
`IndexedDB is required by the default asset manager, but it's not supported by this browser. To use assets, please provide your own asset manager to the project config.`,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
createAsset: (asset: Blob) => {
|
||||||
|
throw new Error(
|
||||||
|
`IndexedDB is required by the default asset manager, but it's not supported by this browser. To use assets, please provide your own asset manager to the project config.`,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const idb = createStore(`${project.address.projectId}-assets`)
|
||||||
|
|
||||||
|
// get all possible asset ids referenced by either static props or keyframes
|
||||||
|
const possibleAssetIDs = getAllPossibleAssetIDs(project)
|
||||||
|
|
||||||
|
// Clean up assets not referenced by the project. We can only do this at the start because otherwise
|
||||||
|
// we'd break undo/redo.
|
||||||
|
const idbKeys = await idb.keys<string>()
|
||||||
|
await Promise.all(
|
||||||
|
idbKeys.map(async (key) => {
|
||||||
|
if (!possibleAssetIDs.includes(key)) {
|
||||||
|
await idb.del(key)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Clean up idb entries exported to disk
|
||||||
|
await Promise.all(
|
||||||
|
idbKeys.map(async (key) => {
|
||||||
|
const assetUrl = `${baseUrl}/${key}`
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(assetUrl, {method: 'HEAD'})
|
||||||
|
if (response.ok) {
|
||||||
|
await idb.del(key)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
notify.error(
|
||||||
|
'Failed to access assets',
|
||||||
|
`Failed to access assets at ${
|
||||||
|
project.config.assets?.baseUrl ?? '/'
|
||||||
|
}. This is likely due to a CORS issue.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// A map for caching the assets outside of the db. We also need this to be able to retrieve idb asset urls synchronously.
|
||||||
|
const assetsMap = new Map(await idb.entries<string, Blob>())
|
||||||
|
|
||||||
|
// A map for caching the object urls created from idb assets.
|
||||||
|
const urlCache = new Map<Blob, string>()
|
||||||
|
|
||||||
|
/** Gets idb aset url from asset blob */
|
||||||
|
const getUrlForAsset = (asset: Blob) => {
|
||||||
|
if (urlCache.has(asset)) {
|
||||||
|
return urlCache.get(asset)!
|
||||||
|
} else {
|
||||||
|
const url = URL.createObjectURL(asset)
|
||||||
|
urlCache.set(asset, url)
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets idb asset url from id */
|
||||||
|
const getUrlForId = (assetId: string) => {
|
||||||
|
const asset = assetsMap.get(assetId)
|
||||||
|
if (!asset) {
|
||||||
|
throw new Error(`Asset with id ${assetId} not found`)
|
||||||
|
}
|
||||||
|
return getUrlForAsset(asset)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getAssetUrl: (assetId: string) => {
|
||||||
|
return assetsMap.has(assetId)
|
||||||
|
? getUrlForId(assetId)
|
||||||
|
: `${baseUrl}/${assetId}`
|
||||||
|
},
|
||||||
|
createAsset: async (asset: File) => {
|
||||||
|
const existingIDs = getAllPossibleAssetIDs(project)
|
||||||
|
|
||||||
|
let sameSame = false
|
||||||
|
|
||||||
|
if (existingIDs.includes(asset.name)) {
|
||||||
|
let existingAsset: Blob | undefined
|
||||||
|
try {
|
||||||
|
existingAsset =
|
||||||
|
assetsMap.get(asset.name) ??
|
||||||
|
(await fetch(`${baseUrl}/${asset.name}`).then((r) =>
|
||||||
|
r.ok ? r.blob() : undefined,
|
||||||
|
))
|
||||||
|
} catch (e) {
|
||||||
|
notify.error(
|
||||||
|
'Failed to access assets',
|
||||||
|
`Failed to access assets at ${
|
||||||
|
project.config.assets?.baseUrl ?? '/'
|
||||||
|
}. This is likely due to a CORS issue.`,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Promise.resolve(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingAsset) {
|
||||||
|
// @ts-ignore
|
||||||
|
sameSame = await blobCompare!.isEqual(asset, existingAsset)
|
||||||
|
|
||||||
|
// if same same, we do nothing
|
||||||
|
if (sameSame) {
|
||||||
|
return asset.name
|
||||||
|
// if different, we ask the user to pls rename
|
||||||
|
} else {
|
||||||
|
/** Initiates rename using a dialog. Returns a boolean indicating if the rename was succesful. */
|
||||||
|
const renameAsset = (text: string): boolean => {
|
||||||
|
const newAssetName = prompt(text, asset.name)
|
||||||
|
|
||||||
|
if (newAssetName === null) {
|
||||||
|
// asset creation canceled
|
||||||
|
return false
|
||||||
|
} else if (newAssetName === '') {
|
||||||
|
return renameAsset(
|
||||||
|
'Asset name cannot be empty. Please choose a different file name for this asset.',
|
||||||
|
)
|
||||||
|
} else if (existingIDs.includes(newAssetName)) {
|
||||||
|
console.log(existingIDs)
|
||||||
|
return renameAsset(
|
||||||
|
'An asset with this name already exists. Please choose a different file name for this asset.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rename asset
|
||||||
|
asset = new File([asset], newAssetName, {type: asset.type})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// rename asset returns false if the user cancels the rename
|
||||||
|
const success = renameAsset(
|
||||||
|
'An asset with this name already exists. Please choose a different file name for this asset.',
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assetsMap.set(asset.name, asset)
|
||||||
|
await idb.set(asset.name, asset)
|
||||||
|
return asset.name
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue