okTurtles / group-income

A decentralized and private (end-to-end encrypted) financial safety net for you and your friends.
https://groupincome.org
GNU Affero General Public License v3.0
331 stars 44 forks source link

Safari database.js: DataCloneError: Failed to store record in an IDBObjectStore: BlobURLs are not yet supported #2191

Closed taoeffect closed 4 months ago

taoeffect commented 4 months ago

Problem

Screenshot 2024-07-11 at 6 45 04 PM

Screenshot 2024-07-11 at 6 45 11 PM

Solution

Here is Claude Sonnet's proposed fix:

To fix the "DataCloneError" issue in Safari related to BlobURLs, we need to modify the setItem function to handle Blob objects differently. Instead of directly storing the Blob, we can convert it to an ArrayBuffer before saving it to IndexedDB. Here's how we can modify the setItem function:

Replace the existing setItem function with the following code:

setItem (key: string, value: any) {
  return db.then(async db => {
    const transaction = db.transaction([storeName], 'readwrite')
    const objectStore = transaction.objectStore(storeName)

    // Check if the value is a Blob
    if (value instanceof Blob) {
      // Convert Blob to ArrayBuffer
      value = await value.arrayBuffer()
    }

    const request = objectStore.put(value, key)
    return new Promise((resolve, reject) => {
      request.onsuccess = () => {
        resolve()
      }
      request.onerror = (e) => {
        reject(e.target.error)
      }
    })
  })
}

This modification does the following:

  1. It checks if the value is an instance of Blob.
  2. If it is a Blob, it converts it to an ArrayBuffer using the arrayBuffer() method.
  3. The ArrayBuffer is then stored in IndexedDB instead of the Blob.

To complement this change, you should also modify the getItem function to convert the ArrayBuffer back to a Blob when retrieving the data:

Replace the existing getItem function with:

getItem (key: string) {
  return db.then(db => {
    const transaction = db.transaction([storeName], 'readonly')
    const objectStore = transaction.objectStore(storeName)
    const request = objectStore.get(key)
    return new Promise((resolve, reject) => {
      request.onsuccess = (event) => {
        let result = event.target.result
        // Check if the result is an ArrayBuffer (former Blob)
        if (result instanceof ArrayBuffer) {
          result = new Blob([result])
        }
        resolve(result)
      }
      request.onerror = (e) => {
        reject(e)
      }
    })
  })
}

These changes should resolve the DataCloneError in Safari while maintaining compatibility with other browsers. The Blob data will be stored as ArrayBuffers in IndexedDB and converted back to Blobs when retrieved.

My proposal would be to just use JSON.stringify / JSON.parse. Here's what 3.5 thought of that:

Using JSON.stringify and JSON.parse is indeed a simpler approach that can work for many cases. However, it's important to note that this method has limitations when dealing with Blobs or other complex data types. Let's modify the setItem and getItem functions to use this approach:

// ... existing code ...

setItem (key: string, value: any) {
  return db.then(db => {
    const transaction = db.transaction([storeName], 'readwrite')
    const objectStore = transaction.objectStore(storeName)
    // Stringify the value before storing
    const stringifiedValue = JSON.stringify(value)
    const request = objectStore.put(stringifiedValue, key)
    return new Promise((resolve, reject) => {
      request.onsuccess = () => {
        resolve()
      }
      request.onerror = (e) => {
        reject(e.target.error)
      }
    })
  })
},

getItem (key: string) {
  return db.then(db => {
    const transaction = db.transaction([storeName], 'readonly')
    const objectStore = transaction.objectStore(storeName)
    const request = objectStore.get(key)
    return new Promise((resolve, reject) => {
      request.onsuccess = (event) => {
        // Parse the stringified value after retrieval
        const parsedValue = JSON.parse(event.target.result)
        resolve(parsedValue)
      }
      request.onerror = (e) => {
        reject(e)
      }
    })
  })
},

// ... rest of the code ...

This approach will work for most JavaScript objects and primitive types. However, there are some important considerations:

  1. Blobs and other binary data: JSON.stringify cannot directly handle Blobs, ArrayBuffers, or other binary data. If you need to store these, you'll need to convert them to a format that can be stringified (like Base64) before storing, and convert them back after retrieval.

  2. Circular references: Objects with circular references cannot be stringified and will throw an error.

  3. Functions: JSON doesn't support functions, so any function properties will be lost during stringification.

  4. Special types: Date objects will be converted to strings and won't be revived as Date objects when parsed.

If your data consists mainly of simple objects, arrays, and primitive types, this approach will work well and solve the Safari issue. However, if you're dealing with Blobs or other complex data types, you might need to implement additional logic to handle these cases specifically.

For example, if you know you're dealing with Blobs, you could do something like this:

setItem (key: string, value: any) {
  return db.then(async db => {
    const transaction = db.transaction([storeName], 'readwrite')
    const objectStore = transaction.objectStore(storeName)
    let storedValue = value

    if (value instanceof Blob) {
      storedValue = {
        _isBlob: true,
        type: value.type,
        data: await new Response(value).text()
      }
    }

    const stringifiedValue = JSON.stringify(storedValue)
    const request = objectStore.put(stringifiedValue, key)
    // ... rest of the function
  })
},

getItem (key: string) {
  return db.then(db => {
    // ... existing code ...
    request.onsuccess = (event) => {
      const parsedValue = JSON.parse(event.target.result)
      if (parsedValue && parsedValue._isBlob) {
        resolve(new Blob([parsedValue.data], { type: parsedValue.type }))
      } else {
        resolve(parsedValue)
      }
    }
    // ... rest of the function
  })
},

This approach gives you the simplicity of JSON for most cases while still allowing you to handle Blobs when necessary. Remember to adjust this based on the specific types of data you're dealing with in your application.

And here's @corrideat's take:

neither are good solutions, because Blobs cannot be JSON-ified and storing Blobs as ArrayBuffer like that will make it impossible to tell between Blob and ArrayBuffer This is what the serdes library does, except that I think it's best to keep that synchronous and there's no way to convert a Blob to an ArrayBuffer synchronously So, I'd go with converting Blob to ArrayBuffer but not in setItem, rather in the files selector, which is where Blobs are being used